Skip to content

Commit fa4fb98

Browse files
committed
Update the dataupload package to be compatible with the implementated inventory API
Signed-off-by: Richard Wall <[email protected]>
1 parent 9d25e21 commit fa4fb98

File tree

4 files changed

+93
-36
lines changed

4 files changed

+93
-36
lines changed

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const (
2222
// maxRetrievePresignedUploadURLBodySize is the maximum allowed size for a response body from the
2323
// Retrieve Presigned Upload URL service.
2424
maxRetrievePresignedUploadURLBodySize = 10 * 1024
25+
26+
// apiPathSnapshotLinks is the URL path of the snapshot-links endpoint of the inventory API.
27+
// This endpoint returns an AWS presigned URL.
28+
// TODO(wallrj): Link to CyberArk API documentation when it is published.
29+
apiPathSnapshotLinks = "/api/ingestions/kubernetes/snapshot-links"
2530
)
2631

2732
type CyberArkClient struct {
@@ -32,8 +37,7 @@ type CyberArkClient struct {
3237
}
3338

3439
type Options struct {
35-
ClusterName string
36-
ClusterDescription string
40+
ClusterName string
3741
}
3842

3943
func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string, authenticateRequest func(req *http.Request) error) (*CyberArkClient, error) {
@@ -51,6 +55,9 @@ func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string, authenticateRe
5155
}, nil
5256
}
5357

58+
// PostDataReadingsWithOptions PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
59+
//
60+
// [AWS presigned URL]: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
5461
func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
5562
if opts.ClusterName == "" {
5663
return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
@@ -64,15 +71,15 @@ func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payloa
6471

6572
presignedUploadURL, err := c.retrievePresignedUploadURL(ctx, hex.EncodeToString(checksum.Sum(nil)), opts)
6673
if err != nil {
67-
return err
74+
return fmt.Errorf("while retrieving snapshot upload URL: %s", err)
6875
}
6976

70-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, presignedUploadURL, encodedBody)
77+
// The snapshot-links endpoint returns an AWS presigned URL which only supports the PUT verb.
78+
req, err := http.NewRequestWithContext(ctx, http.MethodPut, presignedUploadURL, encodedBody)
7179
if err != nil {
7280
return err
7381
}
7482

75-
req.Header.Set("Content-Type", "application/json")
7683
version.SetUserAgent(req)
7784

7885
res, err := c.client.Do(req)
@@ -93,19 +100,19 @@ func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payloa
93100
}
94101

95102
func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksum string, opts Options) (string, error) {
96-
uploadURL, err := url.JoinPath(c.baseURL, "/api/data/kubernetes/upload")
103+
uploadURL, err := url.JoinPath(c.baseURL, apiPathSnapshotLinks)
97104
if err != nil {
98105
return "", err
99106
}
100107

101108
request := struct {
102-
ClusterID string `json:"cluster_id"`
103-
ClusterDescription string `json:"cluster_description"`
104-
Checksum string `json:"checksum_sha256"`
109+
ClusterID string `json:"cluster_id"`
110+
Checksum string `json:"checksum_sha256"`
111+
AgentVersion string `json:"agent_version"`
105112
}{
106-
ClusterID: opts.ClusterName,
107-
ClusterDescription: opts.ClusterDescription,
108-
Checksum: checksum,
113+
ClusterID: opts.ClusterName,
114+
Checksum: checksum,
115+
AgentVersion: version.PreflightVersion,
109116
}
110117

111118
encodedBody := &bytes.Buffer{}
@@ -120,7 +127,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
120127

121128
req.Header.Set("Content-Type", "application/json")
122129
if err := c.authenticateRequest(req); err != nil {
123-
return "", fmt.Errorf("failed to authenticate request")
130+
return "", fmt.Errorf("failed to authenticate request: %s", err)
124131
}
125132
version.SetUserAgent(req)
126133

pkg/internal/cyberark/dataupload/dataupload_test.go

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,18 @@ import (
55
"encoding/pem"
66
"fmt"
77
"net/http"
8+
"os"
89
"testing"
910
"time"
1011

1112
"github.com/stretchr/testify/require"
13+
"k8s.io/klog/v2"
14+
"k8s.io/klog/v2/ktesting"
1215

1316
"github.com/jetstack/preflight/api"
1417
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
18+
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
19+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
1520
)
1621

1722
func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
@@ -33,8 +38,7 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
3338
},
3439
}
3540
defaultOpts := dataupload.Options{
36-
ClusterName: "success-cluster-id",
37-
ClusterDescription: "success-cluster-description",
41+
ClusterName: "success-cluster-id",
3842
}
3943

4044
setToken := func(token string) func(*http.Request) error {
@@ -75,25 +79,25 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
7579
opts: defaultOpts,
7680
authenticate: setToken("fail-token"),
7781
requireFn: func(t *testing.T, err error) {
78-
require.ErrorContains(t, err, "received response with status code 500: should authenticate using the correct bearer token")
82+
require.ErrorContains(t, err, "while retrieving snapshot upload URL: received response with status code 500: should authenticate using the correct bearer token")
7983
},
8084
},
8185
{
8286
name: "invalid JSON from server (RetrievePresignedUploadURL step)",
8387
payload: defaultPayload,
84-
opts: dataupload.Options{ClusterName: "invalid-json-retrieve-presigned", ClusterDescription: defaultOpts.ClusterDescription},
88+
opts: dataupload.Options{ClusterName: "invalid-json-retrieve-presigned"},
8589
authenticate: setToken("success-token"),
8690
requireFn: func(t *testing.T, err error) {
87-
require.ErrorContains(t, err, "rejecting JSON response from server as it was too large or was truncated")
91+
require.ErrorContains(t, err, "while retrieving snapshot upload URL: rejecting JSON response from server as it was too large or was truncated")
8892
},
8993
},
9094
{
91-
name: "500 from server (PostData step)",
95+
name: "500 from server (RetrievePresignedUploadURL step)",
9296
payload: defaultPayload,
93-
opts: dataupload.Options{ClusterName: "invalid-response-post-data", ClusterDescription: defaultOpts.ClusterDescription},
97+
opts: dataupload.Options{ClusterName: "invalid-response-post-data"},
9498
authenticate: setToken("success-token"),
9599
requireFn: func(t *testing.T, err error) {
96-
require.ErrorContains(t, err, "received response with status code 500: mock error")
100+
require.ErrorContains(t, err, "while retrieving snapshot upload URL: received response with status code 500: mock error")
97101
},
98102
},
99103
}
@@ -117,3 +121,52 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
117121
})
118122
}
119123
}
124+
125+
// TestPostDataReadingsWithOptionsWithRealAPI demonstrates that the dataupload code works with the real inventory API.
126+
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
127+
// ARK_SUBDOMAIN should be your tenant subdomain.
128+
// ARK_PLATFORM_DOMAIN should be either integration-cyberark.cloud or cyberark.cloud
129+
func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
130+
platformDomain := os.Getenv("ARK_PLATFORM_DOMAIN")
131+
subdomain := os.Getenv("ARK_SUBDOMAIN")
132+
username := os.Getenv("ARK_USERNAME")
133+
secret := os.Getenv("ARK_SECRET")
134+
135+
if platformDomain == "" || subdomain == "" || username == "" || secret == "" {
136+
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_PLATFORM_DOMAIN, ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
137+
return
138+
}
139+
140+
logger := ktesting.NewLogger(t, ktesting.NewConfig())
141+
ctx := klog.NewContext(t.Context(), logger)
142+
143+
const (
144+
discoveryContextServiceName = "inventory"
145+
separator = "."
146+
)
147+
148+
serviceURL := fmt.Sprintf("https://%s%s%s.%s", subdomain, separator, discoveryContextServiceName, platformDomain)
149+
150+
var (
151+
identityClient *identity.Client
152+
err error
153+
)
154+
if platformDomain == "cyberark.cloud" {
155+
identityClient, err = identity.New(ctx, subdomain)
156+
} else {
157+
discoveryClient := servicediscovery.New(servicediscovery.WithIntegrationEndpoint())
158+
identityClient, err = identity.NewWithDiscoveryClient(ctx, discoveryClient, subdomain)
159+
}
160+
require.NoError(t, err)
161+
162+
err = identityClient.LoginUsernamePassword(ctx, username, []byte(secret))
163+
require.NoError(t, err)
164+
165+
cyberArkClient, err := dataupload.NewCyberArkClient(nil, serviceURL, identityClient.AuthenticateRequest)
166+
require.NoError(t, err)
167+
168+
err = cyberArkClient.PostDataReadingsWithOptions(t.Context(), api.DataReadingsPost{}, dataupload.Options{
169+
ClusterName: "bb068932-c80d-460d-88df-34bc7f3f3297",
170+
})
171+
require.NoError(t, err)
172+
}

pkg/internal/cyberark/dataupload/mock.go

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"crypto/sha256"
55
"encoding/hex"
66
"encoding/json"
7+
"fmt"
78
"io"
89
"net/http"
910
"net/http/httptest"
@@ -16,8 +17,7 @@ import (
1617
const (
1718
successBearerToken = "success-token"
1819

19-
successClusterID = "success-cluster-id"
20-
successClusterDescription = "success-cluster-description"
20+
successClusterID = "success-cluster-id"
2121
)
2222

2323
type mockDataUploadServer struct {
@@ -37,7 +37,7 @@ func (mds *mockDataUploadServer) Close() {
3737

3838
func (mds *mockDataUploadServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3939
switch r.URL.Path {
40-
case "/api/data/kubernetes/upload":
40+
case apiPathSnapshotLinks:
4141
mds.handlePresignedUpload(w, r)
4242
return
4343
case "/presigned-upload":
@@ -80,17 +80,17 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r
8080
}
8181

8282
var req struct {
83-
ClusterID string `json:"cluster_id"`
84-
ClusterDescription string `json:"Cluster_description"`
85-
Checksum string `json:"checksum_sha256"`
83+
ClusterID string `json:"cluster_id"`
84+
Checksum string `json:"checksum_sha256"`
85+
AgentVersion string `json:"agent_version"`
8686
}
8787
if err := json.Unmarshal(body, &req); err != nil {
8888
http.Error(w, "failed to unmarshal post body", http.StatusInternalServerError)
8989
return
9090
}
9191

92-
if req.ClusterDescription != successClusterDescription {
93-
http.Error(w, "post body contains unexpected description", http.StatusInternalServerError)
92+
if req.AgentVersion != version.PreflightVersion {
93+
http.Error(w, fmt.Sprintf("post body contains unexpected agent version: %s", req.AgentVersion), http.StatusInternalServerError)
9494
return
9595
}
9696

@@ -123,7 +123,7 @@ func (mds *mockDataUploadServer) handlePresignedUpload(w http.ResponseWriter, r
123123
}
124124

125125
func (mds *mockDataUploadServer) handleUpload(w http.ResponseWriter, r *http.Request, invalidJSON bool) {
126-
if r.Method != http.MethodPost {
126+
if r.Method != http.MethodPut {
127127
w.WriteHeader(http.StatusMethodNotAllowed)
128128
_, _ = w.Write([]byte(`{"message":"method not allowed"}`))
129129
return
@@ -134,11 +134,6 @@ func (mds *mockDataUploadServer) handleUpload(w http.ResponseWriter, r *http.Req
134134
return
135135
}
136136

137-
if r.Header.Get("Content-Type") != "application/json" {
138-
http.Error(w, "should send JSON on all requests", http.StatusInternalServerError)
139-
return
140-
}
141-
142137
if invalidJSON {
143138
w.WriteHeader(http.StatusOK)
144139
w.Header().Set("Content-Type", "application/json")

pkg/version/version.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ var GoVersion string
2121

2222
// UserAgent return a standard user agent for use with all HTTP requests. This is implemented in one place so
2323
// it's uniform across the Kubernetes Agent.
24+
//
25+
// TODO(wallrj): The prefix "Mozilla/5.0" is currently required by the CyberArk inventory API. Remove the prefix when CyberArk relax the API security settings.
2426
func UserAgent() string {
25-
return fmt.Sprintf("venafi-kubernetes-agent/%s", PreflightVersion)
27+
return fmt.Sprintf("Mozilla/5.0 venafi-kubernetes-agent/%s", PreflightVersion)
2628
}
2729

2830
// SetUserAgent augments an http.Request with a standard user agent.

0 commit comments

Comments
 (0)