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
59 changes: 59 additions & 0 deletions controller/deploy/operator/api/v1alpha1/jumpstarter_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ const (
// provisioner controller deployments are available
ConditionTypeExporterSetControllersReady = "ExporterSetControllersReady"

// ConditionTypeTelemetryDeploymentReady indicates whether the telemetry deployment is available
ConditionTypeTelemetryDeploymentReady = "TelemetryDeploymentReady"

// ConditionTypeReady indicates whether the overall Jumpstarter system is ready
ConditionTypeReady = "Ready"
)
Expand Down Expand Up @@ -201,6 +204,13 @@ type JumpstarterSpec struct {
// Deprecated labels configuration for warning users about label keys that should no longer be used.
// +optional
DeprecatedLabels DeprecatedLabelsConfig `json:"deprecatedLabels,omitempty"`

// Telemetry configuration for the optional telemetry service.
// When enabled, the operator deploys a jumpstarter-telemetry service that receives
// structured log entries from exporters via gRPC. The controller advertises the
// telemetry endpoint to exporters so they can push logs without cluster credentials.
// +optional
Telemetry *TelemetryConfig `json:"telemetry,omitempty"`
}

// HiddenLabelsConfig defines label keys to hide from exporter listings by default.
Expand Down Expand Up @@ -266,6 +276,55 @@ type DeprecatedLabelsConfig struct {
Keys map[string]string `json:"keys,omitempty"`
}

// TelemetryConfig defines configuration for the telemetry service deployment.
// When enabled, the operator creates a Deployment and ClusterIP Service for
// jumpstarter-telemetry, which receives structured log entries from exporters.
type TelemetryConfig struct {
// Enable the telemetry service deployment.
// When enabled, the operator deploys a jumpstarter-telemetry pod and a ClusterIP
// Service, and configures the controller to advertise the endpoint to exporters.
// +kubebuilder:default=false
Enabled bool `json:"enabled,omitempty"`

// Container image for the telemetry pod in 'registry/repository/image:tag' format.
// +kubebuilder:default="quay.io/jumpstarter-dev/jumpstarter-telemetry:latest"
Image string `json:"image,omitempty"`

// Image pull policy for the telemetry container.
// +kubebuilder:default="IfNotPresent"
// +kubebuilder:validation:Enum=Always;IfNotPresent;Never
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`

// Number of telemetry replicas to run.
// Multiple replicas provide HA; each exporter connects to exactly one replica
// via a persistent MetricsStream, so Prometheus sum-by queries across replicas
// yield exact totals without double-counting (see JEP-0013 DD-8).
// +kubebuilder:default=1
// +kubebuilder:validation:Minimum=1
Replicas *int32 `json:"replicas,omitempty"`

// Resource requirements for the telemetry pod.
Resources corev1.ResourceRequirements `json:"resources,omitempty"`

// Logging configuration for the telemetry log ingestion path.
Logging TelemetryLoggingConfig `json:"logging,omitempty"`
}

// TelemetryLoggingConfig configures the log push path to the telemetry service.
type TelemetryLoggingConfig struct {
// Filter controls which log entries are forwarded to the telemetry service.
Filter TelemetryLoggingFilterConfig `json:"filter,omitempty"`
}

// TelemetryLoggingFilterConfig controls which log entries are forwarded to the telemetry service.
type TelemetryLoggingFilterConfig struct {
// Minimum log severity to forward.
// Accepted values: debug, info, warning, error, critical. Defaults to "info".
// +kubebuilder:default="info"
// +kubebuilder:validation:Enum=debug;info;warning;error;critical
MinSeverity string `json:"minSeverity,omitempty"`
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// LeasePolicyConfig defines policy constraints for leases.
type LeasePolicyConfig struct {
// Maximum number of user-defined tags allowed per lease.
Expand Down
58 changes: 58 additions & 0 deletions controller/deploy/operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,125 @@ spec:
type: object
type: array
type: object
telemetry:
description: |-
Telemetry configuration for the optional telemetry service.
When enabled, the operator deploys a jumpstarter-telemetry service that receives
structured log entries from exporters via gRPC. The controller advertises the
telemetry endpoint to exporters so they can push logs without cluster credentials.
properties:
enabled:
default: false
description: |-
Enable the telemetry service deployment.
When enabled, the operator deploys a jumpstarter-telemetry pod and a ClusterIP
Service, and configures the controller to advertise the endpoint to exporters.
type: boolean
image:
default: quay.io/jumpstarter-dev/jumpstarter-telemetry:latest
description: Container image for the telemetry pod in 'registry/repository/image:tag'
format.
type: string
imagePullPolicy:
default: IfNotPresent
description: Image pull policy for the telemetry container.
enum:
- Always
- IfNotPresent
- Never
type: string
logging:
description: Logging configuration for the telemetry log ingestion
path.
properties:
filter:
description: Filter controls which log entries are forwarded
to the telemetry service.
properties:
minSeverity:
default: info
description: |-
Minimum log severity to forward.
Accepted values: debug, info, warning, error, critical. Defaults to "info".
enum:
- debug
- info
- warning
- error
- critical
type: string
type: object
type: object
replicas:
default: 1
description: |-
Number of telemetry replicas to run.
Multiple replicas provide HA; each exporter connects to exactly one replica
via a persistent MetricsStream, so Prometheus sum-by queries across replicas
yield exact totals without double-counting (see JEP-0013 DD-8).
format: int32
minimum: 1
type: integer
resources:
description: Resource requirements for the telemetry pod.
properties:
claims:
description: |-
Claims lists the names of resources, defined in spec.resourceClaims,
that are used by this container.

This field depends on the
DynamicResourceAllocation feature gate.

This field is immutable. It can only be set for containers.
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
properties:
name:
description: |-
Name must match the name of one entry in pod.spec.resourceClaims of
the Pod where this field is used. It makes that resource available
inside a container.
type: string
request:
description: |-
Request is the name chosen for a request in the referenced claim.
If empty, everything from the claim is made available, otherwise
only the result of this request.
type: string
required:
- name
type: object
type: array
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
requests:
additionalProperties:
anyOf:
- type: integer
- type: string
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
x-kubernetes-int-or-string: true
description: |-
Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
otherwise to an implementation-defined value. Requests cannot exceed Limits.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
type: object
type: object
type: object
type: object
status:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ func (r *JumpstarterReconciler) reconcileCertificates(ctx context.Context, js *o
}
}

// Create telemetry certificate if telemetry is enabled
if js.Spec.Telemetry != nil && js.Spec.Telemetry.Enabled {
if err := r.reconcileTelemetryCertificate(ctx, js, issuerRef); err != nil {
return fmt.Errorf("failed to reconcile telemetry certificate: %w", err)
}
}

// Reconcile CA ConfigMap AFTER certificates are created
// This ensures cert-manager has had a chance to create the CA secret
// which we need to populate the ConfigMap for the login service
Expand Down Expand Up @@ -369,6 +376,28 @@ func (r *JumpstarterReconciler) reconcileRouterCertificate(ctx context.Context,
return r.reconcileServerCertificate(ctx, js, issuerRef, certName, "router", dnsNames, extraLabels)
}

// reconcileTelemetryCertificate creates the TLS certificate for the telemetry service.
func (r *JumpstarterReconciler) reconcileTelemetryCertificate(ctx context.Context, js *operatorv1alpha1.Jumpstarter, issuerRef cmmeta.ObjectReference) error {
certName := getTelemetryCertSecretName(js)
includeInternalNames := !isExternalIssuer(js)
dnsNames := r.collectTelemetryDNSNames(js, includeInternalNames)
return r.reconcileServerCertificate(ctx, js, issuerRef, certName, "telemetry", dnsNames, nil)
}

// collectTelemetryDNSNames collects all DNS names for the telemetry certificate.
Comment on lines +379 to +387

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add an integration test inside Describe("Telemetry Lifecycle") that sets both CertManager.Enabled: true and Telemetry.Enabled: true, calls doReconcile(), and asserts a certmanagerv1.Certificate named by GetTelemetryCertSecretName(js) is created with the expected DNS names.

func (r *JumpstarterReconciler) collectTelemetryDNSNames(js *operatorv1alpha1.Jumpstarter, includeInternalNames bool) []string {
var dnsNames []string
if includeInternalNames {
dnsNames = append(dnsNames,
telemetryServiceName,
fmt.Sprintf("%s.%s", telemetryServiceName, js.Namespace),
fmt.Sprintf("%s.%s.svc", telemetryServiceName, js.Namespace),
fmt.Sprintf("%s.%s.svc.cluster.local", telemetryServiceName, js.Namespace),
)
}
return dnsNames
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// collectControllerDNSNames collects all DNS names for the controller certificate.
// When includeInternalNames is false, internal Kubernetes service DNS names are
// omitted so that external issuers (e.g. ACME/Let's Encrypt) don't attempt to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,24 @@ func (r *JumpstarterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

// Reconcile Services
// Reconcile Telemetry Deployment (Service is reconciled below in the networking stage)
if err := r.reconcileTelemetryDeploymentStage(ctx, &jumpstarter); err != nil {
log.Error(err, "Failed to reconcile Telemetry deployment")
return ctrl.Result{}, err
}

// Reconcile Services (controller, router, login endpoints, and telemetry ClusterIP)
if err := r.reconcileServices(ctx, &jumpstarter); err != nil {
log.Error(err, "Failed to reconcile Services")
return ctrl.Result{}, err
}

// Reconcile Telemetry ClusterIP Service (part of the networking stage)
if err := r.reconcileTelemetryServiceStage(ctx, &jumpstarter); err != nil {
log.Error(err, "Failed to reconcile Telemetry service")
return ctrl.Result{}, err
}

// Reconcile ConfigMaps (after deployments and services, before secrets)
if err := r.reconcileConfigMaps(ctx, &jumpstarter, desiredConfigMap); err != nil {
log.Error(err, "Failed to reconcile ConfigMaps")
Expand Down Expand Up @@ -851,6 +863,14 @@ func (r *JumpstarterReconciler) createControllerDeployment(jumpstarter *operator
},
}

// Add telemetry endpoint env var when telemetry is enabled
if jumpstarter.Spec.Telemetry != nil && jumpstarter.Spec.Telemetry.Enabled {
envVars = append(envVars, corev1.EnvVar{
Name: "GRPC_TELEMETRY_ENDPOINT",
Value: telemetryEndpointFor(jumpstarter.Namespace),
})
}
Comment on lines +866 to +872

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This env var is set on the controller deployment but nothing in the controller binary reads GRPC_TELEMETRY_ENDPOINT — the controller loads its telemetry config from the ConfigMap via cfg.Telemetry (wired at cmd/main.go:288), which is already correctly populated by buildConfig below (lines 1315-1327).

Is this intended for a future consumer, or can it be removed? If kept, it should at least have a comment explaining why it exists alongside the ConfigMap path.


var volumeMounts []corev1.VolumeMount
var volumes []corev1.Volume

Expand Down Expand Up @@ -1292,6 +1312,20 @@ func (r *JumpstarterReconciler) buildConfig(ctx context.Context, jumpstarter *op
Keys: jumpstarter.Spec.DeprecatedLabels.Keys,
}

// Telemetry configuration.
// Certificate is intentionally omitted until the telemetry binary supports TLS serving.
if jumpstarter.Spec.Telemetry != nil && jumpstarter.Spec.Telemetry.Enabled {
t := jumpstarter.Spec.Telemetry
telemetryCfg := &config.Telemetry{
Enabled: true,
Endpoint: telemetryEndpointFor(jumpstarter.Namespace),
}
if t.Logging.Filter.MinSeverity != "" {
telemetryCfg.Logging.Filter.MinSeverity = t.Logging.Filter.MinSeverity

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor naming inconsistency: the CRD field here uses minSeverity (camelCase, standard for K8s CRDs), but the config struct at controller/internal/config/types.go:57 serializes as min_severity (snake_case in JSON/YAML tags).

Would recommend updating the config struct's JSON tag to minSeverity as well for uniformity — it keeps the ConfigMap YAML consistent with the CR spec and avoids confusion for operators inspecting both resources. Since the ConfigMap is machine-generated (not hand-edited), the rename should be safe.

AI Generated, but reviewed by me.

}
cfg.Telemetry = telemetryCfg
}

// gRPC keepalive configuration
if jumpstarter.Spec.Controller.GRPC.Keepalive != nil {
ka := &cfg.Grpc.Keepalive
Expand Down
Loading
Loading