Files
pmg/cmd/sandbox/profile_show.go
T
b59c3358e8 fix(sandbox): classify helper-tool errors with usefulerror (#272)
* fix(sandbox): classify helper-tool errors with usefulerror

Sandbox helper commands (profile lint/diff/show/init/list) used to bubble
up plain fmt.Errorf chains from the registry layer, which the TUI then
classified as Unknown and decorated with a bug-report link. Wrap each
error path at the cmd/sandbox boundary so the TUI prints NotFound,
InvalidArgument, or PermissionDenied with actionable hints instead.

Closes #269

* refactor(sandbox): classify registry errors via sentinel wrapping

Replace the fragile substring match in profileLoadError with errors.Is
against new sandbox.ErrProfileNotFound / sandbox.ErrProfileInvalid
sentinels. Every fmt.Errorf in registry.go that previously communicated
"missing" or "malformed" by message text now wraps the corresponding
sentinel, so the cmd layer can classify without inspecting strings.

* fix(sandbox): detect IO error class when wrapping helper errors

Replace static ErrCodeUnknown / ErrCodePermissionDenied wrappings with
ioErrorCode, which inspects the error chain for fs.ErrPermission and
fs.ErrNotExist before falling back. Applied to runProfileList (where an
unreadable user profile directory now classifies as PermissionDenied),
registryInitError, and the stat/MkdirAll/WriteFile paths in profile init.

Also drop redundant doc comments on helpers whose names are self-evident.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-21 19:34:29 +05:30

175 lines
4.9 KiB
Go

package sandbox
import (
"fmt"
"io"
"os"
"github.com/safedep/dry/log"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
type profileShowOptions struct {
resolved bool
driver string
cwd string
home string
jsonOut bool
}
func newProfileShowCommand(factory registryFactory) *cobra.Command {
opts := &profileShowOptions{}
cmd := &cobra.Command{
Use: "show <name>",
Short: "Show a sandbox profile (raw YAML, resolved policy, or driver-rendered)",
Example: " pmg sandbox profile show npm-restrictive --resolved",
Args: cobra.ExactArgs(1),
SilenceErrors: false,
RunE: func(cmd *cobra.Command, args []string) error {
err := runProfileShow(cmd.OutOrStdout(), args[0], opts, factory)
if err != nil {
return sandboxErrorExit(cmd, err)
}
return err
},
}
cmd.Flags().BoolVar(&opts.resolved, "resolved", false, "Print the policy after inheritance and variable expansion")
cmd.Flags().StringVar(&opts.driver, "driver", "", "Render the resolved policy for a specific driver: seatbelt|bubblewrap|landlock")
cmd.Flags().StringVar(&opts.cwd, "cwd", "", "Override ${CWD} during expansion (defaults to current working directory)")
cmd.Flags().StringVar(&opts.home, "home", "", "Override ${HOME} during expansion (defaults to current user home)")
cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Emit output as JSON")
return cmd
}
func runProfileShow(out io.Writer, name string, opts *profileShowOptions, factory registryFactory) error {
if err := validateDriver(opts.driver); err != nil {
return err
}
registry, err := factory()
if err != nil {
return registryInitError(err)
}
if opts.driver != "" {
return runProfileShowDriver(out, name, opts, registry)
}
if opts.resolved {
return runProfileShowResolved(out, name, opts, registry)
}
return runProfileShowRaw(out, name, opts, registry)
}
func runProfileShowRaw(out io.Writer, name string, opts *profileShowOptions, registry pmgsandbox.ProfileRegistry) error {
source, path, data, err := loadProfileSource(name, registry)
if err != nil {
return profileLoadError(err)
}
if opts.jsonOut {
report := map[string]any{
"name": name,
"source": string(source),
"path": path,
"yaml": string(data),
}
return writeJSONIndent(out, report)
}
_, err = out.Write(data)
return err
}
func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions, registry pmgsandbox.ProfileRegistry) error {
policy, err := registry.ResolveProfile(name, pmgsandbox.ResolveOptions{
CWD: opts.cwd,
Home: opts.home,
})
if err != nil {
return profileLoadError(err)
}
if opts.jsonOut {
report := map[string]any{
"name": name,
"policy": policy,
}
return writeJSONIndent(out, report)
}
data, err := yaml.Marshal(policy)
if err != nil {
return wrapUseful(fmt.Errorf("failed to marshal resolved policy: %w", err),
usefulerror.ErrCodeUnknown,
"Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.")
}
_, err = out.Write(data)
return err
}
func runProfileShowDriver(out io.Writer, name string, opts *profileShowOptions, registry pmgsandbox.ProfileRegistry) error {
policy, err := registry.ResolveProfile(name, pmgsandbox.ResolveOptions{
CWD: opts.cwd,
Home: opts.home,
})
if err != nil {
return profileLoadError(err)
}
rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy)
if err != nil {
return wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
"Could not render the policy for the requested driver. Verify the driver is supported on this host.")
}
if opts.jsonOut {
report := map[string]any{
"name": name,
"driver": opts.driver,
"rendered": string(rendered),
}
return writeJSONIndent(out, report)
}
_, err = out.Write(rendered)
return err
}
func loadProfileSource(name string, registry pmgsandbox.ProfileRegistry) (pmgsandbox.ProfileSource, string, []byte, error) {
if data, ok := registry.BuiltinProfileYAML(name); ok {
return pmgsandbox.ProfileSourceBuiltin, "", data, nil
}
summaries, err := registry.ListProfiles()
if err != nil {
log.Warnf("sandbox: failed to enumerate user profiles: %v", err)
}
for _, s := range summaries {
if s.Source == pmgsandbox.ProfileSourceUser && s.Name == name {
data, readErr := os.ReadFile(s.Path)
if readErr != nil {
return "", "", nil, fmt.Errorf("failed to read user profile %s: %w", s.Path, readErr)
}
return pmgsandbox.ProfileSourceUser, s.Path, data, nil
}
}
if data, readErr := os.ReadFile(name); readErr == nil {
return pmgsandbox.ProfileSourceUser, name, data, nil
}
return "", "", nil, notFoundError(
fmt.Sprintf("sandbox profile not found: %s", name),
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.",
)
}