plugincontrol

package
v0.2.13 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 21 Imported by: 0

Documentation

Index

Constants

View Source
const (
	IsolationShared      = "shared"
	IsolationPerInstance = "per-instance"
)

IsolationNames are the stable, persisted isolation values. They intentionally mirror pluginhost.Isolation.String so state files remain readable even if the internal numbering changes.

View Source
const DetailLimit = 240

DetailLimit 是所有出网 detail 字段的长度上限(字节):command_ack 的执行结果 摘要、plugin_ack 的逐实例结果、plugin_status 的实例 detail 共用同一上限。 detail 是「一行事实摘要」,不是日志:超长必须截断。

Variables

View Source
var (
	ErrNotFound                       = errors.New("plugincontrol: not found")
	ErrInvalidState                   = errors.New("plugincontrol: invalid instance state")
	ErrPermissionConfirmationRequired = errors.New("plugincontrol: permission confirmation required")
)

Sentinel errors returned by the plugin control plane. Callers can compare with errors.Is to distinguish stable failure modes without matching text.

View Source
var ErrCacheCorrupt = errors.New("plugincontrol: applied cache corrupt")

ErrCacheCorrupt 表示本地 applied cache 存在但不可解析。

View Source
var ErrSecretForbidden = errors.New("plugincontrol: secret forbidden")

ErrSecretForbidden 表示实例配置绑定的 secret handle 不可用:未声明、不存在、 已吊销、路径不安全或文件权限不合规。映射到稳定错误码 api.PluginErrSecretForbidden,实例 reconcile 一律 fail-closed。

Functions

func BoundHandles

func BoundHandles(config map[string]string) []string

BoundHandles 返回 config 中显式绑定的 secret:// handle(排序、去重)。 只返回引用,永不返回值——这是「实例配置显式绑定」这一半校验的事实源, 也可安全用于日志与上报。

func DiscardSecrets

func DiscardSecrets(values map[string][]byte)

DiscardSecrets 尽快清零并丢弃明文(缩短明文在内存中的存活期)。 v0.1 的 pluginhost 还没有把实例配置送进插件进程的通道,因此 Edge 侧解析 只用于「双校验 + fail-closed」,解析结果立即销毁,绝不落盘、绝不进上报。

func FormatIsolation

func FormatIsolation(value pluginhost.Isolation) string

FormatIsolation converts a pluginhost isolation value into its stable name.

func NormalizeTenant

func NormalizeTenant(tenant string) string

NormalizeTenant 归一租户标识:空值一律落到 default,绝不落到「全部租户」。

func ParseIsolation

func ParseIsolation(value string) (pluginhost.Isolation, error)

ParseIsolation converts a persisted isolation name into the pluginhost value.

func SanitizeDetail

func SanitizeDetail(s string) string

SanitizeDetail 把适配器/插件给出的摘要收敛成可安全上报的单行事实。 它是 Edge 全部出网 detail 的统一兜底(设备命令 ack 与插件控制面共用一份实现, 不各造一套脱敏规则):

  1. 折行与控制字符压成空格(禁止把进程 stdout/stderr 原文当 detail 回传);
  2. 复用 pluginhost.Redact 抹掉 secret / token / password / authorization 形态的值 (单一实现,不另造一份脱敏正则);
  3. 丢弃 stdout / stderr / log 标记之后的全部内容(进程输出原文一律不出网);
  4. 抹掉本机绝对路径(Windows 盘符路径与 POSIX 家目录/系统目录路径); 端口名(COM3、/dev/ttyUSB0)保留——它是设备元数据里本来就公开的 port 字段;
  5. UTF-8 安全截断到 DetailLimit。

上报红线(control-plane-sync.md 不变量 6 与本轮验收要求)由此函数统一兜底: 即使适配器返回了不合规摘要,出网的 detail 也已脱敏。

func SaveAppliedCache

func SaveAppliedCache(path string, cache AppliedCache) error

SaveAppliedCache 用同目录临时文件 + fsync + 原子 rename 落盘(§3.2 要求)。 空路径表示不持久化(进程重启后从 revision 0 开始,靠 Server 全量快照收敛)。

Types

type AppliedCache

type AppliedCache struct {
	AppliedRevision uint64                      `json:"applied_revision"`
	SnapshotDigest  string                      `json:"snapshot_digest"`
	Results         []api.PluginApplyResultData `json:"results,omitempty"`
	UpdatedAt       int64                       `json:"updated_at,omitempty"`
}

AppliedCache 是 Edge 本地插件控制面缓存(control-plane-sync.md §3.2): 最近一次成功应用的 revision、Server 下发的规范化摘要、每实例结果。

刻意**不保存 boot_id**:boot_id 必须每次进程启动都换新(§3.3),从缓存恢复 旧 boot_id 会让 Server 无法识别进程重启、也无法拒绝旧连接的迟到消息。

func LoadAppliedCache

func LoadAppliedCache(path string) (AppliedCache, error)

LoadAppliedCache 读取本地 applied cache。文件缺失 = 从未应用过(零值,非错误)。 文件损坏返回 ErrCacheCorrupt:调用方必须显式记录,然后以零值继续 (声明式全量快照本身就能把 Edge 收敛到 Server 的当前真相,不会因此跑偏)。

func (AppliedCache) IsEmpty

func (c AppliedCache) IsEmpty() bool

IsEmpty 报告是否从未成功应用过任何 revision。

type Applier

type Applier interface {
	// ApplySnapshot 应用一份完整期望态快照,返回**逐实例**结果(每实例一条)。
	// 只要有一条不是 applied,调用方就不推进 revision。
	ApplySnapshot(ctx context.Context, tenant string, instances []api.PluginDesiredInstanceData) ([]api.PluginApplyResultData, error)
	// Observe 返回本地安装物与实例实际态。实现必须自行脱敏:
	// 不含明文 secret、访问令牌、本机绝对路径、插件 stdout/stderr 原文。
	Observe(ctx context.Context, tenant string) ([]api.PluginInstallationStatusData, []api.PluginObservedInstanceData, error)
}

Applier 把 Server 下发的**完整**期望态快照应用到本地,并回报本地实际态。

生产实现是 *Host(驱动 pluginhost.Manager + 本地 desired Store);测试注入 fake,任何测试都不启动真实第三方插件进程。

声明式语义:Applier 只收敛「快照描述的最终态」,不回放断线期间的中间操作 (control-plane-sync.md §1/§8)。因此 ApplySnapshot 必须幂等。

type ApplyUpdateOptions

type ApplyUpdateOptions struct {
	Tenant     string
	InstanceID string
	PluginID   string
	Version    string
}

ApplyUpdateOptions selects the instance whose desired version is advanced after a successful install/update.

type Controller

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

Controller persists desired plugin-instance state and validates each write against the installation lockfile and manifest. It does not launch plugin processes; that is the Plugin Host's job.

func NewController

func NewController(opts ControllerOptions) (*Controller, error)

NewController validates required options and applies defaults.

func (*Controller) ApplyUpdateVersion

func (c *Controller) ApplyUpdateVersion(opts ApplyUpdateOptions) (InstanceState, error)

ApplyUpdateVersion advances one instance's desired version after the installer has already validated compatibility/permissions and written the new lockfile/manifest.

func (*Controller) Disable

func (c *Controller) Disable(tenant, instanceID string) (StateResult, error)

Disable persists disabled desired state for one existing instance.

func (*Controller) Enable

func (c *Controller) Enable(opts EnableOptions) (StateResult, error)

Enable persists an enabled instance state after validating that the plugin is installed, its manifest parses, and its version/contract match the lockfile. It never claims the plugin is online.

func (*Controller) Remove

func (c *Controller) Remove(opts RemoveOptions) (RemoveResult, error)

Remove deletes one instance's desired state. Plugin data is preserved by default and only removed when Purge is explicitly true.

func (*Controller) UpdateCheck

func (c *Controller) UpdateCheck(pluginID string, incoming *registry.Manifest, confirm bool) ([]string, error)

UpdateCheck is the pre-install gate for upgrades: it validates the incoming manifest contract and rejects permission expansion unless confirmed. It performs no state or lockfile mutation.

type ControllerOptions

type ControllerOptions struct {
	Store             *Store
	PluginsDir        string
	LockPath          string
	SchemaPath        string
	CoreVersion       string
	DataDir           string
	SupportedProtocol int
}

ControllerOptions configures the CLI-facing control plane. All paths are injected so tests can point the controller at a temporary tree and never touch real plugin installs.

type EnableOptions

type EnableOptions struct {
	Tenant     string
	InstanceID string
	PluginID   string
	ConfigPath string
	Isolation  pluginhost.Isolation
}

EnableOptions is the input for persisting an enabled desired state.

type FileSecrets

type FileSecrets struct {
	Root     string
	MaxBytes int64
	// PluginsDir 用于读取插件 manifest 的 permissions.secrets(双校验的另一半)。
	PluginsDir string
	// Declared 返回插件声明的 secret 名;nil 时用 declaredFromManifest。
	// 注入点:测试无需真实插件安装树。
	Declared func(pluginID string) ([]string, error)
}

FileSecrets 是文件实现:明文位于 Root/<tenant>/<instance>/<name>, 按 tenant/instance 隔离;权限与路径安全由 internal/secrethandle 强制 (POSIX 要求 0600 等价权限,越界/符号链接/超大文件一律拒绝)。

func NewFileSecrets

func NewFileSecrets(root, pluginsDir string) *FileSecrets

NewFileSecrets 构造文件 secret provider。

func (*FileSecrets) Resolve

func (f *FileSecrets) Resolve(tenant, instanceID, pluginID string, config map[string]string) (map[string][]byte, error)

Resolve 对每个绑定的 handle 做**双校验**后读取明文:

  1. 插件 manifest 已声明该 secret 名(permissions.secrets);
  2. 实例配置显式绑定了该 handle(BoundHandles 给出)。

任一条不满足、或 handle 不存在/已吊销(文件被删除即视为吊销)、路径不安全、 文件权限不合规 → 返回 ErrSecretForbidden 并 fail-closed,绝不回落旧明文缓存 (本实现不持有任何缓存,每次都重新读盘)。

type Host

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

Host reloads installed plugins and enabled instance states into a Manager and runs until its context is canceled. It is the only component that turns desired state into observed process state.

func NewHost

func NewHost(opts HostOptions) (*Host, error)

NewHost validates options and returns a Host. The manager is not started until Load or Run is called.

func (*Host) ApplySnapshot

func (h *Host) ApplySnapshot(ctx context.Context, tenant string, instances []api.PluginDesiredInstanceData) ([]api.PluginApplyResultData, error)

ApplySnapshot 把一份**完整**期望态快照收敛到本地,返回逐实例结果。

声明式语义(control-plane-sync.md §1/§8):只收敛快照描述的最终态, 不回放断线期间的中间操作;因此本方法幂等,可用同一份快照重复调用。

快照里没有、但本地还留着的实例 = Server 已删除:停用并移除, 默认保留插件数据(purge 是 Server 侧显式高风险选项,Edge 不自作主张)。

func (*Host) InstallationStatus

func (h *Host) InstallationStatus() ([]api.PluginInstallationStatusData, error)

InstallationStatus 从 plugins.lock + 已安装 manifest 组装安装物公开事实。

上报红线:**不含本地路径**(Installation.Path 刻意不出现在这里)、 不含启动参数与环境变量;permissions.secrets 只携带 secret 名称,永不是值。

func (*Host) InstanceObservations

func (h *Host) InstanceObservations(tenant string) []api.PluginObservedInstanceData

InstanceObservations 返回本租户实例的实际态。

desired 与 observed 永远分开:Manager 不可观测时回报 HostOnline=false + STOPPED/UNKNOWN,绝不把「期望启用」渲染成「实际健康」。

func (*Host) Load

func (h *Host) Load(ctx context.Context) (LoadResult, error)

Load registers every lockfile installation and starts every enabled desired instance across all tenants exactly once. Disabled instances are left stopped. It is safe to call Load repeatedly; subsequent calls return the cached result. This is the global mode used by `cloudpath plugin host`.

func (*Host) LoadTenant

func (h *Host) LoadTenant(ctx context.Context, tenant string) (LoadResult, error)

LoadTenant registers every lockfile installation but starts only the enabled desired instances of tenant. An empty tenant means "default" and never the global all-tenant set. Used by the edge's in-process driver host so one edge never starts another tenant's instances.

func (*Host) Observe

Observe 返回本地安装物与实例实际态(Edge 是实际态的唯一观测源)。

func (*Host) Run

func (h *Host) Run(ctx context.Context) error

Run loads the configured state, waits for ctx cancellation, and then asks the manager to gracefully close every supervised process.

type HostManager

type HostManager interface {
	RegisterInstallation(pluginhost.Installation) error
	CreateInstance(pluginhost.InstanceSpec) (pluginhost.Instance, error)
	Start(tenant, id string) error
	// Disable 停用一个实例但保留其定义(期望态 enabled=false 时调用)。
	Disable(tenant, id string) error
	// Remove 删除一个实例;默认保留插件数据,只有显式 purge 才删。
	Remove(tenant, id string, opts ...pluginhost.RemoveOption) (pluginhost.RemoveResult, error)
	Close() error
}

HostManager is the seam used by Host to start supervised plugin instances. *pluginhost.Manager satisfies it; tests inject an in-memory fake so no real third-party plugin process is ever launched.

type HostObserver

type HostObserver interface {
	ListInstances(tenant string) []pluginhost.InstanceSnapshot
}

HostObserver 由支持实际态读取的 Manager 可选实现(*pluginhost.Manager 满足)。 刻意与 HostManager 分开:观测能力缺席时 Observe 诚实回报「不可观测」, 而不是编造健康态;既有测试替身也不必被迫实现观测方法。

type HostOptions

type HostOptions struct {
	Manager    HostManager
	Store      *Store
	PluginsDir string
	LockPath   string
	Logger     *slog.Logger
	// Secrets 是本地 secret provider(§7)。nil 表示本 Edge 不提供本地明文:
	// 任何绑定 secret:// handle 的实例都会 fail-closed,绝不静默跳过校验。
	Secrets SecretResolver
}

HostOptions configures the long-running plugin host.

type InstanceState

type InstanceState struct {
	Tenant     string `json:"tenant"`
	InstanceID string `json:"instanceId"`
	PluginID   string `json:"pluginId"`
	Version    string `json:"version"`
	ConfigPath string `json:"configPath,omitempty"`
	Enabled    bool   `json:"enabled"`
	Isolation  string `json:"isolation"`
	// Config 是 Server 期望态里的非敏感配置(control-plane-sync.md §7)。
	// 值只允许非敏感标量或 secret://<name> handle:**明文 secret 永不落盘**。
	// 保存它是为了 Edge 进程重启且仍离线时,能带着最后一个完整 applied revision
	// 的配置续跑(§8「Edge 重启 → 从本地 applied cache 启动」)。
	Config map[string]string `json:"config,omitempty"`
}

InstanceState is the durable desired state for one plugin instance. It is stored as one JSON document per tenant/instance id so a tenant can never see or mutate another tenant's state through the shared filesystem.

func (InstanceState) Validate

func (s InstanceState) Validate() error

Validate checks the persisted fields that are required for later reload.

type LoadResult

type LoadResult struct {
	Installations int
	Instances     int
	Started       int
	Idle          bool
}

LoadResult is a factual summary of one host load.

type MetricsReader

type MetricsReader interface {
	Metrics(tenant, id string) (pluginhost.Metrics, error)
}

MetricsReader 由 Manager 可选实现:提供 message rate 与 last healthy。

type Observation

type Observation struct {
	HostOnline bool
	State      pluginhost.State
	Health     pluginhost.Health
}

Observation is the observed runtime state for one instance. The control plane separates desired state (what the CLI was asked to persist) from observed state (what a live Plugin Host actually reports).

func (Observation) String

func (o Observation) String() string

String returns the stable, human-readable observation. A one-shot CLI enable is intentionally reported as STOPPED rather than HEALTHY because the CLI does not run a plugin process.

type RemoveOptions

type RemoveOptions struct {
	Tenant     string
	InstanceID string
	Purge      bool
}

RemoveOptions controls instance removal. Purge must be set explicitly to delete plugin data; the default preserves it.

type RemoveResult

type RemoveResult struct {
	Purged        bool
	DataPreserved bool
	DataPath      string
}

RemoveResult reports the data-disposition outcome of one Remove.

type SecretResolver

type SecretResolver interface {
	// Resolve 返回 config 里显式绑定的每个 handle 的明文(键 = handle 串)。
	// 任一 handle 不可用即整体失败:不部分解析、不回落旧明文。
	// 返回的错误文本只含 handle 名与稳定原因码,绝不含明文或本机绝对路径。
	Resolve(tenant, instanceID, pluginID string, config map[string]string) (map[string][]byte, error)
}

SecretResolver 在 Edge 本地把 secret://<name> handle 解析成明文 (control-plane-sync.md §7)。

边界:Server 只见 handle,永不接收或解析明文;明文只存在于 Edge 本地 provider 与目标插件进程的短期内存。本接口的实现**不得缓存明文**——handle 被吊销后 插件重启必须重新解析并失败,绝不能从任何本地缓存恢复旧明文。

type StateResult

type StateResult struct {
	Desired  InstanceState
	Observed Observation
}

StateResult separates the persisted desired state from the observed runtime state. A one-shot CLI operation has no live host attached, so Observed is STOPPED/UNKNOWN and HostOnline is false.

type Store

type Store struct {
	Dir string
}

Store persists instance states under Dir/<tenant>/<instance-id>.json using same-directory atomic renames. Every path component is validated before use so tenant or instance ids can never escape the state root.

func NewStore

func NewStore(dir string) *Store

NewStore returns a state store rooted at dir.

func (*Store) Delete

func (s *Store) Delete(tenant, instanceID string) error

Delete removes one instance state. Missing state is not an error.

func (*Store) ListAll

func (s *Store) ListAll() ([]InstanceState, error)

ListAll returns every tenant's instance states, sorted by tenant then instance id. It is the reload view used by the plugin host.

func (*Store) ListTenant

func (s *Store) ListTenant(tenant string) ([]InstanceState, error)

ListTenant returns every instance state for tenant sorted by instance id.

func (*Store) Load

func (s *Store) Load(tenant, instanceID string) (InstanceState, error)

Load reads one tenant-scoped instance state. A missing file maps to ErrNotFound and never leaks the underlying path.

func (*Store) Save

func (s *Store) Save(state InstanceState) error

Save writes one instance state atomically and fsyncs the temporary file.

type SyncOptions

type SyncOptions struct {
	// Tenant 是本 Edge 归属的租户(缺省 default)。期望态/实际态都按它隔离。
	Tenant string
	// CachePath 是本地 applied cache 路径;空 = 不持久化。
	CachePath string
	// Applier 必填。
	Applier Applier
	Logger  *slog.Logger
	Now     func() time.Time
}

SyncOptions 配置 Edge 侧插件控制面收敛器。

type Syncer

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

Syncer 是 Edge 侧插件控制面收敛器:revision 规则、本地 applied cache、 plugin_status 上报组装(control-plane-sync.md §3.2/§4/§8)。

并发:HandleDesired 全程持锁,因此即使 Server 连发多份快照、或读循环为每条 消息各起协程,应用也严格串行,绝不会出现两份快照交错写本地状态。

func NewSyncer

func NewSyncer(opts SyncOptions) (*Syncer, error)

NewSyncer 校验选项、生成本次进程的 boot_id 并载入本地 applied cache。

func (*Syncer) AppliedRevision

func (s *Syncer) AppliedRevision() uint64

AppliedRevision 返回最近一次成功应用的 revision。

func (*Syncer) BootID

func (s *Syncer) BootID() string

BootID 返回本次进程的启动标识。

func (*Syncer) Cache

func (s *Syncer) Cache() AppliedCache

Cache 返回本地 applied cache 的副本(诊断/测试用)。

func (*Syncer) HandleDesired

func (s *Syncer) HandleDesired(ctx context.Context, desired api.PluginDesiredData) api.PluginAckData

HandleDesired 按契约规则处理一份 Server 期望态快照并产出 ack:

  • new revision > applied → apply,成功才推进 applied_revision
  • same revision + same digest → 幂等 ACK,不重复 apply(无副作用)
  • same revision + different digest → reject(协议错误,fail-closed,applied 不变)
  • old revision → reject(迟到/倒序消息不得让本地状态倒退)
  • 空 digest → reject(无法验证同 revision 内容一致性)
  • 部分实例失败 → failed,revision **不前进**,逐实例报告

func (*Syncer) Status

func (s *Syncer) Status(ctx context.Context) api.PluginStatusData

Status 组装一次 plugin_status 全量上报:boot_id(进程级)+ 单调 sequence + applied_revision + 本地安装物 + 实例实际态。

上报脱敏红线(不变量 6):所有 detail 再过一次 SanitizeDetail 兜底, 即使 Applier 失手也不会把绝对路径/凭据形态/stdout 原文送出网。

Jump to

Keyboard shortcuts

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