Skip to content

Commit c51220b

Browse files
CyberArk(agent): add support for MachineHub output mode
- Introduced a new `MachineHub` output mode in the agent configuration. - Added validation logic for `--machine-hub` flag and required environment variables. - Implemented `CyberArkClient` to handle data uploads for MachineHub mode. - Updated tests to cover `MachineHub` mode, including success and failure scenarios. - Refactored and exposed constants for service discovery to improve reusability.
1 parent c2abc78 commit c51220b

File tree

9 files changed

+125
-13
lines changed

9 files changed

+125
-13
lines changed

pkg/agent/config.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package agent
22

33
import (
4+
"crypto/x509"
45
"fmt"
56
"io"
67
"net/url"
@@ -10,9 +11,11 @@ import (
1011

1112
"github.com/go-logr/logr"
1213
"github.com/hashicorp/go-multierror"
14+
"github.com/jetstack/venafi-connection-lib/http_client"
1315
"github.com/spf13/cobra"
1416
"gopkg.in/yaml.v3"
1517
"k8s.io/client-go/rest"
18+
"k8s.io/client-go/transport"
1619

1720
"github.com/jetstack/preflight/api"
1821
"github.com/jetstack/preflight/pkg/client"
@@ -334,6 +337,7 @@ const (
334337
VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account"
335338
VenafiCloudVenafiConnection OutputMode = "Venafi Cloud VenafiConnection"
336339
LocalFile OutputMode = "Local File"
340+
MachineHub OutputMode = "MachineHub"
337341
)
338342

339343
// The command-line flags and the config file are combined into this struct by
@@ -420,6 +424,9 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
420424
case !flags.VenafiCloudMode && flags.CredentialsPath != "":
421425
mode = JetstackSecureOAuth
422426
reason = "--credentials-file was specified without --venafi-cloud"
427+
case flags.MachineHubMode:
428+
mode = MachineHub
429+
reason = "--machine-hub was specified"
423430
case flags.OutputPath != "":
424431
mode = LocalFile
425432
reason = "--output-path was specified"
@@ -433,6 +440,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
433440
" - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" +
434441
" - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" +
435442
" - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" +
443+
" - Use --machine-hub if you want to use the " + string(MachineHub) + " mode.\n" +
436444
" - Use --output-path or output-path in the config file for " + string(LocalFile) + " mode.")
437445
}
438446

@@ -762,6 +770,17 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
762770
}
763771
case LocalFile:
764772
outputClient = client.NewFileClient(cfg.OutputPath)
773+
case MachineHub:
774+
var (
775+
err error
776+
rootCAs *x509.CertPool
777+
)
778+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
779+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
780+
outputClient, err = client.NewCyberArk(httpClient)
781+
if err != nil {
782+
errs = multierror.Append(errs, err)
783+
}
765784
default:
766785
panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode))
767786
}

pkg/agent/config_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
199199
- Use --venafi-connection for the Venafi Cloud VenafiConnection mode.
200200
- Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode.
201201
- Use --api-token if you want to use the Jetstack Secure API Token mode.
202+
- Use --machine-hub if you want to use the MachineHub mode.
202203
- Use --output-path or output-path in the config file for Local File mode.`))
203204
assert.Nil(t, cl)
204205
})
@@ -617,6 +618,38 @@ func Test_ValidateAndCombineConfig(t *testing.T) {
617618
assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode)
618619
})
619620

621+
t.Run("--machine-hub selects MachineHub mode", func(t *testing.T) {
622+
t.Setenv("POD_NAMESPACE", "venafi")
623+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
624+
t.Setenv("ARK_SUBDOMAIN", "tlspk")
625+
t.Setenv("ARK_USERNAME", "[email protected]")
626+
t.Setenv("ARK_SECRET", "test-secret")
627+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
628+
withConfig(""),
629+
withCmdLineFlags("--period", "1m", "--machine-hub"))
630+
require.NoError(t, err)
631+
assert.Equal(t, MachineHub, got.OutputMode)
632+
assert.IsType(t, &client.CyberArkClient{}, cl)
633+
})
634+
635+
t.Run("--machine-hub without required environment variables", func(t *testing.T) {
636+
t.Setenv("POD_NAMESPACE", "venafi")
637+
t.Setenv("KUBECONFIG", withFile(t, fakeKubeconfig))
638+
t.Setenv("ARK_SUBDOMAIN", "")
639+
t.Setenv("ARK_USERNAME", "")
640+
t.Setenv("ARK_SECRET", "")
641+
got, cl, err := ValidateAndCombineConfig(discardLogs(),
642+
withConfig(""),
643+
withCmdLineFlags("--period", "1m", "--machine-hub"))
644+
assert.Equal(t, CombinedConfig{}, got)
645+
assert.Nil(t, cl)
646+
assert.EqualError(t, err, testutil.Undent(`
647+
validating creds: failed loading config using the MachineHub mode: 1 error occurred:
648+
* missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET
649+
650+
`))
651+
})
652+
620653
t.Run("argument: --output-file selects local file mode", func(t *testing.T) {
621654
log, gotLog := recordLogs(t)
622655
got, outputClient, err := ValidateAndCombineConfig(log,

pkg/client/client_cyberark.go

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,51 @@
11
package client
22

33
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
8+
"github.com/jetstack/preflight/api"
9+
"github.com/jetstack/preflight/pkg/internal/cyberark"
410
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
511
)
612

7-
type CyberArkClient = dataupload.CyberArkClient
13+
type CyberArkClient struct {
14+
configLoader cyberark.ClientConfigLoader
15+
httpClient *http.Client
16+
}
17+
18+
var _ Client = &CyberArkClient{}
19+
20+
func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
21+
configLoader := cyberark.LoadClientConfigFromEnvironment
22+
_, err := configLoader()
23+
if err != nil {
24+
return nil, err
25+
}
26+
return &CyberArkClient{
27+
configLoader: configLoader,
28+
httpClient: httpClient,
29+
}, nil
30+
}
31+
32+
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
33+
// ARK_SUBDOMAIN should be your tenant subdomain.
34+
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, options Options) error {
35+
cfg, err := o.configLoader()
36+
if err != nil {
37+
return err
38+
}
39+
datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, cfg)
40+
if err != nil {
41+
return fmt.Errorf("while initializing data upload client: %s", err)
42+
}
843

9-
var NewCyberArkClient = dataupload.New
44+
err = datauploadClient.PutSnapshot(ctx, dataupload.Snapshot{
45+
ClusterID: options.ClusterID,
46+
})
47+
if err != nil {
48+
return fmt.Errorf("while uploading snapshot: %s", err)
49+
}
50+
return nil
51+
}

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ type CyberArkClient struct {
3535
authenticateRequest func(req *http.Request) error
3636
}
3737

38-
type Options struct {
39-
ClusterName string
40-
}
41-
4238
func New(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient {
4339
return &CyberArkClient{
4440
baseURL: baseURL,

pkg/internal/cyberark/identity/mock.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const (
2828
// mockSuccessfulStartAuthenticationToken is the token returned by the
2929
// mock server in response to a successful AdvanceAuthentication request
3030
// Must match what's in testdata/advance_authentication_success.json
31-
mockSuccessfulStartAuthenticationToken = "long-token"
31+
mockSuccessfulStartAuthenticationToken = "success-token"
3232
)
3333

3434
var (

pkg/internal/cyberark/identity/testdata/advance_authentication_success.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"Result": {
44
"AuthLevel": "Normal",
55
"DisplayName": "Namey McNamerson",
6-
"Token": "long-token",
6+
"Token": "success-token",
77
"Auth": "auth-auth",
88
"UserId": "11111111-2222-3333-4444-555555555555",
99
"EmailAddress": "[email protected]",

pkg/internal/cyberark/servicediscovery/discovery.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@ const (
1616
// ProdDiscoveryAPIBaseURL is the base URL for the production CyberArk Service Discovery API
1717
ProdDiscoveryAPIBaseURL = "https://platform-discovery.cyberark.cloud/api/v2/"
1818

19-
// identityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API
19+
// IdentityServiceName is the name of the identity service we're looking for in responses from the Service Discovery API
2020
// We were told to use the identity_administration field, not the identity_user_portal field.
21-
identityServiceName = "identity_administration"
21+
IdentityServiceName = "identity_administration"
22+
23+
// DiscoveryContextServiceName is the name of the discovery and context API
24+
// in responses from the Service Discovery API.
25+
DiscoveryContextServiceName = "discoverycontext"
2226

2327
// maxDiscoverBodySize is the maximum allowed size for a response body from the CyberArk Service Discovery subdomain endpoint
2428
// As of 2025-04-16, a response from the integration environment is ~4kB
@@ -101,7 +105,6 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
101105
}
102106

103107
var services Services
104-
105108
err = json.NewDecoder(io.LimitReader(resp.Body, maxDiscoverBodySize)).Decode(&services)
106109
if err != nil {
107110
if err == io.ErrUnexpectedEOF {
@@ -112,7 +115,7 @@ func (c *Client) DiscoverServices(ctx context.Context, subdomain string) (*Servi
112115
}
113116

114117
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)
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)
116119
}
117120

118121
return &services, nil

pkg/internal/cyberark/servicediscovery/discovery_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func Test_DiscoverIdentityAPIURL(t *testing.T) {
3131
"no identity service in response": {
3232
subdomain: "no-identity",
3333
expectedURL: "",
34-
expectedError: fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", identityServiceName),
34+
expectedError: fmt.Errorf("didn't find %s in service discovery response, which may indicate a suspended tenant; unable to detect CyberArk Identity API URL", IdentityServiceName),
3535
},
3636
"unexpected HTTP response": {
3737
subdomain: "bad-request",

pkg/testutil/envtest.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import (
2626
"sigs.k8s.io/controller-runtime/pkg/envtest"
2727

2828
"github.com/jetstack/preflight/pkg/client"
29+
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
30+
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
31+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
2932
)
3033

3134
// To see the API server logs, set:
@@ -259,6 +262,22 @@ func FakeTPP(t testing.TB) (*httptest.Server, *x509.Certificate) {
259262
return server, cert
260263
}
261264

265+
func FakeCyberArk(t *testing.T) *http.Client {
266+
t.Helper()
267+
268+
identityAPI, _ := identity.MockIdentityServer(t)
269+
discoveryContextAPI, _ := dataupload.MockDataUploadServer(t)
270+
httpClient := servicediscovery.MockDiscoveryServer(t, servicediscovery.Services{
271+
Identity: servicediscovery.ServiceEndpoint{
272+
API: identityAPI,
273+
},
274+
DiscoveryContext: servicediscovery.ServiceEndpoint{
275+
API: discoveryContextAPI,
276+
},
277+
})
278+
return httpClient
279+
}
280+
262281
// Generated using:
263282
//
264283
// helm template ./deploy/charts/venafi-kubernetes-agent -n venafi --set crds.venafiConnection.include=true --show-only templates/venafi-connection-rbac.yaml | grep -ivE '(helm|\/version)'

0 commit comments

Comments
 (0)