diff --git a/cmd/gateway/plugin_cli.go b/cmd/gateway/plugin_cli.go index 98e040b..0223c14 100644 --- a/cmd/gateway/plugin_cli.go +++ b/cmd/gateway/plugin_cli.go @@ -206,11 +206,7 @@ func runPluginCLI(args []string) (bool, int) { } return true, 0 case "validate", "compat": - if len(args) < 3 { - printPluginCLIUsage() - return true, 2 - } - artifact, err := validatePluginPathForCLI(args[2], "") + artifact, err := runPluginValidatePathCLI(args[2:], "") if err != nil { fmt.Fprintln(os.Stderr, err) 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) return true, 0 case "source-validate": - if len(args) < 3 { - printPluginCLIUsage() - return true, 2 - } - source, err := validatePluginPathForCLI(args[2], pluginmanager.ArtifactTypeSource) + source, err := runPluginValidatePathCLI(args[2:], pluginmanager.ArtifactTypeSource) if err != nil { fmt.Fprintln(os.Stderr, err) return true, 1 diff --git a/cmd/gateway/plugin_cli_manifest.go b/cmd/gateway/plugin_cli_manifest.go new file mode 100644 index 0000000..7eb635b --- /dev/null +++ b/cmd/gateway/plugin_cli_manifest.go @@ -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' +} diff --git a/cmd/gateway/plugin_cli_toolchain.go b/cmd/gateway/plugin_cli_toolchain.go index bffaa02..cfc6466 100644 --- a/cmd/gateway/plugin_cli_toolchain.go +++ b/cmd/gateway/plugin_cli_toolchain.go @@ -30,6 +30,7 @@ type pluginBuildCLIOptions struct { BuildType string Out string FromSource string + Manifest string SkipTests bool Vendor bool } @@ -42,10 +43,12 @@ type pluginInitCLIOptions struct { Runtime string Module string Extension string + Format string } type pluginGovernanceCLIOptions struct { Target string + Manifest string ConfigPath string ConfigJSON string Profile string @@ -77,10 +80,11 @@ type pluginBuildRuntimeRequest struct { type pluginTestCLIOptions struct { Target string + Manifest string Profile string ConfigPath string FixturePath string - Manifest pluginmanager.Manifest + Source pluginmanager.Manifest } type cliStaticRuntimeAdapter struct{} @@ -119,7 +123,7 @@ func (goPluginCLIAdapter) Test(ctx context.Context, opts pluginTestCLIOptions) e return err } case "manifest": - if _, err := validatePluginDirectoryForCLI(opts.Target); err != nil { + if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil { return err } 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 { return err } - if _, err := validatePluginDirectoryForCLI(opts.Target); err != nil { + if _, err := validatePluginDirectoryForCLI(opts.Target, opts.Manifest); err != nil { return err } default: @@ -281,12 +285,15 @@ func runPluginManifestCLI(args []string) error { } func runPluginManifestFormatCLI(args []string) error { - target := "manifest.json" + target := "." + manifestPath := "" + artifactType := "" write := false + canonicalJSON := false for i := 0; i < len(args); i++ { arg := args[i] if !strings.HasPrefix(arg, "--") { - if target != "manifest.json" { + if target != "." { return fmt.Errorf("unexpected argument %q", arg) } target = arg @@ -300,39 +307,68 @@ func runPluginManifestFormatCLI(args []string) error { switch key { case "write": write = parsePluginBoolFlag(value) + case "canonical-json": + canonicalJSON = parsePluginBoolFlag(value) + case "manifest": + manifestPath = value + case "type": + artifactType = value default: return fmt.Errorf("unknown manifest format flag --%s", key) } } - 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 { 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 canonicalJSON { + if artifactType == "" { + artifactType = source.Manifest.ArtifactType } - if err := os.WriteFile(manifestPath, formatted, 0644); err != nil { + if artifactType == "" { + artifactType = pluginmanager.ArtifactTypeBinary + } + switch artifactType { + case pluginmanager.ArtifactTypeBinary, pluginmanager.ArtifactTypeSource: + default: + return fmt.Errorf("unsupported --type %q", artifactType) + } + canonical, err := materializedManifestJSON(source.Raw, source.Manifest, artifactType, false) + if err != nil { return err } - fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", manifestPath) + _, err = os.Stdout.Write(canonical) + return err + } + if artifactType != "" { + return errors.New("--type is only valid with --canonical-json") + } + if write { + if source.Format != manifestFormatJSON { + fmt.Fprintf(os.Stdout, "ok manifest=%s validated comments_preserved=true\n", source.Path) + return nil + } + if bytes.Equal(source.Data, source.CanonicalJSON) { + fmt.Fprintf(os.Stdout, "ok manifest=%s unchanged\n", source.Path) + return nil + } + if err := os.WriteFile(source.Path, source.CanonicalJSON, 0644); err != nil { + return err + } + fmt.Fprintf(os.Stdout, "ok manifest=%s formatted\n", source.Path) return nil } - _, 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 } @@ -517,6 +553,7 @@ func runPluginInitCLI(args []string) error { Template: "upstream-dialer", Runtime: pluginmanager.RuntimeGoPlugin, Extension: pluginmanager.ExtensionUpstreamConnect, + Format: manifestFormatYAML, } for i := 0; i < len(args); i++ { arg := args[i] @@ -545,6 +582,8 @@ func runPluginInitCLI(args []string) error { opts.Module = value case "extension": opts.Extension = value + case "manifest-format": + opts.Format = value default: return fmt.Errorf("unknown init flag --%s", key) } @@ -561,6 +600,9 @@ func runPluginInitCLI(args []string) error { if opts.Module == "" { opts.Module = "example.com/" + opts.ID } + if err := validateManifestTemplateFormat(opts.Format); err != nil { + return err + } adapter, err := pluginCLIAdapterForRuntime(opts.Runtime) if err != nil { return err @@ -591,6 +633,8 @@ func runPluginBuildCLI(args []string) error { opts.Out = value case "from-source": opts.FromSource = value + case "manifest": + opts.Manifest = value case "skip-tests": opts.SkipTests = parsePluginBoolFlag(value) case "vendor": @@ -613,12 +657,7 @@ func runPluginBuildCLI(args []string) error { 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) + manifest, raw, err := readPluginDirManifest(opts.Dir, opts.Manifest) if err != nil { return err } @@ -626,13 +665,20 @@ func runPluginBuildCLI(args []string) error { if err != nil { return err } + if !opts.SkipTests { + if err := runGoCommand(context.Background(), opts.Dir, "go", "test", "./..."); err != nil { + return err + } + } outDir := opts.Out - if 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 == "" { 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 @@ -678,6 +724,7 @@ func runPluginTestCLI(args []string) error { profile := "unit,manifest" configPath := "" fixturePath := "" + manifestPath := "" for i := 0; i < len(args); i++ { arg := args[i] if !strings.HasPrefix(arg, "--") { @@ -699,6 +746,8 @@ func runPluginTestCLI(args []string) error { configPath = value case "fixture": fixturePath = value + case "manifest": + manifestPath = value default: 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) return nil } - manifest, _, err := readPluginDirManifest(target) + manifest, _, err := readPluginDirManifest(target, manifestPath) if err != nil { return err } @@ -735,10 +784,11 @@ func runPluginTestCLI(args []string) error { } if err := adapter.Test(context.Background(), pluginTestCLIOptions{ Target: target, + Manifest: manifestPath, Profile: profile, ConfigPath: configPath, FixturePath: fixturePath, - Manifest: manifest, + Source: manifest, }); err != nil { return err } @@ -767,6 +817,8 @@ func parseGovernanceCLIOptions(args []string) (pluginGovernanceCLIOptions, error } i += consumed switch key { + case "manifest": + opts.Manifest = value case "config": opts.ConfigPath = value case "config-json": @@ -836,7 +888,7 @@ func prepareLocalGovernanceManager(opts pluginGovernanceCLIOptions) (*pluginmana return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err } if info.IsDir() { - manifest, raw, err := readPluginDirManifest(targetPath) + manifest, raw, err := readPluginDirManifest(targetPath, opts.Manifest) if err != nil { cleanup() return nil, func() {}, pluginmanager.ArtifactRecord{}, "", err @@ -925,13 +977,50 @@ func encodePluginCLIJSON(value any) error { return encoder.Encode(value) } +func runPluginValidatePathCLI(args []string, expectedArtifactType string) (pluginmanager.ArtifactRecord, error) { + if len(args) == 0 { + return pluginmanager.ArtifactRecord{}, errors.New("plugin validate requires a plugin directory, manifest source, or .mcgp artifact") + } + target := "" + manifestPath := "" + for i := 0; i < len(args); i++ { + arg := args[i] + if !strings.HasPrefix(arg, "--") { + if target != "" { + return pluginmanager.ArtifactRecord{}, fmt.Errorf("unexpected argument %q", arg) + } + target = arg + continue + } + key, value, consumed, err := parsePluginCLIFlag(args, i) + if err != nil { + return pluginmanager.ArtifactRecord{}, err + } + i += consumed + switch key { + case "manifest": + manifestPath = value + default: + return pluginmanager.ArtifactRecord{}, fmt.Errorf("unknown validate flag --%s", key) + } + } + if target == "" { + return pluginmanager.ArtifactRecord{}, errors.New("plugin validate requires a plugin directory, manifest source, or .mcgp artifact") + } + return validatePluginPathForCLIWithManifest(target, expectedArtifactType, manifestPath) +} + func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginmanager.ArtifactRecord, error) { + return validatePluginPathForCLIWithManifest(targetPath, expectedArtifactType, "") +} + +func validatePluginPathForCLIWithManifest(targetPath, expectedArtifactType, manifestPath string) (pluginmanager.ArtifactRecord, error) { info, err := os.Stat(targetPath) if err != nil { return pluginmanager.ArtifactRecord{}, err } if info.IsDir() { - artifact, err := validatePluginDirectoryForCLI(targetPath) + artifact, err := validatePluginDirectoryForCLI(targetPath, manifestPath) if err != nil { return pluginmanager.ArtifactRecord{}, err } @@ -940,9 +1029,9 @@ func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginma } return artifact, nil } - if filepath.Base(targetPath) == "manifest.json" { + if isManifestSourceFile(targetPath) { dir := filepath.Dir(targetPath) - artifact, err := validatePluginDirectoryForCLI(dir) + artifact, err := validatePluginDirectoryForCLI(dir, targetPath) if err != nil { return pluginmanager.ArtifactRecord{}, err } @@ -967,8 +1056,8 @@ func validatePluginPathForCLI(targetPath, expectedArtifactType string) (pluginma return store.ValidateAndStore(upload) } -func validatePluginDirectoryForCLI(dir string) (pluginmanager.ArtifactRecord, error) { - manifest, raw, err := readPluginDirManifest(dir) +func validatePluginDirectoryForCLI(dir, manifestPath string) (pluginmanager.ArtifactRecord, error) { + manifest, raw, err := readPluginDirManifest(dir, manifestPath) if err != nil { return pluginmanager.ArtifactRecord{}, err } @@ -1041,12 +1130,12 @@ func writeGoPluginTemplate(ctx context.Context, opts pluginInitCLIOptions) error 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), + "go.mod": goModTemplate(opts.Module, repoRoot), + "main.go": goPluginMainTemplate(opts), + "main_test.go": goPluginTestTemplate(), + "README.md": readmeTemplate(opts), + manifestFileNameForFormat(opts.Format): manifestTemplate(opts), + "testdata/config.json": configTemplate(opts.Template), } for name, content := range files { target := filepath.Join(opts.Dir, filepath.FromSlash(name)) @@ -1061,39 +1150,6 @@ func writeGoPluginTemplate(ctx context.Context, opts pluginInitCLIOptions) error 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 { @@ -1267,7 +1323,7 @@ func collectSourcePackageFiles(dir, vendorDir string) ([]sourcePackageFile, erro } return nil } - if rel == "manifest.json" { + if isManifestSourceFile(rel) { return nil } if sourcePackageEntryAllowed(rel) { @@ -1421,7 +1477,7 @@ func parsePluginCLIFlag(args []string, index int) (string, string, int, error) { func isPluginBoolFlag(name string) bool { 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 default: return false @@ -1518,12 +1574,53 @@ replace github.com/tursom/mc-gateway => %s `, module, filepath.ToSlash(repoRoot)) } +func validateManifestTemplateFormat(format string) error { + switch format { + case manifestFormatYAML, "yml", manifestFormatTOML, manifestFormatJSONC, manifestFormatJSON: + return nil + default: + return fmt.Errorf("unsupported --manifest-format %q", format) + } +} + +func manifestFileNameForFormat(format string) string { + switch format { + case manifestFormatJSON: + return "manifest.json" + case manifestFormatJSONC: + return "manifest.jsonc" + case manifestFormatTOML: + return "manifest.toml" + case "yml": + return "manifest.yml" + default: + return "manifest.yaml" + } +} + func manifestTemplate(opts pluginInitCLIOptions) string { + switch opts.Format { + case manifestFormatJSON: + return manifestSourceJSONTemplate(opts, false) + case manifestFormatJSONC: + return manifestSourceJSONTemplate(opts, true) + case manifestFormatTOML: + return manifestTOMLTemplate(opts) + default: + return manifestYAMLTemplate(opts) + } +} + +func manifestSourceJSONTemplate(opts pluginInitCLIOptions, jsonc bool) string { mode := pluginmanager.UpstreamModeDialer if opts.Template == "protocol-proxy" { mode = pluginmanager.UpstreamModeProtocolProxy } - 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", "id": %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 } +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 { if opts.Template == "protocol-proxy" { return protocolProxyMainTemplate() diff --git a/cmd/gateway/plugin_cli_toolchain_test.go b/cmd/gateway/plugin_cli_toolchain_test.go index 371d0f1..2b7aa6d 100644 --- a/cmd/gateway/plugin_cli_toolchain_test.go +++ b/cmd/gateway/plugin_cli_toolchain_test.go @@ -2,6 +2,7 @@ package main import ( "archive/zip" + "bytes" "encoding/json" "io" "net/http" @@ -25,12 +26,15 @@ func TestPluginInitCreatesBuildableTemplate(t *testing.T) { if code != 0 { 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 { 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) } } @@ -63,6 +67,40 @@ func TestPluginBuildSourcePackagesTemplate(t *testing.T) { t.Fatalf("validatePluginPathForCLI(source) error = %v", err) } 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) { @@ -107,6 +145,7 @@ func TestPluginManifestFormatWrite(t *testing.T) { "plugin", "init", dir, "--id", "format-plugin", "--module", "example.com/format-plugin", + "--manifest-format", "json", }) if !handled || code != 0 { 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) { dir := filepath.Join(t.TempDir(), "governance-plugin") 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", "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", "preflight", dir, "--manifest", "manifest.yaml", "--config-json", `{"upstream":"127.0.0.1:25566"}`, "--profile", "dev"}, } { handled, code = runPluginCLI(tc) 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) + } + } +} diff --git a/docs/plugin-development-toolchain-design.md b/docs/plugin-development-toolchain-design.md index 49c3e08..f806b88 100644 --- a/docs/plugin-development-toolchain-design.md +++ b/docs/plugin-development-toolchain-design.md @@ -2,7 +2,7 @@ 本文定义插件开发工具链的功能需求和实现边界。目标是让插件作者从新建、开发、测试、打包到发布前检查都使用同一套 `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` 子命名空间 | -| 元数据来源 | `manifest.json` 是唯一人工维护的插件元数据来源 | +| 元数据来源 | 插件目录只允许一个人工维护的 manifest source;包内可信元数据统一为 canonical `manifest.json` | | 打包入口 | `gateway plugin build` 同时承担 build 和 package,不再要求插件目录自带 zip 脚本 | | 示例插件 | `upstream-rewrite` 和 `mc-auth-proxy` 迁移到标准 CLI,删除重复 `build.sh` 和 `render-manifest` 逻辑 | | runtime 扩展 | CLI 通过 runtime build/test adapter 分发逻辑,命令名不随 runtime 改变 | | 校验边界 | 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 命令。 - 不需要手写 `render-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 模板应至少生成: -- `manifest.json` +- `manifest.yaml`(默认;也支持 `manifest.yml`、`manifest.toml`、`manifest.jsonc`、`manifest.json`) - `go.mod` - `main.go` - `main_test.go` @@ -206,7 +207,7 @@ Go plugin 模板应至少生成: - `testdata/config.json` - `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` @@ -222,6 +223,8 @@ Go plugin 模板应至少生成: | `gateway plugin build . --type both` | 同时生成 binary 和 source `.mcgp` | | `gateway plugin build --from-source source.mcgp --out built.mcgp` | 使用 gateway builder 从 source 包生成 binary 包 | +当源码目录内存在多个 `manifest.*` 文件,`build` 必须通过 `--manifest ` 显式选择源文件;同一规则也适用于 `test`、`validate`、`preflight`、`self-test`、`benchmark` 和 `manifest format`。 + 推荐默认输出: - `dist/.mcgp` @@ -231,21 +234,23 @@ Go plugin 模板应至少生成: ### 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`。 - binary 包写入 `runtime.entry=plugin.so`。 - Go plugin binary 包写入实际 `go_version`、`go_os`、`go_arch`。 - 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 ` 显式选择,避免不同格式的 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` build adapter 负责: -1. 读取并校验 `manifest.json`。 +1. 读取并校验唯一 manifest source,或通过 `--manifest` 指定的 manifest source。 2. 运行 `go test ./...`,除非传入 `--skip-tests`。 3. 用固定命令构建 `plugin.so`:`go build -buildmode=plugin -trimpath -buildvcs=false`。 4. 用 `go tool nm` 校验 `Plugin` 符号。 @@ -324,7 +329,7 @@ type PluginBuildAdapter interface { `validate` 应支持三类输入: -- `manifest.json` +- manifest source 文件:`manifest.yaml`、`manifest.yml`、`manifest.toml`、`manifest.jsonc` 或 `manifest.json` - 插件源码目录 - `.mcgp` artifact @@ -457,7 +462,7 @@ CI 产物应至少保存: - README 使用 `gateway plugin build . --type both`。 - README 使用 `gateway plugin test .`。 - 删除或降级 `build.sh` 为兼容包装;最终不再作为主路径。 -- 删除 `cmd/render-manifest`,由 CLI 根据源码 `manifest.json` 生成 artifact manifest。 +- 删除 `cmd/render-manifest`,由 CLI 根据源码 manifest source 生成 artifact manifest。 - 示例插件的测试 fixture 进入 `testdata/fixtures/`。 - 示例插件进入 conformance suite;构建失败视为插件 API 回归。 @@ -483,6 +488,6 @@ CI 产物应至少保存: - 新建 `protocol-proxy` 模板后,能跑通 Minecraft handshake/login smoke fixture。 - `upstream-rewrite` 和 `mc-auth-proxy` 示例插件使用标准 CLI 生成 binary/source `.mcgp`。 - 生成的 `.mcgp` 能通过现有上传和服务端校验。 -- `manifest.json` 与 Go 代码不重复维护插件元数据。 +- manifest source 与 Go 代码不重复维护插件元数据。 - Go plugin adapter 之外的 runtime 可以通过 adapter 注册进入同一套 `init/build/test` 命令。 - CLI 失败输出能定位到字段、文件或 fixture,而不是只返回通用错误。 diff --git a/docs/plugin-system-design.md b/docs/plugin-system-design.md index 7cada2e..07548d8 100644 --- a/docs/plugin-system-design.md +++ b/docs/plugin-system-design.md @@ -20,7 +20,7 @@ - 第一版继续使用 Go `-buildmode=plugin` 的进程内 `.so` 插件。 - 尽量支持热加载:兼容且未加载过的插件可以不重启加载并启用。 - 明确热卸载限制: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`。 - `upstream.connect/v1` 必须支持插件返回自管 `net.Conn`,用于实现完整 stream endpoint/protocol proxy。 - MC 正版/三方登录、身份映射、forwarding 和登录后的协议处理属于插件业务逻辑,不由 gateway core 拼装。 @@ -33,7 +33,7 @@ | 决策/功能点 | 当前设计结论 | 阶段 | 主要章节 | | --- | --- | --- | --- | | 插件信任模型 | 第一版只支持可信 native 插件,不把普通插件当不可信代码运行 | 第一版 | Runtime 与权限声明、安全与运维约束 | -| 插件包格式 | 管理页统一上传 `.mcgp` zip 包,源码/二进制由 `manifest.json` 的 `artifact_type` 决定 | 第一版 | 包格式、源码包构建 | +| 插件包格式 | 管理页统一上传 `.mcgp` zip 包,源码/二进制由包内 canonical `manifest.json` 的 `artifact_type` 决定 | 第一版 | 包格式、源码包构建 | | 自定义扩展名 | 不新增源码包扩展名;同一 `.mcgp` 格式承载 binary/source | 第一版 | 包格式 | | Go plugin ABI | 仅使用 `.mcgp` 内的 `manifest.json` 表示元数据,并记录 Go/API/SDK/ABI fingerprint | 第一版 | Manifest 元数据、Go Plugin ABI Fingerprint | | Admin 管理 | 上传、构建、加载、启用、禁用、切换版本、删除、回滚和审计都进入 Admin/SQLite | 第一版 | 数据模型、生命周期、Admin API | @@ -67,9 +67,9 @@ | 管理页上传、构建、加载、启用/禁用、删除和切换版本 | 全部进入 Admin/SQLite 生命周期,删除已加载 native artifact 后提示重启彻底清理 | 数据模型、生命周期、Admin API、Admin 页面 | | 热加载尽量支持,热卸载承认 Go plugin 限制 | 兼容且未加载过 artifact 可热加载;已加载 Go plugin 只能逻辑禁用,不能真正卸载 | 生命周期、热加载和热卸载、Runbook | | 多进程模型实现进程级热卸载 | 预留 `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 | 源码包构建环境、供应链元数据 | -| 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 的定位 | | 沙箱功能要有未来路线 | 第一版不提供沙箱;预留 sandbox-process、WASM、capability enforcement、stream relay 和 egress 策略 | Sandbox Runtime、Runtime Adapter、第一版默认策略 | | 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 # 可选 ``` -`manifest.json` 是加载前可读取的元数据,用于避免必须执行插件代码才能知道基础信息。上传阶段只解析 zip 和 manifest,不执行插件代码。 +包内 `manifest.json` 是加载前可读取的可信元数据,用于避免必须执行插件代码才能知道基础信息。上传阶段只解析 zip 和 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`。 @@ -2009,7 +2009,7 @@ index 规则: ## 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 符号: @@ -6370,10 +6370,10 @@ type Gateway interface { - `plugin/api` 稳定 API 文档。 - `examples/plugins/upstream-rewrite` 最小模板。 - `examples/plugins/mc-auth-proxy` protocol-proxy 模板。 -- manifest JSON schema。 +- manifest source 多格式解析和 canonical JSON schema。 - 统一的 `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 工具 @@ -6427,7 +6427,7 @@ CLI 规则: 本地开发流程: 1. 从示例复制插件目录。 -2. 编写 `manifest.json`。 +2. 编写唯一 manifest source,默认是 `manifest.yaml`。 3. 使用与 gateway 匹配的 Go toolchain。 4. 运行 `gateway plugin build` 构建 `.mcgp`。 5. 通过 Admin 上传。 @@ -7354,7 +7354,7 @@ examples/plugins/mc-status-motd/ - 定义 `.mcgp` 静态校验规则、大小限制和 zip slip 防护。 - 定义插件 ID、handler ID、task ID、secret name 和 extension point 命名规范。 - 支持 `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 语义。 - 在 `plugin/api` 中补齐 `APIVersion`、extension point metadata、`ErrPass` 等。 - 把 upstream hook 收敛为 request struct。 diff --git a/examples/plugins/extension-ecosystem/README.md b/examples/plugins/extension-ecosystem/README.md index 50d67e5..ec096ac 100644 --- a/examples/plugins/extension-ecosystem/README.md +++ b/examples/plugins/extension-ecosystem/README.md @@ -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. 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`. diff --git a/examples/plugins/extension-ecosystem/manifest.json b/examples/plugins/extension-ecosystem/manifest.json deleted file mode 100644 index 3a3d8ee..0000000 --- a/examples/plugins/extension-ecosystem/manifest.json +++ /dev/null @@ -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" } -} diff --git a/examples/plugins/extension-ecosystem/manifest.yaml b/examples/plugins/extension-ecosystem/manifest.yaml new file mode 100644 index 0000000..a603682 --- /dev/null +++ b/examples/plugins/extension-ecosystem/manifest.yaml @@ -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 diff --git a/examples/plugins/mc-auth-proxy/README.md b/examples/plugins/mc-auth-proxy/README.md index 0492033..55eedda 100644 --- a/examples/plugins/mc-auth-proxy/README.md +++ b/examples/plugins/mc-auth-proxy/README.md @@ -18,6 +18,8 @@ Build and package: The binary package is written to `dist/mc-auth-proxy.mcgp`; the source package 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: diff --git a/examples/plugins/mc-auth-proxy/manifest.json b/examples/plugins/mc-auth-proxy/manifest.json deleted file mode 100644 index 7497e38..0000000 --- a/examples/plugins/mc-auth-proxy/manifest.json +++ /dev/null @@ -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" } - } - } -} diff --git a/examples/plugins/mc-auth-proxy/manifest.yaml b/examples/plugins/mc-auth-proxy/manifest.yaml new file mode 100644 index 0000000..1d17b5e --- /dev/null +++ b/examples/plugins/mc-auth-proxy/manifest.yaml @@ -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 diff --git a/examples/plugins/upstream-rewrite/README.md b/examples/plugins/upstream-rewrite/README.md index 53ab273..47a92f9 100644 --- a/examples/plugins/upstream-rewrite/README.md +++ b/examples/plugins/upstream-rewrite/README.md @@ -14,6 +14,8 @@ Build and package: The binary package is written to `dist/upstream-rewrite.mcgp`; the source 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: diff --git a/examples/plugins/upstream-rewrite/manifest.json b/examples/plugins/upstream-rewrite/manifest.json deleted file mode 100644 index 87c20fe..0000000 --- a/examples/plugins/upstream-rewrite/manifest.json +++ /dev/null @@ -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"] - } -} diff --git a/examples/plugins/upstream-rewrite/manifest.yaml b/examples/plugins/upstream-rewrite/manifest.yaml new file mode 100644 index 0000000..bd583d3 --- /dev/null +++ b/examples/plugins/upstream-rewrite/manifest.yaml @@ -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 diff --git a/go.mod b/go.mod index 26809de..261eb2e 100644 --- a/go.mod +++ b/go.mod @@ -7,11 +7,13 @@ toolchain go1.24.4 require ( github.com/gorilla/websocket v1.5.3 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/quic-go/quic-go v0.52.0 github.com/rs/zerolog v1.33.0 github.com/xtaci/kcp-go v5.4.20+incompatible golang.org/x/crypto v0.43.0 + gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.45.0 ) diff --git a/go.sum b/go.sum index ec00fbb..137e940 100644 --- a/go.sum +++ b/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/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/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/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= 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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=