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 74c039d
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 238 deletions.
94 changes: 0 additions & 94 deletions internal/config/config_suite_test.go

This file was deleted.

4 changes: 2 additions & 2 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ func (c *cli) initialize() error {
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
8 changes: 8 additions & 0 deletions pkg/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,27 +51,31 @@ var _ = Describe("CLI", func() {
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV1}))
Expect(c.Run()).NotTo(HaveOccurred())

By("setting two plugins with different names and versions")
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}))
Expect(c.Run()).NotTo(HaveOccurred())

By("setting two plugins with the same names and different versions")
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{pluginAV1}))
Expect(c.Run()).NotTo(HaveOccurred())

By("setting two plugins with different names and the same version")
c, err = New(WithDefaultPlugins(pluginAV1), WithPlugins(pluginAV1, pluginBV1))
Expect(err).NotTo(HaveOccurred())
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginBV1)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV1}))
Expect(c.Run()).NotTo(HaveOccurred())
})

It("should return an error", func() {
Expand Down Expand Up @@ -115,6 +119,7 @@ var _ = Describe("CLI", func() {
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginBV2)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV1, pluginBV2}))
Expect(c.Run()).NotTo(HaveOccurred())

By(`setting cliPluginKey to "go/v1"`)
setPluginsFlag("go/v1")
Expand All @@ -123,6 +128,7 @@ var _ = Describe("CLI", func() {
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginBV2)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginAV1}))
Expect(c.Run()).NotTo(HaveOccurred())

By(`setting cliPluginKey to "go/v2"`)
setPluginsFlag("go/v2")
Expand All @@ -131,6 +137,7 @@ var _ = Describe("CLI", func() {
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(pluginAV1, pluginBV2)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginBV2}))
Expect(c.Run()).NotTo(HaveOccurred())

By(`setting cliPluginKey to "go.test.com/v2"`)
setPluginsFlag("go.test.com/v2")
Expand All @@ -139,6 +146,7 @@ var _ = Describe("CLI", func() {
Expect(c).NotTo(BeNil())
Expect(c.(*cli).pluginsFromOptions).To(Equal(makeSetByProjVer(allPlugins...)))
Expect(c.(*cli).resolvedPlugins).To(Equal([]plugin.Base{pluginBV2}))
Expect(c.Run()).NotTo(HaveOccurred())
})

It("should return an error", func() {
Expand Down
196 changes: 196 additions & 0 deletions pkg/cli/plugins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
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"
)

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)
}
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))
}
return
}
Loading

0 comments on commit 74c039d

Please sign in to comment.