Skip to content
Merged
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
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ foreman-chart-crds: manifests ## Sync foreman.llmkube.dev CRDs to the foreman ch
synced=$$((synced+1)); \
done; echo "Synced $$synced foreman CRD(s)"

.PHONY: federation-chart-crds
federation-chart-crds: manifests ## Sync federation.llmkube.dev CRDs into the main llmkube chart.
# Federation runs IN the main llmkube operator (config-gated by --federation-role),
# not a separate operator, so its CRD ships in the main chart alongside the
# inference CRDs. Mirrors scripts/sync-crds.sh: same destination and the same
# crds.install + crds.keep resource-policy wrapping the inference CRDs get.
@mkdir -p charts/llmkube/templates/crds
@synced=0; for src in config/crd/bases/federation.llmkube.dev_*.yaml; do \
[ -e "$$src" ] || { echo "no federation CRDs in config/crd/bases (did make manifests run?)"; exit 1; }; \
base=$$(basename $$src); short=$${base#federation.llmkube.dev_}; \
echo "Syncing $$base -> $$short"; \
{ echo '{{- if .Values.crds.install }}'; \
awk '/controller-gen.kubebuilder.io\/version:/{print; print " {{- if .Values.crds.keep }}"; print " helm.sh/resource-policy: keep"; print " {{- end }}"; next}1' "$$src"; \
echo '{{- end }}'; \
} > charts/llmkube/templates/crds/$$short; \
synced=$$((synced+1)); \
done; echo "Synced $$synced federation CRD(s)"

.PHONY: check-helm-rbac
check-helm-rbac: manifests ## Verify the Helm charts' RBAC covers every kubebuilder-generated rule (#379).
@./scripts/check-helm-rbac.sh
Expand Down
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,13 @@ resources:
kind: InferenceService
path: github.com/defilantech/llmkube/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: false
controller: true
domain: llmkube.dev
group: federation
kind: FederatedCluster
path: github.com/defilantech/llmkube/api/federation/v1alpha1
version: v1alpha1
version: "3"
127 changes: 127 additions & 0 deletions api/federation/v1alpha1/federatedcluster_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// Phase values for FederatedClusterStatus.Phase. Datacenter-owned: computed
// by the datacenter controller from LastHeartbeatTime staleness relative to
// a multiple of HeartbeatIntervalSeconds.
const (
// FederatedClusterConnected means a heartbeat was received within the
// expected interval.
FederatedClusterConnected = "Connected"

// FederatedClusterStale means no heartbeat was received for 3x the
// expected interval.
FederatedClusterStale = "Stale"

// FederatedClusterUnreachable means no heartbeat was received for 10x
// the expected interval.
FederatedClusterUnreachable = "Unreachable"
)

// FederatedClusterSpec is admin-owned. displayName and dataResidencyTier are
// set at registration; the edge and the datacenter controller only write status.
type FederatedClusterSpec struct {
// DisplayName is a human label for the site.
// +optional
DisplayName string `json:"displayName,omitempty"`

// DataResidencyTier is recorded now and enforced later by the federation
// router (#1237). It is a free-form tier label (for example "eu", "floor-3").
// +optional
DataResidencyTier string `json:"dataResidencyTier,omitempty"`

// HeartbeatIntervalSeconds is how often the edge is expected to push status.
// The datacenter derives staleness thresholds from it (3x Stale, 10x Unreachable).
// +kubebuilder:default=30
// +kubebuilder:validation:Minimum=5
HeartbeatIntervalSeconds int32 `json:"heartbeatIntervalSeconds,omitempty"`
}

// ClusterCapacity is edge-written node/GPU capacity.
type ClusterCapacity struct {
Nodes int32 `json:"nodes"`
GPUsTotal int32 `json:"gpusTotal"`
GPUsAllocatable int32 `json:"gpusAllocatable"`
}

// ClusterInferenceSummary is edge-written inference health.
type ClusterInferenceSummary struct {
ServicesReady int32 `json:"servicesReady"`
ServicesFailed int32 `json:"servicesFailed"`
ServicesTotal int32 `json:"servicesTotal"`
Models int32 `json:"models"`
}

// FederatedClusterStatus has two writers. The edge writes everything except
// Phase, over an RBAC-scoped status-subresource client. The datacenter
// controller writes ONLY Phase, from LastHeartbeatTime staleness.
type FederatedClusterStatus struct {
// Phase is datacenter-owned: Connected, Stale, or Unreachable.
// +optional
Phase string `json:"phase,omitempty"`

// LastHeartbeatTime is edge-owned: set to now on every successful push.
// +optional
LastHeartbeatTime *metav1.Time `json:"lastHeartbeatTime,omitempty"`

// ObservedVersion is edge-owned: the LLMKube/operator version on the site.
// +optional
ObservedVersion string `json:"observedVersion,omitempty"`

// Capacity is edge-owned node/GPU capacity.
// +optional
Capacity *ClusterCapacity `json:"capacity,omitempty"`

// Inference is edge-owned inference health.
// +optional
Inference *ClusterInferenceSummary `json:"inference,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,shortName=fedcluster;fc
// +kubebuilder:printcolumn:name="Tier",type=string,JSONPath=`.spec.dataResidencyTier`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Last Heartbeat",type=date,JSONPath=`.status.lastHeartbeatTime`
// +kubebuilder:printcolumn:name="GPUs",type=string,JSONPath=`.status.capacity.gpusAllocatable`

// FederatedCluster registers one edge site on the datacenter cluster.
type FederatedCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec FederatedClusterSpec `json:"spec,omitempty"`
Status FederatedClusterStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// FederatedClusterList contains a list of FederatedCluster.
type FederatedClusterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []FederatedCluster `json:"items"`
}

func init() {
SchemeBuilder.Register(&FederatedCluster{}, &FederatedClusterList{})
}
47 changes: 47 additions & 0 deletions api/federation/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package v1alpha1 contains API Schema definitions for the federation
// v1alpha1 API group. Federation registers remote edge-site clusters on a
// datacenter cluster (FederatedCluster) so fleet-wide inference status and
// routing can be reasoned about from one place. Installing LLMKube alone
// does not install or require any of these types.
//
// +kubebuilder:object:generate=true
// +groupName=federation.llmkube.dev
package v1alpha1

import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "federation.llmkube.dev", Version: "v1alpha1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
// scheme.Builder was deprecated in controller-runtime v0.24 (the rationale
// is that api packages should have minimal deps, and scheme.Builder pulls
// controller-runtime into api/). It still works; migrating to the
// runtime.NewSchemeBuilder pattern is a project-wide refactor that touches
// both API groups and is tracked as its own follow-up. Suppress the
// deprecation here only.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} //nolint:staticcheck // SA1019: see comment above

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
158 changes: 158 additions & 0 deletions api/federation/v1alpha1/zz_generated.deepcopy.go

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

16 changes: 16 additions & 0 deletions charts/llmkube/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,22 @@ rules:
- get
- list
- watch
- apiGroups:
- federation.llmkube.dev
resources:
- federatedclusters
verbs:
- get
- list
- watch
- apiGroups:
- federation.llmkube.dev
resources:
- federatedclusters/status
verbs:
- get
- patch
- update
- apiGroups:
- ""
resources:
Expand Down
Loading