Skip to content

Commit

Permalink
resolve plugins to one before passing to cmd setup function
Browse files Browse the repository at this point in the history
  • Loading branch information
estroz committed May 31, 2020
1 parent d658fa2 commit 9eada89
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 301 deletions.
65 changes: 0 additions & 65 deletions internal/config/config_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,74 +21,9 @@ import (

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/spf13/pflag"

internalconfig "sigs.k8s.io/kubebuilder/internal/config"
"sigs.k8s.io/kubebuilder/pkg/model/config"
"sigs.k8s.io/kubebuilder/pkg/plugin"
)

func TestCLI(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}

// Test plugin types and constructors.
type mockPlugin struct {
name, version string
projectVersions []string
}

func (p mockPlugin) Name() string { return p.name }
func (p mockPlugin) Version() string { return p.version }
func (p mockPlugin) SupportedProjectVersions() []string { return p.projectVersions }

func (mockPlugin) UpdateContext(*plugin.Context) {}
func (mockPlugin) BindFlags(*pflag.FlagSet) {}
func (mockPlugin) InjectConfig(*config.Config) {}
func (mockPlugin) Run() error { return nil }

func makeBasePlugin(name, version string, projVers ...string) plugin.Base {
return mockPlugin{name, version, projVers}
}

func makePluginsForKeys(keys ...string) (plugins []plugin.Base) {
for _, key := range keys {
n, v := plugin.SplitKey(key)
plugins = append(plugins, makeBasePlugin(n, v, internalconfig.DefaultVersion))
}
return
}

func makeKeysForPlugins(plugins ...plugin.Base) (keys []string) {
for _, p := range plugins {
keys = append(keys, plugin.KeyFor(p))
}
return
}

type mockAllPlugin struct {
mockPlugin
mockInitPlugin
mockCreateAPIPlugin
mockCreateWebhookPlugin
}

type mockInitPlugin struct{ mockPlugin }
type mockCreateAPIPlugin struct{ mockPlugin }
type mockCreateWebhookPlugin struct{ mockPlugin }

func (p mockInitPlugin) GetInitPlugin() plugin.Init { return p }
func (p mockCreateAPIPlugin) GetCreateAPIPlugin() plugin.CreateAPI { return p }
func (p mockCreateWebhookPlugin) GetCreateWebhookPlugin() plugin.CreateWebhook { return p }

func makeAllPlugin(name, version string, projectVersions ...string) plugin.Base {
p := makeBasePlugin(name, version, projectVersions...).(mockPlugin)
return mockAllPlugin{
p,
mockInitPlugin{p},
mockCreateAPIPlugin{p},
mockCreateWebhookPlugin{p},
}
}
9 changes: 7 additions & 2 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func WithPlugins(plugins ...plugin.Base) Option {
func WithDefaultPlugins(plugins ...plugin.Base) Option {
return func(c *cli) error {
for _, p := range plugins {
// NB(estroz): consider only allowing one default plugin per project version.
for _, version := range p.SupportedProjectVersions() {
c.defaultPluginsFromOptions[version] = append(c.defaultPluginsFromOptions[version], p)
}
Expand Down Expand Up @@ -212,19 +213,23 @@ func (c *cli) initialize() error {
// layout and --plugins values can be short (ex. "go/v2.0.0") or unversioned
// (ex. "go.kubebuilder.io") keys or both, their values may need to be
// resolved to known plugins by key.
//
// NB(estroz): running "init --plugins go" when default plugins contain
// exactly one name match for "go" should result in the matching default
// being selected.
plugins := c.pluginsFromOptions[c.projectVersion]
switch {
case c.cliPluginKey != "":
// Filter plugin by keys passed in CLI.
c.resolvedPlugins, err = resolvePluginsByKey(plugins, c.cliPluginKey)
c.resolvedPlugins, err = filterPluginsByKey(plugins, c.cliPluginKey)
case c.configured && projectConfig.IsV3():
// All non-v1 configs must have a layout key. This check will help with
// migration.
if projectConfig.Layout == "" {
return fmt.Errorf("config must have a layout value")
}
// Filter plugin by config's layout value.
c.resolvedPlugins, err = resolvePluginsByKey(plugins, projectConfig.Layout)
c.resolvedPlugins, err = filterPluginsByKey(plugins, projectConfig.Layout)
default:
// Use the default plugins for this project version.
c.resolvedPlugins = c.defaultPluginsFromOptions[c.projectVersion]
Expand Down
29 changes: 17 additions & 12 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,18 @@ var _ = Describe("CLI", func() {
})

It("should return a valid CLI", func() {
By(`setting cliPluginKey to "go"`)
setPluginsFlag("go")
c, err = New(WithDefaultPlugins(pluginAV1), WithPlugins(pluginAV1, pluginBV2))
Expect(err).NotTo(HaveOccurred())
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginBV2)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV1, pluginBV2}))
// TODO(estroz): this case currently fails because "go" is ambiguous.
// However, running "init --plugins go" when default plugins contain
// exactly one name match for "go" should result in the matching default
// being selected. Once that is implemented, uncomment this case.
//
// By(`setting cliPluginKey to "go"`)
// setPluginsFlag("go")
// c, err = New(WithDefaultPlugins(pluginAV1), WithPlugins(pluginAV1, pluginAV2))
// Expect(err).NotTo(HaveOccurred())
// Expect(c).NotTo(BeNil())
// Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginAV2)))
// Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV2}))

By(`setting cliPluginKey to "go/v1"`)
setPluginsFlag("go/v1")
Expand Down Expand Up @@ -145,15 +150,15 @@ var _ = Describe("CLI", func() {
By(`setting cliPluginKey to an non-existent key "foo"`)
setPluginsFlag("foo")
_, err = New(WithDefaultPlugins(pluginAV1), WithPlugins(pluginAV1, pluginAV2))
Expect(err).To(MatchError(`ambiguous plugin name "foo": no names match`))
Expect(err).To(MatchError(errAmbiguousPlugin{"foo", "no names match"}))

By(`setting cliPluginKey to an ambiguous key "go"`)
setPluginsFlag("go")
c, err = New(WithDefaultPlugins(pluginAV1), WithPlugins(allPlugins...))
Expect(err).NotTo(HaveOccurred())
Expect(c.(*cli).resolvedPlugins).To(Equal(allPlugins))
Expect(c.Run()).To(MatchError(`duplicate initialization plugins for project version "3-alpha": ` +
"go.example.com/v1.0, go.example.com/v2.0"))
Expect(err).To(MatchError(errAmbiguousPlugin{
key: "go",
msg: `possible keys: ["go.example.com/v1.0" "go.example.com/v2.0" "go.test.com/v1.0" "go.test.com/v2.0"]`,
}))
})
})

Expand Down
199 changes: 199 additions & 0 deletions pkg/cli/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
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"
"sort"

"github.com/blang/semver"

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

type errAmbiguousPlugin struct {
key, msg string
}

func (e errAmbiguousPlugin) Error() string {
return fmt.Sprintf("ambiguous plugin %q: %s", e.key, e.msg)
}

// filterPluginsByKey filters versionedPlugins for pluginKey by searching for
// matches to some form of pluginKey. Those forms can be a:
// - Fully qualified key: "go.kubebuilder.io/v2.0"
// - Short key: "go/v2.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" and "go.kubebuilder.io/v2.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.
func filterPluginsByKey(versionedPlugins []plugin.Base, pluginKey string) (filtered []plugin.Base, err error) {

name, version := plugin.SplitKey(pluginKey)

// Compare versions first to narrow the list of name comparisons.
if version == "" {
// Case: if plugin key has no version, check all plugin names.
filtered = versionedPlugins
} else {
// Case: if plugin key has version, filter by version.
filtered = findPluginsMatchingVersion(versionedPlugins, version)
}

if len(filtered) == 0 {
return nil, errAmbiguousPlugin{pluginKey, "no versions match"}
}

// Compare names, taking into account whether name is fully-qualified or not.
shortName := plugin.GetShortName(name)
if name == shortName {
// Case: if plugin name is short, find matching short names.
filtered = findPluginsMatchingShortName(filtered, shortName)
} else {
// Case: if plugin name is fully-qualified, match only fully-qualified names.
filtered = findPluginsMatchingName(filtered, name)
}

if len(filtered) == 0 {
return nil, errAmbiguousPlugin{pluginKey, "no names match"}
}

// Since plugins has already been filtered for matching names and versions,
// it should only contain one matching value for a versionless pluginKey if
// it isn't ambiguous.
if version == "" {
if len(filtered) == 1 {
return filtered, nil
}
return nil, errAmbiguousPlugin{pluginKey, fmt.Sprintf("possible keys: %+q", makePluginKeySlice(filtered))}
}

resolved, err := resolveToPlugin(filtered)
if err != nil {
return nil, errAmbiguousPlugin{pluginKey, err.Error()}
}
return []plugin.Base{resolved}, nil
}

// findPluginsMatchingVersion returns a set of plugins with Version() matching
// version. The set will contain plugins with either major and minor versions
// matching exactly or major versions matching exactly and greater minor versions,
// but not a mix of the two match types.
func findPluginsMatchingVersion(plugins []plugin.Base, version string) []plugin.Base {
// Assume versions have been validated already.
v := must(semver.ParseTolerant(version))

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

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

// must wraps semver.Parse and panics if err is non-nil.
func must(v semver.Version, err error) semver.Version {
if err != nil {
panic(err)
}
return v
}

// findPluginsMatchingName returns a set of plugins with Name() exactly
// matching name.
func findPluginsMatchingName(plugins []plugin.Base, name string) (equal []plugin.Base) {
for _, p := range plugins {
if p.Name() == name {
equal = append(equal, p)
}
}
return equal
}

// findPluginsMatchingShortName returns a set of plugins with
// GetShortName(Name()) exactly matching shortName.
func findPluginsMatchingShortName(plugins []plugin.Base, shortName string) (equal []plugin.Base) {
for _, p := range plugins {
if plugin.GetShortName(p.Name()) == shortName {
equal = append(equal, p)
}
}
return equal
}

func resolveToPlugin(plugins []plugin.Base) (rp plugin.Base, err error) {
// Versions are either an exact match or have greater minor versions, so
// we choose the last in a sorted list of versions to get the correct one.
versions := make([]semver.Version, len(plugins))
for i, p := range plugins {
versions[i] = must(semver.ParseTolerant(p.Version()))
}

if len(versions) == 0 {
return nil, fmt.Errorf("possible versions: %+q", versions)
}

semver.Sort(versions)
useVersion := versions[len(versions)-1]

// If more than one name in plugins exists, the name portion of pluginKey
// needs to be more specific.
nameSet := make(map[string]struct{})
for _, p := range plugins {
nameSet[p.Name()] = struct{}{}
// This condition will only be true once for an unambiguous plugin name,
// since plugin keys have been checked for duplicates already.
if must(semver.ParseTolerant(p.Version())).Equals(useVersion) {
rp = p
}
}
if len(nameSet) != 1 {
return nil, fmt.Errorf("possible names: %+q", makeKeySlice(nameSet))
}

return rp, nil
}

// makeKeySlice returns a slice of all map keys in set.
func makeKeySlice(set map[string]struct{}) (keys []string) {
for key := range set {
keys = append(keys, key)
}
sort.Strings(keys)
return
}

// makePluginKeySlice returns a slice of all keys for each plugin in plugins.
func makePluginKeySlice(plugins []plugin.Base) (keys []string) {
for _, p := range plugins {
keys = append(keys, plugin.KeyFor(p))
}
sort.Strings(keys)
return
}
Loading

0 comments on commit 9eada89

Please sign in to comment.