diff --git a/go.mod b/go.mod index 45c2076fe52c..cf5a0929df0b 100644 --- a/go.mod +++ b/go.mod @@ -75,6 +75,7 @@ require ( k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/controller-runtime v0.6.2 sigs.k8s.io/controller-tools v0.2.4 + sigs.k8s.io/kind v0.9.0 sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 5791a4759115..1980fd264a8d 100644 --- a/go.sum +++ b/go.sum @@ -152,6 +152,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg= +github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible h1:EaK5256H3ELiyaq5O/Zwd6fnghD6DqmZDQmmzzJklUU= github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= @@ -421,6 +423,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.1.0 h1:B0aXl1o/1cP8NbviYiBMkcHBtUjIJ1/Ccg6b+SwCLQg= +github.com/evanphx/json-patch/v5 v5.1.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM= @@ -1238,6 +1242,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= +github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= +github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/performancecopilot/speed v3.0.0+incompatible h1:2WnRzIquHa5QxaJKShDkLM+sc0JPuwhXzK8OYOyt3Vg= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -1962,6 +1968,7 @@ gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200603094226-e3079894b1e8/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= @@ -2102,6 +2109,8 @@ sigs.k8s.io/controller-runtime v0.6.2 h1:jkAnfdTYBpFwlmBn3pS5HFO06SfxvnTZ1p5PeEF sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA= sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= +sigs.k8s.io/kind v0.9.0 h1:SoDlXq6pEc7dGagHULNRCCBYrLH6xOi7lqXTRXeAlg4= +sigs.k8s.io/kind v0.9.0/go.mod h1:cxKQWwmbtRDzQ+RNKnR6gZG6fjbeTtItp5cGf+ww+1Y= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= diff --git a/pkg/builtin/build/build.go b/pkg/builtin/build/build.go index 3746f19c8a7c..e804c1222f92 100644 --- a/pkg/builtin/build/build.go +++ b/pkg/builtin/build/build.go @@ -24,6 +24,7 @@ import ( "github.com/pkg/errors" + "github.com/oam-dev/kubevela/pkg/builtin/kind" "github.com/oam-dev/kubevela/pkg/builtin/registry" cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" ) @@ -129,26 +130,9 @@ func (b *Build) pushImage(io cmdutil.IOStreams, image string) error { io.Infof("pushing image (%s)...\n", image) switch { case b.Push.Local == "kind": - //nolint:gosec - cmd := exec.Command("kind", "load", "docker-image", image) - stdout, err := cmd.StdoutPipe() + err := kind.LoadDockerImage(image) if err != nil { - io.Errorf("pushImage(kind) exec command error, message:%s\n", err.Error()) - return err - } - stderr, err := cmd.StderrPipe() - if err != nil { - io.Errorf("pushImage(kind) exec command error, message:%s\n", err.Error()) - return err - } - if err := cmd.Start(); err != nil { - io.Errorf("pushImage(kind) exec command error, message:%s\n", err.Error()) - return err - } - go asyncLog(stdout, io) - go asyncLog(stderr, io) - if err := cmd.Wait(); err != nil { - io.Errorf("pushImage(kind) wait for command execution error:%s", err.Error()) + io.Errorf("pushImage(kind) load docker image error, message:%s", err) return err } return nil diff --git a/pkg/builtin/kind/client.go b/pkg/builtin/kind/client.go new file mode 100644 index 000000000000..e2978b5db7dd --- /dev/null +++ b/pkg/builtin/kind/client.go @@ -0,0 +1,163 @@ +/* +Copyright 2020 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kind + +import ( + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/kind/pkg/cluster" + "sigs.k8s.io/kind/pkg/cluster/nodes" + "sigs.k8s.io/kind/pkg/cluster/nodeutils" + "sigs.k8s.io/kind/pkg/errors" + "sigs.k8s.io/kind/pkg/exec" + "sigs.k8s.io/kind/pkg/fs" +) + +func LoadDockerImage(imageName string) error { + return LoadDockerImagesWithFlags([]string{imageName}, "", nil) +} + +func LoadDockerImages(imageNames []string) error { + return LoadDockerImagesWithFlags(imageNames, "", nil) +} + +// LoadDockerImagesWithFlags refer to https://github.com/kubernetes-sigs/kind/blob/main/pkg/cmd/kind/load/docker-image/docker-image.go +func LoadDockerImagesWithFlags(imageNames []string, flagName string, flagNodes []string) error { + provider := cluster.NewProvider( + cluster.ProviderWithDocker(), + ) + + // Set cluster context name by default + if flagName == "" { + flagName = cluster.DefaultName + } + + // Check that the image exists locally and gets its ID, if not return error + var imageIDs []string + for _, imageName := range imageNames { + imageID, err := imageID(imageName) + if err != nil { + return fmt.Errorf("image: %q not present locally", imageName) + } + imageIDs = append(imageIDs, imageID) + } + + // Check if the cluster nodes exist + nodeList, err := provider.ListInternalNodes(flagName) + if err != nil { + return err + } + if len(nodeList) == 0 { + return fmt.Errorf("no nodes found for cluster %q", flagName) + } + + // map cluster nodes by their name + nodesByName := map[string]nodes.Node{} + for _, node := range nodeList { + // TODO(bentheelder): this depends on the fact that ListByCluster() + // will have name for nameOrId. + nodesByName[node.String()] = node + } + + // pick only the user selected nodes and ensure they exist + // the default is all nodes unless flags.Nodes is set + candidateNodes := nodeList + if len(flagNodes) > 0 { + candidateNodes = []nodes.Node{} + for _, name := range flagNodes { + node, ok := nodesByName[name] + if !ok { + return fmt.Errorf("unknown node: %q", name) + } + candidateNodes = append(candidateNodes, node) + } + } + + // pick only the nodes that don't have the image + selectedNodes := []nodes.Node{} + fns := []func() error{} + for i, imageName := range imageNames { + imageID := imageIDs[i] + for _, node := range candidateNodes { + id, err := nodeutils.ImageID(node, imageName) + if err != nil || id != imageID { + selectedNodes = append(selectedNodes, node) + } + } + if len(selectedNodes) == 0 { + continue + } + } + + // Setup the tar path where the images will be saved + dir, err := fs.TempDir("", "images-tar") + if err != nil { + return errors.Wrap(err, "failed to create tempdir") + } + defer os.RemoveAll(dir) + imagesTarPath := filepath.Join(dir, "images.tar") + // Save the images into a tar + err = save(imageNames, imagesTarPath) + if err != nil { + return err + } + + // Load the images on the selected nodes + for _, selectedNode := range selectedNodes { + selectedNode := selectedNode // capture loop variable + fns = append(fns, func() error { + return loadImage(imagesTarPath, selectedNode) + }) + } + return errors.UntilErrorConcurrent(fns) +} + +// TODO: we should consider having a cluster method to load images + +// loads an image tarball onto a node +func loadImage(imageTarName string, node nodes.Node) error { + f, err := os.Open(imageTarName) + if err != nil { + return errors.Wrap(err, "failed to open image") + } + defer f.Close() + return nodeutils.LoadImageArchive(node, f) +} + +// save saves images to dest, as in `docker save` +func save(images []string, dest string) error { + commandArgs := append([]string{"save", "-o", dest}, images...) + return exec.Command("docker", commandArgs...).Run() +} + +// imageID return the Id of the container image +func imageID(containerNameOrID string) (string, error) { + cmd := exec.Command("docker", "image", "inspect", + "-f", "{{ .Id }}", + containerNameOrID, // ... against the container + ) + lines, err := exec.OutputLines(cmd) + if err != nil { + return "", err + } + if len(lines) != 1 { + return "", errors.Errorf("Docker image ID should only be one line, got %d lines", len(lines)) + } + return lines[0], nil +} diff --git a/pkg/builtin/kind/client_test.go b/pkg/builtin/kind/client_test.go new file mode 100644 index 000000000000..b13dcc096bda --- /dev/null +++ b/pkg/builtin/kind/client_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kind + +import ( + "testing" +) + +func TestLoadDockerImageNotExist(t *testing.T) { + err := LoadDockerImage("test_not_exist") + if err != nil && err.Error() != "image: \"test_not_exist\" not present locally" { + t.Error(err) + } +} + +func TestLoadDockerImages(t *testing.T) { + err := LoadDockerImages([]string{"test_not_exist"}) + if err != nil && err.Error() != "image: \"test_not_exist\" not present locally" { + t.Error(err) + } +} + +func TestLoadDockerImagesWithFlags(t *testing.T) { + err := LoadDockerImagesWithFlags([]string{"vela-core"}, "", nil) + if err != nil && (err.Error() != "image: \"vela-core\" not present locally" && err.Error() != "no nodes found for cluster \"kind\"") { + t.Error(err) + } +}