feature: support defaults main dir (#2709)

feature: support defaults main dir



feature: support defaults main dir



feature: support defaults main dir



feature: support defaults main dir



feature: support defaults main dir



feature: support defaults main dir

Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com>
This commit is contained in:
zuoxuesong-worker 2025-08-19 10:36:50 +08:00 committed by GitHub
parent b49cfc7fc4
commit 68f3ee1fca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 114 additions and 23 deletions

View File

@ -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 {

View File

@ -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
}

63
pkg/utils/files.go Normal file
View File

@ -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
}