From 615391f941c5047917ae1b3593efa650bced6a65 Mon Sep 17 00:00:00 2001 From: Janpreet Singh Date: Sun, 28 Jul 2024 22:10:50 -0400 Subject: [PATCH] First commit. --- .github/workflows/disabled.yaml | 57 ++++ .github/workflows/docker-release.yaml | 57 ++++ .github/workflows/kdlinter.go | 133 +++++++++ .github/workflows/kdlinter.yaml | 38 +++ .github/workflows/sensitive-data-check.yml | 29 ++ Configuration.md | 243 ++++++++++++++++ Dockerfile | 31 ++ How-to.md | 170 +++++++++++ LandingZone/Empty.txt | 1 + Makefile | 42 +++ README.md | 120 ++++++++ Structure.md | 269 ++++++++++++++++++ VERSION | 1 + bump_version.py | 53 ++++ examples/cluster.kd | 17 ++ examples/cluster.yaml | 36 +++ examples/relay.kd | 3 + examples/templates/ansible/inventory.tmpl | 9 + .../templates/terraform/backend.tfvars.tmpl | 8 + examples/templates/terraform/vm.tfvars.tmpl | 28 ++ go.mod | 44 +++ go.sum | 159 +++++++++++ main.go | 253 ++++++++++++++++ packages/ansible/ansible.go | 29 ++ packages/bead/bead.go | 7 + packages/config/beadconfig.go | 15 + packages/config/config.go | 105 +++++++ packages/config/yamlconfig.go | 40 +++ packages/display/display.go | 83 ++++++ packages/engine/ai.go | 28 ++ packages/engine/engine.go | 52 ++++ packages/engine/formatter.go | 97 +++++++ packages/helper/gitclone.go | 40 +++ packages/helper/helper.go | 33 +++ packages/opa/opa.go | 137 +++++++++ packages/render/driver.go | 157 ++++++++++ packages/render/kd.go | 120 ++++++++ packages/render/writer.go | 57 ++++ packages/render/yaml.go | 62 ++++ packages/terraform/terraform.go | 150 ++++++++++ templates/ansible/inventory.tmpl | 1 + templates/terraform/backend.tfvars.tmpl | 1 + templates/terraform/terraform.tfvars.tmpl | 1 + 43 files changed, 3016 insertions(+) create mode 100644 .github/workflows/disabled.yaml create mode 100644 .github/workflows/docker-release.yaml create mode 100644 .github/workflows/kdlinter.go create mode 100644 .github/workflows/kdlinter.yaml create mode 100644 .github/workflows/sensitive-data-check.yml create mode 100644 Configuration.md create mode 100644 Dockerfile create mode 100644 How-to.md create mode 100644 LandingZone/Empty.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 Structure.md create mode 100644 VERSION create mode 100644 bump_version.py create mode 100644 examples/cluster.kd create mode 100644 examples/cluster.yaml create mode 100644 examples/relay.kd create mode 100644 examples/templates/ansible/inventory.tmpl create mode 100644 examples/templates/terraform/backend.tfvars.tmpl create mode 100644 examples/templates/terraform/vm.tfvars.tmpl create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 packages/ansible/ansible.go create mode 100644 packages/bead/bead.go create mode 100644 packages/config/beadconfig.go create mode 100644 packages/config/config.go create mode 100644 packages/config/yamlconfig.go create mode 100644 packages/display/display.go create mode 100644 packages/engine/ai.go create mode 100644 packages/engine/engine.go create mode 100644 packages/engine/formatter.go create mode 100644 packages/helper/gitclone.go create mode 100644 packages/helper/helper.go create mode 100644 packages/opa/opa.go create mode 100644 packages/render/driver.go create mode 100644 packages/render/kd.go create mode 100644 packages/render/writer.go create mode 100644 packages/render/yaml.go create mode 100644 packages/terraform/terraform.go create mode 100644 templates/ansible/inventory.tmpl create mode 100644 templates/terraform/backend.tfvars.tmpl create mode 100644 templates/terraform/terraform.tfvars.tmpl diff --git a/.github/workflows/disabled.yaml b/.github/workflows/disabled.yaml new file mode 100644 index 0000000..784745b --- /dev/null +++ b/.github/workflows/disabled.yaml @@ -0,0 +1,57 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + if: false + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin + + - name: Build Docker image + run: | + docker build --build-arg TERRAFORM_VERSION=1.9.3 --build-arg ANSIBLE_VERSION=10.2.0 -t ghcr.io/${{ github.repository_owner }}/kado:latest . + + - name: Push Docker image + run: | + docker push ghcr.io/${{ github.repository_owner }}/kado:latest + + run-kado: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Copy .kdconfig to Home Directory + run: | + mkdir -p $HOME + cp .kdconfig $HOME/ + + - name: Run Kado in Docker + run: | + docker run --rm -v ${{ github.workspace }}:/workspace -v $HOME/.kdconfig:/root/.kdconfig ghcr.io/${{ github.repository_owner }}/kado:latest ai diff --git a/.github/workflows/docker-release.yaml b/.github/workflows/docker-release.yaml new file mode 100644 index 0000000..607e194 --- /dev/null +++ b/.github/workflows/docker-release.yaml @@ -0,0 +1,57 @@ +name: Build, Docker, and Release + +on: + push: + branches: + - main + - 'feature/*' + - 'fix/*' + +env: + VERSION_FILE: VERSION + DOCKER_IMAGE: ghcr.io/${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: '1.22.x' + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Log in to GitHub Container Registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin + + - name: Bump version + run: make version-bump + + - name: Read version + id: version + run: echo "::set-output name=version::$(cat $(VERSION_FILE))" + + - name: Build Docker image + run: make docker-build + + - name: Push Docker image to GitHub Packages + run: | + docker push ${{ env.DOCKER_IMAGE }}:latest + docker push ${{ env.DOCKER_IMAGE }}:${{ steps.version.outputs.version }} + + - name: Create GitHub release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ steps.version.outputs.version }} + name: Release ${{ steps.version.outputs.version }} + files: kado + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/kdlinter.go b/.github/workflows/kdlinter.go new file mode 100644 index 0000000..b4ef2bf --- /dev/null +++ b/.github/workflows/kdlinter.go @@ -0,0 +1,133 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +type LintError struct { + File string + Line int + Message string +} + +func (e LintError) Error() string { + return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Message) +} + +func LintKDFile(filePath string) ([]LintError, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer file.Close() + + var lintErrors []LintError + scanner := bufio.NewScanner(file) + lineNumber := 0 + insideBead := false + commentRegex := regexp.MustCompile(`^\s*#`) + emptyLineCount := 0 + + for scanner.Scan() { + lineNumber++ + line := scanner.Text() + trimmedLine := strings.TrimSpace(line) + + if strings.HasPrefix(trimmedLine, "bead ") && strings.HasSuffix(trimmedLine, "{") { + if insideBead { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Nested beads are not allowed"}) + } + insideBead = true + if !strings.HasPrefix(line, "bead ") { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Bead declaration should start at the beginning of the line"}) + } + } else if trimmedLine == "}" && insideBead { + insideBead = false + if !strings.HasPrefix(line, "}") { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Closing brace should be at the beginning of the line"}) + } + } else if insideBead { + if !strings.HasPrefix(line, " ") { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Bead content should be indented with two spaces"}) + } + } else { + if commentRegex.MatchString(trimmedLine) { + if !strings.HasPrefix(trimmedLine, "#") { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Comments should start at the beginning of the line"}) + } + } else if trimmedLine != "" && strings.HasPrefix(line, " ") { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Non-bead content should not be indented"}) + } + } + + if trimmedLine == "" { + emptyLineCount++ + if emptyLineCount > 1 { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Multiple consecutive empty lines are not allowed"}) + } + } else { + emptyLineCount = 0 + } + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + if insideBead { + lintErrors = append(lintErrors, LintError{filePath, lineNumber, "Unclosed bead at end of file"}) + } + + return lintErrors, nil +} + +func LintKDFilesInDir(dir string) ([]LintError, error) { + var allErrors []LintError + + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(info.Name(), ".kd") { + errors, err := LintKDFile(path) + if err != nil { + return fmt.Errorf("failed to lint %s: %v", path, err) + } + allErrors = append(allErrors, errors...) + } + return nil + }) + + if err != nil { + return nil, err + } + + return allErrors, nil +} + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: kdlinter ") + os.Exit(1) + } + + dir := os.Args[1] + errors, err := LintKDFilesInDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + + for _, e := range errors { + fmt.Println(e) + } + + if len(errors) > 0 { + os.Exit(1) + } +} \ No newline at end of file diff --git a/.github/workflows/kdlinter.yaml b/.github/workflows/kdlinter.yaml new file mode 100644 index 0000000..407a0cc --- /dev/null +++ b/.github/workflows/kdlinter.yaml @@ -0,0 +1,38 @@ +name: KD Linter + +on: + push: + branches: + - '**' + +jobs: + lint: + name: Lint KD Files + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Build KD Linter + run: | + go build -o kdlinter kdlinter.go + + - name: Run KD Linter + run: | + ./kdlinter . + continue-on-error: true + + - name: Check Linter Output + run: | + if [ -s lint_output.txt ]; then + echo "Linter found issues:" + cat lint_output.txt + exit 1 + else + echo "No linting issues found." + fi \ No newline at end of file diff --git a/.github/workflows/sensitive-data-check.yml b/.github/workflows/sensitive-data-check.yml new file mode 100644 index 0000000..4ffdf49 --- /dev/null +++ b/.github/workflows/sensitive-data-check.yml @@ -0,0 +1,29 @@ +name: Sensitive Data Check + +on: + push: + branches: + - '**' + +jobs: + gitleaks: + name: Gitleaks + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} + + - name: Check Gitleaks output + if: ${{ failure() }} + run: | + echo "Gitleaks has detected potential sensitive data in your PR." + echo "Please review the Gitleaks output and remove any sensitive information before merging." + exit 1 \ No newline at end of file diff --git a/Configuration.md b/Configuration.md new file mode 100644 index 0000000..b7882b4 --- /dev/null +++ b/Configuration.md @@ -0,0 +1,243 @@ +# Kado Configuration Documentation + +## Bring Your Own Code (BYOC) + +Kado is designed to be flexible and adaptable to your infrastructure needs. Users can plug the source for their parameterized Infrastructure as Code (IaC) into Kado. Kado uses a single source of truth (e.g., `cluster.yaml` and templates) for IaC external variables, which are passed to the IaC tools like Terraform and Ansible. + +This approach ensures that configurations are consistent across different environments and can be easily managed and updated. + +## Overview + +Kado uses a modular configuration approach where each module is called a "bead." Beads are blocks of configuration that define specific aspects of your infrastructure and can relay configurations to other beads. This document provides an overview of the current bead types, their functions, configured/allowed inputs, the processing flow, and the Kado file format including templates. + +## Kado File Format + +Kado uses `*.kd` files to define beads and their configurations. Each `.kd` file can contain multiple beads, and users can have as many `.kd` files and templates as needed. The structure of a `.kd` file is defined as follows: + +### Basic Structure + +A bead is defined using the following structure: + +```hcl +#comment +bead "" { + key = "value" + another_key = "another_value" + #another_comment +} +``` + +- ``: The name of the bead. +- `key = "value"`: Configuration settings. +- `#comment`: Comments for explanation. + +### Example + +```hcl +# This is an example bead configuration +bead "example_bead" { + key1 = "value1" + key2 = "value2" + #additional comments +} +``` + +## Template Files + +Kado uses templates to generate configuration files for various tools like Ansible and Terraform. The templates are stored in the `templates/` directory and can be customized as needed. Below are examples of custom template files used in Kado: + +### Ansible Inventory Template + +**Path**: `templates/ansible/inventory.tmpl` + +```ini + +[proxmox] +{{join "proxmox.nodes.saathi01" "\n"}} +{{join "proxmox.nodes.saathi02" "\n"}} + +[all:vars] +cluster_name={{.Get "proxmox.cluster_name"}} +ansible_user={{.Get "ansible.user"}} +ansible_python_interpreter={{.Get "ansible.python_interpreter"}} +``` + +### Terraform Variables Template + +**Path**: `templates/terraform/vm.tfvars.tmpl` + +```hcl + +aws_region = "{{.Get "aws.s3.region"}}" +pm_api_url = "{{.Get "proxmox.api_url"}}" +pm_user = "{{.Env "PM_USER"}}" +pm_password = "{{.Env "PM_PASSWORD"}}" +vm_roles = { + master = {{.Get "proxmox.vm.roles.master"}} + worker = {{.Get "proxmox.vm.roles.worker"}} + loadbalancer = {{.Get "proxmox.vm.roles.loadbalancer"}} +} +vm_template = {{.Get "proxmox.vm.template"}} +vm_cpu = {{.Get "proxmox.vm.cpu"}} +vm_memory = {{.Get "proxmox.vm.memory"}} +vm_disk_size = "{{.Get "proxmox.vm.disk_size"}}" +vm_storage = "{{.Get "proxmox.vm.storage"}}" +vm_network_bridge = "{{.Get "proxmox.vm.network_bridge"}}" +vm_network_model = "{{.Get "proxmox.vm.network_model"}}" +proxmox_nodes = {{ .GetKeysAsArray "proxmox.nodes" }} +ssh_public_key_content = "/Users/janpreetsingh/.ssh/id_rsa.pub" +ssh_private_key = "/Users/janpreetsingh/.ssh/id_rsa" +ssh_user = "{{.Get "proxmox.vm.ssh_user"}}" +cloud_init_user_data_file = "templates/cloud_init_user_data.yaml" +k8s_master_setup_script = "scripts/k8s_master_setup.sh" +k8s_worker_setup_script = "scripts/k8s_worker_setup.sh" +haproxy_setup_script = "scripts/haproxy_setup.sh" +haproxy_config_file = "templates/haproxy.cfg" +s3_bucket = "{{.Get "aws.s3.bucket"}}" +s3_key = "{{.Get "aws.s3.key"}}" +``` + +### Custom Template Functions + +Kado provides custom template functions to enhance the templating capabilities: + +- `Get`: Fetches the value of a specified key. +- `Env`: Fetches the value of an environment variable. +- `GetKeysAsArray`: Fetches the keys of a map as an array. + +**Note**: The title of the output file (e.g., ``) is added to the top of the file. + +## Bead Types + +### Ansible Bead + +**Purpose**: Defines configurations for running Ansible playbooks. + +**Configured/Allowed Inputs**: +- `enabled`: (boolean) Whether the Ansible bead is enabled. +- `source`: (string) Git repository URL for the Ansible playbook. +- `playbook`: (string) Path to the Ansible playbook. +- `extra_vars_file`: (boolean) Whether to use an extra variables file. +- `relay`: (string) Name of the bead to relay configurations to. +- `relay_field`: (string) Comma-separated list of key-value pairs to relay. + +**Example**: +```hcl +bead "ansible" { + enabled = false + source = "git@github.com:janpreet/proxmox_ansible.git" + playbook = "cluster.yaml" + extra_vars_file = false + relay = opa + relay_field = "source=git@github.com:janpreet/proxmox_ansible.git,path=ansible/policies/proxmox.rego,input=ansible/cluster.yaml,package=data.proxmox.main.allowed" + #extra_vars = "a=b" +} +``` + +### Terraform Bead + +**Purpose**: Defines configurations for running Terraform. + +**Configured/Allowed Inputs**: +- `enabled`: (boolean) Whether the Terraform bead is enabled. +- `source`: (string) Git repository URL for the Terraform configurations. +- `relay`: (string) Name of the bead to relay configurations to. +- `relay_field`: (string) Comma-separated list of key-value pairs to relay. + +**Example**: +```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" +} +``` + +### OPA Bead + +**Purpose**: Defines configurations for running Open Policy Agent (OPA) validations. + +**Configured/Allowed Inputs**: +- `enabled`: (boolean) Whether the OPA bead is enabled. +- `path`: (string) Path to the OPA policy file. +- `input`: (string) Path to the input data file for OPA. +- `package`: (string) OPA package to evaluate. + +**Example**: +```hcl +bead "opa" { + enabled = true + path = "path/to/opa/policy.rego" + input = "path/to/opa/input.json" + package = "data.example.allow" +} +``` + +### Custom Beads + +**Purpose**: Define user-specific configurations. + +**Configured/Allowed Inputs**: User-defined key-value pairs. + +**Example**: +```hcl +bead "banana" { + author = "Jane Doe" + description = "This is a test bead" + version = "3.1" + status = "active" +} +``` + +## Processing Beads + +### Basic Processing Flow + +1. **Initialization**: Beads are read from `*.kd` files and stored in a list. +2. **Validation**: Each bead is validated based on its defined structure and required fields. +3. **Processing**: Beads are processed in the order they appear, executing their configurations. + +### Relay Mechanism + +A bead can relay its configurations to another bead using the `relay` and `relay_field` attributes. This mechanism allows one bead to pass its configurations to another bead for further processing. + +**Relay Example**: +```hcl +bead "ansible" { + enabled = false + source = "git@github.com:janpreet/proxmox_ansible.git" + playbook = "cluster.yaml" + extra_vars_file = false + relay = opa + relay_field = "source=git@github.com:janpreet/proxmox_ansible.git,path=ansible/policies/proxmox.rego,input=ansible/cluster.yaml,package=data.proxmox.main.allowed" +} +``` + +### Relay Overrides + +When a bead relays to another bead, it can override specific configurations using the `relay_field` attribute. The relay field is a comma-separated list of key-value pairs that specify the overrides. + +### Processing Order + +Beads are processed in the order they appear in the `.kd` files. If a bead relays to another bead, the relayed bead is processed next. This ensures that configurations are applied in a logical sequence. + +### Preventing Duplicate Processing + +A bead is processed once unless it is a relayed bead. To prevent duplicate processing: +- Keep track of processed beads using a map. +- Increment the count each time a bead is processed. +- Skip processing if the bead has already been processed, except for relayed beads. + +**Example of Avoiding Duplicate Processing**: +```go +processed := make(map[string]int) + +for _, b := range validBeads { + if err := processBead(b, yamlData, beadMap, processed, &processedBeads, applyPlan, "", false); err != nil { + log.Fatalf("Failed to process bead %s: %v", b.Name, err) + + + } +} +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..67edfd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM alpine:3.18 + +ARG TERRAFORM_VERSION=1.9.3 +ARG ANSIBLE_VERSION=10.2.0 + +RUN apk add --no-cache \ + bash \ + curl \ + python3 \ + py3-pip \ + aws-cli \ + && 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 \ + && mv terraform /usr/local/bin/ \ + && rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip + +RUN curl -L -o /usr/local/bin/opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static \ + && chmod 755 /usr/local/bin/opa + +RUN pip3 install --no-cache-dir ansible==${ANSIBLE_VERSION} + +COPY kado /usr/local/bin/kado + +ENV PATH="/opt/venv/bin:$PATH" + +WORKDIR /workspace + +#ENTRYPOINT ["tail", "-f", "/dev/null"] +ENTRYPOINT ["/usr/local/bin/kado"] \ No newline at end of file diff --git a/How-to.md b/How-to.md new file mode 100644 index 0000000..56be8ac --- /dev/null +++ b/How-to.md @@ -0,0 +1,170 @@ +# Kado Commands and Getting Started + +## Available Commands + +Kado supports several commands to help you manage and process your infrastructure configurations. + +### `version` + +Displays the current version of Kado. + +```sh +kado version +``` + +### `config` + +Loads and displays the bead configurations from the `*.kd` files in the current directory. This command shows the configuration with the order of execution. + +```sh +kado config +``` + +### `set` + +Processes the beads defined in the `*.kd` files and applies the configurations. + +```sh +kado set +``` + +**Note:** If OPA (Open Policy Agent) is enabled and a bead is relayed to OPA for policy evaluation, you cannot run `kado set` without an approved policy. Beads that are not relayed to OPA or do not have policy enforcement can still be processed and set without OPA approval. + +### `fmt` + +Formats the `.kd` files in the proper Kado format. You can format all `.kd` files in the current directory or specify a single `.kd` file to format. + +```sh +kado fmt +# or +kado fmt +``` + +### `ai` + +Analyzes the Terraform and Ansible configurations and provides infrastructure recommendations using an AI model. Requires AI configuration in `~/.kdconfig`. + +```sh +kado ai +``` + +## Getting Started + +### Installation + +1. **Clone the Repository**: + ```sh + git clone https://github.com/janpreet/kado.git + cd kado + ``` + +2. **Build the Binary**: + ```sh + make build + ``` + +3. **Run Kado**: + ```sh + ./kado + ``` + +### Downloading Releases + +You can download pre-built releases of Kado from the [GitHub Releases](https://github.com/janpreet/kado/releases) page. + +### Configuration + +Kado uses a configuration file located at `~/.kdconfig` for AI integration. The configuration file should include the following settings: + +```sh +# ~/.kdconfig +AI_API_KEY= +AI_MODEL= +AI_CLIENT= +AI_ENABLED=disabled # Can be overridden to enabled +``` + +- `AI_API_KEY`: Your API key for the AI model. +- `AI_MODEL`: The model you want to use (e.g., `gpt-3.5-turbo` or `claude-3-5-sonnet-20240620`). +- `AI_CLIENT`: The type of AI client (e.g., `chatgpt` or `anthropic_messages`). +- `AI_ENABLED`: Flag to enable or disable AI integration (default is `disabled`). + +### Running Kado with AI + +1. **Ensure AI is Enabled**: + - Edit the `~/.kdconfig` file and set `AI_ENABLED=enabled`. + +2. **Run the `ai` Command**: + ```sh + kado ai + ``` + + This command will analyze the Terraform and Ansible configurations and provide recommendations. + +## Examples + +### Example `.kd` File + +```hcl +# Example Kado configuration +bead "example_bead" { + key1 = "value1" + key2 = "value2" + #additional_comment +} +``` + +### Example AI Configuration + +```sh +# ~/.kdconfig +AI_API_KEY=your_api_key +AI_MODEL=gpt-3.5-turbo +AI_CLIENT=chatgpt +AI_ENABLED=enabled +``` + +### Running Commands + +- To format `.kd` files: + ```sh + kado fmt + # or to format a specific file + kado fmt example.kd + ``` + +- To display bead configurations with the order of execution: + ```sh + kado config + ``` + +- To process bead configurations and review IaC outputs: + ```sh + kado + ``` + +- To apply IaC config: + ```sh + kado set + ``` + +- To get AI recommendations: + ```sh + kado ai + ``` + +## Command Usage Explanation + +### Running `kado` + +Running `kado` without any additional commands will start processing the bead configurations and apply the configurations found in the `*.kd` files in the current directory. For Ansible this will run playbook in dry-run mode, and Terraform is only until plan for user review. + +### Running `kado set` + +The `kado set` command processes the beads defined in the `*.kd` files and applies the configurations. This command ensures that the infrastructure setup is applied according to the definitions in the `.kd` files. This is equivalent of Terraform apply. + +**Note:** If OPA (Open Policy Agent) is enabled and a bead is relayed to OPA for policy evaluation, you cannot run `kado set` without an approved policy. Beads that are not relayed to OPA or do not have policy enforcement can still be processed and set without OPA approval. + +### Running `kado config` + +The `kado config` command loads and displays the bead configurations from the `*.kd` files in the current directory. It shows the configuration with the order of execution. diff --git a/LandingZone/Empty.txt b/LandingZone/Empty.txt new file mode 100644 index 0000000..b1baae3 --- /dev/null +++ b/LandingZone/Empty.txt @@ -0,0 +1 @@ +# Adding empty folder \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..26042ea --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# Makefile + +# Variables +BINARY_NAME = kado +VERSION_FILE = VERSION +PYTHON_SCRIPT = bump_version.py +VERSION := $(shell cat $(VERSION_FILE)) + +# Define your GitHub username and repository name +GITHUB_USERNAME = janpreet +GITHUB_REPOSITORY = kado +DOCKER_IMAGE = ghcr.io/janpreet/kado + +# Build the binary +build: + GOOS=linux GOARCH=amd64 go build -o $(BINARY_NAME) main.go + +# Build the Docker image +docker-build: build + docker build -t $(DOCKER_IMAGE):latest -t $(DOCKER_IMAGE):$(VERSION) . + +# Push the Docker image to GitHub Packages +docker-push: + docker push $(DOCKER_IMAGE):latest + docker push $(DOCKER_IMAGE):$(VERSION) + +# Bump version, tag, and push +version-bump: + python3 $(PYTHON_SCRIPT) + +tag-and-push: version-bump + git add $(VERSION_FILE) + git commit -m "Bump version to $(VERSION)" + git tag v$(VERSION) + git push origin main --tags + +# Clean up the build +clean: + rm -f $(BINARY_NAME) + +# Default target +all: clean build docker-build docker-push tag-and-push diff --git a/README.md b/README.md new file mode 100644 index 0000000..5658d3f --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ +# Kado# Kado Project Overview + +## Introduction + +Kado is a powerful and flexible tool designed to streamline the management of infrastructure configurations through a modular and declarative approach. Whether you're managing virtual machines, Kubernetes clusters, or other infrastructure components, Kado provides a cohesive framework to integrate and automate your Infrastructure as Code (IaC) processes using Terraform, Ansible, and Open Policy Agent (OPA). + +## Problems Kado Solves + +### 1. **Consistency in Infrastructure Configurations** + +Maintaining consistency in infrastructure configurations across different environments can be challenging. Inconsistent configurations can lead to unexpected behaviors, security vulnerabilities, and operational inefficiencies. + +**How Kado Solves It:** +- Kado uses a single source of truth for infrastructure configurations, defined in `*.kd` files and templates. This ensures that configurations are consistent and repeatable across all environments. + +### 2. **Modular and Scalable Configuration Management** + +As infrastructure grows in complexity, managing configurations becomes increasingly difficult. Large monolithic configuration files are hard to maintain and scale. + +**How Kado Solves It:** +- Kado introduces a modular approach with "beads," which are blocks of configurations that define specific aspects of your infrastructure. This modularity allows you to manage, update, and scale your configurations easily. + +### 3. **Automation and Integration of IaC Tools** + +Integrating and automating different IaC tools like Terraform and Ansible can be cumbersome. Each tool has its own syntax, workflows, and integration points, which can complicate automation. + +**How Kado Solves It:** +- Kado seamlessly integrates Terraform and Ansible by using templates and a unified configuration structure. It automates the execution of these tools, ensuring smooth and efficient workflows. + +### 4. **Policy Enforcement and Compliance** + +Ensuring that your infrastructure complies with security policies and operational guidelines is critical. However, manually enforcing these policies can be error-prone and time-consuming. + +**How Kado Solves It:** +- Kado incorporates Open Policy Agent (OPA) to enforce policies and compliance checks. By relaying configurations to OPA, Kado ensures that only approved policies are applied, enhancing security and compliance. + +### 5. **Simplified Configuration Management** + +Managing infrastructure configurations often requires deep knowledge of various tools and their configurations. This complexity can slow down development and operations teams. + +**How Kado Solves It:** +- Kado abstracts the complexities of individual IaC tools and provides a simplified, unified interface for managing configurations. This reduces the learning curve and enables teams to focus on their core tasks. + +## How Kado Achieves This + +### 1. **Declarative Configuration Files** + +Kado uses `*.kd` files to define infrastructure configurations in a declarative manner. These files contain "beads," which are modular blocks that specify configurations for different aspects of your infrastructure. + +### 2. **Templating System** + +Kado leverages a powerful templating system to generate configuration files for Terraform and Ansible. Templates can be customized to fit specific needs, ensuring flexibility and adaptability. + +### 3. **Automation of IaC Workflows** + +Kado automates the execution of Terraform and Ansible by processing the `*.kd` files and applying the configurations. This automation streamlines workflows and reduces the potential for human error. + +### 4. **Policy Enforcement with OPA** + +By integrating Open Policy Agent, Kado enforces policies and compliance checks on your infrastructure configurations. Beads can relay their configurations to OPA, ensuring that only compliant configurations are applied. + +### 5. **Modular and Extensible Design** + +Kado's bead structure allows for modular and extensible configuration management. Users can define custom beads and templates to fit their unique infrastructure requirements. + +### 6. **Bring Your Own Code (BYOC)** + +Kado supports BYOC, allowing users to plug in their own parameterized IaC code. This means you can bring your existing Terraform and Ansible configurations and integrate them into Kado. + +### 7. Centralized Variable Management +Kado uses a central configuration file (e.g., cluster.yaml) to store variable values that are used across different beads and templates. This ensures consistency and simplifies the management of configuration variables. + + +## Key Features + +### Modular Configuration + +Kado uses a bead-based configuration system where each bead represents a distinct aspect of your infrastructure. This modular approach allows you to define specific configurations for different tools and components, making it easy to manage and update your infrastructure as needed. + +### Single Source of Truth + +Kado leverages a single source of truth for configuration variables, ensuring consistency across different environments. By defining variables in a centralized configuration file (e.g., `cluster.yaml`), Kado ensures that all infrastructure components use the same set of parameters, reducing the risk of configuration drift. + +### Integration with Popular Tools + +Kado seamlessly integrates with popular infrastructure management tools like Terraform, Ansible, and OPA. This integration allows you to harness the power of these tools while benefiting from Kado's unified configuration and management framework. + +### Automation and Relay Mechanism + +Kado automates the deployment and management of infrastructure by processing beads in a defined order. Beads can relay their configurations to other beads, enabling a flexible and dynamic workflow. This relay mechanism ensures that configurations are applied logically and consistently across different components. + +### Policy Enforcement with OPA + +Kado supports policy enforcement using OPA. If OPA is enabled and configured, Kado ensures that infrastructure changes comply with defined policies before applying them. This feature helps maintain security and compliance standards across your infrastructure. + +## Getting Started + +### Easy Configuration + +Kado uses `*.kd` files to define beads and their configurations. Users can have as many `.kd` files and templates as needed, allowing for a highly customizable and scalable setup. + +### Flexible Templates + +Kado supports custom templates for generating configuration files for various tools. These templates can be tailored to meet the specific needs of your infrastructure, providing flexibility and control over the configuration process. + +### Simplified Deployment + +Kado simplifies the deployment process by automating the execution of configured beads. Whether you are using Terraform for provisioning resources, Ansible for configuration management, or OPA for policy enforcement, Kado ensures that everything works together seamlessly. + +### Example Workflow + +1. **Define Beads**: Create `*.kd` files to define your infrastructure components using beads. +2. **Configure Templates**: Customize templates to generate the necessary configuration files for your tools. +3. **Run Kado**: Use Kado commands to process and apply your configurations, ensuring that your infrastructure is deployed and managed consistently. + +## Conclusion + +Kado aims to simplify and streamline the management of your infrastructure as code. By providing a modular, consistent, and automated framework, Kado helps you reduce complexity, minimize errors, and achieve efficient infrastructure management. Whether you are provisioning resources with Terraform, managing configurations with Ansible, or enforcing policies with OPA, Kado brings everything together into a cohesive and powerful tool. + +Dive into the Kado project and experience a new level of simplicity and efficiency in managing your infrastructure! \ No newline at end of file diff --git a/Structure.md b/Structure.md new file mode 100644 index 0000000..b25916e --- /dev/null +++ b/Structure.md @@ -0,0 +1,269 @@ +Here is the detailed documentation for the codebase based on the provided structure: + +## Project Structure + +```plaintext +. +├── Configuration.md +├── Dockerfile +├── How-to.md +├── LandingZone +│   └── Empty.txt +├── Makefile +├── README.md +├── Structure.md +├── VERSION +├── bump_version.py +├── examples +│   ├── cluster.kd +│   ├── cluster.yaml +│   ├── relay.kd +│   └── templates +│   ├── ansible +│   │   └── inventory.tmpl +│   └── terraform +│   ├── backend.tfvars.tmpl +│   └── vm.tfvars.tmpl +├── go.mod +├── go.sum +├── kado +├── main.go +├── packages +│   ├── ansible +│   │   └── ansible.go +│   ├── bead +│   │   └── bead.go +│   ├── config +│   │   ├── beadconfig.go +│   │   ├── config.go +│   │   └── yamlconfig.go +│   ├── display +│   │   └── display.go +│   ├── engine +│   │   ├── ai.go +│   │   ├── engine.go +│   │   └── formatter.go +│   ├── helper +│   │   ├── gitclone.go +│   │   └── helper.go +│   ├── opa +│   │   └── opa.go +│   ├── render +│   │   ├── driver.go +│   │   ├── kd.go +│   │   ├── writer.go +│   │   └── yaml.go +│   └── terraform +│   └── terraform.go +├── templates +│   ├── ansible +│   │   └── inventory.tmpl +│   └── terraform +│   ├── backend.tfvars.tmpl +│   └── terraform.tfvars.tmpl +``` + +## File and Directory Overview + +### Root Directory + +- **LandingZone/**: Directory where the repositories and files required by the beads are cloned and processed. +- **cluster.kd**: The main custom configuration file that defines the beads and their properties. +- **cluster.yaml**: YAML configuration file for the cluster setup. +- **go.mod** and **go.sum**: Go modules files for dependency management. +- **main.go**: The main entry point of the application. +- **readme.md**: Documentation file for the project. +- **testing.kd**: An additional KD file for testing purposes. + +### Packages Directory + +#### Ansible + +- **ansible.go**: Contains functions to handle the execution of Ansible playbooks. + +#### Bead + +- **bead.go**: Defines the structure and properties of a bead. + +#### Config + +- **beadconfig.go**: Contains functions to load bead configurations. +- **config.go**: Contains functions to load general configurations. +- **yamlconfig.go**: Contains functions to load and parse YAML configurations. + +#### Display + +- **display.go**: Contains functions to display bead configurations and YAML content. + +#### Engine + +- **engine.go**: Contains the main function for handling the execution of Ansible playbooks. + +#### Helper + +- **gitclone.go**: Contains functions to clone Git repositories. +- **helper.go**: Contains helper functions for various operations such as file checks and setting up the environment. + +#### OPA + +- **opa.go**: Contains functions to handle OPA policy evaluation and related actions. + +#### Render + +- **driver.go**: Contains the main driver functions for rendering templates. +- **kd.go**: Contains functions to parse and process KD files. +- **writer.go**: Contains functions to write output files. +- **yaml.go**: Contains functions to handle YAML processing. + +#### Terraform + +- **terraform.go**: Contains functions to handle Terraform operations such as planning and applying configurations. + +### Templates Directory + +#### Ansible + +- **inventory.tmpl**: Template for the Ansible inventory. + +#### Terraform + +- **backend.tfvars.tmpl**: Template for Terraform backend variables. +- **vm.tfvars.tmpl**: Template for Terraform VM variables. + +## Detailed Documentation + +### main.go + +This is the main entry point of the application. It orchestrates the processing of beads, loading configurations, and setting up the environment. + +Key Functions: + +- **main**: Entry point of the application. Handles command-line arguments and processes beads. +- **processBead**: Processes a single bead, including cloning repositories, rendering templates, and handling Ansible and Terraform operations. +- **convertYAMLToSlice**: Converts YAML data to a slice of maps. +- **applyRelayOverrides**: Applies overrides for relay fields. + +### packages/bead/bead.go + +Defines the structure and properties of a bead. A bead represents a unit of work or configuration in the system. + +### packages/config/config.go + +Contains functions to load general configurations, including bead configurations and YAML configurations. + +Key Functions: + +- **LoadBeadsConfig**: Loads bead configurations from a custom format file. +- **LoadYAMLConfig**: Loads and parses YAML configuration files. + +### packages/display/display.go + +Contains functions to display bead configurations and YAML content. + +Key Functions: + +- **DisplayBeads**: Displays parsed beads from KD files. +- **DisplayYAMLs**: Displays parsed YAML content. +- **DisplayTemplateOutput**: Displays the result of processing templates. +- **DisplayBeadConfig**: Displays the configuration and order of execution of beads. + +### packages/engine/engine.go + +Contains the main function for handling the execution of Ansible playbooks. + +Key Functions: + +- **HandleAnsible**: Executes an Ansible playbook with the given configuration. + +### packages/helper/helper.go + +Contains helper functions for various operations such as file checks and setting up the environment. + +Key Functions: + +- **FileExists**: Checks if a file exists. +- **SetupLandingZone**: Sets up the LandingZone directory. +- **CloneRepo**: Clones a Git repository. + +### packages/opa/opa.go + +Contains functions to handle OPA (Open Policy Agent) policy evaluation and related actions. + +Key Functions: + +- **HandleOPA**: Handles the processing of the OPA bead, including policy evaluation and action handling. + +### packages/render/kd.go + +Contains functions to parse and process KD files. + +Key Functions: + +- **GetKDFiles**: Gets all KD files in the specified directory. +- **ProcessKdFiles**: Processes all KD files and returns beads and invalid bead names. +- **parseKdFile**: Parses a single KD file and returns beads and invalid bead names. + +### packages/terraform/terraform.go + +Contains functions to handle Terraform operations such as planning and applying configurations. + +Key Functions: + +- **HandleTerraform**: Handles the processing of the Terraform bead, including planning and applying configurations. + +## Bead Structure and Valid Fields + +### General Structure + +A bead is defined in a KD file with the following structure: + +```plaintext +bead "" { + = "" + = "" + ... +} +``` + +### Valid Fields for Specific Beads + +#### Ansible Bead + +- **source**: URL of the Git repository containing the Ansible playbook. +- **playbook**: Path to the Ansible playbook file. +- **extra_vars_file**: Boolean indicating if extra variables file should be used. +- **relay**: Name of the relay bead. +- **relay_field**: Overrides for relay fields. + +#### Terraform Bead + +- **source**: URL of the Git repository containing the Terraform configuration. +- **relay**: Name of the relay bead. +- **relay_field**: Overrides for relay fields. + +#### OPA Bead + +- **path**: Path to the OPA policy file. +- **input**: Path to the input file for OPA evaluation. +- **package**: OPA package to evaluate. + +#### Example Bead Definition + +```plaintext +bead "ansible" { + enabled = true + source = "git@github.com:janpreet/proxmox_ansible.git" + playbook = "cluster.yaml" + extra_vars_file = false + relay = opa + relay_field = "source=git@github.com:janpreet/proxmox_ansible.git,path=ansible/policies/proxmox.rego,input=ansible/cluster.yaml,package=data.proxmox.main.allowed" +} +``` + +## Additional Notes + +- **Relay Mechanism**: Beads can relay execution to another bead using the `relay` and `relay_field` properties. +- **OPA Evaluation**: OPA policies are evaluated to determine if actions (e.g., Terraform apply, Ansible playbook execution) should be allowed or denied. +- **Command-line Arguments**: The application can be run with different command-line arguments (e.g., `version`, `config`, `set`) to control its behavior. + +This detailed documentation should help in understanding the structure, functionality, and usage of the codebase. If you have any further questions or need additional information, feel free to ask! \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..6c6aa7c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file diff --git a/bump_version.py b/bump_version.py new file mode 100644 index 0000000..73d69ad --- /dev/null +++ b/bump_version.py @@ -0,0 +1,53 @@ +import re +import subprocess + +def get_current_branch(): + try: + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip().decode('utf-8') + return branch + except subprocess.CalledProcessError as e: + print(f"Error getting current branch: {e}") + return None + +def read_version(version_file): + with open(version_file, 'r') as file: + return file.read().strip() + +def write_version(version_file, version): + with open(version_file, 'w') as file: + file.write(version + '\n') + +def bump_version(version, part): + major, minor, patch = map(int, version.split('.')) + if part == 'major': + major += 1 + minor = 0 + patch = 0 + elif part == 'minor': + minor += 1 + patch = 0 + elif part == 'patch': + patch += 1 + return f"{major}.{minor}.{patch}" + +def main(): + version_file = 'VERSION' + current_version = read_version(version_file) + branch = get_current_branch() + + if not branch: + print("Could not determine the current git branch.") + return + + if re.match(r'^feature/', branch): + new_version = bump_version(current_version, 'major') + elif re.match(r'^fix/', branch): + new_version = bump_version(current_version, 'minor') + else: + new_version = bump_version(current_version, 'patch') + + write_version(version_file, new_version) + print(f"Bumped version from {current_version} to {new_version}") + +if __name__ == "__main__": + main() diff --git a/examples/cluster.kd b/examples/cluster.kd new file mode 100644 index 0000000..f33ad9b --- /dev/null +++ b/examples/cluster.kd @@ -0,0 +1,17 @@ +# Comment +bead "ansible" { + enabled = true + source = "git@github.com:janpreet/proxmox_ansible.git" + playbook = "cluster.yaml" + extra_vars_file = false + relay = opa + relay_field = "source=git@github.com:janpreet/proxmox_ansible.git,path=ansible/policies/proxmox.rego,input=ansible/cluster.yaml,package=data.proxmox.main.allowed" + # extra_vars = "a=b" +} + +bead "terraform" { + source = "git@github.com:janpreet/proxmox_terraform.git" + enabled = false + 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" +} diff --git a/examples/cluster.yaml b/examples/cluster.yaml new file mode 100644 index 0000000..4d1499c --- /dev/null +++ b/examples/cluster.yaml @@ -0,0 +1,36 @@ +--- +ansible: + user: "user" + python_interpreter: "/usr/bin/python3" + +proxmox: + cluster_name: "pmc" + api_url: "https://1.2.3.4:8006/api2/json" + user: "user" + password: "password" + nodes: + saathi01: + - 1.2.3.4 + saathi02: + - 1.2.3.5 + vm: + roles: + master: 2 + worker: 3 + loadbalancer: 1 + template: 100 + cpu: 2 + memory: 2048 + storage: "local-lvm" + disk_size: "10G" + network_bridge: "vmbr0" + network_model: "virtio" + ssh_public_key_content: "" + ssh_private_key: "" + ssh_user: "ubuntu" + +aws: + s3: + region: "aws-region" + bucket: "s3-bucket" + key: "tf-key" diff --git a/examples/relay.kd b/examples/relay.kd new file mode 100644 index 0000000..359777e --- /dev/null +++ b/examples/relay.kd @@ -0,0 +1,3 @@ +bead "opa" { + enabled = false +} diff --git a/examples/templates/ansible/inventory.tmpl b/examples/templates/ansible/inventory.tmpl new file mode 100644 index 0000000..19d393b --- /dev/null +++ b/examples/templates/ansible/inventory.tmpl @@ -0,0 +1,9 @@ + +[proxmox] +{{join "proxmox.nodes.saathi01" "\n"}} +{{join "proxmox.nodes.saathi02" "\n"}} + +[all:vars] +cluster_name={{.Get "proxmox.cluster_name"}} +ansible_user={{.Get "ansible.user"}} +ansible_python_interpreter={{.Get "ansible.python_interpreter"}} \ No newline at end of file diff --git a/examples/templates/terraform/backend.tfvars.tmpl b/examples/templates/terraform/backend.tfvars.tmpl new file mode 100644 index 0000000..58e287d --- /dev/null +++ b/examples/templates/terraform/backend.tfvars.tmpl @@ -0,0 +1,8 @@ + +terraform { + backend "s3" { + bucket = "{{.Get "aws.s3.bucket"}}" + key = "{{.Get "aws.s3.key"}}" + region = "{{.Get "aws.s3.region"}}" + } +} \ No newline at end of file diff --git a/examples/templates/terraform/vm.tfvars.tmpl b/examples/templates/terraform/vm.tfvars.tmpl new file mode 100644 index 0000000..7de39ba --- /dev/null +++ b/examples/templates/terraform/vm.tfvars.tmpl @@ -0,0 +1,28 @@ + +aws_region = "{{.Get "aws.s3.region"}}" +pm_api_url = "{{.Get "proxmox.api_url"}}" +pm_user = "{{.Env "PM_USER"}}" +pm_password = "{{.Env "PM_PASSWORD"}}" +vm_roles = { + master = {{.Get "proxmox.vm.roles.master"}} + worker = {{.Get "proxmox.vm.roles.worker"}} + loadbalancer = {{.Get "proxmox.vm.roles.loadbalancer"}} +} +vm_template = {{.Get "proxmox.vm.template"}} +vm_cpu = {{.Get "proxmox.vm.cpu"}} +vm_memory = {{.Get "proxmox.vm.memory"}} +vm_disk_size = "{{.Get "proxmox.vm.disk_size"}}" +vm_storage = "{{.Get "proxmox.vm.storage"}}" +vm_network_bridge = "{{.Get "proxmox.vm.network_bridge"}}" +vm_network_model = "{{.Get "proxmox.vm.network_model"}}" +proxmox_nodes = {{ .GetKeysAsArray "proxmox.nodes" }} +ssh_public_key_content = "/Users/janpreetsingh/.ssh/id_rsa.pub" +ssh_private_key = "/Users/janpreetsingh/.ssh/id_rsa" +ssh_user = "{{.Get "proxmox.vm.ssh_user"}}" +cloud_init_user_data_file = "templates/cloud_init_user_data.yaml" +k8s_master_setup_script = "scripts/k8s_master_setup.sh" +k8s_worker_setup_script = "scripts/k8s_worker_setup.sh" +haproxy_setup_script = "scripts/haproxy_setup.sh" +haproxy_config_file = "templates/haproxy.cfg" +s3_bucket = "{{.Get "aws.s3.bucket"}}" +s3_key = "{{.Get "aws.s3.key"}}" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b98e0f6 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module github.com/janpreet/kado + +go 1.22.5 + +require ( + github.com/janpreet/kado-ai v1.0.1 + github.com/open-policy-agent/opa v0.66.0 + github.com/spf13/afero v1.11.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/stretchr/testify v1.9.0 // indirect + github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f974602 --- /dev/null +++ b/go.sum @@ -0,0 +1,159 @@ +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= +github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg= +github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= +github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/janpreet/kado-ai v1.0.1 h1:ZpQcuqrsBF5hSSLifB7cDXJWEaLEYxG4IkqMYG/wKAc= +github.com/janpreet/kado-ai v1.0.1/go.mod h1:7a6TYlmyDw0Yg1Sah55HTXmioTLTDkP2j+DUEM/q50o= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/open-policy-agent/opa v0.66.0 h1:DbrvfJQja0FBRcPOB3Z/BOckocN+M4ApNWyNhSRJt0w= +github.com/open-policy-agent/opa v0.66.0/go.mod h1:EIgNnJcol7AvQR/IcWLwL13k64gHVbNAVG46b2G+/EY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= +github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ca1fd2 --- /dev/null +++ b/main.go @@ -0,0 +1,253 @@ +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/janpreet/kado/packages/bead" + "github.com/janpreet/kado/packages/config" + "github.com/janpreet/kado/packages/display" + "github.com/janpreet/kado/packages/engine" + "github.com/janpreet/kado/packages/helper" + "github.com/janpreet/kado/packages/opa" + "github.com/janpreet/kado/packages/render" + "github.com/janpreet/kado/packages/terraform" +) + +func convertYAMLToSlice(yamlData map[string]interface{}) []map[string]interface{} { + result := []map[string]interface{}{yamlData} + return result +} + +func applyRelayOverrides(b *bead.Bead) map[string]string { + overrides := make(map[string]string) + if relayField, ok := b.Fields["relay_field"]; ok { + pairs := strings.Split(relayField, ",") + for _, pair := range pairs { + keyValue := strings.SplitN(pair, "=", 2) + if len(keyValue) == 2 { + overrides[strings.TrimSpace(keyValue[0])] = strings.TrimSpace(keyValue[1]) + } + } + } + return overrides +} + +func processBead(b bead.Bead, yamlData map[string]interface{}, beadMap map[string]bead.Bead, processed map[string]int, processedBeads *[]string, applyPlan bool, originBead string, relayToOPA bool) error { + if count, ok := processed[b.Name]; ok && count > 0 && originBead == "" { + return nil + } + + fmt.Printf("Processing bead: %s\n", b.Name) + + if originBead != "" { + repoPath := filepath.Join(config.LandingZone, b.Name) + if helper.FileExists(repoPath) { + fmt.Printf("Removing existing repository at: %s\n", repoPath) + err := os.RemoveAll(repoPath) + if err != nil { + return fmt.Errorf("failed to remove existing repository for bead %s: %v", b.Name, err) + } + } + } + + if source, ok := b.Fields["source"]; ok && source != "" { + refs := "" + if refsVal, ok := b.Fields["refs"]; ok { + refs = refsVal + } + err := helper.CloneRepo(source, config.LandingZone, b.Name, refs) + if err != nil { + return fmt.Errorf("failed to clone repo for bead %s: %v", b.Name, err) + } + } + + display.DisplayBead(b) + + if b.Name == "ansible" { + fmt.Println("Processing Ansible templates...") + err := render.ProcessTemplates("templates/ansible", yamlData) + if err != nil { + return fmt.Errorf("failed to process Ansible templates: %v", err) + } + + if relayToOPA { + fmt.Println("Ansible bead is relayed to OPA for evaluation.") + } + + if playbook, ok := b.Fields["playbook"]; ok && playbook != "" { + playbookPath := filepath.Join(config.LandingZone, b.Name, playbook) + inventoryPath := b.Fields["inventory"] + if inventoryPath == "" { + inventoryPath = filepath.Join(config.LandingZone, "inventory.ini") + } + extraVarsFile := false + if extraVarsFileFlag, ok := b.Fields["extra_vars_file"]; ok && extraVarsFileFlag == "true" { + extraVarsFile = true + } + fmt.Printf("Running Ansible playbook: %s with inventory: %s\n", playbookPath, inventoryPath) + if !helper.FileExists(playbookPath) { + return fmt.Errorf("playbook file does not exist: %s", playbookPath) + } + if !relayToOPA || (relayToOPA && applyPlan) { + err := engine.HandleAnsible(b, convertYAMLToSlice(yamlData), extraVarsFile) + if err != nil { + return fmt.Errorf("failed to run Ansible: %v", err) + } + } else { + fmt.Println("Skipping Ansible playbook apply due to OPA evaluation or missing 'set' flag.") + } + } + } + + if b.Name == "terraform" { + fmt.Println("Processing Terraform templates...") + err := render.ProcessTemplates("templates/terraform", yamlData) + if err != nil { + return fmt.Errorf("failed to process Terraform templates: %v", err) + } + + fmt.Println("Running Terraform plan...") + err = terraform.HandleTerraform(b, config.LandingZone, applyPlan) + if err != nil { + return fmt.Errorf("failed to run Terraform: %v", err) + } + } + + if b.Name == "opa" { + fmt.Println("Processing OPA validation...") + err := opa.HandleOPA(b, config.LandingZone, applyPlan, originBead) + if err != nil { + return fmt.Errorf("failed to process OPA: %v", err) + } + } + + *processedBeads = append(*processedBeads, b.Name) + processed[b.Name]++ + + if relay, ok := b.Fields["relay"]; ok { + if relayBead, ok := beadMap[relay]; ok { + + overrides := applyRelayOverrides(&b) + for key, value := range overrides { + relayBead.Fields[key] = value + } + return processBead(relayBead, yamlData, beadMap, processed, processedBeads, applyPlan, b.Name, b.Name == "opa") + } + } + + return nil +} + +func main() { + + if len(os.Args) > 1 && os.Args[1] == "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) + } + + 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) + 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 + } + + if len(os.Args) > 1 && os.Args[1] == "ai" { + engine.RunAI() + return + } + + fmt.Println("Starting processing-") + + applyPlan := len(os.Args) > 1 && os.Args[1] == "set" + + 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...) + } + + yamlData, err := config.LoadYAMLConfig("cluster.yaml") + if err != nil { + log.Fatalf("Failed to load YAML config: %v", err) + } + + err = helper.SetupLandingZone() + if err != nil { + log.Fatalf("Failed to setup LandingZone: %v", err) + } + + var invalidBeadNames []string + var processedBeads []string + + validBeads, invalidBeadReasons := config.GetValidBeadsWithDefaultEnabled(beads) + + beadMap := make(map[string]bead.Bead) + for _, b := range validBeads { + beadMap[b.Name] = b + } + + processed := make(map[string]int) + + for _, b := range validBeads { + if err := processBead(b, yamlData, beadMap, processed, &processedBeads, applyPlan, "", false); err != nil { + log.Fatalf("Failed to process bead %s: %v", b.Name, err) + } + } + + for beadIndex, reason := range invalidBeadReasons { + beadName := fmt.Sprintf("bead_%d", beadIndex) + fmt.Printf("Skipping bead: %s, Reason: %s\n", beadName, reason) + invalidBeadNames = append(invalidBeadNames, fmt.Sprintf("%s: %s", beadName, reason)) + } + + if len(processedBeads) > 0 { + fmt.Println("\nProcessed beads:") + for _, name := range processedBeads { + fmt.Printf(" - %s\n", name) + } + } + + if len(invalidBeadNames) > 0 { + fmt.Println("\nSkipped beads:") + for _, name := range invalidBeadNames { + fmt.Printf(" - %s\n", name) + } + } +} diff --git a/packages/ansible/ansible.go b/packages/ansible/ansible.go new file mode 100644 index 0000000..71503b0 --- /dev/null +++ b/packages/ansible/ansible.go @@ -0,0 +1,29 @@ +package ansible + +import ( + "fmt" + "os/exec" + "strings" +) + +func RunPlaybook(playbookPath, inventoryPath, extraVarsPath string, dryRun bool) error { + args := []string{playbookPath} + if inventoryPath != "" { + args = append(args, "-i", inventoryPath) + } + args = append(args, "--extra-vars", fmt.Sprintf("@%s", extraVarsPath)) + if dryRun { + args = append(args, "--check") + } + + cmd := exec.Command("ansible-playbook", args...) + fmt.Printf("Running Ansible command: ansible-playbook %s\n", strings.Join(args, " ")) + + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to run ansible playbook: %v, output: %s", err, string(output)) + } + + fmt.Printf("Ansible playbook completed. Output:\n%s\n", string(output)) + return nil +} diff --git a/packages/bead/bead.go b/packages/bead/bead.go new file mode 100644 index 0000000..bad0191 --- /dev/null +++ b/packages/bead/bead.go @@ -0,0 +1,7 @@ +package bead + +type Bead struct { + Name string `yaml:"name"` + Enabled *bool `yaml:"enabled"` + Fields map[string]string `yaml:"fields"` +} diff --git a/packages/config/beadconfig.go b/packages/config/beadconfig.go new file mode 100644 index 0000000..50902a0 --- /dev/null +++ b/packages/config/beadconfig.go @@ -0,0 +1,15 @@ +package config + +var ValidBeadNames = map[string]bool{ + "ansible": true, + "terraform": true, + "opa": true, +} + +func GetValidBeads() map[string]struct{} { + return map[string]struct{}{ + "ansible": {}, + "terraform": {}, + "opa": {}, + } +} diff --git a/packages/config/config.go b/packages/config/config.go new file mode 100644 index 0000000..2905beb --- /dev/null +++ b/packages/config/config.go @@ -0,0 +1,105 @@ +package config + +import ( + "bufio" + "fmt" + "github.com/janpreet/kado/packages/bead" + "gopkg.in/yaml.v3" + "os" + "strings" +) + +type YAMLConfig map[string]interface{} + +var LandingZone = "LandingZone" + +const Version = "1.0.0" + +func LoadBeadsConfig(filename string) ([]bead.Bead, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var beads []bead.Bead + scanner := bufio.NewScanner(file) + var currentBead *bead.Bead + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "bead \"") { + if currentBead != nil { + beads = append(beads, *currentBead) + } + currentBead = &bead.Bead{ + Fields: make(map[string]string), + } + currentBead.Name = strings.Trim(line[6:], "\" {") + } else if currentBead != nil { + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.Trim(strings.TrimSpace(parts[1]), "\"") + if key == "enabled" { + enabled := value == "true" + currentBead.Enabled = &enabled + } else { + currentBead.Fields[key] = value + } + } + } + if currentBead != nil { + beads = append(beads, *currentBead) + } + if err := scanner.Err(); err != nil { + return nil, err + } + + return beads, nil +} + +func LoadYAMLConfig(filename string) (map[string]interface{}, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var config map[string]interface{} + err = yaml.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + return config, nil +} + +func GetValidBeadNames() map[string]struct{} { + return GetValidBeads() +} + +func GetValidBeadsWithDefaultEnabled(beads []bead.Bead) ([]bead.Bead, []string) { + var validBeads []bead.Bead + var invalidBeadReasons []string + + for _, b := range beads { + if _, ok := ValidBeadNames[b.Name]; !ok { + invalidBeadReasons = append(invalidBeadReasons, fmt.Sprintf("%s (invalid name)", b.Name)) + continue + } + + if b.Enabled == nil { + validBeads = append(validBeads, b) + } else if !*b.Enabled { + invalidBeadReasons = append(invalidBeadReasons, fmt.Sprintf("%s (disabled)", b.Name)) + } else { + validBeads = append(validBeads, b) + } + } + + return validBeads, invalidBeadReasons +} diff --git a/packages/config/yamlconfig.go b/packages/config/yamlconfig.go new file mode 100644 index 0000000..be834b5 --- /dev/null +++ b/packages/config/yamlconfig.go @@ -0,0 +1,40 @@ +package config + +import ( + "os" + "path/filepath" +) + +func GetYAMLFiles(dir string) ([]string, error) { + var yamlFiles []string + + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, file := range files { + if filepath.Ext(file.Name()) == ".yaml" { + yamlFiles = append(yamlFiles, filepath.Join(dir, file.Name())) + } + } + + return yamlFiles, nil +} + +func GetKdFiles(dir string) ([]string, error) { + var kdFiles []string + + files, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, file := range files { + if filepath.Ext(file.Name()) == ".kd" { + kdFiles = append(kdFiles, filepath.Join(dir, file.Name())) + } + } + + return kdFiles, nil +} diff --git a/packages/display/display.go b/packages/display/display.go new file mode 100644 index 0000000..daddc8d --- /dev/null +++ b/packages/display/display.go @@ -0,0 +1,83 @@ +package display + +import ( + "fmt" + "github.com/janpreet/kado/packages/bead" +) + +func DisplayBeads(kdBeads map[string]bead.Bead, parsedYAMLs []map[string]interface{}) { + for _, b := range kdBeads { + fmt.Printf("Bead: %s\n", b.Name) + for k, v := range b.Fields { + fmt.Printf(" %s = %s\n", k, v) + } + if b.Name == "ansible" { + fmt.Printf("Bead details - Name: %s, Playbook: %s, Inventory: %s, Source: %s, ExtraVarsFile: %s\n", b.Name, b.Fields["playbook"], b.Fields["inventory"], b.Fields["source"], "LandingZone/extra_vars.yaml") + } + } +} + +func DisplayYAMLs(parsedYAMLs []map[string]interface{}) { + for _, yamlContent := range parsedYAMLs { + DisplayYAML(yamlContent) + } +} + +func DisplayYAML(yamlContent map[string]interface{}) { + fmt.Println("YAML Content:") + for key, value := range yamlContent { + fmt.Printf(" %s = %v\n", key, value) + } +} + +func DisplayTemplateOutput(outputPath string) { + fmt.Printf("Template processed successfully. Output written to: %s\n", outputPath) +} + +func DisplayBead(b bead.Bead) { + fmt.Printf("Bead: %s\n", b.Name) + for key, value := range b.Fields { + fmt.Printf(" %s = %s\n", key, value) + } +} + +func DisplayBeadConfig(beads []bead.Bead) { + fmt.Println("Bead Configuration and Order of Execution:") + + displayed := make(map[string]bool) + + var displayBeadChain func(string) + displayBeadChain = func(name string) { + if displayed[name] { + return + } + for _, b := range beads { + if b.Name == name { + if displayed[name] { + continue + } + if len(displayed) > 0 { + fmt.Println("↓") + } + fmt.Printf("Bead: %s\n", b.Name) + for key, value := range b.Fields { + fmt.Printf(" %s = %s\n", key, value) + } + displayed[name] = true + if relay, ok := b.Fields["relay"]; ok { + displayBeadChain(relay) + } + break + } + } + } + + for _, b := range beads { + if !displayed[b.Name] { + if len(displayed) > 0 { + fmt.Println() + } + displayBeadChain(b.Name) + } + } +} diff --git a/packages/engine/ai.go b/packages/engine/ai.go new file mode 100644 index 0000000..eb46b42 --- /dev/null +++ b/packages/engine/ai.go @@ -0,0 +1,28 @@ +package engine + +import ( + "fmt" + "log" + + kadoai "github.com/janpreet/kado-ai/ai" + "github.com/janpreet/kado/packages/config" +) + +func RunAI() { + client, err := kadoai.NewAIClient(config.LandingZone, "") + if err != nil { + log.Fatalf("Error creating AI client: %v", err) + } + + recommendations, err := client.RunAI() + if err != nil { + if err.Error() == "operation cancelled by user" { + fmt.Println("AI analysis cancelled.") + return + } + log.Fatalf("Error running AI: %v", err) + } + + fmt.Println("Infrastructure Recommendations:") + fmt.Println(recommendations) +} \ No newline at end of file diff --git a/packages/engine/engine.go b/packages/engine/engine.go new file mode 100644 index 0000000..5e6178a --- /dev/null +++ b/packages/engine/engine.go @@ -0,0 +1,52 @@ +package engine + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/janpreet/kado/packages/bead" + "github.com/janpreet/kado/packages/config" + "github.com/janpreet/kado/packages/render" +) + +func isDryRun() bool { + if len(os.Args) > 1 && os.Args[1] == "set" { + return false + } + return true +} + +func HandleAnsible(b bead.Bead, yamlData []map[string]interface{}, extraVarsFile bool) error { + dryRun := isDryRun() + + playbook := b.Fields["playbook"] + inventory := b.Fields["inventory"] + if inventory == "" { + inventory = filepath.Join(config.LandingZone, "inventory.ini") + } + + args := []string{"-i", inventory} + if extraVarsFile { + extraVarsPath, err := render.WriteExtraVarsFile(yamlData, "yaml") + if err != nil { + return fmt.Errorf("failed to write extra vars file: %w", err) + } + args = append(args, "--extra-vars", "@"+extraVarsPath) + } + if dryRun { + args = append(args, "--check") + } + args = append(args, filepath.Join(config.LandingZone, b.Name, playbook)) + + cmd := exec.Command("ansible-playbook", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to run ansible playbook: %w", err) + } + + return nil +} diff --git a/packages/engine/formatter.go b/packages/engine/formatter.go new file mode 100644 index 0000000..dfce009 --- /dev/null +++ b/packages/engine/formatter.go @@ -0,0 +1,97 @@ +package engine + +import ( + "bufio" + "bytes" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" +) + +// FormatKDFile formats a single .kd file with proper indentation and without extra newlines at the end. +func FormatKDFile(filePath string) error { + file, err := os.Open(filePath) + if err != nil { + return err + } + defer file.Close() + + var formattedLines []string + scanner := bufio.NewScanner(file) + var buffer bytes.Buffer + insideBead := false + commentRegex := regexp.MustCompile(`^\s*#`) + + for scanner.Scan() { + line := scanner.Text() + trimmedLine := strings.TrimSpace(line) + + if strings.HasPrefix(trimmedLine, "bead ") && strings.HasSuffix(trimmedLine, "{") { + if insideBead { + buffer.WriteString("}\n") + formattedLines = append(formattedLines, buffer.String()) + buffer.Reset() + } + insideBead = true + buffer.WriteString(trimmedLine + "\n") + } else if trimmedLine == "}" && insideBead { + buffer.WriteString(trimmedLine + "\n") + formattedLines = append(formattedLines, buffer.String()) + buffer.Reset() + insideBead = false + } else if insideBead { + buffer.WriteString(" " + trimmedLine + "\n") + } else { + if commentRegex.MatchString(trimmedLine) { + // Remove extra spaces in comments + trimmedLine = strings.TrimSpace(trimmedLine) + trimmedLine = strings.Replace(trimmedLine, "#", "#", 1) + } + if trimmedLine != "" || (len(formattedLines) > 0 && formattedLines[len(formattedLines)-1] != "\n") { + formattedLines = append(formattedLines, trimmedLine+"\n") + } + } + } + + if err := scanner.Err(); err != nil { + return err + } + + if insideBead { + buffer.WriteString("}\n") + formattedLines = append(formattedLines, buffer.String()) + } + + // Remove the last newline if it exists + if len(formattedLines) > 0 && formattedLines[len(formattedLines)-1] == "\n" { + formattedLines = formattedLines[:len(formattedLines)-1] + } + + formattedContent := strings.Join(formattedLines, "") + + err = os.WriteFile(filePath, []byte(formattedContent), 0644) + if err != nil { + return err + } + + return nil +} + +// FormatKDFilesInDir formats all .kd files in a given directory with proper indentation and without extra newlines at the end. +func FormatKDFilesInDir(dir string) error { + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(info.Name(), ".kd") { + err := FormatKDFile(path) + if err != nil { + return fmt.Errorf("failed to format %s: %v", path, err) + } + fmt.Printf("Formatted: %s\n", path) + } + return nil + }) +} diff --git a/packages/helper/gitclone.go b/packages/helper/gitclone.go new file mode 100644 index 0000000..d56f92b --- /dev/null +++ b/packages/helper/gitclone.go @@ -0,0 +1,40 @@ +package helper + +import ( + "log" + "os" + "os/exec" + "path/filepath" +) + +func CloneRepo(source, destination, beadName, refs string) error { + + beadDir := filepath.Join(destination, beadName) + if !FileExists(beadDir) { + err := os.MkdirAll(beadDir, os.ModePerm) + if err != nil { + return err + } + } + + cmd := exec.Command("git", "clone", source, beadDir) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return err + } + + if refs != "" { + cmd = exec.Command("git", "-C", beadDir, "checkout", refs) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return err + } + } + + log.Printf("Repository for %s cloned to: %s", beadName, beadDir) + return nil +} diff --git a/packages/helper/helper.go b/packages/helper/helper.go new file mode 100644 index 0000000..196d43e --- /dev/null +++ b/packages/helper/helper.go @@ -0,0 +1,33 @@ +package helper + +import ( + "fmt" + "os" + + "github.com/janpreet/kado/packages/config" +) + +func SetupLandingZone() error { + landingZone := config.LandingZone + if _, err := os.Stat(landingZone); os.IsNotExist(err) { + err := os.MkdirAll(landingZone, 0755) + if err != nil { + return fmt.Errorf("failed to create landing zone: %v", err) + } + } else { + err := os.RemoveAll(landingZone) + if err != nil { + return fmt.Errorf("failed to clean landing zone: %v", err) + } + err = os.MkdirAll(landingZone, 0755) + if err != nil { + return fmt.Errorf("failed to recreate landing zone: %v", err) + } + } + return nil +} + +func FileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} diff --git a/packages/opa/opa.go b/packages/opa/opa.go new file mode 100644 index 0000000..e32ecbc --- /dev/null +++ b/packages/opa/opa.go @@ -0,0 +1,137 @@ +package opa + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/janpreet/kado/packages/bead" + "github.com/janpreet/kado/packages/engine" + "github.com/janpreet/kado/packages/terraform" + "github.com/open-policy-agent/opa/rego" + "gopkg.in/yaml.v3" +) + +func HandleOPA(b bead.Bead, landingZone string, applyPlan bool, originBead string) error { + fmt.Printf("Processing OPA bead:\n") + for key, val := range b.Fields { + fmt.Printf(" %s = %s\n", key, val) + } + + inputPath, ok := b.Fields["input"] + if !ok { + return fmt.Errorf("input path not specified in bead") + } + fullInputPath := filepath.Join(landingZone, inputPath) + fmt.Printf("Reading input file from path: %s\n", fullInputPath) + inputData, err := os.ReadFile(fullInputPath) + if err != nil { + return fmt.Errorf("failed to read input file: %v", err) + } + + var input interface{} + if filepath.Ext(fullInputPath) == ".yaml" || filepath.Ext(fullInputPath) == ".yml" { + if err := yaml.Unmarshal(inputData, &input); err != nil { + return fmt.Errorf("failed to unmarshal YAML input file: %v", err) + } + } else { + if err := json.Unmarshal(inputData, &input); err != nil { + return fmt.Errorf("failed to unmarshal JSON input file: %v", err) + } + } + + policyPath, ok := b.Fields["path"] + if !ok { + return fmt.Errorf("policy path not specified in bead") + } + fullPolicyPath := filepath.Join(landingZone, policyPath) + fmt.Printf("Reading policy file from path: %s\n", fullPolicyPath) + policyData, err := os.ReadFile(fullPolicyPath) + if err != nil { + return fmt.Errorf("failed to read policy file: %v", err) + } + + packageQuery := "data.terraform.allow" + if pkg, ok := b.Fields["package"]; ok { + packageQuery = pkg + } + fmt.Printf("Evaluating package: %s\n", packageQuery) + + ctx := context.Background() + query, err := rego.New( + rego.Query(packageQuery), + rego.Module("policy.rego", string(policyData)), + ).PrepareForEval(ctx) + if err != nil { + return fmt.Errorf("failed to prepare rego query: %v", err) + } + + results, err := query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + return fmt.Errorf("failed to evaluate rego query: %v", err) + } + + if len(results) == 0 || len(results[0].Expressions) == 0 || results[0].Expressions[0].Value != true { + fmt.Println("Input is denied by OPA policy.") + if applyPlan { + fmt.Println("Skipping action because the input was denied.") + } + } else { + fmt.Println("Input is allowed by OPA policy.") + if applyPlan { + switch originBead { + case "terraform": + fmt.Println("Applying terraform plan...") + err = terraform.HandleTerraform(b, landingZone, true) + if err != nil { + return fmt.Errorf("failed to apply terraform plan: %v", err) + } + case "ansible": + fmt.Println("Applying ansible playbook...") + err = handleAnsibleRelay(b, landingZone) + if err != nil { + return fmt.Errorf("failed to run Ansible: %v", err) + } + default: + fmt.Println("Skipping apply action because origin bead is not terraform or ansible.") + } + } else { + fmt.Println("Skipping apply action because 'set' was not passed.") + } + } + + return nil +} + +func convertYAMLToSlice(yamlData map[string]interface{}) []map[string]interface{} { + result := []map[string]interface{}{yamlData} + return result +} + +func handleAnsibleRelay(b bead.Bead, landingZone string) error { + + yamlPath := filepath.Join(landingZone, "ansible", "cluster.yaml") + yamlData, err := os.ReadFile(yamlPath) + if err != nil { + return fmt.Errorf("failed to read YAML config: %v", err) + } + + var yamlContent map[string]interface{} + if err := yaml.Unmarshal(yamlData, &yamlContent); err != nil { + return fmt.Errorf("failed to unmarshal YAML config: %v", err) + } + + extraVarsFile := false + if extraVarsFileFlag, ok := b.Fields["extra_vars_file"]; ok && extraVarsFileFlag == "true" { + extraVarsFile = true + } + + err = engine.HandleAnsible(b, convertYAMLToSlice(yamlContent), extraVarsFile) + if err != nil { + return fmt.Errorf("failed to run Ansible: %v", err) + } + + return nil +} diff --git a/packages/render/driver.go b/packages/render/driver.go new file mode 100644 index 0000000..fb1c818 --- /dev/null +++ b/packages/render/driver.go @@ -0,0 +1,157 @@ +package render + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/janpreet/kado/packages/config" +) + +func join(data map[string]interface{}, key, delimiter string) string { + var result []string + for i := 0; ; i++ { + arrayKey := fmt.Sprintf("%s[%d]", key, i) + if val, ok := data[arrayKey]; ok { + result = append(result, fmt.Sprintf("%v", val)) + } else { + break + } + } + return strings.Join(result, delimiter) +} + +type FlattenedDataMap struct { + Data map[string]interface{} +} + +func (f FlattenedDataMap) Get(key string) interface{} { + if val, ok := f.Data[key]; ok { + return val + } + return "" +} + +func (f FlattenedDataMap) Env(key string) string { + return os.Getenv(key) +} + +func (f FlattenedDataMap) GetKeysAsArray(key string) string { + keys := strings.Split(key, ".") + var nestedMap map[string]interface{} + + for k, v := range f.Data { + + if strings.HasPrefix(k, key) && strings.Count(k, ".") == len(keys) { + if nestedMap == nil { + nestedMap = make(map[string]interface{}) + } + parts := strings.Split(k, ".") + lastPart := parts[len(parts)-1] + nestedMap[lastPart] = v + } + } + + if nestedMap != nil { + keysArray := make([]string, 0, len(nestedMap)) + for k := range nestedMap { + + strippedKey := strings.Split(k, "[")[0] + if !contains(keysArray, strippedKey) { + keysArray = append(keysArray, fmt.Sprintf("\"%s\"", strippedKey)) + } + } + return fmt.Sprintf("[%s]", strings.Join(keysArray, ", ")) + } + + return "[]" +} + +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +func ProcessTemplate(templatePath string, data map[string]interface{}) (string, error) { + content, err := os.ReadFile(templatePath) + if err != nil { + return "", fmt.Errorf("failed to read template file: %v", err) + } + + lines := strings.Split(string(content), "\n") + var filteredLines []string + for _, line := range lines { + if strings.TrimSpace(line) == "" || strings.HasPrefix(strings.TrimSpace(line), "#") { + filteredLines = append(filteredLines, line) + continue + } + filteredLines = append(filteredLines, line) + } + + firstLine, templateContent := filteredLines[0], strings.Join(filteredLines[1:], "\n") + if !strings.HasPrefix(firstLine, "<") || !strings.HasSuffix(firstLine, ">") { + return "", fmt.Errorf("invalid file name format in template: %s", firstLine) + } + fileName := strings.Trim(firstLine, "<>") + + flatData := FlattenYAML("", data) + + funcMap := template.FuncMap{ + "join": func(key, delimiter string) string { + return join(flatData, key, delimiter) + }, + "Get": func(key string) interface{} { + return FlattenedDataMap{Data: flatData}.Get(key) + }, + "Env": func(key string) string { + return FlattenedDataMap{Data: flatData}.Env(key) + }, + "GetKeysAsArray": func(key string) string { + return FlattenedDataMap{Data: flatData}.GetKeysAsArray(key) + }, + } + + tmpl, err := template.New(filepath.Base(templatePath)).Funcs(funcMap).Parse(templateContent) + 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 { + return "", fmt.Errorf("failed to execute template: %v", err) + } + + outputPath := filepath.Join(config.LandingZone, fileName) + err = WriteToFile(outputPath, output.Bytes()) + if err != nil { + return "", fmt.Errorf("failed to write output file: %v", err) + } + + return outputPath, nil +} + +func ProcessTemplates(templateDir string, data map[string]interface{}) error { + files, err := os.ReadDir(templateDir) + if err != nil { + return fmt.Errorf("failed to read template directory: %v", err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + filePath := filepath.Join(templateDir, file.Name()) + _, err := ProcessTemplate(filePath, data) + if err != nil { + return fmt.Errorf("failed to process template %s: %v", file.Name(), err) + } + } + return nil +} diff --git a/packages/render/kd.go b/packages/render/kd.go new file mode 100644 index 0000000..c72efb2 --- /dev/null +++ b/packages/render/kd.go @@ -0,0 +1,120 @@ +package render + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/janpreet/kado/packages/bead" + "github.com/janpreet/kado/packages/config" +) + +func ProcessKdFiles(files []string) (map[string]bead.Bead, []string, error) { + validBeads := config.GetValidBeads() + kdBeads := make(map[string]bead.Bead) + var invalidKdBeadNames []string + + for _, file := range files { + beads, invalidBeadNames, err := parseKdFile(file) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse kd file %s: %v", file, err) + } + invalidKdBeadNames = append(invalidKdBeadNames, invalidBeadNames...) + + for _, b := range beads { + + if enabled, ok := b.Fields["enabled"]; ok && enabled == "false" { + fmt.Printf("Skipping bead %s because it is disabled\n", b.Name) + continue + } + if _, ok := validBeads[b.Name]; !ok { + invalidKdBeadNames = append(invalidKdBeadNames, b.Name) + } else { + kdBeads[b.Name] = b + } + } + } + + return kdBeads, invalidKdBeadNames, nil +} + +func parseKdFile(filePath string) ([]bead.Bead, []string, error) { + var beads []bead.Bead + var invalidBeadNames []string + + file, err := os.Open(filePath) + if err != nil { + return nil, nil, fmt.Errorf("failed to open file %s: %v", filePath, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var currentBead bead.Bead + var inBead bool + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + if strings.HasPrefix(line, "bead ") { + if inBead { + beads = append(beads, currentBead) + } + inBead = true + currentBead = bead.Bead{Fields: make(map[string]string)} + matches := regexp.MustCompile(`bead "([^"]+)"`).FindStringSubmatch(line) + if len(matches) > 1 { + currentBead.Name = matches[1] + } else { + invalidBeadNames = append(invalidBeadNames, line) + } + } else if inBead { + + if line == "}" { + beads = append(beads, currentBead) + inBead = false + } else { + + parts := strings.SplitN(line, "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.Trim(strings.TrimSpace(parts[1]), `"`) + currentBead.Fields[key] = value + } + } + } + } + + if inBead { + beads = append(beads, currentBead) + } + + if err := scanner.Err(); err != nil { + return nil, nil, fmt.Errorf("error reading file %s: %v", filePath, err) + } + + return beads, invalidBeadNames, nil +} + +func GetKDFiles(dir string) ([]string, error) { + var kdFiles []string + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && filepath.Ext(path) == ".kd" { + kdFiles = append(kdFiles, path) + } + return nil + }) + if err != nil { + return nil, err + } + return kdFiles, nil +} diff --git a/packages/render/writer.go b/packages/render/writer.go new file mode 100644 index 0000000..68d3c01 --- /dev/null +++ b/packages/render/writer.go @@ -0,0 +1,57 @@ +package render + +import ( + "fmt" + "github.com/janpreet/kado/packages/config" + "os" + "path/filepath" +) + +func WriteToFile(filePath string, data []byte) error { + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + if _, err := file.Write(data); err != nil { + return err + } + + return nil +} + +func WriteExtraVarsFile(parsedYAMLs []map[string]interface{}, format string) (string, error) { + var fileName string + switch format { + case "yaml": + fileName = "extra_vars.yaml" + case "tfvars": + fileName = "extra_vars.tfvars" + default: + return "", fmt.Errorf("unsupported format: %s", format) + } + + filePath := filepath.Join(config.LandingZone, fileName) + file, err := os.Create(filePath) + if err != nil { + return "", fmt.Errorf("failed to create extra vars file: %v", err) + } + defer file.Close() + + for _, yamlData := range parsedYAMLs { + for key, value := range yamlData { + line := fmt.Sprintf("%s = %v\n", key, value) + if _, err := file.WriteString(line); err != nil { + return "", fmt.Errorf("failed to write to extra vars file: %v", err) + } + } + } + + return filePath, nil +} diff --git a/packages/render/yaml.go b/packages/render/yaml.go new file mode 100644 index 0000000..67d19e4 --- /dev/null +++ b/packages/render/yaml.go @@ -0,0 +1,62 @@ +package render + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +func ProcessYAMLFiles(files []string) ([]map[string]interface{}, error) { + var parsedYAMLs []map[string]interface{} + for _, file := range files { + content, err := ProcessYAMLFile(file) + if err != nil { + return nil, fmt.Errorf("failed to process YAML file %s: %v", file, err) + } + parsedYAMLs = append(parsedYAMLs, content) + } + return parsedYAMLs, nil +} + +func ProcessYAMLFile(filePath string) (map[string]interface{}, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read YAML file: %v", err) + } + + var content map[string]interface{} + if err := yaml.Unmarshal(data, &content); err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML content: %v", err) + } + + flatContent := FlattenYAML("", content) + + return flatContent, nil +} + +func FlattenYAML(prefix string, content interface{}) map[string]interface{} { + flatMap := make(map[string]interface{}) + + switch content := content.(type) { + case map[string]interface{}: + for key, value := range content { + flatKey := key + if prefix != "" { + flatKey = prefix + "." + key + } + for k, v := range FlattenYAML(flatKey, value) { + flatMap[k] = v + } + } + case []interface{}: + for i, value := range content { + flatKey := fmt.Sprintf("%s[%d]", prefix, i) + flatMap[flatKey] = value + } + default: + flatMap[prefix] = content + } + + return flatMap +} diff --git a/packages/terraform/terraform.go b/packages/terraform/terraform.go new file mode 100644 index 0000000..0b57574 --- /dev/null +++ b/packages/terraform/terraform.go @@ -0,0 +1,150 @@ +package terraform + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/janpreet/kado/packages/bead" +) + +func HandleTerraform(b bead.Bead, landingZone string, applyPlan bool) error { + fmt.Printf("Processing terraform bead:\n") + for key, val := range b.Fields { + fmt.Printf(" %s = %s\n", key, val) + } + + fmt.Println("Getting tfvars files from landing zone:", landingZone) + varFiles, err := getTfvarsFiles(landingZone) + if err != nil { + return fmt.Errorf("failed to get tfvars files: %v", err) + } + + repoPath := filepath.Join(landingZone, b.Name) + planArgs := []string{"plan", "-out=plan.out"} + for _, varFile := range varFiles { + + destPath := filepath.Join(repoPath, filepath.Base(varFile)) + err := moveFile(varFile, destPath) + if err != nil { + return fmt.Errorf("failed to move tfvars file: %v", err) + } + planArgs = append(planArgs, "--var-file", filepath.Base(varFile)) + } + + backendConfigFile := filepath.Join(landingZone, "backend.tfvars") + if fileExists(backendConfigFile) { + destBackendPath := filepath.Join(repoPath, "backend.tfvars") + err := moveFile(backendConfigFile, destBackendPath) + if err != nil { + return fmt.Errorf("failed to move backend.tfvars file: %v", err) + } + } + + initArgs := []string{"init"} + + if fileExists(filepath.Join(repoPath, "backend.tfvars")) { + initArgs = append(initArgs, "-backend-config=backend.tfvars") + } + + fmt.Println("Running terraform init...") + err = runCommand(repoPath, "terraform", initArgs...) + if err != nil { + return fmt.Errorf("failed to run terraform init: %v", err) + } + + fmt.Println("Running terraform plan...") + err = runCommand(repoPath, "terraform", planArgs...) + if err != nil { + return fmt.Errorf("failed to run terraform plan: %v", err) + } + + fmt.Println("Converting plan.out to plan.json...") + showArgs := []string{"show", "-no-color", "-json", "plan.out"} + output, err := runCommandWithOutput(repoPath, "terraform", showArgs...) + if err != nil { + return fmt.Errorf("failed to run terraform show: %v", err) + } + + planJSONPath := filepath.Join(repoPath, "plan.json") + err = os.WriteFile(planJSONPath, output, 0644) + if err != nil { + return fmt.Errorf("failed to write plan.json: %v", err) + } + + fmt.Println("Terraform plan saved as plan.json") + + if applyPlan { + + applyArgs := []string{"apply", "plan.out"} + fmt.Println("Applying terraform plan...") + err = runCommand(repoPath, "terraform", applyArgs...) + if err != nil { + return fmt.Errorf("failed to apply terraform plan: %v", err) + } + } + + return nil +} + +func runCommand(dir, name string, args ...string) error { + fmt.Printf("Executing command: %s %s in directory: %s\n", name, strings.Join(args, " "), dir) + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func runCommandWithOutput(dir, name string, args ...string) ([]byte, error) { + fmt.Printf("Executing command: %s %s in directory: %s\n", name, strings.Join(args, " "), dir) + cmd := exec.Command(name, args...) + cmd.Dir = dir + return cmd.Output() +} + +func getTfvarsFiles(directory string) ([]string, error) { + fmt.Println("Reading tfvars files from directory:", directory) + files, err := os.ReadDir(directory) + if err != nil { + return nil, fmt.Errorf("failed to read directory: %v", err) + } + + var varFiles []string + for _, file := range files { + if file.IsDir() { + continue + } + if strings.HasSuffix(file.Name(), ".tfvars") && file.Name() != "backend.tfvars" { + varFiles = append(varFiles, filepath.Join(directory, file.Name())) + } + } + fmt.Println("Found tfvars files:", varFiles) + return varFiles, nil +} + +func moveFile(src, dest string) error { + input, err := os.ReadFile(src) + if err != nil { + return err + } + err = os.WriteFile(dest, input, 0644) + if err != nil { + return err + } + err = os.Remove(src) + if err != nil { + return err + } + return nil +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if err != nil { + return false + } + return !info.IsDir() +} diff --git a/templates/ansible/inventory.tmpl b/templates/ansible/inventory.tmpl new file mode 100644 index 0000000..6f5d549 --- /dev/null +++ b/templates/ansible/inventory.tmpl @@ -0,0 +1 @@ +# Ansible inventory template \ No newline at end of file diff --git a/templates/terraform/backend.tfvars.tmpl b/templates/terraform/backend.tfvars.tmpl new file mode 100644 index 0000000..8dfba7a --- /dev/null +++ b/templates/terraform/backend.tfvars.tmpl @@ -0,0 +1 @@ +# TF backend template \ No newline at end of file diff --git a/templates/terraform/terraform.tfvars.tmpl b/templates/terraform/terraform.tfvars.tmpl new file mode 100644 index 0000000..49f7e26 --- /dev/null +++ b/templates/terraform/terraform.tfvars.tmpl @@ -0,0 +1 @@ +#TFVars template \ No newline at end of file