Skip to content

Commit 1a40e3f

Browse files
CyberArk(servicediscovery): consolidate service discovery into unified API
- Replaced `DiscoverIdentityAPIURL` with `DiscoverServices` to fetch all service endpoints in a single call. - Updated `Services` struct to include `Identity` and `DiscoveryContext` endpoints. - Refactored dependent modules (`dataupload`, `identity`) to use the new `DiscoverServices` method. - Adjusted tests and mock server to align with the new service discovery implementation. - Removed redundant `platformDomain` logic from `dataupload_test.go`.
1 parent 99dc0aa commit 1a40e3f

File tree

8 files changed

+179
-53
lines changed

8 files changed

+179
-53
lines changed

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
// apiPathSnapshotLinks is the URL path of the snapshot-links endpoint of the inventory API.
2525
// This endpoint returns an AWS presigned URL.
2626
// TODO(wallrj): Link to CyberArk API documentation when it is published.
27-
apiPathSnapshotLinks = "/api/ingestions/kubernetes/snapshot-links"
27+
apiPathSnapshotLinks = "/ingestions/kubernetes/snapshot-links"
2828
)
2929

3030
type CyberArkClient struct {

pkg/internal/cyberark/dataupload/dataupload_test.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
124124
// TestPostDataReadingsWithOptionsWithRealAPI demonstrates that the dataupload code works with the real inventory API.
125125
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
126126
// ARK_SUBDOMAIN should be your tenant subdomain.
127-
// ARK_PLATFORM_DOMAIN should be either integration-cyberark.cloud or cyberark.cloud
128127
//
129128
// To test against a tenant on the integration platform, also set:
130129
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
@@ -134,36 +133,32 @@ func TestCyberArkClient_PostDataReadingsWithOptions(t *testing.T) {
134133
// go test ./pkg/internal/cyberark/dataupload/... \
135134
// -v -count 1 -run TestPostDataReadingsWithOptionsWithRealAPI -args -testing.v 6
136135
func TestPostDataReadingsWithOptionsWithRealAPI(t *testing.T) {
137-
platformDomain := os.Getenv("ARK_PLATFORM_DOMAIN")
138136
subdomain := os.Getenv("ARK_SUBDOMAIN")
139137
username := os.Getenv("ARK_USERNAME")
140138
secret := os.Getenv("ARK_SECRET")
141139

142-
if platformDomain == "" || subdomain == "" || username == "" || secret == "" {
143-
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_PLATFORM_DOMAIN, ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
140+
if subdomain == "" || username == "" || secret == "" {
141+
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
144142
return
145143
}
146144

147145
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
148146
ctx := klog.NewContext(t.Context(), logger)
149147

150-
// TODO(wallrj): get this from the servicediscovery API instead.
151-
inventoryAPI := fmt.Sprintf("https://%s.inventory.%s", subdomain, platformDomain)
152-
153148
var rootCAs *x509.CertPool
154149
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
155150
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
156151

157152
discoveryClient := servicediscovery.New(httpClient)
158153

159-
identityAPI, err := discoveryClient.DiscoverIdentityAPIURL(ctx, subdomain)
154+
services, err := discoveryClient.DiscoverServices(ctx, subdomain)
160155
require.NoError(t, err)
161156

162-
identityClient := identity.New(httpClient, identityAPI, subdomain)
157+
identityClient := identity.New(httpClient, services.Identity.API, subdomain)
163158
err = identityClient.LoginUsernamePassword(ctx, username, []byte(secret))
164159
require.NoError(t, err)
165160

166-
cyberArkClient := dataupload.New(httpClient, inventoryAPI, identityClient.AuthenticateRequest)
161+
cyberArkClient := dataupload.New(httpClient, services.DiscoveryContext.API, identityClient.AuthenticateRequest)
167162
err = cyberArkClient.PostDataReadingsWithOptions(ctx, api.DataReadingsPost{}, dataupload.Options{
168163
ClusterName: "bb068932-c80d-460d-88df-34bc7f3f3297",
169164
})

pkg/internal/cyberark/identity/cmd/testidentity/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ func run(ctx context.Context) error {
5353
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
5454

5555
sdClient := servicediscovery.New(httpClient)
56-
identityAPI, err := sdClient.DiscoverIdentityAPIURL(ctx, subdomain)
56+
services, err := sdClient.DiscoverServices(ctx, subdomain)
5757
if err != nil {
5858
return fmt.Errorf("while performing service discovery: %s", err)
5959
}
6060

61-
client := identity.New(httpClient, identityAPI, subdomain)
61+
client := identity.New(httpClient, services.Identity.API, subdomain)
6262

6363
err = client.LoginUsernamePassword(ctx, username, []byte(password))
6464
if err != nil {

pkg/internal/cyberark/servicediscovery/discovery.go

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
)
1414

1515
const (
16+
// ProdDiscoveryAPIBaseURL is the base URL for the production CyberArk Service Discovery API
1617
ProdDiscoveryAPIBaseURL = "https://platform-discovery.cyberark.cloud/api/v2/"
1718

1819
// identityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API
@@ -32,7 +33,9 @@ type Client struct {
3233
baseURL string
3334
}
3435

35-
// New creates a new CyberArk Service Discovery client, configurable with ClientOpt
36+
// New creates a new CyberArk Service Discovery client. If the ARK_DISCOVERY_API
37+
// environment variable is set, it is used as the base URL for the service
38+
// discovery API. Otherwise, the production URL is used.
3639
func New(httpClient *http.Client) *Client {
3740
baseURL := os.Getenv("ARK_DISCOVERY_API")
3841
if baseURL == "" {
@@ -46,25 +49,43 @@ func New(httpClient *http.Client) *Client {
4649
return client
4750
}
4851

49-
// DiscoverIdentityAPIURL fetches from the service discovery service for a given subdomain
50-
// and parses the CyberArk Identity API URL.
51-
func (c *Client) DiscoverIdentityAPIURL(ctx context.Context, subdomain string) (string, error) {
52+
// ServiceEndpoint represents a single service endpoint returned by the CyberArk
53+
// Service Discovery API. The JSON field names here must match the field names
54+
// returned by the Service Discovery API. Currently, we only care about the
55+
// "api" field. Other fields are intentionally ignored here.
56+
type ServiceEndpoint struct {
57+
API string `json:"api"`
58+
}
59+
60+
// Services represents the relevant services returned by the CyberArk Service
61+
// Discovery API for a given subdomain. Currently, we only care about the
62+
// Identity API and the Discovery Context API. Other services are intentionally
63+
// ignored here. The JSON field names here must match the field names returned
64+
// by the Service Discovery API.
65+
type Services struct {
66+
Identity ServiceEndpoint `json:"identity_administration"`
67+
DiscoveryContext ServiceEndpoint `json:"discoverycontext"`
68+
}
69+
70+
// DiscoverServices fetches from the service discovery service for a given subdomain
71+
// and parses the CyberArk Identity API URL and Inventory API URL.
72+
func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, error) {
5273
endpoint, err := url.JoinPath(c.baseURL, "services", "subdomain", subdomain)
5374
if err != nil {
54-
return "", fmt.Errorf("failed to build a valid URL for subdomain %s; possibly an invalid endpoint: %s", subdomain, err)
75+
return nil, fmt.Errorf("failed to build a valid URL for subdomain %s; possibly an invalid endpoint: %s", subdomain, err)
5576
}
5677

5778
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
5879
if err != nil {
59-
return "", fmt.Errorf("failed to initialise request to %s: %s", endpoint, err)
80+
return nil, fmt.Errorf("failed to initialise request to %s: %s", endpoint, err)
6081
}
6182

6283
request.Header.Set("Accept", "application/json")
6384
version.SetUserAgent(request)
6485

6586
resp, err := c.client.Do(request)
6687
if err != nil {
67-
return "", fmt.Errorf("failed to perform HTTP request: %s", err)
88+
return nil, fmt.Errorf("failed to perform HTTP request: %s", err)
6889
}
6990

7091
defer resp.Body.Close()
@@ -73,32 +94,26 @@ func (c *Client) DiscoverIdentityAPIURL(ctx context.Context, subdomain string) (
7394
// a 404 error is returned with an empty JSON body "{}" if the subdomain is unknown; at the time of writing, we haven't observed
7495
// any other errors and so we can't special case them
7596
if resp.StatusCode == http.StatusNotFound {
76-
return "", fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain)
97+
return nil, fmt.Errorf("got an HTTP 404 response from service discovery; maybe the subdomain %q is incorrect or does not exist?", subdomain)
7798
}
7899

79-
return "", fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
80-
}
81-
82-
type ServiceEndpoint struct {
83-
API string `json:"api"`
84-
// NB: other fields are intentionally ignored here; we only care about the API URL
100+
return nil, fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
85101
}
86102

87-
decodedResponse := make(map[string]ServiceEndpoint)
103+
var services Services
88104

89-
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&decodedResponse)
105+
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&services)
90106
if err != nil {
91107
if err == io.ErrUnexpectedEOF {
92-
return "", fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
108+
return nil, fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
93109
}
94110

95-
return "", fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
111+
return nil, fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
96112
}
97113

98-
identityService, ok := decodedResponse[identityServiceName]
99-
if !ok {
100-
return "", fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", identityServiceName)
114+
if services.Identity.API == "" {
115+
return nil, fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", identityServiceName)
101116
}
102117

103-
return identityService.API, nil
118+
return &services, nil
104119
}

pkg/internal/cyberark/servicediscovery/discovery_test.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"testing"
66

7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
79
"k8s.io/klog/v2"
810
"k8s.io/klog/v2/ktesting"
911

@@ -53,21 +55,26 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
5355
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
5456
ctx := klog.NewContext(t.Context(), logger)
5557

56-
httpClient := MockDiscoveryServer(t, mockIdentityAPIURL)
58+
httpClient := MockDiscoveryServer(t, Services{
59+
Identity: ServiceEndpoint{
60+
API: mockIdentityAPIURL,
61+
},
62+
DiscoveryContext: ServiceEndpoint{
63+
API: mockDiscoveryContextAPIURL,
64+
},
65+
})
5766

5867
client := New(httpClient)
5968

60-
apiURL, err := client.DiscoverIdentityAPIURL(ctx, testSpec.subdomain)
61-
if err != nil {
62-
if err.Error() != testSpec.expectedError.Error() {
63-
t.Errorf("expectedError=%v\nobservedError=%v", testSpec.expectedError, err)
64-
}
69+
services, err := client.DiscoverServices(ctx, testSpec.subdomain)
70+
if testSpec.expectedError != nil {
71+
assert.EqualError(t, err, testSpec.expectedError.Error())
72+
assert.Nil(t, services)
73+
return
6574
}
66-
67-
// NB: we don't exit here because we also want to check the API URL is empty in the event of an error
68-
69-
if apiURL != testSpec.expectedURL {
70-
t.Errorf("expected API URL=%s\nobserved API URL=%s", testSpec.expectedURL, apiURL)
75+
require.NoError(t, err)
76+
if services.Identity.API != testSpec.expectedURL {
77+
t.Errorf("expected API URL=%s\nobserved API URL=%s", testSpec.expectedURL, services.Identity.API)
7178
}
7279
})
7380
}

pkg/internal/cyberark/servicediscovery/mock.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const (
2222
// MockDiscoverySubdomain is the subdomain for which the MockDiscoveryServer will return a success response
2323
MockDiscoverySubdomain = "venafi-test"
2424

25-
mockIdentityAPIURL = "https://ajp5871.id.integration-cyberark.cloud"
25+
mockIdentityAPIURL = "https://ajp5871.id.integration-cyberark.cloud"
26+
mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/api"
2627
)
2728

2829
//go:embed testdata/discovery_success.json.template
@@ -41,15 +42,16 @@ type mockDiscoveryServer struct {
4142
// server.
4243
//
4344
// The mock server will return a successful response when the subdomain is
44-
// `MockDiscoverySubdomain`, and the identity API URL in that response will be
45-
// `identityAPIURL`.
45+
// `MockDiscoverySubdomain`, and the API URLs in the response will match those
46+
// supplied in `services`.
4647
// Other subdomains, can be used to trigger various failure responses.
48+
//
4749
// The returned HTTP client has a transport which logs requests and responses
4850
// depending on log level of the logger supplied in the context.
49-
func MockDiscoveryServer(t testing.TB, identityAPIURL string) *http.Client {
51+
func MockDiscoveryServer(t *testing.T, services Services) *http.Client {
5052
tmpl := template.Must(template.New("mockDiscoverySuccess").Parse(discoverySuccessTemplate))
5153
buf := &bytes.Buffer{}
52-
err := tmpl.Execute(buf, struct{ IdentityAPIURL string }{identityAPIURL})
54+
err := tmpl.Execute(buf, services)
5355
if err != nil {
5456
panic(err)
5557
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
# Test data for CyberArk Discovery
22

3-
All data in this folder is derived from an unauthenticated endpoint accessible from the public internet.
3+
All data in this folder is derived from an unauthenticated endpoint accessible from the public Internet.
4+
5+
To get the original data:
6+
7+
```bash
8+
curl -fsSL "${ARK_DISCOVERY_API}/services/subdomain/${ARK_SUBDOMAIN}" | jq
9+
```
10+
11+
Then replace `identity_administration.api` with `{{ .Identity.API }}` and
12+
`discoverycontext.api` with `{{ .DiscoveryContext.API }}`. Those Go template
13+
fields will be substituted in the tests.

0 commit comments

Comments
 (0)