Skip to content

Commit 7686607

Browse files
CyberArk(client): add CyberArk snapshot conversion and test utilities
- Introduced `ConvertDataReadingsToCyberarkSnapshot` to transform data readings into CyberArk snapshot format. - Enhanced `PostDataReadingsWithOptions` to utilize the new snapshot conversion. - Added `DynamicData` and `DiscoveryData` types for structured data handling. - Updated `DataGathererDynamic` and `DataGathererDiscovery` to return strongly typed data. - Implemented `ParseDataReadings` in `testutil` for decoding and testing data readings. - Added test data and golden file support for snapshot conversion validation. Signed-off-by: Richard Wall <[email protected]>
1 parent 8842333 commit 7686607

File tree

12 files changed

+395
-37
lines changed

12 files changed

+395
-37
lines changed

api/datareading.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package api
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"time"
7+
8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
"k8s.io/apimachinery/pkg/version"
610
)
711

812
// DataReadingsPost is the payload in the upload request.
@@ -28,8 +32,8 @@ type DataReading struct {
2832
type GatheredResource struct {
2933
// Resource is a reference to a k8s object that was found by the informer
3034
// should be of type unstructured.Unstructured, raw Object
31-
Resource interface{}
32-
DeletedAt Time
35+
Resource interface{} `json:"resource"`
36+
DeletedAt Time `json:"deleted_at,omitempty"`
3337
}
3438

3539
func (v GatheredResource) MarshalJSON() ([]byte, error) {
@@ -48,3 +52,32 @@ func (v GatheredResource) MarshalJSON() ([]byte, error) {
4852

4953
return json.Marshal(data)
5054
}
55+
56+
func (v *GatheredResource) UnmarshalJSON(data []byte) error {
57+
var tmpResource struct {
58+
Resource *unstructured.Unstructured `json:"resource"`
59+
DeletedAt Time `json:"deleted_at,omitempty"`
60+
}
61+
62+
d := json.NewDecoder(bytes.NewReader(data))
63+
d.DisallowUnknownFields()
64+
65+
if err := d.Decode(&tmpResource); err != nil {
66+
return err
67+
}
68+
v.Resource = tmpResource.Resource
69+
v.DeletedAt = tmpResource.DeletedAt
70+
return nil
71+
}
72+
73+
// DynamicData is the DataReading.Data returned by the k8s.DataGathererDynamic
74+
// gatherer
75+
type DynamicData struct {
76+
Items []*GatheredResource `json:"items"`
77+
}
78+
79+
// DiscoveryData is the DataReading.Data returned by the k8s.ConfigDiscovery
80+
// gatherer
81+
type DiscoveryData struct {
82+
ServerVersion *version.Info `json:"server_version"`
83+
}

pkg/agent/run.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,14 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf
305305
var readings []*api.DataReading
306306

307307
if config.InputPath != "" {
308+
// TODO(wallrj): The datareadings read from disk can not yet be pushed
309+
// to the CyberArk Discovery and Context API. Why? Because they have
310+
// simple data types such as map[string]interface{}. In contrast, the
311+
// data from data gatherers can be cast to rich types like DynamicData
312+
// or DiscoveryData The CyberArk dataupload client requires the data to
313+
// have rich types to convert it to the Discovery and Context snapshots
314+
// format. Consider refactoring testutil.ParseDataReadings so that it
315+
// can be used here.
308316
log.V(logs.Debug).Info("Reading data from local file", "inputPath", config.InputPath)
309317
data, err := os.ReadFile(config.InputPath)
310318
if err != nil {

pkg/client/client_cyberark.go

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"net/http"
77

8+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
9+
810
"github.com/jetstack/preflight/api"
911
"github.com/jetstack/preflight/pkg/internal/cyberark"
1012
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
@@ -49,14 +51,111 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
4951
return fmt.Errorf("while initializing data upload client: %s", err)
5052
}
5153

52-
err = datauploadClient.PutSnapshot(ctx, dataupload.Snapshot{
53-
// Temporary hard coded cluster ID.
54-
// TODO(wallrj): The clusterID will eventually be extracted from the supplied readings.
55-
ClusterID: "success-cluster-id",
56-
AgentVersion: version.PreflightVersion,
57-
})
54+
snapshot, err := ConvertDataReadingsToCyberarkSnapshot(readings)
55+
if err != nil {
56+
return fmt.Errorf("while converting data readings: %s", err)
57+
}
58+
// Temporary hard coded cluster ID.
59+
// TODO(wallrj): The clusterID will eventually be extracted from the supplied readings.
60+
snapshot.ClusterID = "success-cluster-id"
61+
62+
err = datauploadClient.PutSnapshot(ctx, snapshot)
5863
if err != nil {
5964
return fmt.Errorf("while uploading snapshot: %s", err)
6065
}
6166
return nil
6267
}
68+
69+
type resourceData map[string][]*unstructured.Unstructured
70+
71+
// The names of Datagatherers which have the data to populate the Cyberark
72+
// Snapshot mapped to the key in the Cyberark snapshot.
73+
var gathererNameToResourceDataKeyMap = map[string]string{
74+
"ark/secrets": "secrets",
75+
"ark/serviceaccounts": "serviceaccounts",
76+
"ark/roles": "roles",
77+
"ark/clusterroles": "clusterroles",
78+
"ark/rolebindings": "rolebindings",
79+
"ark/clusterrolebindings": "clusterrolebindings",
80+
"ark/jobs": "jobs",
81+
"ark/cronjobs": "cronjobs",
82+
"ark/deployments": "deployments",
83+
"ark/statefulsets": "statefulsets",
84+
"ark/daemonsets": "daemonsets",
85+
"ark/pods": "pods",
86+
}
87+
88+
// extractServerVersionFromReading converts the opaque data from a DiscoveryData
89+
// data reding to allow access to the Kubernetes version fields within.
90+
func extractServerVersionFromReading(reading *api.DataReading) (string, error) {
91+
data, ok := reading.Data.(*api.DiscoveryData)
92+
if !ok {
93+
return "", fmt.Errorf("failed to convert data: %s", reading.DataGatherer)
94+
}
95+
if data.ServerVersion == nil {
96+
return "unknown", nil
97+
}
98+
return data.ServerVersion.GitVersion, nil
99+
}
100+
101+
// extractResourceListFromReading converts the opaque data from a DynamicData
102+
// data reading to Unstructured resources, to allow access to the metadata and
103+
// other kubernetes API fields.
104+
func extractResourceListFromReading(reading *api.DataReading) ([]*unstructured.Unstructured, error) {
105+
data, ok := reading.Data.(*api.DynamicData)
106+
if !ok {
107+
return nil, fmt.Errorf("failed to convert data: %s", reading.DataGatherer)
108+
}
109+
items := data.Items
110+
resources := make([]*unstructured.Unstructured, len(items))
111+
for i, item := range items {
112+
if resource, ok := item.Resource.(*unstructured.Unstructured); ok {
113+
resources[i] = resource
114+
} else {
115+
return nil, fmt.Errorf("failed to convert resource: %#v", item)
116+
}
117+
}
118+
return resources, nil
119+
}
120+
121+
// ConvertDataReadingsToCyberarkSnapshot converts DataReadings to the Cyberark
122+
// Snapshot format.
123+
func ConvertDataReadingsToCyberarkSnapshot(
124+
readings []*api.DataReading,
125+
) (s dataupload.Snapshot, _ error) {
126+
k8sVersion := ""
127+
resourceData := resourceData{}
128+
for _, reading := range readings {
129+
if reading.DataGatherer == "ark/discovery" {
130+
var err error
131+
k8sVersion, err = extractServerVersionFromReading(reading)
132+
if err != nil {
133+
return s, fmt.Errorf("while extracting server version from data-reading: %s", err)
134+
}
135+
}
136+
if key, found := gathererNameToResourceDataKeyMap[reading.DataGatherer]; found {
137+
resources, err := extractResourceListFromReading(reading)
138+
if err != nil {
139+
return s, fmt.Errorf("while extracting resource list from data-reading: %s", err)
140+
}
141+
resourceData[key] = append(resourceData[key], resources...)
142+
}
143+
}
144+
145+
return dataupload.Snapshot{
146+
AgentVersion: version.PreflightVersion,
147+
K8SVersion: k8sVersion,
148+
Secrets: resourceData["secrets"],
149+
ServiceAccounts: resourceData["serviceaccounts"],
150+
Roles: resourceData["roles"],
151+
ClusterRoles: resourceData["clusterroles"],
152+
RoleBindings: resourceData["rolebindings"],
153+
ClusterRoleBindings: resourceData["clusterrolebindings"],
154+
Jobs: resourceData["jobs"],
155+
CronJobs: resourceData["cronjobs"],
156+
Deployments: resourceData["deployments"],
157+
Statefulsets: resourceData["statefulsets"],
158+
Daemonsets: resourceData["daemonsets"],
159+
Pods: resourceData["pods"],
160+
}, nil
161+
}

pkg/client/client_cyberark_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@ package client_test
22

33
import (
44
"crypto/x509"
5+
"encoding/json"
56
"errors"
7+
"os"
68
"testing"
79

810
"github.com/jetstack/venafi-connection-lib/http_client"
11+
"github.com/stretchr/testify/assert"
912
"github.com/stretchr/testify/require"
1013
"k8s.io/client-go/transport"
1114
"k8s.io/klog/v2"
@@ -39,9 +42,7 @@ func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) {
3942
require.NoError(t, err)
4043

4144
var readings []*api.DataReading
42-
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
43-
ClusterID: "success-cluster-id",
44-
})
45+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{})
4546
require.NoError(t, err)
4647
})
4748
}
@@ -69,10 +70,25 @@ func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
6970
}
7071
require.NoError(t, err)
7172
}
72-
var readings []*api.DataReading
73-
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
74-
ClusterID: "success-cluster-id",
75-
})
73+
readings := testutil.ParseDataReadings(t, testutil.ReadGZIP(t, "testdata/example-1/datareadings.json.gz"))
74+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{})
7675
require.NoError(t, err)
7776
})
7877
}
78+
79+
func TestConvertDataReadingsToCyberarkSnapshot(t *testing.T) {
80+
dataReadings := testutil.ParseDataReadings(t, testutil.ReadGZIP(t, "testdata/example-1/datareadings.json.gz"))
81+
snapshot, err := client.ConvertDataReadingsToCyberarkSnapshot(dataReadings)
82+
require.NoError(t, err)
83+
84+
actualSnapshotBytes, err := json.MarshalIndent(snapshot, "", " ")
85+
require.NoError(t, err)
86+
87+
goldenFilePath := "testdata/example-1/snapshot.json.gz"
88+
if _, update := os.LookupEnv("UPDATE_GOLDEN_FILES"); update {
89+
testutil.WriteGZIP(t, goldenFilePath, actualSnapshotBytes)
90+
} else {
91+
expectedSnapshotBytes := testutil.ReadGZIP(t, goldenFilePath)
92+
assert.JSONEq(t, string(expectedSnapshotBytes), string(actualSnapshotBytes))
93+
}
94+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# README
2+
3+
Data captured from a cert-manager E2E test cluster.
4+
5+
```bash
6+
cd cert-manager
7+
make e2e-setup
8+
```
9+
10+
```bash
11+
cd jetstack-secure
12+
go run . agent \
13+
--log-level 6 \
14+
--one-shot \
15+
--agent-config-file pkg/client/testdata/example-1/agent.yaml \
16+
--output-path pkg/client/testdata/example-1/datareadings.json
17+
gzip pkg/internal/cyberark/dataupload/testdata/example-1/datareadings.json
18+
```
19+
20+
21+
To recreate the golden output file:
22+
23+
```bash
24+
UPDATE_GOLDEN_FILES=true go test ./pkg/client/... -run TestConvertDataReadingsToCyberarkSnapshot
25+
```
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
cluster_id: example-cluster-id
2+
organization_id: example-organization-id
3+
data-gatherers:
4+
# gather k8s apiserver version information
5+
- kind: k8s-discovery
6+
name: ark/discovery
7+
- kind: k8s-dynamic
8+
name: ark/secrets
9+
config:
10+
resource-type:
11+
version: v1
12+
resource: secrets
13+
field-selectors:
14+
- type!=kubernetes.io/service-account-token
15+
- type!=kubernetes.io/dockercfg
16+
- type!=kubernetes.io/dockerconfigjson
17+
- type!=kubernetes.io/basic-auth
18+
- type!=kubernetes.io/ssh-auth
19+
- type!=bootstrap.kubernetes.io/token
20+
- type!=helm.sh/release.v1
21+
- kind: k8s-dynamic
22+
name: ark/serviceaccounts
23+
config:
24+
resource-type:
25+
resource: serviceaccounts
26+
version: v1
27+
- kind: k8s-dynamic
28+
name: ark/roles
29+
config:
30+
resource-type:
31+
version: v1
32+
group: rbac.authorization.k8s.io
33+
resource: roles
34+
- kind: k8s-dynamic
35+
name: ark/clusterroles
36+
config:
37+
resource-type:
38+
version: v1
39+
group: rbac.authorization.k8s.io
40+
resource: clusterroles
41+
- kind: k8s-dynamic
42+
name: ark/rolebindings
43+
config:
44+
resource-type:
45+
version: v1
46+
group: rbac.authorization.k8s.io
47+
resource: rolebindings
48+
- kind: k8s-dynamic
49+
name: ark/clusterrolebindings
50+
config:
51+
resource-type:
52+
version: v1
53+
group: rbac.authorization.k8s.io
54+
resource: clusterrolebindings
55+
- kind: k8s-dynamic
56+
name: ark/jobs
57+
config:
58+
resource-type:
59+
version: v1
60+
group: batch
61+
resource: jobs
62+
- kind: k8s-dynamic
63+
name: ark/cronjobs
64+
config:
65+
resource-type:
66+
version: v1
67+
group: batch
68+
resource: cronjobs
69+
- kind: k8s-dynamic
70+
name: ark/deployments
71+
config:
72+
resource-type:
73+
version: v1
74+
group: apps
75+
resource: deployments
76+
- kind: k8s-dynamic
77+
name: ark/statefulsets
78+
config:
79+
resource-type:
80+
version: v1
81+
group: apps
82+
resource: statefulsets
83+
- kind: k8s-dynamic
84+
name: ark/daemonsets
85+
config:
86+
resource-type:
87+
version: v1
88+
group: apps
89+
resource: daemonsets
90+
- kind: k8s-dynamic
91+
name: ark/pods
92+
config:
93+
resource-type:
94+
version: v1
95+
resource: pods
Binary file not shown.
143 KB
Binary file not shown.

0 commit comments

Comments
 (0)