Skip to content

Commit 1701731

Browse files
authored
fix(plugins): properly handle version preference in Sanitize, add CRD plugin validations (#2162)
* fix(plugins): pick the latest plugin version in sanitize * feat(plugins): add update method * feat(plugins): implement PluginMap * chore(plugins): switch Sanitize to PluginMap * chore(controllers): switch plugin reconciler to PluginMap * chore(plugins): deprecate unused methods for PluginList * chore(plugins): do not return errors for Update method Versions will be validated at a CRD level * chore(plugins): add CRD validation for plugins * chore: regenerate CRDs * chore(plugins): update comments * chore(plugins): add another test case for plugin update * chore(plugins): update comments * chore(plugins): deprecate `Update` method * chore(plugins): improve variable naming in Reconcile * fix(plugins): logging line * fix(plugins): return sorted list in GetPluginList
1 parent 8d72035 commit 1701731

11 files changed

+289
-411
lines changed

api/v1beta1/plugin_list.go

Lines changed: 77 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ package v1beta1
22

33
import (
44
"fmt"
5+
"maps"
6+
"slices"
57
"sort"
68
"strings"
79

810
"github.com/blang/semver/v4"
911
)
1012

1113
type GrafanaPlugin struct {
12-
Name string `json:"name"`
14+
// +kubebuilder:validation:MinLength=1
15+
Name string `json:"name"`
16+
// TODO: kubernetes 1.34+ supports isSemver function, we should migrate to it after 1.33 reaches EOL. For now, using the official pattern https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
17+
// +kubebuilder:validation:Pattern=`^((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?|latest)$`
1318
Version string `json:"version"`
1419
}
1520

@@ -35,100 +40,106 @@ func (p GrafanaPlugin) String() string {
3540
return fmt.Sprintf("%s %s", p.Name, p.Version)
3641
}
3742

38-
type PluginList []GrafanaPlugin
43+
// Update updates the plugin to the requested version if it's valid and newer
44+
func (p *GrafanaPlugin) Update(version string) {
45+
if p.Version == version {
46+
return
47+
}
3948

40-
type PluginMap map[string]PluginList
49+
if version == "" {
50+
return
51+
}
4152

42-
func (l PluginList) String() string {
43-
plugins := make(sort.StringSlice, 0, len(l))
53+
if p.Version == PluginVersionLatest {
54+
return
55+
}
4456

45-
for _, plugin := range l {
46-
plugins = append(plugins, plugin.String())
57+
if version == PluginVersionLatest {
58+
p.Version = version
59+
60+
return
4761
}
4862

49-
sort.Sort(plugins)
63+
// NOTE: We should not see that happening due to CRD validations
64+
// Version is not valid, so don't do anything
65+
requestedVersion, err := semver.Parse(version)
66+
if err != nil {
67+
return
68+
}
5069

51-
return strings.Join(plugins, ",")
52-
}
70+
// NOTE: We should not see that happening due to CRD validations
71+
// Helps to recover in case we have previously stored invalid version
72+
listedVersion, err := semver.Parse(p.Version)
73+
if err != nil {
74+
p.Version = version
75+
return
76+
}
5377

54-
// Update update plugin version
55-
func (l PluginList) Update(plugin *GrafanaPlugin) {
56-
for i, installedPlugin := range l {
57-
if installedPlugin.Name == plugin.Name {
58-
l[i].Version = plugin.Version
59-
break
60-
}
78+
if listedVersion.Compare(requestedVersion) == -1 {
79+
p.Version = version
80+
81+
return
6182
}
6283
}
6384

64-
// Sanitize remove duplicates and enforce semver
65-
func (l PluginList) Sanitize() PluginList {
66-
var sanitized PluginList
85+
// Helps to simplify version consolidation
86+
type PluginMap map[string]GrafanaPlugin
6787

68-
for _, plugin := range l {
69-
if plugin.HasInvalidVersion() {
88+
func (m PluginMap) Merge(plugins PluginList) {
89+
for _, p := range plugins {
90+
if p.HasInvalidVersion() {
7091
continue
7192
}
7293

73-
if !sanitized.HasSomeVersionOf(&plugin) {
74-
sanitized = append(sanitized, plugin)
94+
if plugin, ok := m[p.Name]; ok {
95+
plugin.Update(p.Version)
96+
m[p.Name] = plugin
97+
} else {
98+
m[p.Name] = p
7599
}
76100
}
77-
78-
return sanitized
79101
}
80102

81-
// HasSomeVersionOf returns true if the list contains the same plugin in the exact or a different version
82-
func (l PluginList) HasSomeVersionOf(plugin *GrafanaPlugin) bool {
83-
for _, listedPlugin := range l {
84-
if listedPlugin.Name == plugin.Name {
85-
return true
86-
}
87-
}
103+
func (m PluginMap) GetPluginList() PluginList {
104+
plugins := slices.Collect(maps.Values(m))
105+
slices.SortFunc(plugins, func(a, b GrafanaPlugin) int {
106+
return strings.Compare(a.Name, b.Name)
107+
})
88108

89-
return false
109+
return plugins
90110
}
91111

92-
// HasExactVersionOf returns true if the list contains the same plugin in the same version
93-
func (l PluginList) HasExactVersionOf(plugin *GrafanaPlugin) bool {
94-
for _, listedPlugin := range l {
95-
if listedPlugin.Name == plugin.Name && listedPlugin.Version == plugin.Version {
96-
return true
97-
}
98-
}
112+
func NewPluginMap() PluginMap {
113+
pm := PluginMap{}
99114

100-
return false
115+
return pm
101116
}
102117

103-
// HasNewerVersionOf returns true if the list contains the same plugin but in a newer version
104-
func (l PluginList) HasNewerVersionOf(plugin *GrafanaPlugin) (bool, error) {
105-
for _, listedPlugin := range l {
106-
if listedPlugin.Name != plugin.Name {
107-
continue
108-
}
118+
func NewPluginMapFromList(plugins PluginList) PluginMap {
119+
pm := PluginMap{}
109120

110-
if listedPlugin.Version == PluginVersionLatest && plugin.Version != PluginVersionLatest {
111-
return true, nil
112-
}
121+
pm.Merge(plugins)
113122

114-
if plugin.Version == PluginVersionLatest {
115-
return false, nil
116-
}
123+
return pm
124+
}
117125

118-
listedVersion, err := semver.Parse(listedPlugin.Version)
119-
if err != nil {
120-
return false, err
121-
}
126+
type PluginList []GrafanaPlugin
122127

123-
requestedVersion, err := semver.Parse(plugin.Version)
124-
if err != nil {
125-
return false, err
126-
}
128+
func (l PluginList) String() string {
129+
plugins := make(sort.StringSlice, 0, len(l))
127130

128-
if listedVersion.Compare(requestedVersion) == 1 {
129-
return true, nil
130-
}
131+
for _, plugin := range l {
132+
plugins = append(plugins, plugin.String())
131133
}
132134

133-
return false, nil
135+
sort.Sort(plugins)
136+
137+
return strings.Join(plugins, ",")
138+
}
139+
140+
// Sanitize remove duplicates and enforce semver
141+
func (l PluginList) Sanitize() PluginList {
142+
plugins := NewPluginMapFromList(l)
143+
144+
return plugins.GetPluginList()
134145
}

0 commit comments

Comments
 (0)