From 65a83ed8b1292cf4aef95b5dd6e33dcf0baf3e87 Mon Sep 17 00:00:00 2001 From: Yuhang Wei Date: Mon, 24 Jul 2023 16:19:07 +0800 Subject: [PATCH] KubeOS: add image name check and config ut add verification of image name before pull add config unit testing update quick-start to regulate container image name Signed-off-by: Yuhang Wei --- cmd/agent/server/config.go | 49 ++-- cmd/agent/server/config_test.go | 349 +++++++++++++++++++++++++++ cmd/agent/server/containerd_image.go | 3 + cmd/agent/server/docker_image.go | 3 + cmd/agent/server/utils.go | 13 + docs/quick-start.md | 3 +- 6 files changed, 396 insertions(+), 24 deletions(-) create mode 100644 cmd/agent/server/config_test.go diff --git a/cmd/agent/server/config.go b/cmd/agent/server/config.go index 4ce9e943..b4385e26 100644 --- a/cmd/agent/server/config.go +++ b/cmd/agent/server/config.go @@ -70,7 +70,7 @@ func (k KerSysctlPersist) SetConfig(config *agent.SysConfig) error { logrus.Info("start set kernel.sysctl.persist") configPath := config.ConfigPath if configPath == "" { - configPath = defaultKernelConPath + configPath = getKernelConPath() } if err := createConfigPath(configPath); err != nil { logrus.Errorf("Failed to find config path: %v", err) @@ -94,33 +94,36 @@ type GrubCmdline struct{} // SetConfig sets grub.cmdline configuration func (g GrubCmdline) SetConfig(config *agent.SysConfig) error { logrus.Info("start set grub.cmdline configuration") - fileExist, err := checkFileExist(defalutGrubCfgPath) + fileExist, err := checkFileExist(getGrubCfgPath()) if err != nil { logrus.Errorf("Failed to find config path: %v", err) return err } if !fileExist { - return fmt.Errorf("failed to find grub.cfg %s", defalutGrubCfgPath) + return fmt.Errorf("failed to find grub.cfg %s", getGrubCfgPath()) } - err = getAndSetGrubCfg(config.Contents) + lines, err := getAndSetGrubCfg(config.Contents) if err != nil { logrus.Errorf("Failed to set grub configs: %v", err) return err } + if err := writeConfigToFile(getGrubCfgPath(), lines); err != nil { + return err + } return nil } -func getAndSetGrubCfg(expectConfigs map[string]*agent.KeyInfo) error { - file, err := os.OpenFile(defalutGrubCfgPath, os.O_RDWR, defaultGrubCfgPermission) +func getAndSetGrubCfg(expectConfigs map[string]*agent.KeyInfo) ([]string, error) { + file, err := os.OpenFile(getGrubCfgPath(), os.O_RDWR, defaultGrubCfgPermission) if err != nil { - return err + return []string{}, err } defer file.Close() reFindCurLinux := `^\s*linux.*root=.*` r, err := regexp.Compile(reFindCurLinux) if err != nil { - return err + return []string{}, err } var lines []string @@ -130,24 +133,12 @@ func getAndSetGrubCfg(expectConfigs map[string]*agent.KeyInfo) error { if r.MatchString(line) { line, err = modifyLinuxCfg(expectConfigs, line) if err != nil { - return fmt.Errorf("error modify grub.cfg %v", err) + return []string{}, fmt.Errorf("error modify grub.cfg %v", err) } } lines = append(lines, line) } - - // override new configs to the grub.cfg file - _, err = file.Seek(0, 0) - if err != nil { - return err - } - writer := bufio.NewWriter(file) - for _, line := range lines { - if _, err := fmt.Fprintln(writer, line); err != nil { - return fmt.Errorf("error write grub.cfg %v", err) - } - } - return nil + return lines, nil } func modifyLinuxCfg(m map[string]*agent.KeyInfo, line string) (string, error) { @@ -240,7 +231,7 @@ func ConfigFactoryTemplate(configType string, config *agent.SysConfig) error { } func getProcPath(key string) string { - return filepath.Join(defaultProcPath, strings.Replace(key, ".", "/", -1)) + return filepath.Join(getDefaultProcPath(), strings.Replace(key, ".", "/", -1)) } func getAndSetConfigsFromFile(expectConfigs map[string]*agent.KeyInfo, path string) ([]string, error) { @@ -329,3 +320,15 @@ func createConfigPath(configPath string) error { defer f.Close() return nil } + +func getDefaultProcPath() string { + return "/proc/sys/" +} + +func getKernelConPath() string { + return "/etc/sysctl.conf" +} + +func getGrubCfgPath() string { + return "/boot/efi/EFI/openEuler/grub.cfg" +} diff --git a/cmd/agent/server/config_test.go b/cmd/agent/server/config_test.go new file mode 100644 index 00000000..56b3a47a --- /dev/null +++ b/cmd/agent/server/config_test.go @@ -0,0 +1,349 @@ +/* + * 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 ( + "fmt" + "os" + "reflect" + "regexp" + "sort" + "strings" + "testing" + + "github.com/agiledragon/gomonkey/v2" + + agent "openeuler.org/KubeOS/cmd/agent/api" +) + +func TestKernelSysctl_SetConfig(t *testing.T) { + type args struct { + config *agent.SysConfig + } + tests := []struct { + name string + k KernelSysctl + args args + wantErr bool + }{ + { + name: "add configs", + k: KernelSysctl{}, + args: args{config: &agent.SysConfig{ + Contents: map[string]*agent.KeyInfo{ + "a": {Value: "1"}, + "b": {Value: "2"}, + }, + }}, + wantErr: false, + }, + { + name: "delete", + k: KernelSysctl{}, + args: args{config: &agent.SysConfig{ + Contents: map[string]*agent.KeyInfo{ + "a": {Operation: "delete"}, + }, + }}, + wantErr: false, + }, + { + name: "invalide operation", + k: KernelSysctl{}, + args: args{config: &agent.SysConfig{ + Contents: map[string]*agent.KeyInfo{ + "c": {Operation: "ad"}, + }, + }}, + }, + } + tmpDir := t.TempDir() + patchGetProcPath := gomonkey.ApplyFuncReturn(getDefaultProcPath, tmpDir+"/") + defer patchGetProcPath.Reset() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := KernelSysctl{} + if err := k.SetConfig(tt.args.config); (err != nil) != tt.wantErr { + t.Errorf("KernelSysctl.SetConfig() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestKerSysctlPersist_SetConfig(t *testing.T) { + tmpDir := t.TempDir() + persistPath := tmpDir + "/test-persist.conf" + type args struct { + config *agent.SysConfig + } + tests := []struct { + name string + k KerSysctlPersist + args args + want []string + wantErr bool + }{ + { + name: "add configs", + args: args{ + config: &agent.SysConfig{ + ConfigPath: persistPath, + Contents: map[string]*agent.KeyInfo{ + "a": {Value: "1"}, + "b": {Value: "2"}, + }, + }, + }, + want: []string{ + "a = 1", + "b = 2", + }, + wantErr: false, + }, + { + name: "update", + args: args{ + config: &agent.SysConfig{ + ConfigPath: persistPath, + Contents: map[string]*agent.KeyInfo{ + "a": {Value: "2"}, + }, + }, + }, + want: []string{ + "a = 2", + "b = 2", + }, + wantErr: false, + }, + { + name: "delete", + args: args{ + config: &agent.SysConfig{ + ConfigPath: persistPath, + Contents: map[string]*agent.KeyInfo{ + "a": {Value: "1", Operation: "delete"}, + "b": {Value: "2", Operation: "delete"}, + }, + }, + }, + want: []string{ + "a = 2", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + k := KerSysctlPersist{} + if err := k.SetConfig(tt.args.config); (err != nil) != tt.wantErr { + t.Errorf("KerSysctlPersist.SetConfig() error = %v, wantErr %v", err, tt.wantErr) + } + data, err := os.ReadFile(persistPath) + if err != nil { + t.Errorf("failed to read file %s", persistPath) + } + lines := strings.Split(string(data), "\n") + // remove the last empty line + lines = lines[:len(lines)-1] + sort.Strings(lines) + if !reflect.DeepEqual(lines, tt.want) { + t.Errorf("KerSysctlPersist file contents not equal, expect: %v, get: %v", tt.want, lines) + } + }) + } +} + +func TestGrubCmdline_SetConfig(t *testing.T) { + grubContent := `menuentry 'A' --class KubeOS --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'KubeOS-A' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_gpt + insmod ext2 + set root='hd0,gpt2' + linux /boot/vmlinuz root=UUID=0 ro rootfstype=ext4 nomodeset quiet oops=panic softlockup_panic=1 nmi_watchdog=1 rd.shell=0 selinux=0 crashkernel=256M panic=3 + initrd /boot/initramfs.img +} + +menuentry 'B' --class KubeOS --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option 'KubeOS-B' { + load_video + set gfxpayload=keep + insmod gzio + insmod part_gpt + insmod ext2 + set root='hd0,gpt3' + linux /boot/vmlinuz root=UUID=1 ro rootfstype=ext4 nomodeset quiet oops=panic softlockup_panic=1 nmi_watchdog=1 rd.shell=0 selinux=0 crashkernel=256M panic=3 + initrd /boot/initramfs.img +}` + tmpDir := t.TempDir() + grubCfgPath := tmpDir + "/grub.cfg" + if err := copyGrub(grubContent, grubCfgPath); err != nil { + t.Fatalf("failed to copy grub file %v", err) + } + type args struct { + config *agent.SysConfig + } + tests := []struct { + name string + g GrubCmdline + args args + pattern string + wantErr bool + }{ + { + name: "add, update and delete kernel boot parameters", + g: GrubCmdline{}, + args: args{ + config: &agent.SysConfig{ + Contents: map[string]*agent.KeyInfo{ + "panic": {Value: "5"}, + "quiet": {Value: "", Operation: "delete"}, + "selinux": {Value: "1", Operation: "delete"}, + "acpi": {Value: "off", Operation: "delete"}, + "debug": {}, + "pci": {Value: "nomis"}, + }, + }, + }, + pattern: `(?m)^\s+linux\s+\/boot\/vmlinuz\s+root=UUID=[0-1]\s+ro\s+rootfstype=ext4\s+nomodeset\s+oops=panic\s+softlockup_panic=1\s+nmi_watchdog=1\s+rd\.shell=0\s+selinux=0\s+crashkernel=256M\s+panic=5\s+(debug\spci=nomis|pci=nomis\sdebug)$`, + wantErr: false, + }, + } + patchGetGrubPath := gomonkey.ApplyFuncReturn(getGrubCfgPath, grubCfgPath) + defer patchGetGrubPath.Reset() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := GrubCmdline{} + if err := g.SetConfig(tt.args.config); (err != nil) != tt.wantErr { + t.Errorf("GrubCmdline.SetConfig() error = %v, wantErr %v", err, tt.wantErr) + } + contents, err := os.ReadFile(grubCfgPath) + if err != nil { + t.Fatalf("failed to read grub.cfg") + } + re := regexp.MustCompile(tt.pattern) + match := re.FindAllStringIndex(string(contents), -1) + // it should match partition A and B in total twice + if len(match) != 2 { + t.Fatalf("expected pattern not found in grub.cfg") + } + }) + } +} + +func copyGrub(src string, dst string) error { + // Write data to dst + err := os.WriteFile(dst, []byte(src), 0644) + if err != nil { + return fmt.Errorf("failed to read file %s", dst) + } + return nil +} + +func Test_startConfig(t *testing.T) { + type args struct { + configs []*agent.SysConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "KernelSysctl", + args: args{ + configs: []*agent.SysConfig{ + {Model: KernelSysctlName.String()}, + {Model: KerSysctlPersistName.String()}, + {Model: GrubCmdlineName.String()}, + }, + }, + wantErr: false, + }, + } + patchKerSysctl := gomonkey.ApplyMethodReturn(KernelSysctl{}, "SetConfig", nil) + patchKerSysctlPersist := gomonkey.ApplyMethodReturn(KerSysctlPersist{}, "SetConfig", nil) + patchGrub := gomonkey.ApplyMethodReturn(GrubCmdline{}, "SetConfig", nil) + defer patchKerSysctl.Reset() + defer patchKerSysctlPersist.Reset() + defer patchGrub.Reset() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := startConfig(tt.args.configs); (err != nil) != tt.wantErr { + t.Errorf("startConfig() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_getDefaultProcPath(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "get correct path", + want: "/proc/sys/", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getDefaultProcPath() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAndSetConfigsFromFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getKernelConPath(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "get correct path", + want: "/etc/sysctl.conf", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getKernelConPath() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAndSetConfigsFromFile() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getGrubCfgPath(t *testing.T) { + tests := []struct { + name string + want string + }{ + { + name: "get correct path", + want: "/boot/efi/EFI/openEuler/grub.cfg", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := getGrubCfgPath() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getAndSetConfigsFromFile() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/agent/server/containerd_image.go b/cmd/agent/server/containerd_image.go index ccab9ec4..f180fb54 100644 --- a/cmd/agent/server/containerd_image.go +++ b/cmd/agent/server/containerd_image.go @@ -42,6 +42,9 @@ func (c conImageHandler) downloadImage(req *pb.UpdateRequest) (string, error) { func (c conImageHandler) getRootfsArchive(req *pb.UpdateRequest, neededPath preparePath) (string, error) { imageName := req.ContainerImage + if err := isValidImageName(imageName); err != nil { + return "", err + } mountPath := neededPath.mountPath var containerdCommand string logrus.Infof("start pull %s", imageName) diff --git a/cmd/agent/server/docker_image.go b/cmd/agent/server/docker_image.go index e6fa9d6b..23e596b3 100644 --- a/cmd/agent/server/docker_image.go +++ b/cmd/agent/server/docker_image.go @@ -34,6 +34,9 @@ func (d dockerImageHandler) downloadImage(req *pb.UpdateRequest) (string, error) func (d dockerImageHandler) getRootfsArchive(req *pb.UpdateRequest, neededPath preparePath) (string, error) { imageName := req.ContainerImage + if err := isValidImageName(imageName); err != nil { + return "", err + } logrus.Infof("start pull %s", imageName) if err := runCommand("docker", "pull", imageName); err != nil { return "", err diff --git a/cmd/agent/server/utils.go b/cmd/agent/server/utils.go index d8616003..22136942 100644 --- a/cmd/agent/server/utils.go +++ b/cmd/agent/server/utils.go @@ -19,6 +19,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "syscall" @@ -359,3 +360,15 @@ func isCommandAvailable(name string) bool { } return true } + +func isValidImageName(image string) error { + pattern := `^((?:[\w.-]+)(?::\d+)?\/)*(?:[\w.-]+)(?::[\w_.-]+)?(?:@sha256:[a-fA-F0-9]+)?$` + regEx, err := regexp.Compile(pattern) + if err != nil { + return err + } + if !regEx.MatchString(image) { + return fmt.Errorf("invalid image name %s", image) + } + return nil +} diff --git a/docs/quick-start.md b/docs/quick-start.md index da31480d..0d4dc4bf 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -167,7 +167,7 @@ | opstype | string | 进行的操作,升级,回退或者配置 | 需为 upgrade ,config 或者 rollback ,其他值无效 |是 | | osversion | string | 用于升级或回退的镜像的OS版本 | 需为 KubeOS version , 例如: KubeOS 1.0.0|是 | | maxunavailable | int | 同时进行升级或回退的节点数 | maxunavailable值设置为大于实际集群的节点数时也可正常部署,升级或回退时会按照集群内实际节点数进行|是 | - | containerimage | string | 用于升级的容器镜像 | 需要为容器镜像格式:repository/name:tag,仅在使用容器镜像升级场景下有效|是 | + | containerimage | string | 用于升级的容器镜像 | 需要为容器镜像格式:[REPOSITORY/NAME[:TAG@DIGEST]](https://docs.docker.com/engine/reference/commandline/tag/#extended-description),仅在使用容器镜像升级场景下有效|是 | | imageurl | string | 用于升级的磁盘镜像的地址 | imageurl中包含协议,只支持http或https协议,例如: 仅在使用磁盘镜像升级场景下有效|是 | | checksum | string | 用于升级的磁盘镜像校验的checksum(SHA-256)值或者是用于升级的容器镜像的digests值 | 仅在升级场景下有效 |是 | | flagSafe | bool | 当imageurl的地址使用http协议表示是否是安全的 | 需为 true 或者 false ,仅在imageurl使用http协议时有效 |是 | @@ -522,6 +522,7 @@ kind: Secret metadata: name: root-secret data: + # base64 encode your pub key in one line ssh-pub-key: your-ssh-pub-key --- apiVersion: apps/v1 -- Gitee