Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
85f7119
refactor: split plugins.go into domain specific go files and move it …
Glitchy-Sheep May 11, 2026
f678ce9
refactor: remove unused flags
Glitchy-Sheep May 11, 2026
1ded064
doc: add decent doc.go file explaining the concepts of the plugin com…
Glitchy-Sheep May 11, 2026
1a51b3b
refactor: path resolution refactor (move to single source of truth wi…
Glitchy-Sheep May 12, 2026
7e3e58b
refactor: extract plugin install/run/contract helpers
Glitchy-Sheep May 12, 2026
7300114
contract-v2: introduce structured Requirements DTO with smart unmarshal
Glitchy-Sheep May 12, 2026
f8881de
contract-v2: split plugin validators by mandatory/conditional and fix…
Glitchy-Sheep May 12, 2026
a0b5889
contract-v2: log-only stubs for cluster-side requirements
Glitchy-Sheep May 12, 2026
d43be62
contract-v2: drop v1 flat-array compatibility, contract is v2-only (s…
Glitchy-Sheep May 12, 2026
c59bb80
contract-v2: friendly error messages when contract is invalid
Glitchy-Sheep May 12, 2026
c577d76
contract-v2: surface pending-requirement stubs via stderr instead of …
Glitchy-Sheep May 12, 2026
96bc21e
Merge commit 'abd0f76df9ad736e3cbd3b0e878c40e843502cb5' into feat/new…
ldmonster May 13, 2026
30910a8
Merge commit '0e22aa2e980cb9a6bef0f655620380749d95e8e6' into feat/new…
ldmonster May 14, 2026
efa2b50
log warning when plugin requirement not installed
fuldaxxx May 20, 2026
838fd2a
fix: update comment for mandatory plugin requirement validation
fuldaxxx May 21, 2026
30d53ba
[chore] suppress unparam linter warnings in requirement validators
fuldaxxx May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions internal/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,27 @@ type Flag struct {
Name string
}

// Requirements represents plugin dependencies
// Requirements represents plugin dependencies (v2 schema).
//
// v1 schema: flat array of plugin and module requirements
// v2 schema: structured groups (with matchers anyOf, mandatory, conditional) + deckhouse
type Requirements struct {
Kubernetes KubernetesRequirement
Modules []ModuleRequirement
Plugins []PluginRequirement
Deckhouse DeckhouseRequirement
Modules ModuleRequirementsGroup
Plugins PluginRequirementsGroup
}

// KubernetesRequirement represents Kubernetes version constraint
type KubernetesRequirement struct {
Constraint string
}

// DeckhouseRequirement represents a constraint on the Deckhouse cluster version.
type DeckhouseRequirement struct {
Constraint string
}

// ModuleRequirement represents a required Deckhouse module
type ModuleRequirement struct {
Name string
Expand All @@ -59,3 +68,28 @@ type PluginRequirement struct {
Name string
Constraint string
}

// AnyOfGroup represents an "at least one of" group of module requirements.
// Description is used in user-facing error messages.
type AnyOfGroup struct {
Description string
Modules []ModuleRequirement
}

// PluginRequirementsGroup splits plugin requirements into Mandatory and Conditional.
// - Mandatory: the dependent plugin must be installed AND satisfy the constraint.
// - Conditional: only enforced if the dependent plugin is installed; otherwise skipped.
type PluginRequirementsGroup struct {
Mandatory []PluginRequirement
Conditional []PluginRequirement
}

// ModuleRequirementsGroup splits module requirements into Mandatory, Conditional, and AnyOf.
// - Mandatory: the module must be in the cluster AND satisfy the constraint.
// - Conditional: only enforced if the module is in the cluster.
// - AnyOf: at least one module per group must be in the cluster and satisfy its constraint.
type ModuleRequirementsGroup struct {
Mandatory []ModuleRequirement
Conditional []ModuleRequirement
AnyOf []AnyOfGroup
}
264 changes: 208 additions & 56 deletions internal/plugins/cmd/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,32 @@ func (pc *PluginsCommand) fetchLatestVersion(ctx context.Context, pluginName str
type FailedConstraints map[string]*semver.Constraints

func (pc *PluginsCommand) validateRequirements(plugin *internal.Plugin) (FailedConstraints, error) {
// validate plugin requirements
pc.logger.Debug("validating plugin requirements", slog.String("plugin", plugin.Name))

err := pc.validatePluginConflicts(plugin)
if err != nil {
if err := pc.validatePluginConflicts(plugin); err != nil {
return nil, fmt.Errorf("plugin conflicts: %w", err)
}

failedConstraints, err := pc.validatePluginRequirement(plugin)
failedConstraints, err := pc.validatePluginRequirementMandatory(plugin)
if err != nil {
return nil, fmt.Errorf("plugin requirements: %w", err)
return nil, fmt.Errorf("plugin requirements (mandatory): %w", err)
}

// validate module requirements
pc.logger.Debug("validating module requirements", slog.String("plugin", plugin.Name))
if err := pc.validatePluginRequirementConditional(plugin); err != nil {
return nil, fmt.Errorf("plugin requirements (conditional): %w", err)
}

err = pc.validateModuleRequirement(plugin)
if err != nil {
// Cluster-side requirements - currently log-only, no enforcement.
// Real validation lands when cluster connectivity is added.
if err := pc.validateKubernetesRequirement(plugin); err != nil {
return nil, fmt.Errorf("kubernetes requirement: %w", err)
}
if err := pc.validateDeckhouseRequirement(plugin); err != nil {
return nil, fmt.Errorf("deckhouse requirement: %w", err)
}

pc.logger.Debug("validating module requirements", slog.String("plugin", plugin.Name))
if err := pc.validateModuleRequirement(plugin); err != nil {
return nil, fmt.Errorf("module requirements: %w", err)
}

Expand Down Expand Up @@ -189,75 +197,219 @@ func (pc *PluginsCommand) validatePluginConflicts(plugin *internal.Plugin) error
return nil
}

// validatePluginConflict checks whether installing `plugin` violates any
// constraint that the already-installed `installedPlugin` places on it.
//
// Both Mandatory and Conditional sections of installedPlugin's requirements
// are inspected - if an existing plugin requires us, we must satisfy its
// constraint regardless of whether the requirement is mandatory or conditional.
func validatePluginConflict(plugin *internal.Plugin, installedPlugin *internal.Plugin) error {
for _, requirement := range installedPlugin.Requirements.Plugins {
// installed plugin requirement is the same as the plugin we are validating
if requirement.Name == plugin.Name {
constraint, err := semver.NewConstraint(requirement.Constraint)
if err != nil {
return fmt.Errorf("failed to parse constraint: %w", err)
}

version, err := semver.NewVersion(installedPlugin.Version)
if err != nil {
return fmt.Errorf("failed to parse version: %w", err)
}

if !constraint.Check(version) {
return fmt.Errorf("installing plugin %s %s will make conflict with existing plugin %s %s",
plugin.Name,
plugin.Version,
installedPlugin.Name,
constraint.String())
}
candidates := make([]internal.PluginRequirement, 0,
len(installedPlugin.Requirements.Plugins.Mandatory)+len(installedPlugin.Requirements.Plugins.Conditional))
candidates = append(candidates, installedPlugin.Requirements.Plugins.Mandatory...)
candidates = append(candidates, installedPlugin.Requirements.Plugins.Conditional...)

for _, requirement := range candidates {
if requirement.Name != plugin.Name {
continue
}
constraint, err := semver.NewConstraint(requirement.Constraint)
if err != nil {
return fmt.Errorf("failed to parse constraint: %w", err)
}
// Check the NEW plugin's version against the constraint -
// not installedPlugin.Version (that was a long-standing bug).
version, err := semver.NewVersion(plugin.Version)
if err != nil {
return fmt.Errorf("failed to parse version: %w", err)
}
if !constraint.Check(version) {
return fmt.Errorf("installing plugin %s %s conflicts with existing plugin %s which requires %s %s",
plugin.Name,
plugin.Version,
installedPlugin.Name,
plugin.Name,
constraint.String())
}
}

return nil
}

func (pc *PluginsCommand) validatePluginRequirement(plugin *internal.Plugin) (FailedConstraints, error) {
// validatePluginRequirementMandatory enforces mandatory plugin requirements:
//
// For mandatory requirements:

// - if the dependency is not installed, record a soft failure in FailedConstraints
// - if the dependency is installed but fails the constraint, record a soft failure in FailedConstraints
// - return a non-nil error only for operational failures such as install checks,
// version lookup failures, or invalid version constraints
func (pc *PluginsCommand) validatePluginRequirementMandatory(plugin *internal.Plugin) (FailedConstraints, error) {
result := make(FailedConstraints)

for _, pluginRequirement := range plugin.Requirements.Plugins {
// check if plugin is installed
for _, pluginRequirement := range plugin.Requirements.Plugins.Mandatory {
installed, err := pc.checkInstalled(pluginRequirement.Name)
if err != nil {
return nil, fmt.Errorf("failed to check if plugin is installed: %w", err)
}
if !installed {
pc.logger.Warn("plugin requirement not installed",
slog.String("plugin", plugin.Name),
slog.String("requirement", pluginRequirement.Name))
result[pluginRequirement.Name] = nil
continue
}

// check constraint
if pluginRequirement.Constraint != "" {
installedVersion, err := pc.getInstalledPluginVersion(pluginRequirement.Name)
if err != nil {
return nil, fmt.Errorf("failed to get installed version: %w", err)
}

constraint, err := semver.NewConstraint(pluginRequirement.Constraint)
if err != nil {
return nil, fmt.Errorf("failed to parse constraint: %w", err)
}

if !constraint.Check(installedVersion) {
pc.logger.Warn("plugin requirement not satisfied",
slog.String("plugin", plugin.Name),
slog.String("requirement", pluginRequirement.Name),
slog.String("constraint", pluginRequirement.Constraint),
slog.String("installedVersion", installedVersion.Original()))

result[pluginRequirement.Name] = constraint
}
if pluginRequirement.Constraint == "" {
continue
}
installedVersion, err := pc.getInstalledPluginVersion(pluginRequirement.Name)
if err != nil {
return nil, fmt.Errorf("failed to get installed version: %w", err)
}
constraint, err := semver.NewConstraint(pluginRequirement.Constraint)
if err != nil {
return nil, fmt.Errorf("failed to parse constraint: %w", err)
}
if !constraint.Check(installedVersion) {
pc.logger.Warn("plugin requirement not satisfied",
slog.String("plugin", plugin.Name),
slog.String("requirement", pluginRequirement.Name),
slog.String("constraint", pluginRequirement.Constraint),
slog.String("installedVersion", installedVersion.Original()))
result[pluginRequirement.Name] = constraint
}
}

return result, nil
}

func (pc *PluginsCommand) validateModuleRequirement(_ *internal.Plugin) error {
// TODO: Implement module requirement validation
// validatePluginRequirementConditional enforces conditional plugin requirements:
//
// For conditional requirements:
// - if the dependency is not installed, skip silently;
// - if the dependency is installed but fails the constraint, return a hard error
func (pc *PluginsCommand) validatePluginRequirementConditional(plugin *internal.Plugin) error {
for _, pluginRequirement := range plugin.Requirements.Plugins.Conditional {
installed, err := pc.checkInstalled(pluginRequirement.Name)
if err != nil {
return fmt.Errorf("failed to check if plugin is installed: %w", err)
}
if !installed {
continue
}
if pluginRequirement.Constraint == "" {
continue
}
installedVersion, err := pc.getInstalledPluginVersion(pluginRequirement.Name)
if err != nil {
return fmt.Errorf("failed to get installed version: %w", err)
}
constraint, err := semver.NewConstraint(pluginRequirement.Constraint)
if err != nil {
return fmt.Errorf("failed to parse constraint: %w", err)
}
if !constraint.Check(installedVersion) {
return fmt.Errorf("conditional plugin requirement not satisfied: plugin %s %s installed but %s requires %s",
pluginRequirement.Name,
installedVersion.Original(),
plugin.Name,
pluginRequirement.Constraint)
}
}
return nil
}

// debugPrintPendingRequirement prints a human-readable warning for a declared
// requirement that d8 currently surfaces but does not enforce.
//
// Until the requirement enforcement is implemented, we print a warning to the user.
//
// Example:
//
// ! <title>
// key1: value1
// key2: value2
func debugPrintPendingRequirement(title string, kv ...[2]string) {
fmt.Fprintf(os.Stderr, "! %s\n", title)
for _, p := range kv {
fmt.Fprintf(os.Stderr, " %s: %s\n", p[0], p[1])
}
}

// validateKubernetesRequirement is a log-only stub (yet).
//
// For Kubernetes requirement:
// - if the constraint is empty, skip silently
// - if the constraint is not empty, print a warning
//
//nolint:unparam // stub — will return real errors once enforcement is implemented
func (pc *PluginsCommand) validateKubernetesRequirement(plugin *internal.Plugin) error {
if plugin.Requirements.Kubernetes.Constraint == "" {
return nil
}
debugPrintPendingRequirement(
"plugin declares a Kubernetes version requirement but enforcement is not implemented yet",
[2]string{"plugin", plugin.Name},
[2]string{"constraint", plugin.Requirements.Kubernetes.Constraint},
)
return nil
}

// validateDeckhouseRequirement is a log-only stub. See validateKubernetesRequirement
// for rationale; enforcement will land once Deckhouse version discovery is in place.
//
//nolint:unparam // stub — will return real errors once enforcement is implemented
func (pc *PluginsCommand) validateDeckhouseRequirement(plugin *internal.Plugin) error {
if plugin.Requirements.Deckhouse.Constraint == "" {
return nil
}
debugPrintPendingRequirement(
"plugin declares a Deckhouse version requirement but enforcement is not implemented yet",
[2]string{"plugin", plugin.Name},
[2]string{"constraint", plugin.Requirements.Deckhouse.Constraint},
)
return nil
}

// validateModuleRequirement is a log-only stub. Mandatory, Conditional and
// AnyOf sections are all surfaced so authors and operators see the declared
// expectations even though d8 does not yet inspect the cluster to verify them.
//
//nolint:unparam // stub — will return real errors once enforcement is implemented
func (pc *PluginsCommand) validateModuleRequirement(plugin *internal.Plugin) error {
mods := plugin.Requirements.Modules
if len(mods.Mandatory) == 0 && len(mods.Conditional) == 0 && len(mods.AnyOf) == 0 {
return nil
}

for _, m := range mods.Mandatory {
debugPrintPendingRequirement(
"plugin declares a mandatory module requirement but enforcement is not implemented yet",
[2]string{"plugin", plugin.Name},
[2]string{"module", m.Name},
[2]string{"constraint", m.Constraint},
)
}
for _, m := range mods.Conditional {
debugPrintPendingRequirement(
"plugin declares a conditional module requirement but enforcement is not implemented yet",
[2]string{"plugin", plugin.Name},
[2]string{"module", m.Name},
[2]string{"constraint", m.Constraint},
)
}
for i, grp := range mods.AnyOf {
names := make([]string, 0, len(grp.Modules))
for _, m := range grp.Modules {
names = append(names, m.Name)
}
debugPrintPendingRequirement(
"plugin declares an anyOf module group but enforcement is not implemented yet",
[2]string{"plugin", plugin.Name},
[2]string{"group_index", fmt.Sprintf("%d", i)},
[2]string{"group_description", grp.Description},
[2]string{"modules", strings.Join(names, ", ")},
)
}
return nil
}
Loading
Loading