fix: cancel extension installer subprocesses
- commit
- 8fd3087f5d27d0842bff587436d6adf5d7346471
- author
- brice · · 2026-08-05 03:50 UTC
- parent
- df3ccd3
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
6 files · +50 -16
M Makefile +2 -1
| @@ -50,7 +50,8 @@ vet: | |||
| 50 | 50 | $(GO) vet ./... | |
| 51 | 51 | ||
| 52 | 52 | fmt-check: | |
| 53 | - | @test -z "$$(gofmt -l . | tee /dev/stderr)" || (echo "gofmt issues"; exit 1) | |
| 53 | + | @files="$$(gofmt -l . 2>&1)" || { printf '%s\n' "$$files" >&2; echo "gofmt failed" >&2; exit 1; }; \ | |
| 54 | + | if [ -n "$$files" ]; then printf '%s\n' "$$files" >&2; echo "gofmt issues" >&2; exit 1; fi | |
| 54 | 55 | ||
| 55 | 56 | lint: vet fmt-check | |
| 56 | 57 | ||
| @@ -50,7 +50,8 @@ vet: | |||||
| 50 | $(GO) vet ./... | 50 | $(GO) vet ./... | ||
| 51 | 51 | ||||
| 52 | fmt-check: | 52 | fmt-check: | ||
| 53 | - | @test -z "$$(gofmt -l . | tee /dev/stderr)" || (echo "gofmt issues"; exit 1) | 53 | + | @files="$$(gofmt -l . 2>&1)" || { printf '%s\n' "$$files" >&2; echo "gofmt failed" >&2; exit 1; }; \ |
| 54 | + | if [ -n "$$files" ]; then printf '%s\n' "$$files" >&2; echo "gofmt issues" >&2; exit 1; fi | |||
| 54 | 55 | ||||
| 55 | lint: vet fmt-check | 56 | lint: vet fmt-check | ||
| 56 | 57 | ||||
M docs/extensions.md +5 -4
| @@ -85,8 +85,9 @@ The `examples/extensions/` directory in the repo is reference code, not a defaul | |||
| 85 | 85 | zot ext install --build=go path/to/zot/examples/extensions/hello | |
| 86 | 86 | ||
| 87 | 87 | # or build it in the source tree for a one-run development load | |
| 88 | - | cd path/to/zot/examples/extensions/hello && go build -o hello . | |
| 89 | - | zot --ext path/to/zot/examples/extensions/hello | |
| 88 | + | cd path/to/zot/examples/extensions/hello | |
| 89 | + | go build -o hello . | |
| 90 | + | zot --ext . | |
| 90 | 91 | ``` | |
| 91 | 92 | ||
| 92 | 93 | `zot ext install` never builds source code implicitly. For local installs it | |
| @@ -605,8 +606,8 @@ Reply with `shutdown_ack` and then exit. | |||
| 605 | 606 | ``` | |
| 606 | 607 | zot ext list list installed extensions and their state | |
| 607 | 608 | zot ext doctor diagnose load, registration, and conflict issues | |
| 608 | - | zot ext install [--build=go] <path|git-url> | |
| 609 | - | copy / clone into $ZOT_HOME/extensions/ | |
| 609 | + | zot ext install <path|git-url> copy / clone into $ZOT_HOME/extensions/ | |
| 610 | + | zot ext install --build=go <local-path> build local Go source, then install | |
| 610 | 611 | zot ext remove <name> delete an extension directory | |
| 611 | 612 | zot ext enable <name> re-enable a disabled extension | |
| 612 | 613 | zot ext disable <name> disable without removing | |
| @@ -85,8 +85,9 @@ The `examples/extensions/` directory in the repo is reference code, not a defaul | |||||
| 85 | zot ext install --build=go path/to/zot/examples/extensions/hello | 85 | zot ext install --build=go path/to/zot/examples/extensions/hello | ||
| 86 | 86 | ||||
| 87 | # or build it in the source tree for a one-run development load | 87 | # or build it in the source tree for a one-run development load | ||
| 88 | - | cd path/to/zot/examples/extensions/hello && go build -o hello . | 88 | + | cd path/to/zot/examples/extensions/hello |
| 89 | - | zot --ext path/to/zot/examples/extensions/hello | 89 | + | go build -o hello . |
| 90 | + | zot --ext . | |||
| 90 | ``` | 91 | ``` | ||
| 91 | 92 | ||||
| 92 | `zot ext install` never builds source code implicitly. For local installs it | 93 | `zot ext install` never builds source code implicitly. For local installs it | ||
| @@ -605,8 +606,8 @@ Reply with `shutdown_ack` and then exit. | |||||
| 605 | ``` | 606 | ``` | ||
| 606 | zot ext list list installed extensions and their state | 607 | zot ext list list installed extensions and their state | ||
| 607 | zot ext doctor diagnose load, registration, and conflict issues | 608 | zot ext doctor diagnose load, registration, and conflict issues | ||
| 608 | - | zot ext install [--build=go] <path|git-url> | 609 | + | zot ext install <path|git-url> copy / clone into $ZOT_HOME/extensions/ |
| 609 | - | copy / clone into $ZOT_HOME/extensions/ | 610 | + | zot ext install --build=go <local-path> build local Go source, then install |
| 610 | zot ext remove <name> delete an extension directory | 611 | zot ext remove <name> delete an extension directory | ||
| 611 | zot ext enable <name> re-enable a disabled extension | 612 | zot ext enable <name> re-enable a disabled extension | ||
| 612 | zot ext disable <name> disable without removing | 613 | zot ext disable <name> disable without removing | ||
M packages/agent/args.go +1 -1
| @@ -423,7 +423,7 @@ func PrintHelp(version string) { | |||
| 423 | 423 | ) | |
| 424 | 424 | section("extensions", | |
| 425 | 425 | row{"zot ext list", "list installed extensions"}, | |
| 426 | - | row{"zot ext install <path|url>", "install from a local path or URL"}, | |
| 426 | + | row{"zot ext install <path|git-url>", "install from a local path or Git URL"}, | |
| 427 | 427 | row{"zot ext install --build=go <path>", "build local Go source, then install"}, | |
| 428 | 428 | row{"zot --ext ./path/to/ext", "load an extension for this run only"}, | |
| 429 | 429 | row{"zot ext help", "show all extension subcommands"}, | |
| @@ -423,7 +423,7 @@ func PrintHelp(version string) { | |||||
| 423 | ) | 423 | ) | ||
| 424 | section("extensions", | 424 | section("extensions", | ||
| 425 | row{"zot ext list", "list installed extensions"}, | 425 | row{"zot ext list", "list installed extensions"}, | ||
| 426 | - | row{"zot ext install <path|url>", "install from a local path or URL"}, | 426 | + | row{"zot ext install <path|git-url>", "install from a local path or Git URL"}, |
| 427 | row{"zot ext install --build=go <path>", "build local Go source, then install"}, | 427 | row{"zot ext install --build=go <path>", "build local Go source, then install"}, | ||
| 428 | row{"zot --ext ./path/to/ext", "load an extension for this run only"}, | 428 | row{"zot --ext ./path/to/ext", "load an extension for this run only"}, | ||
| 429 | row{"zot ext help", "show all extension subcommands"}, | 429 | row{"zot ext help", "show all extension subcommands"}, | ||
M packages/agent/cli.go +11 -1
| @@ -620,12 +620,22 @@ func fanoutAgentEvent(mgr *extensions.Manager, ev core.AgentEvent) { | |||
| 620 | 620 | ||
| 621 | 621 | // Run is the top-level entrypoint for the zot binary. | |
| 622 | 622 | func Run(rawArgs []string, version string) error { | |
| 623 | + | // Extension installs can invoke external build and clone processes. | |
| 624 | + | // Give those processes a cancellation context without changing signal | |
| 625 | + | // handling for the interactive modes below. | |
| 626 | + | extCtx := context.Background() | |
| 627 | + | stopExtContext := func() {} | |
| 628 | + | if len(rawArgs) > 0 && rawArgs[0] == "ext" { | |
| 629 | + | extCtx, stopExtContext = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |
| 630 | + | } | |
| 631 | + | defer stopExtContext() | |
| 632 | + | ||
| 623 | 633 | // Subcommand router: `zot bot ...` is handled separately so the | |
| 624 | 634 | // generic flag parser doesn't reject "bot" as a positional arg. | |
| 625 | 635 | if handled, err := runBotCommand(rawArgs, version); handled { | |
| 626 | 636 | return err | |
| 627 | 637 | } | |
| 628 | - | if handled, err := runExtCommand(rawArgs, version); handled { | |
| 638 | + | if handled, err := runExtCommand(extCtx, rawArgs, version); handled { | |
| 629 | 639 | return err | |
| 630 | 640 | } | |
| 631 | 641 | if handled, err := runUpdateCommand(rawArgs, version); handled { | |
| @@ -620,12 +620,22 @@ func fanoutAgentEvent(mgr *extensions.Manager, ev core.AgentEvent) { | |||||
| 620 | 620 | ||||
| 621 | // Run is the top-level entrypoint for the zot binary. | 621 | // Run is the top-level entrypoint for the zot binary. | ||
| 622 | func Run(rawArgs []string, version string) error { | 622 | func Run(rawArgs []string, version string) error { | ||
| 623 | + | // Extension installs can invoke external build and clone processes. | |||
| 624 | + | // Give those processes a cancellation context without changing signal | |||
| 625 | + | // handling for the interactive modes below. | |||
| 626 | + | extCtx := context.Background() | |||
| 627 | + | stopExtContext := func() {} | |||
| 628 | + | if len(rawArgs) > 0 && rawArgs[0] == "ext" { | |||
| 629 | + | extCtx, stopExtContext = signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | |||
| 630 | + | } | |||
| 631 | + | defer stopExtContext() | |||
| 632 | + | ||||
| 623 | // Subcommand router: `zot bot ...` is handled separately so the | 633 | // Subcommand router: `zot bot ...` is handled separately so the | ||
| 624 | // generic flag parser doesn't reject "bot" as a positional arg. | 634 | // generic flag parser doesn't reject "bot" as a positional arg. | ||
| 625 | if handled, err := runBotCommand(rawArgs, version); handled { | 635 | if handled, err := runBotCommand(rawArgs, version); handled { | ||
| 626 | return err | 636 | return err | ||
| 627 | } | 637 | } | ||
| 628 | - | if handled, err := runExtCommand(rawArgs, version); handled { | 638 | + | if handled, err := runExtCommand(extCtx, rawArgs, version); handled { |
| 629 | return err | 639 | return err | ||
| 630 | } | 640 | } | ||
| 631 | if handled, err := runUpdateCommand(rawArgs, version); handled { | 641 | if handled, err := runUpdateCommand(rawArgs, version); handled { | ||
M packages/agent/extcmd.go +14 -9
| @@ -23,7 +23,7 @@ import ( | |||
| 23 | 23 | // (handled=true, err) if rawArgs starts with "ext"; otherwise | |
| 24 | 24 | // (handled=false, nil) so the main router falls through to the | |
| 25 | 25 | // regular flag parser. | |
| 26 | - | func runExtCommand(rawArgs []string, version string) (handled bool, err error) { | |
| 26 | + | func runExtCommand(ctx context.Context, rawArgs []string, version string) (handled bool, err error) { | |
| 27 | 27 | if len(rawArgs) == 0 || rawArgs[0] != "ext" { | |
| 28 | 28 | return false, nil | |
| 29 | 29 | } | |
| @@ -45,7 +45,7 @@ func runExtCommand(rawArgs []string, version string) (handled bool, err error) { | |||
| 45 | 45 | case "remove", "rm": | |
| 46 | 46 | return true, extRemove(rawArgs[2:]) | |
| 47 | 47 | case "install": | |
| 48 | - | return true, extInstall(rawArgs[2:]) | |
| 48 | + | return true, extInstallContext(ctx, rawArgs[2:]) | |
| 49 | 49 | case "help", "-h", "--help": | |
| 50 | 50 | printExtHelp() | |
| 51 | 51 | return true, nil | |
| @@ -473,6 +473,10 @@ func extRemove(args []string) error { | |||
| 473 | 473 | // Builds are never inferred or run implicitly; --build=go is an explicit | |
| 474 | 474 | // opt-in for local Go extensions. | |
| 475 | 475 | func extInstall(args []string) error { | |
| 476 | + | return extInstallContext(context.Background(), args) | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | func extInstallContext(ctx context.Context, args []string) error { | |
| 476 | 480 | opts, err := parseExtInstallArgs(args) | |
| 477 | 481 | if err != nil { | |
| 478 | 482 | return err | |
| @@ -487,7 +491,7 @@ func extInstall(args []string) error { | |||
| 487 | 491 | if opts.builder != "" { | |
| 488 | 492 | return errors.New("--build=go is currently supported only for local extension paths; clone the extension locally and build it there") | |
| 489 | 493 | } | |
| 490 | - | return installGitExtension(src, dest) | |
| 494 | + | return installGitExtension(ctx, src, dest) | |
| 491 | 495 | } | |
| 492 | 496 | ||
| 493 | 497 | // Local path: must be a directory containing extension.json. | |
| @@ -523,7 +527,7 @@ func extInstall(args []string) error { | |||
| 523 | 527 | return fmt.Errorf("copy extension: %w", err) | |
| 524 | 528 | } | |
| 525 | 529 | if opts.builder != "" { | |
| 526 | - | if err := buildLocalExtension(absSrc, staged, manifest); err != nil { | |
| 530 | + | if err := buildLocalExtension(ctx, absSrc, staged, manifest); err != nil { | |
| 527 | 531 | return err | |
| 528 | 532 | } | |
| 529 | 533 | } | |
| @@ -591,7 +595,7 @@ func parseExtInstallArgs(args []string) (extInstallOptions, error) { | |||
| 591 | 595 | return opts, nil | |
| 592 | 596 | } | |
| 593 | 597 | ||
| 594 | - | func buildLocalExtension(sourceDir, stagedDir string, manifest extensions.Manifest) error { | |
| 598 | + | func buildLocalExtension(ctx context.Context, sourceDir, stagedDir string, manifest extensions.Manifest) error { | |
| 595 | 599 | if manifest.Exec == "" { | |
| 596 | 600 | return errors.New("cannot build a theme-only extension; it does not declare an executable") | |
| 597 | 601 | } | |
| @@ -605,7 +609,7 @@ func buildLocalExtension(sourceDir, stagedDir string, manifest extensions.Manife | |||
| 605 | 609 | } | |
| 606 | 610 | ||
| 607 | 611 | fmt.Fprintf(os.Stderr, "building %s with go\n", manifest.Name) | |
| 608 | - | cmd := exec.Command("go", "build", "-trimpath", "-o", stagedExec, ".") | |
| 612 | + | cmd := exec.CommandContext(ctx, "go", "build", "-trimpath", "-o", stagedExec, ".") | |
| 609 | 613 | cmd.Dir = sourceDir | |
| 610 | 614 | cmd.Stdout = os.Stderr | |
| 611 | 615 | cmd.Stderr = os.Stderr | |
| @@ -634,7 +638,7 @@ func extensionRelativeExecPath(dir, execName string) (string, error) { | |||
| 634 | 638 | return rel, nil | |
| 635 | 639 | } | |
| 636 | 640 | ||
| 637 | - | func installGitExtension(src, dest string) error { | |
| 641 | + | func installGitExtension(ctx context.Context, src, dest string) error { | |
| 638 | 642 | stageRoot, err := os.MkdirTemp(dest, ".zot-extension-clone-") | |
| 639 | 643 | if err != nil { | |
| 640 | 644 | return fmt.Errorf("create install staging directory: %w", err) | |
| @@ -642,7 +646,8 @@ func installGitExtension(src, dest string) error { | |||
| 642 | 646 | defer os.RemoveAll(stageRoot) | |
| 643 | 647 | ||
| 644 | 648 | cloneDir := filepath.Join(stageRoot, "extension") | |
| 645 | - | cmd := exec.Command("git", "clone", "--depth", "1", src, cloneDir) | |
| 649 | + | cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", src, cloneDir) | |
| 650 | + | cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") | |
| 646 | 651 | cmd.Stdout = os.Stderr | |
| 647 | 652 | cmd.Stderr = os.Stderr | |
| 648 | 653 | if err := cmd.Run(); err != nil { | |
| @@ -684,7 +689,7 @@ func readExtensionManifest(dir string) (extensions.Manifest, error) { | |||
| 684 | 689 | if manifest.Name != strings.TrimSpace(manifest.Name) { | |
| 685 | 690 | return extensions.Manifest{}, fmt.Errorf("manifest: extension name %q has leading or trailing whitespace", manifest.Name) | |
| 686 | 691 | } | |
| 687 | - | if manifest.Name == "." || manifest.Name == ".." || strings.ContainsAny(manifest.Name, `/\\`) { | |
| 692 | + | if strings.HasPrefix(manifest.Name, ".") || strings.ContainsAny(manifest.Name, `/\\`) { | |
| 688 | 693 | return extensions.Manifest{}, fmt.Errorf("manifest: invalid extension name %q", manifest.Name) | |
| 689 | 694 | } | |
| 690 | 695 | if manifest.Exec == "" && !extensions.HasExtensionTheme(dir) { | |
| @@ -23,7 +23,7 @@ import ( | |||||
| 23 | // (handled=true, err) if rawArgs starts with "ext"; otherwise | 23 | // (handled=true, err) if rawArgs starts with "ext"; otherwise | ||
| 24 | // (handled=false, nil) so the main router falls through to the | 24 | // (handled=false, nil) so the main router falls through to the | ||
| 25 | // regular flag parser. | 25 | // regular flag parser. | ||
| 26 | - | func runExtCommand(rawArgs []string, version string) (handled bool, err error) { | 26 | + | func runExtCommand(ctx context.Context, rawArgs []string, version string) (handled bool, err error) { |
| 27 | if len(rawArgs) == 0 || rawArgs[0] != "ext" { | 27 | if len(rawArgs) == 0 || rawArgs[0] != "ext" { | ||
| 28 | return false, nil | 28 | return false, nil | ||
| 29 | } | 29 | } | ||
| @@ -45,7 +45,7 @@ func runExtCommand(rawArgs []string, version string) (handled bool, err error) { | |||||
| 45 | case "remove", "rm": | 45 | case "remove", "rm": | ||
| 46 | return true, extRemove(rawArgs[2:]) | 46 | return true, extRemove(rawArgs[2:]) | ||
| 47 | case "install": | 47 | case "install": | ||
| 48 | - | return true, extInstall(rawArgs[2:]) | 48 | + | return true, extInstallContext(ctx, rawArgs[2:]) |
| 49 | case "help", "-h", "--help": | 49 | case "help", "-h", "--help": | ||
| 50 | printExtHelp() | 50 | printExtHelp() | ||
| 51 | return true, nil | 51 | return true, nil | ||
| @@ -473,6 +473,10 @@ func extRemove(args []string) error { | |||||
| 473 | // Builds are never inferred or run implicitly; --build=go is an explicit | 473 | // Builds are never inferred or run implicitly; --build=go is an explicit | ||
| 474 | // opt-in for local Go extensions. | 474 | // opt-in for local Go extensions. | ||
| 475 | func extInstall(args []string) error { | 475 | func extInstall(args []string) error { | ||
| 476 | + | return extInstallContext(context.Background(), args) | |||
| 477 | + | } | |||
| 478 | + | ||||
| 479 | + | func extInstallContext(ctx context.Context, args []string) error { | |||
| 476 | opts, err := parseExtInstallArgs(args) | 480 | opts, err := parseExtInstallArgs(args) | ||
| 477 | if err != nil { | 481 | if err != nil { | ||
| 478 | return err | 482 | return err | ||
| @@ -487,7 +491,7 @@ func extInstall(args []string) error { | |||||
| 487 | if opts.builder != "" { | 491 | if opts.builder != "" { | ||
| 488 | return errors.New("--build=go is currently supported only for local extension paths; clone the extension locally and build it there") | 492 | return errors.New("--build=go is currently supported only for local extension paths; clone the extension locally and build it there") | ||
| 489 | } | 493 | } | ||
| 490 | - | return installGitExtension(src, dest) | 494 | + | return installGitExtension(ctx, src, dest) |
| 491 | } | 495 | } | ||
| 492 | 496 | ||||
| 493 | // Local path: must be a directory containing extension.json. | 497 | // Local path: must be a directory containing extension.json. | ||
| @@ -523,7 +527,7 @@ func extInstall(args []string) error { | |||||
| 523 | return fmt.Errorf("copy extension: %w", err) | 527 | return fmt.Errorf("copy extension: %w", err) | ||
| 524 | } | 528 | } | ||
| 525 | if opts.builder != "" { | 529 | if opts.builder != "" { | ||
| 526 | - | if err := buildLocalExtension(absSrc, staged, manifest); err != nil { | 530 | + | if err := buildLocalExtension(ctx, absSrc, staged, manifest); err != nil { |
| 527 | return err | 531 | return err | ||
| 528 | } | 532 | } | ||
| 529 | } | 533 | } | ||
| @@ -591,7 +595,7 @@ func parseExtInstallArgs(args []string) (extInstallOptions, error) { | |||||
| 591 | return opts, nil | 595 | return opts, nil | ||
| 592 | } | 596 | } | ||
| 593 | 597 | ||||
| 594 | - | func buildLocalExtension(sourceDir, stagedDir string, manifest extensions.Manifest) error { | 598 | + | func buildLocalExtension(ctx context.Context, sourceDir, stagedDir string, manifest extensions.Manifest) error { |
| 595 | if manifest.Exec == "" { | 599 | if manifest.Exec == "" { | ||
| 596 | return errors.New("cannot build a theme-only extension; it does not declare an executable") | 600 | return errors.New("cannot build a theme-only extension; it does not declare an executable") | ||
| 597 | } | 601 | } | ||
| @@ -605,7 +609,7 @@ func buildLocalExtension(sourceDir, stagedDir string, manifest extensions.Manife | |||||
| 605 | } | 609 | } | ||
| 606 | 610 | ||||
| 607 | fmt.Fprintf(os.Stderr, "building %s with go\n", manifest.Name) | 611 | fmt.Fprintf(os.Stderr, "building %s with go\n", manifest.Name) | ||
| 608 | - | cmd := exec.Command("go", "build", "-trimpath", "-o", stagedExec, ".") | 612 | + | cmd := exec.CommandContext(ctx, "go", "build", "-trimpath", "-o", stagedExec, ".") |
| 609 | cmd.Dir = sourceDir | 613 | cmd.Dir = sourceDir | ||
| 610 | cmd.Stdout = os.Stderr | 614 | cmd.Stdout = os.Stderr | ||
| 611 | cmd.Stderr = os.Stderr | 615 | cmd.Stderr = os.Stderr | ||
| @@ -634,7 +638,7 @@ func extensionRelativeExecPath(dir, execName string) (string, error) { | |||||
| 634 | return rel, nil | 638 | return rel, nil | ||
| 635 | } | 639 | } | ||
| 636 | 640 | ||||
| 637 | - | func installGitExtension(src, dest string) error { | 641 | + | func installGitExtension(ctx context.Context, src, dest string) error { |
| 638 | stageRoot, err := os.MkdirTemp(dest, ".zot-extension-clone-") | 642 | stageRoot, err := os.MkdirTemp(dest, ".zot-extension-clone-") | ||
| 639 | if err != nil { | 643 | if err != nil { | ||
| 640 | return fmt.Errorf("create install staging directory: %w", err) | 644 | return fmt.Errorf("create install staging directory: %w", err) | ||
| @@ -642,7 +646,8 @@ func installGitExtension(src, dest string) error { | |||||
| 642 | defer os.RemoveAll(stageRoot) | 646 | defer os.RemoveAll(stageRoot) | ||
| 643 | 647 | ||||
| 644 | cloneDir := filepath.Join(stageRoot, "extension") | 648 | cloneDir := filepath.Join(stageRoot, "extension") | ||
| 645 | - | cmd := exec.Command("git", "clone", "--depth", "1", src, cloneDir) | 649 | + | cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", src, cloneDir) |
| 650 | + | cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") | |||
| 646 | cmd.Stdout = os.Stderr | 651 | cmd.Stdout = os.Stderr | ||
| 647 | cmd.Stderr = os.Stderr | 652 | cmd.Stderr = os.Stderr | ||
| 648 | if err := cmd.Run(); err != nil { | 653 | if err := cmd.Run(); err != nil { | ||
| @@ -684,7 +689,7 @@ func readExtensionManifest(dir string) (extensions.Manifest, error) { | |||||
| 684 | if manifest.Name != strings.TrimSpace(manifest.Name) { | 689 | if manifest.Name != strings.TrimSpace(manifest.Name) { | ||
| 685 | return extensions.Manifest{}, fmt.Errorf("manifest: extension name %q has leading or trailing whitespace", manifest.Name) | 690 | return extensions.Manifest{}, fmt.Errorf("manifest: extension name %q has leading or trailing whitespace", manifest.Name) | ||
| 686 | } | 691 | } | ||
| 687 | - | if manifest.Name == "." || manifest.Name == ".." || strings.ContainsAny(manifest.Name, `/\\`) { | 692 | + | if strings.HasPrefix(manifest.Name, ".") || strings.ContainsAny(manifest.Name, `/\\`) { |
| 688 | return extensions.Manifest{}, fmt.Errorf("manifest: invalid extension name %q", manifest.Name) | 693 | return extensions.Manifest{}, fmt.Errorf("manifest: invalid extension name %q", manifest.Name) | ||
| 689 | } | 694 | } | ||
| 690 | if manifest.Exec == "" && !extensions.HasExtensionTheme(dir) { | 695 | if manifest.Exec == "" && !extensions.HasExtensionTheme(dir) { | ||
M packages/agent/extcmd_test.go +17 -0
| @@ -278,6 +278,23 @@ func TestExtInstallRejectsWhitespaceManifestName(t *testing.T) { | |||
| 278 | 278 | } | |
| 279 | 279 | } | |
| 280 | 280 | ||
| 281 | + | func TestExtInstallRejectsDotManifestName(t *testing.T) { | |
| 282 | + | home := t.TempDir() | |
| 283 | + | t.Setenv("ZOT_HOME", home) | |
| 284 | + | src := filepath.Join(t.TempDir(), "dot-name") | |
| 285 | + | if err := os.MkdirAll(src, 0o755); err != nil { | |
| 286 | + | t.Fatal(err) | |
| 287 | + | } | |
| 288 | + | if err := os.WriteFile(filepath.Join(src, "extension.json"), []byte(`{"name":".example","exec":"go"}`), 0o644); err != nil { | |
| 289 | + | t.Fatal(err) | |
| 290 | + | } | |
| 291 | + | ||
| 292 | + | err := extInstall([]string{src}) | |
| 293 | + | if err == nil || !strings.Contains(err.Error(), "invalid extension name") { | |
| 294 | + | t.Fatalf("error = %v, want invalid-name validation error", err) | |
| 295 | + | } | |
| 296 | + | } | |
| 297 | + | ||
| 281 | 298 | func TestExtInstallRejectsBuildForPATHExecutable(t *testing.T) { | |
| 282 | 299 | home := t.TempDir() | |
| 283 | 300 | t.Setenv("ZOT_HOME", home) | |
| @@ -278,6 +278,23 @@ func TestExtInstallRejectsWhitespaceManifestName(t *testing.T) { | |||||
| 278 | } | 278 | } | ||
| 279 | } | 279 | } | ||
| 280 | 280 | ||||
| 281 | + | func TestExtInstallRejectsDotManifestName(t *testing.T) { | |||
| 282 | + | home := t.TempDir() | |||
| 283 | + | t.Setenv("ZOT_HOME", home) | |||
| 284 | + | src := filepath.Join(t.TempDir(), "dot-name") | |||
| 285 | + | if err := os.MkdirAll(src, 0o755); err != nil { | |||
| 286 | + | t.Fatal(err) | |||
| 287 | + | } | |||
| 288 | + | if err := os.WriteFile(filepath.Join(src, "extension.json"), []byte(`{"name":".example","exec":"go"}`), 0o644); err != nil { | |||
| 289 | + | t.Fatal(err) | |||
| 290 | + | } | |||
| 291 | + | ||||
| 292 | + | err := extInstall([]string{src}) | |||
| 293 | + | if err == nil || !strings.Contains(err.Error(), "invalid extension name") { | |||
| 294 | + | t.Fatalf("error = %v, want invalid-name validation error", err) | |||
| 295 | + | } | |||
| 296 | + | } | |||
| 297 | + | ||||
| 281 | func TestExtInstallRejectsBuildForPATHExecutable(t *testing.T) { | 298 | func TestExtInstallRejectsBuildForPATHExecutable(t *testing.T) { | ||
| 282 | home := t.TempDir() | 299 | home := t.TempDir() | ||
| 283 | t.Setenv("ZOT_HOME", home) | 300 | t.Setenv("ZOT_HOME", home) | ||