Skip to content

Commit 26d6e59

Browse files
Merge pull request #684 from jetstack/VC-43403-inventory-api-2
[VC-43753] CyberArk(client): add CyberArk snapshot conversion
2 parents 2d030d4 + 6dd5dc2 commit 26d6e59

File tree

14 files changed

+690
-133
lines changed

14 files changed

+690
-133
lines changed

api/datareading.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package api
33
import (
44
"encoding/json"
55
"time"
6+
7+
"k8s.io/apimachinery/pkg/version"
68
)
79

810
// DataReadingsPost is the payload in the upload request.
@@ -48,3 +50,23 @@ func (v GatheredResource) MarshalJSON() ([]byte, error) {
4850

4951
return json.Marshal(data)
5052
}
53+
54+
// DynamicData is the DataReading.Data returned by the k8s.DataGathererDynamic
55+
// gatherer
56+
type DynamicData struct {
57+
// Items is a list of GatheredResource
58+
Items []*GatheredResource `json:"items"`
59+
}
60+
61+
// DiscoveryData is the DataReading.Data returned by the k8s.ConfigDiscovery
62+
// gatherer
63+
type DiscoveryData struct {
64+
// ClusterID is the unique ID of the Kubernetes cluster which this snapshot was taken from.
65+
// This is sourced from the kube-system namespace UID,
66+
// which is assumed to be stable for the lifetime of the cluster.
67+
// - https://github.com/kubernetes/kubernetes/issues/77487#issuecomment-489786023
68+
ClusterID string `json:"cluster_id"`
69+
// ServerVersion is the version information of the k8s apiserver
70+
// See https://godoc.org/k8s.io/apimachinery/pkg/version#Info
71+
ServerVersion *version.Info `json:"server_version"`
72+
}

examples/machinehub.yaml

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,126 @@
55
# export ARK_SUBDOMAIN= # your CyberArk tenant subdomain
66
# export ARK_USERNAME= # your CyberArk username
77
# export ARK_SECRET= # your CyberArk password
8+
#
9+
# OPTIONAL: the URL for the CyberArk Discovery API if not using the production environment
10+
# # export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
11+
#
812
# go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml
913

1014
data-gatherers:
11-
- kind: "dummy"
12-
name: "dummy"
15+
# Gather Kubernetes API server version information
16+
- name: ark/discovery
17+
kind: k8s-discovery
18+
19+
# Gather Kubernetes secrets, excluding specific types
20+
- name: ark/secrets
21+
kind: k8s-dynamic
22+
config:
23+
resource-type:
24+
version: v1
25+
resource: secrets
26+
field-selectors:
27+
- type!=kubernetes.io/service-account-token
28+
- type!=kubernetes.io/dockercfg
29+
- type!=kubernetes.io/dockerconfigjson
30+
- type!=kubernetes.io/basic-auth
31+
- type!=kubernetes.io/ssh-auth
32+
- type!=bootstrap.kubernetes.io/token
33+
- type!=helm.sh/release.v1
34+
35+
# Gather Kubernetes service accounts
36+
- name: ark/serviceaccounts
37+
kind: k8s-dynamic
38+
config:
39+
resource-type:
40+
resource: serviceaccounts
41+
version: v1
42+
43+
# Gather Kubernetes roles
44+
- name: ark/roles
45+
kind: k8s-dynamic
46+
config:
47+
resource-type:
48+
version: v1
49+
group: rbac.authorization.k8s.io
50+
resource: roles
51+
52+
# Gather Kubernetes cluster roles
53+
- name: ark/clusterroles
54+
kind: k8s-dynamic
55+
config:
56+
resource-type:
57+
version: v1
58+
group: rbac.authorization.k8s.io
59+
resource: clusterroles
60+
61+
# Gather Kubernetes role bindings
62+
- name: ark/rolebindings
63+
kind: k8s-dynamic
64+
config:
65+
resource-type:
66+
version: v1
67+
group: rbac.authorization.k8s.io
68+
resource: rolebindings
69+
70+
# Gather Kubernetes cluster role bindings
71+
- name: ark/clusterrolebindings
72+
kind: k8s-dynamic
73+
config:
74+
resource-type:
75+
version: v1
76+
group: rbac.authorization.k8s.io
77+
resource: clusterrolebindings
78+
79+
# Gather Kubernetes jobs
80+
- name: ark/jobs
81+
kind: k8s-dynamic
82+
config:
83+
resource-type:
84+
version: v1
85+
group: batch
86+
resource: jobs
87+
88+
# Gather Kubernetes cron jobs
89+
- name: ark/cronjobs
90+
kind: k8s-dynamic
91+
config:
92+
resource-type:
93+
version: v1
94+
group: batch
95+
resource: cronjobs
96+
97+
# Gather Kubernetes deployments
98+
- name: ark/deployments
99+
kind: k8s-dynamic
100+
config:
101+
resource-type:
102+
version: v1
103+
group: apps
104+
resource: deployments
105+
106+
# Gather Kubernetes stateful sets
107+
- name: ark/statefulsets
108+
kind: k8s-dynamic
109+
config:
110+
resource-type:
111+
version: v1
112+
group: apps
113+
resource: statefulsets
114+
115+
# Gather Kubernetes daemon sets
116+
- name: ark/daemonsets
117+
kind: k8s-dynamic
118+
config:
119+
resource-type:
120+
version: v1
121+
group: apps
122+
resource: daemonsets
123+
124+
# Gather Kubernetes pods
125+
- name: ark/pods
126+
kind: k8s-dynamic
127+
config:
128+
resource-type:
129+
version: v1
130+
resource: pods

pkg/client/client_cyberark.go

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

8+
"k8s.io/apimachinery/pkg/runtime"
9+
"k8s.io/apimachinery/pkg/util/sets"
10+
811
"github.com/jetstack/preflight/api"
912
"github.com/jetstack/preflight/pkg/internal/cyberark"
1013
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
@@ -36,10 +39,17 @@ func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
3639
}
3740

3841
// PostDataReadingsWithOptions uploads data readings to CyberArk.
42+
// It converts the supplied data readings into a snapshot format expected by CyberArk.
3943
// It initializes a data upload client with the configured HTTP client and credentials,
4044
// then uploads a snapshot.
4145
// The supplied Options are not used by this publisher.
4246
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, _ Options) error {
47+
var snapshot dataupload.Snapshot
48+
if err := convertDataReadings(defaultExtractorFunctions, readings, &snapshot); err != nil {
49+
return fmt.Errorf("while converting data readings: %s", err)
50+
}
51+
snapshot.AgentVersion = version.PreflightVersion
52+
4353
cfg, err := o.configLoader()
4454
if err != nil {
4555
return err
@@ -49,14 +59,134 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
4959
return fmt.Errorf("while initializing data upload client: %s", err)
5060
}
5161

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-
})
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+
// extractClusterIDAndServerVersionFromReading converts the opaque data from a DiscoveryData
70+
// data reading to allow access to the Kubernetes version fields within.
71+
func extractClusterIDAndServerVersionFromReading(reading *api.DataReading, target *dataupload.Snapshot) error {
72+
if reading == nil {
73+
return fmt.Errorf("programmer mistake: the DataReading must not be nil")
74+
}
75+
data, ok := reading.Data.(*api.DiscoveryData)
76+
if !ok {
77+
return fmt.Errorf(
78+
"programmer mistake: the DataReading must have data type *api.DiscoveryData. "+
79+
"This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data)
80+
}
81+
target.ClusterID = data.ClusterID
82+
if data.ServerVersion != nil {
83+
target.K8SVersion = data.ServerVersion.GitVersion
84+
}
85+
return nil
86+
}
87+
88+
// extractResourceListFromReading converts the opaque data from a DynamicData
89+
// data reading to runtime.Object resources, to allow access to the metadata and
90+
// other kubernetes API fields.
91+
func extractResourceListFromReading(reading *api.DataReading, target *[]runtime.Object) error {
92+
if reading == nil {
93+
return fmt.Errorf("programmer mistake: the DataReading must not be nil")
94+
}
95+
data, ok := reading.Data.(*api.DynamicData)
96+
if !ok {
97+
return fmt.Errorf(
98+
"programmer mistake: the DataReading must have data type *api.DynamicData. "+
99+
"This DataReading (%s) has data type %T", reading.DataGatherer, reading.Data)
100+
}
101+
resources := make([]runtime.Object, len(data.Items))
102+
for i, item := range data.Items {
103+
if resource, ok := item.Resource.(runtime.Object); ok {
104+
resources[i] = resource
105+
} else {
106+
return fmt.Errorf(
107+
"programmer mistake: the DynamicData items must have Resource type runtime.Object. "+
108+
"This item (%d) has Resource type %T", i, item.Resource)
109+
}
110+
}
111+
*target = resources
112+
return nil
113+
}
114+
115+
var defaultExtractorFunctions = map[string]func(*api.DataReading, *dataupload.Snapshot) error{
116+
"ark/discovery": extractClusterIDAndServerVersionFromReading,
117+
"ark/secrets": func(r *api.DataReading, s *dataupload.Snapshot) error {
118+
return extractResourceListFromReading(r, &s.Secrets)
119+
},
120+
"ark/serviceaccounts": func(r *api.DataReading, s *dataupload.Snapshot) error {
121+
return extractResourceListFromReading(r, &s.ServiceAccounts)
122+
},
123+
"ark/roles": func(r *api.DataReading, s *dataupload.Snapshot) error {
124+
return extractResourceListFromReading(r, &s.Roles)
125+
},
126+
"ark/clusterroles": func(r *api.DataReading, s *dataupload.Snapshot) error {
127+
return extractResourceListFromReading(r, &s.ClusterRoles)
128+
},
129+
"ark/rolebindings": func(r *api.DataReading, s *dataupload.Snapshot) error {
130+
return extractResourceListFromReading(r, &s.RoleBindings)
131+
},
132+
"ark/clusterrolebindings": func(r *api.DataReading, s *dataupload.Snapshot) error {
133+
return extractResourceListFromReading(r, &s.ClusterRoleBindings)
134+
},
135+
"ark/jobs": func(r *api.DataReading, s *dataupload.Snapshot) error {
136+
return extractResourceListFromReading(r, &s.Jobs)
137+
},
138+
"ark/cronjobs": func(r *api.DataReading, s *dataupload.Snapshot) error {
139+
return extractResourceListFromReading(r, &s.CronJobs)
140+
},
141+
"ark/deployments": func(r *api.DataReading, s *dataupload.Snapshot) error {
142+
return extractResourceListFromReading(r, &s.Deployments)
143+
},
144+
"ark/statefulsets": func(r *api.DataReading, s *dataupload.Snapshot) error {
145+
return extractResourceListFromReading(r, &s.Statefulsets)
146+
},
147+
"ark/daemonsets": func(r *api.DataReading, s *dataupload.Snapshot) error {
148+
return extractResourceListFromReading(r, &s.Daemonsets)
149+
},
150+
"ark/pods": func(r *api.DataReading, s *dataupload.Snapshot) error {
151+
return extractResourceListFromReading(r, &s.Pods)
152+
},
153+
}
154+
155+
// convertDataReadings processes a list of DataReadings using the provided
156+
// extractor functions to populate the fields of the target snapshot.
157+
// It ensures that all expected data gatherers are handled and that there are
158+
// no unhandled data gatherers. If any discrepancies are found, or if any
159+
// extractor function returns an error, it returns an error.
160+
// The extractorFunctions map should contain functions for each expected
161+
// DataGatherer name, which will be called with the corresponding DataReading
162+
// and the target snapshot to populate the relevant fields.
163+
func convertDataReadings(
164+
extractorFunctions map[string]func(*api.DataReading, *dataupload.Snapshot) error,
165+
readings []*api.DataReading,
166+
target *dataupload.Snapshot,
167+
) error {
168+
expectedDataGatherers := sets.KeySet(extractorFunctions)
169+
unhandledDataGatherers := sets.New[string]()
170+
missingDataGatherers := expectedDataGatherers.Clone()
171+
for _, reading := range readings {
172+
dataGathererName := reading.DataGatherer
173+
extractFunc, found := extractorFunctions[dataGathererName]
174+
if !found {
175+
unhandledDataGatherers.Insert(dataGathererName)
176+
continue
177+
}
178+
missingDataGatherers.Delete(dataGathererName)
179+
// Call the extractor function to populate the relevant field in the target snapshot.
180+
if err := extractFunc(reading, target); err != nil {
181+
return fmt.Errorf("while extracting data reading %s: %s", dataGathererName, err)
182+
}
183+
}
184+
if missingDataGatherers.Len() > 0 || unhandledDataGatherers.Len() > 0 {
185+
return fmt.Errorf(
186+
"unexpected data gatherers, missing: %v, unhandled: %v",
187+
sets.List(missingDataGatherers),
188+
sets.List(unhandledDataGatherers),
189+
)
190+
}
191+
return nil
192+
}

0 commit comments

Comments
 (0)