diff --git a/pkg/project/path.go b/pkg/project/path.go index 4cb44208..aed89ea7 100644 --- a/pkg/project/path.go +++ b/pkg/project/path.go @@ -171,6 +171,14 @@ func GetRoleDefaultsRelPath(baseRole string) []string { } } +// GetRoleDefaultsRelDirPath returns possible relative dir paths for a role's defaults files +// The format follows similar structure to role tasks +func GetRoleDefaultsRelDirPath(baseRole string) []string { + return []string{ + filepath.Join(baseRole, _const.ProjectRolesDefaultsDir, "main"), + } +} + // GetIncludeTaskRelPath returns possible relative paths for included task files // The format follows PathFormatIncludeTask structure func GetIncludeTaskRelPath(top string, source string, includeTask string) []string { diff --git a/pkg/project/project.go b/pkg/project/project.go index 76a8fcc1..f9828ece 100644 --- a/pkg/project/project.go +++ b/pkg/project/project.go @@ -33,6 +33,7 @@ import ( _const "github.com/kubesphere/kubekey/v4/pkg/const" "github.com/kubesphere/kubekey/v4/pkg/converter/tmpl" + "github.com/kubesphere/kubekey/v4/pkg/utils" "github.com/kubesphere/kubekey/v4/pkg/variable" ) @@ -169,7 +170,7 @@ func (f *project) loadPlaybook(basePlaybook string) error { // dealImportPlaybook handles the "import_playbook" argument in a play func (f *project) dealImportPlaybook(p kkprojectv1.Play, basePlaybook string) error { if p.ImportPlaybook != "" { - importPlaybook := f.getPath(GetImportPlaybookRelPath(basePlaybook, p.ImportPlaybook)) + importPlaybook, _ := f.getPath(GetImportPlaybookRelPath(basePlaybook, p.ImportPlaybook)) if importPlaybook == "" { return errors.Errorf("failed to find import_playbook %q base on %q. it's should be:\n %s", p.ImportPlaybook, basePlaybook, PathFormatImportPlaybook) } @@ -189,7 +190,7 @@ func (f *project) dealVarsFiles(p *kkprojectv1.Play, basePlaybook string) error if err != nil { return errors.Errorf("failed to parse varFile %q", varsFileStr) } - file := f.getPath(GetVarsFilesRelPath(basePlaybook, varsFile)) + file, _ := f.getPath(GetVarsFilesRelPath(basePlaybook, varsFile)) if file == "" { return errors.Errorf("failed to find vars_files %q base on %q. it's should be:\n %s", varsFile, basePlaybook, PathFormatVarsFile) } @@ -216,12 +217,12 @@ func (f *project) dealVarsFiles(p *kkprojectv1.Play, basePlaybook string) error } func (f *project) dealRole(role *kkprojectv1.Role, basePlaybook string) error { - baseRole := f.getPath(GetRoleRelPath(basePlaybook, role.Role)) + baseRole, _ := f.getPath(GetRoleRelPath(basePlaybook, role.Role)) if baseRole == "" { return errors.Errorf("failed to find role %q base on %q. it's should be:\n %s", role.Role, basePlaybook, PathFormatRole) } // deal dependency - if meta := f.getPath(GetRoleMetaRelPath(baseRole)); meta != "" { + if meta, _ := f.getPath(GetRoleMetaRelPath(baseRole)); meta != "" { mdata, err := fs.ReadFile(f.FS, meta) if err != nil { return errors.Wrapf(err, "failed to read role meta file %q", meta) @@ -238,7 +239,7 @@ func (f *project) dealRole(role *kkprojectv1.Role, basePlaybook string) error { } } // deal tasks - if task := f.getPath(GetRoleTaskRelPath(baseRole)); task != "" { + if task, _ := f.getPath(GetRoleTaskRelPath(baseRole)); task != "" { rdata, err := fs.ReadFile(f.FS, task) if err != nil { return errors.Wrapf(err, "failed to read file %q", task) @@ -250,29 +251,48 @@ func (f *project) dealRole(role *kkprojectv1.Role, basePlaybook string) error { role.Block = blocks } // deal defaults (optional) - if defaults := f.getPath(GetRoleDefaultsRelPath(baseRole)); defaults != "" { + if defaults, _ := f.getPath(GetRoleDefaultsRelPath(baseRole)); defaults != "" { data, err := fs.ReadFile(f.FS, defaults) if err != nil { return errors.Wrapf(err, "failed to read defaults variable file %q", defaults) } - var node yaml.Node - // Unmarshal the YAML document into a root node. - if err := yaml.Unmarshal(data, &node); err != nil { - return errors.Wrap(err, "failed to unmarshal YAML") + err = f.combineRoleVars(role, data) + if err != nil { + return err } - if node.Kind != yaml.DocumentNode || len(node.Content) != 1 { - return errors.Errorf("unsupport vars_files format. it should be single map file") - } - // combine map node - if node.Content[0].Kind == yaml.MappingNode { - // skip empty file - role.Vars = *variable.CombineMappingNode(&role.Vars, node.Content[0]) + } + if dirDefaults, info := f.getPath(GetRoleDefaultsRelDirPath(baseRole)); dirDefaults != "" { + // only handle [roles]/defaults/main directory,if file found but not a directory,skip file + if info.IsDir() { + err := utils.ReadDirFiles(f.FS, dirDefaults, func(data []byte) error { + return f.combineRoleVars(role, data) + }) + if err != nil { + return errors.Wrapf(err, "failed to read defaults variable file %q", dirDefaults) + } } } return nil } +func (f *project) combineRoleVars(role *kkprojectv1.Role, content []byte) error { + var node yaml.Node + // Unmarshal the YAML document into a root node. + if err := yaml.Unmarshal(content, &node); err != nil { + return errors.Wrap(err, "failed to unmarshal YAML") + } + if node.Kind != yaml.DocumentNode || len(node.Content) != 1 { + return errors.Errorf("unsupport vars_files format. it should be single map file") + } + // combine map node + if node.Content[0].Kind == yaml.MappingNode { + // skip empty file + role.Vars = *variable.CombineMappingNode(&role.Vars, node.Content[0]) + } + return nil +} + // dealRoleTask recursively processes the tasks for a given role and its dependencies. // It ensures that all dependent roles are processed before handling the current role's tasks. func (f *project) dealRoleTask(role *kkprojectv1.Role, basePlaybook string) error { @@ -282,7 +302,7 @@ func (f *project) dealRoleTask(role *kkprojectv1.Role, basePlaybook string) erro } } // Get the base path for the current role - baseRole := f.getPath(GetRoleRelPath(basePlaybook, role.Role)) + baseRole, _ := f.getPath(GetRoleRelPath(basePlaybook, role.Role)) // Process the tasks for the current role return f.dealBlock(baseRole, filepath.Join(baseRole, _const.ProjectRolesTasksDir), role.Block) } @@ -304,7 +324,7 @@ func (f *project) dealBlock(top string, source string, blocks []kkprojectv1.Bloc } case block.IncludeTasks != "": // it's an include_tasks directive // Resolve the path to the include_tasks file - includeTask := f.getPath(GetIncludeTaskRelPath(top, source, block.IncludeTasks)) + includeTask, _ := f.getPath(GetIncludeTaskRelPath(top, source, block.IncludeTasks)) if includeTask == "" { return errors.Errorf("failed to find include_task %q base on %q. it's should be:\n %s", block.IncludeTasks, source, PathFormatIncludeTask) } @@ -336,12 +356,12 @@ func (f *project) dealBlock(top string, source string, blocks []kkprojectv1.Bloc } // getPath returns the first valid path from a list of possible paths -func (f *project) getPath(paths []string) string { +func (f *project) getPath(paths []string) (string, fs.FileInfo) { for _, path := range paths { - if _, err := fs.Stat(f.FS, path); err == nil { - return path + if info, err := fs.Stat(f.FS, path); err == nil { + return path, info } } - return "" + return "", nil } diff --git a/pkg/utils/files.go b/pkg/utils/files.go new file mode 100644 index 00000000..20e12e17 --- /dev/null +++ b/pkg/utils/files.go @@ -0,0 +1,63 @@ +package utils + +import ( + "fmt" + "io" + "io/fs" + "strings" +) + +// ReadDirFiles read all file in input fs and dir +func ReadDirFiles(fsys fs.FS, dir string, handler func(data []byte) error) error { + d, err := fsys.Open(dir) + if err != nil { + return fmt.Errorf("failed to open path %s with error: %w", dir, err) + } + defer d.Close() + entries, err := d.(fs.ReadDirFile).ReadDir(-1) + if err != nil { + return fmt.Errorf("read dir %s failed with error: %w", dir, err) + } + for _, entry := range entries { + if entry.IsDir() { + // skip dir + continue + } + if !HasSuffixIn(entry.Name(), []string{"yaml", "yml"}) { + continue + } + filePath := dir + "/" + entry.Name() + if dir == "." { + filePath = entry.Name() + } + // open file + file, err := fsys.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %q with error: %w", filePath, err) + } + // read file content + content, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("read file %q failed with error: %w", filePath, err) + } + err = file.Close() + if err != nil { + return fmt.Errorf("close file %q failed with error: %w", filePath, err) + } + err = handler(content) + if err != nil { + return fmt.Errorf("handle file %q failed with error: %w", filePath, err) + } + } + return nil +} + +// HasSuffixIn check input string a end with one of slice b +func HasSuffixIn(a string, b []string) bool { + for _, suffix := range b { + if strings.HasSuffix(a, suffix) { + return true + } + } + return false +}