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
50 changes: 25 additions & 25 deletions cmd/sandbox-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ import (

var setupLog = ctrl.Log.WithName("setup")

func main() {
var o options
o.bindFlags()
flag.Parse()

if o.printVersion {
fmt.Println(version.Print("sandbox-operator"))
return
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&o.zap)))
if err := o.run(); err != nil {
setupLog.Error(err, "sandbox-operator exited")
os.Exit(1)
}
}

// options holds every operator flag. It is one struct so main stays a
// sequence of named phases rather than a 400-line body.
type options struct {
Expand Down Expand Up @@ -155,22 +171,6 @@ func (o *options) totalWorkers() int {
return o.sandboxWorkers + o.claimWorkers + o.warmPoolWorkers + o.templateWorkers
}

func main() {
var o options
o.bindFlags()
flag.Parse()

if o.printVersion {
fmt.Println(version.Print("sandbox-operator"))
return
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&o.zap)))
if err := o.run(); err != nil {
setupLog.Error(err, "sandbox-operator exited")
os.Exit(1)
}
}

func (o *options) run() error {
if err := o.validate(); err != nil {
return err
Expand Down Expand Up @@ -269,15 +269,6 @@ func (o *options) logSettings() {
}
}

func setupTracing(ctx context.Context, enabled bool) (asmetrics.Instrumenter, func(), error) {
if !enabled {
return asmetrics.NewNoOp(), func() {}, nil
}
initCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return asmetrics.SetupOTel(initCtx, "sandbox-operator")
}

func (o *options) pprofHandlers() map[string]http.Handler {
if !o.enablePprof && !o.enablePprofDebug {
return nil
Expand Down Expand Up @@ -407,6 +398,15 @@ func (o *options) setupExtensionControllers(mgr ctrl.Manager, instrumenter asmet
return nil
}

func setupTracing(ctx context.Context, enabled bool) (asmetrics.Instrumenter, func(), error) {
if !enabled {
return asmetrics.NewNoOp(), func() {}, nil
}
initCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
return asmetrics.SetupOTel(initCtx, "sandbox-operator")
}

// readAllowedLabelDomains reads the optional label-domain allowlist mounted by
// the deployment; a missing file leaves the allowlist empty.
func readAllowedLabelDomains() ([]string, error) {
Expand Down
114 changes: 57 additions & 57 deletions controllers/sandbox_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,6 @@ var Scheme = func() *runtime.Scheme {
// resourceOwnership represents the ownership state of a Kubernetes resource relative to a Sandbox.
type resourceOwnership int

// SandboxReconciler reconciles a Sandbox object.
type SandboxReconciler struct {
client.Client
Scheme *runtime.Scheme
Tracer asmetrics.Instrumenter
ClusterDomain string
PodMutator PodMutator
}

// PodMutator applies runtime-specific defaults to a newly-created Sandbox Pod.
// Implementations must preserve every field explicitly supplied through the
// Sandbox API and return an error instead of silently overriding conflicts.
Expand All @@ -103,6 +94,15 @@ type PodMutator interface {
//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;list;watch;create;update;patch
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;update;patch,resourceNames=sandboxes.agents.x-k8s.io;sandboxclaims.extensions.agents.x-k8s.io;sandboxtemplates.extensions.agents.x-k8s.io;sandboxwarmpools.extensions.agents.x-k8s.io

// SandboxReconciler reconciles a Sandbox object.
type SandboxReconciler struct {
client.Client
Scheme *runtime.Scheme
Tracer asmetrics.Instrumenter
ClusterDomain string
PodMutator PodMutator
}

// Reconcile drives a Sandbox toward its declared spec: child Service/Pod/PVCs,
// status conditions, and expiry.
func (r *SandboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
Expand Down Expand Up @@ -1022,6 +1022,54 @@ func (r *SandboxReconciler) deleteExpiredChild(ctx context.Context, sandbox *san
return nil
}

// checkOwnership determines whether a Kubernetes resource is owned by the given Sandbox,
// has no controller, or is owned by a different controller.
// It returns both the ownership classification and the controller reference (if any),
// so callers can log owner details without redundant GetControllerOf calls.
func checkOwnership(obj client.Object, sandbox *sandboxv1beta1.Sandbox) (resourceOwnership, *metav1.OwnerReference) {
controllerRef := metav1.GetControllerOf(obj)
if controllerRef == nil {
return resourceUnowned, nil
}
if controllerRef.UID == sandbox.UID {
return resourceOwnedBySandbox, controllerRef
}
return resourceOwnedByOther, controllerRef
}

// MergeVolumeClaimVolumes merges PVC-backed volumes into an existing volume
// list, replacing any volumes with matching names. This follows StatefulSet
// semantics where volumeClaimTemplate volumes take priority.
func MergeVolumeClaimVolumes(existing []corev1.Volume, pvcVolumes []corev1.Volume) []corev1.Volume {
if len(pvcVolumes) == 0 {
return existing
}
vctNames := make(map[string]struct{}, len(pvcVolumes))
for _, v := range pvcVolumes {
vctNames[v.Name] = struct{}{}
}
filtered := make([]corev1.Volume, 0, len(existing))
for _, v := range existing {
if _, ok := vctNames[v.Name]; !ok {
filtered = append(filtered, v)
}
}
return append(filtered, pvcVolumes...)
}

// GetNumericHash generates a raw FNV-1a hash value.
func GetNumericHash(input string) uint32 {
h := fnv.New32a()
_, _ = h.Write([]byte(input))
return h.Sum32()
}

// NameHash generates an FNV-1a hash from a string and returns
// it as a fixed-length hexadecimal string.
func NameHash(objectName string) string {
return fmt.Sprintf("%08x", GetNumericHash(objectName))
}

// checks if the sandbox has expired
// returns true if expired, false otherwise
// if not expired, also returns the duration to requeue after.
Expand Down Expand Up @@ -1090,21 +1138,6 @@ func podReadiness(pod *corev1.Pod) (message string, ready bool) {
return "Pod is Ready", true
}

// checkOwnership determines whether a Kubernetes resource is owned by the given Sandbox,
// has no controller, or is owned by a different controller.
// It returns both the ownership classification and the controller reference (if any),
// so callers can log owner details without redundant GetControllerOf calls.
func checkOwnership(obj client.Object, sandbox *sandboxv1beta1.Sandbox) (resourceOwnership, *metav1.OwnerReference) {
controllerRef := metav1.GetControllerOf(obj)
if controllerRef == nil {
return resourceUnowned, nil
}
if controllerRef.UID == sandbox.UID {
return resourceOwnedBySandbox, controllerRef
}
return resourceOwnedByOther, controllerRef
}

// isAdoptable reports whether obj carries the warm-pool adoptable label.
func isAdoptable(obj client.Object) bool {
return obj.GetLabels()[sandboxv1beta1.SandboxAdoptableLabel] == "true"
Expand All @@ -1120,26 +1153,6 @@ func resolvePodName(sandbox *sandboxv1beta1.Sandbox) string {
return sandbox.Name
}

// MergeVolumeClaimVolumes merges PVC-backed volumes into an existing volume
// list, replacing any volumes with matching names. This follows StatefulSet
// semantics where volumeClaimTemplate volumes take priority.
func MergeVolumeClaimVolumes(existing []corev1.Volume, pvcVolumes []corev1.Volume) []corev1.Volume {
if len(pvcVolumes) == 0 {
return existing
}
vctNames := make(map[string]struct{}, len(pvcVolumes))
for _, v := range pvcVolumes {
vctNames[v.Name] = struct{}{}
}
filtered := make([]corev1.Volume, 0, len(existing))
for _, v := range existing {
if _, ok := vctNames[v.Name]; !ok {
filtered = append(filtered, v)
}
}
return append(filtered, pvcVolumes...)
}

// podIPsFromStatus converts the K8s PodIP slice to a plain string slice.
func podIPsFromStatus(podIPs []corev1.PodIP) []string {
if len(podIPs) == 0 {
Expand All @@ -1152,19 +1165,6 @@ func podIPsFromStatus(podIPs []corev1.PodIP) []string {
return ips
}

// GetNumericHash generates a raw FNV-1a hash value.
func GetNumericHash(input string) uint32 {
h := fnv.New32a()
_, _ = h.Write([]byte(input))
return h.Sum32()
}

// NameHash generates an FNV-1a hash from a string and returns
// it as a fixed-length hexadecimal string.
func NameHash(objectName string) string {
return fmt.Sprintf("%08x", GetNumericHash(objectName))
}

// hasSystemReservedPrefix reports whether a key uses a label/annotation prefix
// reserved for the sandbox system or its extensions.
func hasSystemReservedPrefix(key string) bool {
Expand Down
58 changes: 29 additions & 29 deletions extensions/controllers/sandboxclaim_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ type triggeredAdoptionEntry struct {
sandbox string
}

// failure is the reason/message pair behind a not-Ready claim.
type failure struct {
reason string
message string
}

// SandboxClaimReconciler reconciles a SandboxClaim object.
type SandboxClaimReconciler struct {
client.Client
Expand Down Expand Up @@ -560,12 +566,6 @@ func (r *SandboxClaimReconciler) updateStatus(ctx context.Context, oldStatus *ex
return nil
}

// failure is the reason/message pair behind a not-Ready claim.
type failure struct {
reason string
message string
}

func (r *SandboxClaimReconciler) computeReadyCondition(claim *extensionsv1beta1.SandboxClaim, sandbox *v1beta1.Sandbox, err error, isClaimExpired bool) metav1.Condition {
if err != nil {
return notReady(claim, readyFailure(claim, err))
Expand Down Expand Up @@ -1694,6 +1694,29 @@ func (h *sandboxEventHandler) Generic(_ context.Context, _ event.GenericEvent, _
// Generic events are not typically used for pod lifecycle changes we care about.
}

func (h *sandboxEventHandler) Delete(ctx context.Context, e event.DeleteEvent, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
sandbox, ok := e.Object.(*v1beta1.Sandbox)
if !ok {
return
}

warmPoolName := getWarmPoolName(sandbox)

if warmPoolName != "" {
key := queue.SandboxKey{
Namespace: sandbox.Namespace,
Name: sandbox.Name,
}

namespacedWarmPoolName := queue.GetNamespacedWarmPoolName(sandbox.Namespace, warmPoolName)

// Actively delete the Ghost Pod from the memory queue
logger := log.FromContext(ctx)
logger.V(1).Info("Removing deleted sandbox from warm pool queue", "namespace", sandbox.Namespace, "sandbox", key)
h.sandboxQueue.RemoveItem(namespacedWarmPoolName, key)
}
}

func verifySandboxCandidate(candidate *v1beta1.Sandbox, claim *extensionsv1beta1.SandboxClaim) error {
if candidate.Namespace != claim.Namespace {
return fmt.Errorf("%w: sandbox is in %q, claim is in %q", ErrCrossNamespaceAdoption, candidate.Namespace, claim.Namespace)
Expand Down Expand Up @@ -1731,29 +1754,6 @@ func isAdoptable(candidate *v1beta1.Sandbox) error {
return nil
}

func (h *sandboxEventHandler) Delete(ctx context.Context, e event.DeleteEvent, _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
sandbox, ok := e.Object.(*v1beta1.Sandbox)
if !ok {
return
}

warmPoolName := getWarmPoolName(sandbox)

if warmPoolName != "" {
key := queue.SandboxKey{
Namespace: sandbox.Namespace,
Name: sandbox.Name,
}

namespacedWarmPoolName := queue.GetNamespacedWarmPoolName(sandbox.Namespace, warmPoolName)

// Actively delete the Ghost Pod from the memory queue
logger := log.FromContext(ctx)
logger.V(1).Info("Removing deleted sandbox from warm pool queue", "namespace", sandbox.Namespace, "sandbox", key)
h.sandboxQueue.RemoveItem(namespacedWarmPoolName, key)
}
}

type warmPoolEventHandler struct {
sandboxQueue *queue.SimpleSandboxQueue
}
Expand Down
22 changes: 11 additions & 11 deletions pkg/e2bcompat/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,17 +306,6 @@ func (s *Server) nodesWithSandboxes(r *http.Request) ([]string, error) {
return s.opts.Inventory.ListNodes(r.Context())
}

// decodeOptional decodes a JSON body that the schema allows to be absent. An
// empty body leaves the target at its zero value rather than failing, which is
// what pause and fork require.
func decodeOptional(w http.ResponseWriter, r *http.Request, out any) error {
err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(out)
if errors.Is(err, io.EOF) {
return nil
}
return err
}

// isPaused reports whether the sandbox is hibernated, asking its owning node
// rather than trusting the synthesized read view.
//
Expand All @@ -339,6 +328,17 @@ func (s *Server) isPaused(ctx context.Context, sb *sandboxv1beta1.Sandbox) bool
return sb.Labels[scale.PhaseLabel] == phaseHibernated
}

// decodeOptional decodes a JSON body that the schema allows to be absent. An
// empty body leaves the target at its zero value rather than failing, which is
// what pause and fork require.
func decodeOptional(w http.ResponseWriter, r *http.Request, out any) error {
err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBodyBytes)).Decode(out)
if errors.Is(err, io.EOF) {
return nil
}
return err
}

// claimIDOf reports the node-local claim id the store's verbs address.
func claimIDOf(sb *sandboxv1beta1.Sandbox) string {
return sb.Annotations[scale.ClaimIDAnnotation]
Expand Down
Loading