From f1d2766bda44a878777ea266573236271dcc65ed Mon Sep 17 00:00:00 2001 From: haozi007 Date: Thu, 11 Nov 2021 11:15:37 +0000 Subject: [PATCH 11/12] run hook with envs of cluster info Signed-off-by: haozi007 --- docs/hooks_of_eggo.md | 2 +- pkg/api/tools.go | 29 ++++++++ pkg/api/types.go | 42 ++++++++++++ .../binary/infrastructure/infrastructure.go | 10 +-- pkg/utils/dependency/dependency.go | 40 ++++++++--- pkg/utils/dependency/dependency_test.go | 52 +++++++++++++++ pkg/utils/dependency/install.go | 66 +++++++++++++++---- pkg/utils/runner/runner.go | 16 ++--- 8 files changed, 216 insertions(+), 41 deletions(-) create mode 100644 pkg/utils/dependency/dependency_test.go diff --git a/docs/hooks_of_eggo.md b/docs/hooks_of_eggo.md index 92b9a38..b1f09cb 100644 --- a/docs/hooks_of_eggo.md +++ b/docs/hooks_of_eggo.md @@ -22,6 +22,7 @@ - 脚本目录下的所有脚本都会被执行,而子目录中的脚本不会被执行; - 每个脚本的超时时间为60s; +- role可以为master,worker,etcd或者loadbalance; ### 配置文件参数方式 @@ -60,4 +61,3 @@ eggo会在hook执行时,通过环境变量传递部分信息,用于脚本执 | EGGO_NODE_ROLE | hook执行的节点角色 | | EGGO_HOOK_TYPE | hook的类型,prehook或者posthook | | EGGO_OPERATOR | 当前的操作,deploy,cleanup,join,delete。 | - diff --git a/pkg/api/tools.go b/pkg/api/tools.go index 861d70a..4c65dc2 100644 --- a/pkg/api/tools.go +++ b/pkg/api/tools.go @@ -55,6 +55,10 @@ func (p PackageSrcConfig) GetPkgDstPath() string { return p.DstPath } +func (ep APIEndpoint) GetUrl() string { + return fmt.Sprintf("%s/%v", ep.AdvertiseAddress, ep.BindPort) +} + func GetClusterHomePath(cluster string) string { return filepath.Join(EggoHomePath, cluster) } @@ -191,3 +195,28 @@ func ParseScheduleType(schedule string) (ScheduleType, error) { return SchedulePreJoin, fmt.Errorf("invalid schedule type: %s", schedule) } } + +func GetRoleString(roles uint16) []string { + var roleStrs []string + if roles&Master != 0 { + roleStrs = append(roleStrs, "master") + } + if roles&Worker != 0 { + roleStrs = append(roleStrs, "worker") + } + if roles&ETCD != 0 { + roleStrs = append(roleStrs, "etcd") + } + if roles&LoadBalance != 0 { + roleStrs = append(roleStrs, "loadbalance") + } + + return roleStrs +} + +func GetUserTempDir(user string) string { + if user == "root" { + return constants.DefaultRootCopyTempDirHome + } + return fmt.Sprintf(constants.DefaultUserCopyTempHomeFormat, user) +} diff --git a/pkg/api/types.go b/pkg/api/types.go index 6a1351e..fea3f26 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -35,6 +35,37 @@ const ( SchedulePostCleanup ScheduleType = "postcleanup" ) +type HookOperator string + +const ( + HookOpDeploy HookOperator = "deploy" + HookOpCleanup HookOperator = "cleanup" + HookOpJoin HookOperator = "join" + HookOpDelete HookOperator = "delete" +) + +type HookType string + +const ( + PreHookType HookType = "prehook" + PostHookType HookType = "posthook" +) + +type HookRunConfig struct { + ClusterID string + ClusterApiEndpoint string + ClusterConfigDir string + + HookType HookType + Operator HookOperator + + Node *HostConfig + Scheduler ScheduleType + + HookDir string + Hooks []*PackageConfig +} + type RoleInfra struct { OpenPorts []*OpenPorts `json:"open-ports"` Softwares []*PackageConfig `json:"softwares"` @@ -201,6 +232,14 @@ type AddonConfig struct { Filename string `json:"filename"` } +type ClusterHookConf struct { + Type HookType + Operator HookOperator + Target uint16 + HookDir string + HookFiles []string +} + type ClusterConfig struct { Name string `json:"name"` DeployDriver string `json:"deploy-driver"` // default is binary @@ -218,6 +257,9 @@ type ClusterConfig struct { WorkerConfig WorkerConfig `json:"workerconfig"` RoleInfra map[uint16]*RoleInfra `json:"role-infra"` + // do not encode hooks, just set before use it + HooksConf *ClusterHookConf `json:"-"` + // TODO: add other configurations at here } diff --git a/pkg/clusterdeployment/binary/infrastructure/infrastructure.go b/pkg/clusterdeployment/binary/infrastructure/infrastructure.go index 634e338..68faf36 100644 --- a/pkg/clusterdeployment/binary/infrastructure/infrastructure.go +++ b/pkg/clusterdeployment/binary/infrastructure/infrastructure.go @@ -27,7 +27,6 @@ import ( "isula.org/eggo/pkg/api" "isula.org/eggo/pkg/clusterdeployment/binary/cleanupcluster" - "isula.org/eggo/pkg/constants" "isula.org/eggo/pkg/utils" "isula.org/eggo/pkg/utils/dependency" "isula.org/eggo/pkg/utils/nodemanager" @@ -321,13 +320,6 @@ func (it *DestroyInfraTask) Name() string { return "DestroyInfraTask" } -func getCopyDefaultDir(user string) string { - if user == "root" { - return constants.DefaultRootCopyTempDirHome - } - return fmt.Sprintf(constants.DefaultUserCopyTempHomeFormat, user) -} - func (it *DestroyInfraTask) Run(r runner.Runner, hcg *api.HostConfig) error { if hcg == nil { return fmt.Errorf("empty host config") @@ -348,7 +340,7 @@ func (it *DestroyInfraTask) Run(r runner.Runner, hcg *api.HostConfig) error { logrus.Errorf("path %s not in White List and cannot remove", dstDir) return nil } - copyTempDir := getCopyDefaultDir(hcg.UserName) + copyTempDir := api.GetUserTempDir(hcg.UserName) if _, err := r.RunCommand(fmt.Sprintf("sudo -E /bin/sh -c \"rm -rf %s %s %s\"", dstDir, copyTempDir, it.k8sConfigDir)); err != nil { return fmt.Errorf("rm dependency failed: %v", err) } diff --git a/pkg/utils/dependency/dependency.go b/pkg/utils/dependency/dependency.go index 9e6ac22..2c5dc26 100644 --- a/pkg/utils/dependency/dependency.go +++ b/pkg/utils/dependency/dependency.go @@ -293,6 +293,7 @@ func (dy *dependencyYaml) Remove(r runner.Runner) error { } type dependencyShell struct { + envs []string srcPath string shell []*api.PackageConfig } @@ -305,22 +306,45 @@ func NewDependencyShell(srcPath string, shell []*api.PackageConfig) *dependencyS } func (ds *dependencyShell) Install(r runner.Runner) error { - var sb strings.Builder + shellTemplate := ` +#!/bin/bash +{{- range $i, $v := .Envs }} +export {{ $v }} +{{- end }} - sb.WriteString("sudo -E /bin/sh -c \"") - for _, s := range ds.shell { - sb.WriteString(fmt.Sprintf("chmod +x %s/%s && ", ds.srcPath, s.Name)) +{{- $tout := .Timeouts }} +{{- range $i, $v := .Shells }} +chmod +x {{ $v }} && timeout -s SIGKILL {{index $tout $i}} {{ $v }} > /dev/null +if [ $? -ne 0 ]; then + echo "run {{ $v }} failed" + exit 1 +fi +{{- end }} +exit 0 +` + datastore := map[string]interface{}{} + datastore["Envs"] = ds.envs + var shells []string + var timeouts []string + for _, s := range ds.shell { + shells = append(shells, fmt.Sprintf("%s/%s", ds.srcPath, s.Name)) timeout := s.TimeOut if timeout == "" { timeout = "30s" } - sb.WriteString(fmt.Sprintf("timeout -s SIGKILL %s %s/%s > /dev/null ; ", timeout, ds.srcPath, s.Name)) + timeouts = append(timeouts, timeout) } - sb.WriteString("\"") + datastore["Shells"] = shells + datastore["Timeouts"] = timeouts - if _, err := r.RunCommand(sb.String()); err != nil { - return fmt.Errorf("shell execute failed: %v", err) + parsedShell, err := template.TemplateRender(shellTemplate, datastore) + if err != nil { + return err + } + + if _, err := r.RunShell(parsedShell, "exechook"); err != nil { + return fmt.Errorf("hook execute failed: %v", err) } return nil diff --git a/pkg/utils/dependency/dependency_test.go b/pkg/utils/dependency/dependency_test.go new file mode 100644 index 0000000..58ea756 --- /dev/null +++ b/pkg/utils/dependency/dependency_test.go @@ -0,0 +1,52 @@ +package dependency + +import ( + "testing" + + "github.com/sirupsen/logrus" + "isula.org/eggo/pkg/api" +) + +type MockRunner struct { +} + +func (m *MockRunner) Copy(src, dst string) error { + logrus.Infof("copy %s to %s", src, dst) + return nil +} + +func (m *MockRunner) RunCommand(cmd string) (string, error) { + logrus.Infof("run command: %s", cmd) + return "", nil +} + +func (m *MockRunner) RunShell(shell string, name string) (string, error) { + logrus.Infof("run shell: %s, name: %s", shell, name) + return "", nil +} + +func (m *MockRunner) Reconnect() error { + logrus.Infof("reconnect") + return nil +} + +func (m *MockRunner) Close() { + logrus.Infof("close") +} + +func TestNewDependencyShell(t *testing.T) { + var mr MockRunner + + shell := &api.PackageConfig{ + Name: "test.sh", + Type: "shell", + Dst: "/root", + Schedule: api.SchedulePreJoin, + TimeOut: "30s", + } + + dp := NewDependencyShell("/tmp", []*api.PackageConfig{shell}) + if err := dp.Install(&mr); err != nil { + t.Fatalf("run test failed: %v", err) + } +} diff --git a/pkg/utils/dependency/install.go b/pkg/utils/dependency/install.go index 8687602..8cb80f6 100644 --- a/pkg/utils/dependency/install.go +++ b/pkg/utils/dependency/install.go @@ -349,33 +349,77 @@ func getShell(roleInfra *api.RoleInfra, schedule api.ScheduleType) []*api.Packag return shell } -func ExecuteShell(roleInfra *api.RoleInfra, packagePath string, hcf *api.HostConfig, schedule api.ScheduleType) error { - shell := getShell(roleInfra, schedule) - if len(shell) == 0 { +func ExecuteHooks(hookConf *api.HookRunConfig) error { + if hookConf == nil || len(hookConf.Hooks) == 0 { return nil } - logrus.Debugf("run %s shell %v on %v\n", string(schedule), shell, hcf.Address) - dp := &dependencyShell{ - srcPath: path.Join(packagePath, constants.DefaultFilePath), - shell: shell, + var hookStr []string + for _, h := range hookConf.Hooks { + hookStr = append(hookStr, h.Name) } + logrus.Debugf("run %s shell %v on %v\n", string(hookConf.Scheduler), hookStr, hookConf.Node.Address) + + dp := &dependencyShell{ + srcPath: hookConf.HookDir, + shell: hookConf.Hooks, + } + envs := make([]string, 9) + envs[0] = fmt.Sprintf("EGGO_CLUSTER_ID=%s", hookConf.ClusterID) + envs[1] = fmt.Sprintf("EGGO_CLUSTER_API_ENDPOINT=%s", hookConf.ClusterApiEndpoint) + envs[2] = fmt.Sprintf("EGGO_CLUSTER_CONFIG_DIR=%s", hookConf.ClusterConfigDir) + envs[3] = fmt.Sprintf("EGGO_NODE_IP=%s", hookConf.Node.Address) + envs[4] = fmt.Sprintf("EGGO_NODE_NAME=%s", hookConf.Node.Name) + envs[5] = fmt.Sprintf("EGGO_NODE_ARCH=%s", hookConf.Node.Arch) + envs[6] = fmt.Sprintf("EGGO_NODE_ROLE=%s", strings.Join(api.GetRoleString(hookConf.Node.Type), ",")) + envs[7] = fmt.Sprintf("EGGO_HOOK_TYPE=%s", hookConf.HookType) + envs[8] = fmt.Sprintf("EGGO_OPERATOR=%s", hookConf.Operator) + dp.envs = envs dependencyTask := task.NewTaskInstance(&DependencyTask{ dp: dp, }) - if api.IsCleanupSchedule(schedule) { + if api.IsCleanupSchedule(hookConf.Scheduler) { task.SetIgnoreErrorFlag(dependencyTask) } - if err := nodemanager.RunTaskOnNodes(dependencyTask, []string{hcf.Address}); err != nil { - logrus.Errorf("Hook %s failed for %s: %v", string(api.SchedulePreJoin), hcf.Address, err) + if err := nodemanager.RunTaskOnNodes(dependencyTask, []string{hookConf.Node.Address}); err != nil { + logrus.Errorf("Hook %s failed for %s: %v", string(api.SchedulePreJoin), hookConf.Node.Address, err) return err } return nil } +func executeShell(ccfg *api.ClusterConfig, role uint16, hcf *api.HostConfig, schedule api.ScheduleType) error { + shell := getShell(ccfg.RoleInfra[role], schedule) + if len(shell) == 0 { + return nil + } + + htype := api.PreHookType + if strings.HasPrefix(string(schedule), "post") { + htype = api.PostHookType + } + oper := api.HookOpJoin + if strings.HasSuffix(string(schedule), "cleanup") { + oper = api.HookOpCleanup + } + + hookConf := &api.HookRunConfig{ + ClusterID: ccfg.Name, + ClusterApiEndpoint: ccfg.APIEndpoint.GetUrl(), + ClusterConfigDir: ccfg.ConfigDir, + HookType: htype, + Operator: oper, + Node: hcf, + HookDir: path.Join(ccfg.PackageSrc.GetPkgDstPath(), constants.DefaultFilePath), + Hooks: shell, + } + + return ExecuteHooks(hookConf) +} + func HookSchedule(ccfg *api.ClusterConfig, nodes []*api.HostConfig, role []uint16, schedule api.ScheduleType) error { for _, n := range nodes { for _, r := range role { @@ -383,7 +427,7 @@ func HookSchedule(ccfg *api.ClusterConfig, nodes []*api.HostConfig, role []uint1 continue } - if err := ExecuteShell(ccfg.RoleInfra[r], ccfg.PackageSrc.GetPkgDstPath(), n, schedule); err != nil { + if err := executeShell(ccfg, r, n, schedule); err != nil { if api.IsCleanupSchedule(schedule) { logrus.Errorf("execute shell failed for %s at %s: %v", n.Address, string(schedule), err) } else { diff --git a/pkg/utils/runner/runner.go b/pkg/utils/runner/runner.go index c7088df..83a81e9 100644 --- a/pkg/utils/runner/runner.go +++ b/pkg/utils/runner/runner.go @@ -30,7 +30,6 @@ import ( "github.com/kubesphere/kubekey/pkg/util/ssh" "github.com/sirupsen/logrus" "isula.org/eggo/pkg/api" - "isula.org/eggo/pkg/constants" ) const ( @@ -164,7 +163,7 @@ func (ssh *SSHRunner) Reconnect() error { func clearUserTempDir(conn ssh.Connection, host *kkv1alpha1.HostCfg) { tmpShell := "/tmp/" + RunnerShellPrefix + "*" // scp to tmp file - dir := getCopyDefaultDir(host.User) + dir := api.GetUserTempDir(host.User) _, err := conn.Exec(fmt.Sprintf("sudo -E /bin/sh -c \"rm -rf %s; rm -rf %s\"", dir, tmpShell), host) if err != nil { logrus.Warnf("[%s] remove temp dir: %s failed: %v", host.Name, dir, err) @@ -175,7 +174,7 @@ func clearUserTempDir(conn ssh.Connection, host *kkv1alpha1.HostCfg) { func prepareUserTempDir(conn ssh.Connection, host *kkv1alpha1.HostCfg) error { // scp to tmp file - dir := getCopyDefaultDir(host.User) + dir := api.GetUserTempDir(host.User) var sb strings.Builder sb.WriteString("sudo -E /bin/sh -c \"") sb.WriteString(fmt.Sprintf("mkdir -p %s", dir)) @@ -191,18 +190,11 @@ func prepareUserTempDir(conn ssh.Connection, host *kkv1alpha1.HostCfg) error { return nil } -func getCopyDefaultDir(user string) string { - if user == "root" { - return constants.DefaultRootCopyTempDirHome + "/temp" - } - return fmt.Sprintf(constants.DefaultUserCopyTempHomeFormat, user) + "/temp" -} - func (ssh *SSHRunner) copyFile(src, dst string) error { if ssh.Conn == nil { return fmt.Errorf("[%s] SSH runner is not connected", ssh.Host.Name) } - tempDir := getCopyDefaultDir(ssh.Host.User) + tempDir := api.GetUserTempDir(ssh.Host.User) // scp to tmp file tempCpyFile := filepath.Join(tempDir, filepath.Base(src)) err := ssh.Conn.Scp(src, tempCpyFile) @@ -248,7 +240,7 @@ func (ssh *SSHRunner) copyDir(srcDir, dstDir string) error { logrus.Errorf("[%s] create cert tmp tar failed: %v", ssh.Host.Name, err) return err } - tmpCpyDir := getCopyDefaultDir(ssh.Host.User) + tmpCpyDir := api.GetUserTempDir(ssh.Host.User) tmpPkiFile := filepath.Join(tmpCpyDir, "pkg.tar") // scp to user home directory err = ssh.Copy(tmpPkgFile, tmpPkiFile) -- 2.25.1