diff --git a/README.md b/README.md index 384678f..6bd78fe 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Kado is a modular configuration management tool designed to streamline and autom - [Terraform Bead](#terraform-bead) - [OPA Bead](#opa-bead) - [Terragrunt Bead](#terragrunt-bead) +- [Keybase Integration](#keybase-integration) - [Usage](#usage) - [Commands](#commands) - [Getting Started](#getting-started) @@ -183,6 +184,52 @@ bead "terragrunt" { } ``` +## Keybase Integration + +Kado integrates with Keybase to provide secure note storage and referencing within your infrastructure configurations. This feature allows you to store sensitive information securely and reference it in your bead definitions. + +### Keybase Commands + +- `kado keybase link`: Links your Keybase account with Kado. +- `kado keybase note create `: Creates a new encrypted note in Keybase. +- `kado keybase note list`: Lists all stored notes. +- `kado keybase note view `: Displays the content of a specific note. +- `kado keybase note share `: Shares a note with another Keybase user. +- `kado keybase note create-with-tags `: Creates a new note with tags. +- `kado keybase note search-by-tag `: Searches for notes with a specific tag. + +### Note Referencing in Bead Definitions + +You can reference Keybase notes in your bead definitions using the following syntax: + +```hcl +bead "terraform" { + source = "git@github.com:janpreet/proxmox_terraform.git" + enabled = true + relay = opa + relay_field = "source=git@github.com:janpreet/proxmox_terraform.git,path=terraform/policies/proxmox.rego,input=terraform/plan.json,package=data.terraform.allow" + api_key = "{{keybase:note:proxmox_api_key}}" + secret_token = "{{keybase:note:secret_token}}" +} +``` + +In above example, `{{keybase:note:proxmox_api_key}}` and `{{keybase:note:secret_token}}` will be replaced with the content of the corresponding Keybase notes during Kado execution. + +### Benefits of Keybase Integration + +- **Enhanced Security**: Store sensitive information like API keys and tokens securely in Keybase. +- **Version Control**: Keybase notes are version-controlled, allowing you to track changes to sensitive information. +- **Easy Sharing**: Securely share notes with team members using Keybase's encryption. +- **Tagging System**: Organize your notes with tags for easy searching and categorization. + +### Getting Started with Keybase Integration + +1. Ensure you have Keybase installed and configured on your system. +2. Run `kado keybase link` to link your Keybase account with Kado. +3. Create notes for sensitive information: `kado keybase note create ` +4. Use note references in your bead definitions as shown in the example above. + + ## Usage ### Commands diff --git a/main.go b/main.go index 4fb77e6..6c4bb13 100644 --- a/main.go +++ b/main.go @@ -135,51 +135,42 @@ func convertTemplatePaths(paths []interface{}) []string { func main() { var yamlFilePath string - if len(os.Args) > 1 && strings.HasSuffix(os.Args[1], ".yaml") { - yamlFilePath = os.Args[1] - } else { - yamlFilePath = "cluster.yaml" - } - if len(os.Args) > 1 && os.Args[1] == "version" { - fmt.Println("Version:", config.Version) - return - } + if len(os.Args) > 1 { + switch os.Args[1] { + case "version": + fmt.Println("Version:", config.Version) + return - if len(os.Args) > 1 && os.Args[1] == "config" { - kdFiles, err := render.GetKDFiles(".") - if err != nil { - log.Fatalf("Failed to get KD files: %v", err) - } + case "config": + handleConfigCommand() + return - var beads []bead.Bead - for _, kdFile := range kdFiles { - bs, err := config.LoadBeadsConfig(kdFile) - if err != nil { - log.Fatalf("Failed to load beads config from %s: %v", kdFile, err) - } - beads = append(beads, bs...) - } + case "fmt": + handleFormatCommand() + return - display.DisplayBeadConfig(beads) - return - } + case "ai": + engine.RunAI() + return - if len(os.Args) > 1 && os.Args[1] == "fmt" { - dir := "." - if len(os.Args) > 2 { - dir = os.Args[2] - } - err := engine.FormatKDFilesInDir(dir) - if err != nil { - log.Fatalf("Error formatting .kd files: %v", err) - } - return - } + case "keybase": + if len(os.Args) < 3 { + fmt.Println("Usage: kado keybase [debug] ") + return + } + helper.HandleKeybaseCommand(os.Args[2:]) + return - if len(os.Args) > 1 && os.Args[1] == "ai" { - engine.RunAI() - return + default: + if strings.HasSuffix(os.Args[1], ".yaml") { + yamlFilePath = os.Args[1] + } else { + yamlFilePath = "cluster.yaml" + } + } + } else { + yamlFilePath = "cluster.yaml" } fmt.Println("Starting processing-") @@ -285,3 +276,32 @@ func main() { } } + +func handleConfigCommand() { + kdFiles, err := render.GetKDFiles(".") + if err != nil { + log.Fatalf("Failed to get KD files: %v", err) + } + + var beads []bead.Bead + for _, kdFile := range kdFiles { + bs, err := config.LoadBeadsConfig(kdFile) + if err != nil { + log.Fatalf("Failed to load beads config from %s: %v", kdFile, err) + } + beads = append(beads, bs...) + } + + display.DisplayBeadConfig(beads) +} + +func handleFormatCommand() { + dir := "." + if len(os.Args) > 2 { + dir = os.Args[2] + } + err := engine.FormatKDFilesInDir(dir) + if err != nil { + log.Fatalf("Error formatting .kd files: %v", err) + } +} \ No newline at end of file diff --git a/packages/helper/helper.go b/packages/helper/helper.go index 196d43e..1a64234 100644 --- a/packages/helper/helper.go +++ b/packages/helper/helper.go @@ -4,7 +4,11 @@ import ( "fmt" "os" + "bufio" + "log" + "strings" "github.com/janpreet/kado/packages/config" + "github.com/janpreet/kado/packages/keybase" ) func SetupLandingZone() error { @@ -31,3 +35,146 @@ func FileExists(path string) bool { _, err := os.Stat(path) return err == nil } + +func HandleKeybaseCommand(args []string) { + if len(args) == 0 { + fmt.Println("Usage: kado keybase [debug] ") + fmt.Println("Available commands: link, note") + return + } + + + debugIndex := -1 + for i, arg := range args { + if arg == "debug" { + debugIndex = i + break + } + } + + if debugIndex != -1 { + keybase.Debug = true + + args = append(args[:debugIndex], args[debugIndex+1:]...) + } + + switch args[0] { + case "link": + err := keybase.LinkKeybase() + if err != nil { + log.Fatalf("Failed to link Keybase account: %v", err) + } + fmt.Println("Keybase account linked successfully") + case "note": + if len(args) < 2 { + fmt.Println("Usage: kado keybase note ") + return + } + HandleNoteCommand(args[1:]) + default: + fmt.Printf("Unknown Keybase command: %s\n", args[0]) + fmt.Println("Available commands: link, note") + } +} + +func HandleNoteCommand(args []string) { + switch args[0] { + case "create": + if len(args) < 2 { + fmt.Println("Usage: kado keybase note create ") + return + } + noteName := args[1] + fmt.Println("Enter note content (press Ctrl+D when finished):") + scanner := bufio.NewScanner(os.Stdin) + var content strings.Builder + for scanner.Scan() { + content.WriteString(scanner.Text() + "\n") + } + err := keybase.CreateNote(noteName, content.String()) + if err != nil { + log.Fatalf("Failed to create note: %v", err) + } + fmt.Println("Note created successfully") + + case "list": + notes, err := keybase.ListNotes() + if err != nil { + log.Fatalf("Failed to list notes: %v", err) + } + if len(notes) == 0 { + fmt.Println("No notes found") + } else { + fmt.Println("Stored notes:") + for _, note := range notes { + fmt.Println("-", note) + } + } + + case "view": + if len(args) < 2 { + fmt.Println("Usage: kado keybase note view ") + return + } + noteName := args[1] + content, err := keybase.ViewNote(noteName) + if err != nil { + log.Fatalf("Failed to view note: %v", err) + } + fmt.Printf("Content of note '%s':\n%s", noteName, content) + + case "share": + if len(args) < 3 { + fmt.Println("Usage: kado keybase note share ") + return + } + noteName := args[1] + recipient := args[2] + err := keybase.ShareNote(noteName, recipient) + if err != nil { + log.Fatalf("Failed to share note: %v", err) + } + fmt.Printf("Note '%s' shared with %s successfully\n", noteName, recipient) + + case "create-with-tags": + if len(args) < 3 { + fmt.Println("Usage: kado keybase note create-with-tags ") + return + } + noteName := args[1] + tags := strings.Split(args[2], ",") + fmt.Println("Enter note content (press Ctrl+D when finished):") + content := readMultiLineInput() + err := keybase.CreateNoteWithTags(noteName, keybase.Note{Content: content, Tags: tags}) + if err != nil { + log.Fatalf("Failed to create note with tags: %v", err) + } + fmt.Println("Note created successfully with tags") + case "search-by-tag": + if len(args) < 2 { + fmt.Println("Usage: kado keybase note search-by-tag ") + return + } + tag := args[1] + notes, err := keybase.SearchNotesByTag(tag) + if err != nil { + log.Fatalf("Failed to search notes by tag: %v", err) + } + fmt.Printf("Notes with tag '%s':\n", tag) + for _, note := range notes { + fmt.Printf(" - %s\n", note) + } + + default: + fmt.Printf("Unknown note command: %s\n", args[0]) + } +} + +func readMultiLineInput() string { + var content strings.Builder + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + content.WriteString(scanner.Text() + "\n") + } + return content.String() +} \ No newline at end of file diff --git a/packages/helper/note_resolver.go b/packages/helper/note_resolver.go new file mode 100644 index 0000000..5c00f67 --- /dev/null +++ b/packages/helper/note_resolver.go @@ -0,0 +1,22 @@ +package helper + +import ( + "regexp" + "strings" + "fmt" + "github.com/janpreet/kado/packages/keybase" +) + +var noteReferenceRegex = regexp.MustCompile(`{{keybase:note:([^}]+)}}`) + +func resolveNoteReferences(input string) (string, error) { + return noteReferenceRegex.ReplaceAllStringFunc(input, func(match string) string { + noteName := strings.TrimPrefix(strings.TrimSuffix(match, "}}"), "{{keybase:note:") + content, err := keybase.ViewNote(noteName) + if err != nil { + + return fmt.Sprintf("ERROR: Could not resolve note %s", noteName) + } + return strings.TrimSpace(content) + }), nil +} \ No newline at end of file diff --git a/packages/helper/processer.go b/packages/helper/processer.go index 78ef899..5ab5752 100644 --- a/packages/helper/processer.go +++ b/packages/helper/processer.go @@ -1,5 +1,3 @@ -// File: packages/helper/processor.go - package helper import ( @@ -100,7 +98,7 @@ func ProcessTerragruntBead(b bead.Bead, yamlData map[string]interface{}, applyPl return nil } -// Helper functions + func convertTemplatePaths(paths []interface{}) []string { var result []string diff --git a/packages/keybase/keybase.go b/packages/keybase/keybase.go new file mode 100644 index 0000000..db161d8 --- /dev/null +++ b/packages/keybase/keybase.go @@ -0,0 +1,239 @@ +package keybase + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +var Debug bool + +type Note struct { + Content string + Tags []string +} + +func CreateNoteWithTags(noteName string, note Note) error { + content := fmt.Sprintf("Tags: %s\n\n%s", strings.Join(note.Tags, ", "), note.Content) + return CreateNote(noteName, content) +} + +func GetNoteTags(noteName string) ([]string, error) { + content, err := ViewNote(noteName) + if err != nil { + return nil, err + } + + lines := strings.Split(content, "\n") + if len(lines) > 0 && strings.HasPrefix(lines[0], "Tags: ") { + tags := strings.TrimPrefix(lines[0], "Tags: ") + return strings.Split(tags, ", "), nil + } + + return []string{}, nil +} + +func SearchNotesByTag(tag string) ([]string, error) { + notes, err := ListNotes() + if err != nil { + return nil, err + } + + var matchingNotes []string + for _, note := range notes { + tags, err := GetNoteTags(note) + if err != nil { + return nil, err + } + for _, t := range tags { + if t == tag { + matchingNotes = append(matchingNotes, note) + break + } + } + } + + return matchingNotes, nil +} + +func LinkKeybase() error { + cmd := exec.Command("keybase", "login") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to link Keybase account: %v", err) + } + return nil +} + +func InitNoteRepository() error { + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + notesDir := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes") + + cmd := exec.Command("git", "init") + cmd.Dir = notesDir + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to initialize git repository: %v", err) + } + return nil +} + +func CheckKeybaseSetup() error { + cmd := exec.Command("keybase", "status") + output, err := cmd.CombinedOutput() + if Debug { + fmt.Printf("Keybase status output:\n%s\n", string(output)) + } + if err != nil { + return fmt.Errorf("Keybase is not properly set up: %v", err) + } + if !strings.Contains(string(output), "Logged in: yes") { + return fmt.Errorf("You are not logged in to Keybase. Please run 'kado keybase link' first") + } + return nil +} + +func CreateNote(noteName, content string) error { + if err := CheckKeybaseSetup(); err != nil { + return err + } + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + notePath := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes", noteName) + dir := filepath.Dir(notePath) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + if err := os.WriteFile(notePath, []byte(content), 0600); err != nil { + return fmt.Errorf("failed to write note: %v", err) + } + + + if err := gitAddCommit(notePath, "Create note "+noteName); err != nil { + return fmt.Errorf("failed to version note: %v", err) + } + + if Debug { + fmt.Printf("Note created and versioned at: %s\n", notePath) + } + return nil +} + +func ListNotes() ([]string, error) { + if err := CheckKeybaseSetup(); err != nil { + return nil, err + } + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %v", err) + } + notesDir := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes") + if _, err := os.Stat(notesDir); os.IsNotExist(err) { + return []string{}, nil + } + files, err := os.ReadDir(notesDir) + if err != nil { + return nil, fmt.Errorf("failed to read notes directory: %v", err) + } + var notes []string + for _, file := range files { + if !file.IsDir() { + notes = append(notes, file.Name()) + } + } + return notes, nil +} + +func ViewNote(noteName string) (string, error) { + if err := CheckKeybaseSetup(); err != nil { + return "", err + } + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %v", err) + } + notePath := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes", noteName) + content, err := os.ReadFile(notePath) + if err != nil { + return "", fmt.Errorf("failed to read note: %v", err) + } + return string(content), nil +} + +func ShareNote(noteName, recipient string) error { + if err := CheckKeybaseSetup(); err != nil { + return err + } + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + sourcePath := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes", noteName) + destPath := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), recipient, "kado_notes", noteName) + destDir := filepath.Dir(destPath) + if err := os.MkdirAll(destDir, 0700); err != nil { + return fmt.Errorf("failed to create destination directory: %v", err) + } + input, err := os.ReadFile(sourcePath) + if err != nil { + return fmt.Errorf("failed to read source note: %v", err) + } + err = os.WriteFile(destPath, input, 0600) + if err != nil { + return fmt.Errorf("failed to write shared note: %v", err) + } + if Debug { + fmt.Printf("Note shared at: %s\n", destPath) + } + return nil +} + +func UpdateNote(noteName, content string) error { + if err := CheckKeybaseSetup(); err != nil { + return err + } + homeDir, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %v", err) + } + notePath := filepath.Join(homeDir, "Keybase", "private", os.Getenv("USER"), "kado_notes", noteName) + dir := filepath.Dir(notePath) + if err := os.MkdirAll(dir, 0700); err != nil { + return fmt.Errorf("failed to create directory: %v", err) + } + if err := os.WriteFile(notePath, []byte(content), 0600); err != nil { + return fmt.Errorf("failed to write note: %v", err) + } + + if err := gitAddCommit(notePath, "Update note "+noteName); err != nil { + return fmt.Errorf("failed to version note update: %v", err) + } + return nil +} + +func gitAddCommit(filePath, message string) error { + dir := filepath.Dir(filePath) + + addCmd := exec.Command("git", "add", filepath.Base(filePath)) + addCmd.Dir = dir + if err := addCmd.Run(); err != nil { + return fmt.Errorf("git add failed: %v", err) + } + + commitCmd := exec.Command("git", "commit", "-m", message) + commitCmd.Dir = dir + if err := commitCmd.Run(); err != nil { + return fmt.Errorf("git commit failed: %v", err) + } + + return nil +} \ No newline at end of file