foreign

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 24, 2026 License: MPL-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package foreign classifies the live resources an estate does not own, and is the safety property of stateless mode: a live resource nobody claims is surfaced, and is never a deletion candidate.

Its input is one discovery.Result gathered with discovery.Request.CollectUnclaimed set, plus the configuration the resolutions came from. Its output is a report, and only a report: Classify never rewrites a resolution, never stamps a marker, and never hands anything to the projection builder. That is not an implementation detail deferred to a later phase - it is how the protection property is made structural. The prior state a stateless plan runs against is built from resolutions, resolutions come from declared addresses, and an unclaimed live resource has no declared address, so it can never enter the prior state and the plan engine has nothing to propose destroying. Nothing here has to remember to exclude it.

The three classes

Every unclaimed live resource lands in exactly one:

  • ClassForeign: no tofu-estate tag at all, and no declared instance it could plausibly be. Report only. Each one carries its type, live ID, display name, tag summary, and a sentence saying why it is not a bind candidate.
  • ClassBindCandidate: no tofu-estate tag, but its content exactly matches a declared instance of the same type that discovery could not find. Surfaced with an adoption hint - the marker pair to stamp - and never bound. live/MARKERS.md is explicit that ownership is the tag and nothing else; inferring it from a content match would be exactly the guess the marker spec exists to forbid.
  • ClassOtherEstate: carries a tofu-estate tag naming a different estate. Counted, not itemized: another estate's resources are that estate's business.

Malformed markers (tofu-estate present, tofu-address missing or unparseable) and collisions are neither foreign nor owned. They are named errors, raised by discovery as [discovery.Problem]s, and this package does not reclassify them as anything - a resource whose ownership record is broken is not "unowned", it is broken.

Matching is conservative on purpose

A bind candidate needs an exact match on the type's identity-bearing arguments and nothing else. The rules, all of which must hold:

  1. The declared instance is unbound (discovery found no marker for it) and has no instance key. count and for_each members are a keyed or fungible set whose membership is phase 3's slot matcher to decide; picking one here would be a guess wearing a match's clothes.
  2. The type has an entry in the match table below. aws_route_table, aws_internet_gateway and aws_eip do not: nothing in their configuration distinguishes one from another, so no content match over them can mean anything.
  3. Every argument in that entry is statically evaluable from configuration (constants, variables, locals and functions - the same subset identity resolution uses) and non-empty.
  4. The live object carries every one of those attributes, known, non-null, and equal string-for-string. Near misses are not matches: a security group named "web-sg-old" against a declared "web-sg" is plain foreign.
  5. The match is one-to-one. Two live resources matching one address, or one live resource matching two addresses, means every resource involved is reported as plain foreign with the ambiguity named.

When in doubt the answer is ClassForeign, which is the safe direction: foreign is report-only either way, and a missed adoption hint costs an operator one command while a wrong one would attach a plan to somebody else's resource.

Rename candidates, which are about owned resources rather than unowned

One thing here is not about the unclaimed side at all. When a for_each key changes in configuration - this["a"] becomes this["c"] - the live resource keeps its old marker, so discovery reports it as an orphan (this estate's tag, an address nothing declares) and reports this["c"] as unbound, and the plan proposes a create beside a resource that already exists. Both reports are correct and neither is useful on its own.

Result.Renames pairs them, under a rule as conservative as the bind candidate rule above and for the same reason - the marker spec forbids guessing at ownership, and a wrong pairing would offer to point a marker at the wrong resource:

  1. Same resource type and same resource block. The block match is the whole pairing; nothing crosses from aws_subnet.this to aws_subnet.other.
  2. The block uses for_each. count members are excluded outright: a live count member past the declared count is surplus, which the slot matcher has already accounted for, and offering to rename it would fight the scale-down rule.
  3. One orphan and one unbound instance, both ways. Two of either is reported as an ambiguity with no command - which live resource became which key is precisely what a marker cannot say.
  4. The two keys differ, which they always do in the real case, since equal keys would have bound.

Content plays no part. It could only strengthen a pairing the block match has already made, and the identity-bearing attributes it would be made on are not carried for an owned resource anyway - discovery brings back the full listed object for the resources nobody claims and the tags for the ones this estate owns, and matching on tags is what the match table exists to refuse.

Nothing is applied. The marker is not rewritten, the orphan is still an orphan, and the plan still proposes creating the declared instance. What the candidate carries is RenameCandidate.Command: the exact "choudoufu live-mv <old> <new>" that does the rewrite, for an operator who agrees with the pairing.

What was not swept, and why that matters more than what was

A classification is only as wide as the scan behind it, and "nobody looked" must never read as "there are none". Result.Unswept carries the types this pass cannot speak for, each with a reason:

  • the provider cannot list the type (discovery.ProblemTypeNotListable);
  • listing it failed (discovery.ProblemListFailed);
  • it was scanned with the server-side estate filter on (discovery.ScopeEstate), which hides unclaimed resources by construction;
  • no instance of it needed discovery, so it was never listed at all. This is the big one in practice: a configuration's client-named types (aws_s3_bucket, aws_iam_role) are resolved from configuration and never enumerated, so a foreign bucket in the same account is invisible here and saying otherwise would be a lie.

Even a fully swept type is not a census. The AWS provider appends filters of its own to every EC2 list, and one of them is is-default = false: an account's default VPC never appears in a list of aws_vpc, so it can never be classified, reported, or adopted through this path. Against the floci emulator the wide scan finds the default subnets, route tables, gateway and security groups, but not the default VPC. No call this package or discovery can make changes that, and no output either produces should imply completeness.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AttrMatch

type AttrMatch struct {
	Attr  string
	Value string
}

AttrMatch is one identity-bearing argument that matched exactly.

type Candidate

type Candidate struct {
	Resource

	// Addr is the declared instance it matches.
	Addr addrs.AbsResourceInstance

	// Matched are the identity-bearing arguments that matched, with the
	// value both sides agreed on, in argument order. This is the whole
	// evidence for the offer: an operator has to be able to see what the
	// match was made on before acting on it.
	Matched []AttrMatch

	// MarkerEstate and MarkerAddress are the two tag values that would
	// adopt this resource: tofu-estate and tofu-address, the latter already
	// escaped per live/MARKERS.md.
	MarkerEstate  string
	MarkerAddress string

	// Hint is a one-line command that stamps those two tags, for the
	// resource types whose adoption command this package knows. It is empty
	// for a type it does not, and the marker pair above is the contract
	// either way: any tool that writes those two tags adopts the resource.
	Hint string
}

Candidate is an unclaimed live resource that exactly matches one declared, unbound instance. It is an offer, not a binding.

func (Candidate) String

func (c Candidate) String() string

String renders a candidate on one line.

type Class

type Class string

Class is the classification of one unclaimed live resource.

const (
	// ClassForeign is a live resource carrying no tofu-estate tag that no
	// declared instance can be matched to. Report only, never deleted.
	ClassForeign Class = "FOREIGN"

	// ClassBindCandidate is a live resource carrying no tofu-estate tag
	// whose content exactly matches one declared, unbound instance of the
	// same type. Surfaced for explicit adoption; never bound here.
	ClassBindCandidate Class = "BIND_CANDIDATE"

	// ClassOtherEstate is a live resource carrying a tofu-estate tag naming
	// a different estate. Counted, not itemized.
	ClassOtherEstate Class = "OTHER_ESTATE"
)

type EstateCount

type EstateCount struct {
	// Estate is the other estate's name, or empty when the count came from
	// discovery's per-type tally, which records that a resource belonged to
	// some other estate without recording which. A count with no name is
	// still worth printing: it says another estate shares this account.
	Estate string

	// Count is the number of live resources.
	Count int

	// Types are the resource types they were listed as, sorted.
	Types []string
}

EstateCount is how many live resources carry one other estate's marker.

func (EstateCount) String

func (e EstateCount) String() string

String renders an other-estate count on one line.

type Lookalike added in v0.3.0

type Lookalike struct {
	// Addr is the declared instance the plan proposes to create.
	Addr addrs.AbsResourceInstance

	// TypeName is its resource type.
	TypeName string

	// LiveID is the unowned live resource's identity, the handle an operator
	// needs to go look at it. Empty when the provider sent no usable
	// identity, the same case [Resource.LiveID] covers.
	LiveID string

	// DisplayName is the provider's label for it, when it differs from
	// LiveID.
	DisplayName string

	// Matched are the identity-bearing arguments the live resource and the
	// declared instance agreed on exactly - the same evidence a
	// [Candidate] carries. Empty for the generic, no-[matchTable]-entry
	// case: there, nothing in configuration confirmed the match, only
	// cardinality did, and pretending otherwise would overstate the
	// evidence.
	Matched []AttrMatch

	// MarkerEstate and MarkerAddress are the tofu-estate and tofu-address
	// values that adopt the live resource instead of creating a duplicate
	// beside it.
	MarkerEstate  string
	MarkerAddress string

	// Hint is the one-line adoption command composed by the same machinery
	// [Candidate.Hint] uses, empty for a type this fork has no composable
	// tagging verb for.
	Hint string
}

Lookalike is the lookalike guard's warning: a declared instance the plan proposes to create, beside a live resource this estate does not own that might be the very resource being duplicated - most often because its tofu-estate and tofu-address tags were stripped out of band, off a server-assigned resource that no marker means no other way to find. Warn, never block: the create may be genuinely intended, and nothing here changes it.

func Lookalikes added in v0.3.0

func Lookalikes(req Request, res *Result, creates []addrs.AbsResourceInstance) []Lookalike

Lookalikes is the lookalike guard. Given the addresses a plan actually proposes to create, and the Result a Classify call already produced over the same discovery pass the plan's prior state was built from, it returns one warning for every create that a live resource this estate does not own might be the very thing being duplicated.

Two paths, both as conservative as Classify itself, and neither one re-derives what Classify already decided:

  • A type with a [matchTable] entry warns only when Result.Candidates already offers this exact address a match - the one-to-one, every-argument-equal content match Classify computed once. A matchTable type with no confirmed candidate (a near miss, an ambiguity, no unclaimed resource of that type at all) stays silent; a wrong guess would point an operator at the wrong resource, and Classify's own doc says a missed hint is the cheaper mistake.
  • A type absent from the table has nothing in configuration that could confirm a match, so the only safe signal left is cardinality: exactly one unclaimed live resource of the create's type in Result.Foreign. Zero says nothing and more than one is the same ambiguity the one-to-one rule refuses to resolve by guessing, so both stay silent. Like the matchTable path, a keyed (count/for_each) create is skipped outright: distinguishing "the marker was stripped off one member" from "the set legitimately grew" needs the slot matcher, not a cardinality count, and guessing here would flag ordinary scale-out as a duplicate.

Nothing here touches the plan. A create beside a genuine lookalike is still a create; this only makes sure whoever reads the plan sees why it might be the wrong one before applying it.

func (Lookalike) String added in v0.3.0

func (l Lookalike) String() string

String renders a lookalike warning on one line, for logs and test failure output.

type ParentReadFinding added in v0.3.0

type ParentReadFinding struct {
	// TypeName is the child's resource type.
	TypeName string

	// Parent is the admitted type whose identity led this pass to it.
	Parent string

	// ParentAddr is the parent's own resolved address in this estate.
	ParentAddr addrs.AbsResourceInstance

	// ParentValue is the parent identity value the read was scoped to, and
	// also the child's own whole identity for this shape.
	ParentValue string

	// LiveID is the child's live identity as the read found it.
	LiveID string

	// DisplayName is the provider's label for it. Display only.
	DisplayName string

	// Removal is true when this finding also entered the prior state as a
	// destroy; the plan's own resource diff carries the destroy itself, and
	// this is what says a parent read is the reason it could be proposed.
	Removal bool

	// Withheld is why Removal is false, empty when Removal is true.
	Withheld string
}

ParentReadFinding is one live child a parent read found, without a marker or a declared resource block of its own. Mirrors discovery.ParentReadFinding; see that type for how a child qualifies.

func (ParentReadFinding) String added in v0.3.0

func (f ParentReadFinding) String() string

String renders a parent-read finding on one line.

type Removal

type Removal struct {
	Resource

	// Addr is the address its marker names, unescaped: where it sits in the
	// prior state, and the address the plan's destroy line prints.
	Addr addrs.AbsResourceInstance

	// Marker is the tofu-address tag value as carried, and Normalized its
	// escaped form.
	Marker     string
	Normalized string

	// BlockGone is true when the whole resource block is missing from the
	// configuration, as opposed to the block still existing and no longer
	// expanding to this instance key. The two are the same operation to the
	// cloud and different sentences to an operator.
	BlockGone bool

	// Swept is true when the estate-wide sweep found it rather than a scan
	// of a type the configuration declares. It is exactly the case that was
	// invisible before removal exactness landed.
	Swept bool
}

Removal is one live resource this estate owns at an address the configuration no longer declares, which the plan proposes destroying.

func (Removal) String

func (r Removal) String() string

String renders a removal on one line.

type RenameAmbiguity

type RenameAmbiguity struct {
	// Block is the resource block, and TypeName its resource type.
	Block    string
	TypeName string

	// Live are the orphaned live resources of that block.
	Live []RenameLive

	// Declared are the addresses of its unclaimed declared instances,
	// sorted.
	Declared []string

	// Detail is one sentence saying what the obstacle is.
	Detail string
}

RenameAmbiguity is one resource block whose orphans and unclaimed declared instances do not pair one-to-one, reported so that the operator can see the same thing this pass saw and decide for themselves.

func (RenameAmbiguity) String

func (a RenameAmbiguity) String() string

String renders an ambiguity on one line.

type RenameCandidate

type RenameCandidate struct {
	Resource

	// Block is the resource block both sides belong to, which is the whole
	// basis of the pairing.
	Block string

	// Old is the address the live marker claims, unescaped: the address to
	// rename from.
	Old addrs.AbsResourceInstance

	// New is the declared instance nothing claimed: the address to rename to.
	New addrs.AbsResourceInstance

	// Marker is the tofu-address tag value exactly as the live resource
	// carries it, and Normalized is its escaped form - the string the block
	// match was made on.
	Marker     string
	Normalized string

	// MatchedOn are the identity-bearing arguments the live resource and the
	// block's configuration agreed on, when there were any. It is empty for
	// the ordinary pairing, which the block match alone decides; it is the
	// evidence when content is what resolved an ambiguity.
	MatchedOn []AttrMatch

	// Command is the exact, shell-quoted live-mv invocation that
	// rewrites the marker. Running it is the entire rename; nothing else has
	// to happen and no state is touched.
	Command string
}

RenameCandidate is one live resource this estate owns, sitting at a for_each key the configuration no longer declares, offered as the same resource as one declared instance nothing claimed. It is an offer, not a move: only [Command] performs it.

func (RenameCandidate) String

func (r RenameCandidate) String() string

String renders a rename candidate on one line.

type RenameLive

type RenameLive struct {
	LiveID      string
	DisplayName string

	// Marker is the escaped tofu-address value it carries.
	Marker string
}

RenameLive is one orphaned live resource named in an ambiguity.

type Request

type Request struct {
	// Estate is the estate whose ownership boundary this classification is
	// drawn around. A live resource carrying this value in its tofu-estate
	// tag is owned and never reaches this package.
	Estate string

	// Config is the configuration the discovery pass ran against, which is
	// where the declared instances and their identity-bearing arguments are
	// read from.
	Config *configs.Config

	// Discovery is the result of one discovery pass. It must have been
	// gathered with [discovery.Request.CollectUnclaimed] set, or there is
	// nothing to classify - and the scan rows will say so, which is what
	// [Result.Unswept] reports.
	Discovery *discovery.Result

	// Region is the region the discovery pass listed in, as the provider
	// configuration knows it - the same value handed to
	// [discovery.Request.Region]. It travels into the adoption hint as a
	// --region flag, so that the printed command talks to the same region
	// the resource was found in rather than whatever the operator's CLI
	// profile defaults to. Empty leaves the flag off the hint.
	Region string

	// EndpointURL is the custom endpoint the provider configuration reaches
	// the cloud through, when one is configured - a LocalStack-style
	// emulator, a VPC endpoint. It travels into the adoption hint as an
	// --endpoint-url flag for the same reason Region does. Empty leaves the
	// flag off the hint.
	EndpointURL string
}

Request is one classification pass.

type Resource

type Resource struct {
	// TypeName is the resource type it was listed as.
	TypeName string

	// LiveID is the identity the provider serves for it, which is what an
	// operator needs to find it in a console or a CLI. Empty when the
	// provider sent no usable identity.
	LiveID string

	// DisplayName is the provider's label for it. Display only.
	DisplayName string

	// Tags is the resource's whole tag set, sorted by key. It is the
	// evidence for the classification: an operator reading a foreign line
	// wants to see that no tofu-estate is there, and usually recognizes the
	// resource from whatever tags it does carry.
	Tags []Tag

	// Why is one sentence saying why this resource is in the class it is in
	// - for a foreign resource, why it is not a bind candidate.
	Why string
}

Resource is one classified live resource.

func (Resource) String

func (r Resource) String() string

String renders a classified resource on one line.

func (Resource) TagSummary

func (r Resource) TagSummary(n int) string

TagSummary renders at most n tags as "k=v" pairs, with a count of the remainder. n <= 0 renders all of them.

type Result

type Result struct {
	// Estate is the estate the classification was made against.
	Estate string

	// Foreign lists the unclaimed live resources that belong to nobody, in
	// type and live-ID order. Every one of them is report-only.
	Foreign []Resource

	// Candidates lists the unclaimed live resources that exactly match a
	// declared, unbound instance, in declared-address order. Nothing is
	// bound: each carries the marker pair an operator would stamp to adopt
	// it.
	Candidates []Candidate

	// Renames lists the live resources this estate owns whose marker names a
	// for_each key the configuration no longer declares, each paired with the
	// one declared instance of the same resource block that nothing claimed,
	// in declared-address order. Like a bind candidate it is an offer and
	// nothing more: the marker is not rewritten, the plan still proposes
	// creating the new key, and the live resource is still an orphan.
	Renames []RenameCandidate

	// Removals lists the live resources this estate owns and no longer
	// declares, which the plan therefore proposes destroying, in address
	// order. They are the one part of this pass that is not report-only:
	// every one of them is in the prior state the plan ran against, as an
	// instance with no configuration.
	//
	// It is a section of this report rather than of the plan's own output
	// because the plan can only say "this will be destroyed"; the fact that
	// makes it legitimate - that the resource carries this estate's marker
	// at an address the configuration used to declare - is a marker fact,
	// and this is where marker facts are printed.
	Removals []Removal

	// SweepGaps lists the resource types the estate-wide removal sweep could
	// not enumerate, carried through from discovery. An estate with a sweep
	// gap has resources it may own and cannot see, so an empty Removals list
	// is not a claim that nothing is undeclared.
	SweepGaps []SweepGap

	// SweepCovered lists the types the sweep did enumerate, sorted.
	SweepCovered []string

	// Ambiguous lists the resource blocks that have orphans and unclaimed
	// declared instances but no one-to-one pairing between them, sorted by
	// block address. They carry no command: which live resource became which
	// key is exactly what a marker cannot say.
	Ambiguous []RenameAmbiguity

	// OtherEstates counts the live resources carrying another estate's
	// marker, by estate name, sorted. An entry with an empty Estate is the
	// count discovery kept without recording which estate it belonged to
	// (see [EstateCount.Estate]).
	OtherEstates []EstateCount

	// Swept lists the resource types that were listed in full, so that
	// their unclaimed resources are the complete set the provider would
	// admit - subject to the provider-side filters described in the package
	// doc. Sorted.
	Swept []string

	// Unswept lists the types this pass cannot speak for at all, sorted by
	// type name. It is the difference between "there are none" and "nobody
	// looked", and it is never omitted from output on the grounds of being
	// empty-looking.
	Unswept []Unswept

	// ParentReads lists the untaggable children a marked, admitted parent's
	// own identity led the sweep to (issue #60), carried straight through
	// from the discovery pass: each already knows whether it also became a
	// removal ([Removals] and the plan's own resource diff both show that
	// half; this list is what says a parent read is why).
	ParentReads []ParentReadFinding
}

Result is one classification pass over one discovery result.

func Classify

func Classify(ctx context.Context, req Request) (*Result, tfdiags.Diagnostics)

Classify sorts every unclaimed live resource discovery found into foreign, bind candidate, or other estate, and records which resource types the classification can speak for at all.

It reads nothing, writes nothing, and calls no provider: the whole input is the discovery result and the configuration. Error diagnostics mean the inputs were unusable, not that anything about the live system is wrong - there is no live-system condition this package treats as an error, because "somebody else's resource exists" is not one.

func (*Result) AmbiguousFor

func (r *Result) AmbiguousFor(block string) (RenameAmbiguity, bool)

AmbiguousFor returns the ambiguity reported for one resource block.

func (*Result) CandidateFor

func (r *Result) CandidateFor(addr addrs.AbsResourceInstance) (Candidate, bool)

CandidateFor returns the candidate offered for one declared address.

func (*Result) Empty

func (r *Result) Empty() bool

Empty reports whether the classification found nothing to say about live resources at all: no foreign resources, no candidates, no other estates. It says nothing about Result.Unswept, which is reportable on its own - a run that swept nothing has an empty result and the most to explain, and nothing about Result.Renames, which is about resources this estate owns rather than about the ones it does not.

func (*Result) ForeignByID

func (r *Result) ForeignByID(id string) (Resource, bool)

ForeignByID returns the foreign resource with one live ID.

func (*Result) OtherEstateTotal

func (r *Result) OtherEstateTotal() int

OtherEstateTotal is the number of live resources carrying another estate's marker, across all estates.

func (*Result) RenameFor

func (r *Result) RenameFor(addr addrs.AbsResourceInstance) (RenameCandidate, bool)

RenameFor returns the rename candidate offered for one declared address.

func (*Result) String

func (r *Result) String() string

String renders a whole classification as a multi-line summary, for logs and test failure output.

func (*Result) SweptClean

func (r *Result) SweptClean() bool

SweptClean reports whether at least one type was swept in full and nothing unclaimed came back from any of them. This is the distinction the output must never blur: SweptClean means "looked, found none", while an empty result with no swept types means nobody looked.

func (*Result) UnsweptOf

func (r *Result) UnsweptOf(typeName string) (Unswept, bool)

UnsweptOf returns the entry for one type, and whether there is one.

type SweepGap

type SweepGap struct {
	TypeName string
	Reason   SweepGapReason
	Detail   string
}

SweepGap is one resource type the removal sweep could not cover, carried through from discovery so that a reader of this report sees the holes in it beside its contents.

func (SweepGap) String

func (g SweepGap) String() string

String renders a sweep gap on one line.

type SweepGapReason

type SweepGapReason string

SweepGapReason is why one resource type could not be swept for resources this estate owns and no longer declares.

type Tag

type Tag struct {
	Key   string
	Value string
}

Tag is one resource tag.

type Unswept

type Unswept struct {
	// TypeName is the type.
	TypeName string

	// Reason is the machine-readable why.
	Reason UnsweptReason

	// Detail is one sentence for an operator.
	Detail string
}

Unswept is one resource type this classification cannot speak for.

func (Unswept) String

func (u Unswept) String() string

String renders an unswept type on one line.

type UnsweptReason

type UnsweptReason string

UnsweptReason is why a resource type's live population is unknown to this classification.

const (
	// UnsweptNotListable is a type the provider cannot list at all.
	UnsweptNotListable UnsweptReason = "TYPE_NOT_LISTABLE"

	// UnsweptListFailed is a type whose list call errored.
	UnsweptListFailed UnsweptReason = "LIST_FAILED"

	// UnsweptEstateScoped is a type that was listed with the server-side
	// estate filter on, which hides unclaimed resources by construction.
	UnsweptEstateScoped UnsweptReason = "SCOPE_ESTATE"

	// UnsweptNotScanned is a type in the configuration that was never listed
	// because no instance of it needed discovery.
	UnsweptNotScanned UnsweptReason = "NOT_SCANNED"
)

Jump to

Keyboard shortcuts

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