github.com/sealerio/sealer@v0.11.1-0.20240507115618-f4f89c5853ae/pkg/cluster-runtime/hook.go (about) 1 // Copyright © 2022 Alibaba Group Holding Ltd. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package clusterruntime 16 17 import ( 18 "bufio" 19 "bytes" 20 "fmt" 21 "io" 22 "net" 23 "os" 24 "path/filepath" 25 "sort" 26 "strings" 27 28 "github.com/sirupsen/logrus" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 k8syaml "k8s.io/apimachinery/pkg/util/yaml" 32 33 "github.com/sealerio/sealer/common" 34 "github.com/sealerio/sealer/pkg/infradriver" 35 v1 "github.com/sealerio/sealer/types/api/v1" 36 netUtils "github.com/sealerio/sealer/utils/net" 37 "github.com/sealerio/sealer/utils/yaml" 38 ) 39 40 const ( 41 ShellHook HookType = "SHELL" 42 ) 43 44 const ( 45 //PreInstallCluster on master0 46 PreInstallCluster Phase = "pre-install" 47 //PostInstallCluster on master0 48 PostInstallCluster Phase = "post-install" 49 //PreUnInstallCluster on master0 50 PreUnInstallCluster Phase = "pre-uninstall" 51 //PostUnInstallCluster on master0 52 PostUnInstallCluster Phase = "post-uninstall" 53 //PreScaleUpCluster on master0 54 PreScaleUpCluster Phase = "pre-scaleup" 55 //PostScaleUpCluster on master0 56 PostScaleUpCluster Phase = "post-scaleup" 57 //UpgradeCluster on master0 58 UpgradeCluster Phase = "upgrade" 59 //RollbackCluster on master0 60 RollbackCluster Phase = "rollback" 61 62 //PreInitHost on role 63 PreInitHost Phase = "pre-init-host" 64 //PostInitHost on role 65 PostInitHost Phase = "post-init-host" 66 //PreCleanHost on role 67 PreCleanHost Phase = "pre-clean-host" 68 //PostCleanHost on role 69 PostCleanHost Phase = "post-clean-host" 70 //UpgradeHost on role 71 UpgradeHost Phase = "upgrade-host" 72 ) 73 74 type HookType string 75 76 type Scope string 77 78 type Phase string 79 80 const ( 81 ExtraOptionSkipWhenWorkspaceNotExists = "SkipWhenWorkspaceNotExists" 82 ) 83 84 type HookFunc func(data string, onHosts []net.IP, driver infradriver.InfraDriver, extraOpts map[string]bool) error 85 86 var hookFactories = make(map[HookType]HookFunc) 87 88 // HookConfig tell us how to configure hooks for cluster 89 type HookConfig struct { 90 // Name defines hook names, will run hooks in alphabetical order. 91 Name string `json:"name,omitempty"` 92 //Type defines different hook type, currently only have "SHELL","HOSTNAME". 93 Type HookType `json:"type,omitempty"` 94 // Data real hooks data will be applied at install process. 95 Data string `json:"data,omitempty"` 96 // Phase defines when to run hooks. 97 Phase Phase `json:"Phase,omitempty"` 98 // Scope defines which roles of node will be applied with hook Data 99 Scope Scope `json:"scope,omitempty"` 100 } 101 102 type HookConfigList []HookConfig 103 104 func (r HookConfigList) Len() int { return len(r) } 105 func (r HookConfigList) Swap(i, j int) { r[i], r[j] = r[j], r[i] } 106 func (r HookConfigList) Less(i, j int) bool { return r[i].Name < r[j].Name } 107 108 // runHostHook run host scope hook by Phase and only execute hook on the given host list. 109 func (i *Installer) runHostHook(phase Phase, hosts []net.IP) error { 110 hookConfigList, ok := i.hooks[phase] 111 if !ok { 112 logrus.Debugf("no hooks found at phase: %s", phase) 113 return nil 114 } 115 116 extraOpts := map[string]bool{} 117 if phase == PostCleanHost || phase == PreCleanHost { 118 extraOpts[ExtraOptionSkipWhenWorkspaceNotExists] = true 119 } 120 121 // sorted by hookConfig name in alphabetical order 122 sort.Sort(hookConfigList) 123 for _, hookConfig := range hookConfigList { 124 var targetHosts []net.IP 125 expectedHosts := i.getHostIPListByScope(hookConfig.Scope) 126 // Make sure each host got from Scope is in the given host ip list. 127 for _, expected := range expectedHosts { 128 if netUtils.IsInIPList(expected, hosts) { 129 targetHosts = append(targetHosts, expected) 130 } 131 } 132 133 if len(targetHosts) == 0 { 134 logrus.Debugf("no expected host found from hook %s", hookConfig.Name) 135 continue 136 } 137 138 logrus.Infof("start to run hook(%s) on host(%s)", hookConfig.Name, targetHosts) 139 if err := hookFactories[hookConfig.Type](hookConfig.Data, targetHosts, i.infraDriver, extraOpts); err != nil { 140 return fmt.Errorf("failed to run hook: %s", hookConfig.Name) 141 } 142 } 143 144 return nil 145 } 146 147 // runClusterHook run cluster scope hook by Phase that means will only execute hook on master0. 148 func (i *Installer) runClusterHook(master0 net.IP, phase Phase) error { 149 hookConfigList, ok := i.hooks[phase] 150 if !ok { 151 logrus.Debugf("no hooks found at phase: %s", phase) 152 return nil 153 } 154 // sorted by hookConfig name in alphabetical order 155 sort.Sort(hookConfigList) 156 157 extraOpts := map[string]bool{} 158 if phase == PreUnInstallCluster || phase == PostUnInstallCluster { 159 extraOpts[ExtraOptionSkipWhenWorkspaceNotExists] = true 160 } 161 162 for _, hookConfig := range hookConfigList { 163 logrus.Infof("start to run hook(%s) on host(%s)", hookConfig.Name, master0) 164 if err := hookFactories[hookConfig.Type](hookConfig.Data, []net.IP{master0}, i.infraDriver, extraOpts); err != nil { 165 return fmt.Errorf("failed to run hook: %s", hookConfig.Name) 166 } 167 } 168 169 return nil 170 } 171 172 // getHostIPListByScope get ip list for scope, support use '|' to specify multiple scopes, they are ORed 173 func (i *Installer) getHostIPListByScope(scope Scope) []net.IP { 174 var ret []net.IP 175 scopes := strings.Split(string(scope), "|") 176 for _, s := range scopes { 177 hosts := i.infraDriver.GetHostIPListByRole(strings.TrimSpace(s)) 178 179 // remove duplicates 180 for _, h := range hosts { 181 if !netUtils.IsInIPList(h, ret) { 182 ret = append(ret, h) 183 } 184 } 185 } 186 187 return ret 188 } 189 190 func NewShellHook() HookFunc { 191 return func(cmd string, hosts []net.IP, driver infradriver.InfraDriver, extraOpts map[string]bool) error { 192 rootfs := driver.GetClusterRootfsPath() 193 for _, ip := range hosts { 194 logrus.Infof("start to run hook on host %s", ip.String()) 195 wrappedCmd := fmt.Sprintf(common.CdAndExecCmd, rootfs, cmd) 196 if extraOpts[ExtraOptionSkipWhenWorkspaceNotExists] { 197 wrappedCmd = fmt.Sprintf(common.CdIfExistAndExecCmd, rootfs, rootfs, cmd) 198 } 199 200 err := driver.CmdAsync(ip, driver.GetHostEnv(ip), wrappedCmd) 201 if err != nil { 202 return fmt.Errorf("failed to run shell hook(%s) on host(%s): %v", wrappedCmd, ip.String(), err) 203 } 204 } 205 206 return nil 207 } 208 } 209 210 // Register different hook type with its HookFunc to hookFactories 211 func Register(name HookType, factory HookFunc) { 212 if factory == nil { 213 panic("Must not provide nil hookFactory") 214 } 215 _, registered := hookFactories[name] 216 if registered { 217 panic(fmt.Sprintf("hookFactory named %s already registered", name)) 218 } 219 220 hookFactories[name] = factory 221 } 222 223 func transferPluginsToHooks(plugins []v1.Plugin) (map[Phase]HookConfigList, error) { 224 hooks := make(map[Phase]HookConfigList) 225 226 for _, pluginConfig := range plugins { 227 pluginConfig.Spec.Data = strings.TrimSuffix(pluginConfig.Spec.Data, "\n") 228 hookType := HookType(pluginConfig.Spec.Type) 229 230 _, ok := hookFactories[hookType] 231 if !ok { 232 return nil, fmt.Errorf("hook type: %s is not registered", hookType) 233 } 234 235 //split pluginConfig.Spec.Action with "|" to support combined actions 236 phaseList := strings.Split(pluginConfig.Spec.Action, "|") 237 for _, phase := range phaseList { 238 if phase == "" { 239 continue 240 } 241 hookConfig := HookConfig{ 242 Name: pluginConfig.Name, 243 Data: pluginConfig.Spec.Data, 244 Type: hookType, 245 Phase: Phase(phase), 246 Scope: Scope(pluginConfig.Spec.Scope), 247 } 248 249 if _, ok = hooks[hookConfig.Phase]; !ok { 250 // add new Phase 251 hooks[hookConfig.Phase] = []HookConfig{hookConfig} 252 } else { 253 hooks[hookConfig.Phase] = append(hooks[hookConfig.Phase], hookConfig) 254 } 255 } 256 } 257 return hooks, nil 258 } 259 260 // LoadPluginsFromFile load plugin config files from $rootfs/plugins dir. 261 func LoadPluginsFromFile(pluginPath string) ([]v1.Plugin, error) { 262 _, err := os.Stat(pluginPath) 263 if os.IsNotExist(err) { 264 return nil, nil 265 } 266 267 files, err := os.ReadDir(pluginPath) 268 if err != nil { 269 return nil, fmt.Errorf("failed to ReadDir plugin dir %s: %v", pluginPath, err) 270 } 271 272 var plugins []v1.Plugin 273 for _, f := range files { 274 if !yaml.Matcher(f.Name()) { 275 continue 276 } 277 pluginFile := filepath.Join(pluginPath, f.Name()) 278 pluginList, err := decodePluginFile(pluginFile) 279 if err != nil { 280 return nil, fmt.Errorf("failed to decode plugin file %s: %v", pluginFile, err) 281 } 282 plugins = append(plugins, pluginList...) 283 } 284 285 return plugins, nil 286 } 287 288 func decodePluginFile(pluginFile string) ([]v1.Plugin, error) { 289 var plugins []v1.Plugin 290 data, err := os.ReadFile(filepath.Clean(pluginFile)) 291 if err != nil { 292 return nil, err 293 } 294 295 decoder := k8syaml.NewYAMLToJSONDecoder(bufio.NewReaderSize(bytes.NewReader(data), 4096)) 296 for { 297 ext := runtime.RawExtension{} 298 if err := decoder.Decode(&ext); err != nil { 299 if err == io.EOF { 300 return plugins, nil 301 } 302 return nil, err 303 } 304 305 ext.Raw = bytes.TrimSpace(ext.Raw) 306 if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) { 307 continue 308 } 309 metaType := metav1.TypeMeta{} 310 if err := k8syaml.Unmarshal(ext.Raw, &metaType); err != nil { 311 return nil, fmt.Errorf("failed to decode TypeMeta: %v", err) 312 } 313 314 var plu v1.Plugin 315 if err := k8syaml.Unmarshal(ext.Raw, &plu); err != nil { 316 return nil, fmt.Errorf("failed to decode %s[%s]: %v", metaType.Kind, metaType.APIVersion, err) 317 } 318 319 plu.Spec.Data = strings.TrimSuffix(plu.Spec.Data, "\n") 320 plugins = append(plugins, plu) 321 } 322 } 323 324 func init() { 325 Register(ShellHook, NewShellHook()) 326 }