feat(plugin): support multi-format manifest sources
This commit is contained in:
@@ -206,11 +206,7 @@ func runPluginCLI(args []string) (bool, int) {
|
|||||||
}
|
}
|
||||||
return true, 0
|
return true, 0
|
||||||
case "validate", "compat":
|
case "validate", "compat":
|
||||||
if len(args) < 3 {
|
artifact, err := runPluginValidatePathCLI(args[2:], "")
|
||||||
printPluginCLIUsage()
|
|
||||||
return true, 2
|
|
||||||
}
|
|
||||||
artifact, err := validatePluginPathForCLI(args[2], "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return true, 1
|
return true, 1
|
||||||
@@ -219,11 +215,7 @@ func runPluginCLI(args []string) (bool, int) {
|
|||||||
artifact.PluginID, artifact.Version, artifact.SHA256, artifact.APIVersion, artifact.GoVersion, artifact.GOOS, artifact.GOARCH)
|
artifact.PluginID, artifact.Version, artifact.SHA256, artifact.APIVersion, artifact.GoVersion, artifact.GOOS, artifact.GOARCH)
|
||||||
return true, 0
|
return true, 0
|
||||||
case "source-validate":
|
case "source-validate":
|
||||||
if len(args) < 3 {
|
source, err := runPluginValidatePathCLI(args[2:], pluginmanager.ArtifactTypeSource)
|
||||||
printPluginCLIUsage()
|
|
||||||
return true, 2
|
|
||||||
}
|
|
||||||
source, err := validatePluginPathForCLI(args[2], pluginmanager.ArtifactTypeSource)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
return true, 1
|
return true, 1
|
||||||
|
|||||||
346
cmd/gateway/plugin_cli_manifest.go
Normal file
346
cmd/gateway/plugin_cli_manifest.go
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
"github.com/tursom/mc-gateway/internal/pluginmanager"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
manifestFormatJSON = "json"
|
||||||
|
manifestFormatJSONC = "jsonc"
|
||||||
|
manifestFormatYAML = "yaml"
|
||||||
|
manifestFormatTOML = "toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var manifestSourceNames = []string{
|
||||||
|
"manifest.yaml",
|
||||||
|
"manifest.yml",
|
||||||
|
"manifest.toml",
|
||||||
|
"manifest.jsonc",
|
||||||
|
"manifest.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginManifestSource struct {
|
||||||
|
Path string
|
||||||
|
Format string
|
||||||
|
Data []byte
|
||||||
|
Manifest pluginmanager.Manifest
|
||||||
|
Raw map[string]any
|
||||||
|
CanonicalJSON []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPluginDirManifest(dir, explicitManifestPath string) (pluginmanager.Manifest, map[string]any, error) {
|
||||||
|
source, err := readPluginManifestSource(dir, explicitManifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return pluginmanager.Manifest{}, nil, err
|
||||||
|
}
|
||||||
|
return source.Manifest, source.Raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPluginManifestSource(target, explicitManifestPath string) (pluginManifestSource, error) {
|
||||||
|
manifestPath, err := resolveManifestSourcePath(target, explicitManifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return pluginManifestSource{}, err
|
||||||
|
}
|
||||||
|
return readPluginManifestSourceFile(manifestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveManifestSourcePath(target, explicitManifestPath string) (string, error) {
|
||||||
|
if explicitManifestPath != "" {
|
||||||
|
return resolveExplicitManifestPath(target, explicitManifestPath)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
if !isManifestSourceFile(target) {
|
||||||
|
return "", fmt.Errorf("manifest path must be one of %s, got %q", strings.Join(manifestSourceNames, ", "), target)
|
||||||
|
}
|
||||||
|
return target, nil
|
||||||
|
}
|
||||||
|
var found []string
|
||||||
|
for _, name := range manifestSourceNames {
|
||||||
|
candidate := filepath.Join(target, name)
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() {
|
||||||
|
found = append(found, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(found) == 0 {
|
||||||
|
return "", fmt.Errorf("plugin manifest is required: expected one of %s in %s", strings.Join(manifestSourceNames, ", "), target)
|
||||||
|
}
|
||||||
|
if len(found) > 1 {
|
||||||
|
sort.Strings(found)
|
||||||
|
return "", fmt.Errorf("multiple plugin manifests found: %s; pass --manifest to select one", strings.Join(found, ", "))
|
||||||
|
}
|
||||||
|
return found[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveExplicitManifestPath(target, explicitManifestPath string) (string, error) {
|
||||||
|
candidates := []string{explicitManifestPath}
|
||||||
|
if info, err := os.Stat(target); err == nil && info.IsDir() && !filepath.IsAbs(explicitManifestPath) {
|
||||||
|
candidates = []string{filepath.Join(target, explicitManifestPath), explicitManifestPath}
|
||||||
|
}
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
info, err := os.Stat(candidate)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return "", fmt.Errorf("manifest path %q is a directory", candidate)
|
||||||
|
}
|
||||||
|
if !isManifestSourceFile(candidate) {
|
||||||
|
return "", fmt.Errorf("manifest path must be one of %s, got %q", strings.Join(manifestSourceNames, ", "), candidate)
|
||||||
|
}
|
||||||
|
return candidate, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("manifest path %q is not readable", explicitManifestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPluginManifestSourceFile(manifestPath string) (pluginManifestSource, error) {
|
||||||
|
format, err := manifestFormatForPath(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return pluginManifestSource{}, err
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return pluginManifestSource{}, err
|
||||||
|
}
|
||||||
|
raw, err := decodeManifestSource(data, format)
|
||||||
|
if err != nil {
|
||||||
|
return pluginManifestSource{}, fmt.Errorf("invalid %s: %w", filepath.Base(manifestPath), err)
|
||||||
|
}
|
||||||
|
canonical, err := json.MarshalIndent(raw, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return pluginManifestSource{}, err
|
||||||
|
}
|
||||||
|
canonical = append(canonical, '\n')
|
||||||
|
var manifest pluginmanager.Manifest
|
||||||
|
if err := json.Unmarshal(canonical, &manifest); err != nil {
|
||||||
|
return pluginManifestSource{}, fmt.Errorf("invalid %s object: %w", filepath.Base(manifestPath), err)
|
||||||
|
}
|
||||||
|
if manifest.ID == "" {
|
||||||
|
return pluginManifestSource{}, errors.New("manifest id is required")
|
||||||
|
}
|
||||||
|
return pluginManifestSource{
|
||||||
|
Path: manifestPath,
|
||||||
|
Format: format,
|
||||||
|
Data: data,
|
||||||
|
Manifest: manifest,
|
||||||
|
Raw: raw,
|
||||||
|
CanonicalJSON: canonical,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeManifestSource(data []byte, format string) (map[string]any, error) {
|
||||||
|
var raw any
|
||||||
|
switch format {
|
||||||
|
case manifestFormatJSON:
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case manifestFormatJSONC:
|
||||||
|
stripped, err := stripJSONC(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(stripped, &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case manifestFormatYAML:
|
||||||
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case manifestFormatTOML:
|
||||||
|
var table map[string]any
|
||||||
|
if err := toml.Unmarshal(data, &table); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw = table
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported manifest format %q", format)
|
||||||
|
}
|
||||||
|
normalized, ok := normalizeManifestValue(raw).(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("manifest root must be an object")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeManifestValue(value any) any {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
out := make(map[string]any, len(v))
|
||||||
|
for key, item := range v {
|
||||||
|
out[key] = normalizeManifestValue(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case map[any]any:
|
||||||
|
out := make(map[string]any, len(v))
|
||||||
|
for key, item := range v {
|
||||||
|
out[fmt.Sprint(key)] = normalizeManifestValue(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case []any:
|
||||||
|
out := make([]any, len(v))
|
||||||
|
for i, item := range v {
|
||||||
|
out[i] = normalizeManifestValue(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func manifestFormatForPath(filePath string) (string, error) {
|
||||||
|
switch strings.ToLower(filepath.Base(filePath)) {
|
||||||
|
case "manifest.json":
|
||||||
|
return manifestFormatJSON, nil
|
||||||
|
case "manifest.jsonc":
|
||||||
|
return manifestFormatJSONC, nil
|
||||||
|
case "manifest.yaml", "manifest.yml":
|
||||||
|
return manifestFormatYAML, nil
|
||||||
|
case "manifest.toml":
|
||||||
|
return manifestFormatTOML, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported manifest file %q", filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isManifestSourceFile(filePath string) bool {
|
||||||
|
_, err := manifestFormatForPath(filePath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripJSONC(data []byte) ([]byte, error) {
|
||||||
|
withoutComments, err := stripJSONCComments(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return stripJSONCTrailingCommas(withoutComments), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripJSONCComments(data []byte) ([]byte, error) {
|
||||||
|
out := make([]byte, 0, len(data))
|
||||||
|
inString := false
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
ch := data[i]
|
||||||
|
if inString {
|
||||||
|
out = append(out, ch)
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' {
|
||||||
|
inString = true
|
||||||
|
out = append(out, ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '/' && i+1 < len(data) {
|
||||||
|
next := data[i+1]
|
||||||
|
if next == '/' {
|
||||||
|
out = append(out, ' ', ' ')
|
||||||
|
i += 2
|
||||||
|
for ; i < len(data); i++ {
|
||||||
|
if data[i] == '\n' || data[i] == '\r' {
|
||||||
|
out = append(out, data[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
out = append(out, ' ')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if next == '*' {
|
||||||
|
out = append(out, ' ', ' ')
|
||||||
|
i += 2
|
||||||
|
closed := false
|
||||||
|
for ; i < len(data); i++ {
|
||||||
|
if data[i] == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||||
|
out = append(out, ' ', ' ')
|
||||||
|
i++
|
||||||
|
closed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if data[i] == '\n' || data[i] == '\r' {
|
||||||
|
out = append(out, data[i])
|
||||||
|
} else {
|
||||||
|
out = append(out, ' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !closed {
|
||||||
|
return nil, errors.New("unterminated block comment")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, ch)
|
||||||
|
}
|
||||||
|
if inString {
|
||||||
|
return nil, errors.New("unterminated string")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripJSONCTrailingCommas(data []byte) []byte {
|
||||||
|
var out bytes.Buffer
|
||||||
|
inString := false
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
ch := data[i]
|
||||||
|
if inString {
|
||||||
|
out.WriteByte(ch)
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == '"' {
|
||||||
|
inString = true
|
||||||
|
out.WriteByte(ch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ch == ',' {
|
||||||
|
j := i + 1
|
||||||
|
for j < len(data) && isJSONWhitespace(data[j]) {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if j < len(data) && (data[j] == '}' || data[j] == ']') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteByte(ch)
|
||||||
|
}
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONWhitespace(ch byte) bool {
|
||||||
|
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ type pluginBuildCLIOptions struct {
|
|||||||
BuildType string
|
BuildType string
|
||||||
Out string
|
Out string
|
||||||
FromSource string
|
FromSource string
|
||||||
|
Manifest string
|
||||||
SkipTests bool
|
SkipTests bool
|
||||||
Vendor bool
|
Vendor bool
|
||||||
}
|
}
|
||||||
@@ -42,10 +43,12 @@ type pluginInitCLIOptions struct {
|
|||||||
Runtime string
|
Runtime string
|
||||||
Module string
|
Module string
|
||||||
Extension string
|
Extension string
|
||||||
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
type pluginGovernanceCLIOptions struct {
|
type pluginGovernanceCLIOptions struct {
|
||||||
Target string
|
Target string
|
||||||
|
Manifest string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
ConfigJSON string
|
ConfigJSON string
|
||||||
Profile string
|
Profile string
|
||||||
@@ -77,10 +80,11 @@ type pluginBuildRuntimeRequest struct {
|
|||||||
|
|
||||||
type pluginTestCLIOptions struct {
|
type pluginTestCLIOptions struct {
|
||||||
Target string
|
Target string
|
||||||
|
Manifest string
|
||||||
Profile string
|
Profile string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
FixturePath string
|
FixturePath string
|
||||||
Manifest pluginmanager.Manifest
|
Source pluginmanager.Manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
type cliStaticRuntimeAdapter struct{}
|
type cliStaticRuntimeAdapter struct{}
|
||||||
@@ -119,7 +123,7 @@ func (goPluginCLIAdapter) Test(ctx context.Context, opts pluginTestCLIOptions) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "manifest":
|
case "manifest":
|
||||||
if _, err := validatePluginDirectoryForCLI(opts.Target); err != nil {
|
if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case "harness", "protocol-smoke", "conformance":
|
case "harness", "protocol-smoke", "conformance":
|
||||||
@@ -129,7 +133,7 @@ func (goPluginCLIAdapter) Test(ctx context.Context, opts pluginTestCLIOptions) e
|
|||||||
if err := validateTestFileIfSet(opts.FixturePath, "fixture"); err != nil {
|
if err := validateTestFileIfSet(opts.FixturePath, "fixture"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := validatePluginDirectoryForCLI(opts.Target); err != nil {
|
if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -281,12 +285,15 @@ func runPluginManifestCLI(args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runPluginManifestFormatCLI(args []string) error {
|
func runPluginManifestFormatCLI(args []string) error {
|
||||||
target := "manifest.json"
|
target := "."
|
||||||
|
manifestPath := ""
|
||||||
|
artifactType := ""
|
||||||
write := false
|
write := false
|
||||||
|
canonicalJSON := false
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
if !strings.HasPrefix(arg, "--") {
|
if !strings.HasPrefix(arg, "--") {
|
||||||
if target != "manifest.json" {
|
if target != "." {
|
||||||
return fmt.Errorf("unexpected argument %q", arg)
|
return fmt.Errorf("unexpected argument %q", arg)
|
||||||
}
|
}
|
||||||
target = arg
|
target = arg
|
||||||
@@ -300,39 +307,68 @@ func runPluginManifestFormatCLI(args []string) error {
|
|||||||
switch key {
|
switch key {
|
||||||
case "write":
|
case "write":
|
||||||
write = parsePluginBoolFlag(value)
|
write = parsePluginBoolFlag(value)
|
||||||
|
case "canonical-json":
|
||||||
|
canonicalJSON = parsePluginBoolFlag(value)
|
||||||
|
case "manifest":
|
||||||
|
manifestPath = value
|
||||||
|
case "type":
|
||||||
|
artifactType = value
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown manifest format flag --%s", key)
|
return fmt.Errorf("unknown manifest format flag --%s", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
manifestPath, err := resolveManifestPath(target)
|
if write && canonicalJSON {
|
||||||
|
return errors.New("--write and --canonical-json are mutually exclusive")
|
||||||
|
}
|
||||||
|
source, err := readPluginManifestSource(target, manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(manifestPath)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var raw map[string]any
|
_, err = os.Stdout.Write(canonical)
|
||||||
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
|
return err
|
||||||
}
|
}
|
||||||
formatted = append(formatted, '\n')
|
if artifactType != "" {
|
||||||
|
return errors.New("--type is only valid with --canonical-json")
|
||||||
|
}
|
||||||
if write {
|
if write {
|
||||||
if bytes.Equal(data, formatted) {
|
if source.Format != manifestFormatJSON {
|
||||||
fmt.Fprintf(os.Stdout, "ok manifest=%s unchanged\n", manifestPath)
|
fmt.Fprintf(os.Stdout, "ok manifest=%s validated comments_preserved=true\n", source.Path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(manifestPath, formatted, 0644); err != 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
|
return err
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", manifestPath)
|
fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", source.Path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err = os.Stdout.Write(formatted)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +553,7 @@ func runPluginInitCLI(args []string) error {
|
|||||||
Template: "upstream-dialer",
|
Template: "upstream-dialer",
|
||||||
Runtime: pluginmanager.RuntimeGoPlugin,
|
Runtime: pluginmanager.RuntimeGoPlugin,
|
||||||
Extension: pluginmanager.ExtensionUpstreamConnect,
|
Extension: pluginmanager.ExtensionUpstreamConnect,
|
||||||
|
Format: manifestFormatYAML,
|
||||||
}
|
}
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
@@ -545,6 +582,8 @@ func runPluginInitCLI(args []string) error {
|
|||||||
opts.Module = value
|
opts.Module = value
|
||||||
case "extension":
|
case "extension":
|
||||||
opts.Extension = value
|
opts.Extension = value
|
||||||
|
case "manifest-format":
|
||||||
|
opts.Format = value
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown init flag --%s", key)
|
return fmt.Errorf("unknown init flag --%s", key)
|
||||||
}
|
}
|
||||||
@@ -561,6 +600,9 @@ func runPluginInitCLI(args []string) error {
|
|||||||
if opts.Module == "" {
|
if opts.Module == "" {
|
||||||
opts.Module = "example.com/" + opts.ID
|
opts.Module = "example.com/" + opts.ID
|
||||||
}
|
}
|
||||||
|
if err := validateManifestTemplateFormat(opts.Format); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
adapter, err := pluginCLIAdapterForRuntime(opts.Runtime)
|
adapter, err := pluginCLIAdapterForRuntime(opts.Runtime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -591,6 +633,8 @@ func runPluginBuildCLI(args []string) error {
|
|||||||
opts.Out = value
|
opts.Out = value
|
||||||
case "from-source":
|
case "from-source":
|
||||||
opts.FromSource = value
|
opts.FromSource = value
|
||||||
|
case "manifest":
|
||||||
|
opts.Manifest = value
|
||||||
case "skip-tests":
|
case "skip-tests":
|
||||||
opts.SkipTests = parsePluginBoolFlag(value)
|
opts.SkipTests = parsePluginBoolFlag(value)
|
||||||
case "vendor":
|
case "vendor":
|
||||||
@@ -613,12 +657,7 @@ func runPluginBuildCLI(args []string) error {
|
|||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported build --type %q", opts.BuildType)
|
return fmt.Errorf("unsupported build --type %q", opts.BuildType)
|
||||||
}
|
}
|
||||||
if !opts.SkipTests {
|
manifest, raw, err := readPluginDirManifest(opts.Dir, opts.Manifest)
|
||||||
if err := runGoCommand(context.Background(), opts.Dir, "go", "test", "./..."); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifest, raw, err := readPluginDirManifest(opts.Dir)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -626,13 +665,20 @@ func runPluginBuildCLI(args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if !opts.SkipTests {
|
||||||
|
if err := runGoCommand(context.Background(), opts.Dir, "go", "test", "./..."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
outDir := opts.Out
|
outDir := opts.Out
|
||||||
if outDir == "" || strings.HasSuffix(outDir, string(os.PathSeparator)) {
|
if opts.BuildType == "both" {
|
||||||
|
if outDir == "" {
|
||||||
|
outDir = filepath.Join(opts.Dir, "dist")
|
||||||
|
}
|
||||||
|
} else if outDir == "" || strings.HasSuffix(outDir, string(os.PathSeparator)) {
|
||||||
if outDir == "" {
|
if outDir == "" {
|
||||||
outDir = filepath.Join(opts.Dir, "dist")
|
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" {
|
if opts.BuildType == "binary" || opts.BuildType == "both" {
|
||||||
outPath := opts.Out
|
outPath := opts.Out
|
||||||
@@ -678,6 +724,7 @@ func runPluginTestCLI(args []string) error {
|
|||||||
profile := "unit,manifest"
|
profile := "unit,manifest"
|
||||||
configPath := ""
|
configPath := ""
|
||||||
fixturePath := ""
|
fixturePath := ""
|
||||||
|
manifestPath := ""
|
||||||
for i := 0; i < len(args); i++ {
|
for i := 0; i < len(args); i++ {
|
||||||
arg := args[i]
|
arg := args[i]
|
||||||
if !strings.HasPrefix(arg, "--") {
|
if !strings.HasPrefix(arg, "--") {
|
||||||
@@ -699,6 +746,8 @@ func runPluginTestCLI(args []string) error {
|
|||||||
configPath = value
|
configPath = value
|
||||||
case "fixture":
|
case "fixture":
|
||||||
fixturePath = value
|
fixturePath = value
|
||||||
|
case "manifest":
|
||||||
|
manifestPath = value
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown test flag --%s", key)
|
return fmt.Errorf("unknown test flag --%s", key)
|
||||||
}
|
}
|
||||||
@@ -725,7 +774,7 @@ func runPluginTestCLI(args []string) error {
|
|||||||
fmt.Fprintf(os.Stdout, "ok test target=%s profile=%s\n", target, profile)
|
fmt.Fprintf(os.Stdout, "ok test target=%s profile=%s\n", target, profile)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
manifest, _, err := readPluginDirManifest(target)
|
manifest, _, err := readPluginDirManifest(target, manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -735,10 +784,11 @@ func runPluginTestCLI(args []string) error {
|
|||||||
}
|
}
|
||||||
if err := adapter.Test(context.Background(), pluginTestCLIOptions{
|
if err := adapter.Test(context.Background(), pluginTestCLIOptions{
|
||||||
Target: target,
|
Target: target,
|
||||||
|
Manifest: manifestPath,
|
||||||
Profile: profile,
|
Profile: profile,
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
FixturePath: fixturePath,
|
FixturePath: fixturePath,
|
||||||
Manifest: manifest,
|
Source: manifest,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -767,6 +817,8 @@ func parseGovernanceCLIOptions(args []string) (pluginGovernanceCLIOptions, error
|
|||||||
}
|
}
|
||||||
i += consumed
|
i += consumed
|
||||||
switch key {
|
switch key {
|
||||||
|
case "manifest":
|
||||||
|
opts.Manifest = value
|
||||||
case "config":
|
case "config":
|
||||||
opts.ConfigPath = value
|
opts.ConfigPath = value
|
||||||
case "config-json":
|
case "config-json":
|
||||||
@@ -836,7 +888,7 @@ func prepareLocalGovernanceManager(opts pluginGovernanceCLIOptions) (*pluginmana
|
|||||||
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
|
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
manifest, raw, err := readPluginDirManifest(targetPath)
|
manifest, raw, err := readPluginDirManifest(targetPath, opts.Manifest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cleanup()
|
cleanup()
|
||||||
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
|
return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err
|
||||||
@@ -925,13 +977,50 @@ func encodePluginCLIJSON(value any) error {
|
|||||||
return encoder.Encode(value)
|
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) {
|
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)
|
info, err := os.Stat(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pluginmanager.ArtifactRecord{}, err
|
return pluginmanager.ArtifactRecord{}, err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
artifact, err := validatePluginDirectoryForCLI(targetPath)
|
artifact, err := validatePluginDirectoryForCLI(targetPath, manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pluginmanager.ArtifactRecord{}, err
|
return pluginmanager.ArtifactRecord{}, err
|
||||||
}
|
}
|
||||||
@@ -940,9 +1029,9 @@ func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginma
|
|||||||
}
|
}
|
||||||
return artifact, nil
|
return artifact, nil
|
||||||
}
|
}
|
||||||
if filepath.Base(targetPath) == "manifest.json" {
|
if isManifestSourceFile(targetPath) {
|
||||||
dir := filepath.Dir(targetPath)
|
dir := filepath.Dir(targetPath)
|
||||||
artifact, err := validatePluginDirectoryForCLI(dir)
|
artifact, err := validatePluginDirectoryForCLI(dir, targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pluginmanager.ArtifactRecord{}, err
|
return pluginmanager.ArtifactRecord{}, err
|
||||||
}
|
}
|
||||||
@@ -967,8 +1056,8 @@ func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginma
|
|||||||
return store.ValidateAndStore(upload)
|
return store.ValidateAndStore(upload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePluginDirectoryForCLI(dir string) (pluginmanager.ArtifactRecord, error) {
|
func validatePluginDirectoryForCLI(dir, manifestPath string) (pluginmanager.ArtifactRecord, error) {
|
||||||
manifest, raw, err := readPluginDirManifest(dir)
|
manifest, raw, err := readPluginDirManifest(dir, manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pluginmanager.ArtifactRecord{}, err
|
return pluginmanager.ArtifactRecord{}, err
|
||||||
}
|
}
|
||||||
@@ -1045,7 +1134,7 @@ func writeGoPluginTemplate(ctx context.Context, opts pluginInitCLIOptions) error
|
|||||||
"main.go": goPluginMainTemplate(opts),
|
"main.go": goPluginMainTemplate(opts),
|
||||||
"main_test.go": goPluginTestTemplate(),
|
"main_test.go": goPluginTestTemplate(),
|
||||||
"README.md": readmeTemplate(opts),
|
"README.md": readmeTemplate(opts),
|
||||||
"manifest.json": manifestTemplate(opts),
|
manifestFileNameForFormat(opts.Format): manifestTemplate(opts),
|
||||||
"testdata/config.json": configTemplate(opts.Template),
|
"testdata/config.json": configTemplate(opts.Template),
|
||||||
}
|
}
|
||||||
for name, content := range files {
|
for name, content := range files {
|
||||||
@@ -1061,39 +1150,6 @@ func writeGoPluginTemplate(ctx context.Context, opts pluginInitCLIOptions) error
|
|||||||
return nil
|
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) {
|
func materializedManifestJSON(raw map[string]any, manifest pluginmanager.Manifest, artifactType string, vendor bool) ([]byte, error) {
|
||||||
clone := make(map[string]any, len(raw)+4)
|
clone := make(map[string]any, len(raw)+4)
|
||||||
for key, value := range raw {
|
for key, value := range raw {
|
||||||
@@ -1267,7 +1323,7 @@ func collectSourcePackageFiles(dir, vendorDir string) ([]sourcePackageFile, erro
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if rel == "manifest.json" {
|
if isManifestSourceFile(rel) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if sourcePackageEntryAllowed(rel) {
|
if sourcePackageEntryAllowed(rel) {
|
||||||
@@ -1421,7 +1477,7 @@ func parsePluginCLIFlag(args []string, index int) (string, string, int, error) {
|
|||||||
|
|
||||||
func isPluginBoolFlag(name string) bool {
|
func isPluginBoolFlag(name string) bool {
|
||||||
switch name {
|
switch name {
|
||||||
case "skip-tests", "vendor", "json", "quiet", "dry-run", "write", "source", "full-desired":
|
case "skip-tests", "vendor", "json", "quiet", "dry-run", "write", "canonical-json", "source", "full-desired":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -1518,12 +1574,53 @@ replace github.com/tursom/mc-gateway => %s
|
|||||||
`, module, filepath.ToSlash(repoRoot))
|
`, 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 {
|
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
|
mode := pluginmanager.UpstreamModeDialer
|
||||||
if opts.Template == "protocol-proxy" {
|
if opts.Template == "protocol-proxy" {
|
||||||
mode = pluginmanager.UpstreamModeProtocolProxy
|
mode = pluginmanager.UpstreamModeProtocolProxy
|
||||||
}
|
}
|
||||||
manifest := fmt.Sprintf(`{
|
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",
|
"schema_version": "mc-gateway.plugin/v1",
|
||||||
"id": %q,
|
"id": %q,
|
||||||
"name": %q,
|
"name": %q,
|
||||||
@@ -1563,10 +1660,104 @@ func manifestTemplate(opts pluginInitCLIOptions) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, opts.ID, opts.Name, opts.Name+" plugin.", opts.Extension, mode)
|
`, prefix, opts.ID, opts.Name, opts.Name+" plugin.", opts.Extension, mode)
|
||||||
return manifest
|
return manifest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func manifestYAMLTemplate(opts pluginInitCLIOptions) string {
|
||||||
|
mode := pluginmanager.UpstreamModeDialer
|
||||||
|
if opts.Template == "protocol-proxy" {
|
||||||
|
mode = pluginmanager.UpstreamModeProtocolProxy
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`# Human-maintained plugin manifest. Build packages normalize this into 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(`# Human-maintained plugin manifest. Build packages normalize this into 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 {
|
func goPluginMainTemplate(opts pluginInitCLIOptions) string {
|
||||||
if opts.Template == "protocol-proxy" {
|
if opts.Template == "protocol-proxy" {
|
||||||
return protocolProxyMainTemplate()
|
return protocolProxyMainTemplate()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -25,12 +26,15 @@ func TestPluginInitCreatesBuildableTemplate(t *testing.T) {
|
|||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("runPluginCLI(init) code = %d, want 0", code)
|
t.Fatalf("runPluginCLI(init) code = %d, want 0", code)
|
||||||
}
|
}
|
||||||
for _, name := range []string{"manifest.json", "go.mod", "main.go", "main_test.go", "README.md", "testdata/config.json"} {
|
for _, name := range []string{"manifest.yaml", "go.mod", "main.go", "main_test.go", "README.md", "testdata/config.json"} {
|
||||||
if _, err := os.Stat(filepath.Join(dir, filepath.FromSlash(name))); err != nil {
|
if _, err := os.Stat(filepath.Join(dir, filepath.FromSlash(name))); err != nil {
|
||||||
t.Fatalf("generated file %s stat error = %v", name, err)
|
t.Fatalf("generated file %s stat error = %v", name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := validatePluginDirectoryForCLI(dir); err != nil {
|
if _, err := os.Stat(filepath.Join(dir, "manifest.json")); err == nil {
|
||||||
|
t.Fatal("plugin init generated manifest.json by default, want manifest.yaml")
|
||||||
|
}
|
||||||
|
if _, err := validatePluginDirectoryForCLI(dir, ""); err != nil {
|
||||||
t.Fatalf("validatePluginDirectoryForCLI() error = %v", err)
|
t.Fatalf("validatePluginDirectoryForCLI() error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +67,40 @@ func TestPluginBuildSourcePackagesTemplate(t *testing.T) {
|
|||||||
t.Fatalf("validatePluginPathForCLI(source) error = %v", err)
|
t.Fatalf("validatePluginPathForCLI(source) error = %v", err)
|
||||||
}
|
}
|
||||||
assertZipContains(t, out, "manifest.json", "go.mod", "main.go", "main_test.go", "README.md", "testdata/config.json")
|
assertZipContains(t, out, "manifest.json", "go.mod", "main.go", "main_test.go", "README.md", "testdata/config.json")
|
||||||
|
assertZipNotContains(t, out, "manifest.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginBuildBothAcceptsOutDirectory(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "both-plugin")
|
||||||
|
handled, code := runPluginCLI([]string{
|
||||||
|
"plugin", "init", dir,
|
||||||
|
"--id", "both-plugin",
|
||||||
|
"--module", "example.com/both-plugin",
|
||||||
|
})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(init) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
outDir := filepath.Join(t.TempDir(), "packages")
|
||||||
|
handled, code = runPluginCLI([]string{
|
||||||
|
"plugin", "build", dir,
|
||||||
|
"--type", "both",
|
||||||
|
"--out", outDir,
|
||||||
|
"--skip-tests",
|
||||||
|
"--vendor=false",
|
||||||
|
})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(build both) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
binaryOut := filepath.Join(outDir, "both-plugin.mcgp")
|
||||||
|
sourceOut := filepath.Join(outDir, "both-plugin-source.mcgp")
|
||||||
|
if _, err := validatePluginPathForCLI(binaryOut, "binary"); err != nil {
|
||||||
|
t.Fatalf("validatePluginPathForCLI(binary) error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := validatePluginPathForCLI(sourceOut, "source"); err != nil {
|
||||||
|
t.Fatalf("validatePluginPathForCLI(source) error = %v", err)
|
||||||
|
}
|
||||||
|
assertZipNotContains(t, binaryOut, "manifest.yaml")
|
||||||
|
assertZipNotContains(t, sourceOut, "manifest.yaml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPluginTestManifestProfile(t *testing.T) {
|
func TestPluginTestManifestProfile(t *testing.T) {
|
||||||
@@ -107,6 +145,7 @@ func TestPluginManifestFormatWrite(t *testing.T) {
|
|||||||
"plugin", "init", dir,
|
"plugin", "init", dir,
|
||||||
"--id", "format-plugin",
|
"--id", "format-plugin",
|
||||||
"--module", "example.com/format-plugin",
|
"--module", "example.com/format-plugin",
|
||||||
|
"--manifest-format", "json",
|
||||||
})
|
})
|
||||||
if !handled || code != 0 {
|
if !handled || code != 0 {
|
||||||
t.Fatalf("runPluginCLI(init) = (%v, %d), want handled code 0", handled, code)
|
t.Fatalf("runPluginCLI(init) = (%v, %d), want handled code 0", handled, code)
|
||||||
@@ -131,6 +170,157 @@ func TestPluginManifestFormatWrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPluginManifestSourceFormats(t *testing.T) {
|
||||||
|
for _, format := range []string{"yaml", "toml", "jsonc", "json"} {
|
||||||
|
t.Run(format, func(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "format-"+format)
|
||||||
|
handled, code := runPluginCLI([]string{
|
||||||
|
"plugin", "init", dir,
|
||||||
|
"--id", "format-" + format,
|
||||||
|
"--module", "example.com/format-" + format,
|
||||||
|
"--manifest-format", format,
|
||||||
|
})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(init %s) = (%v, %d), want handled code 0", format, handled, code)
|
||||||
|
}
|
||||||
|
source, err := readPluginManifestSource(dir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readPluginManifestSource(%s) error = %v", format, err)
|
||||||
|
}
|
||||||
|
if source.Manifest.ID != "format-"+format {
|
||||||
|
t.Fatalf("manifest id = %q, want format-%s", source.Manifest.ID, format)
|
||||||
|
}
|
||||||
|
if !json.Valid(source.CanonicalJSON) {
|
||||||
|
t.Fatalf("canonical JSON for %s is invalid:\n%s", format, source.CanonicalJSON)
|
||||||
|
}
|
||||||
|
packaged, err := materializedManifestJSON(source.Raw, source.Manifest, "binary", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("materializedManifestJSON(%s) error = %v", format, err)
|
||||||
|
}
|
||||||
|
var raw map[string]any
|
||||||
|
if err := json.Unmarshal(packaged, &raw); err != nil {
|
||||||
|
t.Fatalf("Unmarshal(materialized %s) error = %v", format, err)
|
||||||
|
}
|
||||||
|
if raw["artifact_type"] != "binary" || raw["go_version"] == "" || raw["go_os"] == "" || raw["go_arch"] == "" {
|
||||||
|
t.Fatalf("materialized %s manifest missing package fields: %s", format, packaged)
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "validate", dir})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(validate %s) = (%v, %d), want handled code 0", format, handled, code)
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "test", dir, "--profile", "manifest"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(test %s) = (%v, %d), want handled code 0", format, handled, code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginManifestFormatWritePreservesComments(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
file string
|
||||||
|
content string
|
||||||
|
comment string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "yaml",
|
||||||
|
file: "manifest.yaml",
|
||||||
|
content: "# keep yaml comment\n" + manifestYAMLTemplate(pluginInitCLIOptions{ID: "comment-yaml", Name: "Comment YAML", Extension: "upstream.connect/v1"}),
|
||||||
|
comment: "# keep yaml comment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "toml",
|
||||||
|
file: "manifest.toml",
|
||||||
|
content: "# keep toml comment\n" + manifestTOMLTemplate(pluginInitCLIOptions{ID: "comment-toml", Name: "Comment TOML", Extension: "upstream.connect/v1"}),
|
||||||
|
comment: "# keep toml comment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "jsonc",
|
||||||
|
file: "manifest.jsonc",
|
||||||
|
content: strings.Replace(
|
||||||
|
"// keep jsonc comment\n"+strings.TrimSuffix(manifestSourceJSONTemplate(pluginInitCLIOptions{ID: "comment-jsonc", Name: "Comment JSONC", Extension: "upstream.connect/v1"}, true), "\n"),
|
||||||
|
"\n }\n}",
|
||||||
|
"\n },\n}",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
comment: "// keep jsonc comment",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
manifestPath := filepath.Join(dir, tc.file)
|
||||||
|
if err := os.WriteFile(manifestPath, []byte(tc.content), 0644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(%s) error = %v", tc.file, err)
|
||||||
|
}
|
||||||
|
before, err := readPluginManifestSource(dir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readPluginManifestSource(before) error = %v", err)
|
||||||
|
}
|
||||||
|
handled, code := runPluginCLI([]string{"plugin", "manifest", "format", dir, "--write"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(manifest format %s) = (%v, %d), want handled code 0", tc.name, handled, code)
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%s) error = %v", tc.file, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(data), tc.comment) {
|
||||||
|
t.Fatalf("formatted %s lost comment:\n%s", tc.file, data)
|
||||||
|
}
|
||||||
|
after, err := readPluginManifestSource(dir, "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readPluginManifestSource(after) error = %v", err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(before.CanonicalJSON, after.CanonicalJSON) {
|
||||||
|
t.Fatalf("canonical JSON changed after format\nbefore=%s\nafter=%s", before.CanonicalJSON, after.CanonicalJSON)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluginManifestMultipleSourcesRequireExplicitManifest(t *testing.T) {
|
||||||
|
dir := filepath.Join(t.TempDir(), "multi-manifest")
|
||||||
|
handled, code := runPluginCLI([]string{
|
||||||
|
"plugin", "init", dir,
|
||||||
|
"--id", "multi-manifest",
|
||||||
|
"--module", "example.com/multi-manifest",
|
||||||
|
})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(init) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(manifestSourceJSONTemplate(pluginInitCLIOptions{ID: "multi-manifest", Name: "Multi Manifest", Extension: "upstream.connect/v1"}, false)), 0644); err != nil {
|
||||||
|
t.Fatalf("WriteFile(manifest.json) error = %v", err)
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "validate", dir})
|
||||||
|
if !handled {
|
||||||
|
t.Fatal("runPluginCLI(validate) handled = false")
|
||||||
|
}
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatal("runPluginCLI(validate) code = 0, want failure for multiple manifests")
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "validate", dir, "--manifest", "manifest.yaml"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(validate --manifest) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "test", dir, "--profile", "manifest", "--manifest", "manifest.yaml"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(test --manifest) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
out := filepath.Join(t.TempDir(), "multi-manifest-source.mcgp")
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "build", dir, "--type", "source", "--out", out, "--skip-tests", "--vendor=false", "--manifest", "manifest.yaml"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(build --manifest) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
if _, err := validatePluginPathForCLI(out, "source"); err != nil {
|
||||||
|
t.Fatalf("validatePluginPathForCLI(source) error = %v", err)
|
||||||
|
}
|
||||||
|
handled, code = runPluginCLI([]string{"plugin", "manifest", "format", dir, "--manifest", "manifest.yaml", "--canonical-json", "--type", "source"})
|
||||||
|
if !handled || code != 0 {
|
||||||
|
t.Fatalf("runPluginCLI(manifest format --manifest --canonical-json) = (%v, %d), want handled code 0", handled, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPluginGovernanceCommands(t *testing.T) {
|
func TestPluginGovernanceCommands(t *testing.T) {
|
||||||
dir := filepath.Join(t.TempDir(), "governance-plugin")
|
dir := filepath.Join(t.TempDir(), "governance-plugin")
|
||||||
handled, code := runPluginCLI([]string{
|
handled, code := runPluginCLI([]string{
|
||||||
@@ -155,6 +345,7 @@ func TestPluginGovernanceCommands(t *testing.T) {
|
|||||||
{"plugin", "preflight", artifact, "--config-json", `{"upstream":"127.0.0.1:25566"}`, "--profile", "dev"},
|
{"plugin", "preflight", artifact, "--config-json", `{"upstream":"127.0.0.1:25566"}`, "--profile", "dev"},
|
||||||
{"plugin", "self-test", artifact, "--profile", "dev"},
|
{"plugin", "self-test", artifact, "--profile", "dev"},
|
||||||
{"plugin", "benchmark", artifact, "--profile", "dev", "--benchmark-profile", "local-fast", "--p95-ms", "1", "--p99-ms", "2", "--error-rate", "0", "--baseline-diff", "0.1"},
|
{"plugin", "benchmark", artifact, "--profile", "dev", "--benchmark-profile", "local-fast", "--p95-ms", "1", "--p99-ms", "2", "--error-rate", "0", "--baseline-diff", "0.1"},
|
||||||
|
{"plugin", "preflight", dir, "--manifest", "manifest.yaml", "--config-json", `{"upstream":"127.0.0.1:25566"}`, "--profile", "dev"},
|
||||||
} {
|
} {
|
||||||
handled, code = runPluginCLI(tc)
|
handled, code = runPluginCLI(tc)
|
||||||
if !handled {
|
if !handled {
|
||||||
@@ -339,3 +530,21 @@ func assertZipContains(t *testing.T, zipPath string, names ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func assertZipNotContains(t *testing.T, zipPath string, names ...string) {
|
||||||
|
t.Helper()
|
||||||
|
reader, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("OpenReader(%s) error = %v", zipPath, err)
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
seen := make(map[string]bool, len(reader.File))
|
||||||
|
for _, file := range reader.File {
|
||||||
|
seen[file.Name] = true
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
if seen[name] {
|
||||||
|
t.Fatalf("zip %s unexpectedly contains entry %s", zipPath, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
本文定义插件开发工具链的功能需求和实现边界。目标是让插件作者从新建、开发、测试、打包到发布前检查都使用同一套 `gateway plugin` CLI,而不是在每个示例插件里维护重复脚本。
|
本文定义插件开发工具链的功能需求和实现边界。目标是让插件作者从新建、开发、测试、打包到发布前检查都使用同一套 `gateway plugin` CLI,而不是在每个示例插件里维护重复脚本。
|
||||||
|
|
||||||
本设计以 [plugin-system-design.md](plugin-system-design.md) 和 [plugin-implementation-plan.md](plugin-implementation-plan.md) 为上游约束。插件元数据只以 `manifest.json` 为准,Go 代码中不再维护 `manifestJSON` 或等价重复元数据。
|
本设计以 [plugin-system-design.md](plugin-system-design.md) 和 [plugin-implementation-plan.md](plugin-implementation-plan.md) 为上游约束。插件作者只维护一个 manifest source 文件,支持 `manifest.yaml`、`manifest.yml`、`manifest.toml`、`manifest.jsonc` 或 `manifest.json`;`.mcgp` 包内仍统一物化为 `manifest.json`。Go 代码中不再维护 `manifestJSON` 或等价重复元数据。
|
||||||
|
|
||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@
|
|||||||
| 决策 | 结论 |
|
| 决策 | 结论 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| CLI 命名 | 直接扩展 `gateway plugin init/build/test`,不新增 `dev` 子命名空间 |
|
| CLI 命名 | 直接扩展 `gateway plugin init/build/test`,不新增 `dev` 子命名空间 |
|
||||||
| 元数据来源 | `manifest.json` 是唯一人工维护的插件元数据来源 |
|
| 元数据来源 | 插件目录只允许一个人工维护的 manifest source;包内可信元数据统一为 canonical `manifest.json` |
|
||||||
| 打包入口 | `gateway plugin build` 同时承担 build 和 package,不再要求插件目录自带 zip 脚本 |
|
| 打包入口 | `gateway plugin build` 同时承担 build 和 package,不再要求插件目录自带 zip 脚本 |
|
||||||
| 示例插件 | `upstream-rewrite` 和 `mc-auth-proxy` 迁移到标准 CLI,删除重复 `build.sh` 和 `render-manifest` 逻辑 |
|
| 示例插件 | `upstream-rewrite` 和 `mc-auth-proxy` 迁移到标准 CLI,删除重复 `build.sh` 和 `render-manifest` 逻辑 |
|
||||||
| runtime 扩展 | CLI 通过 runtime build/test adapter 分发逻辑,命令名不随 runtime 改变 |
|
| runtime 扩展 | CLI 通过 runtime build/test adapter 分发逻辑,命令名不随 runtime 改变 |
|
||||||
| 校验边界 | CLI 校验不能替代 gateway 服务端上传、构建、准入和 enable 校验 |
|
| 校验边界 | CLI 校验不能替代 gateway 服务端上传、构建、准入和 enable 校验 |
|
||||||
| source manifest | 源码目录中的 `manifest.json` 是作者输入;artifact 包内的 `manifest.json` 是构建时物化结果,不作为第二份人工维护数据 |
|
| source manifest | 源码目录中的 `manifest.yaml/yml/toml/jsonc/json` 是作者输入;artifact 包内的 `manifest.json` 是构建时物化结果,不作为第二份人工维护数据 |
|
||||||
|
|
||||||
## 命令总览
|
## 命令总览
|
||||||
|
|
||||||
@@ -113,6 +113,7 @@ gateway plugin compat dist/my-plugin.mcgp
|
|||||||
- 不需要手写 zip 命令。
|
- 不需要手写 zip 命令。
|
||||||
- 不需要手写 `render-manifest`。
|
- 不需要手写 `render-manifest`。
|
||||||
- 不需要在 Go 代码中声明 manifest 元数据。
|
- 不需要在 Go 代码中声明 manifest 元数据。
|
||||||
|
- 默认模板生成 `manifest.yaml`;如需其它格式可使用 `gateway plugin init --manifest-format yaml|toml|jsonc|json`。
|
||||||
|
|
||||||
### 本地调试
|
### 本地调试
|
||||||
|
|
||||||
@@ -198,7 +199,7 @@ promotion bundle 默认不包含 secret 明文、secret 密文和 runtime state
|
|||||||
|
|
||||||
Go plugin 模板应至少生成:
|
Go plugin 模板应至少生成:
|
||||||
|
|
||||||
- `manifest.json`
|
- `manifest.yaml`(默认;也支持 `manifest.yml`、`manifest.toml`、`manifest.jsonc`、`manifest.json`)
|
||||||
- `go.mod`
|
- `go.mod`
|
||||||
- `main.go`
|
- `main.go`
|
||||||
- `main_test.go`
|
- `main_test.go`
|
||||||
@@ -206,7 +207,7 @@ Go plugin 模板应至少生成:
|
|||||||
- `testdata/config.json`
|
- `testdata/config.json`
|
||||||
- `testdata/fixtures/`,按模板放置 harness 输入
|
- `testdata/fixtures/`,按模板放置 harness 输入
|
||||||
|
|
||||||
生成的 `manifest.json` 只包含作者应该维护的字段。`go_version`、`go_os`、`go_arch` 等环境相关字段可以为空或使用文档化占位;`build` 时再物化到 artifact manifest。
|
生成的 manifest source 只包含作者应该维护的字段。`go_version`、`go_os`、`go_arch` 等环境相关字段可以为空或使用文档化占位;`build` 时再物化到 artifact manifest。
|
||||||
|
|
||||||
## `gateway plugin build`
|
## `gateway plugin build`
|
||||||
|
|
||||||
@@ -222,6 +223,8 @@ Go plugin 模板应至少生成:
|
|||||||
| `gateway plugin build . --type both` | 同时生成 binary 和 source `.mcgp` |
|
| `gateway plugin build . --type both` | 同时生成 binary 和 source `.mcgp` |
|
||||||
| `gateway plugin build --from-source source.mcgp --out built.mcgp` | 使用 gateway builder 从 source 包生成 binary 包 |
|
| `gateway plugin build --from-source source.mcgp --out built.mcgp` | 使用 gateway builder 从 source 包生成 binary 包 |
|
||||||
|
|
||||||
|
当源码目录内存在多个 `manifest.*` 文件,`build` 必须通过 `--manifest <path>` 显式选择源文件;同一规则也适用于 `test`、`validate`、`preflight`、`self-test`、`benchmark` 和 `manifest format`。
|
||||||
|
|
||||||
推荐默认输出:
|
推荐默认输出:
|
||||||
|
|
||||||
- `dist/<plugin-id>.mcgp`
|
- `dist/<plugin-id>.mcgp`
|
||||||
@@ -231,21 +234,23 @@ Go plugin 模板应至少生成:
|
|||||||
|
|
||||||
### Manifest 物化规则
|
### Manifest 物化规则
|
||||||
|
|
||||||
源码目录中的 `manifest.json` 是唯一人工维护文件。`build` 可以在内存中生成 artifact manifest,并写入 `.mcgp` 包内:
|
源码目录中只能存在一个 manifest source 文件。`build` 读取 `manifest.yaml/yml/toml/jsonc/json` 后在内存中生成 artifact manifest,并写入 `.mcgp` 包内的 canonical `manifest.json`:
|
||||||
|
|
||||||
- `artifact_type` 按 `--type` 写为 `binary` 或 `source`。
|
- `artifact_type` 按 `--type` 写为 `binary` 或 `source`。
|
||||||
- binary 包写入 `runtime.entry=plugin.so`。
|
- binary 包写入 `runtime.entry=plugin.so`。
|
||||||
- Go plugin binary 包写入实际 `go_version`、`go_os`、`go_arch`。
|
- Go plugin binary 包写入实际 `go_version`、`go_os`、`go_arch`。
|
||||||
- source 包写入 `build.type=go`、`build.entry`、`build.output`、`build.tags` 和 vendor 策略。
|
- source 包写入 `build.type=go`、`build.entry`、`build.output`、`build.tags` 和 vendor 策略。
|
||||||
- 构建 provenance、module summary、artifact sha256 等写入 build report 或服务端 build record,不要求回写源码目录的 `manifest.json`。
|
- 构建 provenance、module summary、artifact sha256 等写入 build report 或服务端 build record,不要求回写源码目录的 manifest source。
|
||||||
|
|
||||||
这保证源码仓库里没有第二份需要维护的 manifest,也避免 `manifest.json` 与 Go 代码常量不一致。
|
这保证源码仓库里没有第二份需要维护的 manifest,也避免 manifest source 与 Go 代码常量不一致。
|
||||||
|
|
||||||
|
如果目录中同时存在多个 `manifest.*` 文件,CLI 必须失败并要求传入 `--manifest <path>` 显式选择,避免不同格式的 manifest 分叉。`gateway plugin manifest format --canonical-json --type binary|source` 可查看最终写入对应 `.mcgp` 的规范 JSON;不传 `--type` 时使用 manifest source 中的 `artifact_type`,缺省按 binary 处理。`--write` 对 YAML/TOML/JSONC 必须保留注释,无法保留时不能覆盖源文件。
|
||||||
|
|
||||||
### Go Plugin Adapter
|
### Go Plugin Adapter
|
||||||
|
|
||||||
第一版 `go-plugin` build adapter 负责:
|
第一版 `go-plugin` build adapter 负责:
|
||||||
|
|
||||||
1. 读取并校验 `manifest.json`。
|
1. 读取并校验唯一 manifest source,或通过 `--manifest` 指定的 manifest source。
|
||||||
2. 运行 `go test ./...`,除非传入 `--skip-tests`。
|
2. 运行 `go test ./...`,除非传入 `--skip-tests`。
|
||||||
3. 用固定命令构建 `plugin.so`:`go build -buildmode=plugin -trimpath -buildvcs=false`。
|
3. 用固定命令构建 `plugin.so`:`go build -buildmode=plugin -trimpath -buildvcs=false`。
|
||||||
4. 用 `go tool nm` 校验 `Plugin` 符号。
|
4. 用 `go tool nm` 校验 `Plugin` 符号。
|
||||||
@@ -324,7 +329,7 @@ type PluginBuildAdapter interface {
|
|||||||
|
|
||||||
`validate` 应支持三类输入:
|
`validate` 应支持三类输入:
|
||||||
|
|
||||||
- `manifest.json`
|
- manifest source 文件:`manifest.yaml`、`manifest.yml`、`manifest.toml`、`manifest.jsonc` 或 `manifest.json`
|
||||||
- 插件源码目录
|
- 插件源码目录
|
||||||
- `.mcgp` artifact
|
- `.mcgp` artifact
|
||||||
|
|
||||||
@@ -457,7 +462,7 @@ CI 产物应至少保存:
|
|||||||
- README 使用 `gateway plugin build . --type both`。
|
- README 使用 `gateway plugin build . --type both`。
|
||||||
- README 使用 `gateway plugin test .`。
|
- README 使用 `gateway plugin test .`。
|
||||||
- 删除或降级 `build.sh` 为兼容包装;最终不再作为主路径。
|
- 删除或降级 `build.sh` 为兼容包装;最终不再作为主路径。
|
||||||
- 删除 `cmd/render-manifest`,由 CLI 根据源码 `manifest.json` 生成 artifact manifest。
|
- 删除 `cmd/render-manifest`,由 CLI 根据源码 manifest source 生成 artifact manifest。
|
||||||
- 示例插件的测试 fixture 进入 `testdata/fixtures/`。
|
- 示例插件的测试 fixture 进入 `testdata/fixtures/`。
|
||||||
- 示例插件进入 conformance suite;构建失败视为插件 API 回归。
|
- 示例插件进入 conformance suite;构建失败视为插件 API 回归。
|
||||||
|
|
||||||
@@ -483,6 +488,6 @@ CI 产物应至少保存:
|
|||||||
- 新建 `protocol-proxy` 模板后,能跑通 Minecraft handshake/login smoke fixture。
|
- 新建 `protocol-proxy` 模板后,能跑通 Minecraft handshake/login smoke fixture。
|
||||||
- `upstream-rewrite` 和 `mc-auth-proxy` 示例插件使用标准 CLI 生成 binary/source `.mcgp`。
|
- `upstream-rewrite` 和 `mc-auth-proxy` 示例插件使用标准 CLI 生成 binary/source `.mcgp`。
|
||||||
- 生成的 `.mcgp` 能通过现有上传和服务端校验。
|
- 生成的 `.mcgp` 能通过现有上传和服务端校验。
|
||||||
- `manifest.json` 与 Go 代码不重复维护插件元数据。
|
- manifest source 与 Go 代码不重复维护插件元数据。
|
||||||
- Go plugin adapter 之外的 runtime 可以通过 adapter 注册进入同一套 `init/build/test` 命令。
|
- Go plugin adapter 之外的 runtime 可以通过 adapter 注册进入同一套 `init/build/test` 命令。
|
||||||
- CLI 失败输出能定位到字段、文件或 fixture,而不是只返回通用错误。
|
- CLI 失败输出能定位到字段、文件或 fixture,而不是只返回通用错误。
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
- 第一版继续使用 Go `-buildmode=plugin` 的进程内 `.so` 插件。
|
- 第一版继续使用 Go `-buildmode=plugin` 的进程内 `.so` 插件。
|
||||||
- 尽量支持热加载:兼容且未加载过的插件可以不重启加载并启用。
|
- 尽量支持热加载:兼容且未加载过的插件可以不重启加载并启用。
|
||||||
- 明确热卸载限制:Go plugin 不能真正从进程中卸载,只能逻辑禁用。
|
- 明确热卸载限制:Go plugin 不能真正从进程中卸载,只能逻辑禁用。
|
||||||
- 通过 `manifest.json` 记录插件 ID、版本、目标平台、构建 Go 版本、SDK/API 版本、声明的 extension point 和配置 schema。
|
- 通过 manifest source 记录插件 ID、版本、目标平台、构建 Go 版本、SDK/API 版本、声明的 extension point 和配置 schema;源码目录只维护一份 `manifest.yaml/yml/toml/jsonc/json`,`.mcgp` 包内统一物化为 canonical `manifest.json`。
|
||||||
- 插件能力采用 Extension Point 模型,Hook 是其中一种;第一版先落 `upstream.connect/v1`。
|
- 插件能力采用 Extension Point 模型,Hook 是其中一种;第一版先落 `upstream.connect/v1`。
|
||||||
- `upstream.connect/v1` 必须支持插件返回自管 `net.Conn`,用于实现完整 stream endpoint/protocol proxy。
|
- `upstream.connect/v1` 必须支持插件返回自管 `net.Conn`,用于实现完整 stream endpoint/protocol proxy。
|
||||||
- MC 正版/三方登录、身份映射、forwarding 和登录后的协议处理属于插件业务逻辑,不由 gateway core 拼装。
|
- MC 正版/三方登录、身份映射、forwarding 和登录后的协议处理属于插件业务逻辑,不由 gateway core 拼装。
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
| 决策/功能点 | 当前设计结论 | 阶段 | 主要章节 |
|
| 决策/功能点 | 当前设计结论 | 阶段 | 主要章节 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 插件信任模型 | 第一版只支持可信 native 插件,不把普通插件当不可信代码运行 | 第一版 | Runtime 与权限声明、安全与运维约束 |
|
| 插件信任模型 | 第一版只支持可信 native 插件,不把普通插件当不可信代码运行 | 第一版 | Runtime 与权限声明、安全与运维约束 |
|
||||||
| 插件包格式 | 管理页统一上传 `.mcgp` zip 包,源码/二进制由 `manifest.json` 的 `artifact_type` 决定 | 第一版 | 包格式、源码包构建 |
|
| 插件包格式 | 管理页统一上传 `.mcgp` zip 包,源码/二进制由包内 canonical `manifest.json` 的 `artifact_type` 决定 | 第一版 | 包格式、源码包构建 |
|
||||||
| 自定义扩展名 | 不新增源码包扩展名;同一 `.mcgp` 格式承载 binary/source | 第一版 | 包格式 |
|
| 自定义扩展名 | 不新增源码包扩展名;同一 `.mcgp` 格式承载 binary/source | 第一版 | 包格式 |
|
||||||
| Go plugin ABI | 仅使用 `.mcgp` 内的 `manifest.json` 表示元数据,并记录 Go/API/SDK/ABI fingerprint | 第一版 | Manifest 元数据、Go Plugin ABI Fingerprint |
|
| Go plugin ABI | 仅使用 `.mcgp` 内的 `manifest.json` 表示元数据,并记录 Go/API/SDK/ABI fingerprint | 第一版 | Manifest 元数据、Go Plugin ABI Fingerprint |
|
||||||
| Admin 管理 | 上传、构建、加载、启用、禁用、切换版本、删除、回滚和审计都进入 Admin/SQLite | 第一版 | 数据模型、生命周期、Admin API |
|
| Admin 管理 | 上传、构建、加载、启用、禁用、切换版本、删除、回滚和审计都进入 Admin/SQLite | 第一版 | 数据模型、生命周期、Admin API |
|
||||||
@@ -67,9 +67,9 @@
|
|||||||
| 管理页上传、构建、加载、启用/禁用、删除和切换版本 | 全部进入 Admin/SQLite 生命周期,删除已加载 native artifact 后提示重启彻底清理 | 数据模型、生命周期、Admin API、Admin 页面 |
|
| 管理页上传、构建、加载、启用/禁用、删除和切换版本 | 全部进入 Admin/SQLite 生命周期,删除已加载 native artifact 后提示重启彻底清理 | 数据模型、生命周期、Admin API、Admin 页面 |
|
||||||
| 热加载尽量支持,热卸载承认 Go plugin 限制 | 兼容且未加载过 artifact 可热加载;已加载 Go plugin 只能逻辑禁用,不能真正卸载 | 生命周期、热加载和热卸载、Runbook |
|
| 热加载尽量支持,热卸载承认 Go plugin 限制 | 兼容且未加载过 artifact 可热加载;已加载 Go plugin 只能逻辑禁用,不能真正卸载 | 生命周期、热加载和热卸载、Runbook |
|
||||||
| 多进程模型实现进程级热卸载 | 预留 `go-plugin-process` runtime,主进程只做管理和 fd 编排,子进程负责数据面;通过退出子进程回收 Go plugin | Go Plugin Process Runtime、第一版默认策略 |
|
| 多进程模型实现进程级热卸载 | 预留 `go-plugin-process` runtime,主进程只做管理和 fd 编排,子进程负责数据面;通过退出子进程回收 Go plugin | Go Plugin Process Runtime、第一版默认策略 |
|
||||||
| 插件包不新增源码扩展名 | 统一 `.mcgp` zip,源码/二进制由 `manifest.json.artifact_type` 声明 | 插件包格式 |
|
| 插件包不新增源码扩展名 | 统一 `.mcgp` zip,源码/二进制由包内 `manifest.json.artifact_type` 声明 | 插件包格式 |
|
||||||
| 支持源码包和构建环境设计 | source `.mcgp` 经受控 builder 生成 `plugin.so`;开发 local-process,生产推荐 container builder 或外部 CI | 源码包构建环境、供应链元数据 |
|
| 支持源码包和构建环境设计 | source `.mcgp` 经受控 builder 生成 `plugin.so`;开发 local-process,生产推荐 container builder 或外部 CI | 源码包构建环境、供应链元数据 |
|
||||||
| Manifest 元数据稳定并记录 Go 构建信息 | `manifest.json` 是唯一元数据来源,记录 Go/API/SDK/ABI fingerprint、builder 和 provenance | Manifest 元数据、Go Plugin ABI Fingerprint |
|
| Manifest 元数据稳定并记录 Go 构建信息 | 作者只维护一个 manifest source;包内 canonical `manifest.json` 是服务端唯一可信元数据来源,记录 Go/API/SDK/ABI fingerprint、builder 和 provenance | Manifest 元数据、Go Plugin ABI Fingerprint |
|
||||||
| Hook 之外的插件技术方案 | 统一 Extension Point 模型,覆盖 hook、middleware、provider、event subscriber、rule/policy;mock/mixin/monkey patch 不作为生产机制 | Extension Point 设计、Mock 和 Mixin 的定位 |
|
| Hook 之外的插件技术方案 | 统一 Extension Point 模型,覆盖 hook、middleware、provider、event subscriber、rule/policy;mock/mixin/monkey patch 不作为生产机制 | Extension Point 设计、Mock 和 Mixin 的定位 |
|
||||||
| 沙箱功能要有未来路线 | 第一版不提供沙箱;预留 sandbox-process、WASM、capability enforcement、stream relay 和 egress 策略 | Sandbox Runtime、Runtime Adapter、第一版默认策略 |
|
| 沙箱功能要有未来路线 | 第一版不提供沙箱;预留 sandbox-process、WASM、capability enforcement、stream relay 和 egress 策略 | Sandbox Runtime、Runtime Adapter、第一版默认策略 |
|
||||||
| Alibaba 非侵入 Go 注入的参考价值 | 作为官方/组织 build-time instrumentation 未来能力,不作为普通运行时插件或热加载机制 | Build-Time Instrumentation |
|
| Alibaba 非侵入 Go 注入的参考价值 | 作为官方/组织 build-time instrumentation 未来能力,不作为普通运行时插件或热加载机制 | Build-Time Instrumentation |
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
|
|
||||||
## 插件包格式
|
## 插件包格式
|
||||||
|
|
||||||
管理页上传的插件包统一使用 `.mcgp`,本质是 zip 包。包内内容由 `manifest.json` 决定,不通过扩展名区分源码包和二进制包。
|
管理页上传的插件包统一使用 `.mcgp`,本质是 zip 包。包内内容由 canonical `manifest.json` 决定,不通过扩展名区分源码包和二进制包。开发目录可以维护 `manifest.yaml`、`manifest.yml`、`manifest.toml`、`manifest.jsonc` 或 `manifest.json`,但构建进入 `.mcgp` 时必须统一物化为根目录 `manifest.json`。
|
||||||
|
|
||||||
包类型由两个字段表达:
|
包类型由两个字段表达:
|
||||||
|
|
||||||
@@ -313,7 +313,7 @@ upstream-rewrite.mcgp
|
|||||||
README.md # 可选
|
README.md # 可选
|
||||||
```
|
```
|
||||||
|
|
||||||
`manifest.json` 是加载前可读取的元数据,用于避免必须执行插件代码才能知道基础信息。上传阶段只解析 zip 和 manifest,不执行插件代码。
|
包内 `manifest.json` 是加载前可读取的可信元数据,用于避免必须执行插件代码才能知道基础信息。上传阶段只解析 zip 和 manifest,不执行插件代码。
|
||||||
|
|
||||||
二进制包 manifest 示例:
|
二进制包 manifest 示例:
|
||||||
|
|
||||||
@@ -459,7 +459,7 @@ upstream-rewrite.mcgp
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
二进制包上传后可以直接登记为 artifact。源码包上传后必须先进入 builder,构建出 `plugin.so` 后再登记为 artifact。加载阶段始终只加载最终产物 `plugin.so`;元数据以已校验入库的 `manifest.json` 为准。
|
二进制包上传后可以直接登记为 artifact。源码包上传后必须先进入 builder,构建出 `plugin.so` 后再登记为 artifact。加载阶段始终只加载最终产物 `plugin.so`;元数据以已校验入库的包内 `manifest.json` 为准。
|
||||||
|
|
||||||
开发环境可以允许直接上传 raw `.so`,但生产推荐只接受 `.mcgp`。直接上传 `.so` 时,加载前只能展示文件名、大小和 sha256;生产路径仍应使用 `.mcgp` 提供 `manifest.json`。
|
开发环境可以允许直接上传 raw `.so`,但生产推荐只接受 `.mcgp`。直接上传 `.so` 时,加载前只能展示文件名、大小和 sha256;生产路径仍应使用 `.mcgp` 提供 `manifest.json`。
|
||||||
|
|
||||||
@@ -2009,7 +2009,7 @@ index 规则:
|
|||||||
|
|
||||||
## Manifest 元数据
|
## Manifest 元数据
|
||||||
|
|
||||||
插件包的元数据只来自 `.mcgp` 根目录的 `manifest.json`。上传、准入、构建、兼容性检查和 Admin 展示都必须使用这份静态 manifest;gateway 不通过执行插件代码读取元数据。
|
插件包的元数据只来自 `.mcgp` 根目录的 canonical `manifest.json`。上传、准入、构建、兼容性检查和 Admin 展示都必须使用这份静态 manifest;gateway 不通过执行插件代码读取元数据。源码目录可以使用 YAML、TOML、JSONC 或 JSON 作为唯一 manifest source,但进入 `.mcgp` 前必须规范化为 `manifest.json`。
|
||||||
|
|
||||||
Go plugin 只需要导出一个 factory 符号:
|
Go plugin 只需要导出一个 factory 符号:
|
||||||
|
|
||||||
@@ -6370,10 +6370,10 @@ type Gateway interface {
|
|||||||
- `plugin/api` 稳定 API 文档。
|
- `plugin/api` 稳定 API 文档。
|
||||||
- `examples/plugins/upstream-rewrite` 最小模板。
|
- `examples/plugins/upstream-rewrite` 最小模板。
|
||||||
- `examples/plugins/mc-auth-proxy` protocol-proxy 模板。
|
- `examples/plugins/mc-auth-proxy` protocol-proxy 模板。
|
||||||
- manifest JSON schema。
|
- manifest source 多格式解析和 canonical JSON schema。
|
||||||
- 统一的 `gateway plugin init/build/test` 开发工具链。
|
- 统一的 `gateway plugin init/build/test` 开发工具链。
|
||||||
|
|
||||||
详细工具链设计见 [plugin-development-toolchain-design.md](plugin-development-toolchain-design.md)。工具链必须继续遵守 manifest-only 元数据约束:插件作者只维护 `manifest.json`,Go 代码中不再保存 `manifestJSON` 或等价重复元数据。
|
详细工具链设计见 [plugin-development-toolchain-design.md](plugin-development-toolchain-design.md)。工具链必须继续遵守 manifest-only 元数据约束:插件作者只维护一个 manifest source 文件,Go 代码中不再保存 `manifestJSON` 或等价重复元数据;`.mcgp` 包内仍以 canonical `manifest.json` 作为服务端可信边界。
|
||||||
|
|
||||||
### CLI 工具
|
### CLI 工具
|
||||||
|
|
||||||
@@ -6427,7 +6427,7 @@ CLI 规则:
|
|||||||
本地开发流程:
|
本地开发流程:
|
||||||
|
|
||||||
1. 从示例复制插件目录。
|
1. 从示例复制插件目录。
|
||||||
2. 编写 `manifest.json`。
|
2. 编写唯一 manifest source,默认是 `manifest.yaml`。
|
||||||
3. 使用与 gateway 匹配的 Go toolchain。
|
3. 使用与 gateway 匹配的 Go toolchain。
|
||||||
4. 运行 `gateway plugin build` 构建 `.mcgp`。
|
4. 运行 `gateway plugin build` 构建 `.mcgp`。
|
||||||
5. 通过 Admin 上传。
|
5. 通过 Admin 上传。
|
||||||
@@ -7354,7 +7354,7 @@ examples/plugins/mc-status-motd/
|
|||||||
- 定义 `.mcgp` 静态校验规则、大小限制和 zip slip 防护。
|
- 定义 `.mcgp` 静态校验规则、大小限制和 zip slip 防护。
|
||||||
- 定义插件 ID、handler ID、task ID、secret name 和 extension point 命名规范。
|
- 定义插件 ID、handler ID、task ID、secret name 和 extension point 命名规范。
|
||||||
- 支持 `artifact_type=binary/source` 和 `runtime.type=go-plugin`。
|
- 支持 `artifact_type=binary/source` 和 `runtime.type=go-plugin`。
|
||||||
- 确认插件只需要导出 `Plugin` factory,元数据只来自 `manifest.json`。
|
- 确认插件只需要导出 `Plugin` factory,包内元数据只来自 canonical `manifest.json`。
|
||||||
- 定义 Go plugin ABI fingerprint schema、计算规则、compat diff 和开发模式 override 语义。
|
- 定义 Go plugin ABI fingerprint schema、计算规则、compat diff 和开发模式 override 语义。
|
||||||
- 在 `plugin/api` 中补齐 `APIVersion`、extension point metadata、`ErrPass` 等。
|
- 在 `plugin/api` 中补齐 `APIVersion`、extension point metadata、`ErrPass` 等。
|
||||||
- 把 upstream hook 收敛为 request struct。
|
- 把 upstream hook 收敛为 request struct。
|
||||||
|
|||||||
@@ -8,3 +8,5 @@ This example demonstrates phase 7 extension points:
|
|||||||
- `admin.auth.provider/v1` registers an unavailable external provider while preserving local admin fallback.
|
- `admin.auth.provider/v1` registers an unavailable external provider while preserving local admin fallback.
|
||||||
|
|
||||||
It is intended as a conformance fixture and source example for plugin authors.
|
It is intended as a conformance fixture and source example for plugin authors.
|
||||||
|
The source manifest is maintained as `manifest.yaml`; packaged `.mcgp` artifacts
|
||||||
|
still contain canonical `manifest.json`.
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "mc-gateway.plugin/v1",
|
|
||||||
"id": "extension-ecosystem-example",
|
|
||||||
"name": "Extension Ecosystem Example",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Example fixture for route, status, subscriber and provider extension points.",
|
|
||||||
"artifact_type": "binary",
|
|
||||||
"runtime": {
|
|
||||||
"type": "go-plugin",
|
|
||||||
"entry": "plugin.so",
|
|
||||||
"entry_symbol": "Plugin"
|
|
||||||
},
|
|
||||||
"api_version": "plugin-api/v1",
|
|
||||||
"sdk_module": "github.com/tursom/mc-gateway/plugin/api",
|
|
||||||
"sdk_module_version": "v0.1.0",
|
|
||||||
"go_version": "go1.24.0",
|
|
||||||
"go_os": "linux",
|
|
||||||
"go_arch": "amd64",
|
|
||||||
"extension_points": [
|
|
||||||
{ "type": "provider", "key": "route.resolve/v1" },
|
|
||||||
{ "type": "hook", "key": "status.ping/v1" },
|
|
||||||
{ "type": "event", "key": "event.subscriber/v1" },
|
|
||||||
{ "type": "provider", "key": "admin.auth.provider/v1" }
|
|
||||||
],
|
|
||||||
"capabilities": {
|
|
||||||
"extension_points": ["route.resolve/v1", "status.ping/v1", "event.subscriber/v1", "admin.auth.provider/v1"],
|
|
||||||
"route": { "cache_ttl_ms": 60000 },
|
|
||||||
"status": { "hosts": ["blue.example", "red.example"] },
|
|
||||||
"event_subscriber": { "mode": "at_least_once", "max_retry": 3 },
|
|
||||||
"providers": [{ "type": "admin.auth.provider/v1", "name": "external-identity", "fallback": true }]
|
|
||||||
},
|
|
||||||
"runtime_limits": { "handler_timeout_ms": 1000 },
|
|
||||||
"config_schema": { "type": "object" }
|
|
||||||
}
|
|
||||||
49
examples/plugins/extension-ecosystem/manifest.yaml
Normal file
49
examples/plugins/extension-ecosystem/manifest.yaml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Human-maintained plugin manifest. Build packages normalize this into manifest.json.
|
||||||
|
schema_version: mc-gateway.plugin/v1
|
||||||
|
id: extension-ecosystem-example
|
||||||
|
name: Extension Ecosystem Example
|
||||||
|
version: 0.1.0
|
||||||
|
description: Example fixture for route, status, subscriber and provider extension points.
|
||||||
|
artifact_type: binary
|
||||||
|
runtime:
|
||||||
|
type: go-plugin
|
||||||
|
entry: plugin.so
|
||||||
|
entry_symbol: Plugin
|
||||||
|
api_version: plugin-api/v1
|
||||||
|
sdk_module: github.com/tursom/mc-gateway/plugin/api
|
||||||
|
sdk_module_version: v0.1.0
|
||||||
|
go_version: go1.24.0
|
||||||
|
go_os: linux
|
||||||
|
go_arch: amd64
|
||||||
|
extension_points:
|
||||||
|
- type: provider
|
||||||
|
key: route.resolve/v1
|
||||||
|
- type: hook
|
||||||
|
key: status.ping/v1
|
||||||
|
- type: event
|
||||||
|
key: event.subscriber/v1
|
||||||
|
- type: provider
|
||||||
|
key: admin.auth.provider/v1
|
||||||
|
capabilities:
|
||||||
|
extension_points:
|
||||||
|
- route.resolve/v1
|
||||||
|
- status.ping/v1
|
||||||
|
- event.subscriber/v1
|
||||||
|
- admin.auth.provider/v1
|
||||||
|
route:
|
||||||
|
cache_ttl_ms: 60000
|
||||||
|
status:
|
||||||
|
hosts:
|
||||||
|
- blue.example
|
||||||
|
- red.example
|
||||||
|
event_subscriber:
|
||||||
|
mode: at_least_once
|
||||||
|
max_retry: 3
|
||||||
|
providers:
|
||||||
|
- type: admin.auth.provider/v1
|
||||||
|
name: external-identity
|
||||||
|
fallback: true
|
||||||
|
runtime_limits:
|
||||||
|
handler_timeout_ms: 1000
|
||||||
|
config_schema:
|
||||||
|
type: object
|
||||||
@@ -18,6 +18,8 @@ Build and package:
|
|||||||
|
|
||||||
The binary package is written to `dist/mc-auth-proxy.mcgp`; the source package
|
The binary package is written to `dist/mc-auth-proxy.mcgp`; the source package
|
||||||
is written to `dist/mc-auth-proxy-source.mcgp`.
|
is written to `dist/mc-auth-proxy-source.mcgp`.
|
||||||
|
The source manifest is maintained as `manifest.yaml`; packaged `.mcgp` artifacts
|
||||||
|
still contain canonical `manifest.json`.
|
||||||
|
|
||||||
Build the source package through the gateway builder:
|
Build the source package through the gateway builder:
|
||||||
|
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "mc-gateway.plugin/v1",
|
|
||||||
"id": "mc-auth-proxy",
|
|
||||||
"name": "Minecraft Auth Proxy",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Protocol-proxy example that owns Minecraft login handling and returns a stable fixture disconnect.",
|
|
||||||
"artifact_type": "binary",
|
|
||||||
"runtime": {
|
|
||||||
"type": "go-plugin",
|
|
||||||
"entry": "plugin.so",
|
|
||||||
"entry_symbol": "Plugin"
|
|
||||||
},
|
|
||||||
"api_version": "plugin-api/v1",
|
|
||||||
"sdk_module": "github.com/tursom/mc-gateway/plugin/api",
|
|
||||||
"sdk_module_version": "v0.1.0",
|
|
||||||
"go_version": "go1.24.0",
|
|
||||||
"go_os": "linux",
|
|
||||||
"go_arch": "amd64",
|
|
||||||
"extension_points": [
|
|
||||||
{ "type": "hook", "key": "upstream.connect/v1" }
|
|
||||||
],
|
|
||||||
"capabilities": {
|
|
||||||
"upstream_connect": { "mode": "protocol-proxy" },
|
|
||||||
"minecraft": {
|
|
||||||
"protocol_versions": {
|
|
||||||
"min": 47,
|
|
||||||
"max": 767,
|
|
||||||
"tested": [47, 760, 763, 767],
|
|
||||||
"unsupported_policy": "kick"
|
|
||||||
},
|
|
||||||
"states": {
|
|
||||||
"status": "transparent",
|
|
||||||
"login": "handled",
|
|
||||||
"configuration": "transparent",
|
|
||||||
"play": "transparent"
|
|
||||||
},
|
|
||||||
"auth_modes": ["fixture"],
|
|
||||||
"forwarding": {
|
|
||||||
"supported": ["none", "velocity-modern"],
|
|
||||||
"default": "none",
|
|
||||||
"requires_secret": false
|
|
||||||
},
|
|
||||||
"unsupported_policy": "kick",
|
|
||||||
"modded": {
|
|
||||||
"forge": "transparent",
|
|
||||||
"fabric": "transparent",
|
|
||||||
"fml": "unsupported",
|
|
||||||
"unknown": "pass"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtime_limits": {
|
|
||||||
"handler_timeout_ms": 3000,
|
|
||||||
"initial_write_timeout_ms": 1000
|
|
||||||
},
|
|
||||||
"events": [
|
|
||||||
{ "name": "auth.success", "fields": ["result", "mode"] },
|
|
||||||
{ "name": "auth.failure", "fields": ["result", "mode"] }
|
|
||||||
],
|
|
||||||
"custom_metrics": [
|
|
||||||
{ "name": "auth.attempts", "type": "counter", "labels": ["result", "mode"] }
|
|
||||||
],
|
|
||||||
"external_dependencies": [
|
|
||||||
{
|
|
||||||
"name": "backend",
|
|
||||||
"endpoint": "tcp://",
|
|
||||||
"purpose": "auth",
|
|
||||||
"required": true,
|
|
||||||
"timeout": "3s",
|
|
||||||
"retry": 0,
|
|
||||||
"fail_policy": "fail_closed",
|
|
||||||
"data_classes": ["operational"]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"background_tasks": [
|
|
||||||
{ "id": "profile-cache-gc", "name": "Profile cache GC", "mode": "manual", "manual": true, "timeout": "1s" }
|
|
||||||
],
|
|
||||||
"data_stores": [
|
|
||||||
{ "name": "profile-cache", "schema_version": 1, "data_class": "profile_cache", "quota_bytes": 1048576, "retention": "24h", "exportable": false }
|
|
||||||
],
|
|
||||||
"file_stores": [
|
|
||||||
{ "namespace": "cache", "data_class": "profile_cache", "quota_bytes": 1048576, "retention": "24h" },
|
|
||||||
{ "namespace": "diagnostic", "data_class": "diagnostic", "quota_bytes": 1048576, "retention": "24h" }
|
|
||||||
],
|
|
||||||
"config_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"match_host": { "type": "string" },
|
|
||||||
"fixture_accept": { "type": "boolean" },
|
|
||||||
"disconnect_message": { "type": "string" },
|
|
||||||
"backend": { "type": "string" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
113
examples/plugins/mc-auth-proxy/manifest.yaml
Normal file
113
examples/plugins/mc-auth-proxy/manifest.yaml
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Human-maintained plugin manifest. Build packages normalize this into manifest.json.
|
||||||
|
schema_version: mc-gateway.plugin/v1
|
||||||
|
id: mc-auth-proxy
|
||||||
|
name: Minecraft Auth Proxy
|
||||||
|
version: 0.1.0
|
||||||
|
description: Protocol-proxy example that owns Minecraft login handling and returns a stable fixture disconnect.
|
||||||
|
artifact_type: binary
|
||||||
|
runtime:
|
||||||
|
type: go-plugin
|
||||||
|
entry: plugin.so
|
||||||
|
entry_symbol: Plugin
|
||||||
|
api_version: plugin-api/v1
|
||||||
|
sdk_module: github.com/tursom/mc-gateway/plugin/api
|
||||||
|
sdk_module_version: v0.1.0
|
||||||
|
go_version: go1.24.0
|
||||||
|
go_os: linux
|
||||||
|
go_arch: amd64
|
||||||
|
extension_points:
|
||||||
|
- type: hook
|
||||||
|
key: upstream.connect/v1
|
||||||
|
capabilities:
|
||||||
|
upstream_connect:
|
||||||
|
mode: protocol-proxy
|
||||||
|
minecraft:
|
||||||
|
protocol_versions:
|
||||||
|
min: 47
|
||||||
|
max: 767
|
||||||
|
tested:
|
||||||
|
- 47
|
||||||
|
- 760
|
||||||
|
- 763
|
||||||
|
- 767
|
||||||
|
unsupported_policy: kick
|
||||||
|
states:
|
||||||
|
status: transparent
|
||||||
|
login: handled
|
||||||
|
configuration: transparent
|
||||||
|
play: transparent
|
||||||
|
auth_modes:
|
||||||
|
- fixture
|
||||||
|
forwarding:
|
||||||
|
supported:
|
||||||
|
- none
|
||||||
|
- velocity-modern
|
||||||
|
default: none
|
||||||
|
requires_secret: false
|
||||||
|
unsupported_policy: kick
|
||||||
|
modded:
|
||||||
|
forge: transparent
|
||||||
|
fabric: transparent
|
||||||
|
fml: unsupported
|
||||||
|
unknown: pass
|
||||||
|
runtime_limits:
|
||||||
|
handler_timeout_ms: 3000
|
||||||
|
initial_write_timeout_ms: 1000
|
||||||
|
events:
|
||||||
|
- name: auth.success
|
||||||
|
fields:
|
||||||
|
- result
|
||||||
|
- mode
|
||||||
|
- name: auth.failure
|
||||||
|
fields:
|
||||||
|
- result
|
||||||
|
- mode
|
||||||
|
custom_metrics:
|
||||||
|
- name: auth.attempts
|
||||||
|
type: counter
|
||||||
|
labels:
|
||||||
|
- result
|
||||||
|
- mode
|
||||||
|
external_dependencies:
|
||||||
|
- name: backend
|
||||||
|
endpoint: tcp://
|
||||||
|
purpose: auth
|
||||||
|
required: true
|
||||||
|
timeout: 3s
|
||||||
|
retry: 0
|
||||||
|
fail_policy: fail_closed
|
||||||
|
data_classes:
|
||||||
|
- operational
|
||||||
|
background_tasks:
|
||||||
|
- id: profile-cache-gc
|
||||||
|
name: Profile cache GC
|
||||||
|
mode: manual
|
||||||
|
manual: true
|
||||||
|
timeout: 1s
|
||||||
|
data_stores:
|
||||||
|
- name: profile-cache
|
||||||
|
schema_version: 1
|
||||||
|
data_class: profile_cache
|
||||||
|
quota_bytes: 1048576
|
||||||
|
retention: 24h
|
||||||
|
exportable: false
|
||||||
|
file_stores:
|
||||||
|
- namespace: cache
|
||||||
|
data_class: profile_cache
|
||||||
|
quota_bytes: 1048576
|
||||||
|
retention: 24h
|
||||||
|
- namespace: diagnostic
|
||||||
|
data_class: diagnostic
|
||||||
|
quota_bytes: 1048576
|
||||||
|
retention: 24h
|
||||||
|
config_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
match_host:
|
||||||
|
type: string
|
||||||
|
fixture_accept:
|
||||||
|
type: boolean
|
||||||
|
disconnect_message:
|
||||||
|
type: string
|
||||||
|
backend:
|
||||||
|
type: string
|
||||||
@@ -14,6 +14,8 @@ Build and package:
|
|||||||
|
|
||||||
The binary package is written to `dist/upstream-rewrite.mcgp`; the source
|
The binary package is written to `dist/upstream-rewrite.mcgp`; the source
|
||||||
package is written to `dist/upstream-rewrite-source.mcgp`.
|
package is written to `dist/upstream-rewrite-source.mcgp`.
|
||||||
|
The source manifest is maintained as `manifest.yaml`; packaged `.mcgp` artifacts
|
||||||
|
still contain canonical `manifest.json`.
|
||||||
|
|
||||||
Build the source package through the gateway builder:
|
Build the source package through the gateway builder:
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "mc-gateway.plugin/v1",
|
|
||||||
"id": "upstream-rewrite",
|
|
||||||
"name": "Upstream Rewrite",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "Rewrite selected upstream targets before dialing.",
|
|
||||||
"artifact_type": "binary",
|
|
||||||
"runtime": {
|
|
||||||
"type": "go-plugin",
|
|
||||||
"entry": "plugin.so",
|
|
||||||
"entry_symbol": "Plugin"
|
|
||||||
},
|
|
||||||
"api_version": "plugin-api/v1",
|
|
||||||
"sdk_module": "github.com/tursom/mc-gateway/plugin/api",
|
|
||||||
"sdk_module_version": "v0.1.0",
|
|
||||||
"go_version": "go1.24.4",
|
|
||||||
"go_os": "linux",
|
|
||||||
"go_arch": "amd64",
|
|
||||||
"extension_points": [
|
|
||||||
{ "type": "hook", "key": "upstream.connect/v1" }
|
|
||||||
],
|
|
||||||
"capabilities": {
|
|
||||||
"extension_points": ["upstream.connect/v1"],
|
|
||||||
"network": { "outbound": ["tcp:*:*"] },
|
|
||||||
"filesystem": { "read": [], "write": [] },
|
|
||||||
"env": []
|
|
||||||
},
|
|
||||||
"runtime_limits": {
|
|
||||||
"handler_timeout_ms": 3000
|
|
||||||
},
|
|
||||||
"config_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"match_host": { "type": "string" },
|
|
||||||
"upstream": { "type": "string" }
|
|
||||||
},
|
|
||||||
"required": ["upstream"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
examples/plugins/upstream-rewrite/manifest.yaml
Normal file
41
examples/plugins/upstream-rewrite/manifest.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Human-maintained plugin manifest. Build packages normalize this into manifest.json.
|
||||||
|
schema_version: mc-gateway.plugin/v1
|
||||||
|
id: upstream-rewrite
|
||||||
|
name: Upstream Rewrite
|
||||||
|
version: 0.1.0
|
||||||
|
description: Rewrite selected upstream targets before dialing.
|
||||||
|
artifact_type: binary
|
||||||
|
runtime:
|
||||||
|
type: go-plugin
|
||||||
|
entry: plugin.so
|
||||||
|
entry_symbol: Plugin
|
||||||
|
api_version: plugin-api/v1
|
||||||
|
sdk_module: github.com/tursom/mc-gateway/plugin/api
|
||||||
|
sdk_module_version: v0.1.0
|
||||||
|
go_version: go1.24.4
|
||||||
|
go_os: linux
|
||||||
|
go_arch: amd64
|
||||||
|
extension_points:
|
||||||
|
- type: hook
|
||||||
|
key: upstream.connect/v1
|
||||||
|
capabilities:
|
||||||
|
extension_points:
|
||||||
|
- upstream.connect/v1
|
||||||
|
network:
|
||||||
|
outbound:
|
||||||
|
- tcp:*:*
|
||||||
|
filesystem:
|
||||||
|
read: []
|
||||||
|
write: []
|
||||||
|
env: []
|
||||||
|
runtime_limits:
|
||||||
|
handler_timeout_ms: 3000
|
||||||
|
config_schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
match_host:
|
||||||
|
type: string
|
||||||
|
upstream:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- upstream
|
||||||
2
go.mod
2
go.mod
@@ -7,11 +7,13 @@ toolchain go1.24.4
|
|||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
|
github.com/pelletier/go-toml/v2 v2.4.2
|
||||||
github.com/pires/go-proxyproto v0.8.1
|
github.com/pires/go-proxyproto v0.8.1
|
||||||
github.com/quic-go/quic-go v0.52.0
|
github.com/quic-go/quic-go v0.52.0
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/xtaci/kcp-go v5.4.20+incompatible
|
github.com/xtaci/kcp-go v5.4.20+incompatible
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.43.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.45.0
|
modernc.org/sqlite v1.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
3
go.sum
3
go.sum
@@ -62,6 +62,8 @@ github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
|||||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -157,6 +159,7 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
|
|||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
Reference in New Issue
Block a user