diff --git a/README.md b/README.md index 459afd5..b09e447 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ All plugin config fields are strings (agent gRPC `map` contract). | `policy_labels` | No | JSON map of labels merged into generated evidence labels. | | `resource_identity_fields` | No | JSON object mapping Cloud Custodian resource types to ordered identity field paths. Built-in defaults are used after configured fields. Example: `{"aws.ec2":["InstanceId","Arn"]}`. | | `debug_dump_payloads` | No | Boolean (`true`/`false`) toggle to write standardized resource payload JSON files for troubleshooting. Default: `false`. | -| `debug_payload_output_dir` | No | Directory where debug payload JSON files are written. If set, debug dumping is auto-enabled. Default when enabled without explicit path: `debug-standardized-payloads`. | +| `debug_payload_output_dir` | No | Directory where debug payload JSON files are written. If set, debug dumping is auto-enabled. Default when enabled without explicit path: `ccf-custodian-debug-payloads` under the OS temp directory (`$TMPDIR`, else `/tmp`), e.g. `/tmp/ccf-custodian-debug-payloads`. Use an absolute path: a relative value is still honoured but resolves against the plugin process working directory (which the agent may set per plugin) and logs a warning. The resolved absolute path is logged at `INFO` when dumping is enabled. | | `preserve_execution_artifacts` | No | Boolean (`true`/`false`) toggle to keep the temporary Cloud Custodian execution root after a check execution failure for postmortem review. Default: `false`. | Validation rules: diff --git a/main.go b/main.go index 06d0dae..fe95fd5 100644 --- a/main.go +++ b/main.go @@ -238,7 +238,7 @@ func (c *PluginConfig) Parse() (*ParsedConfig, error) { debugDumpPayloads = true } if debugDumpPayloads && debugPayloadOutputDir == "" { - debugPayloadOutputDir = "debug-standardized-payloads" + debugPayloadOutputDir = defaultDebugPayloadOutputDir() } return &ParsedConfig{ @@ -376,6 +376,15 @@ func custodianCachePath() string { return filepath.Join(os.TempDir(), "cloud-custodian.cache") } +// defaultDebugPayloadOutputDir is where standardized debug payloads are written +// when dumping is enabled without an explicit debug_payload_output_dir. The CCF +// agent may run each plugin with its own working directory, so plugins must not +// create files relative to their cwd; os.TempDir resolves to $TMPDIR or /tmp, +// which is always an absolute, writable location in the agent deployment. +func defaultDebugPayloadOutputDir() string { + return filepath.Join(os.TempDir(), "ccf-custodian-debug-payloads") +} + func custodianDiagnosticInterval(timeout time.Duration) time.Duration { if timeout <= 0 { return custodianWatchInterval @@ -2340,11 +2349,20 @@ func (p *CloudCustodianPlugin) Configure(req *proto.ConfigureRequest) (*proto.Co p.checks = checks if parsed.DebugDumpPayloads { + if !filepath.IsAbs(parsed.DebugPayloadOutputDir) { + p.Logger.Warn("debug_payload_output_dir is relative and resolves against the plugin process working directory; configure an absolute path", + "debug_payload_output_dir", parsed.DebugPayloadOutputDir, + ) + } if err := os.MkdirAll(parsed.DebugPayloadOutputDir, 0o755); err != nil { p.Logger.Error("Failed creating debug payload output directory", "debug_payload_output_dir", parsed.DebugPayloadOutputDir, "error", err) return nil, fmt.Errorf("failed creating debug payload output directory %q: %w", parsed.DebugPayloadOutputDir, err) } - p.Logger.Debug("Debug payload dumping enabled", "debug_payload_output_dir", parsed.DebugPayloadOutputDir) + resolvedDir := parsed.DebugPayloadOutputDir + if absDir, err := filepath.Abs(resolvedDir); err == nil { + resolvedDir = absDir + } + p.Logger.Info("Debug payload dumping enabled", "debug_payload_output_dir", resolvedDir) } if p.executor == nil { diff --git a/main_test.go b/main_test.go index fbcd891..f51f328 100644 --- a/main_test.go +++ b/main_test.go @@ -238,6 +238,132 @@ func TestPluginConfigParse(t *testing.T) { t.Fatalf("unexpected debug output dir: %s", parsed.DebugPayloadOutputDir) } }) + + t.Run("default debug output dir is absolute under os.TempDir", func(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + cfg := &PluginConfig{ + PoliciesYAML: "policies: []", + DebugDumpPayloads: "true", + } + parsed, err := cfg.Parse() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !filepath.IsAbs(parsed.DebugPayloadOutputDir) { + t.Fatalf("expected absolute default debug output dir, got %q", parsed.DebugPayloadOutputDir) + } + if parsed.DebugPayloadOutputDir != filepath.Join(os.TempDir(), "ccf-custodian-debug-payloads") { + t.Fatalf("expected default debug output dir under os.TempDir %q, got %q", os.TempDir(), parsed.DebugPayloadOutputDir) + } + }) + + t.Run("no debug output dir when dumping disabled", func(t *testing.T) { + parsed, err := (&PluginConfig{PoliciesYAML: "policies: []"}).Parse() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if parsed.DebugDumpPayloads || parsed.DebugPayloadOutputDir != "" { + t.Fatalf("expected debug dumping disabled with no dir, got %v %q", parsed.DebugDumpPayloads, parsed.DebugPayloadOutputDir) + } + }) + + t.Run("relative debug output dir is kept as configured", func(t *testing.T) { + parsed, err := (&PluginConfig{ + PoliciesYAML: "policies: []", + DebugPayloadOutputDir: "relative-debug-dir", + }).Parse() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !parsed.DebugDumpPayloads || parsed.DebugPayloadOutputDir != "relative-debug-dir" { + t.Fatalf("expected relative debug output dir to be honoured, got %v %q", parsed.DebugDumpPayloads, parsed.DebugPayloadOutputDir) + } + }) +} + +func TestConfigureDebugPayloadOutputDir(t *testing.T) { + stubLookPath(t, func(binary string) (string, error) { + return "/usr/local/bin/" + binary, nil + }) + + configure := func(t *testing.T, extra map[string]string) (*CloudCustodianPlugin, string) { + t.Helper() + var logs bytes.Buffer + plugin := &CloudCustodianPlugin{Logger: hclog.New(&hclog.LoggerOptions{ + Name: "test", + Level: hclog.Info, + Output: &logs, + })} + config := map[string]string{"policies_yaml": "policies:\n - name: s3-check\n resource: aws.s3"} + for k, v := range extra { + config[k] = v + } + if _, err := plugin.Configure(&proto.ConfigureRequest{Config: config}); err != nil { + t.Fatalf("unexpected configure error: %v", err) + } + return plugin, logs.String() + } + + t.Run("default dir is created under os.TempDir without warning", func(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + cwd := t.TempDir() + t.Chdir(cwd) + + plugin, logs := configure(t, map[string]string{"debug_dump_payloads": "true"}) + want := filepath.Join(os.TempDir(), "ccf-custodian-debug-payloads") + if plugin.parsedConfig.DebugPayloadOutputDir != want { + t.Fatalf("expected default dir %q, got %q", want, plugin.parsedConfig.DebugPayloadOutputDir) + } + if info, err := os.Stat(want); err != nil || !info.IsDir() { + t.Fatalf("expected default debug dir to be created at %q: %v", want, err) + } + if entries, err := os.ReadDir(cwd); err != nil || len(entries) != 0 { + t.Fatalf("expected nothing created in the working directory, got %v (err %v)", entries, err) + } + if strings.Contains(logs, "[WARN]") { + t.Fatalf("expected no warning for default dir, got %q", logs) + } + if !strings.Contains(logs, "Debug payload dumping enabled") || !strings.Contains(logs, want) { + t.Fatalf("expected resolved debug dir to be logged, got %q", logs) + } + }) + + t.Run("explicit absolute dir is kept without warning", func(t *testing.T) { + dir := filepath.Join(t.TempDir(), "abs-debug") + plugin, logs := configure(t, map[string]string{"debug_payload_output_dir": dir}) + if plugin.parsedConfig.DebugPayloadOutputDir != dir { + t.Fatalf("expected configured dir %q, got %q", dir, plugin.parsedConfig.DebugPayloadOutputDir) + } + if _, err := os.Stat(dir); err != nil { + t.Fatalf("expected configured dir to be created: %v", err) + } + if strings.Contains(logs, "[WARN]") { + t.Fatalf("expected no warning for absolute dir, got %q", logs) + } + }) + + t.Run("explicit relative dir is honoured with a warning", func(t *testing.T) { + cwd := t.TempDir() + t.Chdir(cwd) + + plugin, logs := configure(t, map[string]string{"debug_payload_output_dir": "relative-debug"}) + if plugin.parsedConfig.DebugPayloadOutputDir != "relative-debug" { + t.Fatalf("expected relative dir to be kept, got %q", plugin.parsedConfig.DebugPayloadOutputDir) + } + if _, err := os.Stat(filepath.Join(cwd, "relative-debug")); err != nil { + t.Fatalf("expected relative dir to be created under the working directory: %v", err) + } + if !strings.Contains(logs, "[WARN]") || !strings.Contains(logs, "debug_payload_output_dir is relative") { + t.Fatalf("expected relative dir warning, got %q", logs) + } + resolved, err := filepath.Abs("relative-debug") + if err != nil { + t.Fatalf("abs: %v", err) + } + if !strings.Contains(logs, resolved) { + t.Fatalf("expected resolved absolute dir %q to be logged, got %q", resolved, logs) + } + }) } func TestResolvePoliciesYAML(t *testing.T) {