Skip to content

Commit 461f1ff

Browse files
committed
feat: add gather_facts cache
Signed-off-by: joyceliu <[email protected]>
1 parent a876b3c commit 461f1ff

File tree

9 files changed

+292
-95
lines changed

9 files changed

+292
-95
lines changed

pkg/connector/connector.go

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -59,55 +59,48 @@ type Connector interface {
5959
// if connector is not set. when host is localhost, use local connector, else use ssh connector
6060
// vars contains all inventory for host. It's best to define the connector info in inventory file.
6161
func NewConnector(host string, v variable.Variable) (Connector, error) {
62-
vars, err := v.Get(variable.GetAllVariable(host))
62+
ha, err := v.Get(variable.GetAllVariable(host))
6363
if err != nil {
6464
return nil, err
6565
}
66-
connectorVars := make(map[string]any)
67-
if c1, ok := vars.(map[string]any)[_const.VariableConnector]; ok {
68-
if c2, ok := c1.(map[string]any); ok {
69-
connectorVars = c2
70-
}
66+
vd, ok := ha.(map[string]any)
67+
if !ok {
68+
return nil, errors.Errorf("host: %s variable is not a map", host)
69+
}
70+
71+
workdir, err := v.Get(variable.GetWorkDir())
72+
if err != nil {
73+
return nil, err
74+
}
75+
wd, ok := workdir.(string)
76+
if !ok {
77+
return nil, errors.New("workdir in variable should be string")
7178
}
7279

73-
connectedType, _ := variable.StringVar(nil, connectorVars, _const.VariableConnectorType)
80+
connectedType, _ := variable.StringVar(nil, vd, _const.VariableConnector, _const.VariableConnectorType)
7481
switch connectedType {
7582
case connectedLocal:
76-
return newLocalConnector(connectorVars), nil
83+
return newLocalConnector(wd, vd), nil
7784
case connectedSSH:
78-
return newSSHConnector(host, connectorVars), nil
85+
return newSSHConnector(wd, host, vd), nil
7986
case connectedKubernetes:
80-
workdir, err := v.Get(variable.GetWorkDir())
81-
if err != nil {
82-
return nil, err
83-
}
84-
wd, ok := workdir.(string)
85-
if !ok {
86-
return nil, errors.New("workdir in variable should be string")
87-
}
88-
89-
return newKubernetesConnector(host, wd, connectorVars)
87+
return newKubernetesConnector(host, wd, vd)
9088
default:
9189
localHost, _ := os.Hostname()
9290
// get host in connector variable. if empty, set default host: host_name.
93-
hostParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorHost)
91+
hostParam, err := variable.StringVar(nil, vd, _const.VariableConnector, _const.VariableConnectorHost)
9492
if err != nil {
9593
klog.V(4).Infof("connector host is empty use: %s", host)
9694
hostParam = host
9795
}
9896
if host == _const.VariableLocalHost || host == localHost || isLocalIP(hostParam) {
99-
return newLocalConnector(connectorVars), nil
97+
return newLocalConnector(wd, vd), nil
10098
}
10199

102-
return newSSHConnector(host, connectorVars), nil
100+
return newSSHConnector(wd, host, vd), nil
103101
}
104102
}
105103

106-
// GatherFacts get host info.
107-
type GatherFacts interface {
108-
HostInfo(ctx context.Context) (map[string]any, error)
109-
}
110-
111104
// isLocalIP check if given ipAddr is local network ip
112105
func isLocalIP(ipAddr string) bool {
113106
addrs, err := net.InterfaceAddrs()

pkg/connector/gather_facts.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
"sync"
9+
10+
"gopkg.in/yaml.v3"
11+
"k8s.io/klog/v2"
12+
13+
_const "github.com/kubesphere/kubekey/v4/pkg/const"
14+
)
15+
16+
const (
17+
// gatherFactsCacheJSON indicates that facts should be cached in JSON format
18+
gatherFactsCacheJSON = "jsonfile"
19+
// gatherFactsCacheYAML indicates that facts should be cached in YAML format
20+
gatherFactsCacheYAML = "yamlfile"
21+
// gatherFactsCacheMemory indicates that facts should be cached in memory
22+
gatherFactsCacheMemory = "memory"
23+
)
24+
25+
var cache = &memoryCache{
26+
cache: make(map[string]map[string]any),
27+
}
28+
29+
type memoryCache struct {
30+
cache map[string]map[string]any
31+
cacheMutex sync.RWMutex
32+
}
33+
34+
// Get retrieves cached data for a host (thread-safe).
35+
func (m *memoryCache) Get(hostname string) (map[string]any, bool) {
36+
m.cacheMutex.RLock()
37+
defer m.cacheMutex.RUnlock()
38+
data, exists := m.cache[hostname]
39+
return data, exists
40+
}
41+
42+
// Set stores data for a host (thread-safe).
43+
func (m *memoryCache) Set(hostname string, data map[string]any) {
44+
m.cacheMutex.Lock()
45+
defer m.cacheMutex.Unlock()
46+
m.cache[hostname] = data
47+
}
48+
49+
// Delete removes the cached data for a specific hostname (thread-safe).
50+
func (m *memoryCache) Delete(hostname string) {
51+
m.cacheMutex.Lock()
52+
defer m.cacheMutex.Unlock()
53+
delete(m.cache, hostname)
54+
}
55+
56+
// GatherFacts defines an interface for retrieving host information
57+
type GatherFacts interface {
58+
// HostInfo returns a map of host facts gathered from the system
59+
HostInfo(ctx context.Context) (map[string]any, error)
60+
}
61+
62+
// cacheGatherFact implements GatherFacts with caching capabilities
63+
type cacheGatherFact struct {
64+
// inventoryName is the name of the host in the inventory
65+
inventoryName string
66+
// cacheType specifies the format to cache facts (json, yaml, or memory)
67+
cacheType string
68+
// workdir is the base directory for cache files
69+
workdir string
70+
// getHostInfoFn is the function that actually gathers host information
71+
getHostInfoFn func(context.Context) (map[string]any, error)
72+
}
73+
74+
// newCacheGatherFact creates a new cacheGatherFact instance
75+
func newCacheGatherFact(inventoryName, cacheType, workdir string, getHostInfoFn func(context.Context) (map[string]any, error)) *cacheGatherFact {
76+
return &cacheGatherFact{
77+
inventoryName: inventoryName,
78+
cacheType: cacheType,
79+
workdir: workdir,
80+
getHostInfoFn: getHostInfoFn,
81+
}
82+
}
83+
84+
// HostInfo retrieves host facts, either from cache or by gathering them directly
85+
func (c *cacheGatherFact) HostInfo(ctx context.Context) (map[string]any, error) {
86+
switch c.cacheType {
87+
case gatherFactsCacheJSON:
88+
// Build path for JSON cache file
89+
filename := filepath.Join(c.workdir, _const.RuntimeDir, _const.RuntimeGatherFactsCacheDir, c.inventoryName+".json")
90+
data, err := os.ReadFile(filename)
91+
if err != nil {
92+
klog.V(4).Infof("cannot get cache file from %q. get from remote", filename)
93+
// Cache miss - gather facts directly
94+
hostInfo, err := c.getHostInfoFn(ctx)
95+
if err != nil {
96+
return nil, err
97+
}
98+
// Store gathered facts in JSON cache
99+
cacheData, err := json.Marshal(hostInfo)
100+
if err != nil {
101+
return nil, err
102+
}
103+
return hostInfo, os.WriteFile(filename, cacheData, os.ModePerm)
104+
}
105+
// Cache hit - unmarshal and return cached data
106+
var result map[string]any
107+
return result, json.Unmarshal(data, &result)
108+
case gatherFactsCacheYAML:
109+
// Build path for YAML cache file
110+
filename := filepath.Join(c.workdir, _const.RuntimeDir, _const.RuntimeGatherFactsCacheDir, c.inventoryName+".yaml")
111+
data, err := os.ReadFile(filename)
112+
if err != nil {
113+
klog.V(4).Infof("cannot get cache file from %q. get from remote", filename)
114+
// Cache miss - gather facts directly
115+
hostInfo, err := c.getHostInfoFn(ctx)
116+
if err != nil {
117+
return nil, err
118+
}
119+
// Store gathered facts in YAML cache
120+
cacheData, err := yaml.Marshal(hostInfo)
121+
if err != nil {
122+
return nil, err
123+
}
124+
return hostInfo, os.WriteFile(filename, cacheData, os.ModePerm)
125+
}
126+
// Cache hit - unmarshal and return cached data
127+
var result map[string]any
128+
return result, yaml.Unmarshal(data, &result)
129+
case gatherFactsCacheMemory:
130+
if cached, exists := cache.Get(c.inventoryName); exists {
131+
return cached, nil
132+
}
133+
hostInfo, err := c.getHostInfoFn(ctx)
134+
if err != nil {
135+
return nil, err
136+
}
137+
cache.Set(c.inventoryName, hostInfo)
138+
return hostInfo, nil
139+
default: // don't get from cache
140+
// Clear cache before re-fetching
141+
switch c.cacheType {
142+
case gatherFactsCacheJSON:
143+
filename := filepath.Join(c.workdir, _const.RuntimeDir, _const.RuntimeGatherFactsCacheDir, c.inventoryName+".json")
144+
_ = os.Remove(filename)
145+
case gatherFactsCacheYAML:
146+
filename := filepath.Join(c.workdir, _const.RuntimeDir, _const.RuntimeGatherFactsCacheDir, c.inventoryName+".yaml")
147+
_ = os.Remove(filename)
148+
case gatherFactsCacheMemory:
149+
cache.Delete(c.inventoryName)
150+
}
151+
// todo clear cache
152+
return c.getHostInfoFn(ctx)
153+
}
154+
}

pkg/connector/kubernetes_connector.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ const kubeconfigRelPath = ".kube/config"
3535

3636
var _ Connector = &kubernetesConnector{}
3737

38-
func newKubernetesConnector(host string, workdir string, connectorVars map[string]any) (*kubernetesConnector, error) {
39-
kubeconfig, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorKubeconfig)
38+
func newKubernetesConnector(host string, workdir string, hostVars map[string]any) (*kubernetesConnector, error) {
39+
kubeconfig, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorKubeconfig)
4040
if err != nil && host != _const.VariableLocalHost {
4141
return nil, err
4242
}

pkg/connector/local_connector.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,30 @@ import (
3636
var _ Connector = &localConnector{}
3737
var _ GatherFacts = &localConnector{}
3838

39-
func newLocalConnector(connectorVars map[string]any) *localConnector {
40-
password, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPassword)
39+
func newLocalConnector(workdir string, hostVars map[string]any) *localConnector {
40+
password, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPassword)
4141
if err != nil { // password is not necessary when execute with root user.
4242
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.")
4343
}
44+
cacheType, _ := variable.StringVar(nil, hostVars, _const.VariableGatherFactsCache)
45+
connector := &localConnector{
46+
Password: password,
47+
Cmd: exec.New(),
48+
shell: defaultSHELL,
49+
}
50+
// Initialize the cacheGatherFact with a function that will call getHostInfoFromRemote
51+
connector.gatherFacts = newCacheGatherFact(_const.VariableLocalHost, cacheType, workdir, connector.getHostInfo)
4452

45-
return &localConnector{Password: password, Cmd: exec.New(), shell: defaultSHELL}
53+
return connector
4654
}
4755

4856
type localConnector struct {
4957
Password string
5058
Cmd exec.Interface
5159
// shell to execute command
5260
shell string
61+
62+
gatherFacts *cacheGatherFact
5363
}
5464

5565
// 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
116126
return output, errors.Wrapf(err, "failed to execute command")
117127
}
118128

119-
// HostInfo gathers and returns host information for the local host.
129+
// HostInfo from gatherFacts cache
120130
func (c *localConnector) HostInfo(ctx context.Context) (map[string]any, error) {
131+
return c.gatherFacts.HostInfo(ctx)
132+
}
133+
134+
// getHostInfo from remote
135+
func (c *localConnector) getHostInfo(ctx context.Context) (map[string]any, error) {
121136
switch runtime.GOOS {
122137
case "linux":
123138
// os information

pkg/connector/ssh_connector.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,45 +56,50 @@ func init() {
5656
var _ Connector = &sshConnector{}
5757
var _ GatherFacts = &sshConnector{}
5858

59-
func newSSHConnector(host string, connectorVars map[string]any) *sshConnector {
59+
func newSSHConnector(workdir, host string, hostVars map[string]any) *sshConnector {
6060
// get host in connector variable. if empty, set default host: host_name.
61-
hostParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorHost)
61+
hostParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorHost)
6262
if err != nil {
6363
klog.V(4).InfoS("get connector host failed use current hostname", "error", err)
6464
hostParam = host
6565
}
6666
// get port in connector variable. if empty, set default port: 22.
67-
portParam, err := variable.IntVar(nil, connectorVars, _const.VariableConnectorPort)
67+
portParam, err := variable.IntVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPort)
6868
if err != nil {
6969
klog.V(4).Infof("connector port is empty use: %v", defaultSSHPort)
7070
portParam = ptr.To(defaultSSHPort)
7171
}
7272
// get user in connector variable. if empty, set default user: root.
73-
userParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorUser)
73+
userParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorUser)
7474
if err != nil {
7575
klog.V(4).Infof("connector user is empty use: %s", defaultSSHUser)
7676
userParam = defaultSSHUser
7777
}
7878
// get password in connector variable. if empty, should connector by private key.
79-
passwdParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPassword)
79+
passwdParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPassword)
8080
if err != nil {
8181
klog.V(4).InfoS("connector password is empty use public key")
8282
}
8383
// get private key path in connector variable. if empty, set default path: /root/.ssh/id_rsa.
84-
keyParam, err := variable.StringVar(nil, connectorVars, _const.VariableConnectorPrivateKey)
84+
keyParam, err := variable.StringVar(nil, hostVars, _const.VariableConnector, _const.VariableConnectorPrivateKey)
8585
if err != nil {
8686
klog.V(4).Infof("ssh public key is empty, use: %s", defaultSSHPrivateKey)
8787
keyParam = defaultSSHPrivateKey
8888
}
89-
90-
return &sshConnector{
89+
cacheType, _ := variable.StringVar(nil, hostVars, _const.VariableGatherFactsCache)
90+
connector := &sshConnector{
9191
Host: hostParam,
9292
Port: *portParam,
9393
User: userParam,
9494
Password: passwdParam,
9595
PrivateKey: keyParam,
9696
shell: defaultSHELL,
9797
}
98+
99+
// Initialize the cacheGatherFact with a function that will call getHostInfoFromRemote
100+
connector.gatherFacts = newCacheGatherFact(_const.VariableLocalHost, cacheType, workdir, connector.getHostInfo)
101+
102+
return connector
98103
}
99104

100105
type sshConnector struct {
@@ -107,6 +112,8 @@ type sshConnector struct {
107112
client *ssh.Client
108113
// shell to execute command
109114
shell string
115+
116+
gatherFacts *cacheGatherFact
110117
}
111118

112119
// Init connector, get ssh.Client
@@ -291,8 +298,13 @@ func (c *sshConnector) ExecuteCommand(_ context.Context, cmd string) ([]byte, er
291298
return output, errors.Wrap(err, "failed to execute ssh command")
292299
}
293300

294-
// HostInfo for GatherFacts
301+
// HostInfo from gatherFacts cache
295302
func (c *sshConnector) HostInfo(ctx context.Context) (map[string]any, error) {
303+
return c.gatherFacts.HostInfo(ctx)
304+
}
305+
306+
// getHostInfo from remote
307+
func (c *sshConnector) getHostInfo(ctx context.Context) (map[string]any, error) {
296308
// os information
297309
osVars := make(map[string]any)
298310
var osRelease bytes.Buffer
@@ -329,6 +341,8 @@ func (c *sshConnector) HostInfo(ctx context.Context) (map[string]any, error) {
329341
}
330342
procVars[_const.VariableProcessMemory] = convertBytesToMap(mem.Bytes(), ":")
331343

344+
// persistence the hostInfo
345+
332346
return map[string]any{
333347
_const.VariableOS: osVars,
334348
_const.VariableProcess: procVars,

0 commit comments

Comments
 (0)