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
6 changes: 6 additions & 0 deletions controller/Containerfile.telemetry.prebuilt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# CI-only runtime image for the jumpstarter-telemetry binary.
FROM registry.access.redhat.com/ubi9/ubi-micro:9.8-1786321990@sha256:7e7f79ab747bf2b452e3043dd89f388e92be4c7fdcc8b815b58adf6c99c39c95
WORKDIR /
COPY telemetry /telemetry
USER 65532:65532
ENTRYPOINT ["/telemetry"]
18 changes: 16 additions & 2 deletions controller/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ IMG ?= quay.io/jumpstarter-dev/jumpstarter-controller:latest
DOCKER_REPO = $(shell echo $(IMG) | cut -d: -f1)
DOCKER_TAG = $(shell echo $(IMG) | cut -d: -f2)
EXPORTER_SET_CONTROLLER_IMG ?= quay.io/jumpstarter-dev/jumpstarter-exporterset-controller:latest
TELEMETRY_IMG ?= quay.io/jumpstarter-dev/jumpstarter-telemetry:latest
QEMU_RUNTIME_IMG ?= quay.io/jumpstarter-dev/virtual/qemu-runtime:latest
EXPORTER_IMG ?= quay.io/jumpstarter-dev/jumpstarter:latest
# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
Expand Down Expand Up @@ -124,6 +125,7 @@ build: manifests generate fmt vet ## Build manager binary.
go build -ldflags "$(LDFLAGS)" -o bin/manager cmd/main.go
go build -ldflags "$(LDFLAGS)" -o bin/router ./cmd/router
go build -ldflags "$(LDFLAGS)" -o bin/exporter-set-controller cmd/exporter-set-controller/main.go
go build -ldflags "$(LDFLAGS)" -o bin/telemetry cmd/telemetry/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
Expand All @@ -133,6 +135,10 @@ run: manifests generate fmt vet ## Run a controller from your host.
run-router: manifests generate fmt vet ## Run a router from your host.
go run ./cmd/router

.PHONY: run-telemetry
run-telemetry: manifests generate fmt vet ## Run the telemetry service from your host.
go run ./cmd/telemetry/main.go

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
Expand All @@ -146,12 +152,19 @@ docker-build: ## Build docker image with the manager.

.PHONY: docker-build-ci
docker-build-ci: ## Build docker images from pre-compiled host binaries (fast CI path).
rm -rf bin/ci-stage && mkdir -p bin/ci-stage/controller bin/ci-stage/esc
rm -rf bin/ci-stage && mkdir -p bin/ci-stage/controller bin/ci-stage/esc bin/ci-stage/telemetry
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o bin/ci-stage/controller/manager cmd/main.go
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o bin/ci-stage/controller/router ./cmd/router
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o bin/ci-stage/esc/exporter-set-controller cmd/exporter-set-controller/main.go
$(CONTAINER_TOOL) build --build-arg BIN=manager -t $(IMG) -f Containerfile.prebuilt bin/ci-stage/controller
$(CONTAINER_TOOL) build --build-arg BIN=exporter-set-controller -t $(EXPORTER_SET_CONTROLLER_IMG) -f Containerfile.prebuilt bin/ci-stage/esc
$(MAKE) docker-build-telemetry-ci

.PHONY: docker-build-telemetry-ci
docker-build-telemetry-ci: ## CI-optimized: host-compiled telemetry binary, no multi-stage build.
rm -rf bin/ci-stage/telemetry && mkdir -p bin/ci-stage/telemetry
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -ldflags "$(LDFLAGS)" -o bin/ci-stage/telemetry/telemetry cmd/telemetry/main.go
$(CONTAINER_TOOL) build -t $(TELEMETRY_IMG) -f Containerfile.telemetry.prebuilt bin/ci-stage/telemetry

.PHONY: docker-build-exporter-set-controller-ci
docker-build-exporter-set-controller-ci: ## CI-optimized: host-compiled ESC binary, no multi-stage build.
Expand Down Expand Up @@ -226,6 +239,7 @@ deploy: cluster grpcurl ## Deploy controller using the operator. Set SKIP_BUILD=
ifeq ($(SKIP_BUILD),)
$(MAKE) docker-build
$(MAKE) docker-build-exporter-set-controller
$(MAKE) docker-build-telemetry-ci
$(MAKE) build-operator
endif
./hack/deploy_with_operator.sh
Expand All @@ -236,7 +250,7 @@ deploy-ci: cluster grpcurl $(if $(SKIP_BUILD),,docker-build-ci build-operator-ci


.PHONY: deploy-operator
deploy-operator: docker-build docker-build-exporter-set-controller build-operator cluster grpcurl ## Deploy only the operator (without Jumpstarter CR)
deploy-operator: docker-build docker-build-exporter-set-controller docker-build-telemetry-ci build-operator cluster grpcurl ## Deploy only the operator (without Jumpstarter CR)
NETWORKING_MODE=ingress DEPLOY_JUMPSTARTER=false ./hack/deploy_with_operator.sh

.PHONY: deploy-operator-ci
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func (r *JumpstarterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

// Reconcile ConfigMaps (after deployments and services, before secrets)
// Reconcile ConfigMaps (after deployments and services)
if err := r.reconcileConfigMaps(ctx, &jumpstarter, desiredConfigMap); err != nil {
log.Error(err, "Failed to reconcile ConfigMaps")
return ctrl.Result{}, err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ func createTelemetryDeployment(jumpstarter *operatorv1alpha1.Jumpstarter, tlsSec
},
},
},
// Advertised endpoint for self-signed SAN generation (must match controller ConfigMap).
{Name: "GRPC_TELEMETRY_ENDPOINT", Value: telemetryEndpointFor(jumpstarter.Namespace)},
}

var volumeMounts []corev1.VolumeMount
Expand Down Expand Up @@ -437,13 +439,13 @@ func createTelemetryDeployment(jumpstarter *operatorv1alpha1.Jumpstarter, tlsSec
},
},
},
Volumes: volumes,
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: ptr.To(true),
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
},
Volumes: volumes,
ServiceAccountName: jumpstarter.Name + telemetrySASuffix,
},
},
Expand Down Expand Up @@ -498,14 +500,16 @@ func GetTelemetryCertSecretName(js *operatorv1alpha1.Jumpstarter) string {
}

// resolveTelemetryCA reads the CA certificate that exporters need to verify the
// telemetry TLS connection. For self-signed CA mode, the cert is in the CA secret;
// for external issuers, the user-provided caBundle is used.
// telemetry TLS connection. For self-signed CA mode, the cert is in the CA secret.
// For external issuers, the user-provided caBundle is preferred; when absent, ca.crt
// from the issued telemetry TLS secret is used if present. An empty return with no
// error means exporters should rely on the system trust store (public CA issuers).
func (r *JumpstarterReconciler) resolveTelemetryCA(ctx context.Context, jumpstarter *operatorv1alpha1.Jumpstarter) (string, error) {
if jumpstarter.Spec.CertManager.Server != nil && jumpstarter.Spec.CertManager.Server.IssuerRef != nil {
if len(jumpstarter.Spec.CertManager.Server.IssuerRef.CABundle) > 0 {
return string(jumpstarter.Spec.CertManager.Server.IssuerRef.CABundle), nil
}
return "", nil
return r.telemetryCAFromCertSecret(ctx, jumpstarter)
}

// Self-signed CA mode — read from the CA secret created by cert-manager
Expand Down Expand Up @@ -535,6 +539,27 @@ func (r *JumpstarterReconciler) telemetryCANeedsRequeue(ctx context.Context, jum
return err != nil || caCert == ""
}

// telemetryCAFromCertSecret returns ca.crt from the issued telemetry TLS secret,
// when cert-manager includes it. Missing secret or key is not an error: external
// issuers backed by public CAs may not need an explicit bundle in the controller config.
func (r *JumpstarterReconciler) telemetryCAFromCertSecret(ctx context.Context, jumpstarter *operatorv1alpha1.Jumpstarter) (string, error) {
secret := &corev1.Secret{}
err := r.Get(ctx, client.ObjectKey{
Name: GetTelemetryCertSecretName(jumpstarter),
Namespace: jumpstarter.Namespace,
}, secret)
if err != nil {
if errors.IsNotFound(err) {
return "", nil
}
return "", fmt.Errorf("telemetry TLS secret not found: %w", err)
}
if ca, ok := secret.Data["ca.crt"]; ok && len(ca) > 0 {
return string(ca), nil
}
return "", nil
}

// telemetryEndpointFor returns the in-cluster gRPC endpoint for the telemetry service.
func telemetryEndpointFor(namespace string) string {
return fmt.Sprintf("%s.%s.svc:%d", telemetryServiceName, namespace, telemetryPort)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,44 @@ var _ = Describe("Telemetry Lifecycle", func() {
"telemetry should not appear in ConfigMap after disabling")
})

It("includes telemetry CA from issued TLS secret when external issuer has no CABundle", func() {
By("creating a Jumpstarter CR with telemetry, cert-manager, and external issuer")
spec := makeJumpstarterSpec()
spec.CertManager = operatorv1alpha1.CertManagerConfig{
Enabled: true,
Server: &operatorv1alpha1.ServerCertConfig{
IssuerRef: &operatorv1alpha1.IssuerReference{
Name: "my-issuer",
Kind: "ClusterIssuer",
},
},
}
spec.Telemetry = &operatorv1alpha1.TelemetryConfig{
Enabled: true,
Image: "quay.io/jumpstarter-dev/jumpstarter-telemetry:latest",
}
js := &operatorv1alpha1.Jumpstarter{
ObjectMeta: metav1.ObjectMeta{Name: crName, Namespace: crNamespace},
Spec: spec,
}

By("pre-creating the issued telemetry TLS secret with ca.crt")
Expect(k8sClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: GetTelemetryCertSecretName(js),
Namespace: crNamespace,
},
Data: map[string][]byte{
"ca.crt": []byte(testPEM),
},
})).To(Succeed())

cfg, err := newReconciler().buildConfig(ctx, js)
Expect(err).NotTo(HaveOccurred())
Expect(cfg.Telemetry).NotTo(BeNil())
Expect(cfg.Telemetry.Certificate).To(ContainSubstring("BEGIN CERTIFICATE"))
})

It("propagates telemetry config into the controller ConfigMap", func() {
By("creating a Jumpstarter CR with telemetry and a custom minSeverity")
spec := makeJumpstarterSpec()
Expand Down Expand Up @@ -438,13 +476,14 @@ var _ = Describe("Telemetry Lifecycle", func() {

container := dep.Spec.Template.Spec.Containers[0]

// Should have CONTROLLER_KEY + TLS env vars
Expect(container.Env).To(HaveLen(3))
// Should have CONTROLLER_KEY, GRPC_TELEMETRY_ENDPOINT, and TLS env vars
Expect(container.Env).To(HaveLen(4))
envNames := make(map[string]string)
for _, env := range container.Env {
envNames[env.Name] = env.Value
}
Expect(envNames).To(HaveKey("CONTROLLER_KEY"))
Expect(envNames).To(HaveKeyWithValue("GRPC_TELEMETRY_ENDPOINT", telemetryEndpointFor("default")))
Expect(envNames).To(HaveKeyWithValue("EXTERNAL_CERT_PEM", "/tls/tls.crt"))
Expect(envNames).To(HaveKeyWithValue("EXTERNAL_KEY_PEM", "/tls/tls.key"))

Expand Down Expand Up @@ -485,13 +524,14 @@ var _ = Describe("Telemetry Lifecycle", func() {

container := dep.Spec.Template.Spec.Containers[0]

// Should have CONTROLLER_KEY + TLS env vars
Expect(container.Env).To(HaveLen(3))
// Should have CONTROLLER_KEY, GRPC_TELEMETRY_ENDPOINT, and TLS env vars
Expect(container.Env).To(HaveLen(4))
envNames := make(map[string]string)
for _, env := range container.Env {
envNames[env.Name] = env.Value
}
Expect(envNames).To(HaveKey("CONTROLLER_KEY"))
Expect(envNames).To(HaveKeyWithValue("GRPC_TELEMETRY_ENDPOINT", telemetryEndpointFor("default")))
Expect(envNames).To(HaveKeyWithValue("EXTERNAL_CERT_PEM", "/tls/tls.crt"))
Expect(envNames).To(HaveKeyWithValue("EXTERNAL_KEY_PEM", "/tls/tls.key"))

Expand Down Expand Up @@ -528,8 +568,14 @@ var _ = Describe("Telemetry Lifecycle", func() {

container := dep.Spec.Template.Spec.Containers[0]

// Only CONTROLLER_KEY should be set (no TLS env vars)
Expect(container.Env).To(HaveLen(1))
Expect(container.Env).To(HaveLen(2))
envNames := make(map[string]string)
for _, env := range container.Env {
if env.Value != "" {
envNames[env.Name] = env.Value
}
}
Expect(envNames).To(HaveKeyWithValue("GRPC_TELEMETRY_ENDPOINT", telemetryEndpointFor("default")))
Expect(container.Env[0].Name).To(Equal("CONTROLLER_KEY"))

// No volume mounts or volumes
Expand Down Expand Up @@ -811,7 +857,39 @@ var _ = Describe("resolveTelemetryCA", func() {
Expect(err.Error()).To(ContainSubstring("missing tls.crt"))
})

It("returns ('', nil) when an external IssuerRef has a nil CABundle", func() {
It("returns ca.crt from the telemetry TLS secret when an external IssuerRef has no CABundle", func() {
js := &operatorv1alpha1.Jumpstarter{
ObjectMeta: metav1.ObjectMeta{Name: "test-ca-from-secret", Namespace: crNamespace},
Spec: operatorv1alpha1.JumpstarterSpec{
CertManager: operatorv1alpha1.CertManagerConfig{
Enabled: true,
Server: &operatorv1alpha1.ServerCertConfig{
IssuerRef: &operatorv1alpha1.IssuerReference{
Name: "my-issuer",
Kind: "ClusterIssuer",
},
},
},
},
}

Expect(k8sClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: GetTelemetryCertSecretName(js),
Namespace: crNamespace,
},
Data: map[string][]byte{
"ca.crt": []byte(testPEM),
},
})).To(Succeed())

r := &JumpstarterReconciler{Client: k8sClient, Scheme: k8sClient.Scheme()}
ca, err := r.resolveTelemetryCA(ctx, js)
Expect(err).NotTo(HaveOccurred())
Expect(ca).To(ContainSubstring("BEGIN CERTIFICATE"))
})

It("returns ('', nil) when an external IssuerRef has a nil CABundle and no telemetry TLS secret", func() {
js := &operatorv1alpha1.Jumpstarter{
ObjectMeta: metav1.ObjectMeta{Name: "test-ca-no-bundle", Namespace: crNamespace},
Spec: operatorv1alpha1.JumpstarterSpec{
Expand Down
26 changes: 26 additions & 0 deletions controller/deploy/operator/test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,32 @@ func verifyDeploymentHasTLSMount(g Gomega, namespace, name string) {
g.Expect(hasKeyEnv).To(BeTrue(), fmt.Sprintf("deployment %s missing EXTERNAL_KEY_PEM env var", name))
}

// verifyDeploymentHasControllerKey checks that a deployment sources CONTROLLER_KEY from
// jumpstarter-controller-secret. PushLogs bearer-token verification requires the same
// signing seed as the controller.
func verifyDeploymentHasControllerKey(g Gomega, namespace, name string) {
deployment := &appsv1.Deployment{}
err := k8sClient.Get(ctx, types.NamespacedName{
Name: name,
Namespace: namespace,
}, deployment)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(deployment.Spec.Template.Spec.Containers).NotTo(BeEmpty())

var found bool
for _, env := range deployment.Spec.Template.Spec.Containers[0].Env {
if env.Name != "CONTROLLER_KEY" {
continue
}
g.Expect(env.ValueFrom).NotTo(BeNil())
g.Expect(env.ValueFrom.SecretKeyRef).NotTo(BeNil())
g.Expect(env.ValueFrom.SecretKeyRef.Name).To(Equal("jumpstarter-controller-secret"))
g.Expect(env.ValueFrom.SecretKeyRef.Key).To(Equal("key"))
found = true
}
g.Expect(found).To(BeTrue(), fmt.Sprintf("deployment %s missing CONTROLLER_KEY env var", name))
}

// verifyDeploymentHasNoTLSMount checks that a deployment does NOT have TLS configuration.
// This is used with Gomega assertions to verify the deployment has been reconciled without TLS.
func verifyDeploymentHasNoTLSMount(g Gomega, namespace, name string) {
Expand Down
Loading
Loading