Skip to content

Commit c2abc78

Browse files
Merge pull request #703 from jetstack/VC-43403-dataupload-put-snapshot
[VC-43403] CyberArk(dataupload): replace PostDataReadingsWithOptions with PutSnapshot
2 parents 7490f89 + 49ca83c commit c2abc78

File tree

3 files changed

+100
-60
lines changed

3 files changed

+100
-60
lines changed

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212
"net/http"
1313
"net/url"
1414

15-
"github.com/jetstack/preflight/api"
15+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
16+
1617
"github.com/jetstack/preflight/pkg/version"
1718
)
1819

@@ -46,7 +47,43 @@ func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *
4647
}
4748
}
4849

49-
// PostDataReadingsWithOptions PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
50+
// Snapshot is the JSON that the CyberArk Discovery and Context API expects to
51+
// be uploaded to the AWS presigned URL.
52+
type Snapshot struct {
53+
// AgentVersion is the version of the Venafi Kubernetes Agent which is uploading this snapshot.
54+
AgentVersion string `json:"agent_version"`
55+
// ClusterID is the unique ID of the Kubernetes cluster which this snapshot was taken from.
56+
ClusterID string `json:"cluster_id"`
57+
// K8SVersion is the version of Kubernetes which the cluster is running.
58+
K8SVersion string `json:"k8s_version"`
59+
// Secrets is a list of Secret resources in the cluster. Not all Secret
60+
// types are included and only a subset of the Secret data is included.
61+
Secrets []*unstructured.Unstructured `json:"secrets"`
62+
// ServiceAccounts is a list of ServiceAccount resources in the cluster.
63+
ServiceAccounts []*unstructured.Unstructured `json:"serviceaccounts"`
64+
// Roles is a list of Role resources in the cluster.
65+
Roles []*unstructured.Unstructured `json:"roles"`
66+
// ClusterRoles is a list of ClusterRole resources in the cluster.
67+
ClusterRoles []*unstructured.Unstructured `json:"clusterroles"`
68+
// RoleBindings is a list of RoleBinding resources in the cluster.
69+
RoleBindings []*unstructured.Unstructured `json:"rolebindings"`
70+
// ClusterRoleBindings is a list of ClusterRoleBinding resources in the cluster.
71+
ClusterRoleBindings []*unstructured.Unstructured `json:"clusterrolebindings"`
72+
// Jobs is a list of Job resources in the cluster.
73+
Jobs []*unstructured.Unstructured `json:"jobs"`
74+
// CronJobs is a list of CronJob resources in the cluster.
75+
CronJobs []*unstructured.Unstructured `json:"cronjobs"`
76+
// Deployments is a list of Deployment resources in the cluster.
77+
Deployments []*unstructured.Unstructured `json:"deployments"`
78+
// Statefulsets is a list of StatefulSet resources in the cluster.
79+
Statefulsets []*unstructured.Unstructured `json:"statefulsets"`
80+
// Daemonsets is a list of DaemonSet resources in the cluster.
81+
Daemonsets []*unstructured.Unstructured `json:"daemonsets"`
82+
// Pods is a list of Pod resources in the cluster.
83+
Pods []*unstructured.Unstructured `json:"pods"`
84+
}
85+
86+
// PutSnapshot PUTs the supplied snapshot to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
5087
// [AWS presigned URL]: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
5188
//
5289
// A SHA256 checksum header is included in the request, to verify that the payload
@@ -60,20 +97,20 @@ func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *
6097
// If you omit that header, it is possible to PUT any data.
6198
// There is a work around listed in that issue which we have shared with the
6299
// CyberArk API team.
63-
func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
64-
if opts.ClusterName == "" {
65-
return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
100+
func (c *CyberArkClient) PutSnapshot(ctx context.Context, snapshot Snapshot) error {
101+
if snapshot.ClusterID == "" {
102+
return fmt.Errorf("programmer mistake: the snapshot cluster ID cannot be left empty")
66103
}
67104

68105
encodedBody := &bytes.Buffer{}
69106
hash := sha256.New()
70-
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(payload); err != nil {
107+
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(snapshot); err != nil {
71108
return err
72109
}
73110
checksum := hash.Sum(nil)
74111
checksumHex := hex.EncodeToString(checksum)
75112
checksumBase64 := base64.StdEncoding.EncodeToString(checksum)
76-
presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, checksumHex, opts)
113+
presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, checksumHex, snapshot.ClusterID)
77114
if err != nil {
78115
return fmt.Errorf("while retrieving snapshot upload URL: %s", err)
79116
}
@@ -103,7 +140,7 @@ func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payloa
103140
return nil
104141
}
105142

106-
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, opts Options) (string, error) {
143+
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, clusterID string) (string, error) {
107144
uploadURL, err := url.JoinPath(c.baseURL, apiPathSnapshotLinks)
108145
if err != nil {
109146
return "", err
@@ -114,7 +151,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
114151
Checksum string `json:"checksum_sha256"`
115152
AgentVersion string `json:"agent_version"`
116153
}{
117-
ClusterID: opts.ClusterName,
154+
ClusterID: clusterID,
118155
Checksum: checksum,
119156
AgentVersion: version.PreflightVersion,
120157
}

pkg/internal/cyberark/dataupload/dataupload_test.go

Lines changed: 37 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,13 @@ import (
66
"net/http"
77
"os"
88
"testing"
9-
"time"
109

1110
"github.com/jetstack/venafi-connection-lib/http_client"
1211
"github.com/stretchr/testify/require"
1312
"k8s.io/client-go/transport"
1413
"k8s.io/klog/v2"
1514
"k8s.io/klog/v2/ktesting"
1615

17-
"github.com/jetstack/preflight/api"
1816
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
1917
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
2018
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
@@ -23,28 +21,10 @@ import (
2321
_ "k8s.io/klog/v2/ktesting/init"
2422
)
2523

26-
func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
27-
fakeTime := time.Unix(123, 0)
28-
defaultPayload := api.DataReadingsPost{
29-
AgentMetadata: &api.AgentMetadata{
30-
Version: "test-version",
31-
ClusterID: "test",
32-
},
33-
DataGatherTime: fakeTime,
34-
DataReadings: []*api.DataReading{
35-
{
36-
ClusterID: "success-cluster-id",
37-
DataGatherer: "test-gatherer",
38-
Timestamp: api.Time{Time: fakeTime},
39-
Data: map[string]interface{}{"test": "data"},
40-
SchemaVersion: "v1",
41-
},
42-
},
43-
}
44-
defaultOpts := dataupload.Options{
45-
ClusterName: "success-cluster-id",
46-
}
47-
24+
// TestCyberArkClient_PutSnapshot_MockAPI tests the dataupload code against a
25+
// mock API server. The mock server is configured to return different responses
26+
// based on the cluster ID and bearer token used in the request.
27+
func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
4828
setToken := func(token string) func(*http.Request) error {
4929
return func(req *http.Request) error {
5030
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
@@ -54,51 +34,60 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
5434

5535
tests := []struct {
5636
name string
57-
payload api.DataReadingsPost
37+
snapshot dataupload.Snapshot
5838
authenticate func(req *http.Request) error
59-
opts dataupload.Options
6039
requireFn func(t *testing.T, err error)
6140
}{
6241
{
63-
name: "successful upload",
64-
payload: defaultPayload,
65-
opts: defaultOpts,
42+
name: "successful upload",
43+
snapshot: dataupload.Snapshot{
44+
ClusterID: "success-cluster-id",
45+
AgentVersion: "test-version",
46+
},
6647
authenticate: setToken("success-token"),
6748
requireFn: func(t *testing.T, err error) {
6849
require.NoError(t, err)
6950
},
7051
},
7152
{
72-
name: "error when cluster name is empty",
73-
payload: defaultPayload,
74-
opts: dataupload.Options{ClusterName: ""},
53+
name: "error when cluster ID is empty",
54+
snapshot: dataupload.Snapshot{
55+
ClusterID: "",
56+
AgentVersion: "test-version",
57+
},
7558
authenticate: setToken("success-token"),
7659
requireFn: func(t *testing.T, err error) {
77-
require.ErrorContains(t, err, "programmer mistake: the cluster name")
60+
require.ErrorContains(t, err, "programmer mistake: the snapshot cluster ID cannot be left empty")
7861
},
7962
},
8063
{
81-
name: "error when bearer token is incorrect",
82-
payload: defaultPayload,
83-
opts: defaultOpts,
64+
name: "error when bearer token is incorrect",
65+
snapshot: dataupload.Snapshot{
66+
ClusterID: "test",
67+
AgentVersion: "test-version",
68+
},
8469
authenticate: setToken("fail-token"),
8570
requireFn: func(t *testing.T, err error) {
8671
require.ErrorContains(t, err, "while retrieving snapshot upload URL: received response with status code 500: should authenticate using the correct bearer token")
8772
},
8873
},
8974
{
90-
name: "invalid JSON from server (RetrievePresignedUploadURL step)",
91-
payload: defaultPayload,
92-
opts: dataupload.Options{ClusterName: "invalid-json-retrieve-presigned"},
75+
name: "invalid JSON from server (RetrievePresignedUploadURL step)",
76+
snapshot: dataupload.Snapshot{
77+
ClusterID: "invalid-json-retrieve-presigned",
78+
AgentVersion: "test-version",
79+
},
9380
authenticate: setToken("success-token"),
9481
requireFn: func(t *testing.T, err error) {
9582
require.ErrorContains(t, err, "while retrieving snapshot upload URL: rejecting JSON response from server as it was too large or was truncated")
9683
},
9784
},
9885
{
99-
name: "500 from server (RetrievePresignedUploadURL step)",
100-
payload: defaultPayload,
101-
opts: dataupload.Options{ClusterName: "invalid-response-post-data"},
86+
name: "500 from server (RetrievePresignedUploadURL step)",
87+
snapshot: dataupload.Snapshot{
88+
ClusterID: "invalid-response-post-data",
89+
AgentVersion: "test-version",
90+
},
10291
authenticate: setToken("success-token"),
10392
requireFn: func(t *testing.T, err error) {
10493
require.ErrorContains(t, err, "while retrieving snapshot upload URL: received response with status code 500: mock error")
@@ -115,13 +104,13 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
115104

116105
cyberArkClient := dataupload.New(httpClient, datauploadAPIBaseURL, tc.authenticate)
117106

118-
err := cyberArkClient.PostDataReadingsWithOptions(ctx, tc.payload, tc.opts)
107+
err := cyberArkClient.PutSnapshot(ctx, tc.snapshot)
119108
tc.requireFn(t, err)
120109
})
121110
}
122111
}
123112

124-
// TestPostDataReadingsWithOptionsWithRealAPI demonstrates that the dataupload code works with the real inventory API.
113+
// TestCyberArkClient_PutSnapshot_RealAPI demonstrates that the dataupload code works with the real inventory API.
125114
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
126115
// ARK_SUBDOMAIN should be your tenant subdomain.
127116
//
@@ -131,8 +120,8 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
131120
// To enable verbose request logging:
132121
//
133122
// go test ./pkg/internal/cyberark/dataupload/... \
134-
// -v -count 1 -run TestPostDataReadingsWithOptionsWithRealAPI -args -testing.v 6
135-
func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
123+
// -v -count 1 -run TestCyberArkClient_PutSnapshot_RealAPI -args -testing.v 6
124+
func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
136125
subdomain := os.Getenv("ARK_SUBDOMAIN")
137126
username := os.Getenv("ARK_USERNAME")
138127
secret := os.Getenv("ARK_SECRET")
@@ -159,8 +148,8 @@ func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
159148
require.NoError(t, err)
160149

161150
cyberArkClient := dataupload.New(httpClient, services.DiscoveryContext.API, identityClient.AuthenticateRequest)
162-
err = cyberArkClient.PostDataReadingsWithOptions(ctx, api.DataReadingsPost{}, dataupload.Options{
163-
ClusterName: "bb068932-c80d-460d-88df-34bc7f3f3297",
151+
err = cyberArkClient.PutSnapshot(ctx, dataupload.Snapshot{
152+
ClusterID: "bb068932-c80d-460d-88df-34bc7f3f3297",
164153
})
165154
require.NoError(t, err)
166155
}

pkg/internal/cyberark/dataupload/mock.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dataupload
22

33
import (
4+
"bytes"
45
"crypto/sha256"
56
"encoding/base64"
67
"encoding/json"
@@ -10,6 +11,7 @@ import (
1011
"net/http/httptest"
1112
"testing"
1213

14+
"github.com/stretchr/testify/require"
1315
"k8s.io/client-go/transport"
1416

1517
"github.com/jetstack/preflight/pkg/version"
@@ -162,15 +164,27 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r
162164
return
163165
}
164166

165-
checksum := sha256.New()
166-
_, _ = io.Copy(checksum, r.Body)
167+
body, err := io.ReadAll(r.Body)
168+
require.NoError(mds.t, err)
169+
170+
hash := sha256.New()
171+
_, err = hash.Write(body)
172+
require.NoError(mds.t, err)
167173

168174
// AWS S3 responds with a BadDigest error if the request body has a
169175
// different checksum than the checksum supplied in the request header.
170-
if amzChecksum != base64.StdEncoding.EncodeToString(checksum.Sum(nil)) {
176+
if amzChecksum != base64.StdEncoding.EncodeToString(hash.Sum(nil)) {
171177
w.Header().Set("Content-Type", "application/xml")
172178
http.Error(w, amzExampleChecksumError, http.StatusBadRequest)
173179
}
180+
181+
// Verifies that the new Snapshot format is used in the request body.
182+
var snapshot Snapshot
183+
d := json.NewDecoder(bytes.NewBuffer(body))
184+
d.DisallowUnknownFields()
185+
err = d.Decode(&snapshot)
186+
require.NoError(mds.t, err)
187+
174188
// AWS S3 responds with an empty body if the PUT succeeds
175189
w.WriteHeader(http.StatusOK)
176190
}

0 commit comments

Comments
 (0)