Skip to content

Commit cd6bb05

Browse files
Merge pull request #706 from jetstack/VC-43587
Deprecate service/discovery API and implement the new one
2 parents 90f92d7 + f532eb8 commit cd6bb05

File tree

7 files changed

+281
-110
lines changed

7 files changed

+281
-110
lines changed

.envrc.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export CLOUDSDK_COMPUTE_ZONE= # the GCP zone where a GKE cluster will be created
1414
export CLUSTER_NAME= # the name of the GKE cluster which will be created. E.g. cluster-1
1515

1616
# The following variables are required for CyberArk / MachineHub integration tests.
17-
export ARK_SUBDOMAIN= # your CyberArk tenant subdomain
17+
export ARK_SUBDOMAIN= # your CyberArk tenant subdomain e.g. tlskp-test
1818
export ARK_USERNAME= # your CyberArk username
1919
export ARK_SECRET= # your CyberArk password
2020
# OPTIONAL: the URL for the CyberArk Discovery API if not using the production environment
21-
export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
21+
export ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/

pkg/internal/cyberark/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
4949
// ARK_SUBDOMAIN should be your tenant subdomain.
5050
//
5151
// To test against a tenant on the integration platform, also set:
52-
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
52+
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/
5353
//
5454
// To enable verbose request logging:
5555
//

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222
// the login is successful.
2323
//
2424
// To test against a tenant on the integration platform, set:
25-
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
25+
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/
2626
const (
2727
subdomainFlag = "subdomain"
2828
usernameFlag = "username"

pkg/internal/cyberark/servicediscovery/discovery.go

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import (
88
"net/http"
99
"net/url"
1010
"os"
11+
"path"
1112

1213
"github.com/jetstack/preflight/pkg/version"
1314
)
1415

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

1920
// IdentityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API
2021
// We were told to use the identity_administration field, not the identity_user_portal field.
@@ -53,32 +54,54 @@ func New(httpClient *http.Client) *Client {
5354
return client
5455
}
5556

57+
// DiscoveryResponse represents the full JSON response returned by the CyberArk api/tenant-discovery/public API
58+
// The API is documented here https://ca-il-confluence.il.cyber-ark.com/spaces/EV/pages/575618345/Updated+PD+APIs+doc
59+
type DiscoveryResponse struct {
60+
Region string `json:"region"`
61+
DRRegion string `json:"dr_region"`
62+
Subdomain string `json:"subdomain"`
63+
TenantID string `json:"tenant_id"`
64+
PlatformID string `json:"platform_id"`
65+
IdentityID string `json:"identity_id"`
66+
DefaultURL string `json:"default_url"`
67+
TenantFlags map[string]interface{} `json:"tenant_flags"`
68+
Services []Service `json:"services"`
69+
}
70+
71+
type Service struct {
72+
ServiceName string `json:"service_name"`
73+
ServiceSubdomains []string `json:"service_subdomains"`
74+
Region string `json:"region"`
75+
Endpoints []ServiceEndpoint `json:"endpoints"`
76+
}
77+
5678
// ServiceEndpoint represents a single service endpoint returned by the CyberArk
5779
// Service Discovery API. The JSON field names here must match the field names
58-
// returned by the Service Discovery API. Currently, we only care about the
59-
// "api" field. Other fields are intentionally ignored here.
80+
// returned by the Service Discovery API.
6081
type ServiceEndpoint struct {
61-
API string `json:"api"`
82+
IsActive bool `json:"is_active"`
83+
Type string `json:"type"`
84+
UI string `json:"ui"`
85+
API string `json:"api"`
6286
}
6387

64-
// Services represents the relevant services returned by the CyberArk Service
65-
// Discovery API for a given subdomain. Currently, we only care about the
66-
// Identity API and the Discovery Context API. Other services are intentionally
67-
// ignored here. The JSON field names here must match the field names returned
68-
// by the Service Discovery API.
88+
// This is a convenience struct to hold the two ServiceEndpoints we care about.
89+
// Currently, we only care about the Identity API and the Discovery Context API.
6990
type Services struct {
70-
Identity ServiceEndpoint `json:"identity_administration"`
71-
DiscoveryContext ServiceEndpoint `json:"discoverycontext"`
91+
Identity ServiceEndpoint
92+
DiscoveryContext ServiceEndpoint
7293
}
7394

7495
// DiscoverServices fetches from the service discovery service for a given subdomain
7596
// and parses the CyberArk Identity API URL and Inventory API URL.
7697
func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Services, error) {
77-
endpoint, err := url.JoinPath(c.baseURL, "services", "subdomain", subdomain)
98+
u, err := url.Parse(c.baseURL)
7899
if err != nil {
79-
return nil, fmt.Errorf("failed to build a valid URL for subdomain %s; possibly an invalid endpoint: %s", subdomain, err)
100+
return nil, fmt.Errorf("invalid base URL for service discovery: %w", err)
80101
}
81-
102+
u.Path = path.Join(u.Path, "api/public/tenant-discovery")
103+
u.RawQuery = url.Values{"bySubdomain": []string{subdomain}}.Encode()
104+
endpoint := u.String()
82105
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
83106
if err != nil {
84107
return nil, fmt.Errorf("failed to initialise request to %s: %s", endpoint, err)
@@ -104,19 +127,42 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
104127
return nil, fmt.Errorf("got unexpected status code %s from request to service discovery API", resp.Status)
105128
}
106129

107-
var services Services
108-
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&services)
130+
var discoveryResp DiscoveryResponse
131+
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&discoveryResp)
109132
if err != nil {
110133
if err == io.ErrUnexpectedEOF {
111134
return nil, fmt.Errorf("rejecting JSON response from server as it was too large or was truncated")
112135
}
113-
114136
return nil, fmt.Errorf("failed to parse JSON from otherwise successful request to service discovery endpoint: %s", err)
115137
}
138+
var identityAPI, discoveryContextAPI string
139+
for _, svc := range discoveryResp.Services {
140+
switch svc.ServiceName {
141+
case IdentityServiceName:
142+
for _, ep := range svc.Endpoints {
143+
if ep.Type == "main" && ep.IsActive && ep.API != "" {
144+
identityAPI = ep.API
145+
break
146+
}
147+
}
148+
case DiscoveryContextServiceName:
149+
for _, ep := range svc.Endpoints {
150+
if ep.Type == "main" && ep.IsActive && ep.API != "" {
151+
discoveryContextAPI = ep.API
152+
break
153+
}
154+
}
155+
}
156+
}
116157

117-
if services.Identity.API == "" {
118-
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)
158+
if identityAPI == "" {
159+
return nil, fmt.Errorf("didn't find %s in service discovery response, "+
160+
"which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName)
119161
}
162+
//TODO: Should add a check for discoveryContextAPI too?
120163

121-
return &services, nil
164+
return &Services{
165+
Identity: ServiceEndpoint{API: identityAPI},
166+
DiscoveryContext: ServiceEndpoint{API: discoveryContextAPI},
167+
}, nil
122168
}

pkg/internal/cyberark/servicediscovery/mock.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import (
2020

2121
const (
2222
// MockDiscoverySubdomain is the subdomain for which the MockDiscoveryServer will return a success response
23-
MockDiscoverySubdomain = "venafi-test"
23+
MockDiscoverySubdomain = "tlskp-test"
2424

2525
mockIdentityAPIURL = "https://ajp5871.id.integration-cyberark.cloud"
26-
mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/api"
26+
mockDiscoveryContextAPIURL = "https://venafi-test.inventory.integration-cyberark.cloud/"
27+
prefix = "/api/public/tenant-discovery?bySubdomain="
2728
)
2829

2930
//go:embed testdata/discovery_success.json.template
@@ -77,7 +78,7 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request
7778
return
7879
}
7980

80-
if !strings.HasPrefix(r.URL.String(), "/services/subdomain/") {
81+
if !strings.HasPrefix(r.URL.String(), prefix) {
8182
// This was observed by making a request to /api/v2/services/asd
8283
// Normally, we'd expect 404 Not Found but we match the observed response here
8384
w.WriteHeader(http.StatusForbidden)
@@ -97,15 +98,30 @@ func (mds *mockDiscoveryServer) ServeHTTP(w http.ResponseWriter, r *http.Request
9798
return
9899
}
99100

100-
subdomain := strings.TrimPrefix(r.URL.String(), "/services/subdomain/")
101+
subdomain := strings.TrimPrefix(r.URL.String(), prefix)
101102

102103
switch subdomain {
103104
case MockDiscoverySubdomain:
104105
_, _ = w.Write([]byte(mds.successResponse))
105106

106107
case "no-identity":
107108
// return a snippet of valid service discovery JSON, but don't include the identity service
108-
_, _ = w.Write([]byte(`{"data_privacy": {"ui": "https://ui.dataprivacy.integration-cyberark.cloud/", "api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api", "bootstrap": "https://venafi-test-data_privacy.integration-cyberark.cloud", "region": "us-east-1"}}`))
109+
_, _ = w.Write([]byte(`{
110+
"services": [
111+
{
112+
"service_name": "data_privacy",
113+
"region": "us-east-1",
114+
"endpoints": [
115+
{
116+
"is_active": true,
117+
"type": "main",
118+
"ui": "https://ui.dataprivacy.integration-cyberark.cloud/",
119+
"api": "https://us-east-1.dataprivacy.integration-cyberark.cloud/api"
120+
}
121+
]
122+
}
123+
]
124+
}`))
109125

110126
case "bad-request":
111127
// test how the client handles a random unexpected response

pkg/internal/cyberark/servicediscovery/testdata/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ All data in this folder is derived from an unauthenticated endpoint accessible f
44

55
To get the original data:
66

7+
NOTE: This API is not implemented yet as of 02.09.2025 but is expected to be finalised by end of PI3 2025.
78
```bash
8-
curl -fsSL "${ARK_DISCOVERY_API}/services/subdomain/${ARK_SUBDOMAIN}" | jq
9+
curl -fsSL "${ARK_DISCOVERY_API}?bySubdomain=${ARK_SUBDOMAIN}" | jq
910
```
1011

1112
Then replace `identity_administration.api` with `{{ .Identity.API }}` and

0 commit comments

Comments
 (0)