Skip to content

Commit 2690901

Browse files
Add MachineHub output mode and a cyberark client
Signed-off-by: Richard Wall <[email protected]>
1 parent 57d3435 commit 2690901

File tree

17 files changed

+360
-247
lines changed

17 files changed

+360
-247
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.NewCyberArkClient
44+
err = datauploadClient.PutSnapshot(ctx, api.DataReadingsPost{}, dataupload.Options{
45+
ClusterID: options.ClusterID,
46+
})
47+
if err != nil {
48+
return fmt.Errorf("while posting snapshot: %s", err)
49+
}
50+
return nil
51+
}

pkg/client/client_cyberark_test.go

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

pkg/internal/cyberark/client.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
type ClientConfig struct {
15+
subdomain string
16+
username string
17+
secret string
18+
}
19+
20+
type ClientConfigLoader func() (ClientConfig, error)
21+
22+
func LoadClientConfigFromEnvironment() (ClientConfig, error) {
23+
subdomain := os.Getenv("ARK_SUBDOMAIN")
24+
username := os.Getenv("ARK_USERNAME")
25+
secret := os.Getenv("ARK_SECRET")
26+
27+
if subdomain == "" || username == "" || secret == "" {
28+
return ClientConfig{}, errors.New(
29+
"missing environment variables: ARK_SUBDOMAIN, ARK_USERNAME, ARK_SECRET")
30+
}
31+
32+
return ClientConfig{
33+
subdomain: subdomain,
34+
username: username,
35+
secret: secret,
36+
}, nil
37+
38+
}
39+
40+
func NewDatauploadClient(ctx context.Context, httpClient *http.Client, cfg ClientConfig) (*dataupload.CyberArkClient, error) {
41+
discoveryClient := servicediscovery.New(httpClient)
42+
serviceMap, err := discoveryClient.GetServices(ctx, cfg.subdomain)
43+
if err != nil {
44+
return nil, err
45+
}
46+
identityAPI := serviceMap.IdentityAPI()
47+
if identityAPI == "" {
48+
return nil, errors.New("service discovery returned an empty identity API")
49+
}
50+
identityClient := identity.New(httpClient, identityAPI, cfg.subdomain)
51+
err = identityClient.LoginUsernamePassword(ctx, cfg.username, []byte(cfg.secret))
52+
if err != nil {
53+
return nil, err
54+
}
55+
discoveryAPI := serviceMap.DiscoveryContextAPI()
56+
if discoveryAPI == "" {
57+
return nil, errors.New("service discovery returned an empty discovery API")
58+
}
59+
return dataupload.NewCyberArkClient(httpClient, discoveryAPI, identityClient.AuthenticateRequest), nil
60+
}

pkg/internal/cyberark/dataupload/dataupload.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"bytes"
55
"context"
66
"crypto/sha256"
7-
"crypto/x509"
87
"encoding/base64"
98
"encoding/hex"
109
"encoding/json"
@@ -13,8 +12,6 @@ import (
1312
"net/http"
1413
"net/url"
1514

16-
"k8s.io/client-go/transport"
17-
1815
"github.com/jetstack/preflight/api"
1916
"github.com/jetstack/preflight/pkg/version"
2017
)
@@ -27,7 +24,7 @@ const (
2724
// apiPathSnapshotLinks is the URL path of the snapshot-links endpoint of the inventory API.
2825
// This endpoint returns an AWS presigned URL.
2926
// TODO(wallrj): Link to CyberArk API documentation when it is published.
30-
apiPathSnapshotLinks = "/api/ingestions/kubernetes/snapshot-links"
27+
apiPathSnapshotLinks = "/ingestions/kubernetes/snapshot-links"
3128
)
3229

3330
type CyberArkClient struct {
@@ -38,25 +35,18 @@ type CyberArkClient struct {
3835
}
3936

4037
type Options struct {
41-
ClusterName string
38+
ClusterID string
4239
}
4340

44-
func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string, authenticateRequest func(req *http.Request) error) (*CyberArkClient, error) {
45-
cyberClient := &http.Client{}
46-
tr := http.DefaultTransport.(*http.Transport).Clone()
47-
if trustedCAs != nil {
48-
tr.TLSClientConfig.RootCAs = trustedCAs
49-
}
50-
cyberClient.Transport = transport.NewDebuggingRoundTripper(tr, transport.DebugByContext)
51-
41+
func NewCyberArkClient(httpClient *http.Client, baseURL string, authenticateRequest func(req *http.Request) error) *CyberArkClient {
5242
return &CyberArkClient{
5343
baseURL: baseURL,
54-
client: cyberClient,
44+
client: httpClient,
5545
authenticateRequest: authenticateRequest,
56-
}, nil
46+
}
5747
}
5848

59-
// PostDataReadingsWithOptions PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
49+
// PutSnapshot PUTs the supplied payload to an [AWS presigned URL] which it obtains via the CyberArk inventory API.
6050
// [AWS presigned URL]: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
6151
//
6252
// A SHA256 checksum header is included in the request, to verify that the payload
@@ -70,11 +60,7 @@ func NewCyberArkClient(trustedCAs *x509.CertPool, baseURL string, authenticateRe
7060
// If you omit that header, it is possible to PUT any data.
7161
// There is a work around listed in that issue which we have shared with the
7262
// CyberArk API team.
73-
func (c *CyberArkClient) PostDataReadingsWithOptions(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
74-
if opts.ClusterName == "" {
75-
return fmt.Errorf("programmer mistake: the cluster name (aka `cluster_id` in the config file) cannot be left empty")
76-
}
77-
63+
func (c *CyberArkClient) PutSnapshot(ctx context.Context, payload api.DataReadingsPost, opts Options) error {
7864
encodedBody := &bytes.Buffer{}
7965
hash := sha256.New()
8066
if err := json.NewEncoder(io.MultiWriter(encodedBody, hash)).Encode(payload); err != nil {
@@ -124,7 +110,7 @@ func (c *CyberArkClient) retrievePresignedUploadURL(ctx context.Context, checksu
124110
Checksum string `json:"checksum_sha256"`
125111
AgentVersion string `json:"agent_version"`
126112
}{
127-
ClusterID: opts.ClusterName,
113+
ClusterID: opts.ClusterID,
128114
Checksum: checksum,
129115
AgentVersion: version.PreflightVersion,
130116
}

0 commit comments

Comments
 (0)