Skip to content

Commit 5a2ae3c

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 5a2ae3c

File tree

17 files changed

+415
-65
lines changed

17 files changed

+415
-65
lines changed

.envrc.template

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Example .envrc file for use with direnv.
2+
# Copy this file to .envrc and edit the values as required.
3+
# Do not check in your .envrc file to source control as it may contain secrets.
4+
5+
# The following variables are required by the E2E test script: ./hack/e2e/test.sh.
6+
export VEN_API_KEY= # your Venafi Cloud API key with full permissions
7+
export VEN_API_KEY_PULL= # your Venafi Cloud API key with pull-only permissions
8+
export VEN_ZONE= # the Venafi Cloud zone to use for certificate requests
9+
export VEN_VCP_REGION= # the Venafi Cloud region to use (us or eu)
10+
export VEN_API_HOST= # the Venafi Cloud API host (usually api.venafi.cloud or api.venafi.eu)
11+
export OCI_BASE= # the base URL for the OCI registry where the Agent chart and image will be pushed
12+
export CLOUDSDK_CORE_PROJECT= # the GCP project ID where a GKE cluster will be created.
13+
export CLOUDSDK_COMPUTE_ZONE= # the GCP zone where a GKE cluster will be created. E.g. europe-west2-b
14+
export CLUSTER_NAME= # the name of the GKE cluster which will be created. E.g. cluster-1
15+
16+
# The following variables are required for CyberArk / MachineHub integration tests.
17+
export ARK_SUBDOMAIN= # your CyberArk tenant subdomain
18+
export ARK_USERNAME= # your CyberArk username
19+
export ARK_SECRET= # your CyberArk password
20+
# 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

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ predicate.json
1414
*.tgz
1515

1616
_bin
17+
.envrc

examples/machinehub.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# An example agent config for MachineHub output mode.
2+
#
3+
# For example:
4+
#
5+
# export ARK_SUBDOMAIN= # your CyberArk tenant subdomain
6+
# export ARK_USERNAME= # your CyberArk username
7+
# export ARK_SECRET= # your CyberArk password
8+
# go run . agent --one-shot --machine-hub -v 6 --agent-config-file ./examples/machinehub.yaml
9+
10+
data-gatherers:
11+
- kind: "dummy"
12+
name: "dummy"

pkg/agent/config.go

Lines changed: 26 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

@@ -548,6 +556,13 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags)
548556
}
549557
organizationID = cfg.OrganizationID
550558
clusterID = cfg.ClusterID
559+
case MachineHub:
560+
if cfg.ClusterID != "" {
561+
log.Info(fmt.Sprintf(`Ignoring the cluster_id field in the config file. This field is not needed in %s mode.`, res.OutputMode))
562+
}
563+
if cfg.OrganizationID != "" {
564+
log.Info(fmt.Sprintf(`Ignoring the organization_id field in the config file. This field is not needed in %s mode.`, res.OutputMode))
565+
}
551566
}
552567
res.OrganizationID = organizationID
553568
res.ClusterID = clusterID
@@ -762,6 +777,17 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie
762777
}
763778
case LocalFile:
764779
outputClient = client.NewFileClient(cfg.OutputPath)
780+
case MachineHub:
781+
var (
782+
err error
783+
rootCAs *x509.CertPool
784+
)
785+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
786+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
787+
outputClient, err = client.NewCyberArk(httpClient)
788+
if err != nil {
789+
errs = multierror.Append(errs, err)
790+
}
765791
default:
766792
panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode))
767793
}

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: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,62 @@
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"
11+
"github.com/jetstack/preflight/pkg/version"
512
)
613

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

9-
var NewCyberArkClient = dataupload.New
52+
err = datauploadClient.PutSnapshot(ctx, dataupload.Snapshot{
53+
// Temporary hard coded cluster ID.
54+
// TODO(wallrj): The clusterID will eventually be extracted from the supplied readings.
55+
ClusterID: "success-cluster-id",
56+
AgentVersion: version.PreflightVersion,
57+
})
58+
if err != nil {
59+
return fmt.Errorf("while uploading snapshot: %s", err)
60+
}
61+
return nil
62+
}

pkg/client/client_cyberark_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package client_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/api"
15+
"github.com/jetstack/preflight/pkg/client"
16+
"github.com/jetstack/preflight/pkg/internal/cyberark"
17+
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
18+
"github.com/jetstack/preflight/pkg/testutil"
19+
"github.com/jetstack/preflight/pkg/version"
20+
21+
_ "k8s.io/klog/v2/ktesting/init"
22+
)
23+
24+
// TestCyberArkClient_PostDataReadingsWithOptions_MockAPI demonstrates that the
25+
// dataupload code works with the mock CyberArk APIs.
26+
// The environment variables are chosen to match those expected by the mock
27+
// server.
28+
func TestCyberArkClient_PostDataReadingsWithOptions_MockAPI(t *testing.T) {
29+
t.Setenv("ARK_SUBDOMAIN", servicediscovery.MockDiscoverySubdomain)
30+
t.Setenv("ARK_USERNAME", "[email protected]")
31+
t.Setenv("ARK_SECRET", "somepassword")
32+
t.Run("success", func(t *testing.T) {
33+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
34+
ctx := klog.NewContext(t.Context(), logger)
35+
36+
httpClient := testutil.FakeCyberArk(t)
37+
38+
c, err := client.NewCyberArk(httpClient)
39+
require.NoError(t, err)
40+
41+
var readings []*api.DataReading
42+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{})
43+
require.NoError(t, err)
44+
})
45+
}
46+
47+
// TestCyberArkClient_PostDataReadingsWithOptions_RealAPI demonstrates that the
48+
// dataupload code works with the real CyberArk APIs.
49+
//
50+
// To enable verbose request logging:
51+
//
52+
// go test ./pkg/internal/cyberark/dataupload/... \
53+
// -v -count 1 -run TestCyberArkClient_PostDataReadingsWithOptions_RealAPI -args -testing.v 6
54+
func TestCyberArkClient_PostDataReadingsWithOptions_RealAPI(t *testing.T) {
55+
t.Run("success", func(t *testing.T) {
56+
logger := ktesting.NewLogger(t, ktesting.DefaultConfig)
57+
ctx := klog.NewContext(t.Context(), logger)
58+
59+
var rootCAs *x509.CertPool
60+
httpClient := http_client.NewDefaultClient(version.UserAgent(), rootCAs)
61+
httpClient.Transport = transport.NewDebuggingRoundTripper(httpClient.Transport, transport.DebugByContext)
62+
63+
c, err := client.NewCyberArk(httpClient)
64+
if err != nil {
65+
if errors.Is(err, cyberark.ErrMissingEnvironmentVariables) {
66+
t.Skipf("Skipping: %s", err)
67+
}
68+
require.NoError(t, err)
69+
}
70+
var readings []*api.DataReading
71+
err = c.PostDataReadingsWithOptions(ctx, readings, client.Options{})
72+
require.NoError(t, err)
73+
})
74+
}

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+
}

0 commit comments

Comments
 (0)