Files
mc-gateway/cmd/gateway/plugin_cli_toolchain.go
tursom f508ecc1b9
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
docs: 补充中文代码注释
2026-06-27 20:15:29 +08:00

1910 lines
53 KiB
Go

// cmd/gateway/plugin_cli_toolchain.go 包含插件开发者使用的本地源码模板、构建打包流程和测试辅助逻辑。
package main
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/tursom/mc-gateway/internal/pluginmanager"
"github.com/tursom/mc-gateway/plugin/api"
)
type pluginBuildCLIOptions struct {
Dir string
BuildType string
Out string
FromSource string
Manifest string
SkipTests bool
Vendor bool
}
type pluginInitCLIOptions struct {
Dir string
ID string
Name string
Template string
Runtime string
Module string
Extension string
Format string
}
type pluginGovernanceCLIOptions struct {
Target string
Manifest string
ConfigPath string
ConfigJSON string
Profile string
Action string
Priority int
BenchmarkProfile string
P95MS float64
P99MS float64
ErrorRate float64
ActiveProxyCapacity int64
BaselineDiff float64
}
type pluginRuntimeCLIAdapter interface {
RuntimeType() string
Init(context.Context, pluginInitCLIOptions) error
BuildBinary(context.Context, pluginBuildRuntimeRequest) (pluginmanager.ArtifactRecord, error)
BuildSource(context.Context, pluginBuildRuntimeRequest) (pluginmanager.ArtifactRecord, error)
Test(context.Context, pluginTestCLIOptions) error
}
type pluginBuildRuntimeRequest struct {
Dir string
Manifest pluginmanager.Manifest
Raw map[string]any
OutPath string
Vendor bool
}
type pluginTestCLIOptions struct {
Target string
Manifest string
Profile string
ConfigPath string
FixturePath string
Source pluginmanager.Manifest
}
type cliStaticRuntimeAdapter struct{}
func (cliStaticRuntimeAdapter) Load(context.Context, pluginmanager.ArtifactRecord, pluginmanager.PluginRecord, *pluginmanager.Gateway) (api.Plugin, error) {
return nil, errors.New("local CLI governance adapter does not load plugin code")
}
type goPluginCLIAdapter struct{}
func (goPluginCLIAdapter) RuntimeType() string {
return pluginmanager.RuntimeGoPlugin
}
func (goPluginCLIAdapter) Init(ctx context.Context, opts pluginInitCLIOptions) error {
return writeGoPluginTemplate(ctx, opts)
}
func (goPluginCLIAdapter) BuildBinary(ctx context.Context, req pluginBuildRuntimeRequest) (pluginmanager.ArtifactRecord, error) {
return buildBinaryPluginPackageContext(ctx, req.Dir, req.Manifest, req.Raw, req.OutPath)
}
func (goPluginCLIAdapter) BuildSource(ctx context.Context, req pluginBuildRuntimeRequest) (pluginmanager.ArtifactRecord, error) {
return buildSourcePluginPackageContext(ctx, req.Dir, req.Manifest, req.Raw, req.OutPath, req.Vendor)
}
func (goPluginCLIAdapter) Test(ctx context.Context, opts pluginTestCLIOptions) error {
for _, item := range strings.Split(opts.Profile, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
switch item {
case "unit":
if err := runGoCommand(ctx, opts.Target, "go", "test", "./..."); err != nil {
return err
}
case "manifest":
if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil {
return err
}
case "harness", "protocol-smoke", "conformance":
if err := validateTestFileIfSet(opts.ConfigPath, "config"); err != nil {
return err
}
if err := validateTestFileIfSet(opts.FixturePath, "fixture"); err != nil {
return err
}
if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil {
return err
}
default:
return fmt.Errorf("unsupported test profile %q", item)
}
}
return nil
}
func pluginCLIAdapterForRuntime(runtimeType string) (pluginRuntimeCLIAdapter, error) {
if runtimeType == "" {
runtimeType = pluginmanager.RuntimeGoPlugin
}
switch runtimeType {
case pluginmanager.RuntimeGoPlugin:
return goPluginCLIAdapter{}, nil
case pluginmanager.RuntimeBuiltin, pluginmanager.RuntimeSandbox, pluginmanager.RuntimeWASM:
return nil, fmt.Errorf("runtime %q is reserved; no CLI build/test adapter is implemented yet", runtimeType)
default:
return nil, fmt.Errorf("unsupported runtime %q", runtimeType)
}
}
func runPluginFeaturesCLI(args []string) error {
if len(args) > 0 {
return fmt.Errorf("features does not accept positional arguments")
}
features := map[string]any{
"schema_version": pluginmanager.SchemaVersion,
"api_version": pluginmanager.APIVersion,
"artifact_types": []string{
pluginmanager.ArtifactTypeBinary,
pluginmanager.ArtifactTypeSource,
},
"runtime_types": []map[string]any{
{"type": pluginmanager.RuntimeGoPlugin, "implemented": true, "entry": pluginmanager.RuntimeEntry},
{"type": pluginmanager.RuntimeBuiltin, "implemented": false},
{"type": pluginmanager.RuntimeSandbox, "implemented": false},
{"type": pluginmanager.RuntimeWASM, "implemented": false, "entry": pluginmanager.RuntimeWASMEntry},
},
"service_modes": []map[string]any{
{"mode": pluginmanager.PluginServiceModeInProcess, "implemented": true},
{"mode": pluginmanager.PluginServiceModeGoPluginProcess, "implemented": false},
{"mode": pluginmanager.PluginServiceModeSandboxProcess, "implemented": false},
},
"extension_points": []map[string]string{
{"type": "hook", "key": pluginmanager.ExtensionUpstreamConnect},
{"type": "hook", "key": pluginmanager.ExtensionRouteResolve},
{"type": "provider", "key": pluginmanager.ExtensionRouteResolver},
{"type": "hook", "key": pluginmanager.ExtensionRuleEvaluate},
{"type": "hook", "key": pluginmanager.ExtensionConfigValidate},
{"type": "hook", "key": pluginmanager.ExtensionStatusPing},
{"type": "hook", "key": pluginmanager.ExtensionConnectionFilter},
{"type": "hook", "key": pluginmanager.ExtensionHandshakeFilter},
{"type": "subscriber", "key": pluginmanager.ExtensionEventSubscriber},
{"type": "provider", "key": pluginmanager.ExtensionProvider},
{"type": "provider", "key": pluginmanager.ExtensionAuthProvider},
{"type": "provider", "key": pluginmanager.ExtensionAdminAuthProvider},
},
"build": map[string]any{
"implemented_runtimes": []string{pluginmanager.RuntimeGoPlugin},
"builder_types": []string{
pluginmanager.BuilderTypeLocalProcess,
pluginmanager.BuilderTypeContainer,
},
"default_runtime_entry": pluginmanager.RuntimeEntry,
"default_build_entry": pluginmanager.SourceBuildEntry,
},
"cli": map[string]any{
"implemented_commands": []string{
"init",
"features",
"manifest format",
"manifest explain",
"build",
"test",
"preflight",
"self-test",
"benchmark",
"status",
"upload",
"enable",
"disable",
"delete",
"rollback",
"config validate",
"secret check",
"logs",
"events",
"metrics",
"diagnose",
"task list",
"task run",
"data inspect",
"data gc",
"files inspect",
"files gc",
"gc",
"review status",
"review approve",
"review reject",
"review override",
"advisory scan",
"advisory import",
"repo list",
"repo import",
"sbom verify",
"verify",
"runtime features",
"runtime status",
"runtime mode",
"runtime apply",
"inspect",
"validate",
"compat",
"source-validate",
"source-build",
},
"reserved_commands": []string{
"contract",
"conformance",
"schema export",
"repo search",
"repo show",
"sbom generate",
"sign",
"promotion export/import/diff/drift/dr-drill",
"task cancel",
},
},
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(features)
}
func runPluginManifestCLI(args []string) error {
if len(args) == 0 {
return errors.New("usage: gateway plugin manifest format|explain ...")
}
switch args[0] {
case "format":
return runPluginManifestFormatCLI(args[1:])
case "explain":
return runPluginManifestExplainCLI(args[1:])
default:
return fmt.Errorf("unknown manifest command %q", args[0])
}
}
func runPluginManifestFormatCLI(args []string) error {
target := "."
manifestPath := ""
artifactType := ""
write := false
canonicalJSON := false
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if target != "." {
return fmt.Errorf("unexpected argument %q", arg)
}
target = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return err
}
i += consumed
switch key {
case "write":
write = parsePluginBoolFlag(value)
case "canonical-json":
canonicalJSON = parsePluginBoolFlag(value)
case "manifest":
manifestPath = value
case "type":
artifactType = value
default:
return fmt.Errorf("unknown manifest format flag --%s", key)
}
}
if write && canonicalJSON {
return errors.New("--write and --canonical-json are mutually exclusive")
}
source, err := readPluginManifestSource(target, manifestPath)
if err != nil {
return err
}
if canonicalJSON {
if artifactType == "" {
artifactType = source.Manifest.ArtifactType
}
if artifactType == "" {
artifactType = pluginmanager.ArtifactTypeBinary
}
switch artifactType {
case pluginmanager.ArtifactTypeBinary, pluginmanager.ArtifactTypeSource:
default:
return fmt.Errorf("unsupported --type %q", artifactType)
}
canonical, err := materializedManifestJSON(source.Raw, source.Manifest, artifactType, false)
if err != nil {
return err
}
_, err = os.Stdout.Write(canonical)
return err
}
if artifactType != "" {
return errors.New("--type is only valid with --canonical-json")
}
if write {
if source.Format != manifestFormatJSON {
fmt.Fprintf(os.Stdout, "ok manifest=%s validated comments_preserved=true\n", source.Path)
return nil
}
if bytes.Equal(source.Data, source.CanonicalJSON) {
fmt.Fprintf(os.Stdout, "ok manifest=%s unchanged\n", source.Path)
return nil
}
if err := os.WriteFile(source.Path, source.CanonicalJSON, 0644); err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", source.Path)
return nil
}
if source.Format == manifestFormatJSON {
_, err = os.Stdout.Write(source.CanonicalJSON)
return err
}
_, err = os.Stdout.Write(source.Data)
if err == nil && len(source.Data) > 0 && source.Data[len(source.Data)-1] != '\n' {
fmt.Fprintln(os.Stdout)
}
return err
}
func runPluginManifestExplainCLI(args []string) error {
if len(args) != 1 {
return errors.New("usage: gateway plugin manifest explain <field-or-key>")
}
key := strings.TrimSpace(args[0])
explanation, ok := manifestExplanation(key)
if !ok {
return fmt.Errorf("no manifest explanation for %q", key)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(explanation)
}
func manifestExplanation(key string) (map[string]any, bool) {
explanations := map[string]map[string]any{
"schema_version": {
"key": "schema_version",
"required": true,
"value": pluginmanager.SchemaVersion,
"summary": "Manifest schema contract version supported by this gateway.",
},
"artifact_type": {
"key": "artifact_type",
"required": true,
"values": []string{pluginmanager.ArtifactTypeBinary, pluginmanager.ArtifactTypeSource},
"summary": "Declares whether the .mcgp package carries a built runtime artifact or source package.",
},
"runtime.type": {
"key": "runtime.type",
"required": true,
"values": []string{pluginmanager.RuntimeGoPlugin, pluginmanager.RuntimeBuiltin, pluginmanager.RuntimeSandbox, pluginmanager.RuntimeWASM},
"summary": "Selects the runtime adapter. Only go-plugin build/test is implemented by this CLI slice.",
},
"runtime.entry": {
"key": "runtime.entry",
"required": true,
"default": pluginmanager.RuntimeEntry,
"summary": "Path of the runtime entry inside a binary .mcgp package.",
},
"runtime.entry_symbol": {
"key": "runtime.entry_symbol",
"default": "Plugin",
"summary": "Go plugin factory symbol used to create a plugin instance.",
},
"build.entry": {
"key": "build.entry",
"default": pluginmanager.SourceBuildEntry,
"summary": "Go package path built from a source .mcgp package.",
},
"api_version": {
"key": "api_version",
"required": true,
"value": pluginmanager.APIVersion,
"summary": "Plugin API contract version supported by this gateway.",
},
"extension_points": {
"key": "extension_points",
"required": true,
"summary": "Declared extension points used for static validation, governance, conflict analysis, and Admin display.",
},
"upstream.connect/v1": {
"key": pluginmanager.ExtensionUpstreamConnect,
"type": "hook",
"summary": "Extension point for replacing upstream connection creation or returning a protocol-proxy stream endpoint.",
},
"config_schema": {
"key": "config_schema",
"summary": "JSON schema used by Admin UI and preflight to validate plugin configuration before enable/reload.",
},
"capabilities": {
"key": "capabilities",
"summary": "Runtime, network, filesystem, Minecraft, and feature declarations used for review and policy gates.",
},
}
explanation, ok := explanations[key]
return explanation, ok
}
func runPluginPreflightCLI(args []string) error {
opts, err := parseGovernanceCLIOptions(args)
if err != nil {
return err
}
if opts.Target == "" {
return errors.New("preflight requires a plugin directory or binary .mcgp")
}
manager, cleanup, artifact, configJSON, err := prepareLocalGovernanceManager(opts)
if err != nil {
return err
}
defer cleanup()
result, err := manager.RunPreflight(context.Background(), "cli", artifact.PluginID, pluginmanager.PreflightRequest{
ArtifactID: artifact.ID,
Profile: opts.Profile,
Action: opts.Action,
ConfigJSON: configJSON,
})
if err != nil {
return err
}
return encodePluginCLIJSON(map[string]any{
"plugin_id": artifact.PluginID,
"artifact_id": artifact.ID,
"preflight": result,
})
}
func runPluginSelfTestCLI(args []string) error {
opts, err := parseGovernanceCLIOptions(args)
if err != nil {
return err
}
if opts.Target == "" {
return errors.New("self-test requires a plugin directory or binary .mcgp")
}
manager, cleanup, artifact, _, err := prepareLocalGovernanceManager(opts)
if err != nil {
return err
}
defer cleanup()
result, err := manager.RunSelfTest(context.Background(), "cli", artifact.PluginID, pluginmanager.SelfTestRequest{
ArtifactID: artifact.ID,
Profile: opts.Profile,
})
if err != nil {
return err
}
return encodePluginCLIJSON(map[string]any{
"plugin_id": artifact.PluginID,
"artifact_id": artifact.ID,
"self_test": result,
})
}
func runPluginBenchmarkCLI(args []string) error {
opts, err := parseGovernanceCLIOptions(args)
if err != nil {
return err
}
if opts.Target == "" {
return errors.New("benchmark requires a plugin directory or binary .mcgp")
}
if opts.BenchmarkProfile == "" {
opts.BenchmarkProfile = "local-fast"
}
manager, cleanup, artifact, configJSON, err := prepareLocalGovernanceManager(opts)
if err != nil {
return err
}
defer cleanup()
record, err := manager.SaveBenchmark(context.Background(), "cli", pluginmanager.BenchmarkRequest{
ArtifactID: artifact.ID,
Profile: opts.Profile,
BenchmarkProfile: opts.BenchmarkProfile,
P95MS: opts.P95MS,
P99MS: opts.P99MS,
ErrorRate: opts.ErrorRate,
ActiveProxyCapacity: opts.ActiveProxyCapacity,
BaselineDiff: opts.BaselineDiff,
})
if err != nil {
return err
}
decision, err := manager.EvaluateGovernance(context.Background(), artifact.PluginID, artifact.ID, pluginmanager.GovernanceActionEnable, opts.Profile, configJSON)
if err != nil {
return err
}
return encodePluginCLIJSON(map[string]any{
"plugin_id": artifact.PluginID,
"artifact_id": artifact.ID,
"benchmark": record,
"decision": decision,
})
}
func runPluginInitCLI(args []string) error {
opts := pluginInitCLIOptions{
Template: "upstream-dialer",
Runtime: pluginmanager.RuntimeGoPlugin,
Extension: pluginmanager.ExtensionUpstreamConnect,
Format: manifestFormatYAML,
}
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if opts.Dir != "" {
return fmt.Errorf("unexpected argument %q", arg)
}
opts.Dir = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return err
}
i += consumed
switch key {
case "id":
opts.ID = value
case "name":
opts.Name = value
case "template":
opts.Template = value
case "runtime":
opts.Runtime = value
case "module":
opts.Module = value
case "extension":
opts.Extension = value
case "manifest-format":
opts.Format = value
default:
return fmt.Errorf("unknown init flag --%s", key)
}
}
if opts.Dir == "" {
return errors.New("plugin init requires a target directory")
}
if opts.ID == "" {
opts.ID = filepath.Base(opts.Dir)
}
if opts.Name == "" {
opts.Name = titleFromPluginID(opts.ID)
}
if opts.Module == "" {
opts.Module = "example.com/" + opts.ID
}
if err := validateManifestTemplateFormat(opts.Format); err != nil {
return err
}
adapter, err := pluginCLIAdapterForRuntime(opts.Runtime)
if err != nil {
return err
}
return adapter.Init(context.Background(), opts)
}
func runPluginBuildCLI(args []string) error {
opts := pluginBuildCLIOptions{Dir: ".", BuildType: "binary", Vendor: true}
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if opts.Dir != "." {
return fmt.Errorf("unexpected argument %q", arg)
}
opts.Dir = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return err
}
i += consumed
switch key {
case "type":
opts.BuildType = value
case "out":
opts.Out = value
case "from-source":
opts.FromSource = value
case "manifest":
opts.Manifest = value
case "skip-tests":
opts.SkipTests = parsePluginBoolFlag(value)
case "vendor":
opts.Vendor = parsePluginBoolFlag(value)
default:
return fmt.Errorf("unknown build flag --%s", key)
}
}
if opts.FromSource != "" {
build, out, err := buildSourcePackageForCLI(opts.FromSource, opts.Out)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok build=%d plugin=%s source_sha256=%s artifact_sha256=%s builder=%s go=%s status=%s out=%s\n",
build.ID, build.PluginID, build.SourceSHA256, build.ArtifactSHA256, build.BuilderType, build.GoVersion, build.Status, out)
return nil
}
switch opts.BuildType {
case "binary", "source", "both":
default:
return fmt.Errorf("unsupported build --type %q", opts.BuildType)
}
manifest, raw, err := readPluginDirManifest(opts.Dir, opts.Manifest)
if err != nil {
return err
}
adapter, err := pluginCLIAdapterForRuntime(manifest.Runtime.Type)
if err != nil {
return err
}
if !opts.SkipTests {
if err := runGoCommand(context.Background(), opts.Dir, "go", "test", "./..."); err != nil {
return err
}
}
outDir := opts.Out
if opts.BuildType == "both" {
if outDir == "" {
outDir = filepath.Join(opts.Dir, "dist")
}
} else if outDir == "" || strings.HasSuffix(outDir, string(os.PathSeparator)) {
if outDir == "" {
outDir = filepath.Join(opts.Dir, "dist")
}
}
if opts.BuildType == "binary" || opts.BuildType == "both" {
outPath := opts.Out
if outPath == "" || opts.BuildType == "both" || strings.HasSuffix(outPath, string(os.PathSeparator)) {
outPath = filepath.Join(outDir, manifest.ID+".mcgp")
}
artifact, err := adapter.BuildBinary(context.Background(), pluginBuildRuntimeRequest{
Dir: opts.Dir,
Manifest: manifest,
Raw: raw,
OutPath: outPath,
Vendor: opts.Vendor,
})
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok plugin=%s version=%s sha256=%s api=%s go=%s %s/%s out=%s\n",
artifact.PluginID, artifact.Version, artifact.SHA256, artifact.APIVersion, artifact.GoVersion, artifact.GOOS, artifact.GOARCH, outPath)
}
if opts.BuildType == "source" || opts.BuildType == "both" {
outPath := opts.Out
if outPath == "" || opts.BuildType == "both" || strings.HasSuffix(outPath, string(os.PathSeparator)) {
outPath = filepath.Join(outDir, manifest.ID+"-source.mcgp")
}
source, err := adapter.BuildSource(context.Background(), pluginBuildRuntimeRequest{
Dir: opts.Dir,
Manifest: manifest,
Raw: raw,
OutPath: outPath,
Vendor: opts.Vendor,
})
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok source plugin=%s version=%s source_sha256=%s api=%s go=%s %s/%s out=%s\n",
source.PluginID, source.Version, source.SHA256, source.APIVersion, source.GoVersion, source.GOOS, source.GOARCH, outPath)
}
return nil
}
func runPluginTestCLI(args []string) error {
target := "."
profile := "unit,manifest"
configPath := ""
fixturePath := ""
manifestPath := ""
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if target != "." {
return fmt.Errorf("unexpected argument %q", arg)
}
target = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return err
}
i += consumed
switch key {
case "profile":
profile = value
case "config":
configPath = value
case "fixture":
fixturePath = value
case "manifest":
manifestPath = value
default:
return fmt.Errorf("unknown test flag --%s", key)
}
}
info, err := os.Stat(target)
if err != nil {
return err
}
if !info.IsDir() {
for _, item := range strings.Split(profile, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
switch item {
case "manifest", "compat", "conformance":
if _, err := validatePluginPathForCLI(target, ""); err != nil {
return err
}
default:
return fmt.Errorf("test profile %q requires a plugin source directory", item)
}
}
fmt.Fprintf(os.Stdout, "ok test target=%s profile=%s\n", target, profile)
return nil
}
manifest, _, err := readPluginDirManifest(target, manifestPath)
if err != nil {
return err
}
adapter, err := pluginCLIAdapterForRuntime(manifest.Runtime.Type)
if err != nil {
return err
}
if err := adapter.Test(context.Background(), pluginTestCLIOptions{
Target: target,
Manifest: manifestPath,
Profile: profile,
ConfigPath: configPath,
FixturePath: fixturePath,
Source: manifest,
}); err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok test target=%s profile=%s\n", target, profile)
return nil
}
func parseGovernanceCLIOptions(args []string) (pluginGovernanceCLIOptions, error) {
opts := pluginGovernanceCLIOptions{
Profile: pluginmanager.PolicyProfileDev,
Action: pluginmanager.GovernanceActionEnable,
Priority: pluginmanager.DefaultPriority,
}
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if opts.Target != "" {
return pluginGovernanceCLIOptions{}, fmt.Errorf("unexpected argument %q", arg)
}
opts.Target = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return pluginGovernanceCLIOptions{}, err
}
i += consumed
switch key {
case "manifest":
opts.Manifest = value
case "config":
opts.ConfigPath = value
case "config-json":
opts.ConfigJSON = value
case "profile":
opts.Profile = value
case "action":
opts.Action = value
case "priority":
parsed, err := strconv.Atoi(value)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --priority %q: %w", value, err)
}
opts.Priority = parsed
case "benchmark-profile":
opts.BenchmarkProfile = value
case "p95-ms":
parsed, err := strconv.ParseFloat(value, 64)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --p95-ms %q: %w", value, err)
}
opts.P95MS = parsed
case "p99-ms":
parsed, err := strconv.ParseFloat(value, 64)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --p99-ms %q: %w", value, err)
}
opts.P99MS = parsed
case "error-rate":
parsed, err := strconv.ParseFloat(value, 64)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --error-rate %q: %w", value, err)
}
opts.ErrorRate = parsed
case "active-proxy-capacity":
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --active-proxy-capacity %q: %w", value, err)
}
opts.ActiveProxyCapacity = parsed
case "baseline-diff":
parsed, err := strconv.ParseFloat(value, 64)
if err != nil {
return pluginGovernanceCLIOptions{}, fmt.Errorf("invalid --baseline-diff %q: %w", value, err)
}
opts.BaselineDiff = parsed
default:
return pluginGovernanceCLIOptions{}, fmt.Errorf("unknown governance flag --%s", key)
}
}
if opts.ConfigPath != "" && opts.ConfigJSON != "" {
return pluginGovernanceCLIOptions{}, errors.New("--config and --config-json are mutually exclusive")
}
return opts, nil
}
func prepareLocalGovernanceManager(opts pluginGovernanceCLIOptions) (*pluginmanager.Manager, func(), pluginmanager.ArtifactRecord, string, error) {
tmpRoot, err := os.MkdirTemp("", "mcgp-governance-cli-*")
if err != nil {
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
cleanup := func() { _ = os.RemoveAll(tmpRoot) }
targetPath := opts.Target
info, err := os.Stat(targetPath)
if err != nil {
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
if info.IsDir() {
manifest, raw, err := readPluginDirManifest(targetPath, opts.Manifest)
if err != nil {
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
targetPath = filepath.Join(tmpRoot, manifest.ID+".mcgp")
if _, err := buildBinaryPluginPackage(opts.Target, manifest, raw, targetPath); err != nil {
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
}
db, err := openPluginCLIDB(filepath.Join(tmpRoot, "plugins.db"))
if err != nil {
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
manager := pluginmanager.New(pluginmanager.Options{
DB: db,
ArtifactRoot: filepath.Join(tmpRoot, "artifacts"),
Adapter: cliStaticRuntimeAdapter{},
PolicyProfile: opts.Profile,
})
artifact, err := manager.UploadArtifact(context.Background(), pluginmanager.ArtifactUpload{
SourcePath: targetPath,
FileName: filepath.Base(targetPath),
Actor: "cli",
})
if err != nil {
_ = db.Close()
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
if artifact.ArtifactType != pluginmanager.ArtifactTypeBinary {
_ = db.Close()
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", fmt.Errorf("governance target must be a binary artifact, got %q", artifact.ArtifactType)
}
configJSON, err := governanceConfigJSON(opts)
if err != nil {
_ = db.Close()
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
if _, err := manager.SetDesired(context.Background(), "cli", artifact.PluginID, artifact.ID, pluginmanager.DesiredDisabled, configJSON, opts.Priority); err != nil {
_ = db.Close()
cleanup()
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
}
return manager, func() {
_ = db.Close()
cleanup()
}, artifact, configJSON, nil
}
func governanceConfigJSON(opts pluginGovernanceCLIOptions) (string, error) {
if opts.ConfigJSON != "" {
if !json.Valid([]byte(opts.ConfigJSON)) {
return "", errors.New("--config-json must be valid JSON")
}
return opts.ConfigJSON, nil
}
if opts.ConfigPath != "" {
data, err := os.ReadFile(opts.ConfigPath)
if err != nil {
return "", err
}
if !json.Valid(data) {
return "", fmt.Errorf("config file %q must contain valid JSON", opts.ConfigPath)
}
return string(data), nil
}
if info, err := os.Stat(opts.Target); err == nil && info.IsDir() {
defaultConfig := filepath.Join(opts.Target, "testdata", "config.json")
if data, err := os.ReadFile(defaultConfig); err == nil {
if !json.Valid(data) {
return "", fmt.Errorf("default config file %q must contain valid JSON", defaultConfig)
}
return string(data), nil
}
}
return "{}", nil
}
func encodePluginCLIJSON(value any) error {
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
return encoder.Encode(value)
}
func runPluginValidatePathCLI(args []string, expectedArtifactType string) (pluginmanager.ArtifactRecord, error) {
if len(args) == 0 {
return pluginmanager.ArtifactRecord{}, errors.New("plugin validate requires a plugin directory, manifest source, or .mcgp artifact")
}
target := ""
manifestPath := ""
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if target != "" {
return pluginmanager.ArtifactRecord{}, fmt.Errorf("unexpected argument %q", arg)
}
target = arg
continue
}
key, value, consumed, err := parsePluginCLIFlag(args, i)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
i += consumed
switch key {
case "manifest":
manifestPath = value
default:
return pluginmanager.ArtifactRecord{}, fmt.Errorf("unknown validate flag --%s", key)
}
}
if target == "" {
return pluginmanager.ArtifactRecord{}, errors.New("plugin validate requires a plugin directory, manifest source, or .mcgp artifact")
}
return validatePluginPathForCLIWithManifest(target, expectedArtifactType, manifestPath)
}
func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginmanager.ArtifactRecord, error) {
return validatePluginPathForCLIWithManifest(targetPath, expectedArtifactType, "")
}
func validatePluginPathForCLIWithManifest(targetPath, expectedArtifactType, manifestPath string) (pluginmanager.ArtifactRecord, error) {
info, err := os.Stat(targetPath)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if info.IsDir() {
artifact, err := validatePluginDirectoryForCLI(targetPath, manifestPath)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if expectedArtifactType != "" && artifact.ArtifactType != expectedArtifactType {
return pluginmanager.ArtifactRecord{}, fmt.Errorf("artifact_type %q does not match expected %q", artifact.ArtifactType, expectedArtifactType)
}
return artifact, nil
}
if isManifestSourceFile(targetPath) {
dir := filepath.Dir(targetPath)
artifact, err := validatePluginDirectoryForCLI(dir, targetPath)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if expectedArtifactType != "" && artifact.ArtifactType != expectedArtifactType {
return pluginmanager.ArtifactRecord{}, fmt.Errorf("artifact_type %q does not match expected %q", artifact.ArtifactType, expectedArtifactType)
}
return artifact, nil
}
tmpRoot, err := os.MkdirTemp("", "mcgp-cli-*")
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
defer os.RemoveAll(tmpRoot)
store := pluginmanager.NewArtifactStore(tmpRoot)
upload := pluginmanager.ArtifactUpload{SourcePath: targetPath, FileName: filepath.Base(targetPath), Actor: "cli"}
if expectedArtifactType == pluginmanager.ArtifactTypeSource {
return store.ValidateAndStoreSource(upload)
}
if expectedArtifactType == pluginmanager.ArtifactTypeBinary {
return store.ValidateAndStoreBinary(upload)
}
return store.ValidateAndStore(upload)
}
func validatePluginDirectoryForCLI(dir, manifestPath string) (pluginmanager.ArtifactRecord, error) {
manifest, raw, err := readPluginDirManifest(dir, manifestPath)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
tmpRoot, err := os.MkdirTemp("", "mcgp-dir-validate-*")
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
defer os.RemoveAll(tmpRoot)
packagePath := filepath.Join(tmpRoot, manifest.ID+"-source.mcgp")
if _, err := writeSourcePackage(dir, manifest, raw, packagePath, false); err != nil {
return pluginmanager.ArtifactRecord{}, err
}
store := pluginmanager.NewArtifactStore(filepath.Join(tmpRoot, "store"))
return store.ValidateAndStoreSource(pluginmanager.ArtifactUpload{SourcePath: packagePath, FileName: filepath.Base(packagePath), Actor: "cli"})
}
func buildBinaryPluginPackage(dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string) (pluginmanager.ArtifactRecord, error) {
return buildBinaryPluginPackageContext(context.Background(), dir, manifest, raw, outPath)
}
func buildBinaryPluginPackageContext(ctx context.Context, dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string) (pluginmanager.ArtifactRecord, error) {
tmpRoot, err := os.MkdirTemp("", "mcgp-build-binary-*")
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
defer os.RemoveAll(tmpRoot)
pluginPath := filepath.Join(tmpRoot, pluginmanager.RuntimeEntry)
buildEntry := sourceBuildEntryForCLI(manifest)
if err := runGoCommand(ctx, dir, "go", "build", "-buildmode=plugin", "-trimpath", "-buildvcs=false", "-o", pluginPath, buildEntry); err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if err := validatePluginSymbol(ctx, dir, pluginPath, manifest); err != nil {
return pluginmanager.ArtifactRecord{}, err
}
manifestBytes, err := materializedManifestJSON(raw, manifest, pluginmanager.ArtifactTypeBinary, false)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if err := writeBinaryPackage(pluginPath, manifestBytes, dir, outPath); err != nil {
return pluginmanager.ArtifactRecord{}, err
}
return validatePluginPathForCLI(outPath, pluginmanager.ArtifactTypeBinary)
}
func buildSourcePluginPackage(dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string, vendor bool) (pluginmanager.ArtifactRecord, error) {
return buildSourcePluginPackageContext(context.Background(), dir, manifest, raw, outPath, vendor)
}
func buildSourcePluginPackageContext(ctx context.Context, dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string, vendor bool) (pluginmanager.ArtifactRecord, error) {
if _, err := writeSourcePackageContext(ctx, dir, manifest, raw, outPath, vendor); err != nil {
return pluginmanager.ArtifactRecord{}, err
}
return validatePluginPathForCLI(outPath, pluginmanager.ArtifactTypeSource)
}
func writeGoPluginTemplate(ctx context.Context, opts pluginInitCLIOptions) error {
_ = ctx
if err := os.MkdirAll(opts.Dir, 0755); err != nil {
return err
}
existing, err := os.ReadDir(opts.Dir)
if err != nil {
return err
}
if len(existing) > 0 {
return fmt.Errorf("target directory %q is not empty", opts.Dir)
}
repoRoot, err := findRepoRoot()
if err != nil {
return err
}
files := map[string]string{
"go.mod": goModTemplate(opts.Module, repoRoot),
"main.go": goPluginMainTemplate(opts),
"main_test.go": goPluginTestTemplate(),
"README.md": readmeTemplate(opts),
manifestFileNameForFormat(opts.Format): manifestTemplate(opts),
"testdata/config.json": configTemplate(opts.Template),
}
for name, content := range files {
target := filepath.Join(opts.Dir, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
if err := os.WriteFile(target, []byte(content), 0644); err != nil {
return err
}
}
fmt.Fprintf(os.Stdout, "ok init plugin=%s template=%s dir=%s\n", opts.ID, opts.Template, opts.Dir)
return nil
}
func materializedManifestJSON(raw map[string]any, manifest pluginmanager.Manifest, artifactType string, vendor bool) ([]byte, error) {
clone := make(map[string]any, len(raw)+4)
for key, value := range raw {
clone[key] = value
}
clone["artifact_type"] = artifactType
clone["go_version"] = runtime.Version()
clone["go_os"] = runtime.GOOS
clone["go_arch"] = runtime.GOARCH
runtimeMap, _ := clone["runtime"].(map[string]any)
if runtimeMap == nil {
runtimeMap = make(map[string]any)
}
if runtimeMap["type"] == nil || runtimeMap["type"] == "" {
runtimeMap["type"] = pluginmanager.RuntimeGoPlugin
}
if runtimeMap["entry_symbol"] == nil || runtimeMap["entry_symbol"] == "" {
runtimeMap["entry_symbol"] = "Plugin"
}
if artifactType == pluginmanager.ArtifactTypeBinary {
runtimeMap["entry"] = pluginmanager.RuntimeEntry
} else if runtimeMap["entry"] == nil || runtimeMap["entry"] == "" {
runtimeMap["entry"] = pluginmanager.RuntimeEntry
}
clone["runtime"] = runtimeMap
if artifactType == pluginmanager.ArtifactTypeSource {
buildMap, _ := clone["build"].(map[string]any)
if buildMap == nil {
buildMap = make(map[string]any)
}
if buildMap["type"] == nil || buildMap["type"] == "" {
buildMap["type"] = pluginmanager.BuildTypeGo
}
if buildMap["entry"] == nil || buildMap["entry"] == "" {
buildMap["entry"] = sourceBuildEntryForCLI(manifest)
}
if buildMap["output"] == nil || buildMap["output"] == "" {
buildMap["output"] = pluginmanager.RuntimeEntry
}
if buildMap["go_version"] == nil || buildMap["go_version"] == "" {
buildMap["go_version"] = runtime.Version()
}
if buildMap["tags"] == nil {
buildMap["tags"] = []string{}
}
if buildMap["vendor_required"] == nil {
buildMap["vendor_required"] = false
}
clone["build"] = buildMap
}
data, err := json.MarshalIndent(clone, "", " ")
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
func writeBinaryPackage(pluginPath string, manifestBytes []byte, sourceDir, outPath string) error {
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
return err
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
zw := zip.NewWriter(out)
if err := addZipBytes(zw, "manifest.json", manifestBytes); err != nil {
zw.Close()
return err
}
if err := addZipFileStable(zw, pluginmanager.RuntimeEntry, pluginPath); err != nil {
zw.Close()
return err
}
for _, name := range optionalPackageDocs(sourceDir) {
if err := addZipFileStable(zw, filepath.ToSlash(name), filepath.Join(sourceDir, name)); err != nil {
zw.Close()
return err
}
}
if err := zw.Close(); err != nil {
return err
}
return out.Close()
}
func writeSourcePackage(dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string, vendor bool) (string, error) {
return writeSourcePackageContext(context.Background(), dir, manifest, raw, outPath, vendor)
}
func writeSourcePackageContext(ctx context.Context, dir string, manifest pluginmanager.Manifest, raw map[string]any, outPath string, vendor bool) (string, error) {
manifestBytes, err := materializedManifestJSON(raw, manifest, pluginmanager.ArtifactTypeSource, vendor)
if err != nil {
return "", err
}
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
return "", err
}
tmpRoot, err := os.MkdirTemp("", "mcgp-source-package-*")
if err != nil {
return "", err
}
defer os.RemoveAll(tmpRoot)
var vendorDir string
if vendor {
vendorDir = filepath.Join(tmpRoot, "vendor")
if err := runGoCommand(ctx, dir, "go", "mod", "vendor", "-o", vendorDir); err != nil {
return "", err
}
}
files, err := collectSourcePackageFiles(dir, vendorDir)
if err != nil {
return "", err
}
out, err := os.Create(outPath)
if err != nil {
return "", err
}
defer out.Close()
zw := zip.NewWriter(out)
if err := addZipBytes(zw, "manifest.json", manifestBytes); err != nil {
zw.Close()
return "", err
}
for _, file := range files {
if err := addZipFileStable(zw, file.Name, file.Path); err != nil {
zw.Close()
return "", err
}
}
if err := zw.Close(); err != nil {
return "", err
}
if err := out.Close(); err != nil {
return "", err
}
sum, err := fileSHA256ForCLI(outPath)
if err != nil {
return "", err
}
return sum, nil
}
type sourcePackageFile struct {
Name string
Path string
}
func collectSourcePackageFiles(dir, vendorDir string) ([]sourcePackageFile, error) {
var files []sourcePackageFile
err := filepath.WalkDir(dir, func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(dir, filePath)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if rel == "." {
return nil
}
if d.IsDir() {
switch rel {
case ".git", "dist", "vendor":
return filepath.SkipDir
}
if strings.HasPrefix(path.Base(rel), ".") {
return filepath.SkipDir
}
return nil
}
if isManifestSourceFile(rel) {
return nil
}
if sourcePackageEntryAllowed(rel) {
files = append(files, sourcePackageFile{Name: rel, Path: filePath})
}
return nil
})
if err != nil {
return nil, err
}
if vendorDir != "" {
err = filepath.WalkDir(vendorDir, func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
rel, err := filepath.Rel(vendorDir, filePath)
if err != nil {
return err
}
files = append(files, sourcePackageFile{Name: path.Join("vendor", filepath.ToSlash(rel)), Path: filePath})
return nil
})
if err != nil {
return nil, err
}
}
sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })
return files, nil
}
func sourcePackageEntryAllowed(name string) bool {
base := path.Base(name)
lower := strings.ToLower(base)
switch {
case name == "go.mod", name == "go.sum":
return true
case strings.HasSuffix(name, ".go"):
return true
case strings.EqualFold(base, "README.md"), strings.EqualFold(base, "LICENSE"):
return true
case strings.Contains(lower, "sbom"):
return true
case strings.HasPrefix(name, "testdata/"):
return true
default:
return false
}
}
func optionalPackageDocs(sourceDir string) []string {
var docs []string
for _, name := range []string{"README.md", "LICENSE"} {
if info, err := os.Stat(filepath.Join(sourceDir, name)); err == nil && !info.IsDir() {
docs = append(docs, name)
}
}
return docs
}
func validatePluginSymbol(ctx context.Context, dir, pluginPath string, manifest pluginmanager.Manifest) error {
output, err := commandOutputForCLI(ctx, dir, "go", "tool", "nm", pluginPath)
if err != nil {
return fmt.Errorf("inspect built plugin symbols: %w\n%s", err, output)
}
entrySymbol := manifest.Runtime.EntrySymbol
if entrySymbol == "" {
entrySymbol = "Plugin"
}
if !strings.Contains(output, entrySymbol) {
return fmt.Errorf("built plugin is missing entry symbol %q", entrySymbol)
}
return nil
}
func runGoCommand(ctx context.Context, dir string, name string, args ...string) error {
output, err := commandOutputForCLI(ctx, dir, name, args...)
if err != nil {
if strings.TrimSpace(output) == "" {
return err
}
return fmt.Errorf("%s %s failed: %w\n%s", name, strings.Join(args, " "), err, output)
}
return nil
}
func commandOutputForCLI(ctx context.Context, dir string, name string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Dir = dir
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err := cmd.Run()
return strings.TrimSpace(out.String()), err
}
func addZipBytes(zw *zip.Writer, name string, data []byte) error {
header := &zip.FileHeader{Name: name, Method: zip.Deflate}
header.SetMode(0644)
header.Modified = time.Unix(0, 0).UTC()
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
_, err = writer.Write(data)
return err
}
func addZipFileStable(zw *zip.Writer, name, filePath string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
mode := fs.FileMode(0644)
if info, err := os.Stat(filePath); err == nil {
mode = info.Mode().Perm()
}
header := &zip.FileHeader{Name: filepath.ToSlash(name), Method: zip.Deflate}
header.SetMode(mode)
header.Modified = time.Unix(0, 0).UTC()
writer, err := zw.CreateHeader(header)
if err != nil {
return err
}
_, err = writer.Write(data)
return err
}
func parsePluginCLIFlag(args []string, index int) (string, string, int, error) {
arg := args[index]
if !strings.HasPrefix(arg, "--") {
return "", "", 0, fmt.Errorf("expected flag, got %q", arg)
}
body := strings.TrimPrefix(arg, "--")
if body == "" {
return "", "", 0, fmt.Errorf("invalid empty flag %q", arg)
}
if key, value, ok := strings.Cut(body, "="); ok {
return key, value, 0, nil
}
if isPluginBoolFlag(body) {
return body, "true", 0, nil
}
if index+1 >= len(args) || strings.HasPrefix(args[index+1], "--") {
return "", "", 0, fmt.Errorf("flag --%s requires a value", body)
}
return body, args[index+1], 1, nil
}
func isPluginBoolFlag(name string) bool {
switch name {
case "skip-tests", "vendor", "json", "quiet", "dry-run", "write", "canonical-json", "source", "full-desired":
return true
default:
return false
}
}
func parsePluginBoolFlag(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "1", "t", "true", "yes", "y", "on":
return true
default:
return false
}
}
func sourceBuildEntryForCLI(manifest pluginmanager.Manifest) string {
entry := strings.TrimSpace(manifest.Build.Entry)
if entry == "" {
entry = strings.TrimSpace(manifest.Runtime.BuildEntry)
}
if entry == "" {
entry = pluginmanager.SourceBuildEntry
}
return entry
}
func validateTestFileIfSet(filePath, label string) error {
if filePath == "" {
return nil
}
info, err := os.Stat(filePath)
if err != nil {
return fmt.Errorf("%s %q is not readable: %w", label, filePath, err)
}
if info.IsDir() {
return fmt.Errorf("%s %q is a directory", label, filePath)
}
return nil
}
func fileSHA256ForCLI(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func titleFromPluginID(id string) string {
parts := strings.FieldsFunc(id, func(r rune) bool { return r == '-' || r == '_' || r == '.' })
for i, part := range parts {
if part == "" {
continue
}
parts[i] = strings.ToUpper(part[:1]) + part[1:]
}
if len(parts) == 0 {
return id
}
return strings.Join(parts, " ")
}
func findRepoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
data, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err == nil && strings.Contains(string(data), "module github.com/tursom/mc-gateway") {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", errors.New("could not locate mc-gateway repository root")
}
dir = parent
}
}
func goModTemplate(module, repoRoot string) string {
return fmt.Sprintf(`module %s
go 1.24.0
require github.com/tursom/mc-gateway v0.0.0
replace github.com/tursom/mc-gateway => %s
`, module, filepath.ToSlash(repoRoot))
}
func validateManifestTemplateFormat(format string) error {
switch format {
case manifestFormatYAML, "yml", manifestFormatTOML, manifestFormatJSONC, manifestFormatJSON:
return nil
default:
return fmt.Errorf("unsupported --manifest-format %q", format)
}
}
func manifestFileNameForFormat(format string) string {
switch format {
case manifestFormatJSON:
return "manifest.json"
case manifestFormatJSONC:
return "manifest.jsonc"
case manifestFormatTOML:
return "manifest.toml"
case "yml":
return "manifest.yml"
default:
return "manifest.yaml"
}
}
func manifestTemplate(opts pluginInitCLIOptions) string {
switch opts.Format {
case manifestFormatJSON:
return manifestSourceJSONTemplate(opts, false)
case manifestFormatJSONC:
return manifestSourceJSONTemplate(opts, true)
case manifestFormatTOML:
return manifestTOMLTemplate(opts)
default:
return manifestYAMLTemplate(opts)
}
}
func manifestSourceJSONTemplate(opts pluginInitCLIOptions, jsonc bool) string {
mode := pluginmanager.UpstreamModeDialer
if opts.Template == "protocol-proxy" {
mode = pluginmanager.UpstreamModeProtocolProxy
}
prefix := ""
if jsonc {
prefix = "// Human-maintained plugin manifest. Build packages normalize this into manifest.json.\n"
}
manifest := fmt.Sprintf(`%s{
"schema_version": "mc-gateway.plugin/v1",
"id": %q,
"name": %q,
"version": "0.1.0",
"description": %q,
"artifact_type": "source",
"runtime": {
"type": "go-plugin",
"entry": "plugin.so",
"entry_symbol": "Plugin"
},
"build": {
"type": "go",
"entry": ".",
"output": "plugin.so",
"tags": [],
"vendor_required": false
},
"api_version": "plugin-api/v1",
"sdk_module": "github.com/tursom/mc-gateway/plugin/api",
"sdk_module_version": "v0.1.0",
"extension_points": [
{ "type": "hook", "key": %q }
],
"capabilities": {
"upstream_connect": { "mode": %q }
},
"runtime_limits": {
"handler_timeout_ms": 3000,
"initial_write_timeout_ms": 1000
},
"config_schema": {
"type": "object",
"properties": {
"match_host": { "type": "string" },
"upstream": { "type": "string" }
}
}
}
`, prefix, opts.ID, opts.Name, opts.Name+" plugin.", opts.Extension, mode)
return manifest
}
func manifestYAMLTemplate(opts pluginInitCLIOptions) string {
mode := pluginmanager.UpstreamModeDialer
if opts.Template == "protocol-proxy" {
mode = pluginmanager.UpstreamModeProtocolProxy
}
return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。
schema_version: mc-gateway.plugin/v1
id: %q
name: %q
version: 0.1.0
description: %q
artifact_type: source
runtime:
type: go-plugin
entry: plugin.so
entry_symbol: Plugin
build:
type: go
entry: "."
output: plugin.so
tags: []
vendor_required: false
api_version: plugin-api/v1
sdk_module: github.com/tursom/mc-gateway/plugin/api
sdk_module_version: v0.1.0
extension_points:
- type: hook
key: %q
capabilities:
upstream_connect:
mode: %q
runtime_limits:
handler_timeout_ms: 3000
initial_write_timeout_ms: 1000
config_schema:
type: object
properties:
match_host:
type: string
upstream:
type: string
`, opts.ID, opts.Name, opts.Name+" plugin.", opts.Extension, mode)
}
func manifestTOMLTemplate(opts pluginInitCLIOptions) string {
mode := pluginmanager.UpstreamModeDialer
if opts.Template == "protocol-proxy" {
mode = pluginmanager.UpstreamModeProtocolProxy
}
return fmt.Sprintf(`# 人工维护的插件清单;构建插件包时会规范化为 manifest.json。
schema_version = "mc-gateway.plugin/v1"
id = %q
name = %q
version = "0.1.0"
description = %q
artifact_type = "source"
api_version = "plugin-api/v1"
sdk_module = "github.com/tursom/mc-gateway/plugin/api"
sdk_module_version = "v0.1.0"
[runtime]
type = "go-plugin"
entry = "plugin.so"
entry_symbol = "Plugin"
[build]
type = "go"
entry = "."
output = "plugin.so"
tags = []
vendor_required = false
[[extension_points]]
type = "hook"
key = %q
[capabilities.upstream_connect]
mode = %q
[runtime_limits]
handler_timeout_ms = 3000
initial_write_timeout_ms = 1000
[config_schema]
type = "object"
[config_schema.properties.match_host]
type = "string"
[config_schema.properties.upstream]
type = "string"
`, opts.ID, opts.Name, opts.Name+" plugin.", opts.Extension, mode)
}
func goPluginMainTemplate(opts pluginInitCLIOptions) string {
if opts.Template == "protocol-proxy" {
return protocolProxyMainTemplate()
}
return upstreamDialerMainTemplate()
}
func upstreamDialerMainTemplate() string {
return `package main
import (
"net"
"github.com/tursom/mc-gateway/plugin/api"
)
type PluginImpl struct {
api.AbstractPlugin
config Config
}
type Config struct {
MatchHost string ` + "`json:\"match_host\"`" + `
Upstream string ` + "`json:\"upstream\"`" + `
}
func Plugin() api.Plugin {
return &PluginImpl{}
}
func (p *PluginImpl) NewConfigObj() any {
return &Config{}
}
func (p *PluginImpl) ReloadConfig(config any) error {
if cfg, ok := config.(*Config); ok {
p.config = *cfg
}
return nil
}
func (p *PluginImpl) Init(gateway api.Gateway) error {
return api.RegisterHookHandler(
gateway,
api.HookUpstreamConnect,
func(req api.UpstreamConnectRequest) bool {
return p.config.MatchHost == "" || req.Host == p.config.MatchHost || req.Upstream == p.config.MatchHost
},
func(req api.UpstreamConnectRequest) (net.Conn, error) {
if p.config.Upstream == "" {
return nil, api.ErrPass
}
if p.config.MatchHost != "" && req.Host != p.config.MatchHost && req.Upstream != p.config.MatchHost {
return nil, api.ErrPass
}
return net.Dial("tcp", p.config.Upstream)
},
)
}
`
}
func protocolProxyMainTemplate() string {
return `package main
import (
"net"
"github.com/tursom/mc-gateway/plugin/api"
)
type PluginImpl struct {
api.AbstractPlugin
config Config
}
type Config struct {
MatchHost string ` + "`json:\"match_host\"`" + `
}
func Plugin() api.Plugin {
return &PluginImpl{}
}
func (p *PluginImpl) NewConfigObj() any {
return &Config{}
}
func (p *PluginImpl) ReloadConfig(config any) error {
if cfg, ok := config.(*Config); ok {
p.config = *cfg
}
return nil
}
func (p *PluginImpl) Init(gateway api.Gateway) error {
return api.RegisterHookHandler(
gateway,
api.HookUpstreamConnect,
func(req api.UpstreamConnectRequest) bool {
return p.config.MatchHost == "" || req.Host == p.config.MatchHost
},
func(req api.UpstreamConnectRequest) (net.Conn, error) {
return nil, api.ErrPass
},
)
}
`
}
func goPluginTestTemplate() string {
return `package main
import (
"testing"
"github.com/tursom/mc-gateway/plugin/api"
)
func TestPluginFactory(t *testing.T) {
if plugin := Plugin(); plugin == nil {
t.Fatal("Plugin() returned nil")
} else if _, ok := plugin.(api.Plugin); !ok {
t.Fatal("Plugin() did not return api.Plugin")
}
}
`
}
func readmeTemplate(opts pluginInitCLIOptions) string {
return fmt.Sprintf(`# %s
Build and test:
`+"```sh"+`
gateway plugin test .
gateway plugin build . --type both
`+"```"+`
`, opts.Name)
}
func configTemplate(template string) string {
if template == "protocol-proxy" {
return "{\n \"match_host\": \"play.example\"\n}\n"
}
return "{\n \"match_host\": \"play.example\",\n \"upstream\": \"127.0.0.1:25566\"\n}\n"
}