Skip to content

Commit 0bfb107

Browse files
CyberArk(agent): add support for MachineHub output mode
- Introduced a new `MachineHub` output mode in the agent configuration. - Added `--machine-hub` flag to enable the `MachineHub` mode. - Implemented `CyberArkClient` for publishing data readings to CyberArk's API. - Created `LoadClientConfigFromEnvironment` to load CyberArk client configuration from environment variables. - Updated tests to cover `MachineHub` mode and CyberArk client functionality. - Modified mock data and discovery logic to support CyberArk integration. Signed-off-by: Richard Wall <[email protected]>
1 parent 1f1b39f commit 0bfb107

File tree

14 files changed

+368
-65
lines changed

14 files changed

+368
-65
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: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,57 @@
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+
// CyberArkClient is a client for publishing data readings to CyberArk's discoverycontext API.
14+
type CyberArkClient struct {
15+
configLoader cyberark.ClientConfigLoader
16+
httpClient *http.Client
17+
}
18+
19+
var _ Client = &CyberArkClient{}
20+
21+
// NewCyberArk initializes a CyberArk client using configuration from environment variables.
22+
// It requires an HTTP client to be provided, which will be used for making requests.
23+
// The environment variables ARK_SUBDOMAIN, ARK_USERNAME, and ARK_SECRET must be set for authentication.
24+
// If the configuration is invalid or missing, an error is returned.
25+
func NewCyberArk(httpClient *http.Client) (*CyberArkClient, error) {
26+
configLoader := cyberark.LoadClientConfigFromEnvironment
27+
_, err := configLoader()
28+
if err != nil {
29+
return nil, err
30+
}
31+
return &CyberArkClient{
32+
configLoader: configLoader,
33+
httpClient: httpClient,
34+
}, nil
35+
}
36+
37+
// PostDataReadingsWithOptions uploads data readings to CyberArk using the provided options.
38+
// It initializes a data upload client with the configured HTTP client and credentials,
39+
// then uploads a snapshot with the specified cluster ID from the options.
40+
func (o *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, options Options) error {
41+
cfg, err := o.configLoader()
42+
if err != nil {
43+
return err
44+
}
45+
datauploadClient, err := cyberark.NewDatauploadClient(ctx, o.httpClient, cfg)
46+
if err != nil {
47+
return fmt.Errorf("while initializing data upload client: %s", err)
48+
}
849

9-
var NewCyberArkClient = dataupload.New
50+
err = datauploadClient.PutSnapshot(ctx, dataupload.Snapshot{
51+
ClusterID: options.ClusterID,
52+
})
53+
if err != nil {
54+
return fmt.Errorf("while uploading snapshot: %s", err)
55+
}
56+
return nil
57+
}

pkg/client/client_cyberark_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package client_test
2+
3+
import (
4+
"net/http"
5+
"os"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"k8s.io/client-go/transport"
10+
"k8s.io/klog/v2"
11+
"k8s.io/klog/v2/ktesting"
12+
13+
"github.com/jetstack/preflight/api"
14+
"github.com/jetstack/preflight/pkg/client"
15+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
16+
"github.com/jetstack/preflight/pkg/testutil"
17+
18+
_ "k8s.io/klog/v2/ktesting/init"
19+
)
20+
21+
func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) {
22+
t.Setenv("ARK_SUBDOMAIN", servicediscovery.MockDiscoverySubdomain)
23+
t.Setenv("ARK_USERNAME", "[email protected]")
24+
t.Setenv("ARK_SECRET", "somepassword")
25+
t.Run("success", func(t *testing.T) {
26+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
27+
ctx := klog.NewContext(t.Context(), logger)
28+
29+
httpClient := testutil.FakeCyberArk(t)
30+
31+
c, err := client.NewCyberArk(httpClient)
32+
require.NoError(t, err)
33+
34+
var readings []*api.DataReading
35+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
36+
ClusterID: "success-cluster-id",
37+
})
38+
require.NoError(t, err)
39+
})
40+
}
41+
42+
// TestCyberArkClient_PostDataReadingsWithOptions_RealAPI demonstrates that the
43+
// dataupload code works with the real inventory API.
44+
//
45+
// To enable verbose request logging:
46+
//
47+
// go test ./pkg/internal/cyberark/dataupload/... \
48+
// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6
49+
func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
50+
t.Run("success", func(t *testing.T) {
51+
subdomain := os.Getenv("ARK_SUBDOMAIN")
52+
username := os.Getenv("ARK_USERNAME")
53+
secret := os.Getenv("ARK_SECRET")
54+
55+
if subdomain == "" || username == "" || secret == "" {
56+
t.Skip("Skipping because one of the following environment variables is unset or empty: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
57+
return
58+
}
59+
60+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
61+
ctx := klog.NewContext(t.Context(), logger)
62+
63+
c, err := client.NewCyberArk(&http.Client{
64+
Transport: transport.NewDebuggingRoundTripper(http.DefaultTransport, transport.DebugByContext),
65+
})
66+
require.NoError(t, err)
67+
var readings []*api.DataReading
68+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{
69+
ClusterID: "success-cluster-id",
70+
})
71+
require.NoError(t, err)
72+
})
73+
}

pkg/internal/cyberark/client.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cyberark
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net/http"
7+
"os"
8+
9+
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
10+
"github.com/jetstack/preflight/pkg/internal/cyberark/identity"
11+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
12+
)
13+
14+
// ClientConfig holds the configuration needed to initialize a CyberArk client.
15+
type ClientConfig struct {
16+
Subdomain string
17+
Username string
18+
Secret string
19+
}
20+
21+
// ClientConfigLoader is a function type that loads and returns a ClientConfig.
22+
type ClientConfigLoader func() (ClientConfig, error)
23+
24+
// ErrMissingEnvironmentVariables is returned when required environment variables are not set.
25+
var ErrMissingEnvironmentVariables = errors.New("missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
26+
27+
// LoadClientConfigFromEnvironment loads the CyberArk client configuration from environment variables.
28+
// It expects the following environment variables to be set:
29+
// - ARK_SUBDOMAIN: The CyberArk subdomain to use.
30+
// - ARK_USERNAME: The username for authentication.
31+
// - ARK_SECRET: The secret for authentication.
32+
func LoadClientConfigFromEnvironment() (ClientConfig, error) {
33+
subdomain := os.Getenv("ARK_SUBDOMAIN")
34+
username := os.Getenv("ARK_USERNAME")
35+
secret := os.Getenv("ARK_SECRET")
36+
37+
if subdomain == "" || username == "" || secret == "" {
38+
return ClientConfig{}, ErrMissingEnvironmentVariables
39+
}
40+
41+
return ClientConfig{
42+
Subdomain: subdomain,
43+
Username: username,
44+
Secret: secret,
45+
}, nil
46+
47+
}
48+
49+
// NewDatauploadClient initializes and returns a new CyberArk Data Upload client.
50+
// It performs service discovery to find the necessary API endpoints and authenticates
51+
// using the provided client configuration.
52+
func NewDatauploadClient(ctx context.Context, httpClient *http.Client, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
53+
discoveryClient := servicediscovery.New(httpClient)
54+
serviceMap, err := discoveryClient.DiscoverServices(ctx, cfg.Subdomain)
55+
if err != nil {
56+
return nil, err
57+
}
58+
identityAPI := serviceMap.Identity.API
59+
if identityAPI == "" {
60+
return nil, errors.New("service discovery returned an empty identity API")
61+
}
62+
identityClient := identity.New(httpClient, identityAPI, cfg.Subdomain)
63+
err = identityClient.LoginUsernamePassword(ctx, cfg.Username, []byte(cfg.Secret))
64+
if err != nil {
65+
return nil, err
66+
}
67+
discoveryAPI := serviceMap.DiscoveryContext.API
68+
if discoveryAPI == "" {
69+
return nil, errors.New("service discovery returned an empty discovery API")
70+
}
71+
return dataupload.New(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil
72+
}

pkg/internal/cyberark/client_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package cyberark_test
2+
3+
import (
4+
"crypto/x509"
5+
"errors"
6+
"testing"
7+
8+
"github.com/jetstack/venafi-connection-lib/http_client"
9+
"github.com/stretchr/testify/require"
10+
"k8s.io/client-go/transport"
11+
"k8s.io/klog/v2"
12+
"k8s.io/klog/v2/ktesting"
13+
14+
"github.com/jetstack/preflight/pkg/internal/cyberark"
15+
"github.com/jetstack/preflight/pkg/internal/cyberark/dataupload"
16+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
17+
"github.com/jetstack/preflight/pkg/testutil"
18+
"github.com/jetstack/preflight/pkg/version"
19+
20+
_ "k8s.io/klog/v2/ktesting/init"
21+
)
22+
23+
// TestCyberArkClient_PutSnapshot_MockAPI demonstrates that NewDatauploadClient works with the mock API.
24+
func TestCyberArkClient_PutSnapshot_MockAPI(t *testing.T) {
25+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
26+
ctx := klog.NewContext(t.Context(), logger)
27+
28+
httpClient := testutil.FakeCyberArk(t)
29+
30+
cfg := cyberark.ClientConfig{
31+
Subdomain: servicediscovery.MockDiscoverySubdomain,
32+
Username: "[email protected]",
33+
Secret: "somepassword",
34+
}
35+
36+
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg)
37+
require.NoError(t, err)
38+
39+
err = cl.PutSnapshot(ctx, dataupload.Snapshot{
40+
ClusterID: "success-cluster-id",
41+
})
42+
require.NoError(t, err)
43+
}
44+
45+
// TestCyberArkClient_PutSnapshot_RealAPI demonstrates that NewDatauploadClient works with the real inventory API.
46+
//
47+
// An API token is obtained by authenticating with the ARK_USERNAME and ARK_SECRET from the environment.
48+
// ARK_SUBDOMAIN should be your tenant subdomain.
49+
//
50+
// To test against a tenant on the integration platform, also set:
51+
// ARK_DISCOVERY_API=https://platform-discovery.integration-cyberark.cloud/api/v2
52+
//
53+
// To enable verbose request logging:
54+
//
55+
// go test ./pkg/internal/cyberark \
56+
// -v -count 1 -run TestCyberArkClient_PutSnapshot_RealAPI -args -testing.v 6
57+
func TestCyberArkClient_PutSnapshot_RealAPI(t *testing.T) {
58+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
59+
ctx := klog.NewContext(t.Context(), logger)
60+
61+
var rootCAs *x509.CertPool
62+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
63+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
64+
65+
cfg, err := cyberark.LoadClientConfigFromEnvironment()
66+
if err != nil {
67+
if errors.Is(err, cyberark.ErrMissingEnvironmentVariables) {
68+
t.Skipf("Skipping: %s", err)
69+
}
70+
require.NoError(t, err)
71+
}
72+
73+
cl, err := cyberark.NewDatauploadClient(ctx, httpClient, cfg)
74+
require.NoError(t, err)
75+
76+
err = cl.PutSnapshot(ctx, dataupload.Snapshot{
77+
ClusterID: "bb068932-c80d-460d-88df-34bc7f3f3297",
78+
})
79+
require.NoError(t, err)
80+
}

0 commit comments

Comments
 (0)