kubekey/pkg/modules/template.go
zuoxuesong-worker d9c699f80a
feat: feat no root ssh (#2858)
feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh



feat: feat no root ssh

Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com>
2025-11-17 08:29:34 +00:00

325 lines
8.7 KiB
Go

/*
Copyright 2023 The KubeSphere Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package modules
import (
"context"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"strings"
"github.com/cockroachdb/errors"
kkcorev1alpha1 "github.com/kubesphere/kubekey/api/core/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"
"github.com/kubesphere/kubekey/v4/pkg/connector"
_const "github.com/kubesphere/kubekey/v4/pkg/const"
"github.com/kubesphere/kubekey/v4/pkg/converter/tmpl"
"github.com/kubesphere/kubekey/v4/pkg/project"
"github.com/kubesphere/kubekey/v4/pkg/variable"
)
/*
The Template module processes files using Go templates before copying them to remote hosts.
This module allows users to template files with variables before transferring them.
Configuration:
Users can specify source and destination paths:
template:
src: /path/to/file # required: local file path to template
dest: /remote/path # required: destination path on remote host
mode: 0644 # optional: file permissions (default: 0644)
Usage Examples in Playbook Tasks:
1. Basic file templating:
```yaml
- name: Template configuration file
template:
src: config.yaml.tmpl
dest: /etc/app/config.yaml
mode: 0644
register: template_result
```
2. Template with variables:
```yaml
- name: Template with variables
template:
src: app.conf.tmpl
dest: /etc/app/app.conf
register: app_config
```
Return Values:
- On success: Returns "Success" in stdout
- On failure: Returns error message in stderr
*/
type templateArgs struct {
src string
dest string
mode *uint32
}
func newTemplateArgs(_ context.Context, raw runtime.RawExtension, vars map[string]any) (*templateArgs, error) {
var err error
// check args
ta := &templateArgs{}
args := variable.Extension2Variables(raw)
ta.src, err = variable.StringVar(vars, args, "src")
if err != nil {
return nil, errors.New("\"src\" should be string")
}
ta.dest, err = variable.StringVar(vars, args, "dest")
if err != nil {
return nil, errors.New("\"dest\" should be string")
}
mode, err := variable.IntVar(vars, args, "mode")
if err != nil {
klog.V(4).InfoS("get mode error", "error", err)
} else {
if *mode < 0 || *mode > math.MaxUint32 {
return nil, errors.New("mode should be uint32")
}
ta.mode = ptr.To(uint32(*mode))
}
return ta, nil
}
// ModuleTemplate handles the "template" module, processing files with Go templates
func ModuleTemplate(ctx context.Context, options ExecOptions) (string, string, error) {
ha, err := options.getAllVariables()
if err != nil {
return StdoutFailed, StderrGetHostVariable, err
}
ta, err := newTemplateArgs(ctx, options.Args, ha)
if err != nil {
return StdoutFailed, StderrParseArgument, err
}
conn, err := options.getConnector(ctx)
if err != nil {
return StdoutFailed, StderrGetConnector, err
}
defer conn.Close(ctx)
if filepath.IsAbs(ta.src) {
return handleAbsoluteTemplate(ctx, ta, conn, ha)
}
return handleRelativeTemplate(ctx, ta, conn, ha, options)
}
func handleAbsoluteTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any) (string, string, error) {
fileInfo, err := os.Stat(ta.src)
if err != nil {
return StdoutFailed, "failed to get src file in local path", err
}
if fileInfo.IsDir() {
if err := ta.absDir(ctx, conn, vars); err != nil {
return StdoutFailed, "failed to template absolute dir", err
}
return StdoutSuccess, "", nil
}
data, err := os.ReadFile(ta.src)
if err != nil {
return StdoutFailed, "failed to read file", err
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
return StdoutFailed, "failed to template file", err
}
return StdoutSuccess, "", nil
}
func handleRelativeTemplate(ctx context.Context, ta *templateArgs, conn connector.Connector, vars map[string]any, options ExecOptions) (string, string, error) {
pj, err := project.New(ctx, options.Playbook, false)
if err != nil {
return StdoutFailed, "failed to get playbook", nil
}
relPath := filepath.Join(options.Task.Annotations[kkcorev1alpha1.TaskAnnotationRelativePath], _const.ProjectRolesTemplateDir, ta.src)
fileInfo, err := pj.Stat(relPath)
if err != nil {
return StdoutFailed, "failed to stat relative path", err
}
if fileInfo.IsDir() {
if err := handleRelativeDir(ctx, pj, relPath, ta, conn, vars); err != nil {
return StdoutFailed, "failed to template relative dir", err
}
return StdoutSuccess, "", nil
}
data, err := pj.ReadFile(relPath)
if err != nil {
return StdoutFailed, "failed to read relative file", err
}
if err := ta.readFile(ctx, string(data), fileInfo.Mode(), conn, vars); err != nil {
return StdoutFailed, "failed to template relative file", err
}
return StdoutSuccess, "", nil
}
func handleRelativeDir(ctx context.Context, pj project.Project, relPath string, ta *templateArgs, conn connector.Connector, vars map[string]any) error {
return pj.WalkDir(relPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() { // only deal file
return nil
}
info, err := d.Info()
if err != nil {
return errors.Wrapf(err, "failed to get file %q info", path)
}
mode := info.Mode()
if ta.mode != nil {
mode = os.FileMode(*ta.mode)
}
data, err := pj.ReadFile(path)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", path)
}
result, err := tmpl.Parse(vars, string(data))
if err != nil {
return errors.Wrapf(err, "failed to parse file %q", path)
}
dest := ta.dest
if strings.HasSuffix(ta.dest, "/") {
rel, err := pj.Rel(relPath, path)
if err != nil {
return errors.Wrap(err, "failed to get relative filepath")
}
dest = filepath.Join(ta.dest, rel)
}
tmpDest := filepath.Join("/tmp", dest)
if err = conn.PutFile(ctx, result, tmpDest, mode); err != nil {
return err
}
_, _, err = conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
})
}
// relFile when template.src is relative file, get file from project, parse it, and copy to remote.
func (ta templateArgs) readFile(ctx context.Context, data string, mode fs.FileMode, conn connector.Connector, vars map[string]any) error {
result, err := tmpl.Parse(vars, data)
if err != nil {
return err
}
dest := ta.dest
if strings.HasSuffix(ta.dest, "/") {
dest = filepath.Join(ta.dest, filepath.Base(ta.src))
}
if ta.mode != nil {
mode = os.FileMode(*ta.mode)
}
tmpDest := filepath.Join("/tmp", dest)
if err = conn.PutFile(ctx, result, tmpDest, mode); err != nil {
return err
}
_, _, err = conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
}
// absDir when template.src is absolute dir, get all files by os, parse it, and copy to remote.
func (ta templateArgs) absDir(ctx context.Context, conn connector.Connector, vars map[string]any) error {
if err := filepath.WalkDir(ta.src, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() { // only copy file
return nil
}
if err != nil {
return errors.WithStack(err)
}
// get file old mode
info, err := d.Info()
if err != nil {
return errors.Wrapf(err, "failed to get file %q info", path)
}
mode := info.Mode()
if ta.mode != nil {
mode = os.FileMode(*ta.mode)
}
// read file
data, err := os.ReadFile(path)
if err != nil {
return errors.Wrapf(err, "failed to read file %q", path)
}
result, err := tmpl.Parse(vars, string(data))
if err != nil {
return errors.Wrapf(err, "failed to parse file %q", path)
}
// copy file to remote
dest := ta.dest
if strings.HasSuffix(ta.dest, "/") {
rel, err := filepath.Rel(ta.src, path)
if err != nil {
return errors.Wrap(err, "failed to get relative filepath")
}
dest = filepath.Join(ta.dest, rel)
}
tmpDest := filepath.Join("/tmp", dest)
if err = conn.PutFile(ctx, result, tmpDest, mode); err != nil {
return err
}
_, _, err = conn.ExecuteCommand(ctx, fmt.Sprintf("mkdir -p %s\nmv %s %s", filepath.Dir(dest), tmpDest, dest))
return err
}); err != nil {
return errors.Wrapf(err, "failed to walk dir %q", ta.src)
}
return nil
}
func init() {
utilruntime.Must(RegisterModule("template", ModuleTemplate))
}