substrate

package
v0.23.0 Latest Latest
Warning

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

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

Documentation

Overview

Package substrate is the contract GitHub issue #1118 asks for between this fork's live path and a provider family: one Substrate value per family (AWS, Kubernetes) that answers which marker surface a schema carries, how the marker is read off a live or planned object, how it is written, and which sweep client the family's provider block builds.

Before it, those answers were three dispatches that each asked the internal/live/markers predicates on their own: the projection's ownership read (markerSurfaceOf and markersOf), live-mv's surfaceOf, and live-import's ratifyOne carriers. Each was found missing a surface after the unit that should have covered it had merged (#1108, #1104, #1109). They now ask this package, and this package is the one place a new surface has to be taught. The completeness guard in internal/live/markers/seams_test.go measures the functions here the same way it measures every other seam, so a fourth surface fails every dispatch below until it is handled.

It is an extraction: every answer here is the answer the dispatch it replaced gave. GitHub issue #1589: the projection's ownership read used to ask a looser tag question than SurfaceOf (markers.HasTagsAttribute (since deleted)), kept apart in case the two ever disagreed on a real AWS or Kubernetes type. The 2026-09-26 decision package measured the disagreement empty at every pinned provider version, so the ownership read now asks SurfaceOf like every other caller and the second question is gone.

Index

Constants

View Source
const (
	FieldManagerAttr = "field_manager"
	FieldForceAttr   = "force"
)

The schema attributes that make a type field-granular (GitHub issue #1191): a top-level server-side-apply field manager name and the force flag beside it. hashicorp/kubernetes 3.2.1 serves exactly six types with both - kubernetes_labels, kubernetes_annotations, kubernetes_env, kubernetes_config_map_v1_data, kubernetes_secret_v1_data and kubernetes_node_taint - and kubernetes_manifest, the seventh type that can name a field manager, names it in a nested block instead, so it is not this shape.

View Source
const FieldGranularImportSyntax = "apiVersion=APIVERSION,kind=KIND,[namespace=NAMESPACE,]name=NAME"

FieldGranularImportSyntax is the identity string a field-granular type that names its kind renders: the shape hashicorp/kubernetes' own kubernetes_env Read parses its id with (keys in any order; measured against 3.2.1 on 2026-10-03, where an id without apiVersion, kind and name is refused with "ID must contain apiVersion, kind, and name").

View Source
const ManifestImportSyntax = "apiVersion=APIVERSION,kind=KIND,[namespace=NAMESPACE,]name=NAME"

ManifestImportSyntax is the provider's documented import id for a manifest object, the namespace segment present for a namespaced kind only.

Variables

All is every family, in the order an ordered question asks them.

AWS is last, and that is load-bearing for Substrate.SynthesizeIdentity (GitHub issue #1586): the AWS answer is the identity-schema route, which claims every type, so a family with a convention of its own has to be asked before it. TestTheCatchAllFamilyIsLast holds it (GitHub issue #1742). The surface questions are asked of the provider's own family (SurfaceOf), so for a claimed provider the order does not decide them.

Functions

func AddressCarrier added in v0.21.0

func AddressCarrier(surface markers.Surface) (key, noun string)

AddressCarrier is surface's family's [Substrate.AddressCarrier]: the key the block address is written under and what one entry there is called. Empty for the zero Surface, which carries no address.

func AddressInMarkers

func AddressInMarkers(surface markers.Surface) bool

AddressInMarkers reports whether surface's marker map holds the tofu-address key, which is its family's [Substrate.AddressInMarkers]. False for the zero Surface.

func CarrierPaths added in v0.21.0

func CarrierPaths(surface markers.Surface) []cty.Path

CarrierPaths is surface's family's [Substrate.CarrierPaths]: the paths of the maps its marker lives in. Nil for the zero Surface.

func CarrierPhrase

func CarrierPhrase(surface markers.Surface) string

CarrierPhrase names where surface's marker map lives ("tags attribute", "metadata.labels map"), for the one refusal that has to tell an operator the provider returned no such map. Empty for the zero Surface, which has no map to be missing: a caller asks this only of a surface it read.

func CarriesAddress

func CarriesAddress(surface markers.Surface) bool

CarriesAddress reports whether an object on surface carries its block address, which is its family's Substrate.CarriesAddress. Both families do: AWS in the tofu-address tag, Kubernetes in the address annotation beside the estate label (GitHub issue #1641, step 3 of the ruling on #1605). False for the zero Surface.

On Kubernetes an object can still lack the annotation - one an older build created, or one migrated from stock state before live-import stamped it - so a reader that needs the address of one particular object asks that object, not this.

func CreateCollidesOnKey

func CreateCollidesOnKey(surface markers.Surface) bool

CreateCollidesOnKey reports whether, for a declared resource on surface, an object read at its identity means the resource's own create would be refused by the server as a duplicate: the first half of GitHub issue #1546's ruling, "its identity is a server-enforced unique key". False for the zero Surface.

True for the Kubernetes label surface only, and each family's answer says why its other surfaces are excluded ([kubernetes.CreateCollidesOnKey], [aws.CreateCollidesOnKey]).

func CreatedObject

func CreatedObject(surface markers.Surface, created Created) string

CreatedObject is [Substrate.CreatedObject] asked of surface's family. A surface with no family gets the object's id, the one attribute every provider's object has.

func FieldGranularNamesKind added in v0.22.0

func FieldGranularNamesKind(block *configschema.Block) bool

FieldGranularNamesKind reports whether a field-granular schema names the patched object's apiVersion and kind in its own configuration (the top-level required api_version and kind of kubernetes_labels, kubernetes_annotations and kubernetes_env). The other three types patch one fixed kind, which the provider hardcodes.

func FieldGranularShape added in v0.22.0

func FieldGranularShape(block *configschema.Block) (namespaced bool, ok bool)

FieldGranularShape reports whether block is the field-granular Kubernetes shape (GitHub issue #1191, ruled 2026-10-03): a resource that writes some fields of an object it does not own, under a server-side-apply field manager it names itself. Read from the schema, never from a type-name list, for the same reason ObjectMetaShape is:

  • a top-level optional string FieldManagerAttr and a top-level optional bool FieldForceAttr;
  • a "metadata" nested list block of at most one item with a settable string "name", and WITHOUT the computed uid and the labels map ObjectMetaShape requires - the block names the patched object, it is not this resource's own object metadata.

The predicate and ObjectMetaShape can therefore never both answer true for one schema.

What the shape owns is fields, not an object, so its ownership marker is not a label: it is the field manager every write is made under, markers.FieldManagerFor's "choudoufu:<estate>", and the patched object's own estate label (if it has one) is irrelevant to it.

func KnownMarkersOf

func KnownMarkersOf(surface markers.Surface, obj cty.Value) (map[string]string, bool)

KnownMarkersOf is MarkersOf for a caller that must not read an unknown marker map as an empty one. It reports false when the object is null or unknown, when any carrier path reaches an unknown value (the map, or anything on the way to it), or when the surface reader itself reports false. A value beside the carrier being unknown, such as a planned object's metadata.resource_version, does not hide a marker that is known.

A carrier path that ends early at a null, or indexes past an empty list, is not unknown: the reader decides what that object carries.

func KubernetesSweepAttrs

func KubernetesSweepAttrs(val cty.Value, ok bool) kubesweep.Attrs

KubernetesSweepAttrs reads the connection arguments the Kubernetes sweep understands off the evaluated provider block. A marked value is left unread rather than unmarked - the same rule internal/command's projectionProviders.region applies to a sensitive region - EXCEPT for the three arguments that are themselves the credential, which are unmarked and read: see secret below, and GitHub issue #1527 for what leaving them unread cost. It moved here from internal/command with the sweep-client choice (GitHub issue #1118).

func ManualMarkFix

func ManualMarkFix(surface markers.Surface, created Created, want map[string]string, facts Facts) string

ManualMarkFix is [Substrate.ManualMarkFix] asked of surface's family. A surface with no family - the zero Surface, or one For does not recognise - gets the generic sentence naming only the markers, since there is no family to name a command for.

func MarkerNoun

func MarkerNoun(surface markers.Surface) string

MarkerNoun is what one entry of surface's marker map is called ("tag", "label"), or "marker" for the zero Surface.

func MarkersOf

func MarkersOf(surface markers.Surface, obj cty.Value) (map[string]string, bool)

MarkersOf reads the marker map off an object from wherever surface keeps it. The second return is the surface reader's own: false means the object has no such map at all, which on a type whose schema declares one is a provider bug and never a licence to adopt. False for the zero Surface.

func NotACarrier

func NotACarrier(providerType string, block *configschema.Block, typeName string) string

NotACarrier explains why a resource type of provider providerType has nowhere to carry an ownership marker, in its family's own words: the AWS "no tags map this configuration can set" (or the tags map the marker vocabulary cannot round-trip, markers.NotAMarkerSurface), and the Kubernetes "no metadata.labels map". A provider with no family gets a sentence that names no carrier rather than the AWS one. The caller has already established the type carries no surface (SurfaceOf false).

func ObjectMetaShape

func ObjectMetaShape(block *configschema.Block) (namespaced bool, ok bool)

ObjectMetaShape reports whether block carries Kubernetes object metadata, and whether the kind it describes is namespaced. Read from the schema, never from a type-name list, for the same reason markers.Taggable is.

It is the shape internal/live/identity's metadata.go documents: a "metadata" nested block of list nesting with at most one item (declared by max_items, or by markers.UndeclaredSingleObjectMetadata where a plugin-framework schema cannot declare it), holding a settable "name", a computed "uid", a settable "labels" map, and - for a namespaced kind - a settable "namespace".

func ObjectString added in v0.22.0

func ObjectString(obj cty.Value, name string) string

ObjectString reads one top-level string attribute off obj, or "" when it is absent, null, unknown, marked or not a string.

func PostCreateNeeded

func PostCreateNeeded(surface markers.Surface, created Created, facts Facts) (string, bool)

PostCreateNeeded is [Substrate.PostCreateNeeded] asked of surface's family. False for the zero Surface.

func SurfaceOf

func SurfaceOf(providerType string, block *configschema.Block) (markers.Surface, bool)

SurfaceOf is the marker surface a resource type of provider providerType carries, read off its schema, or false when it has none.

GitHub issue #1742: the family is chosen by provider, the rule NotACarrier and ForProvider use, and only that family is asked. A schema alone cannot tell two families apart - an azurerm type's tags map is shaped exactly like an AWS type's - so dispatching on it handed one family's types to whichever claimed the shape first. A provider no family claims (azurerm, datadog today) keeps the compatible default the node stamp has always had: every family is asked in All's order and the first surface its schema carries is the one stamped (maintainer ruling on #1742, 2026-10-02).

This is the question live-mv's surface switch and live-import's carrier choice asked.

func Sweeps

func Sweeps(providerType string) bool

Sweeps reports whether providerType names a family whose own sweep leg finds its objects independently of internal/live/identity's admission table (GitHub issue #1581): a type belonging to such a family needs no row there to be found again once its last block is removed.

It is the family's Substrate.SweepFindsUnadmitted (GitHub issue #1742: it used to compare the sweep kind to SweepLabelList, which a third family's own sweep could never answer). Only Kubernetes qualifies today: its leg lists every kind the cluster serves and joins the result against the estate's objects. AWS's own sweep (SweepTaggingIndex) is the admission table read a different way, so a type with no row gets nothing extra from it, and neither does an unregistered provider ForProvider does not recognise at all.

This is the question internal/live/identity's no-orphan-recovery warning needs, and it is asked by provider - the resource's own resolved provider configuration, never by a type's schema shape. A type can share a Kubernetes-shaped schema (an object-metadata block, say) with an unrelated provider's type by coincidence; only the provider says which sweep leg, if any, will actually look for it again.

func TombstonesRead added in v0.22.0

func TombstonesRead(providerType string) bool

TombstonesRead reports whether a record tombstone written for an object of providerType's family can ever be read: true unless the family is known and its marker dies with the object (Substrate.MarkerOutlivesObject). An unregistered provider answers true, which keeps the tombstone, the conservative side.

Types

type AWSCreateTagFacts

type AWSCreateTagFacts interface {
	CloudControlTypeOrService(tfType string) (string, bool)
	TagsAfterCreate(cfnType string) bool
}

AWSCreateTagFacts is the AWS family's entry in Facts (GitHub issue #1708): live/mapping.json's Terraform-to-CloudFormation join and live/registry.json's tagging.tag_on_create. *internal/live/registry.Roster implements it, nil included (every answer false); internal/command puts the embedded roster under AWS's name. An interface so this package stays below the registry.

type Created

type Created struct {
	Addr     addrs.AbsResourceInstance
	Provider addrs.AbsProviderConfig
	// Object is the object ApplyResourceChange returned; cty.NilVal where
	// the question is asked before the create.
	Object cty.Value
}

Created is the instance a post-create question or write is about: its address, the provider configuration it was applied under, and the object the provider returned. internal/live/projection's CreatedInstance, which the post-create writer is handed, is this type.

func (Created) Type

func (c Created) Type() string

Type is the created instance's resource type name.

type Facts

type Facts map[string]any

Facts is what the run knows about types beyond their schemas, keyed by family Substrate.Name and built once per run by the command layer (for AWS, the registry roster: see AWSCreateTagFacts). Each family reads its own entry and asserts it to the shape it owns; an absent entry, a nil one, or one of another shape reads as a family with no facts, which is an ordinary state (a run whose embedded artifacts did not parse).

func (Facts) Of

func (f Facts) Of(family string) any

Of is family's entry, or nil.

type Hold

type Hold struct {
	// Controller is the controller that holds it: ACK, Crossplane, Helm.
	Controller string
	// HeldBy names the controller and the object of its that holds this
	// one, in one line an operator can act on: "Helm release web/web",
	// "ACK s3 controller (s3-v1.0.14), custom resource in namespace team-a".
	HeldBy string
}

Hold is a family's answer for a held object.

func ControllerHeld

func ControllerHeld(ev HoldEvidence) (Hold, bool)

ControllerHeld asks every family in All's order whether the object ev was read from is held by a controller, and the first to answer decides. The families read disjoint fields of ev, so the order cannot decide an answer today.

type HoldEvidence

type HoldEvidence struct {
	// Tags are an AWS resource's tags.
	Tags map[string]string
	// Annotations are a Kubernetes object's metadata.annotations.
	Annotations map[string]string
}

HoldEvidence is what a sweep leg read off one live object that a family may recognise a controller's hold in. A leg fills what its object carries and leaves the rest empty; each family reads only its own field.

type HoldRecognition

type HoldRecognition struct {
	Mechanism string
	Keys      []string
}

HoldRecognition describes how a family recognises a held object, for the capability matrix (live/substrates.json). Keys empty means the family recognises no controller at all.

type IdentityComponent

type IdentityComponent struct {
	Literal          string
	Attrs            []string
	Block            string
	Path             []string
	OmitIfAbsent     bool
	SameNameIdentity bool
}

IdentityComponent is the subset of identity.Component a family's synthesis sets. Each field means what the identity package's field of the same name means, except SameNameIdentity, which stands for identity.SameNameIdentity in IdentityAttr.

type LabelListSweeper

type LabelListSweeper struct {
	*kubesweep.Client
}

LabelListSweeper is the Kubernetes family's client: the cluster client built from the provider block, whose methods it carries (kubesweep.Sweeper, kubesweep.LabelPatcher).

func (LabelListSweeper) SweepKind

func (LabelListSweeper) SweepKind() Sweep

SweepKind is SweepLabelList.

type Substrate

type Substrate interface {
	// Name is the family's name, which is also the provider type name
	// [ForProvider] matches.
	Name() string

	// Surfaces are the marker surfaces this family's types carry. No
	// surface belongs to two families.
	Surfaces() []markers.Surface

	// SurfaceOf is the surface a resource type's schema carries when it is
	// one of this family's, read off the schema and never off the type
	// name.
	SurfaceOf(block *configschema.Block) (markers.Surface, bool)

	// MarkersOf reads the marker map off an object from wherever surface,
	// one of this family's, keeps it.
	MarkersOf(surface markers.Surface, obj cty.Value) (map[string]string, bool)

	// Writes is how surface's marker, one of this family's, is written.
	Writes(surface markers.Surface) Writes

	// CarriesAddress is whether this family's objects carry the block
	// address beside tofu-estate, so that the sweep can bind a live object
	// back to the block that made it: the AWS tofu-address tag, or the
	// Kubernetes address annotation (GitHub issues #1639 to #1641). Where
	// the address sits is [Substrate.AddressInMarkers]'s question.
	CarriesAddress() bool

	// Sweep is which sweep client the family's provider block builds.
	Sweep() Sweep

	// SweepFindsUnadmitted is whether the family's sweep finds its objects
	// independently of internal/live/identity's admission table, drawing
	// its universe from the provider and the service rather than from the
	// table's rows, so a type with no row is still found once its last
	// block is removed ([Sweeps], GitHub issue #1742).
	SweepFindsUnadmitted() bool

	// MarkerOutlivesObject is whether a deleted object's marker can still
	// be listed after the object is gone, which is the only thing a record
	// tombstone is for (internal/live/projection's tombstoneFields): AWS's
	// tagging index lists a terminated instance's tags for a while. On a
	// family whose marker is a field of the object it is false, a tombstone
	// has no reader, and a destroy deletes the record instead (GitHub issue
	// #1883; live/MARKERS.md's record table).
	MarkerOutlivesObject() bool

	// NewSweeper builds the family's estate-sweep client from its provider
	// block's evaluated configuration (ok false when the run holds none):
	// nil with no error for a family whose sweep runs through the
	// configured provider itself ([SweepTaggingIndex]), and the error for a
	// block the client cannot be built from. The client is a [Sweeper],
	// never a family's concrete type (GitHub issue #1580).
	NewSweeper(providerConfig cty.Value, ok bool) (Sweeper, error)

	// SynthesizeIdentity is how this family's schema identifies an
	// instance of a type the ratified table does not cover, or false when
	// the schema is not one of this family's shapes. Asked in [All]'s
	// order by internal/live/identity's synthesizeTypeIdentity, and the
	// first family to answer decides.
	SynthesizeIdentity(typeName string, schema providers.Schema) (SynthesizedIdentity, bool)
	// contains filtered or unexported methods
}

Substrate is one provider family's answers. There is one value per family (AWS, Kubernetes), each in its own file, and the package-level functions below dispatch over All. A family answers only for its own surfaces; the dispatch is what answers for a schema or a surface whose family is not yet known.

The split is also what keeps the completeness guard (internal/live/markers/seams_test.go) able to see the callers. The guard credits a caller in another package with the surfaces a callee references itself, and the dispatch functions here reference none: they reach the markers predicates only through the family methods. So a caller that asks SurfaceOf and then acts per surface handles exactly the markers.Surface constants it names, and one that forgets a surface is red.

var AWS Substrate = aws{}

AWS is the hashicorp/aws family: tofu-estate and tofu-address in a settable top-level tags map.

var Kubernetes Substrate = kubernetes{}

Kubernetes is the hashicorp/kubernetes family: the tofu-estate label, in metadata[0].labels or, for kubernetes_manifest, in manifest.metadata.labels, with the block address in the markers.AddressAnnotation annotation beside it (#1639).

func For

func For(surface markers.Surface) Substrate

For is the family a surface belongs to, or nil for the zero Surface.

func ForProvider

func ForProvider(providerType string) (Substrate, bool)

ForProvider is the family a provider type name belongs to ("aws", "kubernetes"). It matches the type name alone, which is what every provider-family check it replaced asked.

type Sweep

type Sweep string

Sweep is which estate-sweep client a family's provider block builds.

const (
	// SweepTaggingIndex is the AWS sweep: the discovery legs (the
	// Resource Groups Tagging API index, the provider's own list
	// resources, Cloud Control, direct reads), all driven through the
	// configured provider itself, so no client is built from the block.
	SweepTaggingIndex Sweep = "tagging-index"

	// SweepLabelList is the Kubernetes sweep: one label-selected list per
	// served kind, through a cluster client built from the provider
	// block's own connection arguments (Substrate.NewSweeper).
	SweepLabelList Sweep = "label-list"
)

type Sweeper

type Sweeper interface {
	SweepKind() Sweep
}

Sweeper is a family's estate-sweep client as Substrate.NewSweeper builds it from the provider block. SweepKind is the sweep it serves, which must be its family's own Substrate.Sweep: internal/command's leg for that sweep checks it, then asks the client for the capability the leg lists through (for SweepLabelList, kubesweep.Sweeper), never for a family's concrete type, so a third family's client plugs in by naming a sweep and serving its leg (GitHub issues #1580, #1742).

type SynthesizedIdentity

type SynthesizedIdentity struct {
	// FromIdentitySchema: the provider's own resource identity schema
	// decides, through internal/live/identity's identity-schema route (which
	// reads the full schema map and the configuration signal, neither of
	// which a family sees). Every other field is empty when it is set.
	FromIdentitySchema bool

	// NonAWSProvider is identity.TypeIdentity's field of the same name.
	NonAWSProvider bool

	// Components, ImportSyntax and IdentityAttrs are the entry's, in
	// identity.TypeIdentity's terms.
	Components    []IdentityComponent
	ImportSyntax  string
	IdentityAttrs []string
}

SynthesizedIdentity is a family's reading of one type's identity from its schema.

func SynthesizeIdentity added in v0.22.0

func SynthesizeIdentity(typeName string, schema providers.Schema) (SynthesizedIdentity, bool)

SynthesizeIdentity asks every family in All's order how typeName's schema identifies an instance, and the first to answer decides (internal/live/identity's synthesizeTypeIdentity). The answer is AWS's identity-schema route (SynthesizedIdentity.FromIdentitySchema) for any type no earlier family claims, which is why a family placed after AWS would never be asked (GitHub issue #1742).

type Write

type Write string

Write is how a marker reaches a live object.

const (
	// WriteInCreate: the marker rides in the create call's own arguments,
	// put there by the node-path stamp (internal/live/projection).
	WriteInCreate Write = "in-create"

	// WriteTagsPlan: a plan-then-apply through the provider that changes
	// the tags map and nothing else, refused if it would change anything
	// more (internal/live/liveimport's tags.go, internal/live/mv's
	// rewrite.go).
	WriteTagsPlan Write = "tags-only-plan"

	// WriteLabelsPlan: the same, confined to metadata[0].labels
	// ([markers.WithLabels]; liveimport's labels.go, mv's label.go).
	WriteLabelsPlan Write = "labels-only-plan"

	// WriteAPIPatch: one merge patch of the label straight to the API
	// server under the caller's own credential, sent first with
	// dryRun=All and refused if the answer changes anything beyond the
	// labels (kubesweep.LabelPatcher; liveimport's manifest.go, ruled on
	// #1109 and #1104).
	WriteAPIPatch Write = "api-patch"
)
const (
	// WriteTaggingAPI: the Resource Groups Tagging API's TagResources,
	// addressed by the arn the provider returned, issued after the create
	// of a type whose create call cannot carry tags (live/registry.json's
	// tag_on_create false, GitHub issue #1084).
	WriteTaggingAPI Write = "tagging-api"

	// WriteNeverNeeded: the create call always carries the marker, so no
	// write follows it. Named rather than left empty so that a family that
	// has not answered is distinguishable from one that answered "never".
	WriteNeverNeeded Write = "never-needed"
)

func PostCreateWrite added in v0.21.0

func PostCreateWrite(surface markers.Surface, created Created, facts Facts) (reason string, write Write, needed bool, err error)

PostCreateWrite is the post-create answer for a create of created whose schema carries surface: whether the create withholds the marker ([Substrate.PostCreateNeeded]), the reason, and the write that puts it back (Writes.PostCreate). err is non-nil when needed is true and the family's answers cannot mark the object:

  • the surface names no write, or WriteNeverNeeded;
  • created.Provider is set and the family's MarkerWriter names a different write than the surface does;
  • the family carries the block address (Substrate.CarriesAddress) outside its marker map ([Substrate.AddressInMarkers] false), where the post-create write, which sets the marker map, cannot put it.

The create side asks it with only created.Addr set, as [Substrate.PostCreateNeeded] is asked, so the MarkerWriter check is the write side's. False, with no error, for the zero Surface.

type Writes

type Writes struct {
	// Create is a resource this run creates.
	Create Write
	// Adopt is an existing object a migration (live-import -approve) or a
	// move between estates (live-mv -from-estate) marks.
	Adopt Write
	// PostCreate is a resource this run creates whose create call cannot
	// carry the marker, so it is written onto the object once the create
	// returns (GitHub issue #1587, [WriteTaggingAPI], [WriteNeverNeeded]).
	PostCreate Write
}

Writes is how one surface's marker is written, by occasion.

func WritesOf

func WritesOf(surface markers.Surface) Writes

WritesOf is how surface's marker is written. Every surface is written in the create call; what differs is how an existing object is marked. The zero Writes for the zero Surface.

Jump to

Keyboard shortcuts

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