From 75dec28a2e5b4553ba18a5c5e7ec480bf9b04d96 Mon Sep 17 00:00:00 2001 From: Pradipta Banerjee Date: Wed, 4 Feb 2026 03:09:23 +0000 Subject: [PATCH 1/2] implement dump-initdata command - Add new cobra command for inspecting generated initdata - Support --config flag for alternate config file path - Support --raw flag for base64-encoded annotation output - Default output shows decoded aa.toml, cdh.toml, policy.rego - Register command in root command tree Assisted-by: AI Signed-off-by: Pradipta Banerjee --- cmd/dump_initdata.go | 156 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 cmd/dump_initdata.go diff --git a/cmd/dump_initdata.go b/cmd/dump_initdata.go new file mode 100644 index 0000000..190eb40 --- /dev/null +++ b/cmd/dump_initdata.go @@ -0,0 +1,156 @@ +// Package cmd provides the command-line interface for kubectl-coco. +package cmd + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "fmt" + "io" + "strings" + + "github.com/confidential-devhub/cococtl/pkg/config" + "github.com/confidential-devhub/cococtl/pkg/initdata" + "github.com/pelletier/go-toml/v2" + "github.com/spf13/cobra" +) + +// dumpInitdataCmd displays generated initdata for inspection and debugging. +var dumpInitdataCmd = &cobra.Command{ + Use: "dump-initdata", + Short: "Display generated initdata for inspection", + Long: `Display the generated initdata configuration for inspection and debugging. + +By default, this command shows the decoded contents of: + - aa.toml (Attestation Agent configuration) + - cdh.toml (Confidential Data Hub configuration) + - policy.rego (Kata agent policy) + +Use --raw to output the gzip+base64 encoded annotation value that would +be added to Kubernetes manifests. + +Examples: + # Show decoded initdata from default config + kubectl coco dump-initdata + + # Show decoded initdata from specific config file + kubectl coco dump-initdata --config /path/to/coco-config.toml + + # Show raw base64-encoded annotation value + kubectl coco dump-initdata --raw`, + RunE: runDumpInitdata, +} + +var ( + dumpInitdataConfigPath string + dumpInitdataRaw bool +) + +func init() { + rootCmd.AddCommand(dumpInitdataCmd) + + dumpInitdataCmd.Flags().StringVar(&dumpInitdataConfigPath, "config", "", "Path to CoCo config file (default: ~/.kube/coco-config.toml)") + dumpInitdataCmd.Flags().BoolVar(&dumpInitdataRaw, "raw", false, "Output gzip+base64 encoded annotation value instead of decoded content") +} + +// runDumpInitdata generates and displays initdata for inspection. +func runDumpInitdata(_ *cobra.Command, _ []string) error { + // Determine config path + configPath := dumpInitdataConfigPath + if configPath == "" { + var err error + configPath, err = config.GetConfigPath() + if err != nil { + return fmt.Errorf("failed to get default config path: %w", err) + } + } + + // Load configuration + cfg, err := config.Load(configPath) + if err != nil { + return fmt.Errorf("failed to load config from %s: %w", configPath, err) + } + + if err := cfg.Validate(); err != nil { + return fmt.Errorf("invalid configuration: %w", err) + } + + // Generate initdata (nil for imagePullSecrets - not needed for inspection) + encoded, err := initdata.Generate(cfg, nil) + if err != nil { + return fmt.Errorf("failed to generate initdata: %w", err) + } + + // Output based on --raw flag + if dumpInitdataRaw { + // Output raw base64-encoded value + fmt.Println("# This is the gzip+base64 encoded initdata annotation value") + fmt.Println("# Use this value for the io.katacontainers.config.hypervisor.cc_init_data annotation") + fmt.Println(encoded) + return nil + } + + // Decode and display human-readable content + decoded, err := decodeInitdata(encoded) + if err != nil { + return fmt.Errorf("failed to decode initdata: %w", err) + } + + // Print each section with headers + fmt.Println("=== aa.toml ===") + if aaToml, ok := decoded["aa.toml"]; ok { + fmt.Println(strings.TrimSpace(aaToml)) + } else { + fmt.Println("(not found)") + } + fmt.Println() + + fmt.Println("=== cdh.toml ===") + if cdhToml, ok := decoded["cdh.toml"]; ok { + fmt.Println(strings.TrimSpace(cdhToml)) + } else { + fmt.Println("(not found)") + } + fmt.Println() + + fmt.Println("=== policy.rego ===") + if policy, ok := decoded["policy.rego"]; ok { + fmt.Println(strings.TrimSpace(policy)) + } else { + fmt.Println("(not found)") + } + + return nil +} + +// decodeInitdata decodes a base64+gzip encoded initdata string and extracts the data map. +func decodeInitdata(encoded string) (map[string]string, error) { + // Decode base64 + gzipData, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("failed to decode base64: %w", err) + } + + // Decompress gzip + gzipReader, err := gzip.NewReader(bytes.NewReader(gzipData)) + if err != nil { + return nil, fmt.Errorf("failed to create gzip reader: %w", err) + } + defer func() { + _ = gzipReader.Close() + }() + + tomlData, err := io.ReadAll(gzipReader) + if err != nil { + return nil, fmt.Errorf("failed to decompress gzip data: %w", err) + } + + // Parse TOML to extract data map + var initdataStruct initdata.InitData + + if err := toml.Unmarshal(tomlData, &initdataStruct); err != nil { + return nil, fmt.Errorf("failed to parse initdata TOML: %w", err) + } + + return initdataStruct.Data, nil +} From 0e6edd121b40b4ab6b71a8b58284609624eaf24b Mon Sep 17 00:00:00 2001 From: Pradipta Banerjee Date: Wed, 4 Feb 2026 03:10:20 +0000 Subject: [PATCH 2/2] add unit tests for dump-initdata command - TestDumpInitdataWithValidConfig: verifies decoded output with valid config - TestDumpInitdataWithRawFlag: verifies base64-encoded output with --raw - TestDumpInitdataMissingConfig: verifies error handling for missing config - TestDumpInitdataInvalidConfig: verifies error handling for malformed TOML - TestDecodeInitdata: verifies the decode helper function Assisted-by: AI Signed-off-by: Pradipta Banerjee --- cmd/dump_initdata_test.go | 271 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 cmd/dump_initdata_test.go diff --git a/cmd/dump_initdata_test.go b/cmd/dump_initdata_test.go new file mode 100644 index 0000000..2e04906 --- /dev/null +++ b/cmd/dump_initdata_test.go @@ -0,0 +1,271 @@ +package cmd + +import ( + "bytes" + "encoding/base64" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/confidential-devhub/cococtl/pkg/config" + "github.com/confidential-devhub/cococtl/pkg/initdata" +) + +// TestDumpInitdataWithValidConfig tests dump-initdata with a valid config file. +func TestDumpInitdataWithValidConfig(t *testing.T) { + // Create temporary config file with valid TOML + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "test-config.toml") + + validConfig := `trustee_server = "http://kbs-service.trustee-operator-system.svc.cluster.local:8080" +runtime_class = "kata-cc" +` + + if err := os.WriteFile(configPath, []byte(validConfig), 0600); err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Set the config path flag + originalConfigPath := dumpInitdataConfigPath + originalRaw := dumpInitdataRaw + defer func() { + dumpInitdataConfigPath = originalConfigPath + dumpInitdataRaw = originalRaw + }() + + dumpInitdataConfigPath = configPath + dumpInitdataRaw = false + + // Capture stdout + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + + defer func() { + _ = r.Close() + }() + + os.Stdout = w + + runErr := runDumpInitdata(nil, nil) + + // Restore stdout and read captured output + _ = w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + output := buf.String() + + if runErr != nil { + t.Fatalf("runDumpInitdata failed: %v", runErr) + } + + // Verify output contains expected section headers + if !strings.Contains(output, "=== aa.toml ===") { + t.Error("Output should contain '=== aa.toml ===' section header") + } + if !strings.Contains(output, "=== cdh.toml ===") { + t.Error("Output should contain '=== cdh.toml ===' section header") + } + if !strings.Contains(output, "=== policy.rego ===") { + t.Error("Output should contain '=== policy.rego ===' section header") + } + + // Verify trustee server appears in the output + if !strings.Contains(output, "kbs-service.trustee-operator-system.svc.cluster.local") { + t.Error("Output should contain the configured trustee server URL") + } +} + +// TestDumpInitdataWithRawFlag tests dump-initdata with --raw flag. +func TestDumpInitdataWithRawFlag(t *testing.T) { + // Create temporary config file + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "test-config.toml") + + validConfig := `trustee_server = "http://kbs.test.svc:8080" +runtime_class = "kata-cc" +` + + if err := os.WriteFile(configPath, []byte(validConfig), 0600); err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Set the config path flag and enable raw mode + originalConfigPath := dumpInitdataConfigPath + originalRaw := dumpInitdataRaw + defer func() { + dumpInitdataConfigPath = originalConfigPath + dumpInitdataRaw = originalRaw + }() + + dumpInitdataConfigPath = configPath + dumpInitdataRaw = true + + // Capture stdout + oldStdout := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Failed to create pipe: %v", err) + } + + defer func() { + _ = r.Close() + }() + + os.Stdout = w + + runErr := runDumpInitdata(nil, nil) + + // Restore stdout and read captured output + _ = w.Close() + os.Stdout = oldStdout + + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + output := buf.String() + + if runErr != nil { + t.Fatalf("runDumpInitdata with --raw failed: %v", runErr) + } + + // Find the base64 line (skip comment lines) + lines := strings.Split(strings.TrimSpace(output), "\n") + var base64Line string + for _, line := range lines { + if !strings.HasPrefix(line, "#") && len(line) > 0 { + base64Line = line + break + } + } + + if base64Line == "" { + t.Fatal("Output should contain a base64-encoded string") + } + + // Verify it's valid base64 + _, err = base64.StdEncoding.DecodeString(base64Line) + if err != nil { + t.Errorf("Output is not valid base64: %v", err) + } + + // Verify comment header is present + if !strings.Contains(output, "gzip+base64 encoded initdata") { + t.Error("Output should contain explanatory comment about gzip+base64 encoding") + } +} + +// TestDumpInitdataMissingConfig tests dump-initdata with non-existent config file. +func TestDumpInitdataMissingConfig(t *testing.T) { + // Set a non-existent config path + originalConfigPath := dumpInitdataConfigPath + originalRaw := dumpInitdataRaw + defer func() { + dumpInitdataConfigPath = originalConfigPath + dumpInitdataRaw = originalRaw + }() + + dumpInitdataConfigPath = "/nonexistent/path/to/config.toml" + dumpInitdataRaw = false + + err := runDumpInitdata(nil, nil) + + if err == nil { + t.Fatal("Expected error for missing config file, got nil") + } + + // Verify error message mentions config loading failure + if !strings.Contains(err.Error(), "failed to load config") { + t.Errorf("Error message should mention 'failed to load config', got: %v", err) + } +} + +// TestDumpInitdataInvalidConfig tests dump-initdata with invalid TOML config. +func TestDumpInitdataInvalidConfig(t *testing.T) { + // Create temporary file with invalid TOML + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "invalid-config.toml") + + invalidConfig := `trustee_server = "http://test.local +runtime_class = [broken +` + + if err := os.WriteFile(configPath, []byte(invalidConfig), 0600); err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Set the config path + originalConfigPath := dumpInitdataConfigPath + originalRaw := dumpInitdataRaw + defer func() { + dumpInitdataConfigPath = originalConfigPath + dumpInitdataRaw = originalRaw + }() + + dumpInitdataConfigPath = configPath + dumpInitdataRaw = false + + err := runDumpInitdata(nil, nil) + + if err == nil { + t.Fatal("Expected error for invalid TOML config, got nil") + } + + // Verify error message indicates config or parsing failure + if !strings.Contains(err.Error(), "failed to load config") && !strings.Contains(err.Error(), "parse") { + t.Errorf("Error message should mention config loading or parsing failure, got: %v", err) + } +} + +// TestDecodeInitdata tests the decodeInitdata helper function. +func TestDecodeInitdata(t *testing.T) { + // Create a simple test config + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "test-config.toml") + + validConfig := `trustee_server = "http://test-kbs.default.svc:8080" +runtime_class = "kata-cc" +` + + if err := os.WriteFile(configPath, []byte(validConfig), 0600); err != nil { + t.Fatalf("Failed to create test config: %v", err) + } + + // Load config using the real config package + cfg, err := config.Load(configPath) + if err != nil { + t.Fatalf("Failed to load test config: %v", err) + } + + // Generate initdata using the real initdata package + encoded, err := initdata.Generate(cfg, nil) + if err != nil { + t.Fatalf("Failed to generate initdata: %v", err) + } + + // Now test decoding + data, err := decodeInitdata(encoded) + if err != nil { + t.Fatalf("decodeInitdata failed: %v", err) + } + + // Verify we got the expected keys + if _, ok := data["aa.toml"]; !ok { + t.Error("Decoded data should contain 'aa.toml' key") + } + if _, ok := data["cdh.toml"]; !ok { + t.Error("Decoded data should contain 'cdh.toml' key") + } + if _, ok := data["policy.rego"]; !ok { + t.Error("Decoded data should contain 'policy.rego' key") + } + + // Verify aa.toml contains the trustee server URL + if !strings.Contains(data["aa.toml"], "test-kbs.default.svc:8080") { + t.Error("aa.toml should contain the configured trustee server URL") + } +}