diff --git a/cmd/agent/server/containerd_image.go b/cmd/agent/server/containerd_image.go index f180fb54a74ea7417daaa7c85072231ca3d570a0..fd612745252b8c73e3482500c3b454e321ab5b74 100644 --- a/cmd/agent/server/containerd_image.go +++ b/cmd/agent/server/containerd_image.go @@ -23,9 +23,7 @@ import ( pb "openeuler.org/KubeOS/cmd/agent/api" ) -var ( - defaultNamespace = "k8s.io" -) +const defaultNamespace = "k8s.io" type conImageHandler struct{} @@ -56,7 +54,7 @@ func (c conImageHandler) getRootfsArchive(req *pb.UpdateRequest, neededPath prep } } else { containerdCommand = "ctr" - if err := runCommand("ctr", "-n", defaultNamespace, "images", "pull", "--host-dir", + if err := runCommand("ctr", "-n", defaultNamespace, "images", "pull", "--hosts-dir", "/etc/containerd/certs.d", imageName); err != nil { return "", err } @@ -76,7 +74,7 @@ func (c conImageHandler) getRootfsArchive(req *pb.UpdateRequest, neededPath prep return "", err } defer checkAndCleanMount(mountPath) - if err := copyFile(neededPath.tarPath, mountPath+"/"+rootfsArchive); err != nil { + if err := copyFile(neededPath.tarPath, mountPath+"/"+neededPath.rootfsFile); err != nil { return "", err } return "", nil diff --git a/cmd/agent/server/containerd_image_test.go b/cmd/agent/server/containerd_image_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d7133c3713c3726f72966d691b7d5b9ae1765b59 --- /dev/null +++ b/cmd/agent/server/containerd_image_test.go @@ -0,0 +1,139 @@ +/* + * Copyright (c) Huawei Technologies Co., Ltd. 2023. All rights reserved. + * KubeOS is licensed under the Mulan PSL v2. + * You can use this software according to the terms and conditions of the Mulan PSL v2. + * You may obtain a copy of Mulan PSL v2 at: + * http://license.coscl.org.cn/MulanPSL2 + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR + * PURPOSE. + * See the Mulan PSL v2 for more details. + */ + +// Package server implements server of os-agent and listener of os-agent server. The server uses gRPC interface. +package server + +import ( + "os" + "testing" + + "github.com/agiledragon/gomonkey/v2" + pb "openeuler.org/KubeOS/cmd/agent/api" +) + +func Test_conImageHandler_downloadImage(t *testing.T) { + type args struct { + req *pb.UpdateRequest + } + tests := []struct { + name string + c conImageHandler + args args + want string + wantErr bool + }{ + + { + name: "pullImageError", + c: conImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ContainerImage: "testError"}, + }, + want: "", + wantErr: true, + }, + { + name: "checkSumError", + c: conImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ContainerImage: "docker.io/library/hello-world:latest"}, + }, + want: "", + wantErr: true, + }, + { + name: "normal", + c: conImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ + ContainerImage: "docker.io/library/hello-world:latest", + }, + }, + want: "update-test1/upadte.img", + wantErr: false, + }, + } + patchPrepareEnv := gomonkey.ApplyFunc(prepareEnv, func() (preparePath, error) { + return preparePath{updatePath: "update-test1/", + mountPath: "update-test1/mountPath", + tarPath: "update-test1/mountPath/hello", + imagePath: "update-test1/upadte.img", + rootfsFile: "hello"}, nil + }) + defer patchPrepareEnv.Reset() + patchCreateOSImage := gomonkey.ApplyFunc(createOSImage, func(neededPath preparePath) (string, error) { + return "update-test1/upadte.img", nil + }) + defer patchCreateOSImage.Reset() + + if err := os.MkdirAll("update-test1/mountPath", os.ModePerm); err != nil { + t.Errorf("create test dir error = %v", err) + return + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := conImageHandler{} + if tt.name == "normal" { + imageDigests, err := getOCIImageDigest("crictl", "docker.io/library/hello-world:latest") + if err != nil { + t.Errorf("conImageHandler.getRootfsArchive() get oci image digests error = %v", err) + } + tt.args.req.CheckSum = imageDigests + } + got, err := c.downloadImage(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("conImageHandler.downloadImage() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("conImageHandler.downloadImage() = %v, want %v", got, tt.want) + } + }) + } + defer func() { + if err := runCommand("crictl", "rmi", "docker.io/library/hello-world:latest"); err != nil { + t.Errorf("remove kubeos-temp container error = %v", err) + } + if err := os.RemoveAll("update-test1"); err != nil { + t.Errorf("remove update-test error = %v", err) + } + }() +} + +func Test_copyFile(t *testing.T) { + type args struct { + dstFileName string + srcFileName string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "srcFileNotExist", + args: args{ + dstFileName: "bbb.txt", + srcFileName: "aaa.txt", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := copyFile(tt.args.dstFileName, tt.args.srcFileName); (err != nil) != tt.wantErr { + t.Errorf("copyFile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/cmd/agent/server/docker_image.go b/cmd/agent/server/docker_image.go index 23e596b3beec90b2eb0882529023e77086489946..0b6ee35bde9acb7db5d61f2578ca82064d332f15 100644 --- a/cmd/agent/server/docker_image.go +++ b/cmd/agent/server/docker_image.go @@ -61,7 +61,7 @@ func (d dockerImageHandler) getRootfsArchive(req *pb.UpdateRequest, neededPath p if err != nil { return "", err } - if err := runCommand("docker", "cp", containerId+":/"+rootfsArchive, neededPath.updatePath); err != nil { + if err := runCommand("docker", "cp", containerId+":/"+neededPath.rootfsFile, neededPath.updatePath); err != nil { return "", err } defer func() { diff --git a/cmd/agent/server/docker_image_test.go b/cmd/agent/server/docker_image_test.go index 9987939390dbe33dbc3a97d026352d0fc4002d24..2dbf33700f7cfb499d2a00dfaa89bc48cf6300b8 100644 --- a/cmd/agent/server/docker_image_test.go +++ b/cmd/agent/server/docker_image_test.go @@ -17,38 +17,102 @@ import ( "os" "testing" + "github.com/agiledragon/gomonkey/v2" pb "openeuler.org/KubeOS/cmd/agent/api" ) -func TestpullOSImage(t *testing.T) { +func Test_dockerImageHandler_downloadImage(t *testing.T) { type args struct { req *pb.UpdateRequest } - os.Mkdir("/persist", os.ModePerm) tests := []struct { name string + d dockerImageHandler args args want string wantErr bool }{ - {name: "pull image error", args: args{req: &pb.UpdateRequest{ - DockerImage: "test", - }}, want: "", wantErr: true}, - {name: "normal", args: args{req: &pb.UpdateRequest{ - DockerImage: "centos", - }}, want: "/persist/update.img", wantErr: false}, + { + name: "pullImageError", + d: dockerImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ContainerImage: "testError"}, + }, + want: "", + wantErr: true, + }, + + { + name: "checkSumError", + d: dockerImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ContainerImage: "hello-world", CheckSum: "aaaaaa"}, + }, + want: "", + wantErr: true, + }, + + { + name: "normal", + d: dockerImageHandler{}, + args: args{ + req: &pb.UpdateRequest{ContainerImage: "hello-world"}, + }, + want: "update-test/upadte.img", + wantErr: false, + }, + } + patchPrepareEnv := gomonkey.ApplyFunc(prepareEnv, func() (preparePath, error) { + return preparePath{updatePath: "update-test/", + mountPath: "update-test/mountPath", + tarPath: "update-test/mountPath/hello", + imagePath: "update-test/upadte.img", + rootfsFile: "hello"}, nil + }) + defer patchPrepareEnv.Reset() + + patchCreateOSImage := gomonkey.ApplyFunc(createOSImage, func(neededPath preparePath) (string, error) { + return "update-test/upadte.img", nil + }) + defer patchCreateOSImage.Reset() + + if err := os.MkdirAll("update-test/mountPath", os.ModePerm); err != nil { + t.Errorf("create test dir error = %v", err) + return } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := pullOSImage(tt.args.req) + if tt.name == "normal" { + _, err := runCommandWithOut("docker", "create", "--name", "kubeos-temp", "hello-world") + if err != nil { + t.Errorf("Test_dockerImageHandler_getRootfsArchive create container error = %v", err) + return + } + imageDigests, err := getOCIImageDigest("docker", "hello-world") + + if err != nil { + t.Errorf("Test_dockerImageHandler_getRootfsArchive get oci image digests error = %v", err) + } + tt.args.req.CheckSum = imageDigests + } + d := dockerImageHandler{} + got, err := d.downloadImage(tt.args.req) if (err != nil) != tt.wantErr { - t.Errorf("pullOSImage() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("dockerImageHandler.downloadImage() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("pullOSImage() = %v, want %v", got, tt.want) + t.Errorf("dockerImageHandler.downloadImage() = %v, want %v", got, tt.want) } }) } - defer os.RemoveAll("/persist") + defer func() { + if err := runCommand("docker", "rmi", "hello-world"); err != nil { + t.Errorf("remove kubeos-temp container error = %v", err) + } + if err := os.RemoveAll("update-test"); err != nil { + t.Errorf("remove update-test error = %v", err) + } + }() } diff --git a/cmd/agent/server/utils.go b/cmd/agent/server/utils.go index c8a72c3e579626f6f95e869e47a3241bb5b79bb3..7134d74f155e4f022f4da2fd056987839fdbdb3a 100644 --- a/cmd/agent/server/utils.go +++ b/cmd/agent/server/utils.go @@ -31,10 +31,7 @@ import ( const ( needGBSize = 3 // the max size of update files needed // KB is 1024 B - KB = 1024 -) - -var ( + KB = 1024 rootfsArchive = "os.tar" updateDir = "KubeOS-Update" mountDir = "kubeos-update" @@ -51,6 +48,7 @@ type preparePath struct { mountPath string tarPath string imagePath string + rootfsFile string } func runCommand(name string, args ...string) error { @@ -192,9 +190,10 @@ func prepareEnv() (preparePath, error) { if err := checkDiskSize(needGBSize, PersistDir); err != nil { return preparePath{}, err } + rootfsFile := rootfsArchive updatePath := splicePath(PersistDir, updateDir) mountPath := splicePath(updatePath, mountDir) - tarPath := splicePath(updatePath, rootfsArchive) + tarPath := splicePath(updatePath, rootfsFile) imagePath := splicePath(PersistDir, osImageName) if err := cleanSpace(updatePath, mountPath, imagePath); err != nil { @@ -208,6 +207,7 @@ func prepareEnv() (preparePath, error) { mountPath: mountPath, tarPath: tarPath, imagePath: imagePath, + rootfsFile: rootfsFile, } return upgradePath, nil } @@ -284,50 +284,9 @@ func checkFileExist(path string) (bool, error) { } func checkOCIImageDigestMatch(containerRuntime string, imageName string, checkSum string) error { - var cmdOutput string - var err error - switch containerRuntime { - case "crictl": - cmdOutput, err = runCommandWithOut("crictl", "inspecti", "--output", "go-template", - "--template", "{{.status.repoDigests}}", imageName) - if err != nil { - return err - } - case "docker": - cmdOutput, err = runCommandWithOut("docker", "inspect", "--format", "{{.RepoDigests}}", imageName) - if err != nil { - return err - } - case "ctr": - cmdOutput, err = runCommandWithOut("ctr", "-n", "k8s.io", "images", "ls", "name=="+imageName) - if err != nil { - return err - } - // after Fields, we get slice like [REF TYPE DIGEST SIZE PLATFORMS LABELS x x x x x x] - // the digest is the position 8 element - imageDigest := strings.Split(strings.Fields(cmdOutput)[8], ":")[1] - if imageDigest != checkSum { - logrus.Errorln("checkSumFailed ", imageDigest, " mismatch to ", checkSum) - return fmt.Errorf("checkSumFailed %s mismatch to %s", imageDigest, checkSum) - } - return nil - default: - logrus.Errorln("containerRuntime ", containerRuntime, " cannot be recognized") - return fmt.Errorf("containerRuntime %s cannot be recognized", containerRuntime) - } - // cmdOutput format is as follows: - // [imageRepository/imageName:imageTag@sha256:digests] - // parse the output and get digest - var imageDigests string - outArray := strings.Split(cmdOutput, "@") - if strings.HasPrefix(outArray[len(outArray)-1], "sha256") { - pasredArray := strings.Split(strings.TrimSuffix(outArray[len(outArray)-1], "]"), ":") - // 2 is the expected length of the array after dividing "imageName:imageTag@sha256:digests" based on ':' - rightLen := 2 - if len(pasredArray) == rightLen { - digestIndex := 1 // 1 is the index of digest data in pasredArray - imageDigests = pasredArray[digestIndex] - } + imageDigests, err := getOCIImageDigest(containerRuntime, imageName) + if err != nil { + return err } if imageDigests == "" { logrus.Errorln("error when get ", imageName, " digests") @@ -367,3 +326,48 @@ func isValidImageName(image string) error { } return nil } + +func getOCIImageDigest(containerRuntime string, imageName string) (string, error) { + var cmdOutput string + var err error + var imageDigests string + switch containerRuntime { + case "crictl": + cmdOutput, err = runCommandWithOut("crictl", "inspecti", "--output", "go-template", + "--template", "{{.status.repoDigests}}", imageName) + if err != nil { + return "", err + } + case "docker": + cmdOutput, err = runCommandWithOut("docker", "inspect", "--format", "{{.RepoDigests}}", imageName) + if err != nil { + return "", err + } + case "ctr": + cmdOutput, err = runCommandWithOut("ctr", "-n", "k8s.io", "images", "ls", "name=="+imageName) + if err != nil { + return "", err + } + // after Fields, we get slice like [REF TYPE DIGEST SIZE PLATFORMS LABELS x x x x x x] + // the digest is the position 8 element + imageDigest := strings.Split(strings.Fields(cmdOutput)[8], ":")[1] + return imageDigest, nil + default: + logrus.Errorln("containerRuntime ", containerRuntime, " cannot be recognized") + return "", fmt.Errorf("containerRuntime %s cannot be recognized", containerRuntime) + } + // cmdOutput format is as follows: + // [imageRepository/imageName:imageTag@sha256:digests] + // parse the output and get digest + outArray := strings.Split(cmdOutput, "@") + if strings.HasPrefix(outArray[len(outArray)-1], "sha256") { + pasredArray := strings.Split(strings.TrimSuffix(outArray[len(outArray)-1], "]"), ":") + // 2 is the expected length of the array after dividing "imageName:imageTag@sha256:digests" based on ':' + rightLen := 2 + if len(pasredArray) == rightLen { + digestIndex := 1 // 1 is the index of digest data in pasredArray + imageDigests = pasredArray[digestIndex] + } + } + return imageDigests, nil +}