hashicorp/nomad · error · ErrPluginNotExists

%w: %q

Error message

%w: %q

What it means

NewHostVolumePluginExternal validates the plugin executable before use. It opens the plugin directory with os.OpenRoot as a safety check so the plugin ID cannot traverse outside the directory, and wraps ErrPluginNotExists with the filename if OpenRoot itself fails. This means the configured plugin directory could not be opened at all (missing directory, permission denied, or not a directory). The comment also notes that Stat errors below are intentionally obscured as ErrPluginNotExists to avoid leaking path-escape details.

Source

Thrown at client/hostvolumemanager/host_volume_plugin.go:232

	}

	log.Debug("plugin ran successfully")
	return nil
}

var _ HostVolumePlugin = &HostVolumePluginExternal{}

// NewHostVolumePluginExternal returns an external host volume plugin
// if the specified executable exists on disk.
func NewHostVolumePluginExternal(log hclog.Logger,
	pluginDir, filename, volumesDir, nodePool string) (*HostVolumePluginExternal, error) {
	// this should only be called with already-detected executables, but we'll
	// double-check it anyway, so we can provide a tidy error message if it has
	// changed between fingerprinting and execution and to ensure that the
	// plugin ID can't traverse outside the plugin directory.
	root, err := os.OpenRoot(pluginDir)
	if err != nil {
		return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, filename)
	}

	f, err := root.Stat(filename)
	if err != nil {
		// note we intentionally obscure the root-escape error here and return a
		// ErrPluginNotExists, because there's no legitimate reason to ever get
		// this error
		return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, filename)
	}
	if !helper.IsExecutable(f) {
		return nil, fmt.Errorf("%w: %q", ErrPluginNotExecutable, filename)
	}
	executable := filepath.Join(pluginDir, filename)

	return &HostVolumePluginExternal{
		ID:         filename,
		Executable: executable,
		VolumesDir: volumesDir,

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Verify plugin_dir exists and is a directory accessible to the nomad client user (ls -ld pluginDir); create it or fix the config path.
  2. Fix directory permissions so the nomad user can traverse it (chmod/chown).
  3. If the directory is on a mount (NFS/EBS), confirm the mount succeeded before agent start or add a startup dependency.
  4. Match errors.Is(err, ErrPluginNotExists) in calling code and re-check fingerprint results — the executable may have been removed between fingerprinting and execution.

Example fix

// before: config references a missing dir
plugin_dir = "/opt/nomad/plugins-ext"  // never created

// after
// sudo mkdir -p /opt/nomad/plugins-ext && sudo chown nomad:nomad /opt/nomad/plugins-ext
plugin_dir = "/opt/nomad/plugins-ext"
Defensive patterns

Strategy: validation

Validate before calling

func validatePluginDir(pluginDir string) error {
    fi, err := os.Stat(pluginDir)
    if os.IsNotExist(err) {
        return fmt.Errorf("plugin dir %s does not exist", pluginDir)
    }
    if err != nil {
        return err
    }
    if !fi.IsDir() {
        return fmt.Errorf("%s is not a directory", pluginDir)
    }
    if err := unix.Access(pluginDir, unix.X_OK); err != nil {
        return fmt.Errorf("no search permission on %s", pluginDir)
    }
    return nil
}

Try / catch

if errors.Is(err, ErrPluginNotExists) {
    // plugin dir missing/unreadable or plugin vanished since fingerprint:
    // re-run fingerprint and re-check client plugin_dir config
}

Prevention

When it happens

Trigger: Calling NewHostVolumePluginExternal(pluginDir, filename) (directly or via the client's getPlugin after fingerprinting) when pluginDir does not exist, is not a directory, or the nomad client process lacks permission to open it — errors.Is(err, ErrPluginNotExists) will be true.

Common situations: Client config plugin_dir points to a path that was deleted or never created; the nomad user lacks execute/search permission on the directory; plugin_dir configured as a file instead of a directory; plugin directory on a mount that failed to mount at boot.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/c2f056558e81f785. Report an issue: GitHub.