diff --git a/CICD_BEST_PRACTICES.md b/CICD_BEST_PRACTICES.md new file mode 100644 index 0000000..a164f1d --- /dev/null +++ b/CICD_BEST_PRACTICES.md @@ -0,0 +1,169 @@ +# Kado CI/CD Best Practices + +This guide provides best practices for integrating Kado and Keybase into your CI/CD pipeline, especially when running inside containers. + +## Table of Contents + +1. [Mounting Directories](#mounting-directories) +2. [Keybase Integration](#keybase-integration) +3. [Security Considerations](#security-considerations) +4. [CI/CD Pipeline Configuration](#cicd-pipeline-configuration) +5. [Best Practices](#best-practices) +6. [Troubleshooting](#troubleshooting) + +## Mounting Directories + +To allow Kado to access your configuration files, templates, and other resources, you need to mount the relevant directories from your host system to the container. + +### Docker Run Example + +```bash +docker run -v $(pwd):/workspace ghcr.io/janpreet/kado:latest kado [command] +``` + +This command mounts the current directory to /kado-workspace in the container. + +### Docker Compose Example + +```yaml +yamlCopyversion: '3' +services: + kado: + image: ghcr.io/janpreet/kado:latest + volumes: + - ./:/workspace + command: kado [command] +``` + +### CI/CD Configuration +In your CI/CD pipeline, ensure that your job checks out the repository and mounts it to the container: + +```yaml +name: Deploy Infrastructure + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + container: + image: your-registry/kado:latest + volumes: + - ${{ github.workspace }}:/kado-workspace + steps: + - uses: actions/checkout@v2 + - name: Set up Keybase + run: | + echo "${{ secrets.KEYBASE_PAPERKEY }}" | keybase oneshot + kado keybase link + env: + KEYBASE_PAPERKEY: ${{ secrets.KEYBASE_PAPERKEY }} + - name: Deploy with Kado + run: | + cd /kado-workspace + kado set cluster.yaml +``` + +## Keybase Integration + +### Setting Up Keybase in CI/CD + +1. Generate a paper key for your Keybase account. +2. Store the paper key securely in your CI/CD platform's secret management system. +3. In your CI/CD job, use the paper key to authenticate Keybase: + +```yaml +job_name: + image: your-registry/kado:latest + script: + - echo $KEYBASE_PAPERKEY | keybase oneshot + - kado keybase link + # Your Kado commands here +``` + +### Using Keybase Notes in Templates + +Reference Keybase notes in your templates using the `{{keybase:note:note_name}}` syntax: + +```hcl +pm_user = {{keybase:note:proxmox_api_key}} +pm_password = {{keybase:note:secret_token}} +``` + +## Security Considerations + +1. **Paper Key Security**: Never expose your Keybase paper key in logs or non-secure storage. +2. **Ephemeral Sessions**: Use `keybase oneshot` to create temporary Keybase sessions. +3. **Least Privilege**: Use a Keybase account with minimal necessary permissions for CI/CD. +4. **Secure Note Storage**: Store sensitive information in Keybase notes, not in your codebase. + +## CI/CD Pipeline Configuration + +### Example GitHub Actions Workflow + +```yaml +name: Deploy Infrastructure + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + container: your-registry/kado:latest + steps: + - uses: actions/checkout@v2 + - name: Set up Keybase + run: | + echo "${{ secrets.KEYBASE_PAPERKEY }}" | keybase oneshot + kado keybase link + env: + KEYBASE_PAPERKEY: ${{ secrets.KEYBASE_PAPERKEY }} + - name: Deploy with Kado + run: kado set cluster.yaml +``` + +## Best Practices + +1. **Configuration Management**: + - Use `.kd` files for defining beads. + - Keep sensitive data in Keybase notes, referenced in templates. + +3. **Testing**: + - Implement a test stage in your CI/CD pipeline using `kado -debug`. + - Validate templates and configurations before deployment. + +4. **Logging and Monitoring**: + - Enable debug logging in CI/CD for troubleshooting. + - Monitor Keybase activity for any suspicious actions. + +5. **Secret Rotation**: + - Regularly rotate your Keybase paper key. + - Update Keybase notes with new credentials periodically. + +6. **Error Handling**: + - Implement proper error handling in your CI/CD scripts. + - Set up notifications for pipeline failures. + +## Troubleshooting + +1. **Keybase Authentication Issues**: + - Ensure the paper key is correctly stored in CI/CD secrets. + - Check Keybase logs for authentication errors. + +2. **Template Processing Errors**: + - Verify that all referenced Keybase notes exist. + - Check for syntax errors in your templates. + +3. **Container Issues**: + - Ensure all required tools are installed and accessible in the container. + - Verify the container has necessary permissions to execute Kado and Keybase. + +4. **Pipeline Failures**: + - Review CI/CD logs for specific error messages. + - Test Kado commands locally to replicate issues. + +By following these best practices and guidelines, you can effectively integrate Kado and Keybase into your CI/CD pipeline, ensuring secure and efficient infrastructure management. diff --git a/Dockerfile b/Dockerfile index 67edfd8..381fca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,28 @@ -FROM alpine:3.18 +FROM debian:bullseye-slim ARG TERRAFORM_VERSION=1.9.3 -ARG ANSIBLE_VERSION=10.2.0 +ARG ANSIBLE_VERSION=2.6.13 -RUN apk add --no-cache \ +RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ curl \ - python3 \ - py3-pip \ - aws-cli \ - && pip3 install --no-cache-dir --upgrade pip + gnupg \ + unzip \ + python3-pip \ + awscli \ + libayatana-appindicator3-1 \ + fuse \ + psmisc \ + lsof \ + procps \ + libasound2 \ + libnss3 \ + libxss1 \ + libxtst6 \ + libgtk-3-0 \ + && pip3 install --no-cache-dir --upgrade pip \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ && unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ @@ -21,6 +34,12 @@ RUN curl -L -o /usr/local/bin/opa https://openpolicyagent.org/downloads/latest/o RUN pip3 install --no-cache-dir ansible==${ANSIBLE_VERSION} +RUN curl -s https://keybase.io/docs/server_security/code_signing_key.asc | gpg --import \ + && curl -O https://prerelease.keybase.io/keybase_amd64.deb \ + && dpkg -i keybase_amd64.deb \ + && apt-get install -f \ + && rm keybase_amd64.deb + COPY kado /usr/local/bin/kado ENV PATH="/opt/venv/bin:$PATH" @@ -28,4 +47,4 @@ ENV PATH="/opt/venv/bin:$PATH" WORKDIR /workspace #ENTRYPOINT ["tail", "-f", "/dev/null"] -ENTRYPOINT ["/usr/local/bin/kado"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/kado"] diff --git a/README.md b/README.md index 384678f..a7ab832 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) @@ -90,7 +91,7 @@ Example `vm.tfvars.tmpl`: aws_region = "{{.Get "aws.s3.region"}}" pm_api_url = "{{.Get "proxmox.api_url"}}" pm_user = "{{.Env "PM_USER"}}" -pm_password = "{{.Env "PM_PASSWORD"}}" +pm_password = "{{ keybase:note:secret_token }}" vm_roles = { master = {{.Get "proxmox.vm.roles.master"}} worker = {{.Get "proxmox.vm.roles.worker"}} @@ -183,6 +184,40 @@ bead "terragrunt" { } ``` +## Keybase Integration + +Kado integrates with Keybase to provide secure storage and referencing of sensitive information within your infrastructure configurations. + +### 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. + +### Security Benefits + +- **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. + +### Using Keybase Notes in Templates + +You can reference Keybase notes in your templates using the `{{keybase:note:note_name}}` syntax. This allows you to keep sensitive information like API keys and tokens secure while still being able to use them in your configurations. + +### 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. + +In above template example, `{{keybase:note:secret_token}}` will be replaced with the content of the corresponding Keybase notes during Kado execution. + ## Usage ### Commands @@ -190,9 +225,10 @@ bead "terragrunt" { - `kado [file.yaml]`: Runs the default configuration and processing of beads. You may pass a specific YAML file to Kado. If no file is specified, Kado scans all YAML files in the current directory. - `kado set`: Applies the configuration and processes beads with the `set` flag. - `kado fmt [dir]`: Formats `.kd` files in the specified directory. -- `kado ai`: Runs AI-based recommendations if enabled in the `~/.kdconfig` configuration. +- `kado ai`: Runs AI-based recommendations if enabled. - `kado config`: Displays the current configuration and order of execution. -- `kado -debug`: Runs Kado with debug output enabled, providing detailed information about the execution process. +- `kado -debug`: Runs Kado with debug output enabled. +- `kado keybase `: Manages Keybase integration (link, create/list/view/share notes). ### Getting Started diff --git a/VERSION b/VERSION index 6c6aa7c..341cf11 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/cluster.kd b/cluster.kd index 1a5fa6e..5b5ff60 100644 --- a/cluster.kd +++ b/cluster.kd @@ -14,4 +14,4 @@ bead "terraform" { 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" -} \ No newline at end of file +} 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..1f27c71 --- /dev/null +++ b/packages/helper/note_resolver.go @@ -0,0 +1,47 @@ +package helper + +import ( + "regexp" + "strings" + "fmt" + "log" + "github.com/janpreet/kado/packages/keybase" + "github.com/janpreet/kado/packages/config" +) + +var noteReferenceRegex = regexp.MustCompile(`{{keybase:note:([^}]+)}}`) + +func resolveKeybaseNote(noteName string) (string, error) { + if config.Debug { + log.Printf("Attempting to resolve Keybase note: %s", noteName) + } + + content, err := keybase.ViewNote(noteName) + if err != nil { + if kerr, ok := err.(*keybase.KeybaseError); ok { + switch kerr.Type { + case keybase.ErrNoteNotFound: + log.Printf("WARNING: Keybase note '%s' not found", noteName) + return "", fmt.Errorf("Keybase note '%s' not found", noteName) + case keybase.ErrKeybaseNotInitialized: + log.Printf("ERROR: Keybase is not initialized. Please run 'kado keybase link' first") + return "", fmt.Errorf("Keybase is not initialized") + case keybase.ErrPermissionDenied: + log.Printf("ERROR: Permission denied when accessing Keybase note '%s'", noteName) + return "", fmt.Errorf("Permission denied for Keybase note '%s'", noteName) + default: + log.Printf("ERROR: Failed to retrieve Keybase note '%s': %v", noteName, err) + return "", fmt.Errorf("Failed to retrieve Keybase note '%s': %v", noteName, err) + } + } else { + log.Printf("ERROR: Unexpected error when resolving Keybase note '%s': %v", noteName, err) + return "", fmt.Errorf("Unexpected error when resolving Keybase note '%s': %v", noteName, err) + } + } + + if config.Debug { + log.Printf("Successfully resolved Keybase 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..8561df3 --- /dev/null +++ b/packages/keybase/keybase.go @@ -0,0 +1,283 @@ +package keybase + +import ( + "fmt" + "os" + "log" + "os/exec" + "path/filepath" + "strings" +) + +var Debug bool +type ErrorType int + +const ( + ErrNoteNotFound ErrorType = iota + ErrKeybaseNotInitialized + ErrPermissionDenied + ErrUnknown +) + +type KeybaseError struct { + Type ErrorType + Message string +} + +func (e *KeybaseError) Error() string { + return e.Message +} + + +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) + notesDir := filepath.Dir(notePath) + if err := os.MkdirAll(notesDir, 0700); err != nil { + return fmt.Errorf("failed to create notes 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 { + // Log the error but don't fail the note creation + log.Printf("WARNING: Failed to version note: %v", err) + } + + if Debug { + fmt.Printf("Note created 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 ensureGitRepo(notesDir string) error { + gitDir := filepath.Join(notesDir, ".git") + if _, err := os.Stat(gitDir); os.IsNotExist(err) { + cmd := exec.Command("git", "init") + cmd.Dir = notesDir + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to initialize git repository: %v\nOutput: %s", err, output) + } + } + return nil +} + +func gitAddCommit(notePath, message string) error { + dir := filepath.Dir(notePath) + + // Ensure the repository exists + if err := ensureGitRepo(dir); err != nil { + return err + } + + // Git add + addCmd := exec.Command("git", "add", filepath.Base(notePath)) + addCmd.Dir = dir + if output, err := addCmd.CombinedOutput(); err != nil { + return fmt.Errorf("git add failed: %v\nOutput: %s", err, output) + } + + // Git commit + commitCmd := exec.Command("git", "commit", "-m", message) + commitCmd.Dir = dir + if output, err := commitCmd.CombinedOutput(); err != nil { + // Check if the error is due to no changes + if strings.Contains(string(output), "nothing to commit") { + return nil // This is not an error, just no changes to commit + } + return fmt.Errorf("git commit failed: %v\nOutput: %s", err, output) + } + + return nil +} \ No newline at end of file diff --git a/packages/render/driver.go b/packages/render/driver.go index dab13ef..66ea8ba 100644 --- a/packages/render/driver.go +++ b/packages/render/driver.go @@ -7,10 +7,13 @@ import ( "path/filepath" "strings" "text/template" - + "regexp" "github.com/janpreet/kado/packages/config" + "github.com/janpreet/kado/packages/keybase" ) +var keybaseNoteRegex = regexp.MustCompile(`{{keybase:note:([^}]+)}}`) + func join(data map[string]interface{}, key, delimiter string) string { var result []string for i := 0; ; i++ { @@ -116,12 +119,20 @@ func ProcessTemplate(templatePath string, data map[string]interface{}) (string, "GetKeysAsArray": func(key string) string { return FlattenedDataMap{Data: flatData}.GetKeysAsArray(key) }, + "KeybaseNote": func(noteName string) (string, error) { + return resolveKeybaseNote(noteName) + }, } - tmpl, err := template.New(filepath.Base(templatePath)).Funcs(funcMap).Parse(templateContent) - if err != nil { - return "", fmt.Errorf("failed to parse template: %v", err) - } + processedContent := keybaseNoteRegex.ReplaceAllStringFunc(templateContent, func(match string) string { + noteName := strings.TrimPrefix(strings.TrimSuffix(match, "}}"), "{{keybase:note:") + return fmt.Sprintf(`{{ KeybaseNote "%s" }}`, noteName) + }) + + tmpl, err := template.New(filepath.Base(templatePath)).Funcs(funcMap).Parse(processedContent) + if err != nil { + return "", fmt.Errorf("failed to parse template: %v", err) + } var output bytes.Buffer if err := tmpl.Execute(&output, FlattenedDataMap{Data: flatData}); err != nil { @@ -145,3 +156,11 @@ func ProcessTemplates(templatePaths []string, data map[string]interface{}) error } return nil } + +func resolveKeybaseNote(noteName string) (string, error) { + content, err := keybase.ViewNote(noteName) + if err != nil { + return "", fmt.Errorf("failed to resolve Keybase note %s: %v", noteName, err) + } + return strings.TrimSpace(content), nil +} \ No newline at end of file diff --git a/templates/terraform/vm.tfvars.tmpl b/templates/terraform/vm.tfvars.tmpl index 7de39ba..5c3aa79 100644 --- a/templates/terraform/vm.tfvars.tmpl +++ b/templates/terraform/vm.tfvars.tmpl @@ -1,7 +1,7 @@ aws_region = "{{.Get "aws.s3.region"}}" pm_api_url = "{{.Get "proxmox.api_url"}}" -pm_user = "{{.Env "PM_USER"}}" +pm_user = "{{ keybase:note:test_note }}" pm_password = "{{.Env "PM_PASSWORD"}}" vm_roles = { master = {{.Get "proxmox.vm.roles.master"}}