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
29 changes: 16 additions & 13 deletions api/v1alpha1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,23 +193,26 @@ type SidecarConfig struct {
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`

// TLS, if set, fronts the sidecar API with kube-rbac-proxy on
// :8443 backed by a cert-manager-issued cert. Init-only:
// toggling on a Running SeiNode requires recreation. See seictl#165.
// :8443 using TLS material from a Secret in the SeiNode's
// namespace. The Secret is operator-provisioned (e.g., via a
// cert-manager Certificate in the platform GitOps repo); this
// controller does not create it.
//
// Immutable post-creation. Toggling TLS on an existing SeiNode is a
// delete + recreate operation; data persists via the SeiNode's PVC
// retention mechanism.
//
// +optional
TLS *SidecarTLSSpec `json:"tls,omitempty"`
}

// SidecarTLSSpec configures the cert-manager-issued serving cert for
// the kube-rbac-proxy fronting.
// SidecarTLSSpec references an externally-provisioned TLS Secret.
type SidecarTLSSpec struct {
// IssuerName references a cert-manager Issuer or ClusterIssuer
// that signs the proxy's serving certificate.
// SecretName is a kubernetes.io/tls Secret in the SeiNode's
// namespace. The cert SANs must include the DNS names published
// in status.sidecarTLS.requiredDNSNames; the controller validates
// this before allowing the pod to schedule.
// +kubebuilder:validation:Required
IssuerName string `json:"issuerName"`

// IssuerKind is "Issuer" (namespaced) or "ClusterIssuer".
// +kubebuilder:default=ClusterIssuer
// +kubebuilder:validation:Enum=Issuer;ClusterIssuer
// +optional
IssuerKind string `json:"issuerKind,omitempty"`
// +kubebuilder:validation:MinLength=1
SecretName string `json:"secretName"`
}
39 changes: 39 additions & 0 deletions api/v1alpha1/seinode_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// the populated field determines the node's operating mode.
// +kubebuilder:validation:XValidation:rule="(has(self.fullNode) ? 1 : 0) + (has(self.archive) ? 1 : 0) + (has(self.replayer) ? 1 : 0) + (has(self.validator) ? 1 : 0) == 1",message="exactly one of fullNode, archive, replayer, or validator must be set"
// +kubebuilder:validation:XValidation:rule="!has(self.replayer) || (has(self.peers) && size(self.peers) > 0)",message="peers is required when replayer mode is set"
// +kubebuilder:validation:XValidation:rule="(!has(oldSelf.sidecar) || !has(oldSelf.sidecar.tls)) ? (!has(self.sidecar) || !has(self.sidecar.tls)) : (has(self.sidecar) && has(self.sidecar.tls) && self.sidecar.tls == oldSelf.sidecar.tls)",message="spec.sidecar.tls is immutable; delete + recreate the SeiNode to change TLS configuration"
type SeiNodeSpec struct {
// ChainID of the chain this node belongs to.
// +kubebuilder:validation:MinLength=1
Expand Down Expand Up @@ -273,6 +274,13 @@ const (
// pre-flight validation. Only set on SeiNodes with
// spec.validator.operatorKeyring.
ConditionOperatorKeyringReady = "OperatorKeyringReady"

// ConditionSidecarTLSSecretReady indicates whether the externally-
// provisioned TLS Secret referenced by spec.sidecar.tls.secretName
// is present, well-formed, and has SANs matching the required DNS
// names. Mirrors SigningKeyReady / NodeKeyReady / OperatorKeyringReady.
// Only set on SeiNodes with spec.sidecar.tls.
ConditionSidecarTLSSecretReady = "SidecarTLSSecretReady"
)

// Reasons for the ImportPVCReady condition.
Expand Down Expand Up @@ -303,6 +311,16 @@ const (
ReasonOperatorKeyringInvalid = "OperatorKeyringInvalid" // terminal: fail the plan
)

// Reasons for the SidecarTLSSecretReady condition. String values are
// prefixed to match the existing SigningKey/NodeKey/OperatorKeyring
// reason convention so SRE tooling can grep across condition types.
const (
ReasonTLSSecretReady = "TLSSecretReady" // Secret present, well-formed, SANs match required DNS names
ReasonTLSSecretNotFound = "TLSSecretNotFound" // Secret not found in the SeiNode's namespace
ReasonTLSSecretMalformed = "TLSSecretMalformed" // Wrong type, empty tls.crt/tls.key, or unparseable cert
ReasonTLSSecretSANsMismatch = "TLSSecretSANsMismatch" // Cert parses but cert.DNSNames does not include required SANs
)

// SeiNodeStatus defines the observed state of a SeiNode.
type SeiNodeStatus struct {
// Phase is the high-level lifecycle state.
Expand Down Expand Up @@ -343,6 +361,27 @@ type SeiNodeStatus struct {
// config so the node advertises a reachable address for gossip discovery.
// +optional
ExternalAddress string `json:"externalAddress,omitempty"`

// SidecarTLS is set whenever spec.sidecar.tls is non-nil. Publishes
// the contract platform tooling must satisfy when provisioning the
// TLS Secret. Machine-readable replacement for naming-convention docs.
// +optional
SidecarTLS *SidecarTLSStatus `json:"sidecarTLS,omitempty"`
}

// SidecarTLSStatus declares the controller's expectations of the
// referenced TLS Secret.
type SidecarTLSStatus struct {
// SecretName mirrors spec.sidecar.tls.secretName for visibility.
SecretName string `json:"secretName"`

// RequiredDNSNames is the SAN list the cert in SecretName must
// include. Derived from the SeiNode's headless service DNS:
// {name}.{namespace}.svc.cluster.local and
// {name}-0.{name}.{namespace}.svc.cluster.local. Pre-computable
// by platform tooling from the target spec; published here for
// verification rather than as a prerequisite handshake.
RequiredDNSNames []string `json:"requiredDNSNames"`
}

// +kubebuilder:object:root=true
Expand Down
25 changes: 25 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.

11 changes: 6 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ func main() {
//nolint:staticcheck // TODO: migrate to GetEventRecorder (new events API)
nodeRecorder := mgr.GetEventRecorderFor("seinode-controller")
if err := (&nodecontroller.SeiNodeReconciler{
Client: kc,
Scheme: mgr.GetScheme(),
Recorder: nodeRecorder,
Platform: platformCfg,
Planner: &planner.NodeResolver{BuildSidecarClient: buildSidecarClient},
Client: kc,
APIReader: mgr.GetAPIReader(),
Scheme: mgr.GetScheme(),
Recorder: nodeRecorder,
Platform: platformCfg,
Planner: &planner.NodeResolver{BuildSidecarClient: buildSidecarClient},
PlanExecutor: &planner.Executor[*seiv1alpha1.SeiNode]{
ConfigFor: func(_ context.Context, node *seiv1alpha1.SeiNode) task.ExecutionConfig {
return task.ExecutionConfig{
Expand Down
34 changes: 20 additions & 14 deletions config/crd/sei.io_seinodedeployments.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -612,24 +612,25 @@ spec:
tls:
description: |-
TLS, if set, fronts the sidecar API with kube-rbac-proxy on
:8443 backed by a cert-manager-issued cert. Init-only:
toggling on a Running SeiNode requires recreation. See seictl#165.
:8443 using TLS material from a Secret in the SeiNode's
namespace. The Secret is operator-provisioned (e.g., via a
cert-manager Certificate in the platform GitOps repo); this
controller does not create it.

Immutable post-creation. Toggling TLS on an existing SeiNode is a
delete + recreate operation; data persists via the SeiNode's PVC
retention mechanism.
properties:
issuerKind:
default: ClusterIssuer
description: IssuerKind is "Issuer" (namespaced) or
"ClusterIssuer".
enum:
- Issuer
- ClusterIssuer
type: string
issuerName:
secretName:
description: |-
IssuerName references a cert-manager Issuer or ClusterIssuer
that signs the proxy's serving certificate.
SecretName is a kubernetes.io/tls Secret in the SeiNode's
namespace. The cert SANs must include the DNS names published
in status.sidecarTLS.requiredDNSNames; the controller validates
this before allowing the pod to schedule.
minLength: 1
type: string
required:
- issuerName
- secretName
type: object
type: object
validator:
Expand Down Expand Up @@ -921,6 +922,11 @@ spec:
- message: peers is required when replayer mode is set
rule: '!has(self.replayer) || (has(self.peers) && size(self.peers)
> 0)'
- message: spec.sidecar.tls is immutable; delete + recreate the
SeiNode to change TLS configuration
rule: '(!has(oldSelf.sidecar) || !has(oldSelf.sidecar.tls))
? (!has(self.sidecar) || !has(self.sidecar.tls)) : (has(self.sidecar)
&& has(self.sidecar.tls) && self.sidecar.tls == oldSelf.sidecar.tls)'
required:
- spec
type: object
Expand Down
58 changes: 45 additions & 13 deletions config/crd/sei.io_seinodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -467,23 +467,25 @@ spec:
tls:
description: |-
TLS, if set, fronts the sidecar API with kube-rbac-proxy on
:8443 backed by a cert-manager-issued cert. Init-only:
toggling on a Running SeiNode requires recreation. See seictl#165.
:8443 using TLS material from a Secret in the SeiNode's
namespace. The Secret is operator-provisioned (e.g., via a
cert-manager Certificate in the platform GitOps repo); this
controller does not create it.

Immutable post-creation. Toggling TLS on an existing SeiNode is a
delete + recreate operation; data persists via the SeiNode's PVC
retention mechanism.
properties:
issuerKind:
default: ClusterIssuer
description: IssuerKind is "Issuer" (namespaced) or "ClusterIssuer".
enum:
- Issuer
- ClusterIssuer
type: string
issuerName:
secretName:
description: |-
IssuerName references a cert-manager Issuer or ClusterIssuer
that signs the proxy's serving certificate.
SecretName is a kubernetes.io/tls Secret in the SeiNode's
namespace. The cert SANs must include the DNS names published
in status.sidecarTLS.requiredDNSNames; the controller validates
this before allowing the pod to schedule.
minLength: 1
type: string
required:
- issuerName
- secretName
type: object
type: object
validator:
Expand Down Expand Up @@ -769,6 +771,11 @@ spec:
- message: peers is required when replayer mode is set
rule: '!has(self.replayer) || (has(self.peers) && size(self.peers) >
0)'
- message: spec.sidecar.tls is immutable; delete + recreate the SeiNode
to change TLS configuration
rule: '(!has(oldSelf.sidecar) || !has(oldSelf.sidecar.tls)) ? (!has(self.sidecar)
|| !has(self.sidecar.tls)) : (has(self.sidecar) && has(self.sidecar.tls)
&& self.sidecar.tls == oldSelf.sidecar.tls)'
status:
description: SeiNodeStatus defines the observed state of a SeiNode.
properties:
Expand Down Expand Up @@ -996,6 +1003,31 @@ spec:
items:
type: string
type: array
sidecarTLS:
description: |-
SidecarTLS is set whenever spec.sidecar.tls is non-nil. Publishes
the contract platform tooling must satisfy when provisioning the
TLS Secret. Machine-readable replacement for naming-convention docs.
properties:
requiredDNSNames:
description: |-
RequiredDNSNames is the SAN list the cert in SecretName must
include. Derived from the SeiNode's headless service DNS:
{name}.{namespace}.svc.cluster.local and
{name}-0.{name}.{namespace}.svc.cluster.local. Pre-computable
by platform tooling from the target spec; published here for
verification rather than as a prerequisite handshake.
items:
type: string
type: array
secretName:
description: SecretName mirrors spec.sidecar.tls.secretName for
visibility.
type: string
required:
- requiredDNSNames
- secretName
type: object
type: object
type: object
served: true
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/node/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ type PlatformConfig = platform.Config
// SeiNodeReconciler reconciles a SeiNode object.
type SeiNodeReconciler struct {
client.Client
// APIReader bypasses the controller-runtime cache. Used by preflight
// validation that must observe newly-provisioned Secrets without
// waiting for cache propagation.
APIReader client.Reader
Scheme *runtime.Scheme
Recorder record.EventRecorder
Platform PlatformConfig
Expand Down Expand Up @@ -105,6 +109,8 @@ func (r *SeiNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, fmt.Errorf("reconciling peers: %w", err)
}

r.reconcileSidecarTLSReady(ctx, node)

planAlreadyActive := node.Status.Plan != nil && node.Status.Plan.Phase == seiv1alpha1.TaskPlanActive
if err := r.Planner.ResolvePlan(ctx, node); err != nil {
return ctrl.Result{}, fmt.Errorf("resolving plan: %w", err)
Expand Down
20 changes: 11 additions & 9 deletions internal/controller/node/plan_execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,11 @@ func newProgressionReconciler(t *testing.T, mock *mockSidecarClient, objs ...cli
WithStatusSubresource(&seiv1alpha1.SeiNode{}).
Build()
r := &SeiNodeReconciler{
Client: c,
Scheme: s,
Recorder: record.NewFakeRecorder(100),
Platform: platformtest.Config(),
Client: c,
APIReader: c,
Scheme: s,
Recorder: record.NewFakeRecorder(100),
Platform: platformtest.Config(),
Planner: &planner.NodeResolver{
BuildSidecarClient: func(_ *seiv1alpha1.SeiNode) (task.SidecarClient, error) { return mock, nil },
},
Expand Down Expand Up @@ -785,11 +786,12 @@ func TestReconcileInitializing_SidecarClientError_Requeues(t *testing.T) {
Build()

r := &SeiNodeReconciler{
Client: c,
Scheme: s,
Recorder: record.NewFakeRecorder(100),
Platform: platformtest.Config(),
Planner: &planner.NodeResolver{},
Client: c,
APIReader: c,
Scheme: s,
Recorder: record.NewFakeRecorder(100),
Platform: platformtest.Config(),
Planner: &planner.NodeResolver{},
PlanExecutor: &planner.Executor[*seiv1alpha1.SeiNode]{
ConfigFor: func(_ context.Context, n *seiv1alpha1.SeiNode) task.ExecutionConfig {
return task.ExecutionConfig{
Expand Down
Loading
Loading