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  }