From b069b94cc03f14e10cee389f4ad9ef3af998fe1f Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Mon, 5 Aug 2024 22:23:27 -0400 Subject: [PATCH 01/10] keybase integration test --- README.md | 47 ++++++ main.go | 98 ++++++++----- packages/helper/helper.go | 147 +++++++++++++++++++ packages/helper/note_resolver.go | 22 +++ packages/helper/processer.go | 4 +- packages/keybase/keybase.go | 239 +++++++++++++++++++++++++++++++ 6 files changed, 515 insertions(+), 42 deletions(-) create mode 100644 packages/helper/note_resolver.go create mode 100644 packages/keybase/keybase.go 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 From 14cb8be8c56a2af9875001d57388c32d8f3a9676 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:09:41 -0400 Subject: [PATCH 02/10] updated Dockerfile and added cicd best practices --- CICD_BEST_PRACTICES.md | 169 +++++++++++++++++++++++++++++ Dockerfile | 5 + README.md | 33 ++---- cluster.kd | 2 +- packages/helper/note_resolver.go | 43 ++++++-- packages/keybase/keybase.go | 78 ++++++++++--- packages/render/driver.go | 29 ++++- templates/terraform/vm.tfvars.tmpl | 2 +- 8 files changed, 306 insertions(+), 55 deletions(-) create mode 100644 CICD_BEST_PRACTICES.md 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..a6a794a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,11 @@ 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 -O https://prerelease.keybase.io/keybase_amd64.tar.gz \ + && tar xvf keybase_amd64.tar.gz \ + && mv keybase /usr/local/bin/ \ + && rm keybase_amd64.tar.gz + COPY kado /usr/local/bin/kado ENV PATH="/opt/venv/bin:$PATH" diff --git a/README.md b/README.md index 6bd78fe..a7ab832 100644 --- a/README.md +++ b/README.md @@ -91,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"}} @@ -186,7 +186,7 @@ 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. +Kado integrates with Keybase to provide secure storage and referencing of sensitive information within your infrastructure configurations. ### Keybase Commands @@ -198,30 +198,17 @@ Kado integrates with Keybase to provide secure note storage and referencing with - `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 +### 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. @@ -229,6 +216,7 @@ In above example, `{{keybase:note:proxmox_api_key}}` and `{{keybase:note:secret_ 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 @@ -237,9 +225,10 @@ In above example, `{{keybase:note:proxmox_api_key}}` and `{{keybase:note:secret_ - `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/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/packages/helper/note_resolver.go b/packages/helper/note_resolver.go index 5c00f67..1f27c71 100644 --- a/packages/helper/note_resolver.go +++ b/packages/helper/note_resolver.go @@ -4,19 +4,44 @@ import ( "regexp" "strings" "fmt" + "log" "github.com/janpreet/kado/packages/keybase" + "github.com/janpreet/kado/packages/config" ) 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) +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) } - return strings.TrimSpace(content) - }), nil + } + + 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/keybase/keybase.go b/packages/keybase/keybase.go index db161d8..8561df3 100644 --- a/packages/keybase/keybase.go +++ b/packages/keybase/keybase.go @@ -3,12 +3,31 @@ 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 @@ -70,6 +89,7 @@ func LinkKeybase() error { return nil } + func InitNoteRepository() error { homeDir, err := os.UserHomeDir() if err != nil { @@ -92,10 +112,10 @@ func CheckKeybaseSetup() error { fmt.Printf("Keybase status output:\n%s\n", string(output)) } if err != nil { - return fmt.Errorf("Keybase is not properly set up: %v", err) + 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 fmt.Errorf("you are not logged in to Keybase. Please run 'kado keybase link' first") } return nil } @@ -109,21 +129,22 @@ func CreateNote(noteName, content string) error { 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) - } + 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 { - return fmt.Errorf("failed to version note: %v", err) + // 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 and versioned at: %s\n", notePath) + fmt.Printf("Note created at: %s\n", notePath) } return nil } @@ -220,19 +241,42 @@ func UpdateNote(noteName, content string) error { return nil } -func gitAddCommit(filePath, message string) error { - dir := filepath.Dir(filePath) - - addCmd := exec.Command("git", "add", filepath.Base(filePath)) +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 err := addCmd.Run(); err != nil { - return fmt.Errorf("git add failed: %v", err) + 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 err := commitCmd.Run(); err != nil { - return fmt.Errorf("git commit failed: %v", err) + 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 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"}} From ed90a8ec1907714c9b35e327dd61fe94e424cc6a Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:12:30 -0400 Subject: [PATCH 03/10] bumped version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b9aa64d90bd4e0cae6419f0f48d6c3549d6f8988 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:19:03 -0400 Subject: [PATCH 04/10] updated Dockerfile --- Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index a6a794a..e6f09d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,8 @@ RUN apk add --no-cache \ python3 \ py3-pip \ aws-cli \ + gnupg \ + libsecret \ && pip3 install --no-cache-dir --upgrade pip RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ @@ -21,10 +23,11 @@ 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 -O https://prerelease.keybase.io/keybase_amd64.tar.gz \ - && tar xvf keybase_amd64.tar.gz \ - && mv keybase /usr/local/bin/ \ - && rm keybase_amd64.tar.gz +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 @@ -33,4 +36,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"] From 485df0c43170b7ab1f51bce94a90c7a2fd0a9094 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:23:38 -0400 Subject: [PATCH 05/10] updated Dockerfile --- Dockerfile | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e6f09d5..cd1e5b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,10 @@ RUN apk add --no-cache \ aws-cli \ gnupg \ libsecret \ + make \ + gcc \ + musl-dev \ + git \ && pip3 install --no-cache-dir --upgrade pip RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ @@ -23,11 +27,11 @@ 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 +RUN git clone https://github.com/keybase/client.git /keybase \ + && cd /keybase \ + && make build \ + && cp /keybase/go/bin/keybase /usr/local/bin/ \ + && rm -rf /keybase COPY kado /usr/local/bin/kado From 2152ee4b050667e07f9a51e423fc795bf523036e Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:29:55 -0400 Subject: [PATCH 06/10] updated Dockerfile base --- Dockerfile | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index cd1e5b5..c90acc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,14 @@ -FROM alpine:3.18 +FROM debian:bullseye-slim ARG TERRAFORM_VERSION=1.9.3 ARG ANSIBLE_VERSION=10.2.0 -RUN apk add --no-cache \ +RUN apt-get update && apt-get install -y \ bash \ curl \ - python3 \ - py3-pip \ - aws-cli \ gnupg \ - libsecret \ - make \ - gcc \ - musl-dev \ - git \ + python3-pip \ + awscli \ && pip3 install --no-cache-dir --upgrade pip RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ @@ -27,11 +21,11 @@ RUN curl -L -o /usr/local/bin/opa https://openpolicyagent.org/downloads/latest/o RUN pip3 install --no-cache-dir ansible==${ANSIBLE_VERSION} -RUN git clone https://github.com/keybase/client.git /keybase \ - && cd /keybase \ - && make build \ - && cp /keybase/go/bin/keybase /usr/local/bin/ \ - && rm -rf /keybase +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 From 9f680c5bcff48d742c269d7f2d68c4cadf5b5ac5 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:34:33 -0400 Subject: [PATCH 07/10] updated Dockerfile --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index c90acc3..08b752c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,6 +7,7 @@ RUN apt-get update && apt-get install -y \ bash \ curl \ gnupg \ + unzip \ python3-pip \ awscli \ && pip3 install --no-cache-dir --upgrade pip From 89d066bcee1c5fed2b2e2331722b5507d44fc3af Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:38:53 -0400 Subject: [PATCH 08/10] updated Dockerfile --- Dockerfile | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 08b752c..4b41964 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,16 @@ RUN apt-get update && apt-get install -y \ curl \ gnupg \ unzip \ - python3-pip \ + python3.10 \ + python3.10-venv \ + python3.10-dev \ awscli \ - && pip3 install --no-cache-dir --upgrade pip + && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ + && python3.10 get-pip.py \ + && rm get-pip.py + +RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 \ + && update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ && unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ From 5229cc7f31671978736064d27c9dfd48f00b7d0b Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:43:11 -0400 Subject: [PATCH 09/10] updated Dockerfile --- Dockerfile | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b41964..c2bccfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,16 @@ FROM debian:bullseye-slim ARG TERRAFORM_VERSION=1.9.3 -ARG ANSIBLE_VERSION=10.2.0 +ARG ANSIBLE_VERSION=2.6.13 RUN apt-get update && apt-get install -y \ bash \ curl \ gnupg \ unzip \ - python3.10 \ - python3.10-venv \ - python3.10-dev \ + python3-pip \ awscli \ - && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ - && python3.10 get-pip.py \ - && rm get-pip.py - -RUN update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 \ - && update-alternatives --install /usr/bin/python python /usr/bin/python3.10 1 + && pip3 install --no-cache-dir --upgrade pip RUN curl -LO "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" \ && unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip \ From 26754f3b85eebdf737501fa2a4a136b068e55f41 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Wed, 7 Aug 2024 21:49:05 -0400 Subject: [PATCH 10/10] updated Dockerfile --- Dockerfile | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c2bccfa..381fca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,26 @@ FROM debian:bullseye-slim ARG TERRAFORM_VERSION=1.9.3 ARG ANSIBLE_VERSION=2.6.13 -RUN apt-get update && apt-get install -y \ +RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ curl \ gnupg \ unzip \ python3-pip \ awscli \ - && pip3 install --no-cache-dir --upgrade pip + 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 \