|
| 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 | +} |
0 commit comments