├── VERSION ├── docs ├── reference │ ├── api.md │ ├── crds.md │ └── index.md ├── CNAME ├── faq.md ├── assets │ ├── demo │ │ ├── ui.png │ │ ├── demo.gif │ │ ├── logs.png │ │ ├── comment.png │ │ ├── homepage.png │ │ ├── pr-demo.png │ │ ├── drift-example.png │ │ ├── vscode-debug.png │ │ ├── vscode-breakpoint.png │ │ └── vscode-debug-variables.png │ ├── icon │ │ ├── burrito.png │ │ └── burrito.txt │ ├── design │ │ ├── architecture-overview.png │ │ ├── pr-mr-workflow.excalidraw.png │ │ └── multi-tenant-architecture.excalidraw.png │ └── pr-mr-workflow │ │ ├── github_app_id.png │ │ └── github_installation_id.png ├── .markdownlint.jsonc ├── user-guide │ ├── index.md │ ├── ssh-known-hosts.md │ ├── remediation-strategy.md │ └── additionnal-trigger-path.md ├── examples │ ├── terraform-repository.yaml │ ├── terraform-layer.yaml │ └── values-simple.yaml ├── guides │ ├── index.md │ └── ui.md ├── index.md ├── installation │ └── with-static-manifests.md └── operator-manual │ ├── index.md │ ├── runner-scheduling.md │ └── git-authentication │ ├── gitlab-token.md │ └── github-token.md ├── internal ├── server │ ├── dist │ │ └── .gitkeep │ ├── api │ │ ├── api.go │ │ ├── repositories.go │ │ ├── runs.go │ │ ├── sync.go │ │ └── logs.go │ ├── utils │ │ ├── manual_sync.go │ │ ├── logger.go │ │ └── sessions.go │ └── auth │ │ └── auth.go ├── e2e │ └── testdata │ │ ├── terragrunt │ │ ├── terragrunt.hcl │ │ ├── random-pets │ │ │ ├── prod │ │ │ │ ├── inputs.hcl │ │ │ │ └── terragrunt.hcl │ │ │ └── module.hcl │ │ └── modules │ │ │ └── random-pets │ │ │ └── main.tf │ │ └── terraform │ │ └── random-pets │ │ └── main.tf ├── utils │ ├── authz │ │ └── client.go │ ├── typeutils │ │ └── typeutils.go │ ├── cmd │ │ └── cmd.go │ ├── runner │ │ ├── network_mirror.go │ │ └── plan_diff.go │ ├── k8s_client.go │ └── url │ │ ├── url.go │ │ └── url_test.go ├── webhook │ ├── webhook_test.go │ └── event │ │ ├── common.go │ │ └── testdata │ │ └── pullrequest.yaml ├── burrito │ ├── server.go │ ├── runner.go │ ├── datastore.go │ ├── controllers.go │ ├── config │ │ └── testdata │ │ │ └── test-config-1.yaml │ └── burrito.go ├── runner │ ├── testdata │ │ ├── burrito-examples.bundle │ │ └── repo.yaml │ └── tools │ │ ├── common.go │ │ ├── opentofu │ │ └── opentofu.go │ │ ├── terraform │ │ └── terraform.go │ │ └── base │ │ └── base.go ├── controllers │ ├── terraformpullrequest │ │ ├── comment │ │ │ ├── common.go │ │ │ ├── initial.go │ │ │ ├── templates │ │ │ │ └── comment.md │ │ │ ├── default_test.go │ │ │ └── default.go │ │ └── testdata │ │ │ ├── error-case.yaml │ │ │ └── repository.yaml │ ├── terraformrepository │ │ ├── testdata │ │ │ ├── credentials.yaml │ │ │ └── error-case.yaml │ │ └── polling.go │ ├── terraformrun │ │ └── testdata │ │ │ ├── controller │ │ │ ├── nominal-case.yaml │ │ │ ├── concurrent-case.yaml │ │ │ ├── parallel-case.yaml │ │ │ └── error-case.yaml │ │ │ └── pod │ │ │ ├── nominal-case.yaml │ │ │ └── repository-layers.yaml │ └── terraformlayer │ │ └── testdata │ │ ├── repository.yaml │ │ ├── runs.yaml │ │ ├── unknown-cases.yaml │ │ ├── merge-case.yaml │ │ └── webhook-issue-case.yaml ├── repository │ ├── credentials │ │ └── testdata │ │ │ ├── namespaces.yaml │ │ │ ├── secrets.yaml │ │ │ └── repository.yaml │ ├── types │ │ └── types.go │ ├── providers │ │ ├── standard │ │ │ └── standard.go │ │ ├── github │ │ │ └── api.go │ │ └── gitlab │ │ │ └── api.go │ └── repository.go ├── version │ └── version.go ├── datastore │ ├── storage │ │ ├── utils │ │ │ └── utils.go │ │ ├── error │ │ │ └── error.go │ │ └── encryption.go │ └── api │ │ └── api.go ├── annotations │ └── testdata │ │ └── layer.yaml └── lock │ ├── testdata │ └── layer.yaml │ └── lock.go ├── deploy └── charts │ └── burrito │ ├── templates │ ├── _template.tpl │ ├── ssh-known-hosts.yaml │ ├── rbac-runner.yaml │ ├── networkpolicy.yaml │ ├── issuer.yaml │ └── rbac-server.yaml │ ├── values-dev.yaml │ ├── .helmignore │ ├── values-example.yaml │ ├── values-debug.yaml │ └── Chart.yaml ├── ui ├── src │ ├── vite-env.d.ts │ ├── clients │ │ ├── logs │ │ │ ├── types.ts │ │ │ └── client.ts │ │ ├── runs │ │ │ ├── types.ts │ │ │ └── client.ts │ │ ├── repositories │ │ │ ├── types.ts │ │ │ └── client.ts │ │ ├── reactQueryConfig.ts │ │ ├── layers │ │ │ ├── client.ts │ │ │ └── types.ts │ │ └── auth │ │ │ └── client.ts │ ├── fonts │ │ └── Outfit.ttf │ ├── assets │ │ ├── covers │ │ │ ├── cover-dark.png │ │ │ └── cover-light.png │ │ ├── backgrounds │ │ │ ├── background-dark.png │ │ │ ├── background-light.png │ │ │ ├── background-login-dark.png │ │ │ └── background-login-light.png │ │ ├── icons │ │ │ ├── LoaderIcon.tsx │ │ │ ├── MultiplyIcon.tsx │ │ │ ├── WindowIcon.tsx │ │ │ ├── MinusIcon.tsx │ │ │ ├── SyncIcon.tsx │ │ │ ├── GithubIcon.tsx │ │ │ ├── AngleDownIcon.tsx │ │ │ ├── BarsIcon.tsx │ │ │ ├── ArrowResizeDiagonalIcon.tsx │ │ │ ├── ArrowLeftIcon.tsx │ │ │ ├── CheckIcon.tsx │ │ │ ├── ArrowRightIcon.tsx │ │ │ ├── GitlabIcon.tsx │ │ │ ├── SearchIcon.tsx │ │ │ ├── TimesIcon.tsx │ │ │ ├── AppsIcon.tsx │ │ │ ├── CopyIcon.tsx │ │ │ ├── EyeIcon.tsx │ │ │ ├── DownloadAltIcon.tsx │ │ │ └── ExclamationTriangleIcon.tsx │ │ └── avocado │ │ │ ├── AvocadoSeed.tsx │ │ │ └── AvocadoOff.tsx │ ├── main.tsx │ ├── components │ │ ├── widgets │ │ │ ├── Running.tsx │ │ │ ├── ProgressBar.tsx │ │ │ └── Tag.tsx │ │ ├── core │ │ │ ├── Box.tsx │ │ │ └── Dropdown.tsx │ │ ├── buttons │ │ │ ├── SSOButton.tsx │ │ │ ├── OpenInLogsButton.tsx │ │ │ ├── LogsButton.tsx │ │ │ ├── SocialButton.tsx │ │ │ ├── AttemptButton.tsx │ │ │ └── GenericIconButton.tsx │ │ ├── loaders │ │ │ ├── TableLoader.tsx │ │ │ ├── CardLoader.tsx │ │ │ └── RunCardLoader.tsx │ │ └── navigation │ │ │ ├── NavigationButton.tsx │ │ │ └── NavigationLink.tsx │ ├── layout │ │ └── Layout.tsx │ ├── pages │ │ └── Pulls.tsx │ ├── contexts │ │ └── ThemeContext.tsx │ └── App.tsx ├── .env.template ├── .dockerignore ├── public │ ├── favicon.ico │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── .prettierrc ├── default.conf ├── tsconfig.node.json ├── vite.config.ts ├── .gitignore ├── Dockerfile ├── index.html ├── eslint.config.mjs ├── tsconfig.json ├── README.md └── package.json ├── .github ├── CODEOWNERS └── workflows │ ├── conventional-commits.yaml │ ├── ci-frontend.yaml │ ├── release.yaml │ ├── docs.yaml │ ├── helm.yaml │ └── ci.yaml ├── codecov.yml ├── CONTRIBUTING.md ├── manifests ├── cluster-install │ └── kustomization.yaml ├── base │ ├── runner │ │ ├── kustomization.yaml │ │ ├── serviceaccount.yaml │ │ ├── clusterrolebinding.yaml │ │ └── clusterrole.yaml │ ├── config │ │ ├── burrito-config-cm.yaml │ │ ├── kustomization.yaml │ │ └── burrito-config-secret.yaml │ ├── controllers │ │ ├── kustomization.yaml │ │ ├── serviceaccount.yaml │ │ ├── clusterrolebinding.yaml │ │ └── deployment.yaml │ ├── server │ │ ├── kustomization.yaml │ │ ├── serviceaccount.yaml │ │ ├── service.yaml │ │ ├── clusterrolebinding.yaml │ │ ├── deployment.yaml │ │ └── clusterrole.yaml │ └── kustomization.yaml └── crds │ └── kustomization.yaml ├── .dockerignore ├── cmd ├── version.go ├── server │ ├── start.go │ └── server.go ├── datastore │ ├── start.go │ └── datastore.go ├── runner │ ├── runner.go │ └── start.go ├── controllers │ └── controller.go └── root.go ├── main.go ├── hack └── boilerplate.go.txt ├── .gitignore ├── PROJECT ├── api └── v1alpha1 │ └── groupversion_info.go └── renovate.json /VERSION: -------------------------------------------------------------------------------- 1 | v0.9.1 2 | -------------------------------------------------------------------------------- /docs/reference/api.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/reference/crds.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | docs.burrito.tf -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # TODO - FAQ 2 | -------------------------------------------------------------------------------- /internal/server/dist/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/_template.tpl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # Reference index 2 | -------------------------------------------------------------------------------- /internal/e2e/testdata/terragrunt/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/utils/authz/client.go: -------------------------------------------------------------------------------- 1 | package authz 2 | -------------------------------------------------------------------------------- /internal/webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | package webhook_test 2 | -------------------------------------------------------------------------------- /ui/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/.env.template: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL="http://localhost:8000/api" 2 | -------------------------------------------------------------------------------- /internal/e2e/testdata/terragrunt/random-pets/prod/inputs.hcl: -------------------------------------------------------------------------------- 1 | inputs = {} -------------------------------------------------------------------------------- /ui/src/clients/logs/types.ts: -------------------------------------------------------------------------------- 1 | export type Logs = { 2 | results: string[]; 3 | }; 4 | -------------------------------------------------------------------------------- /ui/src/clients/runs/types.ts: -------------------------------------------------------------------------------- 1 | export type Attempts = { 2 | count: number; 3 | }; 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @spoukke 2 | @Alan-pad 3 | @corrieriluca 4 | @LucasMrqes 5 | @DjinnS 6 | -------------------------------------------------------------------------------- /ui/.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .env 4 | .env.template 5 | .gitignore 6 | Dockerfile -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/public/favicon.ico -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "api/v1alpha1/zz_generated.deepcopy.go" 3 | - "main.go" 4 | - "cmd" 5 | -------------------------------------------------------------------------------- /docs/assets/demo/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/ui.png -------------------------------------------------------------------------------- /ui/src/fonts/Outfit.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/fonts/Outfit.ttf -------------------------------------------------------------------------------- /docs/assets/demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/demo.gif -------------------------------------------------------------------------------- /docs/assets/demo/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/logs.png -------------------------------------------------------------------------------- /docs/assets/demo/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/comment.png -------------------------------------------------------------------------------- /docs/assets/demo/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/homepage.png -------------------------------------------------------------------------------- /docs/assets/demo/pr-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/pr-demo.png -------------------------------------------------------------------------------- /docs/assets/icon/burrito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/icon/burrito.png -------------------------------------------------------------------------------- /internal/burrito/server.go: -------------------------------------------------------------------------------- 1 | package burrito 2 | 3 | func (app *App) StartServer() { 4 | app.Server.Exec() 5 | } 6 | -------------------------------------------------------------------------------- /ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/assets/demo/drift-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/drift-example.png -------------------------------------------------------------------------------- /docs/assets/demo/vscode-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/vscode-debug.png -------------------------------------------------------------------------------- /internal/e2e/testdata/terragrunt/random-pets/module.hcl: -------------------------------------------------------------------------------- 1 | terraform { 2 | source = "../../modules//random-pets" 3 | } 4 | -------------------------------------------------------------------------------- /ui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /ui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /ui/src/assets/covers/cover-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/covers/cover-dark.png -------------------------------------------------------------------------------- /ui/src/assets/covers/cover-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/covers/cover-light.png -------------------------------------------------------------------------------- /docs/assets/demo/vscode-breakpoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/vscode-breakpoint.png -------------------------------------------------------------------------------- /internal/burrito/runner.go: -------------------------------------------------------------------------------- 1 | package burrito 2 | 3 | func (app *App) StartRunner() error { 4 | return app.Runner.Exec() 5 | } 6 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 80 6 | } 7 | -------------------------------------------------------------------------------- /docs/assets/demo/vscode-debug-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/demo/vscode-debug-variables.png -------------------------------------------------------------------------------- /docs/assets/design/architecture-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/design/architecture-overview.png -------------------------------------------------------------------------------- /docs/assets/pr-mr-workflow/github_app_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/pr-mr-workflow/github_app_id.png -------------------------------------------------------------------------------- /internal/burrito/datastore.go: -------------------------------------------------------------------------------- 1 | package burrito 2 | 3 | func (app *App) StartDatastore() error { 4 | app.Datastore.Exec() 5 | return nil 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/assets/backgrounds/background-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/backgrounds/background-dark.png -------------------------------------------------------------------------------- /ui/src/assets/backgrounds/background-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/backgrounds/background-light.png -------------------------------------------------------------------------------- /docs/assets/design/pr-mr-workflow.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/design/pr-mr-workflow.excalidraw.png -------------------------------------------------------------------------------- /internal/burrito/controllers.go: -------------------------------------------------------------------------------- 1 | package burrito 2 | 3 | func (app *App) StartController() error { 4 | app.Controllers.Exec() 5 | return nil 6 | } 7 | -------------------------------------------------------------------------------- /internal/runner/testdata/burrito-examples.bundle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/internal/runner/testdata/burrito-examples.bundle -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Check out the [contributing guide](https://docs.burrito.tf/contributing/) to know how to contribute to Burrito. 4 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/comment/common.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | type Comment interface { 4 | Generate(string) (string, error) 5 | } 6 | -------------------------------------------------------------------------------- /ui/src/assets/backgrounds/background-login-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/backgrounds/background-login-dark.png -------------------------------------------------------------------------------- /ui/src/assets/backgrounds/background-login-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/ui/src/assets/backgrounds/background-login-light.png -------------------------------------------------------------------------------- /docs/assets/pr-mr-workflow/github_installation_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/pr-mr-workflow/github_installation_id.png -------------------------------------------------------------------------------- /manifests/cluster-install/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../base 6 | - ../crds 7 | -------------------------------------------------------------------------------- /ui/src/clients/repositories/types.ts: -------------------------------------------------------------------------------- 1 | export type Repositories = { 2 | results: Repository[]; 3 | }; 4 | 5 | export type Repository = { 6 | name: string; 7 | }; 8 | -------------------------------------------------------------------------------- /docs/assets/design/multi-tenant-architecture.excalidraw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/padok-team/burrito/HEAD/docs/assets/design/multi-tenant-architecture.excalidraw.png -------------------------------------------------------------------------------- /docs/assets/icon/burrito.txt: -------------------------------------------------------------------------------- 1 | The icon is taken from flaticon.com (https://www.flaticon.com/fr/icone-gratuite/burrito_5596710?term=burrito&page=1&position=5&origin=search&related_id=5596710) 2 | -------------------------------------------------------------------------------- /ui/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html =404; 8 | } 9 | } -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/comment/initial.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | type InitialComment struct { 4 | } 5 | 6 | func NewInitialComment() *InitialComment { 7 | return &InitialComment{} 8 | } 9 | -------------------------------------------------------------------------------- /manifests/base/runner/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - serviceaccount.yaml 6 | - clusterrole.yaml 7 | - clusterrolebinding.yaml 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | ui/node_modules/ 6 | ui/dist/ 7 | ui/.env 8 | ui/.env.template 9 | -------------------------------------------------------------------------------- /internal/utils/typeutils/typeutils.go: -------------------------------------------------------------------------------- 1 | package typeutils 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func ParseSecretInt64(data string) int64 { 8 | v, _ := strconv.ParseInt(data, 10, 64) 9 | return v 10 | } 11 | -------------------------------------------------------------------------------- /manifests/base/config/burrito-config-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: burrito-config 6 | app.kubernetes.io/part-of: burrito 7 | name: burrito-config 8 | -------------------------------------------------------------------------------- /docs/.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": false, 3 | "no-inline-html": false, 4 | "no-alt-text": false, 5 | "no-duplicate-heading": false, 6 | "ul-indent": false, 7 | "descriptive-link-text": false 8 | } -------------------------------------------------------------------------------- /manifests/base/config/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - burrito-config-cm.yaml 6 | - burrito-config-secret.yaml 7 | - burrito-ssh-known-hosts.yaml 8 | -------------------------------------------------------------------------------- /manifests/base/config/burrito-config-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: burrito-config 6 | app.kubernetes.io/part-of: burrito 7 | name: burrito-config 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /manifests/base/controllers/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - serviceaccount.yaml 6 | - deployment.yaml 7 | - clusterrole.yaml 8 | - clusterrolebinding.yaml 9 | -------------------------------------------------------------------------------- /docs/user-guide/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This guide is for developers who have burrito installed for them and are managing layers. 4 | 5 | !!! note 6 | Please make sure you've completed the [getting started guide](../getting-started.md). 7 | -------------------------------------------------------------------------------- /internal/e2e/testdata/terraform/random-pets/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_pet" "first" { 2 | length = 1 3 | } 4 | 5 | resource "random_pet" "second" { 6 | length = 2 7 | } 8 | 9 | resource "random_pet" "third" { 10 | length = 3 11 | } 12 | -------------------------------------------------------------------------------- /internal/e2e/testdata/terragrunt/modules/random-pets/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_pet" "first" { 2 | length = 1 3 | } 4 | 5 | resource "random_pet" "second" { 6 | length = 2 7 | } 8 | 9 | resource "random_pet" "third" { 10 | length = 3 11 | } 12 | -------------------------------------------------------------------------------- /internal/runner/tools/common.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | type BaseExec interface { 4 | Init(string) error 5 | Plan(string) error 6 | Apply(string) error 7 | Show(string, string) ([]byte, error) 8 | TenvName() string 9 | GetExecPath() string 10 | } 11 | -------------------------------------------------------------------------------- /manifests/base/server/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - serviceaccount.yaml 6 | - deployment.yaml 7 | - service.yaml 8 | - clusterrole.yaml 9 | - clusterrolebinding.yaml 10 | -------------------------------------------------------------------------------- /manifests/base/runner/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: burrito-runner 5 | labels: 6 | app.kubernetes.io/component: runner 7 | app.kubernetes.io/name: burrito-runner 8 | app.kubernetes.io/part-of: burrito 9 | -------------------------------------------------------------------------------- /manifests/base/server/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: burrito-server 5 | labels: 6 | app.kubernetes.io/component: server 7 | app.kubernetes.io/name: burrito-server 8 | app.kubernetes.io/part-of: burrito 9 | -------------------------------------------------------------------------------- /ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /manifests/base/controllers/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: burrito-controllers 5 | labels: 6 | app.kubernetes.io/component: controllers 7 | app.kubernetes.io/name: burrito-controllers 8 | app.kubernetes.io/part-of: burrito 9 | -------------------------------------------------------------------------------- /ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.tsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /internal/repository/credentials/testdata/namespaces.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: tenant-1 5 | --- 6 | apiVersion: v1 7 | kind: Namespace 8 | metadata: 9 | name: tenant-2 10 | --- 11 | apiVersion: v1 12 | kind: Namespace 13 | metadata: 14 | name: tenant-3 15 | -------------------------------------------------------------------------------- /internal/runner/testdata/repo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRepository 3 | metadata: 4 | name: burrito 5 | namespace: default 6 | spec: 7 | repository: 8 | url: https://github.com/padok-team/burrito-examples 9 | terraform: 10 | enabled: true 11 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | Version = "dev" 9 | CommitHash = "n/a" 10 | BuildTimestamp = "n/a" 11 | ) 12 | 13 | func BuildVersion() string { 14 | return fmt.Sprintf("%s-%s (%s)", Version, CommitHash, BuildTimestamp) 15 | } 16 | -------------------------------------------------------------------------------- /internal/controllers/terraformrepository/testdata/credentials.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: burrito-repo 6 | namespace: default 7 | type: credentials.burrito.tf/repository 8 | stringData: 9 | provider: mock 10 | url: https://github.com/padok-team/burrito-examples 11 | -------------------------------------------------------------------------------- /docs/examples/terraform-repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRepository 3 | metadata: 4 | name: my-repository 5 | namespace: burrito-project 6 | spec: 7 | repository: 8 | url: https://github.com/padok-team/burrito-examples 9 | terraform: 10 | enabled: true 11 | -------------------------------------------------------------------------------- /manifests/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | images: 5 | - name: ghcr.io/padok-team/burrito 6 | newName: ghcr.io/padok-team/burrito 7 | newTag: main 8 | 9 | resources: 10 | - ./controllers 11 | - ./server 12 | - ./runner 13 | - ./config 14 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/controller/nominal-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: nominal-case-1 5 | namespace: default 6 | spec: 7 | action: plan 8 | layer: 9 | name: nominal-case-1 10 | namespace: default 11 | revision: TEST_REVISION 12 | -------------------------------------------------------------------------------- /manifests/crds/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - config.terraform.padok.cloud_terraformpullrequests.yaml 6 | - config.terraform.padok.cloud_terraformrepositories.yaml 7 | - config.terraform.padok.cloud_terraformlayers.yaml 8 | - config.terraform.padok.cloud_terraformruns.yaml 9 | -------------------------------------------------------------------------------- /ui/src/clients/repositories/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { Repositories } from '@/clients/repositories/types.ts'; 4 | 5 | export const fetchRepositories = async () => { 6 | const response = await axios.get( 7 | `${import.meta.env.VITE_API_BASE_URL}/repositories` 8 | ); 9 | return response.data; 10 | }; 11 | -------------------------------------------------------------------------------- /ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [tailwindcss(), react()], 8 | resolve: { 9 | alias: { 10 | "@": "/src", 11 | }, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/pod/nominal-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: nominal-case-extra-args-plan 5 | namespace: default 6 | spec: 7 | action: plan 8 | layer: 9 | name: pod-nominal-case-extra-args 10 | namespace: default 11 | revision: TEST_REVISION 12 | -------------------------------------------------------------------------------- /docs/examples/terraform-layer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformLayer 3 | metadata: 4 | name: my-layer 5 | namespace: burrito-project 6 | spec: 7 | branch: main 8 | path: terraform 9 | remediationStrategy: 10 | autoApply: false 11 | repository: 12 | name: my-repository 13 | namespace: burrito-project 14 | -------------------------------------------------------------------------------- /internal/datastore/storage/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func SanitizePrefix(prefix string) string { 9 | trimmedPrefix := strings.TrimPrefix(prefix, "/") 10 | trimmedPrefix = strings.TrimSuffix(trimmedPrefix, "/") 11 | trimmedPrefix = fmt.Sprintf("%s/", trimmedPrefix) 12 | 13 | return trimmedPrefix 14 | } 15 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | 9 | node_modules 10 | dist 11 | dist-ssr 12 | *.local 13 | .env 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /internal/datastore/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/burrito/config" 5 | "github.com/padok-team/burrito/internal/datastore/storage" 6 | ) 7 | 8 | type API struct { 9 | config *config.Config 10 | Storage storage.Storage 11 | } 12 | 13 | func New(c *config.Config) *API { 14 | return &API{ 15 | config: c, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /internal/e2e/testdata/terragrunt/random-pets/prod/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | include "root" { 2 | path = find_in_parent_folders() 3 | merge_strategy = "deep" 4 | } 5 | 6 | include "module" { 7 | path = find_in_parent_folders("module.hcl") 8 | merge_strategy = "deep" 9 | } 10 | 11 | include "inputs" { 12 | path = "inputs.hcl" 13 | merge_strategy = "deep" 14 | } 15 | -------------------------------------------------------------------------------- /internal/runner/tools/opentofu/opentofu.go: -------------------------------------------------------------------------------- 1 | package opentofu 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/runner/tools/base" 5 | ) 6 | 7 | type OpenTofu struct { 8 | base.BaseTool 9 | } 10 | 11 | func NewOpenTofu(execPath string) *OpenTofu { 12 | return &OpenTofu{ 13 | BaseTool: base.BaseTool{ 14 | ExecPath: execPath, 15 | ToolName: "tofu", 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/runner/tools/terraform/terraform.go: -------------------------------------------------------------------------------- 1 | package terraform 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/runner/tools/base" 5 | ) 6 | 7 | type Terraform struct { 8 | base.BaseTool 9 | } 10 | 11 | func NewTerraform(execPath string) *Terraform { 12 | return &Terraform{ 13 | BaseTool: base.BaseTool{ 14 | ExecPath: execPath, 15 | ToolName: "terraform", 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/padok-team/burrito/internal/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func buildVersionCmd() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "version", 13 | Short: "version", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Println(version.BuildVersion()) 16 | }, 17 | } 18 | return cmd 19 | } 20 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/comment/templates/comment.md: -------------------------------------------------------------------------------- 1 | ## :burrito: Burrito Report 2 | 3 | {{ len .Layers }} layer(s) affected with {{ .Commit }} commit. 4 | 5 | {{ range .Layers }} 6 | 7 | ### Layer {{ .Name }} ({{ .Path }}) 8 | 9 | `{{ .ShortDiff }}` 10 | 11 |
12 | Plan 13 | 14 | ```terraform 15 | {{ .PrettyPlan }} 16 | ``` 17 |
18 | 19 | {{ end }} 20 | -------------------------------------------------------------------------------- /docs/guides/index.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | Follow the 3 guides below to understand how to use Burrito: 4 | 5 | - [IaC Drift detection](./iac-drift-detection.md): Quickly set up Burrito and start monitoring Terraform state drift. 6 | - [PR/MR plan/apply Workflow](./pr-mr-workflow.md): Configure Burrito to automatically plan and apply Terraform code on PR/MR. 7 | - [UI Overview](./ui.md): Learn how to navigate the Burrito UI. 8 | -------------------------------------------------------------------------------- /internal/datastore/storage/error/error.go: -------------------------------------------------------------------------------- 1 | package error 2 | 3 | type StorageError struct { 4 | Err error 5 | Nil bool 6 | } 7 | 8 | func (s *StorageError) Error() string { 9 | return s.Err.Error() 10 | } 11 | 12 | func NotFound(err error) bool { 13 | ce, ok := err.(*StorageError) 14 | if ok { 15 | return ce.Nil 16 | } 17 | return false 18 | } 19 | 20 | func (c *StorageError) NotFound() bool { 21 | return c.Nil 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/clients/runs/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { Attempts } from '@/clients/runs/types.ts'; 4 | 5 | export const fetchAttempts = async ( 6 | namespace: string, 7 | layer: string, 8 | runId: string 9 | ) => { 10 | const response = await axios.get( 11 | `${import.meta.env.VITE_API_BASE_URL}/run/${namespace}/${layer}/${runId}/attempts` 12 | ); 13 | return response.data; 14 | }; 15 | -------------------------------------------------------------------------------- /manifests/base/server/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: server 6 | app.kubernetes.io/name: burrito-server 7 | app.kubernetes.io/part-of: burrito 8 | name: burrito-server 9 | spec: 10 | ports: 11 | - name: http 12 | port: 80 13 | protocol: TCP 14 | targetPort: http 15 | selector: 16 | app.kubernetes.io/name: burrito-server 17 | -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 AS build 2 | WORKDIR /app 3 | COPY package.json yarn.lock ./ 4 | RUN yarn install --frozen-lockfile 5 | COPY . . 6 | ARG API_BASE_URL 7 | ENV VITE_API_BASE_URL=$API_BASE_URL 8 | RUN yarn build 9 | 10 | FROM nginx:stable-alpine 11 | WORKDIR /usr/share/nginx/html 12 | COPY default.conf /etc/nginx/conf.d/default.conf 13 | RUN rm -rf ./* 14 | COPY --from=build /app/dist . 15 | EXPOSE 80 16 | CMD ["nginx", "-g", "daemon off;"] 17 | -------------------------------------------------------------------------------- /ui/src/assets/icons/LoaderIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const LoaderIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default LoaderIcon; 10 | -------------------------------------------------------------------------------- /ui/src/clients/logs/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { Logs } from '@/clients/logs/types.ts'; 4 | 5 | export const fetchLogs = async ( 6 | namespace: string, 7 | layer: string, 8 | runId: string, 9 | attemptId: number | null 10 | ) => { 11 | const response = await axios.get( 12 | `${import.meta.env.VITE_API_BASE_URL}/logs/${namespace}/${layer}/${runId}/${attemptId}` 13 | ); 14 | return response.data; 15 | }; 16 | -------------------------------------------------------------------------------- /deploy/charts/burrito/values-dev.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | burrito: 3 | runner: 4 | image: 5 | repository: burrito 6 | tag: DEV_TAG 7 | pullPolicy: Never 8 | datastore: 9 | storage: 10 | mock: true 11 | global: 12 | deployment: 13 | image: 14 | repository: burrito 15 | tag: DEV_TAG 16 | pullPolicy: Never 17 | tenants: 18 | - namespace: 19 | create: true 20 | name: "burrito-project" 21 | -------------------------------------------------------------------------------- /ui/src/components/widgets/Running.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SyncIcon from '@/assets/icons/SyncIcon'; 4 | 5 | const Running: React.FC = () => { 6 | return ( 7 |
8 | Running 9 | 10 |
11 | ); 12 | }; 13 | 14 | export default Running; 15 | -------------------------------------------------------------------------------- /internal/annotations/testdata/layer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformLayer 3 | metadata: 4 | name: test 5 | namespace: default 6 | spec: 7 | branch: main 8 | path: test/ 9 | remediationStrategy: 10 | autoApply: true 11 | repository: 12 | name: burrito 13 | namespace: default 14 | terraform: 15 | enabled: true 16 | version: 1.3.1 17 | terragrunt: 18 | enabled: true 19 | version: 0.45.4 20 | -------------------------------------------------------------------------------- /internal/server/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/burrito/config" 5 | datastore "github.com/padok-team/burrito/internal/datastore/client" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type API struct { 10 | config *config.Config 11 | Client client.Client 12 | Datastore datastore.Client 13 | } 14 | 15 | func New(c *config.Config) *API { 16 | return &API{ 17 | config: c, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/padok-team/burrito/cmd" 11 | "github.com/padok-team/burrito/internal/burrito" 12 | ) 13 | 14 | func main() { 15 | app, err := burrito.New() 16 | if err != nil { 17 | fmt.Fprintf(os.Stderr, "fatal: %s\n", err) 18 | os.Exit(1) 19 | } 20 | 21 | if err := cmd.New(app).Execute(); err != nil { 22 | os.Exit(1) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /deploy/charts/burrito/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /ui/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Burrito", 3 | "short_name": "Burrito", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/clients/reactQueryConfig.ts: -------------------------------------------------------------------------------- 1 | export const reactQueryKeys = { 2 | layers: ['layers'], 3 | repositories: ['repositories'], 4 | attempts: (namespace: string, layer: string, runId: string) => [ 5 | 'run', 6 | namespace, 7 | layer, 8 | runId, 9 | 'attempts' 10 | ], 11 | logs: ( 12 | namespace: string, 13 | layer: string, 14 | runId: string, 15 | attemptId: number | null 16 | ) => ['logs', namespace, layer, runId, attemptId] 17 | }; 18 | -------------------------------------------------------------------------------- /internal/controllers/terraformlayer/testdata/repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRepository 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: in-cluster-burrito 6 | name: burrito 7 | namespace: default 8 | spec: 9 | overrideRunnerSpec: 10 | imagePullSecrets: 11 | - name: ghcr-creds 12 | repository: 13 | secretName: burrito-repo 14 | url: git@github.com:padok-team/burrito-examples.git 15 | terraform: 16 | enabled: true 17 | -------------------------------------------------------------------------------- /manifests/base/runner/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: runner 6 | app.kubernetes.io/name: burrito-runner 7 | app.kubernetes.io/part-of: burrito 8 | name: burrito-runner 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: burrito-runner 13 | subjects: 14 | - kind: ServiceAccount 15 | name: burrito-runner 16 | namespace: burrito 17 | -------------------------------------------------------------------------------- /manifests/base/server/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: server 6 | app.kubernetes.io/name: burrito-server 7 | app.kubernetes.io/part-of: burrito 8 | name: burrito-server 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: burrito-server 13 | subjects: 14 | - kind: ServiceAccount 15 | name: burrito-server 16 | namespace: burrito 17 | -------------------------------------------------------------------------------- /ui/src/assets/icons/MultiplyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const MultiplyIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default MultiplyIcon; 10 | -------------------------------------------------------------------------------- /ui/src/assets/icons/WindowIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const WindowIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default WindowIcon; 10 | -------------------------------------------------------------------------------- /manifests/base/controllers/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: controllers 6 | app.kubernetes.io/name: burrito-controllers 7 | app.kubernetes.io/part-of: burrito 8 | name: burrito-controllers 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: burrito-controllers 13 | subjects: 14 | - kind: ServiceAccount 15 | name: burrito-controllers 16 | namespace: burrito 17 | -------------------------------------------------------------------------------- /cmd/server/start.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/burrito" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func buildServerStartCmd(app *burrito.App) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "start", 11 | Short: "Start burrito's server", 12 | RunE: func(cmd *cobra.Command, args []string) error { 13 | app.StartServer() 14 | return nil 15 | }, 16 | } 17 | 18 | cmd.Flags().StringVar(&app.Config.Server.Addr, "addr", ":8080", "addr the server listens on") 19 | 20 | return cmd 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/clients/layers/client.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { Layers } from '@/clients/layers/types.ts'; 4 | 5 | export const fetchLayers = async () => { 6 | const response = await axios.get( 7 | `${import.meta.env.VITE_API_BASE_URL}/layers` 8 | ); 9 | return response.data; 10 | }; 11 | 12 | export const syncLayer = async (namespace: string, name: string) => { 13 | const response = await axios.post( 14 | `${import.meta.env.VITE_API_BASE_URL}/layers/${namespace}/${name}/sync` 15 | ); 16 | return response; 17 | }; 18 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/pod/repository-layers.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: config.terraform.padok.cloud/v1alpha1 3 | kind: TerraformLayer 4 | metadata: 5 | name: pod-nominal-case-extra-args 6 | namespace: default 7 | spec: 8 | branch: main 9 | path: terraform/ 10 | repository: 11 | name: burrito 12 | namespace: default 13 | overrideRunnerSpec: 14 | extraPlanArgs: ["--target", "'module.this.random_pet.this[\"first\"]'"] 15 | extraApplyArgs: ["--target", "'module.this.random_pet.this[\"first\"]'"] 16 | extraInitArgs: ["--upgrade"] 17 | -------------------------------------------------------------------------------- /ui/src/assets/icons/MinusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const MinusIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default MinusIcon; 10 | -------------------------------------------------------------------------------- /internal/controllers/terraformlayer/testdata/runs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: run-succeeded 5 | namespace: default 6 | status: 7 | state: "Succeeded" 8 | --- 9 | apiVersion: config.terraform.padok.cloud/v1alpha1 10 | kind: TerraformRun 11 | metadata: 12 | name: run-failed 13 | namespace: default 14 | status: 15 | state: "Failed" 16 | --- 17 | apiVersion: config.terraform.padok.cloud/v1alpha1 18 | kind: TerraformRun 19 | metadata: 20 | name: run-running 21 | namespace: default 22 | status: 23 | state: "Running" 24 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Burrito 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /ui/src/assets/icons/SyncIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const SyncIcon = (props: SVGProps) => ( 4 | 5 | 10 | 11 | ); 12 | 13 | export default SyncIcon; 14 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/controller/concurrent-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: concurrent-case-1 5 | namespace: default 6 | spec: 7 | action: plan 8 | layer: 9 | name: concurrent-case-1 10 | namespace: default 11 | revision: TEST_REVISION 12 | --- 13 | apiVersion: config.terraform.padok.cloud/v1alpha1 14 | kind: TerraformRun 15 | metadata: 16 | name: concurrent-case-2 17 | namespace: default 18 | spec: 19 | action: plan 20 | layer: 21 | name: concurrent-case-1 22 | namespace: default 23 | revision: TEST_REVISION 24 | -------------------------------------------------------------------------------- /ui/src/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | import { ThemeContext } from '@/contexts/ThemeContext'; 5 | 6 | import NavigationBar from '@/components/navigation/NavigationBar'; 7 | 8 | const Layout: React.FC = () => { 9 | const { theme } = useContext(ThemeContext); 10 | return ( 11 |
17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default Layout; 24 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/padok-team/burrito/internal/burrito" 5 | cmdUtils "github.com/padok-team/burrito/internal/utils/cmd" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func BuildServerCmd(app *burrito.App) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "server", 12 | Short: "cmd to use burrito's server", 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | // If we reach this point, it means no subcommand was matched 15 | cmdUtils.UnsupportedCommand(cmd, args) 16 | return cmd.Help() 17 | }, 18 | } 19 | cmd.AddCommand(buildServerStartCmd(app)) 20 | return cmd 21 | } 22 | -------------------------------------------------------------------------------- /internal/utils/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func Verbose(cmd *exec.Cmd) { 12 | cmd.Stdout = os.Stdout 13 | cmd.Stderr = os.Stderr 14 | } 15 | 16 | func UnsupportedCommand(cmd *cobra.Command, args []string) { 17 | if len(args) > 0 { 18 | fmt.Fprintf(os.Stderr, "Error: unknown %s subcommand: %s\n", cmd.Use, args[0]) 19 | } 20 | fmt.Fprintf(os.Stderr, "Run 'burrito %s --help' for usage\n", cmd.Use) 21 | if err := cmd.Help(); err != nil { 22 | fmt.Fprintf(os.Stderr, "Error displaying help: %v\n", err) 23 | os.Exit(1) 24 | } 25 | os.Exit(2) 26 | } 27 | -------------------------------------------------------------------------------- /internal/lock/testdata/layer.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformLayer 3 | metadata: 4 | name: test 5 | namespace: default 6 | spec: 7 | branch: main 8 | path: test/ 9 | remediationStrategy: 10 | autoApply: true 11 | repository: 12 | name: burrito 13 | namespace: default 14 | terraform: 15 | version: 1.3.1 16 | terragrunt: 17 | enabled: true 18 | version: 0.45.4 19 | --- 20 | apiVersion: config.terraform.padok.cloud/v1alpha1 21 | kind: TerraformRun 22 | metadata: 23 | name: test-run 24 | namespace: default 25 | spec: 26 | action: plan 27 | layer: 28 | name: test 29 | namespace: default 30 | -------------------------------------------------------------------------------- /cmd/datastore/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package datastore 5 | 6 | import ( 7 | "github.com/padok-team/burrito/internal/burrito" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func buildDatastoreStartCmd(app *burrito.App) *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "start", 14 | Short: "Start Burrito Datastore", 15 | // Do not display usage on program error 16 | SilenceUsage: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | return app.StartDatastore() 19 | }, 20 | } 21 | cmd.Flags().StringVar(&app.Config.Server.Addr, "addr", ":8080", "addr the datastore listens on") 22 | return cmd 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/components/core/Box.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export interface BoxProps { 5 | className?: string; 6 | variant?: 'light' | 'dark'; 7 | children: React.ReactNode; 8 | } 9 | 10 | const Box: React.FC = ({ 11 | className, 12 | variant = 'light', 13 | children 14 | }) => { 15 | const styles = { 16 | light: `bg-nuances-white 17 | shadow-light`, 18 | dark: `bg-nuances-black 19 | shadow-dark` 20 | }; 21 | 22 | return ( 23 |
24 | {children} 25 |
26 | ); 27 | }; 28 | 29 | export default Box; 30 | -------------------------------------------------------------------------------- /docs/user-guide/ssh-known-hosts.md: -------------------------------------------------------------------------------- 1 | # Manage SSH known hosts 2 | 3 | ## Defaults 4 | 5 | By default, we provide a list of known hosts with public repositories: 6 | 7 | - Azure 8 | - Bitbucket 9 | - GitHub 10 | - Gitlab 11 | - Visual Studio 12 | 13 | ## Override known hosts 14 | 15 | If you need to provide your own keys for other repositories, you can override the default value in the chart with: 16 | 17 | ```yaml 18 | global: 19 | sshKnownHosts: |- 20 | git.domain.com ssh-ed25519 AAAAC3Nxxx 21 | git.domain.com ssh-rsa AAAAB3Nxxx 22 | git.domain.com ecdsa-sha2-nistp256 AAAAE2Vxxx 23 | ``` 24 | 25 | To get those keys, you can run: `ssh-keyscan git.domain.com 2>&1| grep -vE '^#'` 26 | -------------------------------------------------------------------------------- /cmd/runner/runner.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package runner 5 | 6 | import ( 7 | "github.com/padok-team/burrito/internal/burrito" 8 | cmdUtils "github.com/padok-team/burrito/internal/utils/cmd" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func BuildRunnerCmd(app *burrito.App) *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "runner", 15 | Short: "cmd to use burrito's runner", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | // If we reach this point, it means no subcommand was matched 18 | cmdUtils.UnsupportedCommand(cmd, args) 19 | return cmd.Help() 20 | }, 21 | } 22 | cmd.AddCommand(buildRunnerStartCmd(app)) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /cmd/datastore/datastore.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package datastore 5 | 6 | import ( 7 | "github.com/padok-team/burrito/internal/burrito" 8 | cmdUtils "github.com/padok-team/burrito/internal/utils/cmd" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func BuildDatastoreCmd(app *burrito.App) *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "datastore", 15 | Short: "cmd to use burrito's datastore", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | // If we reach this point, it means no subcommand was matched 18 | cmdUtils.UnsupportedCommand(cmd, args) 19 | return cmd.Help() 20 | }, 21 | } 22 | cmd.AddCommand(buildDatastoreStartCmd(app)) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /ui/src/components/buttons/SSOButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '@/components/core/Button'; 4 | 5 | export interface SSOButtonProps { 6 | className?: string; 7 | isLoading?: boolean; 8 | onClick?: () => void; 9 | } 10 | 11 | const SSOButton: React.FC = ({ 12 | className, 13 | isLoading, 14 | onClick 15 | }) => { 16 | return ( 17 | 27 | ); 28 | }; 29 | 30 | export default SSOButton; 31 | -------------------------------------------------------------------------------- /ui/src/clients/layers/types.ts: -------------------------------------------------------------------------------- 1 | export type Layers = { 2 | results: Layer[]; 3 | }; 4 | 5 | export type Layer = { 6 | namespace: string; 7 | name: string; 8 | state: LayerState; 9 | repository: string; 10 | branch: string; 11 | path: string; 12 | runCount: number; 13 | lastRunAt: string; 14 | lastRun: Run; 15 | latestRuns: Run[]; 16 | lastResult: string; 17 | isRunning: boolean; 18 | manualSyncStatus: ManualSyncStatus; 19 | isPR: boolean; 20 | }; 21 | 22 | export type LayerState = 'success' | 'warning' | 'error' | 'disabled'; 23 | export type ManualSyncStatus = 'none' | 'annotated' | 'pending'; 24 | 25 | export type Run = { 26 | id: string; 27 | commit: string; 28 | date: string; 29 | action: string; 30 | }; 31 | -------------------------------------------------------------------------------- /cmd/controllers/controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package controllers 5 | 6 | import ( 7 | "github.com/padok-team/burrito/internal/burrito" 8 | cmdUtils "github.com/padok-team/burrito/internal/utils/cmd" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func BuildControllersCmd(app *burrito.App) *cobra.Command { 13 | cmd := &cobra.Command{ 14 | Use: "controllers", 15 | Short: "cmd to use burrito's controllers", 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | // If we reach this point, it means no subcommand was matched 18 | cmdUtils.UnsupportedCommand(cmd, args) 19 | return cmd.Help() 20 | }, 21 | } 22 | cmd.AddCommand(buildControllersStartCmd(app)) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /manifests/base/runner/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/component: runner 6 | app.kubernetes.io/name: burrito-runner 7 | app.kubernetes.io/part-of: burrito 8 | name: burrito-runner 9 | rules: 10 | - apiGroups: 11 | - coordination.k8s.io 12 | resources: 13 | - leases 14 | verbs: 15 | - get 16 | - delete 17 | - apiGroups: 18 | - config.terraform.padok.cloud 19 | resources: 20 | - terraformlayers 21 | verbs: 22 | - get 23 | - patch 24 | - apiGroups: 25 | - config.terraform.padok.cloud 26 | resources: 27 | - terraformrepositories 28 | verbs: 29 | - get 30 | -------------------------------------------------------------------------------- /docs/examples/values-simple.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | burrito: 3 | controller: 4 | timers: 5 | driftDetection: 10m # run drift detection every 10 minutes 6 | onError: 10s # wait 10 seconds before retrying on error 7 | waitAction: 1m # wait 1 minute before retrying on locked layer 8 | failureGracePeriod: 30s # set a grace period of 30 seconds before retrying on failure (increases exponentially with the amount of failed retries) 9 | datastore: 10 | storage: 11 | mock: true # use a mock storage for the datastore (useful for testing, not recommended for production) 12 | tenants: 13 | - namespace: 14 | create: true 15 | name: "burrito-project" 16 | serviceAccounts: 17 | - name: burrito-runner 18 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yaml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | commitlint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v6 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-node@v6 16 | - name: commitlint (install) 17 | run: | 18 | npm install -g @commitlint/cli @commitlint/config-conventional 19 | echo 'module.exports = {extends: ["@commitlint/config-conventional"]}' > commitlint.config.js 20 | - name: commitlint (run) 21 | run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose 22 | -------------------------------------------------------------------------------- /ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import reactPlugin from 'eslint-plugin-react'; 2 | import hooksPlugin from 'eslint-plugin-react-hooks'; 3 | import tseslint from 'typescript-eslint'; 4 | import eslint from '@eslint/js'; 5 | 6 | export default [ 7 | { 8 | plugins: { 9 | react: reactPlugin, 10 | }, 11 | rules: { 12 | ...reactPlugin.configs['jsx-runtime'].rules, 13 | }, 14 | settings: { 15 | react: { 16 | version: 'detect', 17 | }, 18 | }, 19 | }, 20 | { 21 | plugins: { 22 | 'react-hooks': hooksPlugin, 23 | }, 24 | rules: hooksPlugin.configs.recommended.rules, 25 | }, 26 | eslint.configs.recommended, 27 | ...tseslint.configs.recommended, 28 | { 29 | ignores: ['**/dist'], 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .ssh/* 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | bin 10 | testbin/* 11 | Dockerfile.cross 12 | 13 | *.lock.hcl 14 | *.tfstate 15 | *.tfstate.backup 16 | 17 | .terraform/ 18 | 19 | # Artifact generated by tests 20 | test.out/* 21 | 22 | # Test binary, build with `go test -c` 23 | *.test 24 | 25 | # Output of the go coverage tool, specifically when used with LiteIDE 26 | *.out 27 | 28 | # Kubernetes Generated files - skip generated files, except for vendored files 29 | 30 | !vendor/**/zz_generated.* 31 | 32 | # editor and IDE paraphernalia 33 | .idea 34 | *.swp 35 | *.swo 36 | *~ 37 | 38 | # Python virtual environment (for mkdocs) 39 | .env 40 | .venv 41 | env/ 42 | venv/ 43 | 44 | test-repo/ 45 | TIltfile -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: terraform.padok.cloud 2 | layout: 3 | - go.kubebuilder.io/v3 4 | plugins: 5 | manifests.sdk.operatorframework.io/v2: {} 6 | scorecard.sdk.operatorframework.io/v2: {} 7 | projectName: burrito 8 | repo: github.com/padok-team/burrito 9 | resources: 10 | - api: 11 | crdVersion: v1 12 | namespaced: true 13 | controller: true 14 | domain: terraform.padok.cloud 15 | group: config 16 | kind: TerraformRepository 17 | path: github.com/padok-team/burrito/api/v1alpha1 18 | version: v1alpha1 19 | - api: 20 | crdVersion: v1 21 | namespaced: true 22 | controller: true 23 | domain: terraform.padok.cloud 24 | group: config 25 | kind: TerraformLayer 26 | path: github.com/padok-team/burrito/api/v1alpha1 27 | version: v1alpha1 28 | version: "3" 29 | -------------------------------------------------------------------------------- /ui/src/components/buttons/OpenInLogsButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '@/components/core/Button'; 4 | import ArrowResizeDiagonalIcon from '@/assets/icons/ArrowResizeDiagonalIcon'; 5 | 6 | export interface OpenInLogsButtonProps { 7 | className?: string; 8 | variant?: 'primary' | 'secondary' | 'tertiary'; 9 | onClick?: () => void; 10 | } 11 | 12 | const OpenInLogsButton: React.FC = ({ 13 | className, 14 | variant = 'primary', 15 | onClick 16 | }) => { 17 | return ( 18 | 26 | ); 27 | }; 28 | 29 | export default OpenInLogsButton; 30 | -------------------------------------------------------------------------------- /ui/src/components/loaders/TableLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export interface TableLoaderProps { 5 | className?: string; 6 | variant?: 'light' | 'dark'; 7 | } 8 | 9 | const TableLoader: React.FC = ({ 10 | className, 11 | variant = 'light' 12 | }) => { 13 | const styles = { 14 | light: `bg-[linear-gradient(270deg,#D8EBFF_0%,#ECF5FF_100%)]`, 15 | dark: `bg-[linear-gradient(270deg,#252525_0%,rgba(68,67,67,0.24)_100%)]` 16 | }; 17 | 18 | return ( 19 |
28 | ); 29 | }; 30 | 31 | export default TableLoader; 32 | -------------------------------------------------------------------------------- /internal/utils/runner/network_mirror.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Creates a network mirror configuration file for Terraform with the given endpoint 11 | func CreateNetworkMirrorConfig(targetPath string, endpoint string) error { 12 | terraformrcContent := fmt.Sprintf(` 13 | provider_installation { 14 | network_mirror { 15 | url = "%s" 16 | } 17 | }`, endpoint) 18 | filePath := fmt.Sprintf("%s/config.tfrc", targetPath) 19 | err := os.WriteFile(filePath, []byte(terraformrcContent), 0644) 20 | if err != nil { 21 | return err 22 | } 23 | err = os.Setenv("TF_CLI_CONFIG_FILE", filePath) 24 | if err != nil { 25 | return err 26 | } 27 | log.Infof("network mirror configuration created") 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/utils/runner/plan_diff.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "fmt" 5 | 6 | tfjson "github.com/hashicorp/terraform-json" 7 | ) 8 | 9 | // Produces a diff summary from the given plan 10 | func GetDiff(plan *tfjson.Plan) (bool, string) { 11 | delete := 0 12 | create := 0 13 | update := 0 14 | for _, res := range plan.ResourceChanges { 15 | if res.Change.Actions.Create() { 16 | create++ 17 | } 18 | if res.Change.Actions.Delete() { 19 | delete++ 20 | } 21 | if res.Change.Actions.Update() { 22 | update++ 23 | } 24 | if res.Change.Actions.Replace() { 25 | create++ 26 | delete++ 27 | } 28 | } 29 | diff := false 30 | if create+delete+update > 0 { 31 | diff = true 32 | } 33 | return diff, fmt.Sprintf("Plan: %d to create, %d to update, %d to delete", create, update, delete) 34 | } 35 | -------------------------------------------------------------------------------- /internal/utils/k8s_client.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 7 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | "sigs.k8s.io/controller-runtime/pkg/client" 10 | ) 11 | 12 | // Create a new Kubernetes client with standard resources and Burrito CRDs 13 | func NewK8SClient() (client.Client, error) { 14 | scheme := runtime.NewScheme() 15 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 16 | utilruntime.Must(configv1alpha1.AddToScheme(scheme)) 17 | cl, err := client.New(ctrl.GetConfigOrDie(), client.Options{ 18 | Scheme: scheme, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return cl, err 24 | } 25 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* Importing */ 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/components/loaders/CardLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export interface CardLoaderProps { 5 | className?: string; 6 | variant?: 'light' | 'dark'; 7 | } 8 | 9 | const CardLoader: React.FC = ({ 10 | className, 11 | variant = 'light' 12 | }) => { 13 | const styles = { 14 | light: `bg-[linear-gradient(270deg,#D8EBFF_0%,#ECF5FF_100%)] 15 | shadow-light`, 16 | dark: `bg-[linear-gradient(270deg,#252525_0%,rgba(68,67,67,0.24)_100%)] 17 | shadow-dark` 18 | }; 19 | 20 | return ( 21 |
30 | ); 31 | }; 32 | 33 | export default CardLoader; 34 | -------------------------------------------------------------------------------- /internal/server/utils/manual_sync.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 5 | "github.com/padok-team/burrito/internal/annotations" 6 | ) 7 | 8 | type ManualSyncStatus string 9 | 10 | const ( 11 | ManualSyncNone ManualSyncStatus = "none" 12 | ManualSyncAnnotated ManualSyncStatus = "annotated" 13 | ManualSyncPending ManualSyncStatus = "pending" 14 | ) 15 | 16 | func GetManualSyncStatus(layer configv1alpha1.TerraformLayer) ManualSyncStatus { 17 | if layer.Annotations[annotations.SyncNow] == "true" { 18 | return ManualSyncAnnotated 19 | } 20 | // check the IsSyncScheduled condition on layer 21 | for _, c := range layer.Status.Conditions { 22 | if c.Type == "IsSyncScheduled" && c.Status == "True" { 23 | return ManualSyncPending 24 | } 25 | } 26 | return ManualSyncNone 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/components/loaders/RunCardLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export interface RunCardLoaderProps { 5 | className?: string; 6 | variant?: 'light' | 'dark'; 7 | } 8 | 9 | const RunCardLoader: React.FC = ({ 10 | className, 11 | variant = 'light' 12 | }) => { 13 | const style = { 14 | light: `bg-[linear-gradient(270deg,#D8EBFF_0%,#ECF5FF_100%)] 15 | shadow-light`, 16 | dark: `bg-[linear-gradient(270deg,#252525_0%,rgba(68,67,67,0.24)_100%)] 17 | shadow-dark` 18 | }; 19 | 20 | return ( 21 |
30 | ); 31 | }; 32 | 33 | export default RunCardLoader; 34 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/ssh-known-hosts.yaml: -------------------------------------------------------------------------------- 1 | {{/* 2 | Create ConfigMap in all tenant namespaces + release namespace 3 | */}} 4 | {{- $namespaces := list }} 5 | {{- range $tenant := .Values.tenants }} 6 | {{- $namespaces = append $namespaces $tenant.namespace.name }} 7 | {{- end }} 8 | {{- $namespaces = append $namespaces .Release.Namespace }} 9 | 10 | {{- range $namespace := $namespaces }} 11 | --- 12 | apiVersion: v1 13 | kind: ConfigMap 14 | metadata: 15 | name: burrito-ssh-known-hosts 16 | namespace: {{ $namespace }} 17 | labels: 18 | app.kubernetes.io/name: burrito-ssh-known-hosts 19 | {{- toYaml $.Values.global.metadata.labels | nindent 4 }} 20 | annotations: 21 | {{- toYaml $.Values.global.metadata.annotations | nindent 4 }} 22 | data: 23 | known_hosts: |- 24 | {{- $.Values.global.sshKnownHosts | nindent 4 }} 25 | {{- end }} 26 | -------------------------------------------------------------------------------- /ui/src/pages/Pulls.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | 3 | import { ThemeContext } from '@/contexts/ThemeContext'; 4 | 5 | import TrafficCone from '@/components/temp/TrafficCone'; 6 | 7 | const Pulls: React.FC = () => { 8 | const { theme } = useContext(ThemeContext); 9 | return ( 10 |
11 |
12 | 13 | 20 | Pull requests page 21 | 22 | 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Pulls; 29 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Burrito UI 2 | 3 | Burrito Logo 4 | 5 | Web UI for [Burrito](https://github.com/padok-team/burrito). 6 | 7 | ## Getting started 8 | 9 | 1. Install [NodeJS](https://nodejs.org/en/download/) and [Yarn](https://yarnpkg.com). 10 | 2. Run `yarn install` to install local prerequisites. 11 | 3. Run `yarn dev` to launch the dev UI server. 12 | 4. Run `yarn build` to bundle static resources into the `./dist` directory. 13 | 14 | ## Build Docker production image 15 | 16 | Run the following commands to build the Docker image: 17 | 18 | ```bash 19 | TAG=latest # or any other tag 20 | BURRITO_API_BASE_URL=https://burrito.example.cloud/api # or any other API base URL 21 | docker build -t burrito-ui:$TAG --build-arg API_BASE_URL=$BURRITO_API_BASE_URL . 22 | ``` 23 | -------------------------------------------------------------------------------- /internal/webhook/event/common.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "strings" 5 | 6 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | const PullRequestOpened = "opened" 11 | const PullRequestClosed = "closed" 12 | 13 | type ChangeInfo struct { 14 | ShaBefore string 15 | ShaAfter string 16 | } 17 | 18 | type Event interface { 19 | Handle(client.Client) error 20 | } 21 | 22 | func ParseReference(ref string) string { 23 | refParts := strings.SplitN(ref, "/", 3) 24 | return refParts[len(refParts)-1] 25 | } 26 | 27 | func isPRLinkedToAnyRepositories(pr configv1alpha1.TerraformPullRequest, repos []configv1alpha1.TerraformRepository) bool { 28 | for _, r := range repos { 29 | if r.Name == pr.Spec.Repository.Name && r.Namespace == pr.Spec.Repository.Namespace { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/rbac-runner.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: burrito-runner 6 | {{- with mergeOverwrite (deepCopy .Values.global.metadata) .Values.runners.rbac.metadata }} 7 | labels: 8 | {{- toYaml .labels | nindent 4}} 9 | annotations: 10 | {{- toYaml .annotations | nindent 4}} 11 | {{- end }} 12 | rules: 13 | - apiGroups: 14 | - coordination.k8s.io 15 | resources: 16 | - leases 17 | verbs: 18 | - get 19 | - delete 20 | - apiGroups: 21 | - config.terraform.padok.cloud 22 | resources: 23 | - terraformlayers 24 | verbs: 25 | - get 26 | - patch 27 | - apiGroups: 28 | - config.terraform.padok.cloud 29 | resources: 30 | - terraformruns 31 | verbs: 32 | - get 33 | - apiGroups: 34 | - config.terraform.padok.cloud 35 | resources: 36 | - terraformrepositories 37 | verbs: 38 | - get 39 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/controller/parallel-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: parallel-case-1 5 | namespace: default 6 | spec: 7 | action: plan 8 | layer: 9 | name: parallel-case-1 10 | namespace: default 11 | revision: TEST_REVISION 12 | --- 13 | apiVersion: config.terraform.padok.cloud/v1alpha1 14 | kind: TerraformRun 15 | metadata: 16 | name: parallel-case-2 17 | namespace: default 18 | spec: 19 | action: plan 20 | layer: 21 | name: parallel-case-2 22 | namespace: default 23 | revision: TEST_REVISION 24 | --- 25 | apiVersion: config.terraform.padok.cloud/v1alpha1 26 | kind: TerraformRun 27 | metadata: 28 | name: parallel-case-3 29 | namespace: default 30 | spec: 31 | action: plan 32 | layer: 33 | name: parallel-case-3 34 | namespace: default 35 | revision: TEST_REVISION 36 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Burrito Documentation 2 | 3 | This is the home of the Burrito documentation. Here you will find all the information you need to get started with Burrito. 4 | 5 | - [Overview](./overview.md) helps you understand what Burrito is all about. 6 | - [Getting Started](./getting-started.md) is a step-by-step guide to help you get started with Burrito. 7 | - [Guides](./guides/index.md) provides detailed tutorials to help you understand how to use Burrito. 8 | - [Operator Manual](./operator-manual/index.md) is a detailed guide to help you understand how to install and configure Burrito. 9 | - [User Guide](./user-guide/index.md) is a detailed guide to help you understand how to setup and use Burrito resources. 10 | - [Contributing](./contributing.md) provides information on how to contribute to the Burrito project. 11 | 13 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/comment/default_test.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | // func TestDefaultComment_Generate(t *testing.T) { 8 | // type fields struct { 9 | // layers []configv1alpha1.TerraformLayer 10 | // storage storage.Storage 11 | // } 12 | // tests := []struct { 13 | // name string 14 | // fields fields 15 | // want string 16 | // wantErr bool 17 | // }{ 18 | // // TODO: Add test cases. 19 | // } 20 | // for _, tt := range tests { 21 | // t.Run(tt.name, func(t *testing.T) { 22 | // c := &DefaultComment{ 23 | // layers: tt.fields.layers, 24 | // storage: tt.fields.storage, 25 | // } 26 | // got, err := c.Generate() 27 | // if (err != nil) != tt.wantErr { 28 | // t.Errorf("DefaultComment.Generate() error = %v, wantErr %v", err, tt.wantErr) 29 | // return 30 | // } 31 | // if got != tt.want { 32 | // t.Errorf("DefaultComment.Generate() = %v, want %v", got, tt.want) 33 | // } 34 | // }) 35 | // } 36 | // } 37 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/testdata/error-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformPullRequest 3 | metadata: 4 | name: pr-error-case-1 5 | namespace: default 6 | labels: 7 | app.kubernetes.io/instance: in-cluster-burrito 8 | annotations: 9 | webhook.terraform.padok.cloud/branch-commit: 04410b5b7d90b82ad658b86564a9aa4bce411ac9 10 | spec: 11 | branch: feature-branch 12 | id: "42" 13 | base: main 14 | repository: 15 | name: no-exist 16 | namespace: default 17 | --- 18 | apiVersion: config.terraform.padok.cloud/v1alpha1 19 | kind: TerraformPullRequest 20 | metadata: 21 | name: pr-error-case-2 22 | namespace: default 23 | labels: 24 | app.kubernetes.io/instance: in-cluster-burrito 25 | annotations: 26 | webhook.terraform.padok.cloud/branch-commit: 04410b5b7d90b82ad658b86564a9aa4bce411ac9 27 | spec: 28 | branch: feature-branch 29 | id: "42" 30 | base: main 31 | repository: 32 | name: burrito-no-provider 33 | namespace: default 34 | -------------------------------------------------------------------------------- /ui/src/assets/icons/GithubIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const GithubIcon = (props: SVGProps) => ( 4 | 5 | 9 | 10 | ); 11 | 12 | export default GithubIcon; 13 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "github.com/padok-team/burrito/cmd/controllers" 8 | "github.com/padok-team/burrito/cmd/datastore" 9 | "github.com/padok-team/burrito/cmd/runner" 10 | "github.com/padok-team/burrito/cmd/server" 11 | "github.com/padok-team/burrito/internal/burrito" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func New(app *burrito.App) *cobra.Command { 17 | return buildBurritoCmd(app) 18 | } 19 | 20 | func buildBurritoCmd(app *burrito.App) *cobra.Command { 21 | cmd := &cobra.Command{ 22 | Use: "burrito", 23 | Short: "burrito is a TACoS", 24 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 25 | return app.Config.Load(cmd.Flags()) 26 | }, 27 | } 28 | 29 | cmd.AddCommand(controllers.BuildControllersCmd(app)) 30 | cmd.AddCommand(runner.BuildRunnerCmd(app)) 31 | cmd.AddCommand(server.BuildServerCmd(app)) 32 | cmd.AddCommand(datastore.BuildDatastoreCmd(app)) 33 | cmd.AddCommand(buildVersionCmd()) 34 | return cmd 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/assets/icons/AngleDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const AngleDownIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default AngleDownIcon; 10 | -------------------------------------------------------------------------------- /cmd/runner/start.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2022 NAME HERE 3 | */ 4 | package runner 5 | 6 | import ( 7 | "github.com/padok-team/burrito/internal/burrito" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func buildRunnerStartCmd(app *burrito.App) *cobra.Command { 12 | cmd := &cobra.Command{ 13 | Use: "start", 14 | Short: "Start Burrito runner", 15 | // Do not display usage on program error 16 | SilenceUsage: true, 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | return app.StartRunner() 19 | }, 20 | } 21 | 22 | cmd.Flags().StringVar(&app.Config.Runner.SSHKnownHostsConfigMapName, "ssh-known-hosts-cm-name", "burrito-ssh-known-hosts", "configmap name to get known hosts file from") 23 | cmd.Flags().StringVar(&app.Config.Runner.RunnerBinaryPath, "runner-binary-path", "/runner/bin", "binary path where the runner can expect to find terraform or terragrunt binaries") 24 | cmd.Flags().StringVar(&app.Config.Runner.RepositoryPath, "repository-path", "/runner/repository", "path where the runner fetches the Git repository to work on") 25 | return cmd 26 | } 27 | -------------------------------------------------------------------------------- /internal/burrito/config/testdata/test-config-1.yaml: -------------------------------------------------------------------------------- 1 | runner: 2 | action: "apply" 3 | layer: 4 | name: test 5 | namespace: default 6 | image: 7 | repository: test-repository 8 | tag: test-tag 9 | pullPolicy: Always 10 | sshKnownHostsConfigMapName: burrito-ssh-known-hosts 11 | 12 | controller: 13 | namespaces: 14 | - default 15 | - burrito 16 | timers: 17 | driftDetection: 20m 18 | onError: 1m 19 | waitAction: 1m 20 | failureGracePeriod: 15s 21 | credentialsTTL: 1h 22 | defaultSyncWindows: 23 | - kind: "deny" 24 | schedule: "0 0 * * *" 25 | duration: "1h" 26 | layers: ["layer1", "layer2"] 27 | actions: ["plan", "apply"] 28 | terraformMaxRetries: 5 29 | maxConcurrentReconciles: 1 30 | maxConcurrentRunnerPods: 0 31 | types: ["layer", "repository", "run", "pullrequest"] 32 | leaderElection: 33 | enabled: true 34 | id: 6d185457.terraform.padok.cloud 35 | metricsBindAddress: ":8080" 36 | healthProbeBindAddress: ":8081" 37 | kubernetesWebhookPort: 9443 38 | 39 | server: 40 | addr: ":9090" 41 | -------------------------------------------------------------------------------- /ui/src/components/buttons/LogsButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | import WindowIcon from '@/assets/icons/WindowIcon'; 5 | 6 | export interface LogsButtonProps 7 | extends React.HTMLAttributes { 8 | className?: string; 9 | variant?: 'light' | 'dark'; 10 | } 11 | 12 | const LogsButton = React.forwardRef( 13 | ({ className, variant = 'light', ...props }, ref) => { 14 | return ( 15 | 33 | ); 34 | } 35 | ); 36 | 37 | export default LogsButton; 38 | -------------------------------------------------------------------------------- /internal/server/api/repositories.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type repository struct { 14 | Name string `json:"name"` 15 | } 16 | 17 | type repositoriesResponse struct { 18 | Results []repository `json:"results"` 19 | } 20 | 21 | func (a *API) RepositoriesHandler(c echo.Context) error { 22 | repositories := &configv1alpha1.TerraformRepositoryList{} 23 | err := a.Client.List(context.Background(), repositories) 24 | if err != nil { 25 | log.Errorf("could not list TerraformRepositories: %s", err) 26 | return c.String(http.StatusInternalServerError, "could not list terraform repositories") 27 | } 28 | results := []repository{} 29 | for _, r := range repositories.Items { 30 | results = append(results, repository{ 31 | Name: fmt.Sprintf("%s/%s", r.Namespace, r.Name), 32 | }) 33 | } 34 | return c.JSON(http.StatusOK, &repositoriesResponse{ 35 | Results: results, 36 | }, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /internal/webhook/event/testdata/pullrequest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformPullRequest 3 | metadata: 4 | name: burrito-closed-single-pr-42 5 | namespace: default 6 | spec: 7 | provider: github 8 | branch: feature/branch 9 | id: "42" 10 | base: main 11 | repository: 12 | name: burrito-closed-single-pr 13 | namespace: default 14 | --- 15 | apiVersion: config.terraform.padok.cloud/v1alpha1 16 | kind: TerraformPullRequest 17 | metadata: 18 | name: burrito-closed-multi-pr-1-42 19 | namespace: default 20 | spec: 21 | provider: github 22 | branch: feature/branch 23 | id: "42" 24 | base: main 25 | repository: 26 | name: burrito-closed-multi-pr-1 27 | namespace: default 28 | --- 29 | apiVersion: config.terraform.padok.cloud/v1alpha1 30 | kind: TerraformPullRequest 31 | metadata: 32 | name: burrito-closed-multi-pr-2-42 33 | namespace: default 34 | spec: 35 | provider: github 36 | branch: feature/branch 37 | id: "42" 38 | base: main 39 | repository: 40 | name: burrito-closed-multi-pr-2 41 | namespace: default 42 | -------------------------------------------------------------------------------- /internal/utils/url/url.go: -------------------------------------------------------------------------------- 1 | package url 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Normalize a Github/Gitlab URL (SSH or HTTPS) to a HTTPS URL 9 | func NormalizeUrl(url string) string { 10 | if strings.Contains(url, "https://") { 11 | return removeGitExtension(url) 12 | } 13 | if strings.Contains(url, "http://") { 14 | return removeGitExtension("https://" + url[7:]) 15 | } 16 | // All SSH URL from GitHub are like "git@padok.github.com:/.git" 17 | // We split on ":" then remove ".git" by removing the last characters 18 | // To handle enterprise GitHub, we dynamically get "padok.github.com" 19 | // By removing "git@" at the beginning of the string 20 | server, repo := splitSSHUrl(url) 21 | return fmt.Sprintf("https://%s/%s", server, repo) 22 | } 23 | 24 | func splitSSHUrl(url string) (server string, repo string) { 25 | split := strings.Split(url, ":") 26 | return split[0][4:], removeGitExtension(split[1]) 27 | } 28 | 29 | func removeGitExtension(url string) string { 30 | if strings.HasSuffix(url, ".git") { 31 | return url[:len(url)-4] 32 | } 33 | return url 34 | } 35 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.networkPolicy.enabled }} 2 | {{- if and .Values.networkPolicy.ingressFromTenants.enabled (gt (len .Values.tenants) 0) }} 3 | --- 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | name: allow-traffic-from-burrito-tenants 8 | {{- with mergeOverwrite (deepCopy .Values.global.metadata) .Values.networkPolicy.metadata }} 9 | labels: 10 | {{- toYaml .labels | nindent 4}} 11 | annotations: 12 | {{- toYaml .annotations | nindent 4}} 13 | {{- end }} 14 | spec: 15 | podSelector: {} 16 | policyTypes: 17 | - Ingress 18 | ingress: 19 | # Allow all traffic from tenant namespaces 20 | {{- range .Values.tenants }} 21 | {{- if .namespace.create }} 22 | - from: 23 | - namespaceSelector: 24 | matchLabels: 25 | kubernetes.io/metadata.name: {{ .namespace.name }} 26 | {{- end }} 27 | {{- end }} 28 | {{- with .Values.networkPolicy.ingressFromTenants.additionalIngressRules }} 29 | {{- toYaml . | nindent 4 }} 30 | {{- end }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /docs/installation/with-static-manifests.md: -------------------------------------------------------------------------------- 1 | # Install burrito with static manifests 2 | 3 | ## Requirements 4 | 5 | - Installed [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line tool. 6 | - Have a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) file (default location is `~/.kube/config`). 7 | 8 | ## Install burrito 9 | 10 | !!! info 11 | This will install a mono-tenant version of burrito. See the [Helm installation method](./with-helm.md) for a [multi-tenant-architecture](..//operator-manual/multi-tenant-architecture.md). 12 | 13 | ```bash 14 | kubectl create namespace burrito 15 | kubectl apply -n burrito -f https://raw.githubusercontent.com/padok-team/burrito/main/manifests/install.yaml 16 | ``` 17 | 18 | This will create a new namespace, `burrito`, where burrito services will live. 19 | 20 | !!! warning 21 | The installation manifests include `ClusterRoleBinding` resources that reference `burrito` namespace. If you are installing burrito into a different namespace then make sure to update the namespace reference. 22 | -------------------------------------------------------------------------------- /ui/src/components/widgets/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ProgressBarProps { 4 | /** 5 | * Progress value between 0 and 100 6 | */ 7 | value: number; 8 | label?: string; 9 | color?: string; 10 | className?: string; 11 | } 12 | 13 | const ProgressBar: React.FC = ({ 14 | value, 15 | label, 16 | color = 'bg-blue-500', 17 | className = '' 18 | }) => { 19 | // Ensure the value is between 0 and 100 20 | const normalizedValue = Math.min(Math.max(value, 0), 100); 21 | 22 | return ( 23 |
27 |
35 | {label && {label}} 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default ProgressBar; 42 | -------------------------------------------------------------------------------- /internal/burrito/burrito.go: -------------------------------------------------------------------------------- 1 | package burrito 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/padok-team/burrito/internal/burrito/config" 8 | "github.com/padok-team/burrito/internal/controllers" 9 | "github.com/padok-team/burrito/internal/datastore" 10 | "github.com/padok-team/burrito/internal/runner" 11 | "github.com/padok-team/burrito/internal/server" 12 | ) 13 | 14 | type App struct { 15 | Config *config.Config 16 | 17 | Runner Runner 18 | Controllers Controllers 19 | Server Server 20 | Datastore Datastore 21 | 22 | Out io.Writer 23 | Err io.Writer 24 | } 25 | 26 | type Server interface { 27 | Exec() 28 | } 29 | 30 | type Runner interface { 31 | Exec() error 32 | } 33 | 34 | type Controllers interface { 35 | Exec() 36 | } 37 | 38 | type Datastore interface { 39 | Exec() 40 | } 41 | 42 | func New() (*App, error) { 43 | c := &config.Config{} 44 | app := &App{ 45 | Config: c, 46 | Runner: runner.New(c), 47 | Controllers: controllers.New(c), 48 | Server: server.New(c), 49 | Datastore: datastore.New(c), 50 | Out: os.Stdout, 51 | Err: os.Stderr, 52 | } 53 | return app, nil 54 | } 55 | -------------------------------------------------------------------------------- /deploy/charts/burrito/values-example.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | deployment: 3 | image: 4 | tag: "custom-version" 5 | 6 | config: 7 | burrito: 8 | controller: 9 | timers: 10 | driftDetection: 5m 11 | 12 | controllers: 13 | deployment: 14 | envFrom: 15 | - secretRef: 16 | name: burrito-gh-token 17 | 18 | server: 19 | deployment: 20 | envFrom: 21 | - secretRef: 22 | name: burrito-webhook-secret 23 | ingress: 24 | enabled: true 25 | annotations: 26 | ingress.kubernetes.io/ssl-redirect: "true" 27 | ingressClassName: nginx 28 | host: burrito.padok.cloud 29 | tls: 30 | - secretName: wildcard-padok-cloud-tls 31 | 32 | tenants: 33 | - namespace: 34 | create: true 35 | name: "burrito-project-1" 36 | labels: {} 37 | annotations: {} 38 | serviceAccounts: 39 | - name: runner-project-1 40 | additionalRoleBindings: 41 | - name: custom 42 | role: 43 | kind: ClusterRole 44 | name: my-custom-role 45 | annotations: 46 | iam.cloud.provider/role: cloud-provider-role 47 | labels: {} 48 | -------------------------------------------------------------------------------- /ui/src/assets/icons/BarsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const BarsIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default BarsIcon; 10 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/testdata/repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRepository 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: in-cluster-burrito 6 | name: burrito 7 | namespace: default 8 | spec: 9 | overrideRunnerSpec: 10 | imagePullSecrets: 11 | - name: ghcr-creds 12 | repository: 13 | url: git@github.com:padok-team/burrito-examples.git 14 | terraform: 15 | enabled: true 16 | --- 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: burrito-repo 21 | namespace: default 22 | type: credentials.burrito.tf/repository 23 | stringData: 24 | provider: mock 25 | url: git@github.com:padok-team/burrito-examples.git 26 | --- 27 | apiVersion: config.terraform.padok.cloud/v1alpha1 28 | kind: TerraformRepository 29 | metadata: 30 | labels: 31 | app.kubernetes.io/instance: in-cluster-burrito 32 | name: burrito-no-provider 33 | namespace: default 34 | spec: 35 | overrideRunnerSpec: 36 | imagePullSecrets: 37 | - name: ghcr-creds 38 | repository: 39 | url: git@github.com:padok-team/burrito-examples-no-git-provider.git 40 | terraform: 41 | enabled: true 42 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ArrowResizeDiagonalIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ArrowResizeDiagonalIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default ArrowResizeDiagonalIcon; 10 | -------------------------------------------------------------------------------- /internal/server/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/padok-team/burrito/internal/server/utils" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | type AuthHandlers interface { 12 | HandleLogin(c echo.Context) error 13 | HandleCallback(c echo.Context) error 14 | GetLoginHTTPMethod() string 15 | } 16 | 17 | func HandleLogout(c echo.Context, sessionCookie string) error { 18 | err := utils.InvalidateSession(c, sessionCookie) 19 | if err != nil { 20 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to log out"}) 21 | } 22 | 23 | return c.Redirect(http.StatusTemporaryRedirect, "/login") 24 | } 25 | 26 | func HandleUserInfo(c echo.Context) error { 27 | // Check if the user is authenticated 28 | if c.Get("user_id") == nil { 29 | return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) 30 | } 31 | userEmail := c.Get("user_email") 32 | name := c.Get("user_name") 33 | id := c.Get("user_id") 34 | picture := c.Get("user_picture") 35 | 36 | return c.JSON(http.StatusOK, map[string]string{"email": userEmail.(string), "name": name.(string), "id": id.(string), "picture": picture.(string)}) 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ArrowLeftIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default ArrowLeftIcon; 10 | -------------------------------------------------------------------------------- /deploy/charts/burrito/values-debug.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | burrito: 3 | runner: 4 | image: 5 | repository: burrito 6 | tag: DEV_TAG 7 | pullPolicy: Never 8 | # command: ["/usr/local/bin/dlv"] 9 | # args: ["--listen=0.0.0.0:2346", "--headless=true", "--accept-multiclient", "--api-version=2", "--log", "exec", "/usr/local/bin/burrito", "runner", "start"] 10 | datastore: 11 | storage: 12 | mock: true 13 | global: 14 | deployment: 15 | image: 16 | repository: burrito 17 | tag: DEV_TAG 18 | pullPolicy: Never 19 | tenants: 20 | - namespace: 21 | create: true 22 | name: "burrito-project" 23 | 24 | # controllers: 25 | # deployment: 26 | # mode: Debug 27 | # command: ["/usr/local/bin/dlv"] 28 | # args: ["--listen=0.0.0.0:2345", "--headless=true", "--accept-multiclient", "--api-version=2", "--log", "exec", "/usr/local/bin/burrito", "controllers", "start"] 29 | 30 | # datastore: 31 | # deployment: 32 | # mode: Debug 33 | # command: ["/usr/local/bin/dlv"] 34 | # args: ["--listen=0.0.0.0:2347", "--headless=true", "--accept-multiclient", "--api-version=2", "--log", "exec", "/usr/local/bin/burrito", "datastore", "start"] 35 | -------------------------------------------------------------------------------- /internal/repository/types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "net/http" 5 | 6 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 7 | 8 | "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment" 9 | "github.com/padok-team/burrito/internal/webhook/event" 10 | ) 11 | 12 | type Provider interface { 13 | GetWebhookProvider() (WebhookProvider, error) 14 | GetAPIProvider() (APIProvider, error) 15 | GetGitProvider(repository *configv1alpha1.TerraformRepository) (GitProvider, error) 16 | } 17 | 18 | type GitProvider interface { 19 | GetLatestRevisionForRef(ref string) (string, error) 20 | Bundle(ref string) ([]byte, error) 21 | GetChanges(previousCommit, currentCommit string) []string 22 | } 23 | 24 | type WebhookProvider interface { 25 | ParseWebhookPayload(r *http.Request) (interface{}, bool) 26 | GetEventFromWebhookPayload(interface{}) (event.Event, error) 27 | } 28 | 29 | type APIProvider interface { 30 | GetChanges(repository *configv1alpha1.TerraformRepository, pullRequest *configv1alpha1.TerraformPullRequest) ([]string, error) 31 | Comment(repository *configv1alpha1.TerraformRepository, pullRequest *configv1alpha1.TerraformPullRequest, comment comment.Comment) error 32 | } 33 | -------------------------------------------------------------------------------- /internal/utils/url/url_test.go: -------------------------------------------------------------------------------- 1 | package url_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/padok-team/burrito/internal/utils/url" 7 | ) 8 | 9 | func TestNormalizeURLFullRepos(t *testing.T) { 10 | urlTypes := []string{ 11 | "git@github.com:padok-team/burrito.git", 12 | "git@github.com:padok-team/burrito", 13 | "https://github.com/padok-team/burrito.git", 14 | "https://github.com/padok-team/burrito", 15 | "http://github.com/padok-team/burrito.git", 16 | "http://github.com/padok-team/burrito", 17 | } 18 | expected := "https://github.com/padok-team/burrito" 19 | for _, u := range urlTypes { 20 | normalized := url.NormalizeUrl(u) 21 | if normalized != expected { 22 | t.Errorf("Passed: %s, Expected %s, got %s", u, expected, normalized) 23 | } 24 | } 25 | } 26 | 27 | func TestNormalizeURLPrefixes(t *testing.T) { 28 | urlTypes := []string{ 29 | "git@github.com:padok-team", 30 | "https://github.com/padok-team", 31 | "http://github.com/padok-team", 32 | } 33 | expected := "https://github.com/padok-team" 34 | for _, u := range urlTypes { 35 | normalized := url.NormalizeUrl(u) 36 | if normalized != expected { 37 | t.Errorf("Passed: %s, Expected %s, got %s", u, expected, normalized) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/src/components/widgets/Tag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { LayerState } from '@/clients/layers/types'; 4 | 5 | export interface TagProps { 6 | variant: LayerState; 7 | } 8 | 9 | const Tag: React.FC = ({ variant }) => { 10 | const styles = { 11 | success: `bg-status-success-default 12 | text-nuances-black`, 13 | warning: `bg-status-warning-default 14 | text-nuances-black`, 15 | error: `bg-status-error-default 16 | text-nuances-white`, 17 | disabled: `bg-nuances-50 18 | text-nuances-200` 19 | }; 20 | 21 | const getContent = () => { 22 | switch (variant) { 23 | case 'success': 24 | return 'OK'; 25 | case 'warning': 26 | return 'OutOfSync'; 27 | case 'error': 28 | return 'Error'; 29 | case 'disabled': 30 | return 'Disabled'; 31 | } 32 | }; 33 | 34 | return ( 35 |
48 | {getContent()} 49 |
50 | ); 51 | }; 52 | 53 | export default Tag; 54 | -------------------------------------------------------------------------------- /internal/controllers/terraformlayer/testdata/unknown-cases.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: config.terraform.padok.cloud/v1alpha1 3 | kind: TerraformLayer 4 | metadata: 5 | name: non-existent-repository 6 | namespace: default 7 | annotations: 8 | webhook.terraform.padok.cloud/relevant-commit: cb9f15b90861c8c4364cdde63d17837c7a9ccca9 9 | spec: 10 | branch: main 11 | path: nominal-case-one/ 12 | remediationStrategy: 13 | autoApply: true 14 | repository: 15 | name: non-existent 16 | namespace: default 17 | terraform: 18 | enabled: true 19 | version: 1.3.1 20 | terragrunt: 21 | enabled: true 22 | version: 0.45.4 23 | 24 | --- 25 | apiVersion: config.terraform.padok.cloud/v1alpha1 26 | kind: TerraformLayer 27 | metadata: 28 | name: last-run-not-exist 29 | namespace: default 30 | annotations: 31 | webhook.terraform.padok.cloud/relevant-commit: cb9f15b90861c8c4364cdde63d17837c7a9ccca9 32 | spec: 33 | branch: main 34 | path: last-run-not-exist/ 35 | remediationStrategy: 36 | autoApply: true 37 | repository: 38 | name: burrito 39 | namespace: default 40 | terraform: 41 | enabled: true 42 | version: 1.3.1 43 | status: 44 | lastRun: 45 | name: run-does-not-exist 46 | namespace: default 47 | -------------------------------------------------------------------------------- /internal/server/api/runs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/labstack/echo/v4" 9 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 10 | "k8s.io/apimachinery/pkg/types" 11 | ) 12 | 13 | type GetAttemptsResponse struct { 14 | Count int `json:"count"` 15 | } 16 | 17 | func getRunAttemptArgs(c echo.Context) (string, string, error) { 18 | namespace := c.Param("namespace") 19 | run := c.Param("run") 20 | if namespace == "" || run == "" { 21 | return "", "", fmt.Errorf("missing query parameters") 22 | } 23 | return namespace, run, nil 24 | } 25 | 26 | func (a *API) GetAttemptsHandler(c echo.Context) error { 27 | namespace, run, err := getRunAttemptArgs(c) 28 | if err != nil { 29 | return c.String(http.StatusBadRequest, err.Error()) 30 | } 31 | runObject := &configv1alpha1.TerraformRun{} 32 | err = a.Client.Get(context.Background(), types.NamespacedName{Name: run, Namespace: namespace}, runObject) 33 | if err != nil { 34 | return c.String(http.StatusInternalServerError, "could not get run attempt, there's an issue with the cluster: "+err.Error()) 35 | } 36 | response := GetAttemptsResponse{Count: len(runObject.Status.Attempts)} 37 | return c.JSON(http.StatusOK, &response) 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/assets/icons/CheckIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const CheckIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default CheckIcon; 10 | -------------------------------------------------------------------------------- /docs/operator-manual/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This guide is for administrators and operators wanting to install and configure Burrito for other developers. 4 | 5 | !!! note 6 | Please make sure you've completed the [getting started guide](../getting-started.md). 7 | 8 | ## In this section 9 | 10 | - [Architecture](./architecture.md) - Overview of Burrito's architecture and components 11 | - [Repository Controller](./repository-controller.md) - How Burrito manages Git repositories, bundles, and revisions 12 | - [Git Authentication](./git-authentication.md) - Setting up authentication for Git repositories 13 | - [PR/MR Workflow](./pr-mr-workflow.md) - Configuration for handling pull requests and merge requests 14 | - [Git Webhook](./git-webhook.md) - Setting up webhooks for repository events 15 | - [Advanced Configuration](./advanced-configuration.md) - Additional configuration options 16 | - [Multi-tenant Architecture](./multi-tenant-architecture.md) - Running Burrito in a multi-tenant environment 17 | - [Datastore](./datastore.md) - Configuration of the datastore component 18 | - [Provider Caching](./provider-caching.md) - Caching Terraform providers for better performance 19 | - [Runner Scheduling](./runner-scheduling.md) - Configuring how Terraform runners are scheduled 20 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ArrowRightIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ArrowRightIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default ArrowRightIcon; 10 | -------------------------------------------------------------------------------- /internal/server/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "time" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/labstack/echo/v4/middleware" 10 | ) 11 | 12 | var LoggerMiddlewareConfig = middleware.RequestLoggerConfig{ 13 | LogLatency: true, 14 | LogRemoteIP: true, 15 | LogHost: true, 16 | LogMethod: true, 17 | LogURI: true, 18 | LogStatus: true, 19 | LogError: true, 20 | LogResponseSize: true, 21 | LogContentLength: true, 22 | LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { 23 | email := "unauthenticated" 24 | if e := c.Get("user_email"); e != nil { 25 | email, _ = e.(string) 26 | } 27 | log.WithFields(log.Fields{ 28 | "time": time.Now().Format(time.RFC3339Nano), 29 | "remote_ip": v.RemoteIP, 30 | "host": v.Host, 31 | "method": v.Method, 32 | "uri": v.URI, 33 | "status": v.Status, 34 | "error": v.Error, 35 | "latency": v.Latency.Nanoseconds() / 1000, 36 | "latency_human": v.Latency, 37 | "bytes_in": v.ContentLength, 38 | "bytes_out": v.ResponseSize, 39 | "email": email, 40 | }).Info("request") 41 | return nil 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /internal/controllers/terraformrepository/testdata/error-case.yaml: -------------------------------------------------------------------------------- 1 | # Repository with an invalid URL repository 2 | --- 3 | apiVersion: config.terraform.padok.cloud/v1alpha1 4 | kind: TerraformRepository 5 | metadata: 6 | name: repo-unknown 7 | namespace: default 8 | spec: 9 | repository: 10 | url: https://git.mock.com/unknown 11 | terraform: 12 | enabled: true 13 | --- 14 | apiVersion: config.terraform.padok.cloud/v1alpha1 15 | kind: TerraformLayer 16 | metadata: 17 | name: repo-unknown-layer 18 | namespace: default 19 | spec: 20 | branch: branch 21 | path: layer/ 22 | repository: 23 | name: repo-unknown 24 | namespace: default 25 | --- 26 | # Repository with an invalid URL repository 27 | --- 28 | apiVersion: config.terraform.padok.cloud/v1alpha1 29 | kind: TerraformRepository 30 | metadata: 31 | name: repo-with-last-sync-failed 32 | namespace: default 33 | spec: 34 | repository: 35 | url: https://git.mock.com/unknown 36 | terraform: 37 | enabled: true 38 | --- 39 | apiVersion: config.terraform.padok.cloud/v1alpha1 40 | kind: TerraformLayer 41 | metadata: 42 | name: repo-with-last-sync-failed-layer 43 | namespace: default 44 | spec: 45 | branch: branch 46 | path: layer/ 47 | repository: 48 | name: repo-with-last-sync-failed 49 | namespace: default 50 | -------------------------------------------------------------------------------- /internal/server/utils/sessions.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/labstack/echo-contrib/session" 7 | "github.com/labstack/echo/v4" 8 | "github.com/labstack/gommon/log" 9 | ) 10 | 11 | // RemoveSessionCookie manually removes the session cookie from the response. 12 | // This is useful when the session does not exist server-side, but client-side cookies still exist 13 | func RemoveSessionCookie(c echo.Context, sessionCookie string) error { 14 | http.SetCookie(c.Response(), &http.Cookie{ 15 | Name: sessionCookie, 16 | Value: "", 17 | Path: "/", 18 | MaxAge: -1, 19 | HttpOnly: true, 20 | Secure: c.Request().TLS != nil, 21 | }) 22 | return nil 23 | } 24 | 25 | // InvalidateSession clears the session data and sets the session cookie to expire immediately. 26 | func InvalidateSession(c echo.Context, sessionCookie string) error { 27 | sess, err := session.Get(sessionCookie, c) 28 | if err != nil { 29 | log.Warn("Tried to invalidate session, but session was not found") 30 | } 31 | 32 | sess.Values = make(map[interface{}]interface{}) 33 | sess.Options.MaxAge = -1 34 | 35 | if err := sess.Save(c.Request(), c.Response()); err != nil { 36 | return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save session") 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createContext } from 'react'; 2 | 3 | interface ThemeContextProps { 4 | theme: 'light' | 'dark'; 5 | setTheme: (theme: 'light' | 'dark') => void; 6 | } 7 | 8 | export const ThemeContext = createContext({ 9 | theme: 'light', 10 | setTheme: () => {} 11 | }); 12 | 13 | const getInitialTheme = () => { 14 | const theme = localStorage.getItem('theme'); 15 | if (theme === 'light' || theme === 'dark') { 16 | return theme; 17 | } 18 | 19 | const userMedia = matchMedia('(prefers-color-scheme: dark)'); 20 | if (userMedia.matches) { 21 | localStorage.setItem('theme', 'dark'); 22 | return 'dark'; 23 | } else { 24 | localStorage.setItem('theme', 'light'); 25 | return 'light'; 26 | } 27 | }; 28 | 29 | interface ThemeProviderProps { 30 | children: React.ReactNode; 31 | } 32 | 33 | const ThemeProvider: React.FC = ({ children }) => { 34 | const [theme, setTheme] = useState<'light' | 'dark'>(getInitialTheme()); 35 | 36 | useEffect(() => { 37 | localStorage.setItem('theme', theme); 38 | }, [theme]); 39 | 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | }; 46 | 47 | export default ThemeProvider; 48 | -------------------------------------------------------------------------------- /ui/src/components/buttons/SocialButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Button from '@/components/core/Button'; 4 | import GithubIcon from '@/assets/icons/GithubIcon'; 5 | import GitlabIcon from '@/assets/icons/GitlabIcon'; 6 | 7 | export interface SocialButtonProps { 8 | className?: string; 9 | variant: 'github' | 'gitlab'; 10 | isLoading?: boolean; 11 | onClick?: () => void; 12 | } 13 | 14 | const SocialButton: React.FC = ({ 15 | className, 16 | variant, 17 | isLoading, 18 | onClick 19 | }) => { 20 | const getContent = () => { 21 | switch (variant) { 22 | case 'github': 23 | return ( 24 | <> 25 | 26 | Login with GitHub 27 | 28 | ); 29 | case 'gitlab': 30 | return ( 31 | <> 32 | 33 | Login with Gitlab 34 | 35 | ); 36 | } 37 | }; 38 | 39 | return ( 40 | 50 | ); 51 | }; 52 | 53 | export default SocialButton; 54 | -------------------------------------------------------------------------------- /ui/src/assets/icons/GitlabIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const GitlabIcon = (props: SVGProps) => ( 4 | 11 | 15 | 19 | 23 | 27 | 28 | ); 29 | 30 | export default GitlabIcon; 31 | -------------------------------------------------------------------------------- /docs/operator-manual/runner-scheduling.md: -------------------------------------------------------------------------------- 1 | # Fine-tuning the scheduling of runner pods 2 | 3 | Burrito creates runner pods to execute plans and apply changes on your infrastructure. The scheduling of these pods can be fine-tuned to better fit your needs. (e.g. to avoid running too many pods at the same time, or to reduce the cost of your underlying infrastructure). 4 | 5 | ## Limit the number of runner pods in parallel 6 | 7 | By default, Burrito does not limit the number of runner pods that can run in parallel. This can lead to a high number of pods running at the same time, which can be costly or can overload your infrastructure. 8 | 9 | It is possible to limit the number of runner pods that can run in parallel by setting the `BURRITO_CONTROLLER_MAXCONCURRENTRUNNERPODS` environment variable in the controller, or by setting the `config.burrito.controller.maxConcurrentRunnerPods` value in the [Helm chart values file](https://github.com/padok-team/burrito/blob/main/deploy/charts/burrito/values.yaml). 10 | 11 | You can also set this value in the TerraformRepository CRD by setting the `spec.maxConcurrentRunnerPods` field. 12 | 13 | If the value of this parameter is set to `0`, there is no limit to the number of runner pods that can run in parallel. 14 | 15 | When Burrito creates a pod, if the setting is both set in the controller and in the TerraformRepository, the TerraformRepository value will take precedence. 16 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/issuer.yaml: -------------------------------------------------------------------------------- 1 | {{- if or (and .Values.hermitcrab.enabled .Values.hermitcrab.tls.certManager.use) (and .Values.datastore.tls.enabled .Values.datastore.tls.certManager.use) }} 2 | apiVersion: cert-manager.io/v1 3 | kind: Issuer 4 | metadata: 5 | name: burrito-selfsigned-issuer 6 | {{- with .Values.global.metadata }} 7 | labels: 8 | {{- toYaml .labels | nindent 4}} 9 | annotations: 10 | {{- toYaml .annotations | nindent 4}} 11 | {{- end }} 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1 16 | kind: Certificate 17 | metadata: 18 | name: burrito-ca 19 | {{- with .Values.global.metadata}} 20 | labels: 21 | {{- toYaml .labels | nindent 4}} 22 | annotations: 23 | {{- toYaml .annotations | nindent 4}} 24 | {{- end }} 25 | spec: 26 | isCA: true 27 | commonName: burrito-ca 28 | secretName: burrito-ca 29 | privateKey: 30 | algorithm: ECDSA 31 | size: 256 32 | issuerRef: 33 | name: burrito-selfsigned-issuer 34 | kind: Issuer 35 | group: cert-manager.io 36 | --- 37 | apiVersion: cert-manager.io/v1 38 | kind: Issuer 39 | metadata: 40 | name: burrito-ca-issuer 41 | {{- with .Values.global.metadata}} 42 | labels: 43 | {{- toYaml .labels | nindent 4}} 44 | annotations: 45 | {{- toYaml .annotations | nindent 4}} 46 | {{- end }} 47 | spec: 48 | ca: 49 | secretName: burrito-ca 50 | {{- end }} 51 | -------------------------------------------------------------------------------- /ui/src/assets/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const SearchIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default SearchIcon; 10 | -------------------------------------------------------------------------------- /api/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1alpha1 contains API Schema definitions for the config v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=config.terraform.padok.cloud 20 | package v1alpha1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "config.terraform.padok.cloud", Version: "v1alpha1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /deploy/charts/burrito/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: burrito 3 | description: A Helm chart for handling a complete burrito deployment 4 | type: application 5 | version: 0.9.1 6 | appVersion: "v0.9.1" 7 | home: https://docs.burrito.tf/ 8 | icon: https://raw.githubusercontent.com/padok-team/burrito/refs/heads/main/docs/assets/icon/burrito.png 9 | sources: 10 | - https://github.com/padok-team/burrito 11 | annotations: 12 | artifacthub.io/crds: | 13 | - kind: TerraformLayer 14 | version: v1alpha1 15 | name: terraformlayers.config.terraform.padok.cloud 16 | displayName: TerraformLayer 17 | description: TerraformLayer is the Schema for the TerraformLayers API. 18 | - kind: TerraformPullRequest 19 | version: v1alpha1 20 | name: terraformpullrequests.config.terraform.padok.cloud 21 | displayName: TerraformPullRequest 22 | description: TerraformPullRequest is the Schema for the TerraformPullRequests API. 23 | - kind: TerraformRepository 24 | version: v1alpha1 25 | name: terraformrepositories.config.terraform.padok.cloud 26 | displayName: TerraformRepository 27 | description: TerraformRepository is the Schema for the TerraformRepositories API. 28 | - kind: TerraformRun 29 | version: v1alpha1 30 | name: terraformruns.config.terraform.padok.cloud 31 | displayName: TerraformRun 32 | description: TerraformRun is the Schema for the TerraformRuns API. 33 | -------------------------------------------------------------------------------- /internal/controllers/terraformlayer/testdata/merge-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformLayer 3 | metadata: 4 | name: merge-case-1 5 | namespace: default 6 | annotations: 7 | webhook.terraform.padok.cloud/relevant-commit: cb9f15b90861c8c4364cdde63d17837c7a9ccca9 8 | spec: 9 | branch: main 10 | path: merge-case-one/ 11 | remediationStrategy: 12 | autoApply: true 13 | repository: 14 | name: burrito 15 | namespace: default 16 | terraform: 17 | enabled: true 18 | version: 1.3.1 19 | terragrunt: 20 | enabled: true 21 | version: 0.45.4 22 | overrideRunnerSpec: 23 | imagePullSecrets: 24 | - name: gh-token 25 | image: ghcr.io/padok-team/super-burrito:v0.1.0 26 | tolerations: 27 | - effect: "NoSchedule" 28 | key: "padok.cloud/no-schedule" 29 | nodeSelector: 30 | padok.cloud: "true" 31 | serviceAccountName: "test" 32 | resources: 33 | limits: 34 | cpu: 1 35 | memory: 0.5 36 | env: 37 | - name: "test" 38 | value: "test" 39 | envFrom: 40 | - secretRef: 41 | name: test 42 | optional: true 43 | volumeMounts: 44 | - mountPath: /test 45 | name: test 46 | readOnly: true 47 | volumes: 48 | - name: test 49 | emptyDir: {} 50 | metadata: 51 | annotations: 52 | padok.cloud: "test" 53 | labels: 54 | test: "true" 55 | -------------------------------------------------------------------------------- /.github/workflows/ci-frontend.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration for Frontend 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - ".github/workflows/ci-frontend.yaml" 12 | - "ui/**" 13 | 14 | env: 15 | NODE_VERSION: 22 16 | 17 | jobs: 18 | build: 19 | name: Build UI 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v6 23 | - uses: actions/setup-node@v6 24 | with: 25 | node-version: "${{ env.NODE_VERSION }}" 26 | - name: Run install 27 | uses: borales/actions-yarn@v5 28 | with: 29 | cmd: install 30 | dir: ui 31 | - name: Run build 32 | uses: borales/actions-yarn@v5 33 | with: 34 | cmd: build 35 | dir: ui 36 | 37 | lint: 38 | name: Lint TS 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v6 42 | - uses: actions/setup-node@v6 43 | with: 44 | node-version: "${{ env.NODE_VERSION }}" 45 | - name: Run install 46 | uses: borales/actions-yarn@v5 47 | with: 48 | cmd: install 49 | dir: ui 50 | - name: Run eslint 51 | uses: borales/actions-yarn@v5 52 | with: 53 | cmd: lint 54 | dir: ui 55 | - name: Run prettier 56 | uses: borales/actions-yarn@v5 57 | with: 58 | cmd: format-check 59 | dir: ui 60 | -------------------------------------------------------------------------------- /internal/controllers/terraformrun/testdata/controller/error-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRun 3 | metadata: 4 | name: error-case-1 5 | namespace: default 6 | spec: 7 | action: plan 8 | layer: 9 | name: unknown-layer 10 | namespace: default 11 | revision: TEST_REVISION 12 | --- 13 | apiVersion: config.terraform.padok.cloud/v1alpha1 14 | kind: TerraformLayer 15 | metadata: 16 | name: unknown-repo-layer-1 17 | namespace: default 18 | spec: 19 | branch: main 20 | path: error/ 21 | repository: 22 | name: unknown-repo 23 | namespace: default 24 | revision: TEST_REVISION 25 | --- 26 | apiVersion: config.terraform.padok.cloud/v1alpha1 27 | kind: TerraformRun 28 | metadata: 29 | name: error-case-2 30 | namespace: default 31 | spec: 32 | action: plan 33 | layer: 34 | name: unknown-repo-layer-1 35 | namespace: default 36 | revision: TEST_REVISION 37 | --- 38 | apiVersion: config.terraform.padok.cloud/v1alpha1 39 | kind: TerraformRun 40 | metadata: 41 | name: error-case-3 42 | namespace: default 43 | spec: 44 | action: apply 45 | layer: 46 | name: error-case-1 47 | namespace: default 48 | revision: TEST_REVISION 49 | --- 50 | apiVersion: config.terraform.padok.cloud/v1alpha1 51 | kind: TerraformRun 52 | metadata: 53 | name: error-case-4 54 | namespace: default 55 | spec: 56 | action: apply 57 | layer: 58 | name: nominal-case-1 59 | namespace: default 60 | revision: UNKNOWN_REVISION 61 | -------------------------------------------------------------------------------- /ui/src/assets/avocado/AvocadoSeed.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const AvocadoSeed = (props: SVGProps) => ( 4 | 12 | 18 | 24 | 30 | 36 | 41 | 42 | ); 43 | 44 | export default AvocadoSeed; 45 | -------------------------------------------------------------------------------- /ui/src/assets/icons/TimesIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const TimesIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default TimesIcon; 10 | -------------------------------------------------------------------------------- /internal/controllers/terraformlayer/testdata/webhook-issue-case.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformLayer 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: in-cluster-burrito 6 | name: webhook-issue-case-1 7 | namespace: default 8 | annotations: 9 | runner.terraform.padok.cloud/plan-commit: ca9b6c80ac8fb5cd837ae9b374b79ff33f472558 10 | runner.terraform.padok.cloud/plan-date: Mon May 8 11:21:53 UTC 2023 11 | runner.terraform.padok.cloud/plan-sum: AuP6pMNxWsbSZKnxZvxD842wy0qaF9JCX8HW1nFeL1I= 12 | runner.terraform.padok.cloud/plan-run: run-succeeded/0 13 | runner.terraform.padok.cloud/apply-commit: f4f5c237f34bac134e4503c40de9d142c1e04077 14 | runner.terraform.padok.cloud/apply-date: Sun May 7 11:21:53 UTC 2023 15 | runner.terraform.padok.cloud/apply-sum: AuP6pMNxWsD842wy0qabSZKnxZvxF9JCX8HW1nFeL1I= 16 | webhook.terraform.padok.cloud/branch-commit: f4f5c237f34bac134e4503c40de9d142c1e04077 17 | webhook.terraform.padok.cloud/branch-commit-date: Sun May 7 11:21:53 UTC 2023 18 | webhook.terraform.padok.cloud/relevant-commit: f4f5c237f34bac134e4503c40de9d142c1e04077 19 | webhook.terraform.padok.cloud/relevant-commit-date: Sun May 7 11:21:53 UTC 2023 20 | spec: 21 | branch: main 22 | path: webhook-issue-case-1/ 23 | remediationStrategy: 24 | autoApply: true 25 | repository: 26 | name: burrito 27 | namespace: default 28 | terraform: 29 | enabled: true 30 | version: 1.3.1 31 | terragrunt: 32 | enabled: true 33 | version: 0.45.4 34 | -------------------------------------------------------------------------------- /ui/src/assets/icons/AppsIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const AppsIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default AppsIcon; 10 | -------------------------------------------------------------------------------- /internal/server/api/sync.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/labstack/echo/v4" 8 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 9 | "github.com/padok-team/burrito/internal/annotations" 10 | "github.com/padok-team/burrito/internal/server/utils" 11 | log "github.com/sirupsen/logrus" 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | func (a *API) SyncLayerHandler(c echo.Context) error { 16 | layer := &configv1alpha1.TerraformLayer{} 17 | err := a.Client.Get(context.Background(), client.ObjectKey{ 18 | Namespace: c.Param("namespace"), 19 | Name: c.Param("layer"), 20 | }, layer) 21 | if err != nil { 22 | log.Errorf("could not get terraform layer: %s", err) 23 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "An error occurred while getting the layer"}) 24 | } 25 | syncStatus := utils.GetManualSyncStatus(*layer) 26 | if syncStatus == utils.ManualSyncAnnotated || syncStatus == utils.ManualSyncPending { 27 | return c.JSON(http.StatusConflict, map[string]string{"error": "Layer sync already triggered"}) 28 | } 29 | 30 | err = annotations.Add(context.Background(), a.Client, layer, map[string]string{ 31 | annotations.SyncNow: "true", 32 | }) 33 | if err != nil { 34 | log.Errorf("could not update terraform layer annotations: %s", err) 35 | return c.JSON(http.StatusInternalServerError, map[string]string{"error": "An error occurred while updating the layer annotations"}) 36 | } 37 | return c.JSON(http.StatusOK, map[string]string{"status": "Layer sync triggered"}) 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/components/navigation/NavigationButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export interface NavigationButtonProps { 4 | variant?: 'light' | 'dark'; 5 | icon: React.ReactNode; 6 | selected?: boolean; 7 | onClick?: () => void; 8 | } 9 | 10 | const NavigationButton: React.FC = ({ 11 | variant = 'dark', 12 | icon, 13 | selected, 14 | onClick 15 | }) => { 16 | const styles = { 17 | base: { 18 | light: `bg-primary-400 19 | fill-primary-600 20 | hover:bg-primary-500 21 | focus-visible:outline-primary-600`, 22 | 23 | dark: `bg-nuances-400 24 | fill-nuances-300 25 | hover:bg-nuances-black 26 | focus-visible:outline-nuances-300` 27 | }, 28 | 29 | selected: { 30 | light: `bg-nuances-black 31 | fill-nuances-white 32 | focus-visible:outline-nuances-black`, 33 | 34 | dark: `bg-nuances-50 35 | fill-nuances-black 36 | focus-visible:outline-nuances-50` 37 | } 38 | }; 39 | 40 | return ( 41 | 60 | ); 61 | }; 62 | 63 | export default NavigationButton; 64 | -------------------------------------------------------------------------------- /ui/src/clients/auth/client.ts: -------------------------------------------------------------------------------- 1 | export const basicAuth = async (formData: { 2 | username: string; 3 | password: string; 4 | }) => { 5 | const response = await fetch('/auth/login', { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/x-www-form-urlencoded' 9 | }, 10 | body: new URLSearchParams({ 11 | username: formData.username, 12 | password: formData.password 13 | }) 14 | }); 15 | 16 | if (!response.ok) { 17 | throw new Error('Invalid credentials'); 18 | } 19 | 20 | return null; 21 | }; 22 | 23 | /** 24 | * Fetches the supported authentication method from the server. 25 | * Expects JSON response: { type: 'basic' | 'oauth' } 26 | */ 27 | export async function getAuthType(): Promise<'basic' | 'oauth'> { 28 | const res = await fetch('/auth/type', { credentials: 'include' }); 29 | if (!res.ok) { 30 | throw new Error(`Failed to fetch auth type: ${res.status}`); 31 | } 32 | const data = (await res.json()) as { type: string }; 33 | return data.type?.toLowerCase() === 'oauth' ? 'oauth' : 'basic'; 34 | } 35 | 36 | /** 37 | * Fetches current user info from session 38 | * Expects JSON response: { id: string, name: string, email: string } 39 | */ 40 | export interface UserInfo { 41 | id: string; 42 | name?: string; 43 | email?: string; 44 | picture?: string; 45 | } 46 | export async function getUserInfo(): Promise { 47 | const res = await fetch('/auth/user', { credentials: 'include' }); 48 | if (!res.ok) { 49 | throw new Error('Failed to fetch user info'); 50 | } 51 | return res.json(); 52 | } 53 | -------------------------------------------------------------------------------- /docs/operator-manual/git-authentication/gitlab-token.md: -------------------------------------------------------------------------------- 1 | # GitLab Token Authentication 2 | 3 | ## Generate a private token 4 | 5 | You need a private token for your GitLab app to configure Burrito. You can generate a private token in your GitLab account. 6 | 7 | Follow the instructions in the GitLab documentation for [generating a private token](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token). 8 | 9 | ## Configure credentials with GitLab Token 10 | 11 | Set up a credentials secret with the `gitlabToken` field. 12 | 13 | ### Repository-specific credentials example 14 | 15 | ```yaml 16 | apiVersion: config.terraform.padok.cloud/v1alpha1 17 | kind: TerraformRepository 18 | metadata: 19 | name: my-repository 20 | namespace: burrito-project 21 | spec: 22 | repository: 23 | url: https://gitlab.com/owner/repo 24 | terraform: 25 | enabled: true 26 | --- 27 | apiVersion: v1 28 | kind: Secret 29 | metadata: 30 | name: burrito-repo 31 | namespace: burrito-project 32 | type: credentials.burrito.tf/repository 33 | stringData: 34 | provider: gitlab 35 | url: https://gitlab.com/owner/repo 36 | gitlabToken: "glpat-xxxx" 37 | webhookSecret: "my-webhook-secret" 38 | ``` 39 | 40 | ### Shared credentials example 41 | 42 | ```yaml 43 | apiVersion: v1 44 | kind: Secret 45 | metadata: 46 | name: gitlab-token-credentials 47 | namespace: burrito-system 48 | type: credentials.burrito.tf/shared 49 | stringData: 50 | provider: gitlab 51 | url: https://gitlab.example.com/owner 52 | gitlabToken: "glpat-xxxx" 53 | webhookSecret: "my-webhook-secret" 54 | ``` 55 | -------------------------------------------------------------------------------- /ui/src/assets/icons/CopyIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const CopyIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default CopyIcon; 10 | -------------------------------------------------------------------------------- /ui/src/assets/icons/EyeIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const EyeIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default EyeIcon; 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | pull-requests: write 12 | 13 | jobs: 14 | version: 15 | runs-on: ubuntu-latest 16 | environment: production 17 | env: 18 | CHART_PATH: ./deploy/charts/burrito 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v6 22 | with: 23 | fetch-depth: 0 24 | ref: main 25 | 26 | - name: Bump VERSION file 27 | run: | 28 | echo ${{ github.ref_name }} > VERSION 29 | 30 | - name: Bump Helm Chart versions 31 | run: | 32 | export CHART_VERSION=$(echo ${{ github.ref_name }} | sed 's/v//g') 33 | export APP_VERSION=${{ github.ref_name }} 34 | yq -i '.version = env(CHART_VERSION)' $CHART_PATH/Chart.yaml 35 | yq -i '.appVersion = env(APP_VERSION)' $CHART_PATH/Chart.yaml 36 | 37 | - name: Commit version to repository and open PR 38 | env: 39 | GH_TOKEN: ${{ github.token }} 40 | run: | 41 | BRANCH_NAME="bump-version-${{ github.ref_name }}" 42 | git config user.name "github-actions[bot]" 43 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 44 | git add . 45 | git switch -c $BRANCH_NAME 46 | git commit -m "chore(release): bump version to ${{ github.ref_name }}" 47 | git push origin $BRANCH_NAME 48 | gh pr create --fill --base main --head $BRANCH_NAME 49 | 50 | build-and-push: 51 | uses: ./.github/workflows/build-and-push.yaml 52 | -------------------------------------------------------------------------------- /docs/user-guide/remediation-strategy.md: -------------------------------------------------------------------------------- 1 | # Choose a remediation strategy 2 | 3 | The remediation strategy is the way to tell Burrito how it should handle the remediation of drifts on your Terraform layers. 4 | 5 | As for the [runner spec override](./override-runner.md), you can specify a `spec.remediationStrategy` either on the `TerraformRepository` or the `TerraformLayer`. 6 | 7 | The configuration of the `TerraformLayer` will take precedence. 8 | 9 | ## `spec.remediationStrategy` API reference 10 | 11 | | Field | Type | Default | Effect | 12 | | :------------------: | :-----: | :-------------------------------------------: | :-----------------------------------------------------------------------: | 13 | | `autoApply` | Boolean | `false` | If `true` when a `plan` shows drift, it will run an `apply`. | 14 | | `onError.maxRetries` | Integer | `5` or value defined in Burrito configuration | How many times Burrito should retry a `plan`/`apply` when a runner fails. | 15 | 16 | !!! warning 17 | This operator is still experimental. Use `spec.remediationStrategy.autoApply: true` at your own risk. 18 | 19 | ## Example 20 | 21 | With this example configuration, Burrito will create `apply` runs for this layer, with a maximum of 3 retries. 22 | 23 | ```yaml 24 | apiVersion: config.terraform.padok.cloud/v1alpha1 25 | kind: TerraformLayer 26 | metadata: 27 | name: random-pets-terragrunt 28 | spec: 29 | remediationStrategy: 30 | autoApply: true 31 | onError: 32 | maxRetries: 3 33 | # ... snipped ... 34 | ``` 35 | -------------------------------------------------------------------------------- /internal/repository/credentials/testdata/secrets.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: repository-secret-shared 5 | namespace: default 6 | type: credentials.burrito.tf/shared 7 | stringData: 8 | provider: github 9 | username: username-shared 10 | password: password-shared 11 | url: https://github.com 12 | --- 13 | apiVersion: v1 14 | kind: Secret 15 | metadata: 16 | name: repository-secret-present 17 | namespace: default 18 | annotations: 19 | credentials.terraform.padok.cloud/allowed-tenants: default 20 | type: credentials.burrito.tf/repository 21 | stringData: 22 | provider: github 23 | username: username-present 24 | password: password-present 25 | url: https://github.com/padok-team/burrito.git 26 | --- 27 | apiVersion: v1 28 | kind: Secret 29 | metadata: 30 | name: shared-but-not-allowed 31 | namespace: default 32 | annotations: 33 | credentials.terraform.padok.cloud/allowed-tenants: tenant-1 34 | type: credentials.burrito.tf/shared 35 | stringData: 36 | provider: github 37 | username: username-shared 38 | password: password-shared 39 | url: https://gitlab.com 40 | --- 41 | apiVersion: v1 42 | kind: Secret 43 | metadata: 44 | name: two-shared-secret-match-2 45 | namespace: default 46 | type: credentials.burrito.tf/shared 47 | stringData: 48 | provider: github 49 | username: username-match-2 50 | password: password-match-2 51 | url: https://mat.com 52 | --- 53 | apiVersion: v1 54 | kind: Secret 55 | metadata: 56 | name: two-shared-secret-match-1 57 | namespace: default 58 | type: credentials.burrito.tf/shared 59 | stringData: 60 | provider: github 61 | username: username-match-1 62 | password: password-match-1 63 | url: https://mat.com/padok-team 64 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":label(renovate)", 6 | ":configMigration", 7 | ":semanticPrefixFix", 8 | ":separateMultipleMajorReleases", 9 | ":automergeDigest", 10 | ":automergePatch" 11 | ], 12 | "packageRules": [ 13 | { 14 | "matchFileNames": [ 15 | "ui/**" 16 | ], 17 | "matchUpdateTypes": [ 18 | "*" 19 | ], 20 | "groupName": "ui dependencies", 21 | "groupSlug": "ui-dependencies", 22 | "labels": [ 23 | "ui" 24 | ] 25 | }, 26 | { 27 | "matchUpdateTypes": [ 28 | "patch" 29 | ], 30 | "groupName": "all patch dependencies", 31 | "groupSlug": "all-patch", 32 | "matchPackageNames": [ 33 | "*" 34 | ] 35 | }, 36 | { 37 | "matchManagers": [ 38 | "dockerfile" 39 | ], 40 | "matchUpdateTypes": [ 41 | "digest" 42 | ], 43 | "pinDigests": true, 44 | "commitMessagePrefix": "chore(docker):", 45 | "commitMessageAction": "pin digests", 46 | "groupName": "docker pin digests", 47 | "groupSlug": "docker-all-digests" 48 | }, 49 | { 50 | "groupName": "go github sdk", 51 | "groupSlug": "go-github", 52 | "matchDatasources": [ 53 | "go" 54 | ], 55 | "matchPackageNames": [ 56 | "github.com/google/go-github/**", 57 | "github.com/bradleyfalzon/ghinstallation/**" 58 | ], 59 | "separateMajorMinor": false, 60 | "separateMultipleMajor": false 61 | } 62 | ], 63 | "postUpdateOptions": [ 64 | "gomodTidy", 65 | "gomodUpdateImportPaths" 66 | ] 67 | } -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "burrito-ui", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint -c eslint.config.mjs ./src/**/*.{ts,tsx} --report-unused-disable-directives --max-warnings 0 .", 10 | "preview": "vite preview", 11 | "format": "prettier --config .prettierrc 'src/**/*.{ts,tsx}' --write", 12 | "format-check": "prettier --config .prettierrc 'src/**/*.{ts,tsx}' --check" 13 | }, 14 | "dependencies": { 15 | "@floating-ui/react": "^0.27.0", 16 | "@tanstack/react-query": "^5.8.3", 17 | "@tanstack/react-table": "^8.10.7", 18 | "axios": "^1.5.1", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0", 21 | "react-focus-lock": "^2.13.2", 22 | "react-router-dom": "^7.0.0", 23 | "react-tooltip": "^5.21.6", 24 | "tailwind-merge": "^3.3.0" 25 | }, 26 | "devDependencies": { 27 | "@eslint/compat": "^1.2.1", 28 | "@tailwindcss/vite": "^4.1.7", 29 | "@types/react": "^19.0.0", 30 | "@types/react-dom": "^19.0.0", 31 | "@typescript-eslint/eslint-plugin": "^8.0.0", 32 | "@typescript-eslint/parser": "^8.0.0", 33 | "@vitejs/plugin-react-swc": "^4.0.0", 34 | "eslint": "^9.0.0", 35 | "eslint-config-prettier": "^10.0.0", 36 | "eslint-plugin-prettier": "^5.2.1", 37 | "eslint-plugin-react": "^7.37.2", 38 | "eslint-plugin-react-hooks": "^5.0.0", 39 | "eslint-plugin-react-refresh": "^0.4.13", 40 | "prettier": "3.6.2", 41 | "tailwindcss": "^4.1.7", 42 | "typescript": "^5.3.3", 43 | "typescript-eslint": "^8.11.0", 44 | "vite": "^7.0.0" 45 | }, 46 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610" 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/components/buttons/AttemptButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | import TimesIcon from '@/assets/icons/TimesIcon'; 5 | 6 | export interface AttemptButtonProps { 7 | className?: string; 8 | variant?: 'light' | 'dark'; 9 | attempt: number; 10 | isActive?: boolean; 11 | onClick?: () => void; 12 | onClose?: () => void; 13 | } 14 | 15 | const AttemptButton: React.FC = ({ 16 | className, 17 | variant = 'light', 18 | attempt, 19 | isActive, 20 | onClick, 21 | onClose 22 | }) => { 23 | const styles = { 24 | base: { 25 | light: `bg-primary-300 26 | text-nuances-black 27 | fill-primary-600`, 28 | 29 | dark: `bg-nuances-300 30 | text-nuances-400 31 | fill-nuances-400` 32 | }, 33 | 34 | isActive: { 35 | light: `bg-primary-500 36 | text-nuances-black 37 | fill-primary-600`, 38 | 39 | dark: `bg-nuances-black 40 | text-nuances-white 41 | fill-nuances-50` 42 | } 43 | }; 44 | 45 | const handleClose = (e: React.MouseEvent) => { 46 | e.stopPropagation(); 47 | onClose?.(); 48 | }; 49 | 50 | return ( 51 | 69 | ); 70 | }; 71 | 72 | export default AttemptButton; 73 | -------------------------------------------------------------------------------- /manifests/base/controllers/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: burrito-controllers 5 | labels: 6 | app.kubernetes.io/component: controllers 7 | app.kubernetes.io/name: burrito-controllers 8 | app.kubernetes.io/part-of: burrito 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: burrito-controllers 13 | replicas: 1 14 | template: 15 | metadata: 16 | annotations: 17 | kubectl.kubernetes.io/default-container: burrito 18 | labels: 19 | app.kubernetes.io/name: burrito-controllers 20 | spec: 21 | securityContext: 22 | runAsNonRoot: true 23 | containers: 24 | - name: burrito 25 | args: 26 | - controllers 27 | - start 28 | - --namespaces=burrito 29 | image: ghcr.io/padok-team/burrito:main 30 | imagePullPolicy: Always 31 | envFrom: 32 | - configMapRef: 33 | name: burrito-config 34 | optional: true 35 | - secretRef: 36 | name: burrito-config 37 | optional: true 38 | securityContext: 39 | allowPrivilegeEscalation: false 40 | capabilities: 41 | drop: 42 | - "ALL" 43 | livenessProbe: 44 | httpGet: 45 | path: /healthz 46 | port: 8081 47 | initialDelaySeconds: 15 48 | periodSeconds: 20 49 | readinessProbe: 50 | httpGet: 51 | path: /readyz 52 | port: 8081 53 | initialDelaySeconds: 5 54 | periodSeconds: 10 55 | serviceAccountName: burrito-controllers 56 | terminationGracePeriodSeconds: 10 57 | -------------------------------------------------------------------------------- /ui/src/components/buttons/GenericIconButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { Tooltip } from 'react-tooltip'; 4 | export interface GenericIconButtonProps { 5 | className?: string; 6 | variant?: 'light' | 'dark'; 7 | disabled?: boolean; 8 | tooltip?: string; 9 | width?: number; 10 | height?: number; 11 | onClick?: () => void; 12 | Icon: React.FC>; 13 | } 14 | 15 | const GenericIconButton: React.FC = ({ 16 | className, 17 | variant, 18 | disabled, 19 | tooltip, 20 | onClick, 21 | width = 40, 22 | height = 40, 23 | Icon 24 | }) => { 25 | const hoverClass = !disabled 26 | ? variant === 'light' 27 | ? 'hover:bg-primary-300' 28 | : 'hover:bg-nuances-black' 29 | : ''; 30 | return ( 31 |
32 | 37 | 59 |
60 | ); 61 | }; 62 | 63 | export default GenericIconButton; 64 | -------------------------------------------------------------------------------- /manifests/base/server/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: burrito-server 5 | labels: 6 | app.kubernetes.io/component: server 7 | app.kubernetes.io/name: burrito-server 8 | app.kubernetes.io/part-of: burrito 9 | spec: 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: burrito-server 13 | replicas: 1 14 | template: 15 | metadata: 16 | annotations: 17 | kubectl.kubernetes.io/default-container: burrito 18 | labels: 19 | app.kubernetes.io/name: burrito-server 20 | spec: 21 | securityContext: 22 | runAsNonRoot: true 23 | containers: 24 | - name: burrito 25 | args: 26 | - server 27 | - start 28 | ports: 29 | - containerPort: 8080 30 | name: http 31 | envFrom: 32 | - configMapRef: 33 | name: burrito-config 34 | optional: true 35 | - secretRef: 36 | name: burrito-config 37 | optional: true 38 | image: ghcr.io/padok-team/burrito:main 39 | imagePullPolicy: Always 40 | securityContext: 41 | allowPrivilegeEscalation: false 42 | capabilities: 43 | drop: 44 | - "ALL" 45 | livenessProbe: 46 | httpGet: 47 | path: /healthz 48 | port: 8080 49 | initialDelaySeconds: 15 50 | periodSeconds: 20 51 | readinessProbe: 52 | httpGet: 53 | path: /healthz 54 | port: 8080 55 | initialDelaySeconds: 5 56 | periodSeconds: 10 57 | serviceAccountName: burrito-server 58 | terminationGracePeriodSeconds: 10 59 | -------------------------------------------------------------------------------- /ui/src/components/navigation/NavigationLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | import { Link, LinkProps } from 'react-router-dom'; 5 | 6 | export interface NavigationLinkProps { 7 | className?: string; 8 | to: LinkProps['to']; 9 | children: React.ReactNode; 10 | disabled?: boolean; 11 | } 12 | 13 | const NavigationLink: React.FC = ({ 14 | className, 15 | to, 16 | children, 17 | disabled 18 | }) => { 19 | return !disabled ? ( 20 | 21 |
53 | {children} 54 |
55 | 56 | ) : ( 57 |
64 | {children} 65 |
66 | ); 67 | }; 68 | 69 | export default NavigationLink; 70 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v* 8 | pull_request: 9 | branches: 10 | - main 11 | paths: 12 | - ".github/workflows/docs.yaml" 13 | - "docs/**" 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v6 23 | with: 24 | fetch-depth: 0 # Fetch all branches to include 'gh-pages' branch 25 | - uses: actions/setup-python@v6 26 | with: 27 | python-version: 3.x 28 | - uses: actions/cache@v4 29 | with: 30 | key: mkdocs-material-${{ github.ref }} 31 | path: .cache 32 | restore-keys: | 33 | mkdocs-material- 34 | 35 | - name: Install dependencies 36 | run: pip install mkdocs-material mike 37 | 38 | - name: Lint Markdown files 39 | uses: DavidAnson/markdownlint-cli2-action@v20 40 | with: 41 | config: 'docs/.markdownlint.jsonc' 42 | globs: 'docs/**/*.md' 43 | 44 | - name: Build pages 45 | run: mkdocs build --strict 46 | 47 | - name: Configure Git user 48 | run: | 49 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 50 | git config --local user.name "github-actions[bot]" 51 | 52 | - name: Deploy pages (unstable) 53 | if: github.ref == 'refs/heads/main' 54 | run: mike deploy --push --update-aliases unstable 55 | 56 | - name: Deploy pages (new release) 57 | if: startsWith(github.ref, 'refs/tags/') 58 | run: | 59 | mike deploy --push --update-aliases ${{ github.ref_name }} latest 60 | echo "Deployed version ${{ github.ref_name }} to GitHub Pages" 61 | -------------------------------------------------------------------------------- /internal/datastore/storage/encryption.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/padok-team/burrito/internal/burrito/config" 8 | "github.com/padok-team/burrito/internal/utils/encryption" 9 | ) 10 | 11 | type EncryptionManager struct { 12 | DefaultEncryptor *encryption.Encryptor 13 | config config.EncryptionConfig 14 | } 15 | 16 | func NewEncryptionManager(config config.EncryptionConfig) (*EncryptionManager, error) { 17 | em := &EncryptionManager{ 18 | config: config, 19 | } 20 | 21 | encryptionKey := os.Getenv("BURRITO_DATASTORE_STORAGE_ENCRYPTION_KEY") 22 | 23 | if config.Enabled && encryptionKey == "" { 24 | return nil, fmt.Errorf("encryption is enabled but no encryption key is provided in the environment variable BURRITO_DATASTORE_STORAGE_ENCRYPTION_KEY") 25 | } else if config.Enabled && encryptionKey != "" { 26 | encryptor, err := encryption.NewEncryptor(encryptionKey) 27 | if err != nil { 28 | return nil, fmt.Errorf("failed to create encryptor: %w", err) 29 | } 30 | em.DefaultEncryptor = encryptor 31 | } else { 32 | em.DefaultEncryptor = nil 33 | } 34 | 35 | return em, nil 36 | } 37 | 38 | func (em *EncryptionManager) Encrypt(namespace string, plaintext []byte) ([]byte, error) { 39 | if em.DefaultEncryptor == nil { 40 | return plaintext, nil 41 | } 42 | 43 | return em.DefaultEncryptor.Encrypt(plaintext) 44 | } 45 | 46 | func (em *EncryptionManager) Decrypt(namespace string, ciphertext []byte) ([]byte, error) { 47 | if em.DefaultEncryptor == nil { 48 | return ciphertext, nil 49 | } 50 | 51 | // Try to decrypt the data. If it fails, return the original ciphertext as this might be a migration from an unencrypted state 52 | decrypted, err := em.DefaultEncryptor.Decrypt(ciphertext) 53 | if err != nil { 54 | return ciphertext, nil 55 | } 56 | 57 | return decrypted, nil 58 | } 59 | -------------------------------------------------------------------------------- /internal/repository/providers/standard/standard.go: -------------------------------------------------------------------------------- 1 | package standard 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/go-git/go-git/v5/plumbing/transport/http" 9 | "github.com/go-git/go-git/v5/plumbing/transport/ssh" 10 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 11 | "github.com/padok-team/burrito/internal/repository/credentials" 12 | "github.com/padok-team/burrito/internal/repository/types" 13 | ) 14 | 15 | type Standard struct { 16 | Config credentials.Credential 17 | } 18 | 19 | func (s *Standard) GetWebhookProvider() (types.WebhookProvider, error) { 20 | return nil, fmt.Errorf("webhooks are not supported for standard git provider. Provide a specific credentials for providers such as GitHub or GitLab") 21 | } 22 | 23 | func (s *Standard) GetAPIProvider() (types.APIProvider, error) { 24 | return nil, fmt.Errorf("API is not supported for standard git provider. Provide a specific credentials for providers such as GitHub or GitLab") 25 | } 26 | 27 | func (s *Standard) GetGitProvider(repository *configv1alpha1.TerraformRepository) (types.GitProvider, error) { 28 | repoURL := repository.Spec.Repository.Url 29 | repositoryProvider := &GitProvider{ 30 | RepoURL: repoURL, 31 | } 32 | if repoURL == "" { 33 | return nil, errors.New("repository URL is required") 34 | } 35 | isSSH := strings.HasPrefix(repoURL, "git@") || strings.Contains(repoURL, "ssh://") 36 | 37 | if isSSH && s.Config.SSHPrivateKey != "" { 38 | publicKeys, err := ssh.NewPublicKeys("git", []byte(s.Config.SSHPrivateKey), "") 39 | if err != nil { 40 | return nil, err 41 | } 42 | repositoryProvider.AuthMethod = publicKeys 43 | } else if s.Config.Username != "" && s.Config.Password != "" { 44 | repositoryProvider.AuthMethod = &http.BasicAuth{ 45 | Username: s.Config.Username, 46 | Password: s.Config.Password, 47 | } 48 | } 49 | return repositoryProvider, nil 50 | } 51 | -------------------------------------------------------------------------------- /docs/guides/ui.md: -------------------------------------------------------------------------------- 1 | # UI Overview 2 | 3 | The Burrito UI is a web-based interface that allows you to view the state of your Terraform layers and resources, as well as the drift between the desired and actual state of your infrastructure. 4 | 5 | ## Pre-requisites 6 | 7 | - A running Burrito installation 8 | 9 | ## Accessing the UI 10 | 11 | The Burrito UI is accessible via a web browser. To access the UI, you need to expose the `burrito-server` service locally or on a public URL. 12 | 13 | ## Authentication 14 | 15 | By default, Burrito uses basic authentication with a username and password. The default credentials are: 16 | 17 | - **Username:** `admin` 18 | - **Password:** Generated on install in a Kubernetes Secret named `burrito-admin-credentials` in the Burrito server namespace. 19 | 20 | More secure authentication methods like OpenID Connect (OIDC) can be configured for production use. For more details, see the [Authentication Guide](../operator-manual/user-authentication.md). 21 | 22 | ## Features 23 | 24 | ### Homepage 25 | 26 | The homepage displays a list of all the Terraform layers that have been added to Burrito. Each layer is displayed as a card with the following information: 27 | 28 | - Namespace 29 | - Repository 30 | - Branch 31 | - Code path 32 | - Last plan result 33 | - State (Error, Out-of-sync, OK) 34 | 35 | ![Homepage](../assets/demo/homepage.png) 36 | 37 | ### Terraform / Terragrunt logs 38 | 39 | Click on the layer card to view the Terraform or Terragrunt logs for that layer. You can explore previous runs and view the logs for each run. The maximum number of logs to keep is configurable. 40 | ![Logs](../assets/demo/logs.png) 41 | 42 | A dedicated page for exploring the logs is also available. 43 | 44 | ### More to come 45 | 46 | Burrito is under active development, and we are working on adding more features to the UI such as: 47 | 48 | - "Plan and apply" buttons 49 | - Notifications 50 | - User management 51 | - Pull request view 52 | ... and more! 53 | -------------------------------------------------------------------------------- /internal/server/api/logs.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | type GetLogsResponse struct { 12 | Results []string `json:"results"` 13 | } 14 | 15 | func getLogsArgs(c echo.Context) (string, string, string, string, error) { 16 | namespace := c.Param("namespace") 17 | layer := c.Param("layer") 18 | run := c.Param("run") 19 | attempt := c.Param("attempt") 20 | if namespace == "" || layer == "" || run == "" || attempt == "" { 21 | return "", "", "", "", fmt.Errorf("missing query parameters") 22 | } 23 | return namespace, layer, run, attempt, nil 24 | } 25 | 26 | // logs/${namespace}/${layer}/${runId}/${attemptId} 27 | func (a *API) GetLogsHandler(c echo.Context) error { 28 | namespace, layer, run, attempt, err := getLogsArgs(c) 29 | if err != nil { 30 | return c.String(http.StatusBadRequest, err.Error()) 31 | } 32 | response := GetLogsResponse{} 33 | content, err := a.Datastore.GetLogs(namespace, layer, run, attempt) 34 | if err != nil { 35 | return c.String(http.StatusInternalServerError, "could not get logs, there's an issue with the storage backend") 36 | } 37 | response.Results = content 38 | return c.JSON(http.StatusOK, &response) 39 | } 40 | 41 | func (a *API) DownloadLogsHandler(c echo.Context) error { 42 | namespace, layer, run, attempt, err := getLogsArgs(c) 43 | if err != nil { 44 | return c.String(http.StatusBadRequest, err.Error()) 45 | } 46 | content, err := a.Datastore.GetLogs(namespace, layer, run, attempt) 47 | file := strings.Join(content, "\n") 48 | if err != nil { 49 | return c.String(http.StatusInternalServerError, "could not get logs, there's an issue with the storage backend") 50 | } 51 | c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s_%s_%s_%s.log", namespace, layer, run, attempt)) 52 | c.Response().Header().Set("Content-Type", "application/octet-stream") 53 | return c.Blob(http.StatusOK, "application/octet-stream", []byte(file)) 54 | } 55 | -------------------------------------------------------------------------------- /docs/operator-manual/git-authentication/github-token.md: -------------------------------------------------------------------------------- 1 | # GitHub Token Authentication 2 | 3 | ## Generate a personal access token 4 | 5 | You need a personal access token to configure Burrito. You can generate a personal access token in your GitHub account. 6 | 7 | Follow the instructions in the GitHub documentation for [creating a personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token): 8 | 9 | - It should be a **fine-grained token**. 10 | - **Permissions**: Configure the following **Repository Permissions**: 11 | - **Metadata:** Select Read-only. 12 | - **Contents:** Select Read-only. 13 | - **Pull requests:** Select Read & write. This is required to issue comments on pull requests. 14 | - Under **Repository access**, select which repositories you want the token to access. 15 | 16 | ## Configure credentials with GitHub Token 17 | 18 | Set up a credentials secret with the `githubToken` field. 19 | 20 | ### Repository-specific credentials example 21 | 22 | ```yaml 23 | apiVersion: config.terraform.padok.cloud/v1alpha1 24 | kind: TerraformRepository 25 | metadata: 26 | name: my-repository 27 | namespace: burrito-project 28 | spec: 29 | repository: 30 | url: https://github.com/owner/repo 31 | terraform: 32 | enabled: true 33 | --- 34 | apiVersion: v1 35 | kind: Secret 36 | metadata: 37 | name: burrito-repo 38 | namespace: burrito-project 39 | type: credentials.burrito.tf/repository 40 | stringData: 41 | provider: github 42 | url: https://github.com/owner/repo 43 | githubToken: "github_pat_xxx" 44 | webhookSecret: "my-webhook-secret" 45 | ``` 46 | 47 | ### Shared credentials example 48 | 49 | ```yaml 50 | apiVersion: v1 51 | kind: Secret 52 | metadata: 53 | name: github-token-credentials 54 | namespace: burrito-system 55 | type: credentials.burrito.tf/shared 56 | stringData: 57 | provider: github 58 | url: https://github.com/owner 59 | githubToken: "github_pat_xxx" 60 | webhookSecret: "my-webhook-secret" 61 | ``` 62 | -------------------------------------------------------------------------------- /internal/controllers/terraformrepository/polling.go: -------------------------------------------------------------------------------- 1 | package terraformrepository 2 | 3 | import ( 4 | "context" 5 | 6 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 7 | "github.com/padok-team/burrito/internal/annotations" 8 | ) 9 | 10 | // Returns the list of layers that are managed by this repository 11 | func (r *Reconciler) retrieveManagedLayers(ctx context.Context, repository *configv1alpha1.TerraformRepository) ([]configv1alpha1.TerraformLayer, error) { 12 | // get all layers that depends on the repository (layer.spec.repository.name == repository.name) 13 | layers := &configv1alpha1.TerraformLayerList{} 14 | if err := r.List(ctx, layers); err != nil { 15 | return nil, err 16 | } 17 | managedLayers := []configv1alpha1.TerraformLayer{} 18 | for _, layer := range layers.Items { 19 | if layer.Spec.Repository.Name == repository.Name { 20 | managedLayers = append(managedLayers, layer) 21 | } 22 | } 23 | return managedLayers, nil 24 | } 25 | 26 | // Returns a list of all refs (branches and tags) among a list of layers from the same repository (duplicated allowed) 27 | func retrieveAllLayerRefs(layers []configv1alpha1.TerraformLayer) []string { 28 | refs := []string{} 29 | for _, layer := range layers { 30 | refs = append(refs, layer.Spec.Branch) 31 | } 32 | return refs 33 | } 34 | 35 | // Returns a list of all layers referencing a specific ref 36 | func retrieveLayersForRef(ref string, layers []configv1alpha1.TerraformLayer) []configv1alpha1.TerraformLayer { 37 | result := []configv1alpha1.TerraformLayer{} 38 | for _, layer := range layers { 39 | if layer.Spec.Branch == ref { 40 | result = append(result, layer) 41 | } 42 | } 43 | return result 44 | } 45 | 46 | // Checks if there is at least one new layer in the list of layers (without last branch commit annotation) 47 | func isThereANewLayer(layers []configv1alpha1.TerraformLayer) bool { 48 | for _, layer := range layers { 49 | if _, ok := layer.Annotations[annotations.LastBranchCommit]; !ok { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /internal/controllers/terraformpullrequest/comment/default.go: -------------------------------------------------------------------------------- 1 | package comment 2 | 3 | import ( 4 | "bytes" 5 | "text/template" 6 | 7 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 8 | datastore "github.com/padok-team/burrito/internal/datastore/client" 9 | 10 | _ "embed" 11 | ) 12 | 13 | var ( 14 | //go:embed templates/comment.md 15 | defaultTemplateRaw string 16 | defaultTemplate = template.Must(template.New("report").Parse(defaultTemplateRaw)) 17 | ) 18 | 19 | type ReportedLayer struct { 20 | Name string 21 | ShortDiff string 22 | Path string 23 | PrettyPlan string 24 | } 25 | 26 | type DefaultComment struct { 27 | layers []configv1alpha1.TerraformLayer 28 | datastore datastore.Client 29 | } 30 | 31 | type DefaultCommentInput struct { 32 | } 33 | 34 | func NewDefaultComment(layers []configv1alpha1.TerraformLayer, datastore datastore.Client) *DefaultComment { 35 | return &DefaultComment{ 36 | layers: layers, 37 | datastore: datastore, 38 | } 39 | } 40 | 41 | func (c *DefaultComment) Generate(commit string) (string, error) { 42 | var reportedLayers []ReportedLayer 43 | for _, layer := range c.layers { 44 | plan, err := c.datastore.GetPlan(layer.Namespace, layer.Name, layer.Status.LastRun.Name, "", "pretty") 45 | if err != nil { 46 | return "", err 47 | } 48 | shortDiff, err := c.datastore.GetPlan(layer.Namespace, layer.Name, layer.Status.LastRun.Name, "", "short") 49 | if err != nil { 50 | return "", err 51 | } 52 | reportedLayer := ReportedLayer{ 53 | Name: layer.Name, 54 | Path: layer.Spec.Path, 55 | ShortDiff: string(shortDiff), 56 | PrettyPlan: string(plan), 57 | } 58 | reportedLayers = append(reportedLayers, reportedLayer) 59 | 60 | } 61 | data := struct { 62 | Commit string 63 | Layers []ReportedLayer 64 | }{ 65 | Commit: commit, 66 | Layers: reportedLayers, 67 | } 68 | comment := bytes.NewBufferString("") 69 | err := defaultTemplate.Execute(comment, data) 70 | if err != nil { 71 | return "", err 72 | } 73 | return comment.String(), nil 74 | } 75 | -------------------------------------------------------------------------------- /ui/src/components/core/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | import AngleDownIcon from '@/assets/icons/AngleDownIcon'; 5 | 6 | export interface DropdownProps { 7 | className?: string; 8 | variant?: 'light' | 'dark'; 9 | label: string; 10 | filled?: boolean; 11 | disabled?: boolean; 12 | } 13 | 14 | const Dropdown = React.forwardRef( 15 | ( 16 | { className, variant = 'light', label, filled, disabled, ...props }, 17 | ref 18 | ) => { 19 | const styles = { 20 | base: { 21 | light: `bg-primary-400 22 | text-primary-600 23 | fill-primary-600`, 24 | 25 | dark: `bg-nuances-400 26 | text-nuances-300 27 | fill-nuances-300` 28 | }, 29 | 30 | filled: { 31 | light: `text-nuances-black`, 32 | dark: `text-nuances-50` 33 | }, 34 | 35 | disabled: `bg-nuances-50 36 | text-nuances-200 37 | fill-nuances-200 38 | hover:outline-0 39 | focus:outline-0 40 | cursor-default` 41 | }; 42 | 43 | return ( 44 |
74 | {label} 75 | 76 |
77 | ); 78 | } 79 | ); 80 | 81 | export default Dropdown; 82 | -------------------------------------------------------------------------------- /ui/src/assets/icons/DownloadAltIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const DownloadAltIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default DownloadAltIcon; 10 | -------------------------------------------------------------------------------- /internal/runner/tools/base/base.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "errors" 5 | "os/exec" 6 | 7 | c "github.com/padok-team/burrito/internal/utils/cmd" 8 | ) 9 | 10 | // BaseTool provides common functionality for Terraform and OpenTofu 11 | type BaseTool struct { 12 | ExecPath string 13 | WorkingDir string 14 | ToolName string // "terraform" or "tofu" 15 | } 16 | 17 | func (t *BaseTool) TenvName() string { 18 | return t.ToolName 19 | } 20 | 21 | func (t *BaseTool) Init(workingDir string) error { 22 | t.WorkingDir = workingDir 23 | cmd := exec.Command(t.ExecPath, "init", "-no-color", "-upgrade") 24 | c.Verbose(cmd) 25 | cmd.Dir = workingDir 26 | if err := cmd.Run(); err != nil { 27 | return err 28 | } 29 | return nil 30 | } 31 | 32 | func (t *BaseTool) Plan(planArtifactPath string) error { 33 | cmd := exec.Command(t.ExecPath, "plan", "-no-color", "-out", planArtifactPath) 34 | c.Verbose(cmd) 35 | cmd.Dir = t.WorkingDir 36 | if err := cmd.Run(); err != nil { 37 | return err 38 | } 39 | return nil 40 | } 41 | 42 | func (t *BaseTool) Apply(planArtifactPath string) error { 43 | var cmd *exec.Cmd 44 | if planArtifactPath != "" { 45 | cmd = exec.Command(t.ExecPath, "apply", "-no-color", "-auto-approve", planArtifactPath) 46 | } else { 47 | cmd = exec.Command(t.ExecPath, "apply", "-no-color", "-auto-approve") 48 | } 49 | c.Verbose(cmd) 50 | cmd.Dir = t.WorkingDir 51 | if err := cmd.Run(); err != nil { 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func (t *BaseTool) Show(planArtifactPath, mode string) ([]byte, error) { 58 | var cmd *exec.Cmd 59 | switch mode { 60 | case "json": 61 | cmd = exec.Command(t.ExecPath, "show", "-no-color", "-json", planArtifactPath) 62 | case "pretty": 63 | cmd = exec.Command(t.ExecPath, "show", "-no-color", planArtifactPath) 64 | default: 65 | return nil, errors.New("invalid mode") 66 | } 67 | 68 | cmd.Dir = t.WorkingDir 69 | out, err := cmd.Output() 70 | if err != nil { 71 | return nil, err 72 | } 73 | return out, nil 74 | } 75 | 76 | func (t *BaseTool) GetExecPath() string { 77 | return t.ExecPath 78 | } 79 | -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import { 3 | createBrowserRouter, 4 | RouterProvider, 5 | Navigate 6 | } from 'react-router-dom'; 7 | 8 | import ThemeProvider from '@/contexts/ThemeContext'; 9 | 10 | import Layout from '@/layout/Layout'; 11 | import Layers from '@/pages/Layers'; 12 | import Pulls from '@/pages/Pulls'; 13 | import Logs from '@/pages/Logs'; 14 | import Login from '@/pages/Login'; 15 | 16 | const queryClient = new QueryClient({ 17 | defaultOptions: { 18 | queries: { 19 | retry: ( 20 | failureCount, 21 | error: Error & { response?: { status: number } } 22 | ) => { 23 | // Don't retry on 401 errors 24 | if (error?.response?.status === 401) { 25 | window.location.href = '/login'; 26 | return false; 27 | } 28 | return failureCount < 3; 29 | } 30 | }, 31 | mutations: { 32 | onError: (error: Error & { response?: { status: number } }) => { 33 | if (error?.response?.status === 401) { 34 | window.location.href = '/login'; 35 | } 36 | } 37 | } 38 | } 39 | }); 40 | const router = createBrowserRouter([ 41 | { 42 | path: '/', 43 | element: , 44 | children: [ 45 | { 46 | index: true, 47 | element: 48 | }, 49 | { 50 | path: 'layers', 51 | element: 52 | }, 53 | { 54 | path: 'pulls', 55 | element: 56 | }, 57 | { 58 | path: 'logs/:namespace?/:layerId?/:runId?', 59 | element: 60 | } 61 | ] 62 | }, 63 | { 64 | path: '/login', 65 | element: 66 | }, 67 | { 68 | path: '*', 69 | element: 70 | } 71 | ]); 72 | 73 | function App() { 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /manifests/base/server/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: burrito-server 5 | labels: 6 | app.kubernetes.io/component: server 7 | app.kubernetes.io/name: burrito-server 8 | app.kubernetes.io/part-of: burrito 9 | rules: 10 | - apiGroups: 11 | - config.terraform.padok.cloud 12 | resources: 13 | - terraformlayers 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - config.terraform.padok.cloud 24 | resources: 25 | - terraformrepositories 26 | verbs: 27 | - create 28 | - delete 29 | - get 30 | - list 31 | - patch 32 | - update 33 | - watch 34 | - apiGroups: 35 | - config.terraform.padok.cloud 36 | resources: 37 | - terraformlayers/finalizers 38 | verbs: 39 | - update 40 | - apiGroups: 41 | - config.terraform.padok.cloud 42 | resources: 43 | - terraformpullrequests 44 | verbs: 45 | - create 46 | - delete 47 | - get 48 | - list 49 | - patch 50 | - update 51 | - watch 52 | - apiGroups: 53 | - config.terraform.padok.cloud 54 | resources: 55 | - terraformpullrequests/finalizers 56 | verbs: 57 | - update 58 | - apiGroups: 59 | - config.terraform.padok.cloud 60 | resources: 61 | - terraformpullrequests/status 62 | verbs: 63 | - get 64 | - patch 65 | - update 66 | - apiGroups: 67 | - config.terraform.padok.cloud 68 | resources: 69 | - terraformruns 70 | verbs: 71 | - create 72 | - delete 73 | - get 74 | - list 75 | - patch 76 | - update 77 | - watch 78 | - apiGroups: 79 | - config.terraform.padok.cloud 80 | resources: 81 | - terraformruns/finalizers 82 | verbs: 83 | - update 84 | - apiGroups: 85 | - config.terraform.padok.cloud 86 | resources: 87 | - terraformruns/status 88 | verbs: 89 | - get 90 | - patch 91 | - update 92 | -------------------------------------------------------------------------------- /.github/workflows/helm.yaml: -------------------------------------------------------------------------------- 1 | name: Helm CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - ".github/workflows/helm.yaml" 14 | - "deploy/charts/**" 15 | 16 | env: 17 | CHART_NAME: burrito 18 | CHART_PATH: ./deploy/charts/burrito 19 | CHART_REPO: ghcr.io/${{ github.repository_owner }}/charts 20 | 21 | jobs: 22 | helm-render: 23 | name: Helm Render 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v6 28 | 29 | - name: Helm Render 30 | run: helm template ${{ env.CHART_PATH }} 31 | 32 | helm-push: 33 | name: Helm Push 34 | runs-on: ubuntu-latest 35 | needs: helm-render 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v6 39 | 40 | - name: GHCR Login 41 | run: echo ${{ secrets.GITHUB_TOKEN }} | helm registry login ghcr.io -u ${{ github.repository_owner }} --password-stdin 42 | 43 | - name: Compute versions 44 | shell: bash 45 | run: | 46 | CURRENT_VERSION=$(yq $CHART_PATH/Chart.yaml --expression .version) 47 | if [[ ${{ github.event_name }} == 'pull_request' || ${{ github.event_name }} == 'push' && ${{ github.ref_type }} == 'branch' ]]; then 48 | echo "VERSION=$(echo $CURRENT_VERSION-${{ github.sha }})" >> $GITHUB_ENV 49 | echo "APP_VERSION=${{ github.sha }}" >> $GITHUB_ENV 50 | elif [[ ${{ github.event_name }} == 'push' && ${{ github.ref_type }} == 'tag' ]]; then 51 | echo "VERSION=$(echo ${{ github.ref_name }} | sed 's/v//')" >> $GITHUB_ENV 52 | echo "APP_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV 53 | else 54 | echo "Unsupported event type" 55 | exit 1 56 | fi 57 | 58 | - name: Helm Package 59 | run: helm package ${{ env.CHART_PATH }} -u --version ${{ env.VERSION }} --app-version ${{ env.APP_VERSION }} 60 | 61 | - name: Helm Push 62 | run: helm push ./${{ env.CHART_NAME }}-${{ env.VERSION }}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts 63 | -------------------------------------------------------------------------------- /docs/user-guide/additionnal-trigger-path.md: -------------------------------------------------------------------------------- 1 | # Additionnal Trigger Paths 2 | 3 | By default, when you are creating a layer, you must specify a repository and a path. This path is used to trigger the layer changes which means that when a change occurs in this path, the layer will be plan / apply accordingly. 4 | 5 | Sometimes, you need to trigger changes on a layer where the changes are not in the same path (e.g. update made on an internal terraform module hosted on the same repository). 6 | 7 | That's where the additional trigger paths feature comes! 8 | 9 | Let's take the following `TerraformLayer`: 10 | 11 | ```yaml 12 | apiVersion: config.terraform.padok.cloud/v1alpha1 13 | kind: TerraformLayer 14 | metadata: 15 | name: random-pets-terragrunt 16 | spec: 17 | terraform: 18 | enabled: true 19 | version: "1.3.1" 20 | terragrunt: 21 | enabled: true 22 | version: "0.45.4" 23 | remediationStrategy: 24 | autoApply: true 25 | path: "terragrunt/random-pets/test" 26 | branch: "main" 27 | repository: 28 | name: burrito 29 | namespace: burrito 30 | ``` 31 | 32 | The repository's path of my `TerraformLayer` is set to `terragrunt/random-pets/test`. But I want to trigger the layer plan / apply when a change occurs on my module which is in the `modules/random-pets` directory of my repository. 33 | 34 | To do so, I just have to add the `config.terraform.padok.cloud/additionnal-trigger-paths` annotation to my `TerraformLayer` as below. Note you can set several paths separated with a comma. 35 | 36 | ```yaml 37 | apiVersion: config.terraform.padok.cloud/v1alpha1 38 | kind: TerraformLayer 39 | metadata: 40 | name: random-pets-terragrunt 41 | annotations: 42 | config.terraform.padok.cloud/additionnal-trigger-paths: "modules/random-pets" 43 | spec: 44 | terraform: 45 | enabled: true 46 | version: "1.3.1" 47 | terragrunt: 48 | enabled: true 49 | version: "0.45.4" 50 | remediationStrategy: 51 | autoApply: true 52 | path: "terragrunt/random-pets/test" 53 | branch: "main" 54 | repository: 55 | name: burrito 56 | namespace: burrito 57 | ``` 58 | 59 | Now, when a change occurs in the `modules/random-pets` directory, the layer will be plan / apply. 60 | -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "fmt" 5 | 6 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 7 | "github.com/padok-team/burrito/internal/repository/credentials" 8 | "github.com/padok-team/burrito/internal/repository/providers/github" 9 | "github.com/padok-team/burrito/internal/repository/providers/gitlab" 10 | "github.com/padok-team/burrito/internal/repository/providers/mock" 11 | "github.com/padok-team/burrito/internal/repository/providers/standard" 12 | "github.com/padok-team/burrito/internal/repository/types" 13 | ) 14 | 15 | func GetGitProviderFromRepository(store *credentials.CredentialStore, repo *configv1alpha1.TerraformRepository) (types.GitProvider, error) { 16 | creds, err := store.GetCredentials(repo) 17 | // If no credentials, it may be a standard public repository 18 | if err != nil { 19 | return getStandardGitNoAuth(repo.Spec.Repository.Url), nil 20 | } 21 | provider, err := GetProviderFromCredentials(*creds) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return provider.GetGitProvider(repo) 27 | } 28 | 29 | func GetAPIProviderFromRepository(store *credentials.CredentialStore, repo *configv1alpha1.TerraformRepository) (types.APIProvider, error) { 30 | creds, err := store.GetCredentials(repo) 31 | if err != nil { 32 | return nil, err 33 | } 34 | provider, err := GetProviderFromCredentials(*creds) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | return provider.GetAPIProvider() 40 | } 41 | 42 | func GetProviderFromCredentials(RepositoryCredentials credentials.Credential) (types.Provider, error) { 43 | switch RepositoryCredentials.Provider { 44 | case "github": 45 | return &github.Github{Config: RepositoryCredentials}, nil 46 | case "gitlab": 47 | return &gitlab.Gitlab{Config: RepositoryCredentials}, nil 48 | case "git": 49 | return &standard.Standard{Config: RepositoryCredentials}, nil 50 | case "mock": 51 | return &mock.Mock{}, nil 52 | default: 53 | return nil, fmt.Errorf("unknown provider: %s", RepositoryCredentials.Provider) 54 | } 55 | } 56 | 57 | func getStandardGitNoAuth(URL string) types.GitProvider { 58 | return &standard.GitProvider{ 59 | RepoURL: URL, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /deploy/charts/burrito/templates/rbac-server.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: burrito-server 5 | {{- with mergeOverwrite (deepCopy .Values.global.metadata) .Values.server.metadata }} 6 | labels: 7 | {{- toYaml .labels | nindent 4}} 8 | annotations: 9 | {{- toYaml .annotations | nindent 4}} 10 | {{- end }} 11 | rules: 12 | - apiGroups: 13 | - config.terraform.padok.cloud 14 | resources: 15 | - terraformlayers 16 | verbs: 17 | - create 18 | - delete 19 | - get 20 | - list 21 | - patch 22 | - update 23 | - watch 24 | - apiGroups: 25 | - config.terraform.padok.cloud 26 | resources: 27 | - terraformrepositories 28 | verbs: 29 | - create 30 | - delete 31 | - get 32 | - list 33 | - patch 34 | - update 35 | - watch 36 | - apiGroups: 37 | - config.terraform.padok.cloud 38 | resources: 39 | - terraformlayers/finalizers 40 | verbs: 41 | - update 42 | - apiGroups: 43 | - config.terraform.padok.cloud 44 | resources: 45 | - terraformpullrequests 46 | verbs: 47 | - create 48 | - delete 49 | - get 50 | - list 51 | - patch 52 | - update 53 | - watch 54 | - apiGroups: 55 | - config.terraform.padok.cloud 56 | resources: 57 | - terraformpullrequests/finalizers 58 | verbs: 59 | - update 60 | - apiGroups: 61 | - config.terraform.padok.cloud 62 | resources: 63 | - terraformpullrequests/status 64 | verbs: 65 | - get 66 | - patch 67 | - update 68 | - apiGroups: 69 | - config.terraform.padok.cloud 70 | resources: 71 | - terraformruns 72 | verbs: 73 | - create 74 | - delete 75 | - get 76 | - list 77 | - patch 78 | - update 79 | - watch 80 | - apiGroups: 81 | - config.terraform.padok.cloud 82 | resources: 83 | - terraformruns/finalizers 84 | verbs: 85 | - update 86 | - apiGroups: 87 | - config.terraform.padok.cloud 88 | resources: 89 | - terraformruns/status 90 | verbs: 91 | - get 92 | - patch 93 | - update 94 | - apiGroups: 95 | - "" 96 | resources: 97 | - secrets 98 | verbs: 99 | - list 100 | - create 101 | - update 102 | - watch 103 | - get 104 | -------------------------------------------------------------------------------- /internal/repository/credentials/testdata/repository.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.terraform.padok.cloud/v1alpha1 2 | kind: TerraformRepository 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: in-cluster-burrito 6 | name: repository-secret-present 7 | namespace: default 8 | spec: 9 | overrideRunnerSpec: 10 | imagePullSecrets: 11 | - name: ghcr-creds 12 | repository: 13 | url: git@github.com:padok-team/burrito.git 14 | terraform: 15 | enabled: true 16 | --- 17 | apiVersion: config.terraform.padok.cloud/v1alpha1 18 | kind: TerraformRepository 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: in-cluster-burrito 22 | name: repository-secret-not-present 23 | namespace: default 24 | spec: 25 | overrideRunnerSpec: 26 | imagePullSecrets: 27 | - name: ghcr-creds 28 | repository: 29 | url: git@github.com:padok-team/not-present.git 30 | terraform: 31 | enabled: true 32 | --- 33 | apiVersion: config.terraform.padok.cloud/v1alpha1 34 | kind: TerraformRepository 35 | metadata: 36 | labels: 37 | app.kubernetes.io/instance: in-cluster-burrito 38 | name: no-secret-present 39 | namespace: default 40 | spec: 41 | overrideRunnerSpec: 42 | imagePullSecrets: 43 | - name: ghcr-creds 44 | repository: 45 | url: git@gitlab.com:padok-team/not-present.git 46 | terraform: 47 | enabled: true 48 | --- 49 | apiVersion: config.terraform.padok.cloud/v1alpha1 50 | kind: TerraformRepository 51 | metadata: 52 | labels: 53 | app.kubernetes.io/instance: in-cluster-burrito 54 | name: not-allowed-secret 55 | namespace: default 56 | spec: 57 | overrideRunnerSpec: 58 | imagePullSecrets: 59 | - name: ghcr-creds 60 | repository: 61 | url: git@gitlab.com:padok-team/not-present.git 62 | terraform: 63 | enabled: true 64 | --- 65 | apiVersion: config.terraform.padok.cloud/v1alpha1 66 | kind: TerraformRepository 67 | metadata: 68 | labels: 69 | app.kubernetes.io/instance: in-cluster-burrito 70 | name: two-shared-secret-match 71 | namespace: default 72 | spec: 73 | overrideRunnerSpec: 74 | imagePullSecrets: 75 | - name: ghcr-creds 76 | repository: 77 | url: git@mat.com:padok-team/two-shared-secret.git 78 | terraform: 79 | enabled: true 80 | -------------------------------------------------------------------------------- /ui/src/assets/icons/ExclamationTriangleIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const ExclamationTriangleIcon = (props: SVGProps) => ( 4 | 5 | 6 | 7 | ); 8 | 9 | export default ExclamationTriangleIcon; 10 | -------------------------------------------------------------------------------- /internal/lock/lock.go: -------------------------------------------------------------------------------- 1 | package lock 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "hash/fnv" 7 | 8 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 9 | coordination "k8s.io/api/coordination/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/types" 13 | "sigs.k8s.io/controller-runtime/pkg/client" 14 | ) 15 | 16 | const lockPrefix string = "burrito-layer-lock" 17 | 18 | func hash(s string) uint32 { 19 | h := fnv.New32a() 20 | h.Write([]byte(s)) 21 | return h.Sum32() 22 | } 23 | 24 | func getLeaseName(layer *configv1alpha1.TerraformLayer) string { 25 | return fmt.Sprintf("%s-%d", lockPrefix, hash(layer.Spec.Repository.Name+layer.Spec.Repository.Namespace+layer.Spec.Path)) 26 | } 27 | 28 | func getLeaseLock(layer *configv1alpha1.TerraformLayer, run *configv1alpha1.TerraformRun) *coordination.Lease { 29 | identity := "burrito-controller" 30 | name := getLeaseName(layer) 31 | lease := &coordination.Lease{ 32 | Spec: coordination.LeaseSpec{ 33 | HolderIdentity: &identity, 34 | }, 35 | } 36 | lease.SetName(name) 37 | lease.SetNamespace(layer.Namespace) 38 | lease.SetOwnerReferences([]metav1.OwnerReference{ 39 | { 40 | APIVersion: run.GetAPIVersion(), 41 | Kind: run.GetKind(), 42 | Name: run.Name, 43 | UID: run.UID, 44 | }, 45 | }) 46 | return lease 47 | } 48 | 49 | func IsLayerLocked(ctx context.Context, c client.Client, layer *configv1alpha1.TerraformLayer) (bool, error) { 50 | err := c.Get(ctx, types.NamespacedName{ 51 | Name: getLeaseName(layer), 52 | Namespace: layer.Namespace, 53 | }, &coordination.Lease{}) 54 | if errors.IsNotFound(err) { 55 | return false, nil 56 | } 57 | if err != nil { 58 | return false, err 59 | } 60 | return true, nil 61 | } 62 | 63 | func CreateLock(ctx context.Context, c client.Client, layer *configv1alpha1.TerraformLayer, run *configv1alpha1.TerraformRun) error { 64 | leaseLock := getLeaseLock(layer, run) 65 | return c.Create(ctx, leaseLock) 66 | } 67 | 68 | func DeleteLock(ctx context.Context, c client.Client, layer *configv1alpha1.TerraformLayer, run *configv1alpha1.TerraformRun) error { 69 | leaseLock := getLeaseLock(layer, run) 70 | return c.Delete(ctx, leaseLock) 71 | } 72 | -------------------------------------------------------------------------------- /internal/repository/providers/github/api.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | "github.com/google/go-github/v80/github" 8 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 9 | "github.com/padok-team/burrito/internal/annotations" 10 | "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type APIProvider struct { 15 | client *github.Client 16 | } 17 | 18 | func (api *APIProvider) GetChanges(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]string, error) { 19 | owner, repoName := parseGithubUrl(repository.Spec.Repository.Url) 20 | id, err := strconv.Atoi(pr.Spec.ID) 21 | if err != nil { 22 | log.Errorf("Error while parsing Github pull request ID: %s", err) 23 | return []string{}, err 24 | } 25 | // Per page is 30 by default, max is 100 26 | opts := &github.ListOptions{ 27 | PerPage: 100, 28 | } 29 | // Get all pull request files from Github 30 | var allChangedFiles []string 31 | for { 32 | changedFiles, resp, err := api.client.PullRequests.ListFiles(context.TODO(), owner, repoName, id, opts) 33 | if err != nil { 34 | return []string{}, err 35 | } 36 | for _, file := range changedFiles { 37 | if *file.Status != "unchanged" { 38 | allChangedFiles = append(allChangedFiles, *file.Filename) 39 | } 40 | } 41 | if resp.NextPage == 0 { 42 | break 43 | } 44 | opts.Page = resp.NextPage 45 | } 46 | return allChangedFiles, nil 47 | } 48 | 49 | func (api *APIProvider) Comment(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest, comment comment.Comment) error { 50 | body, err := comment.Generate(pr.Annotations[annotations.LastBranchCommit]) 51 | if err != nil { 52 | log.Errorf("Error while generating comment: %s", err) 53 | return err 54 | } 55 | owner, repoName := parseGithubUrl(repository.Spec.Repository.Url) 56 | id, err := strconv.Atoi(pr.Spec.ID) 57 | if err != nil { 58 | log.Errorf("Error while parsing Github pull request ID: %s", err) 59 | return err 60 | } 61 | _, _, err = api.client.Issues.CreateComment(context.TODO(), owner, repoName, id, &github.IssueComment{ 62 | Body: &body, 63 | }) 64 | return err 65 | } 66 | -------------------------------------------------------------------------------- /internal/repository/providers/gitlab/api.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "strconv" 5 | 6 | configv1alpha1 "github.com/padok-team/burrito/api/v1alpha1" 7 | "github.com/padok-team/burrito/internal/annotations" 8 | "github.com/padok-team/burrito/internal/controllers/terraformpullrequest/comment" 9 | log "github.com/sirupsen/logrus" 10 | gitlab "gitlab.com/gitlab-org/api/client-go" 11 | ) 12 | 13 | type APIProvider struct { 14 | client *gitlab.Client 15 | } 16 | 17 | func (api *APIProvider) GetChanges(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest) ([]string, error) { 18 | id, err := strconv.Atoi(pr.Spec.ID) 19 | if err != nil { 20 | log.Errorf("Error while parsing Gitlab merge request ID: %s", err) 21 | return []string{}, err 22 | } 23 | listOpts := gitlab.ListMergeRequestDiffsOptions{ 24 | ListOptions: gitlab.ListOptions{ 25 | PerPage: 20, 26 | }, 27 | } 28 | var changes []string 29 | for { 30 | diffs, resp, err := api.client.MergeRequests.ListMergeRequestDiffs(getGitlabNamespacedName(repository.Spec.Repository.Url), id, &listOpts) 31 | if err != nil { 32 | log.Errorf("Error while getting merge request changes: %s", err) 33 | return []string{}, err 34 | } 35 | for _, change := range diffs { 36 | changes = append(changes, change.NewPath) 37 | } 38 | if resp.NextPage == 0 { 39 | break 40 | } 41 | listOpts.Page = resp.NextPage 42 | } 43 | return changes, nil 44 | } 45 | 46 | func (api *APIProvider) Comment(repository *configv1alpha1.TerraformRepository, pr *configv1alpha1.TerraformPullRequest, comment comment.Comment) error { 47 | body, err := comment.Generate(pr.Annotations[annotations.LastBranchCommit]) 48 | if err != nil { 49 | log.Errorf("Error while generating comment: %s", err) 50 | return err 51 | } 52 | id, err := strconv.Atoi(pr.Spec.ID) 53 | if err != nil { 54 | log.Errorf("Error while parsing Gitlab merge request ID: %s", err) 55 | return err 56 | } 57 | _, _, err = api.client.Notes.CreateMergeRequestNote(getGitlabNamespacedName(repository.Spec.Repository.Url), id, &gitlab.CreateMergeRequestNoteOptions{ 58 | Body: gitlab.Ptr(body), 59 | }) 60 | if err != nil { 61 | log.Errorf("Error while creating merge request note: %s", err) 62 | return err 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | paths: 11 | - ".github/workflows/build-and-push.yaml" 12 | - ".github/workflows/ci.yaml" 13 | - "**.go" 14 | - "go.mod" 15 | - "go.sum" 16 | - "Dockerfile" 17 | 18 | permissions: 19 | packages: write 20 | id-token: write # Required for Codecov 21 | 22 | jobs: 23 | unit-tests: 24 | name: Unit Tests 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v6 29 | - name: Cache envtest binaries 30 | uses: actions/cache@v4 31 | with: 32 | path: ./bin/ 33 | key: binaries 34 | - name: Setup Golang 35 | uses: actions/setup-go@v6 36 | with: 37 | go-version-file: "go.mod" 38 | - name: Install envtest 39 | run: make envtest 40 | - name: Setup envtest 41 | run: ./bin/setup-envtest use 42 | - name: Set up Docker Buildx for docker compose 43 | uses: docker/setup-buildx-action@v3 44 | - name: Run tests 45 | run: make test 46 | - name: Upload coverage reports to Codecov 47 | uses: codecov/codecov-action@v5 48 | with: 49 | use_oidc: true 50 | 51 | check-codegen: 52 | name: Check Codegen 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v6 57 | - name: Setup Golang 58 | uses: actions/setup-go@v6 59 | with: 60 | go-version-file: "go.mod" 61 | - name: Generate manifests 62 | run: make manifests 63 | - name: Check nothing has changed 64 | run: | 65 | git diff --exit-code ./manifests 66 | 67 | lint-go: 68 | name: Lint Go 69 | runs-on: ubuntu-latest 70 | steps: 71 | - uses: actions/checkout@v6 72 | - uses: actions/setup-go@v6 73 | with: 74 | go-version-file: "go.mod" 75 | - name: golangci-lint 76 | uses: golangci/golangci-lint-action@v9 77 | with: 78 | version: latest 79 | args: --issues-exit-code=0 80 | 81 | build-and-push: 82 | uses: ./.github/workflows/build-and-push.yaml 83 | -------------------------------------------------------------------------------- /ui/src/assets/avocado/AvocadoOff.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | const AvocadoOff = (props: SVGProps) => ( 4 | 12 | 17 | 23 | 30 | 36 | 37 | ); 38 | 39 | export default AvocadoOff; 40 | --------------------------------------------------------------------------------