Files
mc-gateway/cmd/gateway/plugin_cli_toolchain.go

1717 lines
48 KiB
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
SkipTests bool
Vendor bool
}
type pluginInitCLIOptions struct {
Dir string
ID string
Name string
Template string
Runtime string
Module string
Extension string
}
type pluginGovernanceCLIOptions struct {
Target 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
Profile string
ConfigPath string
FixturePath string
Manifest 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); 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); 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 := "manifest.json"
write := false
for i := 0; i < len(args); i++ {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
if target != "manifest.json" {
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)
default:
return fmt.Errorf("unknown manifest format flag --%s", key)
}
}
manifestPath, err := resolveManifestPath(target)
if err != nil {
return err
}
data, err := os.ReadFile(manifestPath)
if err != nil {
return err
}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("invalid manifest.json: %w", err)
}
formatted, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return err
}
formatted = append(formatted, '\n')
if write {
if bytes.Equal(data, formatted) {
fmt.Fprintf(os.Stdout, "ok manifest=%s unchanged\n", manifestPath)
return nil
}
if err := os.WriteFile(manifestPath, formatted, 0644); err != nil {
return err
}
fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", manifestPath)
return nil
}
_, err = os.Stdout.Write(formatted)
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,
}
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
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
}
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 "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)
}
if !opts.SkipTests {
if err := runGoCommand(context.Background(), opts.Dir, "go", "test", "./..."); err != nil {
return err
}
}
manifest, raw, err := readPluginDirManifest(opts.Dir)
if err != nil {
return err
}
adapter, err := pluginCLIAdapterForRuntime(manifest.Runtime.Type)
if err != nil {
return err
}
outDir := opts.Out
if outDir == "" || strings.HasSuffix(outDir, string(os.PathSeparator)) {
if outDir == "" {
outDir = filepath.Join(opts.Dir, "dist")
}
} else if opts.BuildType == "both" {
return errors.New("--out must be a directory when --type both")
}
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 := ""
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
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)
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,
Profile: profile,
ConfigPath: configPath,
FixturePath: fixturePath,
Manifest: 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 "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)
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 validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginmanager.ArtifactRecord, error) {
info, err := os.Stat(targetPath)
if err != nil {
return pluginmanager.ArtifactRecord{}, err
}
if info.IsDir() {
artifact, err := validatePluginDirectoryForCLI(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
}
if filepath.Base(targetPath) == "manifest.json" {
dir := filepath.Dir(targetPath)
artifact, err := validatePluginDirectoryForCLI(dir)
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 string) (pluginmanager.ArtifactRecord, error) {
manifest, raw, err := readPluginDirManifest(dir)
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),
"manifest.json": 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 readPluginDirManifest(dir string) (pluginmanager.Manifest, map[string]any, error) {
data, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
if err != nil {
return pluginmanager.Manifest{}, nil, err
}
var manifest pluginmanager.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return pluginmanager.Manifest{}, nil, fmt.Errorf("invalid manifest.json: %w", err)
}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return pluginmanager.Manifest{}, nil, fmt.Errorf("invalid manifest.json object: %w", err)
}
if manifest.ID == "" {
return pluginmanager.Manifest{}, nil, errors.New("manifest id is required")
}
return manifest, raw, nil
}
func resolveManifestPath(target string) (string, error) {
info, err := os.Stat(target)
if err != nil {
return "", err
}
if info.IsDir() {
return filepath.Join(target, "manifest.json"), nil
}
if filepath.Base(target) != "manifest.json" {
return "", fmt.Errorf("manifest path must be manifest.json or a plugin directory, got %q", target)
}
return target, 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 rel == "manifest.json" {
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", "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 manifestTemplate(opts pluginInitCLIOptions) string {
mode := pluginmanager.UpstreamModeDialer
if opts.Template == "protocol-proxy" {
mode = pluginmanager.UpstreamModeProtocolProxy
}
manifest := fmt.Sprintf(`{
"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)
return manifest
}
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"
}