diff --git a/api/core/v1/config_types.go b/api/core/v1/config_types.go index 7105ae65..232be669 100644 --- a/api/core/v1/config_types.go +++ b/api/core/v1/config_types.go @@ -72,7 +72,7 @@ func (c *Config) MarshalJSON() ([]byte, error) { // This provides direct access to the config values stored in Spec.Object. func (c *Config) Value() map[string]any { if c.Spec.Object == nil { - return make(map[string]any) + c.Spec.Object = &unstructured.Unstructured{Object: make(map[string]any)} } return c.Spec.Object.(*unstructured.Unstructured).Object diff --git a/builtin/core/playbooks/hook/pre_install.yaml b/builtin/core/playbooks/hook/pre_install.yaml index 8e82003c..ef7779aa 100644 --- a/builtin/core/playbooks/hook/pre_install.yaml +++ b/builtin/core/playbooks/hook/pre_install.yaml @@ -2,7 +2,7 @@ - hosts: - all vars: - work_dir: /root/kubekey + # work_dir: default is /kubekey binary_dir: | {{ .work_dir }}/kubekey scripts_dir: | diff --git a/builtin/core/playbooks/precheck.yaml b/builtin/core/playbooks/precheck.yaml index 4cd15bea..db80546d 100644 --- a/builtin/core/playbooks/precheck.yaml +++ b/builtin/core/playbooks/precheck.yaml @@ -15,6 +15,7 @@ gather_facts: true tags: ["always"] roles: - - precheck/env_check + - role: precheck/env_check + tags: ["always"] - import_playbook: hook/post_install.yaml \ No newline at end of file diff --git a/cmd/kk/app/options/option.go b/cmd/kk/app/options/option.go index 8336d237..ac1c4edb 100644 --- a/cmd/kk/app/options/option.go +++ b/cmd/kk/app/options/option.go @@ -177,7 +177,7 @@ func (o *CommonOptions) Complete(playbook *kkcorev1.Playbook) error { o.Workdir = filepath.Join(wd, o.Workdir) } // Generate and complete the configuration. - if err := o.completeConfig(o.Config); err != nil { + if err := o.completeConfig(); err != nil { return err } playbook.Spec.Config = ptr.Deref(o.Config, kkcorev1.Config{}) @@ -196,7 +196,7 @@ func (o *CommonOptions) Complete(playbook *kkcorev1.Playbook) error { } // genConfig generate config by ConfigFile and set value by command args. -func (o *CommonOptions) completeConfig(config *kkcorev1.Config) error { +func (o *CommonOptions) completeConfig() error { // set value by command args if o.Workdir != "" { if err := unstructured.SetNestedField(o.Config.Value(), o.Workdir, _const.Workdir); err != nil { @@ -215,7 +215,7 @@ func (o *CommonOptions) completeConfig(config *kkcorev1.Config) error { if i == 0 || i == -1 { return errors.New("--set value should be k=v") } - if err := setValue(config, setVal[:i], setVal[i+1:]); err != nil { + if err := setValue(o.Config, setVal[:i], setVal[i+1:]); err != nil { return err } } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 65640d9f..67497b7a 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -59,55 +59,48 @@ type Connector interface { // if connector is not set. when host is localhost, use local connector, else use ssh connector // vars contains all inventory for host. It's best to define the connector info in inventory file. func NewConnector(host string, v variable.Variable) (Connector, error) { - vars, err := v.Get(variable.GetAllVariable(host)) + ha, err := v.Get(variable.GetAllVariable(host)) if err != nil { return nil, err } - connectorVars := make(map[string]any) - if c1, ok := vars.(map[string]any)[_const.VariableConnector]; ok { - if c2, ok := c1.(map[string]any); ok { - connectorVars = c2 - } + vd, ok := ha.(map[string]any) + if !ok { + return nil, errors.Errorf("host: %s variable is not a map", host) } - connectedType, _ := variable.StringVar(nil, connectorVars, _const.VariableConnectorType) + workdir, err := v.Get(variable.GetWorkDir()) + if err != nil { + return nil, err + } + wd, ok := workdir.(string) + if !ok { + return nil, errors.New("workdir in variable should be string") + } + + connectedType, _ := variable.StringVar(nil, vd, _const.VariableConnector, _const.VariableConnectorType) switch connectedType { case connectedLocal: - return newLocalConnector(connectorVars), nil + return newLocalConnector(wd, vd), nil case connectedSSH: - return newSSHConnector(host, connectorVars), nil + return newSSHConnector(wd, host, vd), nil case connectedKubernetes: - workdir, err := v.Get(variable.GetWorkDir()) - if err != nil { - return nil, err - } - wd, ok := workdir.(string) - if !ok { - return nil, errors.New("workdir in variable should be string") - } - - return newKubernetesConnector(host, wd, connectorVars) + return newKubernetesConnector(host, wd, vd) default: localHost, _ := os.Hostname() // get host in connector variable. if empty, set default host: host_name. - hostParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorHost) + hostParam, err := variable.StringVar(nil, vd, _const.VariableConnector, _const.VariableConnectorHost) if err != nil { klog.V(4).Infof("connector host is empty use: %s", host) hostParam = host } if host == _const.VariableLocalHost || host == localHost || isLocalIP(hostParam) { - return newLocalConnector(connectorVars), nil + return newLocalConnector(wd, vd), nil } - return newSSHConnector(host, connectorVars), nil + return newSSHConnector(wd, host, vd), nil } } -// GatherFacts get host info. -type GatherFacts interface { - HostInfo(ctx context.Context) (map[string]any, error) -} - // isLocalIP check if given ipAddr is local network ip func isLocalIP(ipAddr string) bool { addrs, err := net.InterfaceAddrs() diff --git a/pkg/connector/gather_facts.go b/pkg/connector/gather_facts.go new file mode 100644 index 00000000..9df0f996 --- /dev/null +++ b/pkg/connector/gather_facts.go @@ -0,0 +1,177 @@ +package connector + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "sync" + + "github.com/cockroachdb/errors" + "gopkg.in/yaml.v3" + "k8s.io/klog/v2" + + _const "github.com/kubesphere/kubekey/v4/pkg/const" +) + +const ( + // gatherFactsCacheJSON indicates that facts should be cached in JSON format + gatherFactsCacheJSON = "jsonfile" + // gatherFactsCacheYAML indicates that facts should be cached in YAML format + gatherFactsCacheYAML = "yamlfile" + // gatherFactsCacheMemory indicates that facts should be cached in memory + gatherFactsCacheMemory = "memory" +) + +var cache = &memoryCache{ + cache: make(map[string]map[string]any), +} + +type memoryCache struct { + cache map[string]map[string]any + cacheMutex sync.RWMutex +} + +// Get retrieves cached data for a host (thread-safe). +func (m *memoryCache) Get(hostname string) (map[string]any, bool) { + m.cacheMutex.RLock() + defer m.cacheMutex.RUnlock() + data, exists := m.cache[hostname] + return data, exists +} + +// Set stores data for a host (thread-safe). +func (m *memoryCache) Set(hostname string, data map[string]any) { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + m.cache[hostname] = data +} + +// Delete removes the cached data for a specific hostname (thread-safe). +func (m *memoryCache) Delete(hostname string) { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + delete(m.cache, hostname) +} + +// GatherFacts defines an interface for retrieving host information +type GatherFacts interface { + // HostInfo returns a map of host facts gathered from the system + HostInfo(ctx context.Context) (map[string]any, error) +} + +// cacheGatherFact implements GatherFacts with caching capabilities +type cacheGatherFact struct { + // inventoryName is the name of the host in the inventory + inventoryName string + // cacheType specifies the format to cache facts (json, yaml, or memory) + cacheType string + // cacheDir is the cache dir in local + cacheDir string + // getHostInfoFn is the function that actually gathers host information + getHostInfoFn func(context.Context) (map[string]any, error) +} + +// newCacheGatherFact creates a new cacheGatherFact instance +func newCacheGatherFact(inventoryName, cacheType, workdir string, getHostInfoFn func(context.Context) (map[string]any, error)) *cacheGatherFact { + return &cacheGatherFact{ + inventoryName: inventoryName, + cacheType: cacheType, + cacheDir: filepath.Join(workdir, _const.RuntimeDir, _const.RuntimeGatherFactsCacheDir), + getHostInfoFn: getHostInfoFn, + } +} + +// HostInfo returns host information from cache or fetches it remotely if not cached. +// The caching behavior depends on the configured cache type (JSON, YAML, or memory). +func (c *cacheGatherFact) HostInfo(ctx context.Context) (map[string]any, error) { + switch c.cacheType { + case gatherFactsCacheJSON: + return c.handleJSONCache(ctx) + case gatherFactsCacheYAML: + return c.handleYAMLCache(ctx) + case gatherFactsCacheMemory: + return c.handleMemoryCache(ctx) + default: + // fallback: delete possible cache and fetch directly + _ = os.Remove(filepath.Join(c.cacheDir, c.inventoryName+".json")) + _ = os.Remove(filepath.Join(c.cacheDir, c.inventoryName+".yaml")) + cache.Delete(c.inventoryName) + return c.getHostInfoFn(ctx) + } +} + +// ensureCacheDir ensures the cache directory exists, creating it if necessary +func (c *cacheGatherFact) ensureCacheDir() error { + if _, err := os.Stat(c.cacheDir); err != nil { + if os.IsNotExist(err) { + return os.MkdirAll(c.cacheDir, os.ModePerm) + } + return err + } + return nil +} + +// handleJSONCache handles caching host information in JSON format. +// It attempts to read from the cache file first, falling back to remote fetch if needed. +func (c *cacheGatherFact) handleJSONCache(ctx context.Context) (map[string]any, error) { + if err := c.ensureCacheDir(); err != nil { + return nil, errors.Wrapf(err, "json cache dir error for host %q", c.inventoryName) + } + filename := filepath.Join(c.cacheDir, c.inventoryName+".json") + data, err := os.ReadFile(filename) + if err != nil { + klog.V(4).Infof("JSON cache miss for %q. Fetching remotely.", filename) + return c.fetchAndCache(ctx, filename, json.Marshal) + } + var result map[string]any + return result, json.Unmarshal(data, &result) +} + +// handleYAMLCache handles caching host information in YAML format. +// It attempts to read from the cache file first, falling back to remote fetch if needed. +func (c *cacheGatherFact) handleYAMLCache(ctx context.Context) (map[string]any, error) { + if err := c.ensureCacheDir(); err != nil { + return nil, errors.Wrapf(err, "yaml cache dir error for host %q", c.inventoryName) + } + filename := filepath.Join(c.cacheDir, c.inventoryName+".yaml") + data, err := os.ReadFile(filename) + if err != nil { + klog.V(4).Infof("YAML cache miss for %q. Fetching remotely.", filename) + return c.fetchAndCache(ctx, filename, yaml.Marshal) + } + var result map[string]any + return result, yaml.Unmarshal(data, &result) +} + +// fetchAndCache fetches host information remotely and caches it to a file. +// marshalFn specifies how to marshal the data (JSON or YAML). +func (c *cacheGatherFact) fetchAndCache( + ctx context.Context, + filename string, + marshalFn func(any) ([]byte, error), +) (map[string]any, error) { + hostInfo, err := c.getHostInfoFn(ctx) + if err != nil { + return nil, err + } + data, err := marshalFn(hostInfo) + if err != nil { + return nil, err + } + return hostInfo, os.WriteFile(filename, data, os.ModePerm) +} + +// handleMemoryCache handles caching host information in memory. +// It checks the in-memory cache first, falling back to remote fetch if needed. +func (c *cacheGatherFact) handleMemoryCache(ctx context.Context) (map[string]any, error) { + if cached, exists := cache.Get(c.inventoryName); exists { + return cached, nil + } + hostInfo, err := c.getHostInfoFn(ctx) + if err != nil { + return nil, err + } + cache.Set(c.inventoryName, hostInfo) + return hostInfo, nil +} diff --git a/pkg/connector/kubernetes_connector.go b/pkg/connector/kubernetes_connector.go index 57215b19..10bae216 100644 --- a/pkg/connector/kubernetes_connector.go +++ b/pkg/connector/kubernetes_connector.go @@ -35,8 +35,8 @@ const kubeconfigRelPath = ".kube/config" var _ Connector = &kubernetesConnector{} -func newKubernetesConnector(host string, workdir string, connectorVars map[string]any) (*kubernetesConnector, error) { - kubeconfig, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorKubeconfig) +func newKubernetesConnector(host string, workdir string, hostVars map[string]any) (*kubernetesConnector, error) { + kubeconfig, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorKubeconfig) if err != nil && host != _const.VariableLocalHost { return nil, err } diff --git a/pkg/connector/local_connector.go b/pkg/connector/local_connector.go index 8b6b6dc6..30c3becf 100644 --- a/pkg/connector/local_connector.go +++ b/pkg/connector/local_connector.go @@ -36,13 +36,21 @@ import ( var _ Connector = &localConnector{} var _ GatherFacts = &localConnector{} -func newLocalConnector(connectorVars map[string]any) *localConnector { - password, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPassword) +func newLocalConnector(workdir string, hostVars map[string]any) *localConnector { + password, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPassword) if err != nil { // password is not necessary when execute with root user. klog.V(4).Info("Warning: Failed to obtain local connector password when executing command with sudo. Please ensure the 'kk' process is run by a root-privileged user.") } + cacheType, _ := variable.StringVar(nil, hostVars, _const.VariableGatherFactsCache) + connector := &localConnector{ + Password: password, + Cmd: exec.New(), + shell: defaultSHELL, + } + // Initialize the cacheGatherFact with a function that will call getHostInfoFromRemote + connector.gatherFacts = newCacheGatherFact(_const.VariableLocalHost, cacheType, workdir, connector.getHostInfo) - return &localConnector{Password: password, Cmd: exec.New(), shell: defaultSHELL} + return connector } type localConnector struct { @@ -50,6 +58,8 @@ type localConnector struct { Cmd exec.Interface // shell to execute command shell string + + gatherFacts *cacheGatherFact } // Init initializes the local connector. This method does nothing for localConnector. @@ -116,8 +126,13 @@ func (c *localConnector) ExecuteCommand(ctx context.Context, cmd string) ([]byte return output, errors.Wrapf(err, "failed to execute command") } -// HostInfo gathers and returns host information for the local host. +// HostInfo from gatherFacts cache func (c *localConnector) HostInfo(ctx context.Context) (map[string]any, error) { + return c.gatherFacts.HostInfo(ctx) +} + +// getHostInfo from remote +func (c *localConnector) getHostInfo(ctx context.Context) (map[string]any, error) { switch runtime.GOOS { case "linux": // os information diff --git a/pkg/connector/ssh_connector.go b/pkg/connector/ssh_connector.go index 9b9eb740..bd846081 100644 --- a/pkg/connector/ssh_connector.go +++ b/pkg/connector/ssh_connector.go @@ -56,38 +56,38 @@ func init() { var _ Connector = &sshConnector{} var _ GatherFacts = &sshConnector{} -func newSSHConnector(host string, connectorVars map[string]any) *sshConnector { +func newSSHConnector(workdir, host string, hostVars map[string]any) *sshConnector { // get host in connector variable. if empty, set default host: host_name. - hostParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorHost) + hostParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorHost) if err != nil { klog.V(4).InfoS("get connector host failed use current hostname", "error", err) hostParam = host } // get port in connector variable. if empty, set default port: 22. - portParam, err := variable.IntVar(nil, connectorVars, _const.VariableConnectorPort) + portParam, err := variable.IntVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPort) if err != nil { klog.V(4).Infof("connector port is empty use: %v", defaultSSHPort) portParam = ptr.To(defaultSSHPort) } // get user in connector variable. if empty, set default user: root. - userParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorUser) + userParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorUser) if err != nil { klog.V(4).Infof("connector user is empty use: %s", defaultSSHUser) userParam = defaultSSHUser } // get password in connector variable. if empty, should connector by private key. - passwdParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPassword) + passwdParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPassword) if err != nil { klog.V(4).InfoS("connector password is empty use public key") } // get private key path in connector variable. if empty, set default path: /root/.ssh/id_rsa. - keyParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPrivateKey) + keyParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPrivateKey) if err != nil { klog.V(4).Infof("ssh public key is empty, use: %s", defaultSSHPrivateKey) keyParam = defaultSSHPrivateKey } - - return &sshConnector{ + cacheType, _ := variable.StringVar(nil, hostVars, _const.VariableGatherFactsCache) + connector := &sshConnector{ Host: hostParam, Port: *portParam, User: userParam, @@ -95,6 +95,11 @@ func newSSHConnector(host string, connectorVars map[string]any) *sshConnector { PrivateKey: keyParam, shell: defaultSHELL, } + + // Initialize the cacheGatherFact with a function that will call getHostInfoFromRemote + connector.gatherFacts = newCacheGatherFact(_const.VariableLocalHost, cacheType, workdir, connector.getHostInfo) + + return connector } type sshConnector struct { @@ -107,6 +112,8 @@ type sshConnector struct { client *ssh.Client // shell to execute command shell string + + gatherFacts *cacheGatherFact } // Init connector, get ssh.Client @@ -291,8 +298,13 @@ func (c *sshConnector) ExecuteCommand(_ context.Context, cmd string) ([]byte, er return output, errors.Wrap(err, "failed to execute ssh command") } -// HostInfo for GatherFacts +// HostInfo from gatherFacts cache func (c *sshConnector) HostInfo(ctx context.Context) (map[string]any, error) { + return c.gatherFacts.HostInfo(ctx) +} + +// getHostInfo from remote +func (c *sshConnector) getHostInfo(ctx context.Context) (map[string]any, error) { // os information osVars := make(map[string]any) var osRelease bytes.Buffer @@ -329,6 +341,8 @@ func (c *sshConnector) HostInfo(ctx context.Context) (map[string]any, error) { } procVars[_const.VariableProcessMemory] = convertBytesToMap(mem.Bytes(), ":") + // persistence the hostInfo + return map[string]any{ _const.VariableOS: osVars, _const.VariableProcess: procVars, diff --git a/pkg/const/common.go b/pkg/const/common.go index ed66921d..40f5a8ea 100644 --- a/pkg/const/common.go +++ b/pkg/const/common.go @@ -17,7 +17,7 @@ limitations under the License. package _const // variable specific key in system -const ( // === From inventory === +const ( // === From Global Parameter === // VariableLocalHost is the default local host name in inventory. VariableLocalHost = "localhost" // VariableIPv4 is the ipv4 in inventory. @@ -42,6 +42,8 @@ const ( // === From inventory === VariableConnectorPrivateKey = "private_key" // VariableConnectorKubeconfig is connected auth key for VariableConnector. VariableConnectorKubeconfig = "kubeconfig" + // VariableGatherFactsCache type in runtimedir. support jsonfile, yamlfile, memory. + VariableGatherFactsCache = "fact_caching" ) const ( // === From system generate === diff --git a/pkg/const/workdir.go b/pkg/const/workdir.go index 096e7c5a..5ad70c56 100644 --- a/pkg/const/workdir.go +++ b/pkg/const/workdir.go @@ -41,19 +41,24 @@ work_dir/ |-- scripts_dir/ | |-- runtime/ -|-- group/version/ -| | |-- playbooks/ -| | | |-- namespace/ -| | | | |-- playbook.yaml -| | | | |-- /playbookName/variable/ -| | | | | |-- location.json -| | | | | |-- hostname.json -| | |-- tasks/ -| | | |-- namespace/ -| | | | |-- task.yaml -| | |-- inventories/ -| | | |-- namespace/ -| | | | |-- inventory.yaml +|-- | -- gather_facts_caches +|-- | -- | -- inventory +| |-- group/version/ +| | | |-- playbooks/ +| | | | |-- namespace/ +| | | | | |-- playbook.yaml +| | | | | |-- /playbookName/variable/ +| | | | | | |-- location.yaml +| | | | | | |-- inventory_name1.yaml +| | | | | | |-- inventory_name2.yaml +| +| | | |-- inventories/ +| | | | |-- namespace/ +| | | | | |-- inventory.yaml +| |-- group/version/ +| | | |-- tasks/ +| | | | |-- namespace/ +| | | | | |-- task.yaml | |-- kubernetes/ @@ -103,6 +108,9 @@ const BinaryImagesDir = "images" // RuntimeDir used to store runtime data for the current task execution. By default, its path is set to {{ .work_dir/runtime }}. const RuntimeDir = "runtime" +// RuntimeGatherFactsCacheDir is a fixed directory name under runtime, used to store cached host facts gathered during execution. +const RuntimeGatherFactsCacheDir = "gather_facts_caches" + // RuntimePlaybookDir stores playbook resources created during playbook execution. const RuntimePlaybookDir = "playbooks" diff --git a/pkg/modules/command_test.go b/pkg/modules/command_test.go index c6c4b4c3..c46c2bb5 100644 --- a/pkg/modules/command_test.go +++ b/pkg/modules/command_test.go @@ -39,7 +39,7 @@ func TestCommand(t *testing.T) { Variable: &testVariable{}, }, ctxFunc: context.Background, - exceptStderr: "failed to connector of \"\" error: host is not set", + exceptStderr: "failed to connector of \"\" error: workdir in variable should be string", }, { name: "exec command success", diff --git a/pkg/variable/helper.go b/pkg/variable/helper.go index c8eba16c..0f789700 100644 --- a/pkg/variable/helper.go +++ b/pkg/variable/helper.go @@ -20,11 +20,13 @@ import ( "reflect" "slices" "strconv" + "strings" "time" "github.com/cockroachdb/errors" kkcorev1 "github.com/kubesphere/kubekey/api/core/v1" "gopkg.in/yaml.v3" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/json" "k8s.io/klog/v2" @@ -203,32 +205,34 @@ func HostsInGroup(inv kkcorev1.Inventory, groupName string) []string { } // StringVar get string value by key -func StringVar(d map[string]any, args map[string]any, key string) (string, error) { - val, ok := args[key] - if !ok { - return "", errors.Errorf("cannot find variable %q", key) - } +func StringVar(ctx map[string]any, args map[string]any, keys ...string) (string, error) { // convert to string - sv, ok := val.(string) - if !ok { - return "", errors.Errorf("variable %q is not string", key) + sv, found, err := unstructured.NestedString(args, keys...) + if err != nil { + return "", errors.WithStack(err) + } + if !found { + return "", errors.Errorf("cannot find variable %q", strings.Join(keys, ".")) } - return tmpl.ParseFunc(d, sv, func(b []byte) string { return string(b) }) + return tmpl.ParseFunc(ctx, sv, func(b []byte) string { return string(b) }) } // StringSliceVar get string slice value by key -func StringSliceVar(d map[string]any, vars map[string]any, key string) ([]string, error) { - val, ok := vars[key] - if !ok { - return nil, errors.Errorf("cannot find variable %q", key) +func StringSliceVar(ctx map[string]any, args map[string]any, keys ...string) ([]string, error) { + val, found, err := unstructured.NestedFieldNoCopy(args, keys...) + if err != nil { + return nil, errors.WithStack(err) + } + if !found { + return nil, errors.Errorf("cannot find variable %q", strings.Join(keys, ".")) } switch valv := val.(type) { case []string: var ss []string for _, a := range valv { - as, err := tmpl.ParseFunc(d, a, func(b []byte) string { return string(b) }) + as, err := tmpl.ParseFunc(ctx, a, func(b []byte) string { return string(b) }) if err != nil { return nil, err } @@ -242,12 +246,12 @@ func StringSliceVar(d map[string]any, vars map[string]any, key string) ([]string for _, a := range valv { av, ok := a.(string) if !ok { - klog.V(6).InfoS("variable is not string", "key", key) + klog.V(6).InfoS("variable is not string", "key", keys) return nil, nil } - as, err := tmpl.ParseFunc(d, av, func(b []byte) string { return string(b) }) + as, err := tmpl.ParseFunc(ctx, av, func(b []byte) string { return string(b) }) if err != nil { return nil, err } @@ -257,7 +261,7 @@ func StringSliceVar(d map[string]any, vars map[string]any, key string) ([]string return ss, nil case string: - as, err := tmpl.Parse(d, valv) + as, err := tmpl.Parse(ctx, valv) if err != nil { return nil, err } @@ -269,16 +273,20 @@ func StringSliceVar(d map[string]any, vars map[string]any, key string) ([]string return []string{string(as)}, nil default: - return nil, errors.Errorf("unsupported variable %q type", key) + return nil, errors.Errorf("unsupported variable %q type", strings.Join(keys, ".")) } } // IntVar get int value by key -func IntVar(d map[string]any, vars map[string]any, key string) (*int, error) { - val, ok := vars[key] - if !ok { - return nil, errors.Errorf("cannot find variable %q", key) +func IntVar(ctx map[string]any, args map[string]any, keys ...string) (*int, error) { + val, found, err := unstructured.NestedFieldNoCopy(args, keys...) + if err != nil { + return nil, errors.WithStack(err) } + if !found { + return nil, errors.Errorf("cannot find variable %q", strings.Join(keys, ".")) + } + // default convert to int v := reflect.ValueOf(val) switch v.Kind() { @@ -287,34 +295,37 @@ func IntVar(d map[string]any, vars map[string]any, key string) (*int, error) { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: u := v.Uint() if u > uint64(^uint(0)>>1) { - return nil, errors.Errorf("variable %q value %d overflows int", key, u) + return nil, errors.Errorf("variable %q value %d overflows int", strings.Join(keys, "."), u) } return ptr.To(int(u)), nil case reflect.Float32, reflect.Float64: return ptr.To(int(v.Float())), nil case reflect.String: - vs, err := tmpl.ParseFunc(d, v.String(), func(b []byte) string { return string(b) }) + vs, err := tmpl.ParseFunc(ctx, v.String(), func(b []byte) string { return string(b) }) if err != nil { return nil, err } atoi, err := strconv.Atoi(vs) if err != nil { - return nil, errors.Wrapf(err, "failed to convert string %q to int of key %q", vs, key) + return nil, errors.Wrapf(err, "failed to convert string %q to int of key %q", vs, strings.Join(keys, ".")) } return ptr.To(atoi), nil default: - return nil, errors.Errorf("unsupported variable %q type", key) + return nil, errors.Errorf("unsupported variable %q type", strings.Join(keys, ".")) } } // BoolVar get bool value by key -func BoolVar(d map[string]any, args map[string]any, key string) (*bool, error) { - val, ok := args[key] - if !ok { - return nil, errors.Errorf("failed to find variable of key %q", key) +func BoolVar(ctx map[string]any, args map[string]any, keys ...string) (*bool, error) { + val, found, err := unstructured.NestedFieldNoCopy(args, keys...) + if err != nil { + return nil, errors.WithStack(err) + } + if !found { + return nil, errors.Errorf("cannot find variable %q", strings.Join(keys, ".")) } // default convert to int v := reflect.ValueOf(val) @@ -322,7 +333,7 @@ func BoolVar(d map[string]any, args map[string]any, key string) (*bool, error) { case reflect.Bool: return ptr.To(v.Bool()), nil case reflect.String: - vs, err := tmpl.ParseBool(d, v.String()) + vs, err := tmpl.ParseBool(ctx, v.String()) if err != nil { return nil, err } @@ -330,12 +341,12 @@ func BoolVar(d map[string]any, args map[string]any, key string) (*bool, error) { return ptr.To(vs), nil } - return nil, errors.Errorf("unsupported variable %q type", key) + return nil, errors.Errorf("unsupported variable %q type", strings.Join(keys, ".")) } // DurationVar get time.Duration value by key -func DurationVar(d map[string]any, args map[string]any, key string) (time.Duration, error) { - stringVar, err := StringVar(d, args, key) +func DurationVar(ctx map[string]any, args map[string]any, key string) (time.Duration, error) { + stringVar, err := StringVar(ctx, args, key) if err != nil { return 0, err } @@ -359,7 +370,7 @@ func Extension2Variables(ext runtime.RawExtension) map[string]any { // Extension2Slice convert runtime.RawExtension to slice // if runtime.RawExtension contains tmpl syntax, parse it. -func Extension2Slice(d map[string]any, ext runtime.RawExtension) []any { +func Extension2Slice(ctx map[string]any, ext runtime.RawExtension) []any { if len(ext.Raw) == 0 { return nil } @@ -370,7 +381,7 @@ func Extension2Slice(d map[string]any, ext runtime.RawExtension) []any { return data } // try converter template string - val, err := Extension2String(d, ext) + val, err := Extension2String(ctx, ext) if err != nil { klog.ErrorS(err, "extension2string error", "input", string(ext.Raw)) } @@ -384,7 +395,7 @@ func Extension2Slice(d map[string]any, ext runtime.RawExtension) []any { // Extension2String convert runtime.RawExtension to string. // if runtime.RawExtension contains tmpl syntax, parse it. -func Extension2String(d map[string]any, ext runtime.RawExtension) (string, error) { +func Extension2String(ctx map[string]any, ext runtime.RawExtension) (string, error) { if len(ext.Raw) == 0 { return "", nil } @@ -395,7 +406,7 @@ func Extension2String(d map[string]any, ext runtime.RawExtension) (string, error input = ns } - result, err := tmpl.Parse(d, input) + result, err := tmpl.Parse(ctx, input) if err != nil { return "", err }