Skip to content
Open
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
1 change: 1 addition & 0 deletions internal/controller/appserver/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,7 @@ var _ = Describe("App server assets", func() {

Expect(deployment.Spec.Template.Spec.InitContainers).To(ConsistOf(
utils.GeneratePostgresWaitInitContainer(testReconcilerInstance.GetPostgresImage()),
utils.GenerateRHOKPWaitInitContainer(testReconcilerInstance.GetAppServerImage(), testReconcilerInstance.GetNamespace()),
corev1.Container{
Name: "rag-0",
Image: "rag-ocp-product-docs:4.19",
Expand Down
3 changes: 3 additions & 0 deletions internal/controller/appserver/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ func GenerateOLSDeployment(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig) (

initContainers := []corev1.Container{}
initContainers = append(initContainers, utils.GeneratePostgresWaitInitContainer(r.GetPostgresImage()))
if !cr.Spec.OLSConfig.ByokRAGOnly {
initContainers = append(initContainers, utils.GenerateRHOKPWaitInitContainer(r.GetAppServerImage(), r.GetNamespace()))
}
if len(cr.Spec.OLSConfig.RAG) > 0 {
ragInitContainers := GenerateRAGInitContainers(cr)
initContainers = append(initContainers, ragInitContainers...)
Expand Down
28 changes: 28 additions & 0 deletions internal/controller/appserver/deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,34 @@ var _ = Describe("App server deployment generation", func() {
}
})

It("should include wait-for-rhokp init container when ByokRAGOnly is false", func() {
cr.Spec.OLSConfig.ByokRAGOnly = false
dep, err := GenerateOLSDeployment(testReconcilerInstance, cr)
Expect(err).NotTo(HaveOccurred())

var found bool
for _, ic := range dep.Spec.Template.Spec.InitContainers {
if ic.Name == utils.RHOKPWaitInitContainerName {
found = true
Expect(ic.Image).To(Equal(testReconcilerInstance.GetAppServerImage()))
Expect(ic.VolumeMounts).ToNot(BeEmpty())
break
}
}
Expect(found).To(BeTrue(), "wait-for-rhokp init container should be present when ByokRAGOnly is false")
})

It("should not include wait-for-rhokp init container when ByokRAGOnly is true", func() {
cr.Spec.OLSConfig.ByokRAGOnly = true
dep, err := GenerateOLSDeployment(testReconcilerInstance, cr)
Expect(err).NotTo(HaveOccurred())

for _, ic := range dep.Spec.Template.Spec.InitContainers {
Expect(ic.Name).NotTo(Equal(utils.RHOKPWaitInitContainerName),
"wait-for-rhokp init container should not be present when ByokRAGOnly is true")
}
})

It("should add OLS_ROSA_PRODUCT when configured on the reconciler", func() {
cr.Spec.OLSConfig.IntrospectionEnabled = utils.BoolPtr(false)
cr.Spec.OLSConfig.UserDataCollection = olsv1alpha1.UserDataCollectionSpec{
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/utils/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ const (
PostgresDeploymentName = "lightspeed-postgres-server"
// PostgresWaitInitContainerName is the name of the init container that waits for Postgres to accept connections
PostgresWaitInitContainerName = "wait-for-postgres"
// RHOKPWaitInitContainerName is the name of the init container that waits for RHOKP/Solr to be reachable
RHOKPWaitInitContainerName = "wait-for-rhokp"
// PostgresSecretKeyName is the name of the key holding Postgres server secret
PostgresSecretKeyName = "password"
// PostgresDefaultUser is the default user name for postgres
Expand Down
92 changes: 92 additions & 0 deletions internal/controller/utils/rhokp_wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package utils

import (
"fmt"
"path"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

// RHOKPWaitMaxSeconds is the maximum time the wait-for-rhokp init container
// will poll before giving up. Matches the RHOKP startup-probe budget
// (20 s initial-delay + 34 failures × 10 s period ≈ 360 s).
const RHOKPWaitMaxSeconds = 360

// GenerateRHOKPWaitInitContainer returns an init container that blocks until the
// standalone RHOKP/Solr instance is reachable on its HTTPS endpoint.
//
// Without this gate the app-server's SolrHybridSearch initializer may exhaust
// its 120 s retry budget before RHOKP is ready, permanently caching a nil
// reference for product-docs RAG (OLS-3799).
//
// The container uses the app-server image (which ships curl) and polls the
// Solr admin/ping endpoint. The RHOKP CA certificate volume must already
// be defined on the pod (AppRHOKPCACertVolumeName).
func GenerateRHOKPWaitInitContainer(image, namespace string) corev1.Container {
rhokpURL := RHOKPServiceURL(namespace) + RHOOKPReadinessHTTPPath

caPath := path.Join(OLSAppCertsMountRoot, AppRHOKPCACertDir, AppRHOKPCACertFile)

script := fmt.Sprintf(`
if ! command -v curl >/dev/null 2>&1; then
echo "wait-for-rhokp: curl not found in image" >&2
exit 1
fi

sleep_sec=1
max_sleep=30
start=$(date +%%s)
max_elapsed=%d
url="%s"
ca="%s"

backoff() {
sleep "$sleep_sec"
sleep_sec=$((sleep_sec * 2))
[ "$sleep_sec" -gt "$max_sleep" ] && sleep_sec="$max_sleep"
}

while true; do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing command -v curl guard

postgres_wait.go validates its tool exists before entering the poll loop:

if ! command -v pg_isready >/dev/null 2>&1; then
  echo "wait-for-postgres: pg_isready not found in image" >&2
  exit 1
fi

This script has no equivalent check for curl. If curl is ever removed from the app-server image, the init container will loop for 360 seconds (the curl invocation suppresses all output via >/dev/null 2>&1) before timing out — a confusing silent failure instead of a fast one.

Suggestion: add command -v curl check before the while true loop, matching the postgres pattern.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added command -v curl guard before the poll loop, matching the postgres pattern. Fails fast with a clear message instead of silently looping for 360s.

now=$(date +%%s)
elapsed=$((now - start))
if [ "$elapsed" -ge "$max_elapsed" ]; then
echo "wait-for-rhokp: timed out after ${max_elapsed}s" >&2
exit 1
fi

if curl -sf --cacert "$ca" --max-time 5 "$url" >/dev/null 2>&1; then
echo "wait-for-rhokp: RHOKP/Solr is reachable"
exit 0
fi

echo "wait-for-rhokp: not ready yet (elapsed=${elapsed}s)" >&2
backoff
done
`, RHOKPWaitMaxSeconds, rhokpURL, caPath)

return corev1.Container{
Name: RHOKPWaitInitContainerName,
Image: image,
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{"/bin/sh", "-c", script},
VolumeMounts: []corev1.VolumeMount{
{
Name: AppRHOKPCACertVolumeName,
MountPath: path.Join(OLSAppCertsMountRoot, AppRHOKPCACertDir),
ReadOnly: true,
},
},
SecurityContext: RestrictedContainerSecurityContext(),
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("10m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("64Mi"),
},
},
}
}
79 changes: 79 additions & 0 deletions internal/controller/utils/rhokp_wait_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package utils

import (
"fmt"
"path"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

var _ = Describe("RHOKP wait", func() {

Describe("GenerateRHOKPWaitInitContainer", func() {
const testNS = "openshift-lightspeed"

It("uses the provided image and generates correct container spec", func() {
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, testNS)
Expect(c.Name).To(Equal(RHOKPWaitInitContainerName))
Expect(c.Image).To(Equal(OLSAppServerImageDefault))
Expect(c.SecurityContext).To(Equal(RestrictedContainerSecurityContext()))
Expect(c.Resources.Requests).To(HaveKey(corev1.ResourceCPU))
Expect(c.Resources.Requests).To(HaveKey(corev1.ResourceMemory))
Expect(c.Resources.Requests[corev1.ResourceCPU]).To(Equal(resource.MustParse("10m")))
Expect(c.Resources.Requests[corev1.ResourceMemory]).To(Equal(resource.MustParse("32Mi")))
Expect(c.Resources.Limits[corev1.ResourceCPU]).To(Equal(resource.MustParse("100m")))
Expect(c.Resources.Limits[corev1.ResourceMemory]).To(Equal(resource.MustParse("64Mi")))
Expect(c.Command).To(HaveLen(3))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

It("mounts the RHOKP CA certificate volume", func() {
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, testNS)
Expect(c.VolumeMounts).To(HaveLen(1))
Expect(c.VolumeMounts[0].Name).To(Equal(AppRHOKPCACertVolumeName))
Expect(c.VolumeMounts[0].MountPath).To(Equal(path.Join(OLSAppCertsMountRoot, AppRHOKPCACertDir)))
Expect(c.VolumeMounts[0].ReadOnly).To(BeTrue())
})

It("contains curl-based readiness check using admin/ping endpoint", func() {
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, testNS)
script := c.Command[2]

By("using curl for connectivity check")
Expect(script).To(ContainSubstring("curl"))
Expect(script).To(ContainSubstring("--cacert"))

By("targeting the RHOKP admin/ping endpoint")
expectedURL := RHOKPServiceURL(testNS) + RHOOKPReadinessHTTPPath
Expect(script).To(ContainSubstring(expectedURL))
})

It("guards against missing curl binary", func() {
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, testNS)
script := c.Command[2]
Expect(script).To(ContainSubstring("command -v curl"))
Expect(script).To(ContainSubstring("curl not found in image"))
})

It("has timeout and backoff logic", func() {
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, testNS)
script := c.Command[2]

Expect(script).To(ContainSubstring(fmt.Sprintf("max_elapsed=%d", RHOKPWaitMaxSeconds)))
Expect(script).To(ContainSubstring("backoff"))
Expect(script).To(ContainSubstring("timed out"))
Expect(script).To(ContainSubstring("not ready yet"))
Expect(script).To(ContainSubstring("RHOKP/Solr is reachable"))
})

It("uses the correct namespace in the service URL", func() {
customNS := "my-custom-namespace"
c := GenerateRHOKPWaitInitContainer(OLSAppServerImageDefault, customNS)
script := c.Command[2]
Expect(script).To(ContainSubstring(fmt.Sprintf("%s.%s.svc", RHOKPServiceName, customNS)))
})
})
})