Skip to content

Commit a04cdad

Browse files
CyberArk(client): add CyberArk snapshot conversion
- Introduced `convertDataReadings` to process `DataReading` objects into snapshots. - Added support for extracting Kubernetes server version and dynamic resources. - Updated `CyberArkClient` to use the new data conversion logic. - Refactored `DiscoveryData` and `DynamicData` structures for better type safety. - Replaced `unstructured.Unstructured` with `runtime.Object` in `Snapshot` fields. - Enhanced `DataGathererDiscovery` and `DataGathererDynamic` to return strongly typed data. - Added unit tests for new data extraction and conversion functions. Signed-off-by: Richard Wall <[email protected]>
1 parent 2d030d4 commit a04cdad

File tree

8 files changed

+506
-43
lines changed

8 files changed

+506
-43
lines changed

api/datareading.go

Lines changed: 17 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,18 @@ 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+
// ServerVersion is the version information of the k8s apiserver
65+
// See https://godoc.org/k8s.io/apimachinery/pkg/version#Info
66+
ServerVersion *version.Info `json:"server_version"`
67+
}

pkg/client/client_cyberark.go

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ 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"
11-
"github.com/jetstack/preflight/pkg/version"
1214
)
1315

1416
// CyberArkClient is a client for publishing data readings to CyberArk's discoverycontext API.
@@ -48,15 +50,144 @@ func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readin
4850
if err != nil {
4951
return fmt.Errorf("while initializing data upload client: %s", err)
5052
}
53+
var snapshot dataupload.Snapshot
54+
if err := convertDataReadings(defaultExtractorFunctions, readings, &snapshot); err != nil {
55+
return fmt.Errorf("while converting data readings: %s", err)
56+
}
57+
// Temporary hard coded cluster ID.
58+
// TODO(wallrj): The clusterID will eventually be extracted from the supplied readings.
59+
snapshot.ClusterID = "success-cluster-id"
5160

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

0 commit comments

Comments
 (0)