Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
56 changes: 36 additions & 20 deletions controllers/evalhub/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package evalhub

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -49,7 +50,8 @@ func TestBuildDeploymentSpec(t *testing.T) {
Namespace: testNamespace,
},
Data: map[string]string{
configMapEvalHubImageKey: "quay.io/test/eval-hub:v1.2.3",
configMapEvalHubImageKey: "quay.io/test/eval-hub:v1.2.3",
configMapKubeRBACProxyImageKey: "quay.io/test/kube-rbac-proxy:v1",
},
}

Expand Down Expand Up @@ -84,7 +86,7 @@ func TestBuildDeploymentSpec(t *testing.T) {

// Check pod template
podSpec := deploymentSpec.Template.Spec
require.Len(t, podSpec.Containers, 1)
require.Len(t, podSpec.Containers, 2)

// Find the evalhub container
var container *corev1.Container
Expand All @@ -104,8 +106,8 @@ func TestBuildDeploymentSpec(t *testing.T) {
// Check ports
require.Len(t, container.Ports, 1)
port := container.Ports[0]
assert.Equal(t, "https", port.Name)
assert.Equal(t, int32(8443), port.ContainerPort)
assert.Equal(t, "evalhub", port.Name)
assert.Equal(t, int32(evalHubAppPort), port.ContainerPort)
assert.Equal(t, corev1.ProtocolTCP, port.Protocol)

// Check environment variables
Expand All @@ -115,8 +117,8 @@ func TestBuildDeploymentSpec(t *testing.T) {
}

// Check default environment variables
assert.Equal(t, "0.0.0.0", envVarMap["API_HOST"])
assert.Equal(t, "8443", envVarMap["PORT"])
assert.Equal(t, "127.0.0.1", envVarMap["API_HOST"])
assert.Equal(t, fmt.Sprintf("%d", evalHubAppPort), envVarMap["PORT"])
assert.Equal(t, "/etc/tls/private/tls.crt", envVarMap["TLS_CERT_FILE"])
assert.Equal(t, "/etc/tls/private/tls.key", envVarMap["TLS_KEY_FILE"])
assert.Equal(t, "INFO", envVarMap["LOG_LEVEL"])
Expand Down Expand Up @@ -146,20 +148,31 @@ func TestBuildDeploymentSpec(t *testing.T) {
assert.True(t, *container.SecurityContext.RunAsNonRoot)
assert.Contains(t, container.SecurityContext.Capabilities.Drop, corev1.Capability("ALL"))

// Check health probes (HTTPGet with HTTPS)
require.NotNil(t, container.LivenessProbe)
require.NotNil(t, container.LivenessProbe.HTTPGet)
assert.Equal(t, "/api/v1/health", container.LivenessProbe.HTTPGet.Path)
assert.Equal(t, corev1.URISchemeHTTPS, container.LivenessProbe.HTTPGet.Scheme)
assert.Equal(t, int32(30), container.LivenessProbe.InitialDelaySeconds)
assert.Equal(t, int32(10), container.LivenessProbe.PeriodSeconds)

require.NotNil(t, container.ReadinessProbe)
require.NotNil(t, container.ReadinessProbe.HTTPGet)
assert.Equal(t, "/api/v1/health", container.ReadinessProbe.HTTPGet.Path)
assert.Equal(t, corev1.URISchemeHTTPS, container.ReadinessProbe.HTTPGet.Scheme)
assert.Equal(t, int32(10), container.ReadinessProbe.InitialDelaySeconds)
assert.Equal(t, int32(5), container.ReadinessProbe.PeriodSeconds)
// Eval-hub has no kubelet probes; health is checked via kube-rbac-proxy on servicePort.
assert.Nil(t, container.LivenessProbe)
assert.Nil(t, container.ReadinessProbe)

krp := findContainer(t, podSpec.Containers, kubeRBACProxyContainerName)
assert.Equal(t, "quay.io/test/kube-rbac-proxy:v1", krp.Image)
assert.Contains(t, krp.Args, "--upstream=http://127.0.0.1:"+fmt.Sprintf("%d/", evalHubAppPort))
assert.Contains(t, krp.Args, "--ignore-paths="+evalHubHealthPath)
require.NotNil(t, krp.ReadinessProbe)
require.NotNil(t, krp.ReadinessProbe.HTTPGet)
assert.Equal(t, evalHubHealthPath, krp.ReadinessProbe.HTTPGet.Path)
assert.Equal(t, "", krp.ReadinessProbe.HTTPGet.Host)
assert.Equal(t, intstr.FromInt(servicePort), krp.ReadinessProbe.HTTPGet.Port)
assert.Equal(t, corev1.URISchemeHTTPS, krp.ReadinessProbe.HTTPGet.Scheme)
assert.Equal(t, int32(10), krp.ReadinessProbe.InitialDelaySeconds)
assert.Equal(t, int32(5), krp.ReadinessProbe.PeriodSeconds)

require.NotNil(t, krp.LivenessProbe)
require.NotNil(t, krp.LivenessProbe.HTTPGet)
assert.Equal(t, evalHubHealthPath, krp.LivenessProbe.HTTPGet.Path)
assert.Equal(t, "", krp.LivenessProbe.HTTPGet.Host)
assert.Equal(t, intstr.FromInt(servicePort), krp.LivenessProbe.HTTPGet.Port)
assert.Equal(t, corev1.URISchemeHTTPS, krp.LivenessProbe.HTTPGet.Scheme)
assert.Equal(t, int32(30), krp.LivenessProbe.InitialDelaySeconds)
assert.Equal(t, int32(10), krp.LivenessProbe.PeriodSeconds)

// Check pod security context
require.NotNil(t, podSpec.SecurityContext)
Expand Down Expand Up @@ -197,6 +210,9 @@ func TestBuildDeploymentSpec(t *testing.T) {
}
require.NotNil(t, container)
assert.Equal(t, defaultEvalHubImage, container.Image)

krp := findContainer(t, deploymentSpec.Template.Spec.Containers, kubeRBACProxyContainerName)
assert.Equal(t, defaultKubeRBACProxyImage, krp.Image)
})

t.Run("should add provider volume and mount when providerCMNames is non-nil", func(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion controllers/evalhub/configmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
type ServiceConfig struct {
Port int `json:"port"`
Host string `json:"host,omitempty"`
DisableAuth bool `json:"disable_auth,omitempty"`
ReadyFile string `json:"ready_file"`
TerminationFile string `json:"termination_file"`
EvalInitImage string `json:"eval_init_image,omitempty"`
Expand Down Expand Up @@ -153,7 +154,8 @@ func (r *EvalHubReconciler) generateConfigData(ctx context.Context, instance *ev

config := EvalHubConfig{
Service: ServiceConfig{
Port: containerPort,
Port: evalHubAppPort,
DisableAuth: true,
ReadyFile: "/tmp/repo-ready",
TerminationFile: "/tmp/termination-log",
EvalInitImage: evalHubImage,
Expand All @@ -162,6 +164,7 @@ func (r *EvalHubReconciler) generateConfigData(ctx context.Context, instance *ev
EnvMappings: EnvMappings{
"PORT": "service.port",
"API_HOST": "service.host",
"DISABLE_AUTH": "service.disable_auth",
"TLS_CERT_FILE": "service.tls_cert_file",
"TLS_KEY_FILE": "service.tls_key_file",
"DB_URL": "database.url",
Expand Down
20 changes: 16 additions & 4 deletions controllers/evalhub/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,27 @@ const (

// Container configuration
containerName = "evalhub"
containerPort = 8443
// evalHubAppPort is where eval-hub binds TLS (loopback only); kube-rbac-proxy listens on servicePort.
evalHubAppPort = 8444
// evalHubHealthPath is forwarded by kube-rbac-proxy; use --ignore-paths so kubelet probes skip authn/z.
evalHubHealthPath = "/api/v1/health"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

// Service configuration
// Service configuration (public HTTPS targets kube-rbac-proxy on this port)
serviceName = "evalhub"
servicePort = 8443

// kube-rbac-proxy sidecar
kubeRBACProxyContainerName = "kube-rbac-proxy"
kubeRBACProxyConfigMountPath = "/etc/kube-rbac-proxy/auth.yaml"
evalHubAuthConfigMapKey = "auth.yaml"
kubeRBACProxyUpstreamCAMountPath = "/etc/kube-rbac-proxy/upstream-ca"
kubeRBACProxyHealthPort = 9443

// Configuration constants
configMapName = "trustyai-service-operator-config"
configMapEvalHubImageKey = "evalHubImage"
configMapName = "trustyai-service-operator-config"
configMapEvalHubImageKey = "evalHubImage"
configMapKubeRBACProxyImageKey = "kube-rbac-proxy"
defaultKubeRBACProxyImage = "quay.io/openshift/origin-kube-rbac-proxy:4.19"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@nbs-rh hard-coded version?

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.

This is the fallback if a version is not provided through the operator configmap. Since we do not own kube-rbac-proxy repo, it is better to stick to a default image that we know works well rather than use ":latest"? Btw, this version 4.19 does not have our changes with endpoint-specific SARs - it needs to be added to the configmap and here in code as default once we have an image.


// TLS configuration (OpenShift service serving certificates)
tlsSecretMountPath = "/etc/tls/private"
Expand Down
120 changes: 99 additions & 21 deletions controllers/evalhub/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -74,17 +75,22 @@ func (r *EvalHubReconciler) buildDeploymentSpec(ctx context.Context, instance *e
evalHubImage = defaultEvalHubImage
}

kubeRBACProxyImage, err := r.getKubeRBACProxyImage(ctx)
if err != nil {
log.FromContext(ctx).Error(err, "Error getting kube-rbac-proxy image from ConfigMap. Using the default image value of "+defaultKubeRBACProxyImage)
kubeRBACProxyImage = defaultKubeRBACProxyImage
}

// Build default environment variables
// EvalHub serves TLS directly using OpenShift service serving certificates.
// Auth is handled internally via SAR checks.
// EvalHub listens on loopback only; kube-rbac-proxy terminates TLS on servicePort and enforces SAR (auth.yaml).
defaultEnvVars := []corev1.EnvVar{
{
Name: "API_HOST",
Value: "0.0.0.0",
Value: "127.0.0.1",
},
{
Name: "PORT",
Value: fmt.Sprintf("%d", containerPort),
Value: fmt.Sprintf("%d", evalHubAppPort),
},
{
Name: "TLS_CERT_FILE",
Expand Down Expand Up @@ -116,7 +122,7 @@ func (r *EvalHubReconciler) buildDeploymentSpec(ctx context.Context, instance *e
},
{
Name: "SERVICE_URL",
Value: fmt.Sprintf("https://%s.%s.svc.cluster.local:8443", instance.Name, instance.Namespace),
Value: fmt.Sprintf("https://%s.%s.svc.cluster.local:%d", instance.Name, instance.Namespace, servicePort),
},
{
Name: "EVALHUB_INSTANCE_NAME",
Expand Down Expand Up @@ -191,40 +197,103 @@ func (r *EvalHubReconciler) buildDeploymentSpec(ctx context.Context, instance *e
ImagePullPolicy: corev1.PullAlways,
Ports: []corev1.ContainerPort{
{
Name: "https",
ContainerPort: containerPort,
Name: "evalhub",
ContainerPort: evalHubAppPort,
Protocol: corev1.ProtocolTCP,
},
},
Env: env,
Resources: defaultResourceRequirements,
SecurityContext: defaultSecurityContext,
VolumeMounts: volumeMounts,
// HTTPGet probes with HTTPS scheme — kubelet skips TLS verification for probe requests.
LivenessProbe: &corev1.Probe{
// Liveness/readiness run on kube-rbac-proxy (same URL path as clients) with --ignore-paths on evalHubHealthPath.
}

upstreamURL := fmt.Sprintf("http://127.0.0.1:%d/", evalHubAppPort)
upstreamCAPath := kubeRBACProxyUpstreamCAMountPath + "/" + serviceCACertFile

kubeRBACProxyContainer := corev1.Container{
Name: kubeRBACProxyContainerName,
Image: kubeRBACProxyImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Args: []string{
"--secure-listen-address=0.0.0.0:" + fmt.Sprintf("%d", servicePort),
"--upstream=" + upstreamURL,
"--upstream-ca-file=" + upstreamCAPath,
"--config-file=" + kubeRBACProxyConfigMountPath,
"--tls-cert-file=" + tlsSecretMountPath + "/" + tlsCertFile,
"--tls-private-key-file=" + tlsSecretMountPath + "/" + tlsKeyFile,
"--proxy-endpoints-port=" + fmt.Sprintf("%d", kubeRBACProxyHealthPort),
"--ignore-paths=" + evalHubHealthPath,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"--auth-header-fields-enabled",
"--auth-header-user-field-name=X-User",
"--v=0",
},
Ports: []corev1.ContainerPort{
{
Name: "https",
ContainerPort: servicePort,
Protocol: corev1.ProtocolTCP,
},
{
Name: "proxy-healthz",
ContainerPort: kubeRBACProxyHealthPort,
Protocol: corev1.ProtocolTCP,
},
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("50m"),
corev1.ResourceMemory: resource.MustParse("32Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@nbs-rh is it to be expected that these are hard-coded here?

corev1.ResourceMemory: resource.MustParse("64Mi"),
},
},
SecurityContext: defaultSecurityContext,
VolumeMounts: []corev1.VolumeMount{
{
Name: "evalhub-config",
MountPath: kubeRBACProxyConfigMountPath,
SubPath: evalHubAuthConfigMapKey,
ReadOnly: true,
},
{
Name: instance.Name + "-tls",
MountPath: tlsSecretMountPath,
ReadOnly: true,
},
{
Name: serviceCAVolumeName,
MountPath: kubeRBACProxyUpstreamCAMountPath,
ReadOnly: true,
},
},
ReadinessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/api/v1/health",
Port: intstr.FromInt(containerPort),
Path: evalHubHealthPath,
Port: intstr.FromInt(servicePort),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 30,
PeriodSeconds: 10,
InitialDelaySeconds: 10,
PeriodSeconds: 5,
TimeoutSeconds: 5,
FailureThreshold: 3,
},
ReadinessProbe: &corev1.Probe{
LivenessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
HTTPGet: &corev1.HTTPGetAction{
Path: "/api/v1/health",
Port: intstr.FromInt(containerPort),
Path: evalHubHealthPath,
Port: intstr.FromInt(servicePort),
Scheme: corev1.URISchemeHTTPS,
},
},
InitialDelaySeconds: 10,
PeriodSeconds: 5,
TimeoutSeconds: 3,
InitialDelaySeconds: 30,
PeriodSeconds: 10,
TimeoutSeconds: 5,
FailureThreshold: 3,
},
}
Expand Down Expand Up @@ -309,14 +378,14 @@ func (r *EvalHubReconciler) buildDeploymentSpec(ctx context.Context, instance *e
},
})
}
// Pod template with EvalHub container and required volumes
// Pod template with EvalHub + kube-rbac-proxy and required volumes
podTemplate := corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
ServiceAccountName: generateServiceAccountName(instance),
Containers: []corev1.Container{container},
Containers: []corev1.Container{container, kubeRBACProxyContainer},
SecurityContext: defaultPodSecurityContext,
RestartPolicy: corev1.RestartPolicyAlways,
Volumes: volumes,
Expand Down Expand Up @@ -347,6 +416,15 @@ func (r *EvalHubReconciler) buildDeploymentSpec(ctx context.Context, instance *e
}, nil
}

// getKubeRBACProxyImage retrieves the kube-rbac-proxy image from the operator ConfigMap.
func (r *EvalHubReconciler) getKubeRBACProxyImage(ctx context.Context) (string, error) {
namespace := r.Namespace
if namespace == "" {
namespace = "trustyai-service-operator-system"
}
return utils.GetImageFromConfigMapWithFallback(ctx, r.Client, configMapKubeRBACProxyImageKey, configMapName, namespace, defaultKubeRBACProxyImage)
}

// getEvalHubImage retrieves the EvalHub image from ConfigMap with fallback to default
func (r *EvalHubReconciler) getEvalHubImage(ctx context.Context) (string, error) {
// Get the namespace where the operator is deployed (where the ConfigMap should be)
Expand Down
Loading
Loading