Skip to content
105 changes: 76 additions & 29 deletions api/v1alpha1/amaltheasession_children.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"fmt"
"io"
"maps"
"net/url"
"os"
"sort"
Expand Down Expand Up @@ -95,7 +96,6 @@ func (cr *AmaltheaSession) SessionVolumes() ([]v1.Volume, []v1.VolumeMount) {

// StatefulSet returns a AmaltheaSession StatefulSet object
func (cr *AmaltheaSession) StatefulSet(clusterType ClusterType) (appsv1.StatefulSet, error) {
labels := labelsForAmaltheaSession(cr.Name)
replicas := int32(1)
if cr.Spec.Hibernated {
replicas = 0
Expand Down Expand Up @@ -160,20 +160,23 @@ func (cr *AmaltheaSession) StatefulSet(clusterType ClusterType) (appsv1.Stateful

sts := appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name,
Namespace: cr.Namespace,
Name: cr.Name,
Namespace: cr.Namespace,
Labels: cr.childLabels(),
Annotations: cr.Spec.Template.Metadata.Annotations,
},
Spec: appsv1.StatefulSetSpec{
// NOTE: Parallel pod management policy is important
// If set to default (i.e. not parallel) K8s waits for the pod to become ready before restarting on updates
PodManagementPolicy: appsv1.ParallelPodManagement,
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
MatchLabels: selectorLabels(cr.Name),
},
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
Labels: cr.childLabels(),
Annotations: cr.Spec.Template.Metadata.Annotations,
},
Spec: pod,
},
Expand All @@ -188,19 +191,20 @@ func (cr *AmaltheaSession) StatefulSet(clusterType ClusterType) (appsv1.Stateful

// Service returns a AmaltheaSession Service object
func (cr *AmaltheaSession) Service() v1.Service {
labels := labelsForAmaltheaSession(cr.Name)
targetPort := cr.Spec.Session.Port
if cr.Spec.Authentication != nil && cr.Spec.Authentication.Enabled {
targetPort = authenticatedPort
}

svc := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name,
Namespace: cr.Namespace,
Name: cr.Name,
Namespace: cr.Namespace,
Labels: cr.childLabels(),
Annotations: cr.Spec.Template.Metadata.Annotations,
},
Spec: v1.ServiceSpec{
Selector: labels,
Selector: selectorLabels(cr.Name),
Ports: []v1.ServicePort{
{
Protocol: v1.ProtocolTCP,
Expand Down Expand Up @@ -255,20 +259,26 @@ func (cr *AmaltheaSession) ingressPathPrefix() string {

// Ingress returns a AmaltheaSession Ingress object
func (cr *AmaltheaSession) Ingress() *networkingv1.Ingress {
labels := labelsForAmaltheaSession(cr.Name)

ingress := cr.Spec.Ingress

if ingress == nil {
return nil
}

conflicts := findConflicts(ingress.Annotations, cr.Spec.Template.Metadata.Annotations)
if len(conflicts) > 0 {
log.Log.Info("Found conflicts in ingress annotations, will ignore templated conflicting annotations for ingress", "conflicting keys", conflicts)
}
annotations := map[string]string{}
// NOTE: Order between the two copy calls is important to avoid overwriting ingress annotations
maps.Copy(annotations, cr.Spec.Template.Metadata.Annotations)
maps.Copy(annotations, cr.Spec.Ingress.Annotations)
ing := &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name,
Namespace: cr.Namespace,
Labels: labels,
Annotations: ingress.Annotations,
Labels: cr.childLabels(),
Annotations: annotations,
},
Spec: networkingv1.IngressSpec{
IngressClassName: ingress.IngressClassName,
Expand Down Expand Up @@ -331,17 +341,17 @@ func (cr *AmaltheaSession) Ingress() *networkingv1.Ingress {

// PVC returned the desired specification for a persistent volume claim
func (cr *AmaltheaSession) PVC() v1.PersistentVolumeClaim {
labels := labelsForAmaltheaSession(cr.Name)
requests := v1.ResourceList{"storage": resource.MustParse("1Gi")}
if cr.Spec.Session.Storage.Size != nil {
requests = v1.ResourceList{"storage": *cr.Spec.Session.Storage.Size}
}

pvc := v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: cr.Name,
Namespace: cr.Namespace,
Labels: labels,
Name: cr.Name,
Namespace: cr.Namespace,
Labels: cr.childLabels(),
Annotations: cr.Spec.Template.Metadata.Annotations,
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
Expand All @@ -352,10 +362,11 @@ func (cr *AmaltheaSession) PVC() v1.PersistentVolumeClaim {
return pvc
}

// labelsForAmaltheaSessino returns the labels for selecting the resources
// selectorLabels returns the labels for selecting the resources
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
func labelsForAmaltheaSession(name string) map[string]string {
return map[string]string{"app.kubernetes.io/name": "AmaltheaSession",
func selectorLabels(name string) map[string]string {
Comment thread
olevski marked this conversation as resolved.
return map[string]string{
"app.kubernetes.io/name": "AmaltheaSession",
"app.kubernetes.io/instance": name,
"app.kubernetes.io/part-of": "amaltheasession-operator",
"app.kubernetes.io/created-by": "controller-manager",
Expand Down Expand Up @@ -530,15 +541,17 @@ func (as *AmaltheaSession) DataSources() ([]v1.PersistentVolumeClaim, []v1.Volum
case Rclone:
storageClass := rcloneStorageClass
readOnly := ds.AccessMode == v1.ReadOnlyMany
annotations := map[string]string{}
maps.Copy(annotations, as.Spec.Template.Metadata.Annotations)
annotations[rcloneStorageSecretNameAnnotation] = ds.SecretRef.Name
pvcs = append(
pvcs,
v1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: pvcName,
Namespace: as.Namespace,
Annotations: map[string]string{
rcloneStorageSecretNameAnnotation: ds.SecretRef.Name,
},
Name: pvcName,
Namespace: as.Namespace,
Annotations: annotations,
Labels: as.childLabels(),
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{ds.AccessMode},
Expand Down Expand Up @@ -614,12 +627,12 @@ func (as *AmaltheaSession) InternalSecretName() string {
// The secret will contain either, both or none of these configurations depending
// on the configuration of the Amalthea session.
func (as *AmaltheaSession) Secret() v1.Secret {
labels := labelsForAmaltheaSession(as.Name)
secret := v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: as.InternalSecretName(),
Namespace: as.Namespace,
Labels: labels,
Name: as.InternalSecretName(),
Namespace: as.Namespace,
Labels: as.childLabels(),
Annotations: as.Spec.Template.Metadata.Annotations,
},
}
// Secret used to secure the tunnel for remote sessions
Expand Down Expand Up @@ -955,3 +968,37 @@ func (cr *AmaltheaSession) tunnelContainer() v1.Container {

return tunnelContainer
}

// findConflicst will return all the keys from source that can be found in desintation.
func findConflicts(destination, source map[string]string) []string {
conflicts := []string{}
for srcKey := range source {
_, found := destination[srcKey]
if found {
conflicts = append(conflicts, srcKey)
}
}
return conflicts
}

func (cr *AmaltheaSession) childLabels() map[string]string {
labels := map[string]string{}
maps.Copy(labels, cr.Spec.Template.Metadata.Labels)
selectorLabels := selectorLabels(cr.Name)
conflicts := findConflicts(labels, selectorLabels)
if len(conflicts) > 0 {
log.Log.Info(
"Found conflicts in template labels and selector labels, the selector labels will take precedence",
"template labels",
labels,
"selector labels",
selectorLabels,
"conflicting keys",
conflicts,
)
}
// NOTE: stuff from selectorLabels will overwrite conflicts in labels (if there are any)
// This is the desired behaviour, we do not want to overwrite the selector labels.
maps.Copy(labels, selectorLabels)
return labels
}
14 changes: 14 additions & 0 deletions api/v1alpha1/amaltheasession_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ type AmaltheaSessionSpec struct {
// +optional
// The name of the service account that should be used for the session Pod
ServiceAccountName string `json:"serviceAccountName,omitempty"`

// +optional
// Template for the fields that should be added to all children (and their children if applicable).
Template Template `json:"template,omitempty"`
}

type Session struct {
Expand Down Expand Up @@ -582,6 +586,16 @@ type ReadinessProbe struct {
Type ReadinessProbeType `json:"type,omitempty"`
}

type Template struct {
// +optional
Metadata TemplateMetadata `json:"metadata,omitzero"`
Comment thread
leafty marked this conversation as resolved.
Comment thread
olevski marked this conversation as resolved.
}

type TemplateMetadata struct {
Annotations map[string]string `json:"annotations,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}

// +kubebuilder:validation:Enum={local,remote}
type SessionLocation string

Expand Down
46 changes: 46 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

2 changes: 1 addition & 1 deletion bundle/manifests/amalthea.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ metadata:
}
]
capabilities: Basic Install
createdAt: "2025-06-25T07:56:15Z"
createdAt: "2025-07-03T16:02:07Z"
operators.operatorframework.io/builder: operator-sdk-v1.38.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
name: amalthea.v0.0.1
Expand Down
16 changes: 16 additions & 0 deletions bundle/manifests/amalthea.dev_amaltheasessions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6436,6 +6436,22 @@ spec:
required:
- image
type: object
template:
description: Template for the fields that should be added to all children
(and their children if applicable).
properties:
metadata:
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
type: object
tolerations:
description: |-
If specified, the pod's tolerations.
Expand Down
16 changes: 16 additions & 0 deletions config/crd/bases/amalthea.dev_amaltheasessions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6436,6 +6436,22 @@ spec:
required:
- image
type: object
template:
description: Template for the fields that should be added to all children
(and their children if applicable).
properties:
metadata:
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
type: object
tolerations:
description: |-
If specified, the pod's tolerations.
Expand Down
16 changes: 16 additions & 0 deletions helm-chart/amalthea-sessions/templates/amaltheasession-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6438,6 +6438,22 @@ spec:
required:
- image
type: object
template:
description: Template for the fields that should be added to all children
(and their children if applicable).
properties:
metadata:
properties:
annotations:
additionalProperties:
type: string
type: object
labels:
additionalProperties:
type: string
type: object
type: object
type: object
tolerations:
description: |-
If specified, the pod's tolerations.
Expand Down
2 changes: 1 addition & 1 deletion helm-chart/amalthea-sessions/templates/manager-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ rules:
resources:
- persistentvolumeclaims
- services
verbs: [create, get, list, watch]
verbs: [create, get, list, watch, patch]
- apiGroups:
- apps
resources:
Expand Down
Loading