Skip to content

Commit

Permalink
Multiple plugins with the same name are now allowed by resolvePlugins…
Browse files Browse the repository at this point in the history
…ByKey

to permit a CLI to understand multiple plugin versions. For example,
a config with "layout: go/v2.0" and another config with "layout: go/v2.1"
should both be understood by a particular CLI.

pkg/cli: remove validations requiring plugin names be unique within a CLI,
and resolve all matching plugins such that they can contain multiple
plugins with the same name. As long as there is only one plugin type,
ex. Init, per set of resolved plugins, no errors will occur.
  • Loading branch information
estroz committed May 30, 2020
1 parent 9eb8673 commit 7b2a309
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 178 deletions.
73 changes: 2 additions & 71 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,6 @@ func WithPlugins(plugins ...plugin.Base) Option {
return func(c *cli) error {
for _, p := range plugins {
for _, version := range p.SupportedProjectVersions() {
if _, ok := c.pluginsFromOptions[version]; !ok {
c.pluginsFromOptions[version] = []plugin.Base{}
}
c.pluginsFromOptions[version] = append(c.pluginsFromOptions[version], p)
}
}
Expand All @@ -151,9 +148,6 @@ func WithDefaultPlugins(plugins ...plugin.Base) Option {
return func(c *cli) error {
for _, p := range plugins {
for _, version := range p.SupportedProjectVersions() {
if _, ok := c.defaultPluginsFromOptions[version]; !ok {
c.defaultPluginsFromOptions[version] = []plugin.Base{}
}
c.defaultPluginsFromOptions[version] = append(c.defaultPluginsFromOptions[version], p)
}
}
Expand Down Expand Up @@ -196,8 +190,8 @@ func (c *cli) initialize() error {
c.projectVersion = projectConfig.Version

if projectConfig.IsV1() {
return fmt.Errorf(noticeColor, "The v1 projects are no longer supported.\n"+
"See how to upgrade your project to v2: https://book.kubebuilder.io/migration/guide.html\n")
return fmt.Errorf(noticeColor, "project version 1 is no longer supported.\n"+
"See how to upgrade your project: https://book.kubebuilder.io/migration/guide.html\n")
}
} else {
return fmt.Errorf("failed to read config: %v", err)
Expand Down Expand Up @@ -334,7 +328,6 @@ func (c cli) validate() error {

// validatePlugins validates the name and versions of a list of plugins.
func validatePlugins(plugins ...plugin.Base) error {
pluginNameSet := make(map[string]struct{}, len(plugins))
for _, p := range plugins {
pluginName := p.Name()
if err := plugin.ValidateName(pluginName); err != nil {
Expand All @@ -351,12 +344,6 @@ func validatePlugins(plugins ...plugin.Base) error {
pluginName, projectVersion, err)
}
}
// Check for duplicate plugin names. Names outside of a version can
// conflict because multiple project versions of a plugin may exist.
if _, seen := pluginNameSet[pluginName]; seen {
return fmt.Errorf("two plugins have the same name: %q", pluginName)
}
pluginNameSet[pluginName] = struct{}{}
}
return nil
}
Expand Down Expand Up @@ -389,62 +376,6 @@ func (c cli) buildRootCmd() *cobra.Command {
return rootCmd
}

// resolvePluginsByKey finds a plugin for pluginKey if it exactly matches
// some form of a known plugin's key. Those forms can be a:
// - Fully qualified key: "go.kubebuilder.io/v2.0.0"
// - Short key: "go/v2.0.0"
// - Fully qualified name: "go.kubebuilder.io"
// - Short name: "go"
// Some of these keys may conflict, ex. the fully-qualified and short names of
// "go.kubebuilder.io/v1.0.0" and "go.kubebuilder.io/v2.0.0" have ambiguous
// unversioned names "go.kubernetes.io" and "go". If pluginKey is ambiguous
// or does not match any known plugin's key, an error is returned.
//
// Note: resolvePluginsByKey returns a slice so initialize() can generalize
// setting default plugins if no pluginKey is set.
func resolvePluginsByKey(versionedPlugins []plugin.Base, pluginKey string) ([]plugin.Base, error) {
// Make a set of all possible key combinations to check pluginKey against.
// If the key is not ambiguous, set a valid pointer to the plugin for that
// key, otherwise set a tombstone so we know it is ambiguous. There will
// always be at least one key per plugin if their names are fully-qualified.
//
// Note: this isn't actually that inefficient compared to a memory-efficient
// solution since we're working with very small N's; it is also very simple.
allPluginKeyCombos := make(map[string]*plugin.Base)
for i, p := range versionedPlugins {
key := plugin.KeyFor(p)
// Short-circuit if we have an exact match.
if key == pluginKey {
return []plugin.Base{p}, nil
}
name := p.Name()
keys := []string{key, name}
if shortName := plugin.GetShortName(name); name != shortName {
keys = append(keys, shortName)
keys = append(keys, plugin.Key(shortName, p.Version()))
}

pp := &versionedPlugins[i]
for _, k := range keys {
if _, hasKey := allPluginKeyCombos[k]; hasKey {
allPluginKeyCombos[k] = nil
} else {
allPluginKeyCombos[k] = pp
}
}
}

pp, hasKey := allPluginKeyCombos[pluginKey]
if !hasKey {
return nil, fmt.Errorf("plugin key %q does not match a known plugin", pluginKey)
}
if pp == nil {
return nil, fmt.Errorf("plugin key %q matches more than one known plugin", pluginKey)
}

return []plugin.Base{*pp}, nil
}

// defaultCommand returns the root command without its subcommands.
func (c cli) defaultCommand() *cobra.Command {
return &cobra.Command{
Expand Down
107 changes: 0 additions & 107 deletions pkg/cli/cli_test.go

This file was deleted.

116 changes: 116 additions & 0 deletions pkg/cli/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cli

import (
"fmt"

"github.com/blang/semver"

"sigs.k8s.io/kubebuilder/pkg/plugin"
)

// resolvePluginsByKey finds a plugin for pluginKey if it exactly matches
// some form of a known plugin's key. Those forms can be a:
// - Fully qualified key: "go.kubebuilder.io/v2.0.0"
// - Short key: "go/v2.0.0"
// - Fully qualified name: "go.kubebuilder.io"
// - Short name: "go"
// Some of these keys may conflict, ex. the fully-qualified and short names of
// "go.kubebuilder.io/v1.0.0" and "go.kubebuilder.io/v2.0.0" have ambiguous
// unversioned names "go.kubernetes.io" and "go". If pluginKey is ambiguous
// or does not match any known plugin's key, an error is returned.
//
// Note: resolvePluginsByKey returns a slice so initialize() can generalize
// setting default plugins if no pluginKey is set.
func resolvePluginsByKey(versionedPlugins []plugin.Base, pluginKey string) (resolved []plugin.Base, err error) {
name, version := plugin.SplitKey(pluginKey)
shortName := plugin.GetShortName(name)

if version == "" {
// Case: if plugin key has no version, check all plugin names.
resolved = versionedPlugins
} else {
// Case: if plugin key has version, filter by version.
resolved = findPluginsMatchingVersion(versionedPlugins, version)
}

if len(resolved) == 0 {
return nil, fmt.Errorf("ambiguous plugin version %q: no versions match", version)
}

if name == shortName {
// Case: if plugin name is short, find matching short names.
resolved = findPluginsMatchingShortName(resolved, shortName)
} else {
// Case: if plugin name is fully-qualified, match only fully-qualified names.
resolved = findPluginsMatchingName(resolved, name)
}

if len(resolved) == 0 {
return nil, fmt.Errorf("ambiguous plugin name %q: no names match", name)
}

return resolved, nil
}

func findPluginsMatchingName(ps []plugin.Base, name string) (equal []plugin.Base) {
for _, p := range ps {
if p.Name() == name {
equal = append(equal, p)
}
}
return equal
}

func findPluginsMatchingShortName(ps []plugin.Base, shortName string) (equal []plugin.Base) {
for _, p := range ps {
if plugin.GetShortName(p.Name()) == shortName {
equal = append(equal, p)
}
}
return equal
}

func findPluginsMatchingVersion(ps []plugin.Base, version string) []plugin.Base {
// Assume versions have been validated already.
sv := must(semver.ParseTolerant(version))

var equal, matchingMajor []plugin.Base
for _, p := range ps {
pv := must(semver.ParseTolerant(p.Version()))
if sv.Major == pv.Major {
if sv.Minor == pv.Minor {
equal = append(equal, p)
} else {
matchingMajor = append(matchingMajor, p)
}
}
}

if len(equal) != 0 {
return equal
}
return matchingMajor
}

func must(v semver.Version, err error) semver.Version {
if err != nil {
panic(err)
}
return v
}
Loading

0 comments on commit 7b2a309

Please sign in to comment.