Documentation
¶
Index ¶
- Constants
- Variables
- func CleanupStaleNodes(ctx context.Context, targets []CleanupTarget) error
- func DeleteTag(ctx context.Context, tag string) error
- func EnsureTags(ctx context.Context, tags []string) error
- func FindExactDeviceNodeID(ctx context.Context, hostname string) (string, int, error)
- func HostnameMatchesServiceName(hostname, service string) bool
- func InviteRoles() []string
- func NewTailscaleClient() (*tailscale.Client, error)
- func ReadTags(ctx context.Context) ([]string, error)
- func RevokePendingDeviceInvite(ctx context.Context, target DeviceTarget, id string) (string, error)
- func ValidateInviteID(id string) error
- func ValidateInviteKind(kind string) error
- func ValidateInviteRole(role string) error
- type CleanupResult
- func CleanupStaleNodesResult(ctx context.Context, targets []CleanupTarget) (CleanupResult, error)
- func CleanupStaleNodesResultWithDryRun(ctx context.Context, targets []CleanupTarget, dryRun bool) (CleanupResult, error)
- func DeleteDevicesForService(ctx context.Context, target CleanupTarget) (CleanupResult, error)
- type CleanupTarget
- type DeviceInviteOutcomeUnknown
- type DeviceTarget
- type FunnelPolicyRequest
- type Invite
- func CreateDeviceInvite(ctx context.Context, target DeviceTarget, email string, ...) (Invite, error)
- func CreateUserInvite(ctx context.Context, email, role string, printLink bool) (Invite, error)
- func ListDeviceInvitesForTarget(ctx context.Context, target DeviceTarget) ([]Invite, error)
- func ResendInvite(ctx context.Context, kind, id string, targets []DeviceTarget) (Invite, error)
- func RevokeInvite(ctx context.Context, kind, id string, targets []DeviceTarget) (Invite, error)
- type InviteList
- type InviteTargetError
- type InviteTargetStatus
- type Membership
- type PeopleInviteTargetGone
- type PolicyMutationResult
- type TailnetDevice
Constants ¶
const ( PolicyWriteNotAttempted = "not_attempted" PolicyWriteUnchanged = "unchanged" PolicyWriteChanged = "changed" PolicyWriteRejected = "rejected" PolicyWriteUnknown = "unknown" )
const ( InviteKindUser = "user" InviteKindDevice = "device" InviteRoleMember = "member" InviteRoleAdmin = "admin" InviteRoleITAdmin = "it-admin" InviteRoleNetworkAdmin = "network-admin" InviteRoleBillingAdmin = "billing-admin" InviteRoleAuditor = "auditor" )
const APIBaseURLEnv = "TSLINK_API_BASE_URL"
APIBaseURLEnv redirects the Tailscale REST API client to a loopback-only endpoint. It exists so command-layer tests can exercise the real tailapi implementation without contacting Tailscale.
const DefaultTag = "tag:tsmain"
DefaultTag is the default ACL tag applied to tslink services.
const FunnelNodeAttr = "funnel"
FunnelNodeAttr is the policy-file attribute that authorizes Funnel nodes.
Variables ¶
var ErrNoAPIClient = errors.New("no API client available")
ErrNoAPIClient means cleanup cannot use the Tailscale API because no API key is configured.
var ErrPolicyAccessDenied = errors.New("tailnet policy access forbidden")
ErrPolicyAccessDenied reports that the configured API credential can reach the tailnet policy endpoint but lacks permission to read or update it.
var ErrPolicyConflict = errors.New("tailnet policy changed concurrently; retry the operation")
ErrPolicyConflict reports that the policy ETag changed between read and write. Callers may retry the entire read-modify-write operation.
var ErrTailnetHTTPSDisabled = errors.New("tailnet HTTPS is disabled")
ErrTailnetHTTPSDisabled reports the non-policy Funnel prerequisite without issuing an ACL write. Enabling it requires the tailnet settings API and the networking_settings OAuth scope; nodeAttrs cannot express this setting.
ErrTailnetSettingsUnavailable means TSLink refused to guess whether HTTPS is enabled. An ACL POST is unsafe until the non-policy prerequisite can be read with the networking_settings OAuth scope.
Functions ¶
func CleanupStaleNodes ¶
func CleanupStaleNodes(ctx context.Context, targets []CleanupTarget) error
CleanupStaleNodes reports stale nodes that conflict with the given service targets.
func DeleteTag ¶
DeleteTag removes a tag from tagOwners using the lossless HuJSON path. A referenced tag is refused, except for TSLink's exact canonical Funnel grant, which is removed atomically with the plumbing tag so teardown cannot leave a dangling nodeAttrs reference. Returns ErrNoAPIClient if no API client is available.
func EnsureTags ¶
EnsureTags checks that the given tags exist in the ACL tagOwners. Missing tags are created with owner ["autogroup:admin"]. The Raw/Set path and surgical HuJSON AST edit preserve comments, formatting, and unknown keys. Returns ErrNoAPIClient for non-empty tag input if no API client is available.
func FindExactDeviceNodeID ¶
FindExactDeviceNodeID performs the read-only lookup used by cleanup adoption. It deliberately uses literal hostname equality and requires a TSLink tag: suffix, prefix, wildcard, regex, foreign-tagged, and first-match selection are never accepted for migration proof.
func HostnameMatchesServiceName ¶
HostnameMatchesServiceName reports whether a tailnet hostname is either the literal service name or one of the numeric collision variants tsnet appends when the plain hostname is already taken (<name>-1, <name>-2, ...). It is the same discovery predicate cleanup uses to decide which hostname-only matches to report as Protected, exported so read-only views can label a device's local family without reimplementing the rule.
Discovery only. A true result never authorizes deletion; DELETE remains gated on exact NodeID ownership proof.
func InviteRoles ¶
func InviteRoles() []string
InviteRoles returns the exact role enum accepted by the archived first-party OpenAPI contract.
func NewTailscaleClient ¶
NewTailscaleClient exposes the same fail-closed loopback override gate to higher-level callers that must pass a client factory into credentials.
func ReadTags ¶
ReadTags returns user-managed tag names from the tailnet ACL tagOwners. The shared Funnel plumbing tag is deliberately hidden from `tslink tags pull` so it is not presented as a user tag. Returns ErrNoAPIClient if no API client is available.
func RevokePendingDeviceInvite ¶
RevokePendingDeviceInvite only deletes the recorded ID on its proven node. Accepted shares are reported separately. Absence is idempotent success.
func ValidateInviteID ¶
ValidateInviteID accepts the bare decimal strings used by both first-party invite schemas. Rejecting every non-ASCII digit also keeps path separators, percent escapes, and dot segments away from the HTTP boundary.
func ValidateInviteKind ¶
ValidateInviteKind requires callers to select a namespace explicitly so a colliding numeric ID can never make TSLink mutate the other invite kind.
func ValidateInviteRole ¶
ValidateInviteRole rejects unsupported roles before any API client or network request is created.
Types ¶
type CleanupResult ¶
type CleanupResult struct {
Matched []string
WouldDelete []string
Deleted []string
Protected []string
Skipped bool
SkipReason string
ResolvedOwnershipIDs []string
}
CleanupResult describes a stale-node cleanup attempt.
func CleanupStaleNodesResult ¶
func CleanupStaleNodesResult(ctx context.Context, targets []CleanupTarget) (CleanupResult, error)
CleanupStaleNodesResult reconciles exact ownership and may issue DELETE only for a device whose NodeID appears in a CleanupTarget.
func CleanupStaleNodesResultWithDryRun ¶
func CleanupStaleNodesResultWithDryRun(ctx context.Context, targets []CleanupTarget, dryRun bool) (CleanupResult, error)
CleanupStaleNodesResultWithDryRun reconciles devices using exact NodeID ownership. Hostname matches without exact proof remain Protected.
func DeleteDevicesForService ¶
func DeleteDevicesForService(ctx context.Context, target CleanupTarget) (CleanupResult, error)
DeleteDevicesForService deletes exact recorded NodeIDs and reports every hostname-only match as protected.
type CleanupTarget ¶
CleanupTarget identifies the expected TSLink-owned device identity.
func CleanupTargetForOwnedService ¶
func CleanupTargetForOwnedService(svc registry.Service, nodeIDs []string) CleanupTarget
CleanupTargetForOwnedService attaches durable exact NodeID proof. Hostname remains discovery-only and can never authorize DELETE.
func CleanupTargetForService ¶
func CleanupTargetForService(svc registry.Service) CleanupTarget
CleanupTargetForService builds the remote cleanup ownership target for a service.
func CleanupTargetsForServices ¶
func CleanupTargetsForServices(services []registry.Service) []CleanupTarget
CleanupTargetsForServices builds remote cleanup ownership targets for services.
type DeviceInviteOutcomeUnknown ¶
type DeviceInviteOutcomeUnknown struct{ Err error }
DeviceInviteOutcomeUnknown means POST was attempted; even an HTTP refusal or unreadable response does not prove the remote side effect absent.
func (*DeviceInviteOutcomeUnknown) Error ¶
func (e *DeviceInviteOutcomeUnknown) Error() string
func (*DeviceInviteOutcomeUnknown) Unwrap ¶
func (e *DeviceInviteOutcomeUnknown) Unwrap() error
type DeviceTarget ¶
DeviceTarget combines the collision-aware hostname with the exact stable node ID recorded by the running TSLink node. Hostname matching finds candidates; NodeID is the ownership proof.
type FunnelPolicyRequest ¶
FunnelPolicyRequest describes one fused Funnel policy transaction. Tags are ordinary tagOwners entries that may be created with the legacy admin owner; Target is the shared Funnel tag and Owners are existing tag identities held by the OAuth clients that must be able to mint keys for Target.
type Invite ¶
type Invite struct {
Grant *registry.Person `json:"grant,omitempty"`
AccessState string `json:"access_state,omitempty"`
Message string `json:"message,omitempty"`
TargetKind string `json:"target_kind,omitempty"`
Kind string `json:"kind"`
ID string `json:"id,omitempty"`
Recipient string `json:"recipient,omitempty"`
Email string `json:"email,omitempty"`
Emailed bool `json:"emailed"`
InviteURL string `json:"invite_url,omitempty"`
Role string `json:"role,omitempty"`
Service string `json:"service,omitempty"`
DeviceID int64 `json:"device_id,omitempty"`
MultiUse bool `json:"multi_use,omitempty"`
AllowExitNode bool `json:"allow_exit_node,omitempty"`
Accepted bool `json:"accepted,omitempty"`
Created string `json:"created,omitempty"`
LastEmailSentAt string `json:"last_email_sent_at,omitempty"`
}
Invite is the stable TSLink representation of either first-party invite schema. InviteURL is copied verbatim from the API and is never constructed.
func CreateDeviceInvite ¶
func CreateUserInvite ¶
CreateUserInvite creates exactly one invite. email is always the named recipient; printLink controls whether it is omitted from the API body.
func ListDeviceInvitesForTarget ¶
func ListDeviceInvitesForTarget(ctx context.Context, target DeviceTarget) ([]Invite, error)
ListDeviceInvitesForTarget needs no running snapshot: the durable node ID and hostname must still match a currently owned remote device.
func ResendInvite ¶
func RevokeInvite ¶
type InviteList ¶
type InviteList struct {
Complete bool `json:"complete"`
UserInvites []Invite `json:"user_invites"`
DeviceInvites []Invite `json:"device_invites"`
DeviceTargets []InviteTargetStatus `json:"device_targets"`
Count int `json:"count"`
}
func ListInvites ¶
func ListInvites(ctx context.Context, targets []DeviceTarget) (InviteList, error)
type InviteTargetError ¶
type InviteTargetStatus ¶
type InviteTargetStatus struct {
TargetKind string `json:"target_kind,omitempty"`
Service string `json:"service"`
Checked bool `json:"checked"`
InviteCount int `json:"invite_count"`
Error *InviteTargetError `json:"error,omitempty"`
}
type Membership ¶ added in v0.3.0
type Membership string
const ( MembershipMember Membership = "member" MembershipGuest Membership = "non_member" )
func ResolveMembership ¶ added in v0.3.0
func ResolveMembership(ctx context.Context, login, controlURL string) (Membership, error)
ResolveMembership uses the complete user directory, never online devices, email domains or a persisted guest bit. Unknown is an error, not a guest.
type PeopleInviteTargetGone ¶
type PeopleInviteTargetGone struct{ Target DeviceTarget }
PeopleInviteTargetGone is evidence from a successful device listing that the recorded node and hostname are absent. Lookup failures and ownership mismatches never produce this terminal evidence.
func (*PeopleInviteTargetGone) Error ¶
func (e *PeopleInviteTargetGone) Error() string
type PolicyMutationResult ¶
PolicyMutationResult distinguishes a confirmed change from an idempotent no-op and from a write whose server-side outcome could not be observed.
func EnsureFunnelAttr ¶
func EnsureFunnelAttr(ctx context.Context, request FunnelPolicyRequest) (PolicyMutationResult, error)
EnsureFunnelAttr performs the complete Funnel policy provisioning in one Raw -> surgical patch -> Set transaction: ordinary requested tags, the shared tagOwners rule with caller-derived owner tags, and the exact-target nodeAttrs grant. It never modifies a grant whose target is not exactly the requested target. Existing wildcard or broader Funnel grants are recognized as coverage and left byte-for-byte untouched.
type TailnetDevice ¶
type TailnetDevice struct {
Hostname string
Name string
Tags []string
OS string
CreatedAt *time.Time
LastSeenAt *time.Time
ConnectedToControl bool
Authorized bool
}
TailnetDevice is the read-only projection of one TSLink-tagged tailnet device. It deliberately omits NodeID and every other node identity value: deletion authorization lives only in tailapi.CleanupStaleNodesResultWithDryRun, which matches owned[d.NodeID] exactly, and a NodeID that never leaves this package cannot be copied into a caller that widens that predicate.
func ListTSLinkDevices ¶
func ListTSLinkDevices(ctx context.Context) ([]TailnetDevice, error)
ListTSLinkDevices lists every TSLink-tagged device in the tailnet using the same one-GET device listing and the same TSLink tag predicate that cleanup adoption uses. It is strictly read-only: it issues no DELETE, records no ownership, and grants no deletion authority.
The tailnet is shared across machines, so the returned set spans every host running TSLink. Deciding which rows belong to the calling machine is the caller's job; this function has no access to any local registry.