Skip to content

Commit 720bccc

Browse files
committed
add docker-image command for backwards compatibility
1 parent ce0c0db commit 720bccc

File tree

2 files changed

+268
-0
lines changed

2 files changed

+268
-0
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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 load images from docker.",
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+
83+
func runE(logger log.Logger, flags *flagpole, args []string) error {
84+
provider := cluster.NewProvider(
85+
cluster.ProviderWithLogger(logger),
86+
runtime.GetDefault(logger),
87+
)
88+
89+
saveProvider := cluster.NewProvider(
90+
cluster.ProviderWithDocker(),
91+
runtime.GetDefault(logger),
92+
)
93+
94+
// Check that the image exists locally and gets its ID, if not return error
95+
imageNames := removeDuplicates(args)
96+
var imageIDs []string
97+
for _, imageName := range imageNames {
98+
imageID, err := saveProvider.ContainerImageID(imageName)
99+
if err != nil {
100+
return fmt.Errorf("image: %q not present locally", imageName)
101+
}
102+
imageIDs = append(imageIDs, imageID)
103+
}
104+
105+
// Check if the cluster nodes exist
106+
nodeList, err := provider.ListInternalNodes(flags.Name)
107+
if err != nil {
108+
return err
109+
}
110+
if len(nodeList) == 0 {
111+
return fmt.Errorf("no nodes found for cluster %q", flags.Name)
112+
}
113+
114+
// map cluster nodes by their name
115+
nodesByName := map[string]nodes.Node{}
116+
for _, node := range nodeList {
117+
// TODO(bentheelder): this depends on the fact that ListByCluster()
118+
// will have name for nameOrId.
119+
nodesByName[node.String()] = node
120+
}
121+
122+
// pick only the user selected nodes and ensure they exist
123+
// the default is all nodes unless flags.Nodes is set
124+
candidateNodes := nodeList
125+
if len(flags.Nodes) > 0 {
126+
candidateNodes = []nodes.Node{}
127+
for _, name := range flags.Nodes {
128+
node, ok := nodesByName[name]
129+
if !ok {
130+
return fmt.Errorf("unknown node: %q", name)
131+
}
132+
candidateNodes = append(candidateNodes, node)
133+
}
134+
}
135+
136+
// pick only the nodes that don't have the image
137+
selectedNodes := map[string]nodes.Node{}
138+
fns := []func() error{}
139+
for i, imageName := range imageNames {
140+
imageID := imageIDs[i]
141+
processed := false
142+
for _, node := range candidateNodes {
143+
exists, reTagRequired, sanitizedImageName := checkIfImageReTagRequired(node, imageID, imageName, nodeutils.ImageTags)
144+
if exists && !reTagRequired {
145+
continue
146+
}
147+
148+
if reTagRequired {
149+
// We will try to re-tag the image. If the re-tag fails, we will fall back to the default behavior of loading
150+
// the images into the nodes again
151+
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)
152+
if err := nodeutils.ReTagImage(node, imageID, sanitizedImageName); err != nil {
153+
logger.Errorf("failed to re-tag image on the node %s due to an error %s. Will load it instead...", node.String(), err)
154+
selectedNodes[node.String()] = node
155+
} else {
156+
processed = true
157+
}
158+
continue
159+
}
160+
id, err := nodeutils.ImageID(node, imageName)
161+
if err != nil || id != imageID {
162+
selectedNodes[node.String()] = node
163+
logger.V(0).Infof("Image: %q with ID %q not yet present on node %q, loading...", imageName, imageID, node.String())
164+
}
165+
continue
166+
}
167+
if len(selectedNodes) == 0 && !processed {
168+
logger.V(0).Infof("Image: %q with ID %q found to be already present on all nodes.", imageName, imageID)
169+
}
170+
}
171+
172+
// return early if no node needs the image
173+
if len(selectedNodes) == 0 {
174+
return nil
175+
}
176+
177+
// Setup the tar path where the images will be saved
178+
dir, err := fs.TempDir("", "images-tar")
179+
if err != nil {
180+
return errors.Wrap(err, "failed to create tempdir")
181+
}
182+
defer os.RemoveAll(dir)
183+
imagesTarPath := filepath.Join(dir, "images.tar")
184+
// Save the images into a tar
185+
err = saveProvider.ContainerSave(imageNames, imagesTarPath)
186+
if err != nil {
187+
return err
188+
}
189+
190+
// Load the images on the selected nodes
191+
for _, selectedNode := range selectedNodes {
192+
selectedNode := selectedNode // capture loop variable
193+
fns = append(fns, func() error {
194+
return loadImage(imagesTarPath, selectedNode)
195+
})
196+
}
197+
return errors.UntilErrorConcurrent(fns)
198+
}
199+
200+
// TODO: we should consider having a cluster method to load images
201+
202+
// loads an image tarball onto a node
203+
func loadImage(imageTarName string, node nodes.Node) error {
204+
f, err := os.Open(imageTarName)
205+
if err != nil {
206+
return errors.Wrap(err, "failed to open image")
207+
}
208+
defer f.Close()
209+
return nodeutils.LoadImageArchive(node, f)
210+
}
211+
212+
// removeDuplicates removes duplicates from a string slice
213+
func removeDuplicates(slice []string) []string {
214+
result := []string{}
215+
seenKeys := make(map[string]struct{})
216+
for _, k := range slice {
217+
if _, seen := seenKeys[k]; !seen {
218+
result = append(result, k)
219+
seenKeys[k] = struct{}{}
220+
}
221+
}
222+
return result
223+
}
224+
225+
// checkIfImageExists makes sure we only perform the reverse lookup of the ImageID to tag map
226+
func checkIfImageReTagRequired(node nodes.Node, imageID, imageName string, tagFetcher imageTagFetcher) (exists, reTagRequired bool, sanitizedImage string) {
227+
tags, err := tagFetcher(node, imageID)
228+
if len(tags) == 0 || err != nil {
229+
exists = false
230+
return
231+
}
232+
exists = true
233+
sanitizedImage = sanitizeImage(imageName)
234+
if ok := tags[sanitizedImage]; ok {
235+
reTagRequired = false
236+
return
237+
}
238+
reTagRequired = true
239+
return
240+
}
241+
242+
// sanitizeImage is a helper to return human readable image name
243+
// This is a modified version of the same function found under providers/podman/images.go
244+
func sanitizeImage(image string) (sanitizedName string) {
245+
const (
246+
defaultDomain = "docker.io/"
247+
officialRepoName = "library"
248+
)
249+
sanitizedName = image
250+
251+
if !strings.ContainsRune(image, '/') {
252+
sanitizedName = officialRepoName + "/" + image
253+
}
254+
255+
i := strings.IndexRune(sanitizedName, '/')
256+
if i == -1 || (!strings.ContainsAny(sanitizedName[:i], ".:") && sanitizedName[:i] != "localhost") {
257+
sanitizedName = defaultDomain + sanitizedName
258+
}
259+
260+
i = strings.IndexRune(sanitizedName, ':')
261+
if i == -1 {
262+
sanitizedName += ":latest"
263+
}
264+
265+
return
266+
}

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
)
@@ -45,6 +46,7 @@ func NewCommand(logger log.Logger, streams cmd.IOStreams) *cobra.Command {
4546
}
4647
// add subcommands
4748
cmd.AddCommand(containerimage.NewCommand(logger, streams))
49+
cmd.AddCommand(dockerimage.NewCommand(logger, streams))
4850
cmd.AddCommand(imagearchive.NewCommand(logger, streams))
4951
return cmd
5052
}

0 commit comments

Comments
 (0)