Skip to content

Commit 316f5ea

Browse files
authored
Merge pull request #11432 from andriyDev/CustomAddonImages
Persist custom addon image/registry settings.
2 parents 8c864b0 + df32362 commit 316f5ea

File tree

14 files changed

+329
-63
lines changed

14 files changed

+329
-63
lines changed

pkg/addons/addons.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,12 @@ https://github.com/kubernetes/minikube/issues/7332`, out.V{"driver_name": cc.Dri
185185
exit.Error(reason.GuestCpConfig, "Error getting primary control plane", err)
186186
}
187187

188+
// Persist images even if the machine is running so starting gets the correct images.
189+
images, customRegistries, err := assets.SelectAndPersistImages(addon, cc)
190+
if err != nil {
191+
exit.Error(reason.HostSaveProfile, "Failed to persist images", err)
192+
}
193+
188194
mName := config.MachineName(*cc, cp)
189195
host, err := machine.LoadHost(api, mName)
190196
if err != nil || !machine.IsRunning(api, mName) {
@@ -224,7 +230,7 @@ https://github.com/kubernetes/minikube/issues/7332`, out.V{"driver_name": cc.Dri
224230
out.WarningT("At least needs control plane nodes to enable addon")
225231
}
226232

227-
data := assets.GenerateTemplateData(addon, cc.KubernetesConfig, networkInfo)
233+
data := assets.GenerateTemplateData(addon, cc.KubernetesConfig, networkInfo, images, customRegistries)
228234
return enableOrDisableAddonInternal(cc, addon, runner, data, enable)
229235
}
230236

pkg/minikube/assets/addons.go

+109-43
Original file line numberDiff line numberDiff line change
@@ -660,8 +660,110 @@ var Addons = map[string]*Addon{
660660
}),
661661
}
662662

663+
// parseMapString creates a map based on `str` which is encoded as <key1>=<value1>,<key2>=<value2>,...
664+
func parseMapString(str string) map[string]string {
665+
mapResult := make(map[string]string)
666+
if str == "" {
667+
return mapResult
668+
}
669+
for _, pairText := range strings.Split(str, ",") {
670+
vals := strings.Split(pairText, "=")
671+
if len(vals) != 2 {
672+
out.WarningT("Ignoring invalid pair entry {{.pair}}", out.V{"pair": pairText})
673+
continue
674+
}
675+
mapResult[vals[0]] = vals[1]
676+
}
677+
return mapResult
678+
}
679+
680+
// mergeMaps creates a map with the union of `sourceMap` and `overrideMap` where collisions take the value of `overrideMap`.
681+
func mergeMaps(sourceMap, overrideMap map[string]string) map[string]string {
682+
result := make(map[string]string)
683+
for name, value := range sourceMap {
684+
result[name] = value
685+
}
686+
for name, value := range overrideMap {
687+
result[name] = value
688+
}
689+
return result
690+
}
691+
692+
// filterKeySpace creates a map of the values in `targetMap` where the keys are also in `keySpace`.
693+
func filterKeySpace(keySpace map[string]string, targetMap map[string]string) map[string]string {
694+
result := make(map[string]string)
695+
for name := range keySpace {
696+
if value, ok := targetMap[name]; ok {
697+
result[name] = value
698+
}
699+
}
700+
return result
701+
}
702+
703+
// overrideDefaults creates a copy of `defaultMap` where `overrideMap` replaces any of its values that `overrideMap` contains.
704+
func overrideDefaults(defaultMap, overrideMap map[string]string) map[string]string {
705+
return mergeMaps(defaultMap, filterKeySpace(defaultMap, overrideMap))
706+
}
707+
708+
// SelectAndPersistImages selects which images to use based on addon default images, previously persisted images, and newly requested images - which are then persisted for future enables.
709+
func SelectAndPersistImages(addon *Addon, cc *config.ClusterConfig) (images, customRegistries map[string]string, err error) {
710+
addonDefaultImages := addon.Images
711+
if addonDefaultImages == nil {
712+
addonDefaultImages = make(map[string]string)
713+
}
714+
715+
// Use previously configured custom images.
716+
images = overrideDefaults(addonDefaultImages, cc.CustomAddonImages)
717+
if viper.IsSet(config.AddonImages) {
718+
// Parse the AddonImages flag if present.
719+
newImages := parseMapString(viper.GetString(config.AddonImages))
720+
for name, image := range newImages {
721+
if image == "" {
722+
out.WarningT("Ignoring empty custom image {{.name}}", out.V{"name": name})
723+
delete(newImages, name)
724+
continue
725+
}
726+
if _, ok := addonDefaultImages[name]; !ok {
727+
out.WarningT("Ignoring unknown custom image {{.name}}", out.V{"name": name})
728+
}
729+
}
730+
// Use newly configured custom images.
731+
images = overrideDefaults(addonDefaultImages, newImages)
732+
// Store custom addon images to be written.
733+
cc.CustomAddonImages = mergeMaps(cc.CustomAddonImages, images)
734+
}
735+
736+
// Use previously configured custom registries.
737+
customRegistries = filterKeySpace(addonDefaultImages, cc.CustomAddonRegistries) // filter by images map because registry map may omit default registry.
738+
if viper.IsSet(config.AddonRegistries) {
739+
// Parse the AddonRegistries flag if present.
740+
customRegistries = parseMapString(viper.GetString(config.AddonRegistries))
741+
for name := range customRegistries {
742+
if _, ok := addonDefaultImages[name]; !ok { // check images map because registry map may omitted default registry
743+
out.WarningT("Ignoring unknown custom registry {{.name}}", out.V{"name": name})
744+
delete(customRegistries, name)
745+
}
746+
}
747+
// Since registry map may omit default registry, any previously set custom registries for these images must be cleared.
748+
for name := range addonDefaultImages {
749+
delete(cc.CustomAddonRegistries, name)
750+
}
751+
// Merge newly set registries into custom addon registries to be written.
752+
cc.CustomAddonRegistries = mergeMaps(cc.CustomAddonRegistries, customRegistries)
753+
}
754+
755+
err = nil
756+
// If images or registries were specified, save the config afterward.
757+
if viper.IsSet(config.AddonImages) || viper.IsSet(config.AddonRegistries) {
758+
// Since these values are only set when a user enables an addon, it is safe to refer to the profile name.
759+
err = config.Write(viper.GetString(config.ProfileName), cc)
760+
// Whether err is nil or not we still return here.
761+
}
762+
return images, customRegistries, err
763+
}
764+
663765
// GenerateTemplateData generates template data for template assets
664-
func GenerateTemplateData(addon *Addon, cfg config.KubernetesConfig, netInfo NetworkInfo) interface{} {
766+
func GenerateTemplateData(addon *Addon, cfg config.KubernetesConfig, netInfo NetworkInfo, images, customRegistries map[string]string) interface{} {
665767

666768
a := runtime.GOARCH
667769
// Some legacy docker images still need the -arch suffix
@@ -670,6 +772,7 @@ func GenerateTemplateData(addon *Addon, cfg config.KubernetesConfig, netInfo Net
670772
if runtime.GOARCH != "amd64" {
671773
ea = "-" + runtime.GOARCH
672774
}
775+
673776
opts := struct {
674777
Arch string
675778
ExoticArch string
@@ -688,58 +791,21 @@ func GenerateTemplateData(addon *Addon, cfg config.KubernetesConfig, netInfo Net
688791
LoadBalancerStartIP: cfg.LoadBalancerStartIP,
689792
LoadBalancerEndIP: cfg.LoadBalancerEndIP,
690793
CustomIngressCert: cfg.CustomIngressCert,
691-
Images: addon.Images,
794+
Images: images,
692795
Registries: addon.Registries,
693-
CustomRegistries: make(map[string]string),
796+
CustomRegistries: customRegistries,
694797
NetworkInfo: make(map[string]string),
695798
}
696799
if opts.ImageRepository != "" && !strings.HasSuffix(opts.ImageRepository, "/") {
697800
opts.ImageRepository += "/"
698801
}
699-
700-
// Network info for generating template
701-
opts.NetworkInfo["ControlPlaneNodeIP"] = netInfo.ControlPlaneNodeIP
702-
opts.NetworkInfo["ControlPlaneNodePort"] = fmt.Sprint(netInfo.ControlPlaneNodePort)
703-
704-
if opts.Images == nil {
705-
opts.Images = make(map[string]string) // Avoid nil access when rendering
706-
}
707-
708-
images := viper.GetString(config.AddonImages)
709-
if images != "" {
710-
for _, image := range strings.Split(images, ",") {
711-
vals := strings.Split(image, "=")
712-
if len(vals) != 2 || vals[1] == "" {
713-
out.WarningT("Ignoring invalid custom image {{.conf}}", out.V{"conf": image})
714-
continue
715-
}
716-
if _, ok := opts.Images[vals[0]]; ok {
717-
opts.Images[vals[0]] = vals[1]
718-
} else {
719-
out.WarningT("Ignoring unknown custom image {{.name}}", out.V{"name": vals[0]})
720-
}
721-
}
722-
}
723-
724802
if opts.Registries == nil {
725803
opts.Registries = make(map[string]string)
726804
}
727805

728-
registries := viper.GetString(config.AddonRegistries)
729-
if registries != "" {
730-
for _, registry := range strings.Split(registries, ",") {
731-
vals := strings.Split(registry, "=")
732-
if len(vals) != 2 {
733-
out.WarningT("Ignoring invalid custom registry {{.conf}}", out.V{"conf": registry})
734-
continue
735-
}
736-
if _, ok := opts.Images[vals[0]]; ok { // check images map because registry map may omitted default registry
737-
opts.CustomRegistries[vals[0]] = vals[1]
738-
} else {
739-
out.WarningT("Ignoring unknown custom registry {{.name}}", out.V{"name": vals[0]})
740-
}
741-
}
742-
}
806+
// Network info for generating template
807+
opts.NetworkInfo["ControlPlaneNodeIP"] = netInfo.ControlPlaneNodeIP
808+
opts.NetworkInfo["ControlPlaneNodePort"] = fmt.Sprint(netInfo.ControlPlaneNodePort)
743809

744810
// Append postfix "/" to registries
745811
for k, v := range opts.Registries {

pkg/minikube/assets/addons_test.go

+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
Copyright 2016 The Kubernetes Authors All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package assets
18+
19+
import "testing"
20+
21+
// mapsEqual returns true if and only if `a` contains all the same pairs as `b`.
22+
func mapsEqual(a, b map[string]string) bool {
23+
for aKey, aValue := range a {
24+
if bValue, ok := b[aKey]; !ok || aValue != bValue {
25+
return false
26+
}
27+
}
28+
29+
for bKey := range b {
30+
if _, ok := a[bKey]; !ok {
31+
return false
32+
}
33+
}
34+
return true
35+
}
36+
37+
func TestParseMapString(t *testing.T) {
38+
cases := map[string]map[string]string{
39+
"Ardvark=1,B=2,Cantaloupe=3": {"Ardvark": "1", "B": "2", "Cantaloupe": "3"},
40+
"A=,B=2,C=": {"A": "", "B": "2", "C": ""},
41+
"": {},
42+
"malformed,good=howdy,manyequals==,": {"good": "howdy"},
43+
}
44+
for actual, expected := range cases {
45+
if parsedMap := parseMapString(actual); !mapsEqual(parsedMap, expected) {
46+
t.Errorf("Parsed map from string \"%s\" differs from expected map: Actual: %v Expected: %v", actual, parsedMap, expected)
47+
}
48+
}
49+
}
50+
51+
func TestMergeMaps(t *testing.T) {
52+
type TestCase struct {
53+
sourceMap map[string]string
54+
overrideMap map[string]string
55+
expectedMap map[string]string
56+
}
57+
cases := []TestCase{
58+
{
59+
sourceMap: map[string]string{"A": "1", "B": "2"},
60+
overrideMap: map[string]string{"B": "7", "C": "3"},
61+
expectedMap: map[string]string{"A": "1", "B": "7", "C": "3"},
62+
},
63+
{
64+
sourceMap: map[string]string{"B": "7", "C": "3"},
65+
overrideMap: map[string]string{"A": "1", "B": "2"},
66+
expectedMap: map[string]string{"A": "1", "B": "2", "C": "3"},
67+
},
68+
{
69+
sourceMap: map[string]string{"B": "7", "C": "3"},
70+
overrideMap: map[string]string{},
71+
expectedMap: map[string]string{"B": "7", "C": "3"},
72+
},
73+
{
74+
sourceMap: map[string]string{},
75+
overrideMap: map[string]string{"B": "7", "C": "3"},
76+
expectedMap: map[string]string{"B": "7", "C": "3"},
77+
},
78+
}
79+
for _, test := range cases {
80+
if actualMap := mergeMaps(test.sourceMap, test.overrideMap); !mapsEqual(actualMap, test.expectedMap) {
81+
t.Errorf("Merging maps (source=%v, override=%v) differs from expected map: Actual: %v Expected: %v", test.sourceMap, test.overrideMap, actualMap, test.expectedMap)
82+
}
83+
}
84+
}
85+
86+
func TestFilterKeySpace(t *testing.T) {
87+
type TestCase struct {
88+
keySpace map[string]string
89+
targetMap map[string]string
90+
expectedMap map[string]string
91+
}
92+
cases := []TestCase{
93+
{
94+
keySpace: map[string]string{"A": "0", "B": ""},
95+
targetMap: map[string]string{"B": "1", "C": "2", "D": "3"},
96+
expectedMap: map[string]string{"B": "1"},
97+
},
98+
{
99+
keySpace: map[string]string{},
100+
targetMap: map[string]string{"B": "1", "C": "2", "D": "3"},
101+
expectedMap: map[string]string{},
102+
},
103+
{
104+
keySpace: map[string]string{"B": "1", "C": "2", "D": "3"},
105+
targetMap: map[string]string{},
106+
expectedMap: map[string]string{},
107+
},
108+
}
109+
for _, test := range cases {
110+
if actualMap := filterKeySpace(test.keySpace, test.targetMap); !mapsEqual(actualMap, test.expectedMap) {
111+
t.Errorf("Filtering keyspace of map (keyspace=%v, target=%v) differs from expected map: Actual: %v Expected: %v", test.keySpace, test.targetMap, actualMap, test.expectedMap)
112+
}
113+
}
114+
}
115+
116+
func TestOverrideDefautls(t *testing.T) {
117+
type TestCase struct {
118+
defaultMap map[string]string
119+
overrideMap map[string]string
120+
expectedMap map[string]string
121+
}
122+
cases := []TestCase{
123+
{
124+
defaultMap: map[string]string{"A": "1", "B": "2", "C": "3"},
125+
overrideMap: map[string]string{"B": "7", "C": "8"},
126+
expectedMap: map[string]string{"A": "1", "B": "7", "C": "8"},
127+
},
128+
{
129+
defaultMap: map[string]string{"A": "1", "B": "2", "C": "3"},
130+
overrideMap: map[string]string{"B": "7", "D": "8", "E": "9"},
131+
expectedMap: map[string]string{"A": "1", "B": "7", "C": "3"},
132+
},
133+
{
134+
defaultMap: map[string]string{"A": "1", "B": "2", "C": "3"},
135+
overrideMap: map[string]string{"B": "7", "D": "8", "E": "9"},
136+
expectedMap: map[string]string{"A": "1", "B": "7", "C": "3"},
137+
},
138+
}
139+
for _, test := range cases {
140+
if actualMap := overrideDefaults(test.defaultMap, test.overrideMap); !mapsEqual(actualMap, test.expectedMap) {
141+
t.Errorf("Override defaults (defaults=%v, overrides=%v) differs from expected map: Actual: %v Expected: %v", test.defaultMap, test.overrideMap, actualMap, test.expectedMap)
142+
}
143+
}
144+
}

pkg/minikube/config/types.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ type ClusterConfig struct {
7474
KubernetesConfig KubernetesConfig
7575
Nodes []Node
7676
Addons map[string]bool
77-
VerifyComponents map[string]bool // map of components to verify and wait for after start.
77+
CustomAddonImages map[string]string // Maps image names to the image to use for addons. e.g. Dashboard -> k8s.gcr.io/echoserver:1.4 makes dashboard addon use echoserver for its Dashboard deployment.
78+
CustomAddonRegistries map[string]string // Maps image names to the registry to use for addons. See CustomAddonImages for example.
79+
VerifyComponents map[string]bool // map of components to verify and wait for after start.
7880
StartHostTimeout time.Duration
7981
ScheduledStop *ScheduledStopConfig
8082
ExposedPorts []string // Only used by the docker and podman driver

site/content/en/docs/contrib/tests.en.md

+3
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,9 @@ runs the initial minikube start
332332
#### validateDeploying
333333
deploys an app the minikube cluster
334334

335+
#### validateEnableAddonWhileActive
336+
makes sure addons can be enabled while cluster is active.
337+
335338
#### validateStop
336339
tests minikube stop
337340

0 commit comments

Comments
 (0)