Help us shape the future of Go! Take the 2026 Go Developer Survey

kubesweep

package
v0.32.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MPL-2.0 Imports: 34 Imported by: 0

Documentation

Overview

Package kubesweep is the Kubernetes estate sweep (GitHub issue #1065, under #1016's ruling): which live objects carry this estate's label, so that one whose block the configuration no longer declares can be proposed for removal, exactly as the AWS tagging sweep does for a tagged resource.

What a sweep is here

Kubernetes has no cross-kind label-filtered list, so the AWS shape - one GetResources call over the whole account - does not survive. What does survive is nearly as good: every kind supports a cluster-wide, label-selected list (GET /api/v1/configmaps?labelSelector=..., across every namespace at once), which returns only this estate's objects and never grows with the cluster. A sweep is therefore one list per kind, not one per kind per namespace as #1016 first estimated, plus the two discovery calls (/api, /apis) that say which kinds the cluster serves.

Which kinds

Every kind the cluster serves with list and delete verbs. A kind the provider has a resource type for is joined by kind name: hashicorp/kubernetes names its types kubernetes_<snake_kind> with an optional API-version suffix (kubernetes_config_map and kubernetes_config_map_v1 both manage a ConfigMap; kubernetes_ingress managed the retired extensions/v1beta1 Ingress and kubernetes_ingress_v1 the current one), so KindOfType recovers the kind from the type name and TypeFor picks, for a listed object, the type the configuration declares for that kind when it declares one, else the versioned name when there is one (it targets the API the cluster serves today), else the plain one. Every other served kind - every CRD, and the built-in kinds the provider never gave a type - is listed under the manifest type (GitHub issue #1079's third unit): the provider's one type that manages any kind the cluster serves, admitted by schema shape (markers.ManifestSurface) and named to Sweeper.Kinds by the caller that read the schema, so that no type name is spelled out here. kubernetes_manifest imports by apiVersion, kind, namespace and name (ManifestImportID) and destroys through the live object it read, so an orphan of such a kind can be acted on exactly as one of a built-in kind can. Before #1079 those kinds were not listed at all, on the reasoning that what cannot be destroyed through the provider should not be proposed; once kubernetes_manifest is in the type universe that reasoning points the other way.

What is excluded, and why it is the whole safety of this

A controller copies template labels: a Deployment's pod-template labels reach its ReplicaSets and Pods, a StatefulSet's volumeClaimTemplate labels reach its PVCs. A tofu-estate label an author put in a template therefore lands on objects nobody declared, and every one of them would be an orphan and a delete candidate - a wrong marker nobody wrote, which HANDOFF's safety rule names as worse than a refusal. ControllerMade is the test that keeps them out and Client.List never returns an object it judges, and it does not rest on metadata.ownerReferences alone. This comment said it did until GitHub issue #1179, and the PVC half of the sentence above is exactly where that was false: a StatefulSet's volumeClaimTemplate PVCs carry no owner reference, because the default persistentVolumeClaimRetentionPolicy is Retain and they are built to outlive the StatefulSet. What excludes them is the second signal, the managers that wrote the object's own content. The server-created singletons (the default ServiceAccount, kube-root-ca.crt) carry no estate label and are never selected.

Index

Constants

View Source
const (
	HelmReleaseNameAnnotation      = "meta.helm.sh/release-name"
	HelmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace"
)

The annotations Helm 3 writes on every object a release installs (since Helm 3.2, and what `helm install` checks before adopting an existing object). `helm template` does not write them, so a chart rendered into kubernetes_manifest blocks carries neither.

View Source
const (
	SweepQPS   = 50
	SweepBurst = 300
)

SweepQPS and SweepBurst are the client-side rate limits every client built from RestConfig or New runs under, kubectl's own for discovery (its ConfigFlags set QPS 50, burst 300).

Left at zero, client-go gives each client its own 5 QPS, burst 10 token bucket. A sweep is a List per kind the cluster serves, about 60 on a bare kind cluster and a hundred once a chart's CRDs are in, all against one API server in one burst, so under those defaults it spent 18 of its 18 seconds waiting on its own limiter, every plan and every apply. The server's own API Priority and Fairness is what protects it from load; these bound a runaway loop, not a sweep.

View Source
const AddressAnnotation = "choudoufu.intentius.io/tofu-address"

AddressAnnotation is the annotation a stamped object carries its block's escaped address in, markers.AddressAnnotation (GitHub issue #1639). Spelled out here for the reason RecordStoreObject spells the record store's strings out: this package does not import markers, and internal/live/discovery's kubernetes_test.go pins the two equal.

View Source
const ControllerHelm = "Helm"

ControllerHelm is HeldObject.Controller for an object a Helm release holds. It is the Kubernetes counterpart of markers.ControllerACK and markers.ControllerCrossplane: one "controller" word across substrates.

View Source
const DefaultFieldManager = "Terraform"

DefaultFieldManager is the field manager hashicorp/kubernetes writes a kubernetes_manifest object under when the block sets no `field_manager { name = ... }`. See this file's doc comment for how it was established.

View Source
const TypePrefix = "kubernetes_"

TypePrefix is what every hashicorp/kubernetes resource type name begins with.

Variables

View Source
var AfterMarkerPatch func(ref ObjectRef)

AfterMarkerPatch, when not nil, is called by Client.PatchMarkers between its two requests: after the merge patch writing the markers has landed and before the request handing their ownership to the field manager's Apply entry. That gap is the window GitHub issue #1858's rerun recovers. Nothing in an ordinary build sets it; an e2eTestingFeatures build of live-mv does, from TOFU_E2E_LIVE_MV_INTERRUPT (internal/command/live_mv_e2etesting.go), so that an estate script can kill a real move inside the window (GitHub issue #1883).

View Source
var FieldGranularMapAttrs = []string{"labels", "annotations", "template_annotations", "data"}

FieldGranularMapAttrs are the map attributes a field-granular type writes into its object, in the order the boundary reads them, each with the FieldsV1 path to the map it writes (FieldGranularMapRoot).

View Source
var TaintsRoot = []string{"f:spec", "f:taints"}

TaintsRoot is the FieldsV1 path to a Node's taints.

Functions

func AtomicReleased

func AtomicReleased(obj *unstructured.Unstructured, root []string) bool

AtomicReleased reports whether obj's value at root - a FieldsV1 path of "f:" steps - is absent or empty. For an atomic list that is how a released manager's leaf is told from a held one (#1917): managedFields records both as the same memberless leaf, but an apply of the empty list leaves the list empty, and any later write of a member by another manager - an Update, or a forced Apply - takes the leaf away from it. So a manager still holding the leaf over a non-empty list wrote that list, and one holding it over an empty one owns nothing. False for a path with any other kind of step, which this cannot read.

func ChangedOutsideMarkers

func ChangedOutsideMarkers(live, planned map[string]any, annotationKey string) []string

ChangedOutsideMarkers names the paths at which planned - the object the server answered a dry run of a marker patch with - differs from live, the object it holds now, outside the labels map, the one annotation annotationKey names (the block address, GitHub issue #1639) and the server's own bookkeeping. It is how a marker patch is judged: live-import's adoption of a manifest-declared object and live-mv's rename of one both refuse a write whose dry run reports anything here, because something between the client and the store (a mutating admission webhook, most likely) rewrote more than was asked for.

A difference is reported at the shallowest path where the two disagree, so a rewritten spec reads "spec.replicas" and not one line per leaf beneath it.

func ControllerMade

func ControllerMade(obj *unstructured.Unstructured) bool

ControllerMade reports whether a live object was made by a controller rather than declared by anyone, on four signals, any one sufficient:

  • Helm's release annotation, meta.helm.sh/release-name (HelmRelease; GitHub issue #1607, ruled on #1604 and #1105). Helm writes it on every object it installs, and those objects are the release's: a chart value carrying tofu-estate puts the estate's label on them, and without this signal the sweep proposed destroying them from under the release, because helm, not a control-plane manager, wrote their content and nothing owns them. helm_release itself stays refused; this only keeps the release's objects out of the sweep. ControllerMade reads the annotation alone and cannot see whether the release still exists - Client.List is the one that can, and it checks before trusting this signal (GitHub issue #1625): moving an object off Helm without re-creating it leaves the annotation in place, because server-side apply only touches fields its own writer claims, so a stale annotation must not keep naming a holder that is gone. A caller with no cluster to ask - a unit test, say - gets the annotation-only answer this function has always given.
  • a non-empty metadata.ownerReferences. This catches the objects a garbage-collected controller makes (a ReplicaSet's from its Deployment, a Pod's from its ReplicaSet, an EndpointSlice's from its Service). It does NOT catch everything a controller creates from a template, which this comment asserted until GitHub issue #1179: a StatefulSet's volumeClaimTemplate PVCs carry no owner reference at all under the default persistentVolumeClaimRetentionPolicy of Retain, because they are meant to outlive the StatefulSet.
  • metadata.managedFields whose content-writing entries are all control-plane managers. An object's content - everything outside metadata and outside the status subresource - is written into being by whoever created it, so the manager that owns f:spec (f:data for a ConfigMap, f:subsets for an Endpoints) is the object's author. A PVC a StatefulSet made has kube-controller-manager owning f:spec however many other managers have since touched its labels, which is what #1179 needs and what "every manager is a control-plane manager" below cannot give: one kubectl label on a controller-made PVC removes that object from the exclusion for good, and the sweep then proposes destroying a bound volume.
  • failing that, every managedFields entry naming a control-plane manager, for an object whose managers claim nothing but metadata - a ServiceAccount, say. This is the signal #1065 added for the legacy core/v1 Endpoints the endpoints controller mirrors a Service's labels onto.

The second signal can only widen the exclusion over the third, never narrow it: an object every one of whose managers is control-plane has only control-plane content writers too. So a change here cannot turn an excluded object into a delete candidate, only the other way, and the cost of being wrong is an orphan the sweep declines to propose rather than a live object it offers to destroy.

An object with no managedFields at all (a cluster older than the field manager mechanism, or a client that strips them) is judged on owner references alone. Note that kubectl's own get strips managedFields from its output unless --show-managed-fields is passed, which is how #1179 came to record a volumeClaimTemplate PVC as carrying none; the API server has them and so does the dynamic client Client.List reads through.

func EnvContainers

func EnvContainers(raw []byte, kind string) (names, initNames []string)

EnvContainers reads, from one manager's FieldsV1 document on an object of kind, the containers whose env that manager owns any of: names under spec.containers, and initNames under spec.initContainers.

func EnvRoot

func EnvRoot(kind, container string, init bool) []string

EnvRoot is the FieldsV1 path to one container's env: the container (or, with init, init container) named container, under kind's pod spec.

func FieldGranularFixedKind

func FieldGranularFixedKind(typeName string) (apiVersion, kind string)

FieldGranularFixedKind is the object a field-granular type that names no kind patches. hashicorp/kubernetes hardcodes it per type, and names each such type after the patched kind's own type plus the field it writes: kubernetes_config_map_v1_data patches what kubernetes_config_map_v1 manages, kubernetes_node_taint what kubernetes_node would. So the last segment is dropped and the kind is KindOfType's, the same join the sweep makes. Every kind reached this way at 3.2.1 (ConfigMap, Secret, Node) is in the core group, whose API version is "v1".

func FieldGranularMapRoot

func FieldGranularMapRoot(attr string) (root []string, ok bool)

FieldGranularMapRoot is the FieldsV1 path to the map attr writes: metadata.labels, metadata.annotations, the pod template's annotations, or the object's data. ok is false for any other attribute.

func KindOfType

func KindOfType(typeName string) (kind string, versioned bool, ok bool)

KindOfType recovers the Kubernetes kind a provider resource type manages from its name: kubernetes_config_map_v1 is ConfigMap, versioned true. A name outside the provider's prefix reports ok false.

func KindTypes

func KindTypes(typeNames []string) map[string][]string

KindTypes groups provider resource type names by the kind they manage: ConfigMap -> [kubernetes_config_map, kubernetes_config_map_v1]. Names outside the provider's prefix are ignored.

func ListItemMember

func ListItemMember(keys map[string]string) string

ListItemMember is the FieldsV1 spelling of an associative-list item with the given key fields, e.g. k:{"name":"app"}. encoding/json sorts a map's keys, which is the order the API server writes a multi-field key in (sigs.k8s.io/structured-merge-diff's PathElement serialisation sorts by field name).

func ManagedMetadataKeys

func ManagedMetadataKeys(obj *unstructured.Unstructured, fieldManager string, fields []string) (keys map[string]map[string]bool, present bool)

ManagedMetadataKeys reports which keys of each named metadata map fieldManager owns on obj, read from the object's own metadata.managedFields.

fields names the metadata maps to answer for - "labels" and "annotations", as [markers.ManifestComputedMetadataAttrs] spells them. They are passed in rather than hardcoded so that the map this returns is keyed by exactly the names the caller loops over; a caller and a callee that spell the same field differently is a defect this signature cannot have. Every named field gets an entry, empty when the manager owns nothing there.

present reports whether obj carries managedFields AT ALL, which is the distinction a caller has to act on:

  • present, with an empty set for a field: this manager owns nothing there. That is an exact answer, not a failure - an object adopted from another tool, or one this fork has never applied, genuinely has no keys of ours to remove.
  • not present: the object carries no managedFields entries whatsoever (a cluster predating the mechanism, or a client that stripped them on the way through). Nothing can be concluded, and a caller that treats it as "owns nothing" is silently back to the wrong answer.

fieldManager defaults to DefaultFieldManager when empty. Entries for a subresource are skipped, as ControllerMade skips them, because a status writer is not a writer of the object's own metadata; entries for every operation are unioned, because both an Apply and an Update entry under our manager's name record fields our manager wrote.

func ManagerFields

func ManagerFields(entries []metav1.ManagedFieldsEntry, manager string) (raw []byte, ok bool, err error)

ManagerFields is the union of manager's Apply entries outside a subresource, as one FieldsV1 document; ok is false when manager has none or they own nothing. An Update entry under the same name is not counted: the provider writes these types by server-side apply only, and an Update entry's fields are not ones an Apply under the name can release.

func ManifestImportID

func ManifestImportID(apiVersion, kind, namespace, name string) string

ManifestImportID renders the provider's documented import id for a kubernetes_manifest object, the namespace segment present for a namespaced kind only: the same string internal/live/identity renders for a declared block, so a listed object and a resolution meet on it.

func ManifestOrphanResourceName

func ManifestOrphanResourceName(kind, namespace, name string) string

ManifestOrphanResourceName is OrphanResourceName for an object listed under the manifest type: orphan_<kind>_<namespace>_<name>, the kind folded to lower case, because that type files every kind under one name and two kinds can each have an object of the same namespace and name.

func MapMember

func MapMember(key string) string

MapMember is the FieldsV1 spelling of a map key.

func MarkersHeldByUpdate

func MarkersHeldByUpdate(obj *unstructured.Unstructured, fieldManager string, labels, annotations []string) (bool, error)

MarkersHeldByUpdate reports whether fieldManager's Update entry on obj still owns any of the given marker keys, which is what a Client.PatchMarkers killed between its two requests leaves behind (GitHub issue #1764): the values are already right, so a caller that judges by them alone writes nothing, and the provider's next apply that changes a marker conflicts with that entry. A caller whose markers already read right re-sends Client.PatchMarkers when this is true; it is false for an object whose markers the Apply entry already holds, so a clean rerun still sends nothing. fieldManager defaults to DefaultFieldManager when empty. A managedFields entry that cannot be decoded answers true with the error, so the caller's write reports it.

func NaturalKey

func NaturalKey(namespace, name string) string

NaturalKey is the NAMESPACE/NAME (or NAME, for a cluster-scoped kind) a listed object and a declared block meet on, whichever type either is filed under - the join every reader of a Kubernetes listing makes, the sweep and live-ls alike.

func OrphanResourceName

func OrphanResourceName(namespace, name string) string

OrphanResourceName is the resource name an undeclared object is planned under, since an estate-only label carries no configuration address: orphan_<namespace>_<name>, or orphan_<name> for a cluster-scoped kind, with every character outside a resource name's grammar folded to "_". Namespaces and names are DNS labels, so only "." can need folding.

func OwnedAt

func OwnedAt(raw []byte, root []string) (members []string, atomic bool, found bool)

OwnedAt reports what one FieldsV1 document owns at root: the member names under it, sorted, and atomic true when root is owned as a leaf. It is FieldOwners' per-manager reading, exposed for a caller holding one manager's fields rather than a whole object.

func ParseManifestImportID

func ParseManifestImportID(id string) (apiVersion, kind, namespace, name string, ok bool)

ParseManifestImportID reads a ManifestImportID back. ok is false for anything that is not one, including a value with the four keys in another order (the provider's own parser takes them in any order, but nothing here writes them in any other).

func PodSpecRoot

func PodSpecRoot(kind string) []string

PodSpecRoot is the FieldsV1 path to the pod spec a kind keeps its containers in: a Pod's own spec, a CronJob's job template's pod template, and every other workload's pod template.

func RecordStoreObject

func RecordStoreObject(obj *unstructured.Unstructured) bool

RecordStoreObject reports that obj is one of the estate's own record Secrets, written by record_store "kubernetes" (GitHub issue #1392).

Those objects carry the estate's tofu-estate label, deliberately: it is what live/kubernetes/estate-boundary.yaml fences a write to them with, and what keeps them from being the one thing in the estate whose ownership is not recorded the way everything else's is. But the sweep reads that same label as "this object is in the estate", and an object in the estate that no configuration block declares is an orphan the plan proposes to DESTROY. Measured on kind on 2026-09-19: an ordinary second plan of a Kubernetes estate proposed destroying all five of its own record Secrets, which would have deleted the estate's records as a side effect of planning it.

The test is both the managed-by label and the record-key annotation. Either alone is something a user could plausibly put on a Secret of their own; together they are this store's objects and nothing else. The two strings are staterecord's, spelled out here for the same reason staterecord spells tofu-estate out: neither package imports the other, and internal/live/projection's kubernetes_store_test.go pins them equal.

func RestConfig

func RestConfig(a Attrs) (*restclient.Config, error)

RestConfig builds the client configuration the sweep connects with.

func TransferFieldOwnership

func TransferFieldOwnership(entries []metav1.ManagedFieldsEntry, from, to string, writes []FieldWrite) ([]metav1.ManagedFieldsEntry, bool, error)

TransferFieldOwnership returns entries with every field of writes that from's entries own moved into to's Apply entry - created, from the first entry anything moved out of, when to has none - and changed false when from owned none of them.

"A field of writes" is a write's member and everything under it (an env item's name and value as well as the item), plus the write's root when from owns the root itself and nothing else under it after the move: an atomic list such as a Node's taints is owned as one leaf, and a map's own "." marker is left to no one otherwise. Nothing outside the writes moves, so a key from wrote that the configuration does not name stays from's, and every other manager's entries are returned as they were: this hands fields over from exactly one manager, which is what lets a migration take the provider's default manager's fields without ever taking another estate's.

func TypeFor

func TypeFor(kindTypes map[string][]string, declared map[string]bool, kind string) (string, bool)

TypeFor picks the provider type a listed object of kind is filed under: the type the configuration declares for that kind when it declares exactly one, else the versioned name when the kind has one, else the plain name. ok is false for a kind the provider has no type for.

Types

type Attrs

type Attrs struct {
	InCluster             bool
	ConfigPath            string
	ConfigPaths           []string
	ConfigContext         string
	ConfigContextAuthInfo string
	ConfigContextCluster  string
	Host                  string
	Token                 string
	Insecure              bool
	ClusterCACertificate  string
	ClientCertificate     string
	ClientKey             string
	// Exec is the provider block's exec block, the way every EKS root
	// authenticates (GitHub issue #1114): host and cluster_ca_certificate
	// name the cluster and a credential plugin - `aws eks get-token`, or
	// aws-iam-authenticator - produces the bearer token for it. Nil when
	// the block declares none, which leaves whatever the kubeconfig or
	// the explicit token supplied.
	//
	// A kubeconfig whose user is an exec plugin needs nothing here: it
	// arrives through clientcmd, which sets restclient.Config.ExecProvider
	// itself, and client-go's rest package links the exec credential
	// provider unconditionally. This field is for the provider block's
	// own exec block, which reaches no kubeconfig at all.
	Exec *ExecCredential
}

Attrs are the hashicorp/kubernetes provider block's connection arguments this sweep understands, read off the evaluated provider configuration by the caller. They follow the provider's own precedence (internal/backend/remote-state/kubernetes has the same loader for the state backend): in_cluster_config first; otherwise a kubeconfig named by config_path, config_paths, KUBE_CONFIG_PATH, KUBE_CONFIG_PATHS or KUBECONFIG, with config_context and its two overrides; and host, token and the TLS arguments layered on top of whatever the kubeconfig gave.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is Sweeper over a real API server.

func New

func New(cfg *restclient.Config) (*Client, error)

New connects. Nothing is called until Client.Kinds or Client.List.

Building the clients is where an exec block client-go cannot use at all is caught: rest.Config.TransportConfig asks the exec credential provider for an authenticator here, so an api_version it does not know fails now rather than at the first request.

The server's warning headers are dropped. A sweep lists every kind the cluster serves, deprecated ones included (v1 Endpoints from 1.33), and client-go's default handler prints each warning as a raw klog line on stderr, timestamped, in the middle of a plan the user did not ask about those kinds in. Stock surfaces no such line: the provider's own client logs them, and only under TF_LOG.

A config with QPS or Burst unset gets SweepQPS and SweepBurst, since the one sweep is a List per kind the cluster serves.

func NewWith

NewWith is New over already-built clients, for tests.

func NewWithMetadata

func NewWithMetadata(disc discovery.DiscoveryInterface, dyn dynamic.Interface, meta metadata.Interface) *Client

NewWithMetadata is NewWith plus the metadata client New builds.

func (*Client) DeleteMarkers

func (c *Client) DeleteMarkers(ctx context.Context, ref ObjectRef, pin ObjectPin, labels, annotations []string, fieldManager string, dryRun bool) (*unstructured.Unstructured, string, error)

DeleteMarkers implements LabelReleaser. The body is a JSON merge patch naming each key inside metadata.labels and metadata.annotations with a null value, which RFC 7386 defines as "remove this key", and nothing else - the request cannot carry a change to any other field, for the same reason Client.PatchMarkers's cannot. A key the object does not carry is a no-op on the server, not an error. A set pin adds metadata.resourceVersion and metadata.uid, which change nothing on a stored object that already carries them and refuse the patch on one that does not (ObjectPin).

func (*Client) DryRun

func (c *Client) DryRun(ctx context.Context, manifest map[string]any, update bool) (DryRunResult, error)

DryRun implements Sweeper. The group-version's resource list names the resource the kind is served as and whether it is namespaced - the same request Client.Serves makes - and the object goes to the server as kubectl create --dry-run=server or kubectl replace --dry-run=server would send it. An update needs the live object's resourceVersion first: a custom resource refuses an unconditional update ("must be specified for an update"), so one GET precedes the PUT, as kubectl replace's does. A status error the server answered with is its verdict, returned as a rejection; any other error, including a 5xx, is a cluster that could not answer.

func (*Client) Kinds

func (c *Client) Kinds(ctx context.Context, typeNames []string, manifestType string) ([]Kind, []string, error)

Kinds implements Sweeper: every group's preferred version of every resource, joined to typeNames by group and kind (GitHub issue #1111; kind alone let a CRD spelled like a built-in's, in its own group, be filed under that built-in's type with the wrong import id - [servesBuiltinKind] is the group half of the join). A kind served by more than one group (Event in the core group and in events.k8s.io) is listed once per group, since those are distinct resources; a resource served at more than one version within a group is listed once, since those are one resource, at the version [preferredVersions] picks.

func (*Client) List

func (c *Client) List(ctx context.Context, k Kind, key, value string) ([]Object, Skipped, error)

List implements Sweeper: one cluster-wide, label-selected list.

func (*Client) ListFieldManaged

func (c *Client) ListFieldManaged(ctx context.Context, k Kind, manager string) ([]FieldManagedObject, error)

ListFieldManaged implements FieldManagedLister.

func (*Client) PatchMarkers

func (c *Client) PatchMarkers(ctx context.Context, ref ObjectRef, labels, annotations map[string]string, fieldManager string, dryRun bool) (*unstructured.Unstructured, string, error)

PatchMarkers implements LabelPatcher. The body is a JSON merge patch naming the given keys inside metadata.labels and metadata.annotations and nothing else, so the request itself cannot carry a change to any other field; what the SERVER then does with it is the caller's to check, which is what the dry run is for.

func (*Client) ReadObject

func (c *Client) ReadObject(ctx context.Context, ref ObjectRef) (*unstructured.Unstructured, bool, error)

ReadObject implements LabelPatcher.

func (*Client) Serves

func (c *Client) Serves(ctx context.Context, apiVersion, kind string) (bool, error)

Serves implements Sweeper: one GET of the group-version's resource list, the same request API discovery makes per group, asked at the exact version the block names rather than the group's preferred one, because a kind served at v2 alone does not serve a manifest written for v1. A 404 is the server's word that the group-version is not served, and reads as false with no error; any other failure is returned as is.

func (*Client) TransferFields

func (c *Client) TransferFields(ctx context.Context, ref ObjectRef, from, to string, writes []FieldWrite) (bool, error)

TransferFields implements FieldTransferer: TransferFieldOwnership over the object as read, sent as one JSON patch replacing metadata.managedFields and nothing else, pinned to the resourceVersion read so a write landing in between is answered with a conflict - the transfer is then recomputed from a fresh read - rather than having its ownership overwritten. It changes no field's value, so the server records no Update entry for it; the same request [Client.handMarkersToApply] makes for the marker labels (GitHub issue #1704). moved is false, with nothing sent, when from owns none of the writes' fields.

type ConnectCause

type ConnectCause string

ConnectCause names why a cluster call produced no answer.

const (
	// CauseUnclassified is a failure this does not recognise: it is
	// passed through unchanged rather than given a cause it might not
	// have. A per-kind RBAC denial on one list is one of these - it is
	// not a connection problem at all.
	CauseUnclassified ConnectCause = ""
	// CauseNoCredentials is the cluster refusing a request that carried
	// no credential, because the configuration supplied none: no token,
	// no client certificate, no exec block and no kubeconfig the loader
	// could read.
	CauseNoCredentials ConnectCause = "no credentials"
	// CauseExecPlugin is the credential plugin failing: it is not on
	// PATH, it exited non-zero, it printed something that is not an
	// ExecCredential, or it answered in a protocol version the exec
	// block does not name. The cluster was never asked.
	CauseExecPlugin ConnectCause = "exec plugin"
	// CauseRejected is the cluster answering 401 with a credential in
	// hand: the connection is good and the credential is not accepted.
	// On EKS an expired or wrong-cluster token from the plugin lands
	// here, and the plugin itself is fine.
	CauseRejected ConnectCause = "credential rejected"
	// CauseUnreachable is the cluster not answering at all: the dial
	// failed, the TLS handshake failed, the deadline passed. This is the
	// one thing "unavailable" used to mean and the only one it was ever
	// right about.
	CauseUnreachable ConnectCause = "unreachable"
)

type Credentials

type Credentials struct {
	// Known distinguishes a client built from a [restclient.Config] from
	// one built over already-made clients ([NewWith], which tests use):
	// the second knows nothing about credentials, so it must not claim
	// there were none.
	Known bool
	// Exec is the credential plugin's command when one is configured,
	// whether it came from the provider block's exec block or from a
	// kubeconfig user, and empty when none is.
	Exec string
	// Any reports that something in the configuration can identify a
	// user. False means every request goes out anonymous.
	Any bool
}

Credentials is what a built client knows about how it was told to authenticate, kept so that a refusal from the cluster can be reported against what was actually sent.

func CredentialsOf

func CredentialsOf(cfg *restclient.Config) Credentials

CredentialsOf reads how cfg authenticates. The set of fields is client-go's own canIdentifyUser (tools/clientcmd/client_config.go) widened by the two file-backed forms a rest.Config can carry that a kubeconfig's merged config cannot.

func (Credentials) Diagnose

func (cr Credentials) Diagnose(err error) (ConnectCause, string)

Diagnose classifies a cluster call's failure and returns the sentence that names the cause, or CauseUnclassified and an empty string for one it does not recognise.

type DryRunResult

type DryRunResult struct {
	// Accepted reports that the server would have written the object.
	Accepted bool
	// Message is the server's rejection, verbatim, when Accepted is
	// false: a validation failure against the kind's schema, an
	// admission policy's denial, a 403 from RBAC.
	Message string
	// Defaulted counts the fields the server's answer carries that the
	// submitted manifest did not, outside metadata and status: what
	// defaulting and mutating admission would add on write.
	Defaulted int
}

DryRunResult is what the API server said to a Sweeper.DryRun.

type ExecCredential

type ExecCredential struct {
	// APIVersion is the exec block's api_version:
	// client.authentication.k8s.io/v1beta1 for an EKS root written any
	// time in the last five years, /v1 for a newer one.
	APIVersion string
	// Command is the plugin's executable, resolved on PATH by client-go.
	Command string
	// Args are the plugin's arguments, in order.
	Args []string
	// Env are environment variables set for the plugin on top of this
	// process's own, in no particular order.
	Env map[string]string
}

ExecCredential is the hashicorp/kubernetes provider block's exec block: a credential plugin speaking the client.authentication.k8s.io ExecCredential protocol, run on every request whose credential has expired. The credential it produces is the caller's own, which is what keeps the admission policy's "the fence binds the credential" property true of the sweep as well as of the provider.

type FieldManagedLister

type FieldManagedLister interface {
	ListFieldManaged(ctx context.Context, k Kind, manager string) ([]FieldManagedObject, error)
}

FieldManagedLister lists, for one kind, every object, with the fields a field manager owns of each through server-side apply (nil where it owns none). Client implements it; it is its own interface rather than a Sweeper method so that a test's sweeper need not grow one, and a caller holding a sweeper asks for it with a type assertion.

The API server cannot select on managedFields, so the list is every object of the kind, metadata only where the client has a metadata client, filtered here. That is the cost of a marker that lives in managedFields rather than in a label (GitHub issue #1191's ruling): one unselected list per kind.

type FieldManagedObject

type FieldManagedObject struct {
	APIVersion string
	Kind       string
	Namespace  string
	Name       string
	// Fields is the manager's FieldsV1 document: the union of every Apply
	// entry it has on the object outside a subresource. Nil when the
	// manager owns no field of the object.
	Fields []byte
}

FieldManagedObject is one live object some fields of which a given field manager owns, with exactly what it owns.

type FieldOwner

type FieldOwner struct {
	Manager string
	// Members is the subset of the write's members this manager owns,
	// sorted. Empty with Atomic true when the manager owns Root as a whole
	// - an atomic list such as a Node's spec.taints, which managedFields
	// records as one leaf with no members.
	Members []string
	Atomic  bool
}

FieldOwner is one manager's claim on a planned write.

func FieldOwners

func FieldOwners(obj *unstructured.Unstructured, write FieldWrite, exclude string) []FieldOwner

FieldOwners reports, for every manager other than exclude that owns any part of write on obj, what it owns. Entries for a subresource are skipped, as ManagedMetadataKeys skips them. The result is sorted by manager name, so a message built from it reads the same on every run.

type FieldTransferer

type FieldTransferer interface {
	ReadObject(ctx context.Context, ref ObjectRef) (obj *unstructured.Unstructured, found bool, err error)
	TransferFields(ctx context.Context, ref ObjectRef, from, to string, writes []FieldWrite) (moved bool, err error)
}

FieldTransferer moves ownership of a field-granular write's fields from one manager to another on a live object. Client implements it.

type FieldWrite

type FieldWrite struct {
	Root    []string
	Members []string
	Atomic  bool
}

FieldWrite is where one field-granular write lands in its object.

Root is the FieldsV1 path to the set the write adds members to - for metadata.labels, ["f:metadata", "f:labels"]; for one container's env, ["f:spec", "f:template", "f:spec", "f:containers", `k:{"name":"app"}`, "f:env"]. Members are the FieldsV1 spellings of the members it sets under Root: "f:<key>" for a map key, `k:{...}` for an associative-list item.

Atomic is true when Root may be an atomic list - a Node's spec.taints - which managedFields records as one leaf with no members, so a manager holding that leaf owns the write whole while the list holds anything. A release (an apply of the empty list) leaves the same leaf behind, so a leaf over an empty live value owns nothing (#1917): AtomicReleased. Every other root is granular: a leaf with no members there is what a release leaves behind (an apply of the empty map or env list, #1885) and owns nothing.

type ForbiddenDetail

type ForbiddenDetail struct {
	// Verb is the RBAC verb denied ("list").
	Verb string
	// Resource is the API resource name denied, plural and lower-case as
	// RBAC spells it ("secrets", "configmaps").
	Resource string
	// Namespace is the namespace the check was scoped to, or "" for a
	// cluster-scoped check - which is what an estate sweep's cluster-wide
	// list (no namespace on the request) asks the authorizer for.
	Namespace string
}

ForbiddenDetail is what the API server's own Forbidden message says was refused.

func Forbidden

func Forbidden(err error) (ForbiddenDetail, bool)

Forbidden reports whether err is the API server refusing a request as RBAC-forbidden (HTTP 403), and what its own message says was refused. False for anything else, including a cluster that never answered (unreachable) or answered but would not authenticate the credential at all (401, Credentials.Diagnose's CauseRejected) - a 403 is the one case where the cluster's own words name the fix better than any wrapper here could.

type HeldObject

type HeldObject struct {
	Kind      string
	Namespace string
	Name      string
	Labels    map[string]string
	// Annotations are its metadata.annotations, which carry the holder's
	// own signal (Helm's release annotation): the Kubernetes leg asks
	// the family which controller they name (GitHub issue #1706).
	Annotations map[string]string
	// Controller is the controller that holds it: today always
	// [ControllerHelm].
	Controller string
	// HeldBy names the holder for a reader: "Helm release NAMESPACE/NAME".
	HeldBy string
}

HeldObject is one live object carrying the estate's label that a controller holds, with the holder named, so the sweep and live-ls can report it with its parent rather than only count it (the 2026-09-26 ruling on GitHub issue #1604).

type Kind

type Kind struct {
	GVR        schema.GroupVersionResource
	Kind       string
	Namespaced bool
	// APIVersion is the group/version the kind is listed at ("v1" for the
	// core group), the first key of a [ManifestImportID].
	APIVersion string
	// TypeNames are the provider types that manage this kind, sorted;
	// the manifest type alone when no built-in type does.
	TypeNames []string
	// Manifest reports that this kind is listed under the manifest type:
	// no built-in type manages it, and an object of it imports by
	// [ManifestImportID] rather than NAMESPACE/NAME.
	Manifest bool
}

Kind is one listable kind the cluster serves that the provider has a resource type for.

type LabelPatcher

type LabelPatcher interface {
	// ReadObject returns the live object at ref. found is false, with a
	// nil error, when the server answers that no such object exists; an
	// error is a cluster that could not answer, which is never the same
	// thing.
	ReadObject(ctx context.Context, ref ObjectRef) (obj *unstructured.Unstructured, found bool, err error)

	// PatchMarkers sets every entry of labels into metadata.labels and
	// every entry of annotations into metadata.annotations on the object
	// at ref, through ONE merge patch under fieldManager, and returns the
	// object the server produced. A real (not dry-run) write then moves
	// the markers' ownership from fieldManager's Update entry to its Apply
	// entry, so the provider's own apply can change them later without a
	// field manager conflict (GitHub issue #1704; this file's doc comment
	// says why it is a second request). The label is the tofu-estate marker; the
	// annotation is the block address beside it (GitHub issue #1639), and
	// the two go in one request so an object is never left carrying one
	// without the other by a write that half landed.
	//
	// With dryRun the server validates, defaults, runs admission and
	// persists nothing, so the returned object is what the real write
	// would have stored. rejected carries the server's own words for a
	// refusal it answered with (a validation failure, an admission
	// policy's denial, a 403 from RBAC) and is empty when the server
	// accepted; err is a cluster that could not answer at all.
	PatchMarkers(ctx context.Context, ref ObjectRef, labels, annotations map[string]string, fieldManager string, dryRun bool) (obj *unstructured.Unstructured, rejected string, err error)
}

LabelPatcher is the cluster half of a label write, narrowed to the two calls a caller needs: read the object as it is, and set one label on it. Client implements it against a real API server; a test stands in for one.

Both live-import's adoption of a manifest-shape object (#1109) and live-mv's cross-estate move of one (#1104) make exactly this write, so there is one implementation of it and one place its safety is argued.

type LabelReleaser

type LabelReleaser interface {
	// ReadObject is [LabelPatcher.ReadObject].
	ReadObject(ctx context.Context, ref ObjectRef) (obj *unstructured.Unstructured, found bool, err error)

	// DeleteMarkers removes every key in labels from metadata.labels and
	// every key in annotations from metadata.annotations on the object at
	// ref, through ONE merge patch under fieldManager, and returns the
	// object the server produced. pin, when set, makes the patch apply only
	// to the object version it names ([ObjectPin]). dryRun, rejected and
	// err mean what they mean on [LabelPatcher.PatchMarkers].
	DeleteMarkers(ctx context.Context, ref ObjectRef, pin ObjectPin, labels, annotations []string, fieldManager string, dryRun bool) (obj *unstructured.Unstructured, rejected string, err error)
}

LabelReleaser is the cluster half of a marker release (GitHub issue #1656): read the object as it is, and delete marker keys from it. It is what internal/live/untag needs to release a manifest-shape orphan under undeclared_tagged = "untag". Client implements it against a real API server; a test stands in for one.

type Object

type Object struct {
	Kind      string
	Namespace string
	Name      string
	Labels    map[string]string
	// Address is the object's [AddressAnnotation] as carried: the escaped
	// address of the block that stamped it (GitHub issue #1639), or empty
	// when the object carries none - one an older build made, or one a
	// controller stripped. It is read off the same LIST response the
	// labels are, so carrying it costs no request (GitHub issue #1640).
	Address string
	// ImportID is the provider's documented import id for the type the
	// kind is filed under: NAMESPACE/NAME, or NAME for a cluster-scoped
	// kind, for a built-in type; [ManifestImportID] for the manifest type.
	ImportID string
	// DeletionTimestamp is metadata.deletionTimestamp as RFC 3339, or
	// empty for an object nobody has asked to go (GitHub issue #1184). Set,
	// it means the API server accepted a delete and the object is held:
	// by Finalizers, or for its grace period. A terminating object is still
	// a live, labelled object, so the sweep lists it like any other.
	DeletionTimestamp string
	// Finalizers is metadata.finalizers, in the server's order.
	Finalizers []string
}

Object is one live object carrying the estate's label whose controller, if any, is not the one that made it - metadata.ownerReferences is empty.

type ObjectPin

type ObjectPin struct {
	ResourceVersion string
	UID             string
}

ObjectPin names the exact object a write was decided against: its metadata.resourceVersion and metadata.uid. A merge patch carrying them is refused by the API server with a conflict when the stored object's differ - it was written since (resourceVersion) or deleted and recreated under the same name (uid) - so a write checked against what an object carried cannot land on what it carries now. GitHub issue #1743: an untag release checked the estate label, and live-mv could move the object to another estate between that read and the patch. The zero value pins nothing.

func PinOf

PinOf is the ObjectPin for obj as read.

type ObjectReader

type ObjectReader interface {
	ReadObject(ctx context.Context, ref ObjectRef) (obj *unstructured.Unstructured, found bool, err error)
}

ObjectReader reads one live object back. *Client is one; it is the one method of LabelPatcher a field-owner check needs, named on its own so a caller holding only a Sweeper can ask for exactly this.

type ObjectRef

type ObjectRef struct {
	APIVersion string
	Kind       string
	Namespace  string
	Name       string
}

ObjectRef names one live object by the natural key GitHub issue #1016 ruled a Kubernetes object is bound by: its group-version, its kind, and its namespace and name (Namespace empty for a cluster-scoped kind).

func (ObjectRef) String

func (r ObjectRef) String() string

String renders the ref the way ManifestImportID renders it, so that a message about an object reads the same wherever it was produced.

type Release

type Release struct {
	Namespace string
	Name      string
}

Release names a Helm release.

func HelmRelease

func HelmRelease(obj *unstructured.Unstructured) (Release, bool)

HelmRelease reports the Helm release that holds obj, read off Helm's release annotations. A non-empty meta.helm.sh/release-name is the signal; the namespace annotation only completes the name.

func HelmReleaseOf

func HelmReleaseOf(ann map[string]string) (Release, bool)

HelmReleaseOf is HelmRelease read off an object's annotations alone, for a caller holding the annotations rather than the object (substrate.Kubernetes's ControllerHeld, GitHub issue #1706).

func (Release) String

func (r Release) String() string

String is "Helm release NAMESPACE/NAME", or "Helm release NAME" when the object did not say which namespace the release lives in.

type Skipped

type Skipped struct {
	// Count is every object set aside: controller-made ones
	// ([ControllerMade]), Helm release ones among them, and the estate's
	// own record Secrets ([RecordStoreObject]).
	Count int
	// Held are the ones whose holder can be named, in listing order: today
	// the objects a Helm release holds ([HelmRelease]; GitHub issue #1607).
	Held []HeldObject
	// Unlisted are the objects annotated with a Helm release that is live
	// but whose manifest does not list them (GitHub issue #1738 item 4):
	// a chart's drop under helm.sh/resource-policy: keep, or a copy of a
	// Helm object's YAML. Not held, since the release does not answer for
	// them, and not returned for the destroy proposal either, since the
	// one thing that tells them apart from the release's own is a manifest
	// match: each is reported, HeldBy naming the release, and counted in
	// Count.
	Unlisted []HeldObject
}

Skipped is what Sweeper.List set aside: objects carrying the estate's label that are never orphans, because something other than the estate holds them.

type Sweeper

type Sweeper interface {
	// Kinds are the listable kinds among typeNames' kinds, in a stable
	// order - plus, when manifestType names the provider's type that
	// manages any served kind (empty when it has none), every other kind
	// the cluster serves with list and delete verbs, filed under that
	// type - and the provider types whose kinds the cluster does not
	// serve at all.
	Kinds(ctx context.Context, typeNames []string, manifestType string) (kinds []Kind, unserved []string, err error)
	// List returns every object of k carrying label key=value, excluding
	// controller-held ones, and what it excluded: how many, and, for each
	// one whose holder it can name, the object and its holder.
	List(ctx context.Context, k Kind, key, value string) (objects []Object, ownerSkipped Skipped, err error)
	// Serves reports whether the cluster serves kind at exactly apiVersion
	// (GitHub issue #1079's fourth ruling): false with a nil error when the
	// group-version is not served or serves no such kind, so that a
	// manifest block naming it can be refused by name before the provider
	// asks the cluster for a schema it has not got. An error is a cluster
	// that could not answer, which is never grounds to refuse a block.
	Serves(ctx context.Context, apiVersion, kind string) (bool, error)
	// DryRun submits manifest - a planned kubernetes_manifest object, the
	// stamped label inside it - to the API server the way the apply
	// would write it, a POST for a create or a PUT for an update, with
	// dryRun=All (GitHub issue #1081, item 3): the server validates,
	// defaults and runs admission and persists nothing. The result is
	// the server's own answer, accepted or the rejection in its words;
	// err is a cluster that could not answer at all, which is a coverage
	// gap and never grounds to refuse the plan.
	DryRun(ctx context.Context, manifest map[string]any, update bool) (DryRunResult, error)
}

Sweeper is what discovery asks of a Kubernetes sweep, so a test can stand in for a cluster.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL