Skip to content

Commit ac16e4c

Browse files
micahleeGitHub Enterprise
authored andcommitted
Merge pull request #19 from Conjur-Enterprise/cnjr-11979-cluster-member-list
CNJR-11979: Add etcd cluster member list to inspect report
2 parents a36e7e7 + ecf2b57 commit ac16e4c

File tree

4 files changed

+417
-0
lines changed

4 files changed

+417
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1919
machine to the inspect report. CNJR-11976
2020
- Container command history check that records the recent command history from
2121
inside a container to the inspect report. CNJR-11978
22+
- Etcd cluster members check that captures the current cluster members
23+
(as reported by `evoke cluster member list`) in the inspect report. CNJR-11979
2224

2325
## [0.4.2] - 2025-03-25
2426

pkg/checks/etcd_cluster_members.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Package checks defines all of the possible Conjur Inspect checks that can
2+
// be run.
3+
package checks
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"strings"
10+
11+
"github.com/cyberark/conjur-inspect/pkg/check"
12+
"github.com/cyberark/conjur-inspect/pkg/container"
13+
"github.com/cyberark/conjur-inspect/pkg/log"
14+
"github.com/cyberark/conjur-inspect/pkg/shell"
15+
)
16+
17+
// Alias io.ReadAll as a variable so that we can stub it out for unit tests
18+
var readAllFuncClusterMembers = io.ReadAll
19+
20+
// EtcdClusterMembers collects the current etcd cluster members by running
21+
// `evoke cluster member list` in an enrolled cluster node
22+
type EtcdClusterMembers struct {
23+
Provider container.ContainerProvider
24+
}
25+
26+
// Describe provides a textual description of what this check gathers info on
27+
func (ecm *EtcdClusterMembers) Describe() string {
28+
return fmt.Sprintf("Etcd Cluster Members (%s)", ecm.Provider.Name())
29+
}
30+
31+
// Run executes the cluster member list command and saves the output
32+
func (ecm *EtcdClusterMembers) Run(runContext *check.RunContext) []check.Result {
33+
// If there is no container ID, return
34+
if strings.TrimSpace(runContext.ContainerID) == "" {
35+
return []check.Result{}
36+
}
37+
38+
container := ecm.Provider.Container(runContext.ContainerID)
39+
40+
// Check if node is enrolled in a cluster
41+
isEnrolled, err := ecm.isNodeEnrolled(container)
42+
if err != nil {
43+
return check.ErrorResult(ecm, err)
44+
}
45+
46+
// If not enrolled, return empty results
47+
if !isEnrolled {
48+
return []check.Result{}
49+
}
50+
51+
// Run evoke cluster member list command
52+
stdout, stderr, err := container.Exec("evoke", "cluster", "member", "list")
53+
if err != nil {
54+
stderrMsg := shell.ReadOrDefault(stderr, "N/A")
55+
return check.ErrorResult(
56+
ecm,
57+
fmt.Errorf("failed to get cluster members: %w (stderr: %s)", err, stderrMsg),
58+
)
59+
}
60+
61+
// Read the stdout data
62+
memberListOutput, err := readAllFuncClusterMembers(stdout)
63+
if err != nil {
64+
return check.ErrorResult(
65+
ecm,
66+
fmt.Errorf("failed to read cluster member list output: %w", err),
67+
)
68+
}
69+
70+
// Save raw output to OutputStore
71+
providerSuffix := strings.ToLower(ecm.Provider.Name())
72+
outputFileName := fmt.Sprintf("etcd-cluster-members-%s.txt", providerSuffix)
73+
_, saveErr := runContext.OutputStore.Save(outputFileName, strings.NewReader(string(memberListOutput)))
74+
if saveErr != nil {
75+
log.Warn("Failed to save cluster member list output: %w", saveErr)
76+
}
77+
78+
return []check.Result{}
79+
}
80+
81+
// isNodeEnrolled checks if the node is enrolled in a cluster by reading
82+
// /etc/cinc/solo.json and verifying conjur.cluster_name exists and is non-empty
83+
func (ecm *EtcdClusterMembers) isNodeEnrolled(container container.Container) (bool, error) {
84+
stdout, stderr, err := container.Exec("cat", "/etc/cinc/solo.json")
85+
if err != nil {
86+
stderrMsg := shell.ReadOrDefault(stderr, "N/A")
87+
return false, fmt.Errorf("failed to read solo.json: %w (stderr: %s)", err, stderrMsg)
88+
}
89+
90+
soloJSONBytes, err := readAllFuncClusterMembers(stdout)
91+
if err != nil {
92+
return false, fmt.Errorf("failed to read solo.json content: %w", err)
93+
}
94+
95+
// Parse solo.json to extract conjur.cluster_name
96+
var soloConfig map[string]interface{}
97+
err = json.Unmarshal(soloJSONBytes, &soloConfig)
98+
if err != nil {
99+
return false, fmt.Errorf("failed to parse solo.json: %w", err)
100+
}
101+
102+
// Check if conjur section exists
103+
conjurConfig, exists := soloConfig["conjur"]
104+
if !exists {
105+
return false, fmt.Errorf("conjur section not found in solo.json")
106+
}
107+
108+
conjurMap, ok := conjurConfig.(map[string]interface{})
109+
if !ok {
110+
return false, fmt.Errorf("conjur section is not a valid object in solo.json")
111+
}
112+
113+
// Check if cluster_name exists and is non-empty
114+
clusterName, exists := conjurMap["cluster_name"]
115+
if !exists {
116+
return false, nil // Not enrolled
117+
}
118+
119+
clusterNameStr, ok := clusterName.(string)
120+
if !ok {
121+
return false, fmt.Errorf("cluster_name is not a string in solo.json")
122+
}
123+
124+
return strings.TrimSpace(clusterNameStr) != "", nil
125+
}

0 commit comments

Comments
 (0)