Skip to content

Commit cd44a79

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 5a2ae3c commit cd44a79

File tree

13 files changed

+411
-44
lines changed

13 files changed

+411
-44
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: 112 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/runtime"
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,118 @@ 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][]runtime.Object
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 reading 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(
94+
"programmer mistake: the DataReading must have data type *api.DiscoveryData. "+
95+
"This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data)
96+
}
97+
if data.ServerVersion == nil {
98+
return "unknown", nil
99+
}
100+
return data.ServerVersion.GitVersion, nil
101+
}
102+
103+
// extractResourceListFromReading converts the opaque data from a DynamicData
104+
// data reading to runtime.Object resources, to allow access to the metadata and
105+
// other kubernetes API fields.
106+
func extractResourceListFromReading(reading *api.DataReading) ([]runtime.Object, error) {
107+
data, ok := reading.Data.(*api.DynamicData)
108+
if !ok {
109+
return nil, fmt.Errorf(
110+
"programmer mistake: the DataReading must have data type *api.DynamicData. "+
111+
"This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data)
112+
}
113+
resources := make([]runtime.Object, len(data.Items))
114+
for i, item := range data.Items {
115+
if resource, ok := item.Resource.(runtime.Object); ok {
116+
resources[i] = resource
117+
} else {
118+
return nil, fmt.Errorf(
119+
"programmer mistake: the DynamicData items must have Resource type runtime.Object. "+
120+
"This item (%d) has Resource type %T", i, item.Resource)
121+
}
122+
}
123+
return resources, nil
124+
}
125+
126+
// ConvertDataReadingsToCyberarkSnapshot converts a list of DataReadings to a CyberArk Snapshot.
127+
// It extracts the Kubernetes version from the "ark/discovery" DataReading and
128+
// collects resources from other DataReadings based on their DataGatherer names.
129+
// If any required data is missing or cannot be converted, an error is returned.
130+
func ConvertDataReadingsToCyberarkSnapshot(
131+
readings []*api.DataReading,
132+
) (s dataupload.Snapshot, _ error) {
133+
k8sVersion := ""
134+
resourceData := resourceData{}
135+
for _, reading := range readings {
136+
if reading.DataGatherer == "ark/discovery" {
137+
var err error
138+
k8sVersion, err = extractServerVersionFromReading(reading)
139+
if err != nil {
140+
return s, fmt.Errorf("while extracting server version from data-reading: %s", err)
141+
}
142+
}
143+
if key, found := gathererNameToResourceDataKeyMap[reading.DataGatherer]; found {
144+
resources, err := extractResourceListFromReading(reading)
145+
if err != nil {
146+
return s, fmt.Errorf("while extracting resource list from data-reading: %s", err)
147+
}
148+
resourceData[key] = append(resourceData[key], resources...)
149+
}
150+
}
151+
152+
return dataupload.Snapshot{
153+
AgentVersion: version.PreflightVersion,
154+
K8SVersion: k8sVersion,
155+
Secrets: resourceData["secrets"],
156+
ServiceAccounts: resourceData["serviceaccounts"],
157+
Roles: resourceData["roles"],
158+
ClusterRoles: resourceData["clusterroles"],
159+
RoleBindings: resourceData["rolebindings"],
160+
ClusterRoleBindings: resourceData["clusterrolebindings"],
161+
Jobs: resourceData["jobs"],
162+
CronJobs: resourceData["cronjobs"],
163+
Deployments: resourceData["deployments"],
164+
Statefulsets: resourceData["statefulsets"],
165+
Daemonsets: resourceData["daemonsets"],
166+
Pods: resourceData["pods"],
167+
}, nil
168+
}

pkg/client/client_cyberark_test.go

Lines changed: 21 additions & 1 deletion
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"
@@ -67,8 +70,25 @@ func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
6770
}
6871
require.NoError(t, err)
6972
}
70-
var readings []*api.DataReading
73+
readings := testutil.ParseDataReadings(t, testutil.ReadGZIP(t, "testdata/example-1/datareadings.json.gz"))
7174
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{})
7275
require.NoError(t, err)
7376
})
7477
}
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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
data-gatherers:
2+
# gather k8s apiserver version information
3+
- kind: k8s-discovery
4+
name: ark/discovery
5+
- kind: k8s-dynamic
6+
name: ark/secrets
7+
config:
8+
resource-type:
9+
version: v1
10+
resource: secrets
11+
field-selectors:
12+
- type!=kubernetes.io/service-account-token
13+
- type!=kubernetes.io/dockercfg
14+
- type!=kubernetes.io/dockerconfigjson
15+
- type!=kubernetes.io/basic-auth
16+
- type!=kubernetes.io/ssh-auth
17+
- type!=bootstrap.kubernetes.io/token
18+
- type!=helm.sh/release.v1
19+
- kind: k8s-dynamic
20+
name: ark/serviceaccounts
21+
config:
22+
resource-type:
23+
resource: serviceaccounts
24+
version: v1
25+
- kind: k8s-dynamic
26+
name: ark/roles
27+
config:
28+
resource-type:
29+
version: v1
30+
group: rbac.authorization.k8s.io
31+
resource: roles
32+
- kind: k8s-dynamic
33+
name: ark/clusterroles
34+
config:
35+
resource-type:
36+
version: v1
37+
group: rbac.authorization.k8s.io
38+
resource: clusterroles
39+
- kind: k8s-dynamic
40+
name: ark/rolebindings
41+
config:
42+
resource-type:
43+
version: v1
44+
group: rbac.authorization.k8s.io
45+
resource: rolebindings
46+
- kind: k8s-dynamic
47+
name: ark/clusterrolebindings
48+
config:
49+
resource-type:
50+
version: v1
51+
group: rbac.authorization.k8s.io
52+
resource: clusterrolebindings
53+
- kind: k8s-dynamic
54+
name: ark/jobs
55+
config:
56+
resource-type:
57+
version: v1
58+
group: batch
59+
resource: jobs
60+
- kind: k8s-dynamic
61+
name: ark/cronjobs
62+
config:
63+
resource-type:
64+
version: v1
65+
group: batch
66+
resource: cronjobs
67+
- kind: k8s-dynamic
68+
name: ark/deployments
69+
config:
70+
resource-type:
71+
version: v1
72+
group: apps
73+
resource: deployments
74+
- kind: k8s-dynamic
75+
name: ark/statefulsets
76+
config:
77+
resource-type:
78+
version: v1
79+
group: apps
80+
resource: statefulsets
81+
- kind: k8s-dynamic
82+
name: ark/daemonsets
83+
config:
84+
resource-type:
85+
version: v1
86+
group: apps
87+
resource: daemonsets
88+
- kind: k8s-dynamic
89+
name: ark/pods
90+
config:
91+
resource-type:
92+
version: v1
93+
resource: pods
154 KB
Binary file not shown.
143 KB
Binary file not shown.

pkg/datagatherer/k8s/discovery.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"k8s.io/client-go/discovery"
88

9+
"github.com/jetstack/preflight/api"
910
"github.com/jetstack/preflight/pkg/datagatherer"
1011
)
1112

@@ -59,15 +60,12 @@ func (g *DataGathererDiscovery) WaitForCacheSync(ctx context.Context) error {
5960

6061
// Fetch will fetch discovery data from the apiserver, or return an error
6162
func (g *DataGathererDiscovery) Fetch() (interface{}, int, error) {
62-
data, err := g.cl.ServerVersion()
63+
serverVersion, err := g.cl.ServerVersion()
6364
if err != nil {
6465
return nil, -1, fmt.Errorf("failed to get server version: %v", err)
6566
}
6667

67-
response := map[string]interface{}{
68-
// data has type Info: https://godoc.org/k8s.io/apimachinery/pkg/version#Info
69-
"server_version": data,
70-
}
71-
72-
return response, len(response), nil
68+
return &api.DiscoveryData{
69+
ServerVersion: serverVersion,
70+
}, 1, nil
7371
}

0 commit comments

Comments
 (0)