diff --git a/README.md b/README.md index 08a1caeb..911197ec 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,19 @@ To build and run a version from master: go run main.go agent --agent-config-file ./path/to/agent/config/file.yaml -p 0h1m0s ``` -You can find an example agent file [here](https://github.com/jetstack/preflight/blob/master/agent.yaml). +You can configure the agent to perform one data gathering loop and output the data to a local file: + +```bash +go run . agent \ + --agent-config-file examples/one-shot-secret.yaml \ + --one-shot \ + --output-path output.json +``` + +> Some examples of agent configuration files: +> - [./agent.yaml](./agent.yaml). +> - [./examples/one-shot-secret.yaml](./examples/one-shot-secret.yaml). +> - [./examples/cert-manager-agent.yaml](./examples/cert-manager-agent.yaml). You might also want to run a local echo server to monitor requests sent by the agent: diff --git a/cmd/agent_test.go b/cmd/agent_test.go index 85fc266d..265c9abb 100644 --- a/cmd/agent_test.go +++ b/cmd/agent_test.go @@ -19,8 +19,6 @@ func TestAgentRunOneShot(t *testing.T) { "preflight", "agent", "--one-shot", - // TODO(wallrj): This should not be required when an `--input-file` has been supplied. - "--api-token=should-not-be-required", "--agent-config-file=testdata/agent/one-shot/success/config.yaml", "--input-path=testdata/agent/one-shot/success/input.json", "--output-path=/dev/null", diff --git a/examples/one-shot-secret.yaml b/examples/one-shot-secret.yaml index 08f6d8d9..3e054ade 100644 --- a/examples/one-shot-secret.yaml +++ b/examples/one-shot-secret.yaml @@ -4,7 +4,7 @@ # It gathers only secrets and it does not attempt to upload to Venafi. # For example: # -# builds/preflight agent \ +# go run . agent \ # --agent-config-file examples/one-shot-secret.yaml \ # --one-shot \ # --output-path output.json diff --git a/pkg/agent/config.go b/pkg/agent/config.go index 48923ddc..2f691938 100644 --- a/pkg/agent/config.go +++ b/pkg/agent/config.go @@ -324,15 +324,16 @@ func InitAgentCmdFlags(c *cobra.Command, cfg *AgentCmdFlags) { } -// TLSPKMode controls how to authenticate to TLSPK / Jetstack Secure. Only one -// TLSPKMode may be provided if using those backends. -type TLSPKMode string +// OutputMode controls how the collected data is published. +// Only one OutputMode may be provided. +type OutputMode string const ( - JetstackSecureOAuth TLSPKMode = "Jetstack Secure OAuth" - JetstackSecureAPIToken TLSPKMode = "Jetstack Secure API Token" - VenafiCloudKeypair TLSPKMode = "Venafi Cloud Key Pair Service Account" - VenafiCloudVenafiConnection TLSPKMode = "Venafi Cloud VenafiConnection" + JetstackSecureOAuth OutputMode = "Jetstack Secure OAuth" + JetstackSecureAPIToken OutputMode = "Jetstack Secure API Token" + VenafiCloudKeypair OutputMode = "Venafi Cloud Key Pair Service Account" + VenafiCloudVenafiConnection OutputMode = "Venafi Cloud VenafiConnection" + LocalFile OutputMode = "Local File" ) // The command-line flags and the config file are combined into this struct by @@ -345,7 +346,7 @@ type CombinedConfig struct { StrictMode bool OneShot bool - TLSPKMode TLSPKMode + OutputMode OutputMode // Used by all TLSPK modes. ClusterID string @@ -389,7 +390,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) { var ( - mode TLSPKMode + mode OutputMode reason string keysAndValues []any ) @@ -419,18 +420,25 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) case !flags.VenafiCloudMode && flags.CredentialsPath != "": mode = JetstackSecureOAuth reason = "--credentials-file was specified without --venafi-cloud" + case flags.OutputPath != "": + mode = LocalFile + reason = "--output-path was specified" + case cfg.OutputPath != "": + mode = LocalFile + reason = "output-path was specified in the config file" default: - return CombinedConfig{}, nil, fmt.Errorf("no TLSPK mode specified. " + - "To enable one of the TLSPK modes, you can:\n" + + return CombinedConfig{}, nil, fmt.Errorf("no output mode specified. " + + "To enable one of the output modes, you can:\n" + " - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the " + string(VenafiCloudKeypair) + " mode.\n" + " - Use --venafi-connection for the " + string(VenafiCloudVenafiConnection) + " mode.\n" + " - Use --credentials-file alone if you want to use the " + string(JetstackSecureOAuth) + " mode.\n" + - " - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.") + " - Use --api-token if you want to use the " + string(JetstackSecureAPIToken) + " mode.\n" + + " - Use --output-path or output-path in the config file for " + string(LocalFile) + " mode.") } keysAndValues = append(keysAndValues, "mode", mode, "reason", reason) - log.V(logs.Debug).Info("Configured to push to Venafi", keysAndValues...) - res.TLSPKMode = mode + log.V(logs.Debug).Info("Output mode selected", keysAndValues...) + res.OutputMode = mode } var errs error @@ -459,7 +467,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) endpointPath = cfg.Endpoint.Path case !hasServerField && !hasEndpointField: server = "https://preflight.jetstack.io" - if res.TLSPKMode == VenafiCloudKeypair { + if res.OutputMode == VenafiCloudKeypair { // The VenafiCloudVenafiConnection mode doesn't need a server. server = client.VenafiCloudProdURL } @@ -468,7 +476,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) if urlErr != nil || url.Hostname() == "" { errs = multierror.Append(errs, fmt.Errorf("server %q is not a valid URL", server)) } - if res.TLSPKMode == VenafiCloudVenafiConnection && server != "" { + if res.OutputMode == VenafiCloudVenafiConnection && server != "" { log.Info(fmt.Sprintf("ignoring the server field specified in the config file. In %s mode, this field is not needed.", VenafiCloudVenafiConnection)) server = "" } @@ -479,10 +487,10 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) // Validation of `venafi-cloud.upload_path`. { var uploadPath string - switch res.TLSPKMode { // nolint:exhaustive + switch res.OutputMode { // nolint:exhaustive case VenafiCloudKeypair: if cfg.VenafiCloud == nil || cfg.VenafiCloud.UploadPath == "" { - errs = multierror.Append(errs, fmt.Errorf("the venafi-cloud.upload_path field is required when using the %s mode", res.TLSPKMode)) + errs = multierror.Append(errs, fmt.Errorf("the venafi-cloud.upload_path field is required when using the %s mode", res.OutputMode)) break // Skip to the end of the switch statement. } _, urlErr := url.Parse(cfg.VenafiCloud.UploadPath) @@ -499,7 +507,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) // change this value with the new --venafi-connection flag, and this // field is simply ignored. if cfg.VenafiCloud != nil && cfg.VenafiCloud.UploadPath != "" { - log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.TLSPKMode)) + log.Info(fmt.Sprintf(`ignoring the venafi-cloud.upload_path field in the config file. In %s mode, this field is not needed.`, res.OutputMode)) } uploadPath = "" } @@ -517,7 +525,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) // https://venafi.atlassian.net/browse/VC-35385 is done. { if cfg.VenafiCloud != nil && cfg.VenafiCloud.UploaderID != "" { - log.Info(fmt.Sprintf(`ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in %s mode.`, res.TLSPKMode)) + log.Info(fmt.Sprintf(`ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in %s mode.`, res.OutputMode)) } } @@ -525,10 +533,10 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) { var clusterID string var organizationID string // Only used by the old jetstack-secure mode. - switch res.TLSPKMode { // nolint:exhaustive + switch res.OutputMode { // nolint:exhaustive case VenafiCloudKeypair, VenafiCloudVenafiConnection: if cfg.ClusterID == "" { - errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.TLSPKMode)) + errs = multierror.Append(errs, fmt.Errorf("cluster_id is required in %s mode", res.OutputMode)) } clusterID = cfg.ClusterID case JetstackSecureOAuth, JetstackSecureAPIToken: @@ -587,7 +595,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) var err error installNS, err = getInClusterNamespace() if err != nil { - if res.TLSPKMode == VenafiCloudVenafiConnection { + if res.OutputMode == VenafiCloudVenafiConnection { errs = multierror.Append(errs, fmt.Errorf("could not guess which namespace the agent is running in: %w", err)) } } @@ -596,7 +604,7 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) } // Validation of --venafi-connection and --venafi-connection-namespace. - if res.TLSPKMode == VenafiCloudVenafiConnection { + if res.OutputMode == VenafiCloudVenafiConnection { res.VenConnName = flags.VenConnName venConnNS := flags.VenConnNS if flags.VenConnNS == "" { @@ -643,12 +651,12 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) return CombinedConfig{}, nil, errs } - preflightClient, err := validateCredsAndCreateClient(log, flags.CredentialsPath, flags.ClientID, flags.PrivateKeyPath, flags.APIToken, res) + outputClient, err := validateCredsAndCreateClient(log, flags.CredentialsPath, flags.ClientID, flags.PrivateKeyPath, flags.APIToken, res) if err != nil { return CombinedConfig{}, nil, multierror.Prefix(err, "validating creds:") } - return res, preflightClient, nil + return res, outputClient, nil } // Validation of --credentials-file/-k, --client-id, and --private-key-path, @@ -660,9 +668,9 @@ func ValidateAndCombineConfig(log logr.Logger, cfg Config, flags AgentCmdFlags) func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClientID, flagPrivateKeyPath, flagAPIToken string, cfg CombinedConfig) (client.Client, error) { var errs error - var preflightClient client.Client + var outputClient client.Client metadata := &api.AgentMetadata{Version: version.PreflightVersion, ClusterID: cfg.ClusterID} - switch cfg.TLSPKMode { + switch cfg.OutputMode { case JetstackSecureOAuth: // Note that there are no command line flags to configure the // JetstackSecureOAuth mode. @@ -678,7 +686,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie break // Don't continue with the client if credentials file invalid. } - preflightClient, err = client.NewOAuthClient(metadata, creds, cfg.Server) + outputClient, err = client.NewOAuthClient(metadata, creds, cfg.Server) if err != nil { errs = multierror.Append(errs, err) } @@ -730,7 +738,7 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie log.Info("Loading upload_path from \"venafi-cloud\" configuration.") var err error - preflightClient, err = client.NewVenafiCloudClient(metadata, creds, cfg.Server, uploaderID, cfg.UploadPath) + outputClient, err = client.NewVenafiCloudClient(metadata, creds, cfg.Server, uploaderID, cfg.UploadPath) if err != nil { errs = multierror.Append(errs, err) } @@ -742,25 +750,27 @@ func validateCredsAndCreateClient(log logr.Logger, flagCredentialsPath, flagClie break // Don't continue with the client if kubeconfig wasn't loaded. } - preflightClient, err = client.NewVenConnClient(restCfg, metadata, cfg.InstallNS, cfg.VenConnName, cfg.VenConnNS, nil) + outputClient, err = client.NewVenConnClient(restCfg, metadata, cfg.InstallNS, cfg.VenConnName, cfg.VenConnNS, nil) if err != nil { errs = multierror.Append(errs, err) } case JetstackSecureAPIToken: var err error - preflightClient, err = client.NewAPITokenClient(metadata, flagAPIToken, cfg.Server) + outputClient, err = client.NewAPITokenClient(metadata, flagAPIToken, cfg.Server) if err != nil { errs = multierror.Append(errs, err) } + case LocalFile: + outputClient = client.NewFileClient(cfg.OutputPath) default: - panic(fmt.Errorf("programmer mistake: auth mode not implemented: %s", cfg.TLSPKMode)) + panic(fmt.Errorf("programmer mistake: output mode not implemented: %s", cfg.OutputMode)) } if errs != nil { - return nil, fmt.Errorf("failed loading config using the %s mode: %w", cfg.TLSPKMode, errs) + return nil, fmt.Errorf("failed loading config using the %s mode: %w", cfg.OutputMode, errs) } - return preflightClient, nil + return outputClient, nil } // Same as ValidateAndCombineConfig but just for validating the data gatherers. diff --git a/pkg/agent/config_test.go b/pkg/agent/config_test.go index de79c202..fe5211d3 100644 --- a/pkg/agent/config_test.go +++ b/pkg/agent/config_test.go @@ -96,7 +96,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { withCmdLineFlags("--period", "99m", "--credentials-file", fakeCredsPath)) require.NoError(t, err) assert.Equal(t, testutil.Undent(` - INFO Configured to push to Venafi mode="Jetstack Secure OAuth" reason="--credentials-file was specified without --venafi-cloud" + INFO Output mode selected mode="Jetstack Secure OAuth" reason="--credentials-file was specified without --venafi-cloud" INFO Both the 'period' field and --period are set. Using the value provided with --period. `), gotLogs.String()) assert.Equal(t, 99*time.Minute, got.Period) @@ -178,12 +178,12 @@ func Test_ValidateAndCombineConfig(t *testing.T) { // The log line printed by pflag is not captured by the log recorder. assert.Equal(t, testutil.Undent(` - INFO Configured to push to Venafi mode="Jetstack Secure OAuth" reason="--credentials-file was specified without --venafi-cloud" + INFO Output mode selected mode="Jetstack Secure OAuth" reason="--credentials-file was specified without --venafi-cloud" INFO Using period from config period="1h0m0s" `), b.String()) }) - t.Run("error when no auth method specified", func(t *testing.T) { + t.Run("error when no output mode specified", func(t *testing.T) { _, cl, err := ValidateAndCombineConfig(discardLogs(), withConfig(testutil.Undent(` server: https://api.venafi.eu @@ -194,11 +194,12 @@ func Test_ValidateAndCombineConfig(t *testing.T) { withoutCmdLineFlags(), ) assert.EqualError(t, err, testutil.Undent(` - no TLSPK mode specified. To enable one of the TLSPK modes, you can: + no output mode specified. To enable one of the output modes, you can: - Use (--venafi-cloud with --credentials-file) or (--client-id with --private-key-path) to use the Venafi Cloud Key Pair Service Account mode. - Use --venafi-connection for the Venafi Cloud VenafiConnection mode. - Use --credentials-file alone if you want to use the Jetstack Secure OAuth mode. - - Use --api-token if you want to use the Jetstack Secure API Token mode.`)) + - Use --api-token if you want to use the Jetstack Secure API Token mode. + - Use --output-path or output-path in the config file for Local File mode.`)) assert.Nil(t, cl) }) @@ -226,8 +227,8 @@ func Test_ValidateAndCombineConfig(t *testing.T) { withCmdLineFlags("--credentials-file", credsPath), ) expect := CombinedConfig{ - TLSPKMode: "Jetstack Secure OAuth", - ClusterID: "example-cluster", + OutputMode: "Jetstack Secure OAuth", + ClusterID: "example-cluster", DataGatherers: []DataGatherer{{Kind: "dummy", Name: "d1", Config: &dummyConfig{}, @@ -275,7 +276,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { InputPath: "/home", OutputPath: "/nothome", UploadPath: "/testing/path", - TLSPKMode: VenafiCloudKeypair, + OutputMode: VenafiCloudKeypair, ClusterID: "the cluster name", BackoffMaxTime: 99 * time.Minute, InstallNS: "venafi", @@ -299,7 +300,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { withCmdLineFlags("--client-id", "5bc7d07c-45da-11ef-a878-523f1e1d7de1", "--private-key-path", privKeyPath), ) require.NoError(t, err) - assert.Equal(t, VenafiCloudKeypair, got.TLSPKMode) + assert.Equal(t, VenafiCloudKeypair, got.OutputMode) assert.IsType(t, &client.VenafiCloudClient{}, cl) }) @@ -388,7 +389,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { `)), withCmdLineFlags("--credentials-file", path)) require.NoError(t, err) - assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, OrganizationID: "foo", ClusterID: "bar", TLSPKMode: JetstackSecureOAuth, BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) + assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, OrganizationID: "foo", ClusterID: "bar", OutputMode: JetstackSecureOAuth, BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) assert.IsType(t, &client.OAuthClient{}, cl) }) @@ -467,7 +468,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { `)), withCmdLineFlags("--client-id", "5bc7d07c-45da-11ef-a878-523f1e1d7de1", "--private-key-path", path)) require.NoError(t, err) - assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, TLSPKMode: VenafiCloudKeypair, ClusterID: "the cluster name", UploadPath: "/foo/bar", BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) + assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, OutputMode: VenafiCloudKeypair, ClusterID: "the cluster name", UploadPath: "/foo/bar", BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) assert.IsType(t, &client.VenafiCloudClient{}, cl) }) @@ -489,7 +490,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { `)), withCmdLineFlags("--venafi-cloud", "--credentials-file", credsPath)) require.NoError(t, err) - assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, TLSPKMode: VenafiCloudKeypair, ClusterID: "the cluster name", UploadPath: "/foo/bar", BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) + assert.Equal(t, CombinedConfig{Server: "https://api.venafi.eu", Period: time.Hour, OutputMode: VenafiCloudKeypair, ClusterID: "the cluster name", UploadPath: "/foo/bar", BackoffMaxTime: 10 * time.Minute, InstallNS: "venafi"}, got) }) t.Run("venafi-cloud-keypair-auth: venafi-cloud.upload_path field is required", func(t *testing.T) { @@ -566,7 +567,7 @@ func Test_ValidateAndCombineConfig(t *testing.T) { assert.Equal(t, CombinedConfig{ Period: 1 * time.Hour, ClusterID: "the cluster name", - TLSPKMode: VenafiCloudVenafiConnection, + OutputMode: VenafiCloudVenafiConnection, VenConnName: "venafi-components", VenConnNS: "venafi", InstallNS: "venafi", @@ -592,13 +593,13 @@ func Test_ValidateAndCombineConfig(t *testing.T) { ) require.NoError(t, err) assert.Equal(t, testutil.Undent(` - INFO Configured to push to Venafi venConnName="venafi-components" mode="Venafi Cloud VenafiConnection" reason="--venafi-connection was specified" + INFO Output mode selected venConnName="venafi-components" mode="Venafi Cloud VenafiConnection" reason="--venafi-connection was specified" INFO ignoring the server field specified in the config file. In Venafi Cloud VenafiConnection mode, this field is not needed. INFO ignoring the venafi-cloud.upload_path field in the config file. In Venafi Cloud VenafiConnection mode, this field is not needed. INFO ignoring the venafi-cloud.uploader_id field in the config file. This field is not needed in Venafi Cloud VenafiConnection mode. INFO Using period from config period="1h0m0s" `), gotLogs.String()) - assert.Equal(t, VenafiCloudVenafiConnection, got.TLSPKMode) + assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode) assert.IsType(t, &client.VenConnClient{}, gotCl) }) @@ -613,7 +614,35 @@ func Test_ValidateAndCombineConfig(t *testing.T) { `)), withCmdLineFlags("--venafi-connection", "venafi-components")) require.NoError(t, err) - assert.Equal(t, VenafiCloudVenafiConnection, got.TLSPKMode) + assert.Equal(t, VenafiCloudVenafiConnection, got.OutputMode) + }) + + t.Run("argument: --output-file selects local file mode", func(t *testing.T) { + log, gotLog := recordLogs(t) + got, outputClient, err := ValidateAndCombineConfig(log, + withConfig(""), + withCmdLineFlags("--period", "1m", "--output-path", "/foo/bar/baz")) + require.NoError(t, err) + assert.Equal(t, LocalFile, got.OutputMode) + assert.Equal(t, testutil.Undent(` + INFO Output mode selected mode="Local File" reason="--output-path was specified" + `), gotLog.String()) + assert.IsType(t, &client.FileClient{}, outputClient) + }) + + t.Run("config: output-path selects local file mode", func(t *testing.T) { + log, gotLog := recordLogs(t) + got, outputClient, err := ValidateAndCombineConfig(log, + withConfig(testutil.Undent(` + output-path: /foo/bar/baz + `)), + withCmdLineFlags("--period=1h")) + require.NoError(t, err) + assert.Equal(t, LocalFile, got.OutputMode) + assert.Equal(t, testutil.Undent(` + INFO Output mode selected mode="Local File" reason="output-path was specified in the config file" + `), gotLog.String()) + assert.IsType(t, &client.FileClient{}, outputClient) }) // When --input-path is supplied, the data is being read from a local file @@ -637,7 +666,6 @@ func Test_ValidateAndCombineConfig(t *testing.T) { "--one-shot", "--input-path", expectedInputPath, "--output-path", "/dev/null", - "--api-token", "should-not-be-required", ), ) require.NoError(t, err) @@ -680,7 +708,7 @@ func Test_ValidateAndCombineConfig_VenafiCloudKeyPair(t *testing.T) { ) require.NoError(t, err) testutil.TrustCA(t, cl, cert) - assert.Equal(t, VenafiCloudKeypair, got.TLSPKMode) + assert.Equal(t, VenafiCloudKeypair, got.OutputMode) err = cl.PostDataReadingsWithOptions(ctx, nil, client.Options{ClusterName: "test cluster name"}) require.NoError(t, err) diff --git a/pkg/agent/run.go b/pkg/agent/run.go index 43dd8aa6..7bf737c3 100644 --- a/pkg/agent/run.go +++ b/pkg/agent/run.go @@ -322,17 +322,7 @@ func gatherAndOutputData(ctx context.Context, eventf Eventf, config CombinedConf } } - if config.OutputPath != "" { - data, err := json.MarshalIndent(readings, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal JSON: %s", err) - } - err = os.WriteFile(config.OutputPath, data, 0644) - if err != nil { - return fmt.Errorf("failed to output to local file: %s", err) - } - log.Info("Data saved to local file", "outputPath", config.OutputPath) - } else { + { group, ctx := errgroup.WithContext(ctx) backOff := backoff.NewExponentialBackOff() @@ -413,39 +403,19 @@ func gatherData(ctx context.Context, config CombinedConfig, dataGatherers map[st func postData(ctx context.Context, config CombinedConfig, preflightClient client.Client, readings []*api.DataReading) error { log := klog.FromContext(ctx).WithName("postData") - baseURL := config.Server - - log.V(logs.Debug).Info("Posting data", "baseURL", baseURL) - - switch config.TLSPKMode { // nolint:exhaustive - case VenafiCloudKeypair, VenafiCloudVenafiConnection: + ctx = klog.NewContext(ctx, log) + err := preflightClient.PostDataReadingsWithOptions(ctx, readings, client.Options{ + ClusterName: config.ClusterID, + ClusterDescription: config.ClusterDescription, // orgID and clusterID are not required for Venafi Cloud auth - err := preflightClient.PostDataReadingsWithOptions(ctx, readings, client.Options{ - ClusterName: config.ClusterID, - ClusterDescription: config.ClusterDescription, - }) - if err != nil { - return fmt.Errorf("post to server failed: %+v", err) - } - log.Info("Data sent successfully") - - return nil - - case JetstackSecureOAuth, JetstackSecureAPIToken: - err := preflightClient.PostDataReadingsWithOptions(ctx, readings, client.Options{ - OrgID: config.OrganizationID, - ClusterID: config.ClusterID, - }) - if err != nil { - return fmt.Errorf("post to server failed: %+v", err) - } - log.Info("Data sent successfully") - - return err - - default: - return fmt.Errorf("not implemented for mode %s", config.TLSPKMode) + OrgID: config.OrganizationID, + ClusterID: config.ClusterID, + }) + if err != nil { + return fmt.Errorf("post to server failed: %+v", err) } + log.Info("Data sent successfully") + return nil } // listenAndServe starts the supplied HTTP server and stops it gracefully when diff --git a/pkg/client/client_file.go b/pkg/client/client_file.go new file mode 100644 index 00000000..419f2164 --- /dev/null +++ b/pkg/client/client_file.go @@ -0,0 +1,37 @@ +package client + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "k8s.io/klog/v2" + + "github.com/jetstack/preflight/api" +) + +// FileClient writes the supplied readings to a file, in JSON format. +type FileClient struct { + path string +} + +func NewFileClient(path string) Client { + return &FileClient{ + path: path, + } +} + +func (o *FileClient) PostDataReadingsWithOptions(ctx context.Context, readings []*api.DataReading, _ Options) error { + log := klog.FromContext(ctx) + data, err := json.MarshalIndent(readings, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %s", err) + } + err = os.WriteFile(o.path, data, 0644) + if err != nil { + return fmt.Errorf("failed to write file: %s", err) + } + log.Info("Data saved to local file", "outputPath", o.path) + return nil +} diff --git a/pkg/client/client_file_test.go b/pkg/client/client_file_test.go new file mode 100644 index 00000000..484a8b5e --- /dev/null +++ b/pkg/client/client_file_test.go @@ -0,0 +1,82 @@ +package client + +import ( + "encoding/json" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/klog/v2" + "k8s.io/klog/v2/ktesting" + + "github.com/jetstack/preflight/api" +) + +func TestFileClient_PostDataReadingsWithOptions(t *testing.T) { + type testCase struct { + name string + path string + readings []*api.DataReading + expectedJSON string + expectedError string + } + tests := []testCase{ + { + name: "success", + path: "{tmp}/data.json", + readings: []*api.DataReading{}, + expectedJSON: "[]", + }, + { + name: "success-overwrite", + path: "{tmp}/exists.json", + readings: []*api.DataReading{}, + expectedJSON: "[]", + }, + { + name: "json-marshal-error", + path: "{tmp}/data.json", + readings: []*api.DataReading{ + { + Data: json.RawMessage("x"), + }, + }, + expectedError: "failed to marshal JSON: json: error calling MarshalJSON for type json.RawMessage: invalid character 'x' looking for beginning of value", + expectedJSON: "[]", + }, + { + name: "no-such-file-or-directory", + path: "{tmp}/no-such-folder/data.json", + readings: []*api.DataReading{}, + expectedError: "failed to write file: open {tmp}/no-such-folder/data.json: no such file or directory", + expectedJSON: "[]", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + log := ktesting.NewLogger(t, ktesting.DefaultConfig) + ctx := klog.NewContext(t.Context(), log) + tmpDir := t.TempDir() + require.NoError(t, os.WriteFile(tmpDir+"/exists.json", []byte("existing-content"), 0644)) + + path := strings.ReplaceAll(tc.path, "{tmp}", tmpDir) + expectedError := strings.ReplaceAll(tc.expectedError, "{tmp}", tmpDir) + + c := NewFileClient(path) + err := c.PostDataReadingsWithOptions(ctx, tc.readings, Options{}) + + if expectedError != "" { + assert.EqualError(t, err, expectedError) + return + } + require.NoError(t, err) + assert.FileExists(t, path) + actualJSON, err := os.ReadFile(path) + require.NoError(t, err) + assert.JSONEq(t, tc.expectedJSON, string(actualJSON)) + }) + } +}