refresolver

package
v1.7.0 Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Overview

Package refresolver fetches Tekton Tasks/Pipelines referenced via taskRef.resolver / pipelineRef.resolver. It is intentionally distinct from internal/resolver, which performs $(...) variable substitution. The two packages share no types and do not depend on each other; the naming separation exists because both senses of "resolver" appear in the Tekton spec.

Phase 1 (Track 1 #9) lands the type scaffolding and an "inline" stub resolver used by the test harness. Concrete resolvers (git, hub, http, bundles, cluster) and the remote ResolutionRequest driver land in subsequent phases. See docs/superpowers/plans/2026-05-04-resolvers.md.

Index

Constants

View Source
const HTTPTokenEnv = "TKNACT_HTTP_RESOLVER_TOKEN"

HTTPTokenEnv is the environment variable name the http resolver consults for a bearer token. An explicit HTTPOptions.Token wins over the env var.

Variables

View Source
var (
	// ErrResolverNotRegistered fires when no Resolver is registered
	// for the requested name. In Phase 1, every name except "inline"
	// triggers this.
	ErrResolverNotRegistered = errors.New("refresolver: resolver not registered")

	// ErrResolverNotAllowed fires when the requested name is registered
	// but excluded by the active allow-list (--resolver-allow=...).
	ErrResolverNotAllowed = errors.New("refresolver: resolver not allowed")

	// ErrInlineNoData fires when the inline resolver is asked for a
	// name the test harness didn't preload. Distinct from
	// ErrResolverNotRegistered so tests can branch on harness misuse
	// vs Phase-1-not-implemented errors.
	ErrInlineNoData = errors.New("refresolver: inline resolver has no data for the requested key")
)

Sentinel errors. Engine code uses errors.Is to branch on these.

View Source
var ErrClusterContextRequired = errors.New("refresolver: cluster resolver requires a kubeconfig context (set --cluster-resolver-context or add `cluster` to --resolver-allow with KUBECONFIG explicitly set)")

ErrClusterContextRequired fires when the cluster resolver is asked to resolve without a kubeconfig context — either explicitly named via --cluster-resolver-context or implied by KUBECONFIG plus a current-context. The resolver refuses by default to avoid silently reading from whatever KUBECONFIG happens to point at.

Functions

func CacheKey

func CacheKey(resolverName string, params map[string]string) string

CacheKey computes the per-(resolver, substituted-params) cache key per spec §3:

sha256(resolver-name + "\x00" + sortedKVs(SUBSTITUTED-params))

The hash is computed AFTER the engine has substituted each param's $(...) references against the dispatch-time resolver.Context, so two PipelineTasks resolving the same upstream Pipeline / Task via the same resolver but whose `resolver.params` substitute to different values yield different cache keys (and miss the per-run cache independently). This invariant is exercised by TestRunOneDoesNotCacheAcrossDifferentSubstitutedParams.

Types

type BundlesOptions

type BundlesOptions struct {
	// AllowInsecureHTTP opts the bundles resolver into plain HTTP for
	// non-loopback registries. Loopback registries (127.0.0.1, ::1,
	// localhost) always permit plain HTTP so unit tests using
	// httptest.NewServer + go-containerregistry's pkg/registry work
	// out of the box.
	//
	// Threaded through from --resolver-allow-insecure-http on the run
	// command (CI-only escape hatch). The default keychain via
	// ~/.docker/config.json is honored regardless.
	AllowInsecureHTTP bool

	// Keychain overrides the authentication source. Production callers
	// pass nil; the resolver then uses authn.DefaultKeychain which reads
	// ~/.docker/config.json. Tests may inject a custom keychain (e.g.
	// authn.NewMultiKeychain).
	Keychain authn.Keychain

	// RemoteOptions, when non-nil, replaces the default remote.Option
	// list passed to go-containerregistry. Tests use this to inject
	// registry transports without going through global state.
	RemoteOptions []remote.Option
}

BundlesOptions configures a bundles resolver. All fields optional.

type BundlesResolver

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

BundlesResolver implements Resolver for taskRef.resolver: bundles.

Resolver params:

bundle   required. OCI ref like gcr.io/foo/catalog:v1.
name     required. metadata.name of the resource to extract.
kind     optional, default "task". One of "task", "pipeline".

On a happy path the resolver pulls the named OCI image, walks each layer's annotations looking for the matching (kind, name) pair, and returns the YAML bytes embedded in that layer's tar entry.

func NewBundlesResolver

func NewBundlesResolver(opts BundlesOptions) *BundlesResolver

NewBundlesResolver constructs a bundles resolver.

func (*BundlesResolver) Name

func (b *BundlesResolver) Name() string

Name implements Resolver.

func (*BundlesResolver) Resolve

func (b *BundlesResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver. See type-level docs for the param shape.

type CacheEntry

type CacheEntry struct {
	Resolver string    `json:"resolver"`
	Key      string    `json:"key"`
	Path     string    `json:"path"`
	Size     int64     `json:"size"`
	ModTime  time.Time `json:"mod_time"`
}

CacheEntry describes one row in `tkn-act cache list`.

type ClusterResolver

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

ClusterResolver implements Resolver for taskRef.resolver: cluster.

Resolver params:

name       required. metadata.name of the resource to read.
kind       optional, default "task". One of "task", "pipeline".
namespace  optional, default "default".

The resolver reads via a kube dynamic client built from --cluster-resolver-kubeconfig (or KUBECONFIG / ~/.kube/config) at the context named by --cluster-resolver-context. The result is serialized back to YAML so the engine can feed it through the standard loader path.

SECURITY: this resolver is OFF BY DEFAULT in NewDefaultRegistry. KUBECONFIG may point at a production cluster; we require the user to opt in explicitly via --resolver-allow=...,cluster or by setting --cluster-resolver-context.

func NewClusterResolver

func NewClusterResolver(opts ClusterResolverOptions) (*ClusterResolver, error)

NewClusterResolver constructs a cluster resolver. Returns an error if neither opts.Dynamic nor a usable kubeconfig+context can be found — that's distinct from ErrClusterContextRequired (which fires at resolve-time when the registry decides whether to dispatch).

func (*ClusterResolver) Name

func (c *ClusterResolver) Name() string

Name implements Resolver.

func (*ClusterResolver) Resolve

func (c *ClusterResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver. See type-level docs for the param shape.

type ClusterResolverOptions

type ClusterResolverOptions struct {
	// Context names the kubeconfig context to read from. Empty means
	// "use the kubeconfig's current-context."
	Context string

	// Kubeconfig overrides the kubeconfig file path. Empty falls back to
	// $KUBECONFIG / ~/.kube/config per `clientcmd.NewDefaultClientConfigLoadingRules`.
	Kubeconfig string

	// Dynamic, when non-nil, replaces the dynamic client constructed
	// from the kubeconfig. Tests use this to inject a fake client.
	Dynamic dynamic.Interface
}

ClusterResolverOptions configures a cluster resolver. All fields are optional; the production CLI plumbs values from --cluster-resolver-context and --cluster-resolver-kubeconfig.

type DiskCache

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

DiskCache is the on-disk resolved-bytes cache. Entries are keyed by (resolver, sortedKVs(SUBSTITUTED-params)) per the spec §3 cache-key invariant. The on-disk layout is:

<root>/<resolver>/<key>.yaml      // resolved bytes
<root>/<resolver>/<key>.json      // metadata (resolver, params, source, sha256, fetched-at)

Splitting by resolver lets `tkn-act cache prune` / `cache list` scope operations to a single resolver if desired, and matches the spec's documented layout.

The Cache is concurrency-safe across processes only at the OS-level — two tkn-act runs racing to populate the same key may both write; the last writer wins (and both bytes are content-equivalent if the upstream is deterministic, which it is for git/hub/http/bundles by their cache-key invariants).

func NewDiskCache

func NewDiskCache(root string) *DiskCache

NewDiskCache returns a DiskCache rooted at root. root may not exist yet — the first Put will MkdirAll.

func (*DiskCache) Clear

func (c *DiskCache) Clear() (int, error)

Clear deletes every entry under the cache root. Returns the number of entries removed. A missing root is a no-op.

func (*DiskCache) Get

func (c *DiskCache) Get(req Request) (Resolved, bool, error)

Get returns the cached Resolved for req. ok=false on miss; non-nil err only on filesystem corruption (truncated meta JSON, etc.).

The returned Resolved has Cached=true so callers don't need to set it separately.

func (*DiskCache) Has

func (c *DiskCache) Has(req Request) bool

Has returns true iff a cached entry exists for req. Errors reading the filesystem are conservative: the file is treated as missing.

func (*DiskCache) List

func (c *DiskCache) List() ([]CacheEntry, error)

List walks the cache root and returns every cached blob. Errors reading individual sub-directories abort with the partial list.

func (*DiskCache) PruneOlderThan

func (c *DiskCache) PruneOlderThan(older time.Duration) (int, error)

PruneOlderThan deletes every cache entry whose mod-time is older than cutoff (now - older). Returns the number of entries pruned. A missing root is a no-op.

func (*DiskCache) Put

func (c *DiskCache) Put(req Request, resolved Resolved) error

Put writes resolved.Bytes + a small JSON metadata sidecar under the cache root. Existing entries are overwritten.

func (*DiskCache) Root

func (c *DiskCache) Root() string

Root returns the cache's filesystem root.

type GitResolver

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

GitResolver implements Resolver for taskRef.resolver: git. It honors the standard Tekton resolver params:

url        (required) — repo URL (file://, https://, ssh://, git@..)
revision   (default "main") — branch, tag, or full SHA
pathInRepo (required) — relative path to the YAML inside the repo

The implementation uses go-git for cross-platform portability (no git CLI exec on the host required). Clones are shallow (Depth: 1) for the happy path and land in <cacheDir>/git/<sha256(url+revision)>/repo/. On a cache hit (the directory already exists), bytes are served from the cached working tree without any network IO.

HTTPS / file:// URLs are accepted by default; plain http:// is refused unless AllowInsecureHTTP is true. SSH URLs (ssh:// or git@host:path) are passed through to go-git, which uses ssh-agent transparently when present (no in-tkn-act key handling).

func NewGit

func NewGit(cacheDir string) *GitResolver

NewGit returns a GitResolver that caches under cacheDir. cacheDir may be empty — in which case clones go to a tempdir and are cleaned up at the end of each Resolve call (no cache reuse). Production callers should pass a non-empty cacheDir (the CLI default is $XDG_CACHE_HOME/tkn-act/resolved).

func (*GitResolver) Name

func (g *GitResolver) Name() string

Name implements Resolver.

func (*GitResolver) Resolve

func (g *GitResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver. See type-level docs for the param shape.

func (*GitResolver) SetAllowInsecureHTTP

func (g *GitResolver) SetAllowInsecureHTTP(b bool)

SetAllowInsecureHTTP toggles whether plain http:// URLs are accepted. The Registry constructor wires this from Options.AllowInsecureHTTP.

type HTTPOptions

type HTTPOptions struct {
	// AllowInsecureHTTP opts the resolver into plain http:// URLs against
	// non-loopback hosts. Loopback URLs are always allowed (so unit tests
	// using httptest.NewServer work). The CLI plumbs this from
	// --resolver-allow-insecure-http (a CI-only escape hatch).
	AllowInsecureHTTP bool
	// Token, if non-empty, is sent as `Authorization: Bearer <token>`. If
	// empty the resolver consults the TKNACT_HTTP_RESOLVER_TOKEN env var
	// at Resolve time; a non-empty env value is then used.
	Token string
	// Client overrides http.Client. Tests may inject a httptest server's
	// transport. The default has a 30-second timeout.
	Client *http.Client
}

HTTPOptions configures an http resolver. All fields optional.

type HTTPResolver

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

HTTPResolver fetches Tasks/Pipelines from a plain HTTPS URL.

Resolver params:

url   required. https:// by default; http:// only against loopback or
      with --resolver-allow-insecure-http.

5xx responses retry once with a 250ms backoff. Bearer tokens come from HTTPOptions.Token (precedence) or the TKNACT_HTTP_RESOLVER_TOKEN env var.

func NewHTTPResolver

func NewHTTPResolver(opts HTTPOptions) *HTTPResolver

NewHTTPResolver constructs an http resolver.

func (*HTTPResolver) Name

func (h *HTTPResolver) Name() string

Name implements Resolver.

func (*HTTPResolver) Resolve

func (h *HTTPResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver.

type HubOptions

type HubOptions struct {
	// BaseURL points at a Tekton Hub HTTP API. Default
	// https://api.hub.tekton.dev. HTTPS is required (the spec is firm
	// about hub being HTTPS-only).
	BaseURL string
	// Token, if non-empty, is sent as `Authorization: Bearer <token>`.
	Token string
	// Client overrides the http.Client. Tests inject httptest server
	// transport when needed; the default http.DefaultClient with a
	// 30-second timeout is used otherwise.
	Client *http.Client
}

HubOptions configures a hub resolver. All fields optional.

type HubResolver

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

HubResolver fetches Tasks/Pipelines from a Tekton Hub HTTP API per https://github.com/tektoncd/hub. It honors the public API shape:

GET <baseURL>/v1/resource/<catalog>/<kind>/<name>/<version>/yaml

Resolver params:

name      required
version   optional, default "latest"
kind      optional, default "task"
catalog   optional, default "tekton"

HTTPS is required regardless of the run-level --resolver-allow-insecure-http flag (that flag scopes to the http resolver only). 5xx responses are retried once with a 250ms backoff; 404s are surfaced with a "not found" diagnostic.

func NewHubResolver

func NewHubResolver(opts HubOptions) *HubResolver

NewHubResolver constructs a hub resolver with the given options.

func (*HubResolver) Name

func (h *HubResolver) Name() string

Name implements Resolver.

func (*HubResolver) Resolve

func (h *HubResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver.

type InlineResolver

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

InlineResolver is a magic resolver used by the test harness. Tests preload (key, bytes) pairs via Add; the engine's lazy-dispatch path invokes Resolve with the same key in Request.Params["name"]. Real resolvers (git, hub, ...) land in Phase 2-4.

func NewInlineResolver

func NewInlineResolver() *InlineResolver

NewInlineResolver returns an empty InlineResolver. Use Add to load bytes the engine will look up by Request.Params["name"].

func (*InlineResolver) Add

func (i *InlineResolver) Add(key string, bytes []byte)

Add registers bytes for the given key. The engine's resolver Request must carry Params["name"] equal to key for the inline lookup to fire.

func (*InlineResolver) Name

func (i *InlineResolver) Name() string

Name implements Resolver.

func (*InlineResolver) Resolve

func (i *InlineResolver) Resolve(_ context.Context, req Request) (Resolved, error)

Resolve implements Resolver. The lookup key is Request.Params["name"]; missing or unknown names produce ErrInlineNoData.

type Kind

type Kind int

Kind disambiguates whether a Request resolves to a Task or a Pipeline.

const (
	KindTask Kind = iota
	KindPipeline
)

type Options

type Options struct {
	// Allow restricts which resolver names will dispatch. Empty
	// means "no allow-list applied" (every registered resolver
	// dispatches). The CLI default in Phase 1 is
	// {"git","hub","http","bundles"} — none of those are registered
	// yet, but the validator's allow-list checks already use this
	// list.
	Allow []string
	// CacheDir is the on-disk cache location. Phase 1 stores it but
	// does not yet read or write it (Phase 6 wires the cache).
	CacheDir string
	// Offline rejects any cache miss with an error. Phase 1 stores it
	// but does not yet enforce it (Phase 6 wires --offline).
	Offline bool
	// AllowInsecureHTTP opts the http resolver into plain http://. Phase
	// 1 stores it but does nothing with it (Phase 3 wires it).
	AllowInsecureHTTP bool

	// AllowCluster opts the cluster resolver into the default registry.
	// The cluster resolver reads from the user's KUBECONFIG and is
	// disabled by default for safety (KUBECONFIG may point at
	// production). Set true via --resolver-allow=...,cluster on the
	// CLI; alternatively the engine flips this when --cluster-resolver-
	// context=<ctx> is set explicitly.
	AllowCluster bool

	// ClusterResolverContext, when non-empty, names the kubeconfig
	// context the cluster resolver reads from. Empty means "use the
	// kubeconfig's current-context."
	ClusterResolverContext string

	// ClusterResolverKubeconfig overrides the path the cluster resolver
	// loads its kubeconfig from. Empty falls back to the standard
	// $KUBECONFIG / ~/.kube/config resolution chain.
	ClusterResolverKubeconfig string
}

Options configures a Registry built via NewDefaultRegistry. Phase 1 only honors Allow + CacheDir + Offline; later phases plug concrete resolvers in here.

type Registry

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

Registry routes Requests to one of its registered Resolvers, applies the allow-list, and layers a per-run + on-disk cache on top.

func NewDefaultRegistry

func NewDefaultRegistry(opts Options) *Registry

NewDefaultRegistry returns a Registry pre-populated with whatever resolvers ship by default. Phase 1 shipped just the inline stub; Phase 2 adds the git resolver. Phase 3-4 add hub/http/bundles/cluster here.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry. Tests that want a single stub resolver call this and then Register their stub.

func (*Registry) Cache

func (r *Registry) Cache() *DiskCache

Cache returns the currently-installed disk cache, or nil.

func (*Registry) Inline

func (r *Registry) Inline() *InlineResolver

Inline returns the inline resolver registered with this Registry, or nil if none was registered. Used by tests to feed bytes in.

func (*Registry) Offline

func (r *Registry) Offline() bool

Offline reports whether the registry is in --offline mode.

func (*Registry) Register

func (r *Registry) Register(res Resolver)

Register adds a Resolver to the registry, keyed by its Name(). A repeat call with the same Name() replaces the prior entry.

func (*Registry) Remote

func (r *Registry) Remote() *RemoteResolver

Remote returns the currently-configured remote driver, or nil if Mode B is not active.

func (*Registry) Resolve

func (r *Registry) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve dispatches the Request to the appropriate Resolver. The per-run cache short-circuits repeated identical requests within a single run. Phase 1 does not yet consult the on-disk cache (Phase 6).

Mode B (remote) routing: when SetRemote has installed a remote driver, EVERY Resolve goes through it regardless of the Request's Resolver name (custom names are the whole point of Mode B; the validator's RemoteResolverEnabled Option has already cleared arbitrary names). Direct registrations are bypassed in this mode.

func (*Registry) SetAllow

func (r *Registry) SetAllow(names []string)

SetAllow replaces the allow-list. Empty / nil means "no allow-list applied" (every registered resolver dispatches).

func (*Registry) SetCache

func (r *Registry) SetCache(c *DiskCache)

SetCache installs the on-disk cache layer. Pass nil to disable. The cache is consulted between the per-run map and the registered Resolver: hits short-circuit the network call; misses fall through and Put the result on success.

func (*Registry) SetOffline

func (r *Registry) SetOffline(b bool)

SetOffline toggles the --offline gate. When true, every cache miss surfaces an error before dispatching to a Resolver.

func (*Registry) SetRemote

func (r *Registry) SetRemote(rr *RemoteResolver)

SetRemote installs the Mode B remote-resolver driver. When non-nil, every dispatch goes through remote (the validator's RemoteResolverEnabled Option already short-circuits the allow-list for any resolver name in this mode). Setting nil restores direct dispatch.

type RemoteResolver

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

RemoteResolver is the Mode B implementation: it submits a ResolutionRequest to a remote Tekton cluster and waits for the controller to fill in status.data.

SECURITY: the remote resolver speaks to whichever cluster the supplied kubeconfig context points at, under that context's service-account RBAC. tkn-act never elevates privileges and never stores credentials; the user's kubectl identity is the only thing the remote cluster sees.

func NewRemoteResolver

func NewRemoteResolver(opts RemoteResolverOptions) (*RemoteResolver, error)

NewRemoteResolver builds a RemoteResolver, loading the kube client from kubeconfig if opts.Dynamic is unset. Returns an error if the kubeconfig context can't be loaded.

func NewRemoteResolverFromOptions

func NewRemoteResolverFromOptions(opts RemoteResolverOptions) *RemoteResolver

NewRemoteResolverFromOptions returns a RemoteResolver with the supplied options, no kubeconfig loading. Tests use this to bypass the on-disk kubeconfig loader; the CLI uses NewRemoteResolver.

func (*RemoteResolver) Name

func (r *RemoteResolver) Name() string

Name implements Resolver. The literal name is `remote`, but Registry routes any unknown name through Remote when SetRemote() has been called (Mode B short-circuits the direct allow-list).

func (*RemoteResolver) Resolve

func (r *RemoteResolver) Resolve(ctx context.Context, req Request) (Resolved, error)

Resolve implements Resolver. The lifecycle is:

  1. Build a ResolutionRequest with spec.params from req.Params.
  2. Create() it via the dynamic client; pick v1beta1 first, fall back to v1alpha1 on NoKindMatchError (older clusters).
  3. Poll status.conditions[Succeeded] until True / False / timeout.
  4. On True: base64-decode status.data and return as Resolved.Bytes.
  5. On False or any error: surface a typed error including reason+message.
  6. ALWAYS Delete() the ResolutionRequest on the way out — using context.Background() so SIGINT mid-resolution still triggers cleanup (Critical 5 invariant).

type RemoteResolverOptions

type RemoteResolverOptions struct {
	// Dynamic, when non-nil, replaces the dynamic client constructed
	// from the kubeconfig. Tests use this to bypass kubeconfig loading.
	Dynamic dynamic.Interface

	// Kubeconfig overrides the kubeconfig file path. Empty falls back
	// to KUBECONFIG / ~/.kube/config per
	// clientcmd.NewDefaultClientConfigLoadingRules.
	Kubeconfig string

	// Context names the kubeconfig context to load. Empty means "use
	// the kubeconfig's current-context."
	Context string

	// Namespace where ResolutionRequests are submitted. Default "default".
	Namespace string

	// Timeout is the per-request wall-clock budget. Default 60s.
	Timeout time.Duration

	// PollInterval gates how often status is polled when no Watch event
	// arrives. Default 500ms.
	PollInterval time.Duration
}

RemoteResolverOptions configures a RemoteResolver. The CLI plumbs these from --remote-resolver-context / --remote-resolver-namespace / --remote-resolver-timeout. Tests inject Dynamic directly.

type Request

type Request struct {
	Kind     Kind
	Resolver string
	// Params hold resolver-specific keys. Always already substituted —
	// resolver implementations must never see "$(tasks.X.results.Y)".
	Params map[string]string
}

Request is the input shape resolvers consume. Params are already substituted (the engine ran the same $(...) substitution it runs on PipelineTask.Params before dispatching to the resolver), so resolver implementations see only literal values.

type Resolved

type Resolved struct {
	Bytes  []byte
	Source string
	SHA256 string
	// Cached is true if the bytes came from a cache layer rather than
	// a fresh fetch. Surfaces in resolver-end events.
	Cached bool
}

Resolved is the output shape. Bytes is the raw YAML the loader consumes via loader.LoadBytes; Source is a human-readable origin string for the resolver-end event; SHA256 is the hex digest of the returned Bytes (used for the on-disk cache invalidation diagnostic).

type Resolver

type Resolver interface {
	Name() string
	Resolve(ctx context.Context, req Request) (Resolved, error)
}

Resolver is the per-protocol fetcher. Implementations live in this package as files (git.go, hub.go, ...) but the interface is the only thing the engine sees.

Jump to

Keyboard shortcuts

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