Skip to content

Commit 7f09e46

Browse files
committed
add docker-image command for backwards compatibility
1 parent 321e06e commit 7f09e46

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
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 load
18+
19+
import (
20+
"fmt"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/spf13/cobra"
26+
27+
"sigs.k8s.io/kind/pkg/cluster"
28+
"sigs.k8s.io/kind/pkg/cluster/nodes"
29+
"sigs.k8s.io/kind/pkg/cluster/nodeutils"
30+
"sigs.k8s.io/kind/pkg/cmd"
31+
"sigs.k8s.io/kind/pkg/errors"
32+
"sigs.k8s.io/kind/pkg/fs"
33+
"sigs.k8s.io/kind/pkg/log"
34+
35+
"sigs.k8s.io/kind/pkg/internal/cli"
36+
"sigs.k8s.io/kind/pkg/internal/runtime"
37+
)
38+
39+
type (
40+
imageTagFetcher func(nodes.Node, string) (map[string]bool, error)
41+
)
42+
43+
type flagpole struct {
44+
Name string
45+
Nodes []string
46+
}
47+
48+
// NewCommand returns a new cobra.Command for loading an image into a cluster
49+
func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command {
50+
flags := &flagpole{}
51+
cmd := &cobra.Command{
52+
Args: func(cmd *cobra.Command, args []string) error {
53+
if len(args) < 1 {
54+
return errors.New("a list of image names is required")
55+
}
56+
return nil
57+
},
58+
Use: "docker-image <IMAGE> [IMAGE...]",
59+
Short: "Loads docker images from host into nodes",
60+
Long: "Loads docker images from host into all or specified nodes by name",
61+
Deprecated: "Please use 'kind load container-image' instead of 'kind load docker-image', 'docker-image' will be removed in future releases and is restricted to the docker-provider.",
62+
RunE: func(cmd *cobra.Command, args []string) error {
63+
cli.OverrideDefaultName(cmd.Flags())
64+
return runE(logger, flags, args)
65+
},
66+
}
67+
cmd.Flags().StringVarP(
68+
&flags.Name,
69+
"name",
70+
"n",
71+
cluster.DefaultName,
72+
"the cluster context name",
73+
)
74+
cmd.Flags().StringSliceVar(
75+
&flags.Nodes,
76+
"nodes",
77+
nil,
78+
"comma separated list of nodes to load images into",
79+
)
80+
return cmd
81+
}
82+
func runE(logger log.Logger, flags *flagpole, args []string) error {
83+
provider := cluster.NewProvider(
84+
cluster.ProviderWithLogger(logger),
85+
runtime.GetDefault(logger),
86+
)
87+
88+
saveProvider := cluster.NewProvider(
89+
cluster.ProviderWithDocker(),
90+
runtime.GetDefault(logger),
91+
)
92+
93+
// Check that the image exists locally and gets its ID, if not return error
94+
imageNames := removeDuplicates(args)
95+
var imageIDs []string
96+
for _, imageName := range imageNames {
97+
imageID, err := saveProvider.ContainerImageID(imageName)
98+
if err != nil {
99+
return fmt.Errorf("image: %q not present locally", imageName)
100+
}
101+
imageIDs = append(imageIDs, imageID)
102+
}
103+
104+
// Check if the cluster nodes exist
105+
nodeList, err := provider.ListInternalNodes(flags.Name)
106+
if err != nil {
107+
return err
108+
}
109+
if len(nodeList) == 0 {
110+
return fmt.Errorf("no nodes found for cluster %q", flags.Name)
111+
}
112+
113+
// map cluster nodes by their name
114+
nodesByName := map[string]nodes.Node{}
115+
for _, node := range nodeList {
116+
// TODO(bentheelder): this depends on the fact that ListByCluster()
117+
// will have name for nameOrId.
118+
nodesByName[node.String()] = node
119+
}
120+
121+
// pick only the user selected nodes and ensure they exist
122+
// the default is all nodes unless flags.Nodes is set
123+
candidateNodes := nodeList
124+
if len(flags.Nodes) > 0 {
125+
candidateNodes = []nodes.Node{}
126+
for _, name := range flags.Nodes {
127+
node, ok := nodesByName[name]
128+
if !ok {
129+
return fmt.Errorf("unknown node: %q", name)
130+
}
131+
candidateNodes = append(candidateNodes, node)
132+
}
133+
}
134+
135+
// pick only the nodes that don't have the image
136+
selectedNodes := map[string]nodes.Node{}
137+
fns := []func() error{}
138+
for i, imageName := range imageNames {
139+
imageID := imageIDs[i]
140+
processed := false
141+
for _, node := range candidateNodes {
142+
exists, reTagRequired, sanitizedImageName := checkIfImageReTagRequired(node, imageID, imageName, nodeutils.ImageTags)
143+
if exists && !reTagRequired {
144+
continue
145+
}
146+
147+
if reTagRequired {
148+
// We will try to re-tag the image. If the re-tag fails, we will fall back to the default behavior of loading
149+
// the images into the nodes again
150+
logger.V(0).Infof("Image with ID: %s already present on the node %s but is missing the tag %s. re-tagging...", imageID, node.String(), sanitizedImageName)
151+
if err := nodeutils.ReTagImage(node, imageID, sanitizedImageName); err != nil {
152+
logger.Errorf("failed to re-tag image on the node %s due to an error %s. Will load it instead...", node.String(), err)
153+
selectedNodes[node.String()] = node
154+
} else {
155+
processed = true
156+
}
157+
continue
158+
}
159+
id, err := nodeutils.ImageID(node, imageName)
160+
if err != nil || id != imageID {
161+
selectedNodes[node.String()] = node
162+
logger.V(0).Infof("Image: %q with ID %q not yet present on node %q, loading...", imageName, imageID, node.String())
163+
}
164+
continue
165+
}
166+
if len(selectedNodes) == 0 && !processed {
167+
logger.V(0).Infof("Image: %q with ID %q found to be already present on all nodes.", imageName, imageID)
168+
}
169+
}
170+
171+
// return early if no node needs the image
172+
if len(selectedNodes) == 0 {
173+
return nil
174+
}
175+
176+
// Setup the tar path where the images will be saved
177+
dir, err := fs.TempDir("", "images-tar")
178+
if err != nil {
179+
return errors.Wrap(err, "failed to create tempdir")
180+
}
181+
defer os.RemoveAll(dir)
182+
imagesTarPath := filepath.Join(dir, "images.tar")
183+
// Save the images into a tar
184+
err = saveProvider.ContainerSave(imageNames, imagesTarPath)
185+
if err != nil {
186+
return err
187+
}
188+
189+
// Load the images on the selected nodes
190+
for _, selectedNode := range selectedNodes {
191+
selectedNode := selectedNode // capture loop variable
192+
fns = append(fns, func() error {
193+
return loadImage(imagesTarPath, selectedNode)
194+
})
195+
}
196+
return errors.UntilErrorConcurrent(fns)
197+
}
198+
199+
// TODO: we should consider having a cluster method to load images
200+
201+
// loads an image tarball onto a node
202+
func loadImage(imageTarName string, node nodes.Node) error {
203+
f, err := os.Open(imageTarName)
204+
if err != nil {
205+
return errors.Wrap(err, "failed to open image")
206+
}
207+
defer f.Close()
208+
return nodeutils.LoadImageArchive(node, f)
209+
}
210+
211+
// removeDuplicates removes duplicates from a string slice
212+
func removeDuplicates(slice []string) []string {
213+
result := []string{}
214+
seenKeys := make(map[string]struct{})
215+
for _, k := range slice {
216+
if _, seen := seenKeys[k]; !seen {
217+
result = append(result, k)
218+
seenKeys[k] = struct{}{}
219+
}
220+
}
221+
return result
222+
}
223+
224+
// checkIfImageExists makes sure we only perform the reverse lookup of the ImageID to tag map
225+
func checkIfImageReTagRequired(node nodes.Node, imageID, imageName string, tagFetcher imageTagFetcher) (exists, reTagRequired bool, sanitizedImage string) {
226+
tags, err := tagFetcher(node, imageID)
227+
if len(tags) == 0 || err != nil {
228+
exists = false
229+
return
230+
}
231+
exists = true
232+
sanitizedImage = sanitizeImage(imageName)
233+
if ok := tags[sanitizedImage]; ok {
234+
reTagRequired = false
235+
return
236+
}
237+
reTagRequired = true
238+
return
239+
}
240+
241+
// sanitizeImage is a helper to return human readable image name
242+
// This is a modified version of the same function found under providers/podman/images.go
243+
func sanitizeImage(image string) (sanitizedName string) {
244+
const (
245+
defaultDomain = "docker.io/"
246+
officialRepoName = "library"
247+
)
248+
sanitizedName = image
249+
250+
if !strings.ContainsRune(image, '/') {
251+
sanitizedName = officialRepoName + "/" + image
252+
}
253+
254+
i := strings.IndexRune(sanitizedName, '/')
255+
if i == -1 || (!strings.ContainsAny(sanitizedName[:i], ".:") && sanitizedName[:i] != "localhost") {
256+
sanitizedName = defaultDomain + sanitizedName
257+
}
258+
259+
i = strings.IndexRune(sanitizedName, ':')
260+
if i == -1 {
261+
sanitizedName += ":latest"
262+
}
263+
264+
return
265+
}

pkg/cmd/kind/load/load.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"sigs.k8s.io/kind/pkg/cmd"
2626
containerimage "sigs.k8s.io/kind/pkg/cmd/kind/load/container-image"
27+
dockerimage "sigs.k8s.io/kind/pkg/cmd/kind/load/docker-image"
2728
imagearchive "sigs.k8s.io/kind/pkg/cmd/kind/load/image-archive"
2829
"sigs.k8s.io/kind/pkg/log"
2930
)
@@ -46,5 +47,6 @@ func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command {
4647
// add subcommands
4748
cmd.AddCommand(containerimage.NewCommand(logger, streams))
4849
cmd.AddCommand(imagearchive.NewCommand(logger, streams))
50+
cmd.AddCommand(dockerimage.NewCommand(logger, streams))
4951
return cmd
5052
}

0 commit comments

Comments
 (0)