├── .commitlintrc.js ├── .devcontainer └── devcontainer.json ├── .dockerignore ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bugs.yml │ ├── config.yml │ └── features.yml └── workflows │ ├── check-commits.yml │ ├── check-links.yml │ ├── check-markdown.yml │ ├── check-signed.yml │ ├── check-unit-tests.yml │ ├── releaser.yaml │ └── tests.yml ├── .gitignore ├── .golangci.yaml ├── .goreleaser.yaml ├── .markdownlinkcheck.json ├── .markdownlint.json ├── .tool-versions ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.MD ├── build └── Dockerfile ├── cmd ├── akamai │ ├── command.go │ └── create.go ├── aws │ ├── command.go │ ├── create.go │ ├── create_test.go │ └── quota.go ├── azure │ ├── command.go │ └── create.go ├── civo │ ├── backup.go │ ├── command.go │ ├── create.go │ └── quota.go ├── digitalocean │ ├── command.go │ └── create.go ├── generate.go ├── google │ ├── command.go │ └── create.go ├── info.go ├── k3d │ ├── command.go │ ├── create.go │ ├── destroy.go │ ├── mkcert.go │ ├── root-credentials.go │ └── vault.go ├── k3s │ ├── command.go │ └── create.go ├── launch.go ├── letsencrypt.go ├── logs.go ├── reset.go ├── root.go ├── terraform.go ├── version.go └── vultr │ ├── command.go │ └── create.go ├── config.yaml ├── go.mod ├── go.sum ├── images ├── kubefirst-arch.png ├── kubefirst-light.svg ├── kubefirst.svg └── provisioning.png ├── internal ├── catalog │ └── catalog.go ├── cluster │ └── cluster.go ├── common │ └── common.go ├── generate │ ├── files.go │ ├── scaffold.go │ ├── scaffold │ │ ├── {{ .AppName }}.yaml │ │ └── {{ .AppName }} │ │ │ ├── Chart.yaml │ │ │ └── values.yaml │ ├── scaffold_test.go │ └── testdata │ │ └── scaffold │ │ ├── development │ │ ├── app.yaml │ │ └── app │ │ │ ├── Chart.yaml │ │ │ └── values.yaml │ │ ├── production │ │ ├── metaphor.yaml │ │ └── metaphor │ │ │ ├── Chart.yaml │ │ │ └── values.yaml │ │ └── some-environment │ │ ├── some-app.yaml │ │ └── some-app │ │ ├── Chart.yaml │ │ └── values.yaml ├── gitShim │ ├── containerRegistryAuth.go │ └── init.go ├── helm │ └── types.go ├── k3d │ └── menu.go ├── launch │ ├── cmd.go │ └── constants.go ├── progress │ ├── command.go │ ├── constants.go │ ├── message.go │ ├── progress.go │ ├── styles.go │ └── types.go ├── provision │ ├── provision.go │ ├── provisionWatcher.go │ └── provisionWatcher_test.go ├── provisionLogs │ ├── command.go │ ├── provisionLogs.go │ └── types.go ├── segment │ └── segment.go ├── step │ ├── stepper.go │ └── stepper_test.go ├── types │ ├── flags.go │ └── proxy.go └── utilities │ ├── flags.go │ └── utilities.go ├── main.go └── tools ├── aws-assume-role.sh └── aws-create-role.tf /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | const rules = require('@commitlint/rules'); 2 | 3 | module.exports = { 4 | rules: { 5 | 'header-max-length': [2, 'always', 72], 6 | }, 7 | plugins: [ 8 | { 9 | rules: { 10 | 'header-max-length': (parsed, _when, _value) => { 11 | parsed.header = parsed.header.replace(/\s\(#[0-9]+\)$/, '') 12 | return rules.default['header-max-length'](parsed, _when, _value) 13 | }, 14 | }, 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devcontainer", 3 | "image": "ghcr.io/kubefirst/devcontainers/full", 4 | "features": {}, 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [], 8 | "settings": {} 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.env 2 | node_modules 3 | .terraform 4 | .gitlab-bot-access-token 5 | .gitlab-runner-registration-token 6 | .vscode 7 | terraform-ssh-key 8 | terraform-ssh-key.pub 9 | kubeconfig_* 10 | */cypress/screenshots/ 11 | */cypress/videos/ 12 | dist/ 13 | **/.DS_Store 14 | /git 15 | bin 16 | .vscode/settings.json 17 | logs/ 18 | /tmp 19 | lint_log.txt 20 | credentials 21 | .idea 22 | kubefirst 23 | .git 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.go] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [Makefile] 17 | indent_style = tab 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bugs.yml: -------------------------------------------------------------------------------- 1 | name: Bug 2 | description: Report an issue with kubefirst. Please create one GitHub issue per bug! 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to report this issue! If you need help, please ask your question in our [Slack community](http://kubefirst.io/slack). 9 | - type: input 10 | id: version 11 | attributes: 12 | label: Which version of kubefirst are you using? 13 | description: Run `kubefirst version` to find the version number 14 | validations: 15 | required: true 16 | - type: dropdown 17 | id: cloud 18 | attributes: 19 | label: Which cloud provider? 20 | multiple: true 21 | options: 22 | - None specific 23 | - Akamai 24 | - AWS 25 | - Azure 26 | - Civo 27 | - DigitalOcean 28 | - Google Cloud 29 | - k3d (local) 30 | - K3s 31 | - Vultr 32 | validations: 33 | required: true 34 | - type: dropdown 35 | id: dns 36 | attributes: 37 | label: Which DNS? 38 | multiple: true 39 | options: 40 | - None specific 41 | - Cloud ones (default) 42 | - Cloudflare 43 | validations: 44 | required: true 45 | - type: dropdown 46 | id: type 47 | attributes: 48 | label: Which installation type? 49 | multiple: true 50 | options: 51 | - None specific 52 | - CLI 53 | - Marketplace 54 | - UI (Console app) 55 | validations: 56 | required: true 57 | - type: dropdown 58 | id: git 59 | attributes: 60 | label: Which distributed Git provider? 61 | multiple: true 62 | options: 63 | - None specific 64 | - GitHub 65 | - GitLab 66 | validations: 67 | required: true 68 | - type: dropdown 69 | id: gitopstemplate 70 | attributes: 71 | label: Did you use a fork of `gitops-template`? 72 | options: 73 | - "No" 74 | - "Yes" 75 | validations: 76 | required: true 77 | - type: dropdown 78 | id: os 79 | attributes: 80 | label: Which Operating System? 81 | description: Please add the architecture in the issue description. If you selected "Other", please specify in the issue. 82 | options: 83 | - None specific 84 | - macOS 85 | - Linux 86 | - Windows 87 | - Other 88 | validations: 89 | required: true 90 | - type: textarea 91 | id: issue 92 | attributes: 93 | label: What is the issue? 94 | description: | 95 | Give us as many details as possible. 96 | 97 | Tip: You can attach images or log files by dragging files in this textbox. 98 | placeholder: Tell us what can be improved! 99 | validations: 100 | required: true 101 | - type: checkboxes 102 | id: terms 103 | attributes: 104 | label: Code of Conduct 105 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/konstructio/kubefirst/blob/main/CODE_OF_CONDUCT.md) 106 | options: 107 | - label: I agree to follow this project's Code of Conduct 108 | required: true 109 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Documentations 4 | url: https://github.com/konstructio/kubefirst-docs/issues/new?assignees=&labels=docs&template=docs.yml&title=%5BDocs%5D%3A+ 5 | about: Any suggestions related to the documentation, whether it's an issue, missing information, unclear steps or new page that should be created 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/features.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Share which feature you think kubefirst is missing. Please create one GitHub issue per feature idea! 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to share your feature idea with us! 9 | - type: textarea 10 | id: feature 11 | attributes: 12 | label: What is your feature idea? 13 | description: | 14 | Give us as many details as possible. 15 | placeholder: Tell us what can be improved! 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: reason 20 | attributes: 21 | label: Why is it needed? 22 | description: | 23 | Give us as many details as possible. 24 | placeholder: Tell us what can be improved! 25 | validations: 26 | required: true 27 | - type: checkboxes 28 | id: adoption 29 | attributes: 30 | label: Is this missing feature preventing you from using kubefirst? 31 | options: 32 | - label: "Yes" 33 | - type: checkboxes 34 | id: terms 35 | attributes: 36 | label: Code of Conduct 37 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/konstructio/kubefirst/blob/main/CODE_OF_CONDUCT.md) 38 | options: 39 | - label: I agree to follow this project's Code of Conduct 40 | required: true 41 | -------------------------------------------------------------------------------- /.github/workflows/check-commits.yml: -------------------------------------------------------------------------------- 1 | name: Check Commit Messages 2 | on: push 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout the code 10 | uses: actions/checkout@v4.0.0 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: Lint the commits 15 | uses: wagoid/commitlint-github-action@v5.4.3 16 | with: 17 | configFile: .commitlintrc.js 18 | failOnWarnings: true 19 | -------------------------------------------------------------------------------- /.github/workflows/check-links.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Markdown Links Validation 3 | 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | markdown-link-check: 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - name: Checkout this repository 12 | uses: actions/checkout@v4.0.0 13 | 14 | - name: Validate Links Markdown .md files 15 | if: always() 16 | uses: gaurav-nelson/github-action-markdown-link-check@1.0.15 17 | with: 18 | config-file: '.markdownlinkcheck.json' 19 | use-quiet-mode: 'yes' 20 | file-extension: .md 21 | -------------------------------------------------------------------------------- /.github/workflows/check-markdown.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Markdown Syntax Validation 3 | 4 | on: [push, workflow_dispatch] 5 | 6 | jobs: 7 | markdown-check: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout this repository 12 | uses: actions/checkout@v4.0.0 13 | 14 | - name: Validate Markdown .md 15 | uses: DavidAnson/markdownlint-cli2-action@v13.0.0 16 | with: 17 | config: .markdownlint.json 18 | globs: "**.md" 19 | -------------------------------------------------------------------------------- /.github/workflows/check-signed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Validate if commits are signed 3 | on: [pull_request, pull_request_target] 4 | 5 | jobs: 6 | signed-commits-check: 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Check out the repository code 11 | uses: actions/checkout@v4.1.4 12 | 13 | - name: Check if the commits are signed 14 | uses: 1Password/check-signed-commits-action@v1 15 | -------------------------------------------------------------------------------- /.github/workflows/check-unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run unit tests 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up Go 12 | uses: actions/setup-go@v5 13 | with: 14 | go-version-file: go.mod 15 | - name: Run GolangCI-Lint 16 | uses: golangci/golangci-lint-action@v6 17 | with: 18 | version: v1.60.3 19 | - name: Test application 20 | run: go test -short -v ./... 21 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: kray-releaser 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 9 | RELEASE_NOTES: "generated by a new release in kubefirst/kubefirst repo" 10 | 11 | jobs: 12 | release-repos: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Update version file 18 | run: echo $GITHUB_REF_NAME > VERSION.md 19 | - name: Release konstructio/gitops-template 20 | run: gh release create -R konstructio/gitops-template ${{ github.REF_NAME }} --generate-notes 21 | goreleaser: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - 25 | name: Checkout 26 | uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | - 30 | name: Set up Go 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version-file: go.mod 34 | 35 | - 36 | name: Run GoReleaser 37 | uses: goreleaser/goreleaser-action@v3 38 | with: 39 | distribution: goreleaser 40 | version: latest 41 | args: release --clean 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | ref: ${{ github.ref }} 13 | - name: Running Docker Compose tests 14 | run: | 15 | docker-compose -f docker-compose-test.yaml build --no-cache \ 16 | && docker compose -f docker-compose-test.yaml run kubefirst-unit-tests 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.env 2 | node_modules 3 | .terraform 4 | .gitlab-bot-access-token 5 | .gitlab-runner-registration-token 6 | .vscode 7 | terraform-ssh-key 8 | terraform-ssh-key.pub 9 | kubeconfig_* 10 | */cypress/screenshots/ 11 | */cypress/videos/ 12 | dist/ 13 | **/.DS_Store 14 | /git 15 | bin 16 | .vscode/settings.json 17 | logs/ 18 | /tmp 19 | lint_log.txt 20 | .idea 21 | k3d-linux-amd64.1 22 | k3d-linux-amd64 23 | my.test 24 | go.test 25 | kubefirst.yaml 26 | # kubefirst # <- this is causing files in docs to not commit, need a more explicit path ignored 27 | 28 | __debug_* 29 | kubefirst 30 | launch.json 31 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: false 3 | concurrency: 5 4 | timeout: 5m 5 | 6 | linters: 7 | disable-all: true 8 | enable: 9 | - gosimple 10 | - govet 11 | - ineffassign 12 | - staticcheck 13 | - unused 14 | - asasalint 15 | - asciicheck 16 | - bidichk 17 | - bodyclose 18 | - contextcheck 19 | - decorder 20 | - dogsled 21 | - dupl 22 | - dupword 23 | - durationcheck 24 | - errchkjson 25 | - errname 26 | - errorlint 27 | - exhaustive 28 | - copyloopvar 29 | - ginkgolinter 30 | - gocheckcompilerdirectives 31 | - gochecksumtype 32 | - gocritic 33 | - gocyclo 34 | - gofmt 35 | - gofumpt 36 | - goheader 37 | - goimports 38 | - gomodguard 39 | - goprintffuncname 40 | - gosec 41 | - gosmopolitan 42 | - grouper 43 | - importas 44 | - inamedparam 45 | - interfacebloat 46 | - ireturn 47 | - loggercheck 48 | - makezero 49 | - mirror 50 | - misspell 51 | - nakedret 52 | - nilerr 53 | - nilnil 54 | - nonamedreturns 55 | - nosprintfhostport 56 | - paralleltest 57 | - prealloc 58 | - predeclared 59 | - promlinter 60 | - protogetter 61 | - reassign 62 | - revive 63 | - rowserrcheck 64 | - sloglint 65 | - spancheck 66 | - sqlclosecheck 67 | - stylecheck 68 | - tenv 69 | - testableexamples 70 | - testifylint 71 | - testpackage 72 | - thelper 73 | - tparallel 74 | - unconvert 75 | - unparam 76 | - usestdlibvars 77 | - wastedassign 78 | - whitespace 79 | - wrapcheck 80 | - zerologlint 81 | 82 | linters-settings: 83 | perfsprint: 84 | int-conversion: false 85 | err-error: false 86 | errorf: true 87 | sprintf1: true 88 | strconcat: false 89 | 90 | ireturn: 91 | allow: 92 | - anon 93 | - error 94 | - empty 95 | - stdlib 96 | - ssh.PublicKey 97 | - tea.Model 98 | 99 | gosec: 100 | confidence: medium 101 | excludes: 102 | - G107 # Potential HTTP request made with variable url: these are often false positives or intentional 103 | - G110 # Decompression bombs: we can check these manually when submitting code 104 | - G306 # Poor file permissions used when creating a directory: we can check these manually when submitting code 105 | - G404 # Use of weak random number generator (math/rand instead of crypto/rand): we can live with these 106 | 107 | stylecheck: 108 | checks: 109 | - "all" 110 | - "-ST1003" # this is covered by a different linter 111 | 112 | gocyclo: 113 | min-complexity: 60 114 | 115 | exhaustive: 116 | check-generated: false 117 | explicit-exhaustive-switch: false 118 | explicit-exhaustive-map: false 119 | default-case-required: false 120 | default-signifies-exhaustive: true 121 | package-scope-only: false 122 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | # You may remove this if you don't use go modules. 4 | - go mod tidy 5 | # # you may remove this if you don't need go generate 6 | # - go generate ./... 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | # - windows 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - -X github.com/konstructio/kubefirst-api/configs.K1Version=v{{.Version}} 18 | 19 | #archives: 20 | # - replacements: 21 | # darwin: Darwin 22 | # linux: Linux 23 | # windows: Windows 24 | # 386: i386 25 | # amd64: x86_64 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: '{{ incpatch .Version }}-next' 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | brews: 37 | - name: kubefirst 38 | homepage: https://github.com/konstructio/kubefirst 39 | repository: 40 | owner: konstructio 41 | name: homebrew-taps 42 | dependencies: 43 | - aws-iam-authenticator 44 | version: 2 45 | -------------------------------------------------------------------------------- /.markdownlinkcheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https://www.linkedin.com.*" 5 | }, 6 | { 7 | "pattern": ".*\\]$" 8 | }, 9 | { 10 | "pattern": "https://www.enterprisetimes.co.uk*" 11 | }, 12 | { 13 | "pattern": "https://www.pexels.com*" 14 | }, 15 | { 16 | "pattern": "https://www.pngrepo.com" 17 | }, 18 | { 19 | "pattern": "https://tdwi.org*" 20 | }, 21 | { 22 | "pattern": "https://www.fakepersongenerator.com*" 23 | }, 24 | { 25 | "pattern": "https://dash.readme.com*" 26 | }, 27 | { 28 | "pattern": "https://cfpland.com*" 29 | }, 30 | { 31 | "pattern": "https://n8n.io*" 32 | }, 33 | { 34 | "pattern": "https://confs.tech*" 35 | }, 36 | { 37 | "pattern": "https://businesswire.com*" 38 | }, 39 | { 40 | "pattern": "https://www.tmcnet.com*" 41 | }, 42 | { 43 | "pattern": "https://.*kubefirst.dev*" 44 | }, 45 | { 46 | "pattern": "http://localhost*" 47 | }, 48 | { 49 | "pattern": "https://gitlab.com*" 50 | }, 51 | { 52 | "pattern": "https://.*vultr.com*" 53 | } 54 | ], 55 | "replacementPatterns": [ 56 | { 57 | "pattern": "{require(\"(*)\").default}", 58 | "replacement": "*" 59 | }, 60 | { 61 | "pattern": "^/img/*", 62 | "replacement": "/static/img/*" 63 | } 64 | ], 65 | "timeout": "30s", 66 | "retryOn429": true, 67 | "retryCount": 2, 68 | "fallbackRetryDelay": "1m", 69 | "aliveStatusCodes": [200, 429] 70 | } 71 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD024": false, 4 | "MD025": false, 5 | "MD033": false, 6 | "MD049": { 7 | "style": "underscore" 8 | }, 9 | "MD050": { 10 | "style": "asterisk" 11 | } 12 | } -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.20.5 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others' private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [conduct@konstruct.io](mailto:conduct@konstruct.io). All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | 49 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 50 | 51 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 52 | 53 | ### 2. Warning 54 | 55 | **Community Impact**: A violation through a single incident or series of actions. 56 | 57 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 58 | 59 | ### 3. Temporary Ban 60 | 61 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 62 | 63 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 64 | 65 | ### 4. Permanent Ban 66 | 67 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 68 | 69 | **Consequence**: A permanent ban from any sort of public interaction within the community. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html) version 2.0. 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Kubefirst 2 | 3 | Firstly, we want to thank you for investing your valuable time to contribute to Kubefirst! 4 | 5 | _⚠️ Please note that this file is a work-in-progress, so more details will be added in the future._ 6 | 7 | Note we have a [code of conduct](CODE_OF_CONDUCT.md) which needs to be followed in all your interactions with the project to keep our community healthy. 8 | 9 | ## Ways to Contribute 10 | 11 | At Kubefirst, we believe that every contribution is valuable, not just the code one, which means we welcome 12 | 13 | - [bug reports](https://github.com/konstructio/kubefirst/issues/new); 14 | - [feature requests](https://github.com/konstructio/kubefirst/issues/new?assignees=&labels=feature-request&template=feature_request.md&title=); 15 | - [documentations issues reports](https://github.com/konstructio/kubefirst/issues/new?assignees=&labels=feature-request&template=feature_request.md&title=) like unclear section, missing information or even typos; 16 | - and, of course, any code contributions to Kubefirst, or the documentations. 17 | 18 | Before making a code change, first discuss your idea via an [issue](https://github.com/konstructio/kubefirst/issues/new/choose). Please check if a feature request or bug report does [already exist](https://github.com/konstructio/kubefirst/issues/) before creating a new one. 19 | 20 | ## Getting Started with the Code 21 | 22 | ### Dev containers 23 | 24 | A [.devcontainer](https://containers.dev/) configuration is provided to allow for a full-featured development environment. 25 | 26 | ### Local development 27 | 28 | #### The CLI 29 | 30 | Kubefirst is created using the [Go Programming Language](https://go.dev). To set up your computer, follow [these steps](https://go.dev/doc/install). 31 | 32 | Once Go is installed, you can run Kubefirst from any branch using `go run .`. Go will automatically install the needed modules listed in the [go.mod](go.mod) file. Since Go is a compiled programming language, every time you use the `run` command, Go will compile the code before running it. If you want to save time, you can compile your code using `go build`, which will generate a file named `kubefirst`. You will then be able to run your compiled version with the `./kubefirst` command. 33 | 34 | If you want to create a [Civo cluster](https://kubefirst.konstruct.io/docs/civo/quick-start/install/cli), the command would be `go run . civo create`. 35 | 36 | #### GitOps Template 37 | 38 | Note that even if you run kubefirst from `main`, the [gitops-template](https://github.com/konstructio/gitops-template) version used will be the [latest release](https://github.com/konstructio/gitops-template/releases). If you also want to use the latest from `main` for the template, you need to run to use the `--gitops-template-url`, and the `--gitops-template-branch` as follow: 39 | 40 | ```shell 41 | go run . civo create --gitops-template-url https://github.com/konstructio/gitops-template --gitops-template-branch main 42 | ``` 43 | 44 | #### Kubefirst API 45 | 46 | If you need to use a specific branch or latest from `main` that wasn't released yet for the [kubefirst-api](https://github.com/konstructio/kubefirst-api) repository, you will need to first run it locally as described in [its documentation](https://github.com/konstructio/kubefirst-api#running-locally). You will also need to run the code from [console](https://github.com/konstructio/console) repository, whether you need to use a specific version of the code or not, as we don't expose the API directly. To do so, follow the [instructions in its README](https://github.com/konstructio/console#setup-instructions). Before running the CLI as mentionned "The CLI" section, you need to export a local variable: 47 | 48 | ```shell 49 | export K1_CONSOLE_REMOTE_URL="http://localhost:3000" 50 | ``` 51 | 52 | The previous steps will work for all clouds except k3d which use our runtime for now: we have plan to remove this dependencies completely and use the API also to make the code easier to maintain, and less prone to issues. For that step, instead of running the API, and console locally, you simply need to clone the [kubefirst-api](https://github.com/konstructio/kubefirst-api) repository locally, and add the following line in the `go.mod` file: 53 | 54 | ```go 55 | github.com/konstructio/kubefirst-api vX.X.XX => /path-to/kubefirst-api/ 56 | ``` 57 | 58 | Replace `vX.X.XX` with the latest version used in the mode file for the API, and the `/path-to/kubefirst-api/` with the path to the folder of your locally Kubefirst API folder. 59 | 60 | ## Getting Started with the Documentation 61 | 62 | Please check the [CONTRIBUTING.md](https://github.com/konstructio/kubefirst-docs/blob/main/CONTRIBUTING.md) file from the [docs](https://github.com/konstructio/kubefirst-docs/) repository. 63 | 64 | ## Help 65 | 66 | If you need help in your Kubefirst journey as a contributor, please join our [Slack Community](http://kubefirst.io/slack). We have the `#contributors` channel where you can ask any questions or get help with anything contribution-related. For support as a user, please ask in the `#helping-hands` channel, or directly to @fharper (Fred in Slack), our Developer Advocate. 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Kubefirst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | 5 | 6 | 7 |

8 |

9 | GitOps Infrastructure & Application Delivery Platform 10 |

11 | 12 |

13 | Install |  14 | Twitter |  15 | LinkedIn |  16 | Slack |  17 | Blog 18 |

19 | 20 |

21 | 22 | 23 | 24 |

25 | 26 | --- 27 | 28 | # Kubefirst CLI 29 | 30 | The Kubefirst CLI creates instant GitOps platforms that integrate some of the best tools in cloud native from scratch in minutes. 31 | 32 | Each of our platforms have install guides that detail the prerequesites, commands, and resulting platform that you'll receive. 33 | 34 | - [k3d (local)](https://kubefirst.konstruct.io/docs/k3d/overview) 35 | - [Akamai](https://docs.kubefirst.io/akamai/overview) 36 | - [AWS](https://kubefirst.konstruct.io/docs/aws/overview) 37 | - [Azure](https://docs.kubefirst.io/azure/overview) 38 | - [Civo](https://kubefirst.konstruct.io/docs/civo/overview) 39 | - [DigitalOcean](https://kubefirst.konstruct.io/docs/do/overview) 40 | - [Google Cloud](https://kubefirst.konstruct.io/docs/gcp/overview) 41 | - [Vultr](https://kubefirst.konstruct.io/docs/vultr/overview) 42 | - [K3s](https://kubefirst.konstruct.io/docs/k3s/overview) 43 | 44 | ## Overview 45 | 46 | 47 | 48 | ![kubefirst architecture diagram](images/kubefirst-arch.png) 49 | 50 | ## Feed K-Ray 51 | 52 | Feed K-Ray a GitHub star ⭐ above to bookmark our project and keep K-Ray happy!! 53 | 54 | [![Star History Chart](https://api.star-history.com/svg?repos=kubefirst/kubefirst&type=Date)](https://star-history.com/#kubefirst/kubefirst&Date) 55 | 56 | ## Contributions 57 | 58 | We want to thank all of our contributors who created a pull request to fix a bug, add a new feature or update the [documentation](https://github.com/konstructio/kubefirst-docs/). We also value a lot contributions in the form of bug reporting or feature requests: it helps us continuously make kubefirst better. Lastly, helping the users in our Slack community, or helping us share the love on social media are also ways in which you support us tremendously. We know your time is valuable, and we can't thank you enough for everything you do: we wouldn't be where we are without you! 59 | 60 | A special thanks to [DrummyFloyd](https://github.com/DrummyFloyd) who, in addition to adding support for k3s, is a champion all around within our community and the kubefirst project 🫶 61 | 62 | If you want to help in any capacity, check [CONTRIBUTING.md](CONTRIBUTING.md). 63 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | If you find any security issue with kubefirst, please report them immediately to us by sending an email to [product@kubefirst.io](mailto:product@kubefirst.io). 4 | 5 | Please note that we do not have any bounty program at the moment. 6 | -------------------------------------------------------------------------------- /SUPPORT.MD: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## Issues 4 | 5 | If you are having an issue with kubefirst, we highly suggest that you share the problem with us on our Slack Community (read the next section for more information). If you are certain it's a bug with our platform, you can create an [issue](https://github.com/konstructio/kubefirst/issues/new/choose). We'll get back to you as soon as possible. 6 | 7 | ## Questions 8 | 9 | If you have a question about how to use Kubefirst, please join our [Slack community](https://kubefirst.io/slack), and ask your question in the `#helping-hands` channel. If you prefer to ask in private, you can send a direct message to our Principal Developer Advocate, Frédéric Harper, `@Fred` on Slack. 10 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 buildpack-deps:bullseye-scm 2 | # we are using buidlpack-deps:bullseye-scm https://github.com/docker-library/golang/blob/8d0fa6028120904e16fe761f095bd0620b68eab2/1.18/bullseye/Dockerfile 3 | 4 | ARG KUBEFIRST_VERSION=1.10.5 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y unzip curl jq vim unzip less \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | # Kubernetes client 11 | RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.21.3/bin/$(uname -s)/amd64/kubectl && \ 12 | chmod +x ./kubectl && \ 13 | mv kubectl /usr/local/bin/ 14 | 15 | # AWS cli 16 | RUN curl -LO https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip && \ 17 | unzip awscli-exe-linux-x86_64.zip && \ 18 | ./aws/install && \ 19 | rm -r aws && \ 20 | rm awscli-exe-linux-x86_64.zip 21 | 22 | # AWS EKS cli 23 | RUN curl -LO https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_linux_amd64.tar.gz && \ 24 | tar -xvzf eksctl_linux_amd64.tar.gz -C /usr/local/bin/ && \ 25 | rm eksctl_linux_amd64.tar.gz 26 | 27 | # AWS IAM Authenticator tool 28 | RUN curl -LO https://s3.us-west-2.amazonaws.com/amazon-eks/1.21.2/2021-07-05/bin/linux/amd64/aws-iam-authenticator && \ 29 | chmod +x aws-iam-authenticator && \ 30 | mv aws-iam-authenticator /usr/local/bin/ 31 | 32 | # change shell from bin/sh to bin/bash 33 | SHELL ["/bin/bash", "-c"] 34 | 35 | # Kubefirst cli 36 | RUN curl -LO https://github.com/konstructio/kubefirst/releases/download/$KUBEFIRST_VERSION/kubefirst_${KUBEFIRST_VERSION:1}_linux_amd64.tar.gz && \ 37 | tar -xvzf kubefirst_${KUBEFIRST_VERSION:1}_linux_amd64.tar.gz -C /usr/local/bin/ && \ 38 | chmod +x /usr/local/bin/kubefirst && \ 39 | rm kubefirst_${KUBEFIRST_VERSION:1}_linux_amd64.tar.gz 40 | 41 | # setup user 42 | RUN useradd -ms /bin/bash developer 43 | USER developer 44 | WORKDIR /home/developer/kubefirst 45 | 46 | RUN kubefirst version 47 | -------------------------------------------------------------------------------- /cmd/akamai/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package akamai 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/constants" 13 | "github.com/konstructio/kubefirst/internal/catalog" 14 | "github.com/konstructio/kubefirst/internal/cluster" 15 | "github.com/konstructio/kubefirst/internal/common" 16 | "github.com/konstructio/kubefirst/internal/provision" 17 | "github.com/konstructio/kubefirst/internal/step" 18 | "github.com/konstructio/kubefirst/internal/utilities" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var ( 23 | // Supported providers 24 | supportedDNSProviders = []string{"cloudflare"} 25 | supportedGitProviders = []string{"github", "gitlab"} 26 | // Supported git protocols 27 | supportedGitProtocolOverride = []string{"https", "ssh"} 28 | ) 29 | 30 | func NewCommand() *cobra.Command { 31 | akamaiCmd := &cobra.Command{ 32 | Use: "akamai", 33 | Short: "kubefirst akamai installation", 34 | Long: "kubefirst akamai", 35 | Run: func(_ *cobra.Command, _ []string) { 36 | fmt.Println("To learn more about akamai in kubefirst, run:") 37 | fmt.Println(" kubefirst akamai --help") 38 | }, 39 | } 40 | 41 | // wire up new commands 42 | akamaiCmd.AddCommand(Create(), Destroy(), RootCredentials()) 43 | 44 | return akamaiCmd 45 | } 46 | 47 | func Create() *cobra.Command { 48 | createCmd := &cobra.Command{ 49 | Use: "create", 50 | Short: "create the kubefirst platform running on akamai kubernetes", 51 | TraverseChildren: true, 52 | RunE: func(cmd *cobra.Command, _ []string) error { 53 | ctx := cmd.Context() 54 | cloudProvider := "akamai" 55 | estimatedTimeMin := 25 56 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 57 | 58 | stepper.DisplayLogHints(cloudProvider, estimatedTimeMin) 59 | 60 | stepper.NewProgressStep("Validate Configuration") 61 | 62 | cliFlags, err := utilities.GetFlags(cmd, cloudProvider) 63 | if err != nil { 64 | wrerr := fmt.Errorf("error during flag retrieval: %w", err) 65 | stepper.FailCurrentStep(wrerr) 66 | return wrerr 67 | } 68 | 69 | isValid, catalogApps, err := catalog.ValidateCatalogApps(ctx, cliFlags.InstallCatalogApps) 70 | if !isValid { 71 | wrerr := fmt.Errorf("catalog validation failed: %w", err) 72 | stepper.FailCurrentStep(wrerr) 73 | return wrerr 74 | } 75 | 76 | err = ValidateProvidedFlags(cliFlags.GitProvider, cliFlags.DNSProvider) 77 | if err != nil { 78 | wrerr := fmt.Errorf("error during flag validation: %w", err) 79 | stepper.FailCurrentStep(wrerr) 80 | return wrerr 81 | } 82 | 83 | stepper.CompleteCurrentStep() 84 | 85 | clusterClient := cluster.Client{} 86 | 87 | provision := provision.NewProvisioner(provision.NewProvisionWatcher(cliFlags.ClusterName, &clusterClient), stepper) 88 | 89 | if err := provision.ProvisionManagementCluster(ctx, cliFlags, catalogApps); err != nil { 90 | return fmt.Errorf("failed to create cluster: %w", err) 91 | } 92 | 93 | return nil 94 | }, 95 | } 96 | 97 | akamaiDefaults := constants.GetCloudDefaults().Akamai 98 | 99 | // todo review defaults and update descriptions 100 | createCmd.Flags().String("alerts-email", "", "email address for let's encrypt certificate notifications (required)") 101 | createCmd.MarkFlagRequired("alerts-email") 102 | createCmd.Flags().Bool("ci", false, "if running kubefirst in ci, set this flag to disable interactive features") 103 | createCmd.Flags().String("cloud-region", "us-central", "the akamai region to provision infrastructure in") 104 | createCmd.Flags().String("cluster-name", "kubefirst", "the name of the cluster to create") 105 | createCmd.Flags().String("cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") 106 | createCmd.Flags().String("node-count", akamaiDefaults.NodeCount, "the node count for the cluster") 107 | createCmd.Flags().String("node-type", akamaiDefaults.InstanceSize, "the instance size of the cluster to create") 108 | createCmd.Flags().String("dns-provider", "cloudflare", fmt.Sprintf("the dns provider - one of: %q", supportedDNSProviders)) 109 | createCmd.Flags().String("subdomain", "", "the subdomain to use for DNS records (Cloudflare)") 110 | createCmd.Flags().String("domain-name", "", "the DNS Name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") 111 | createCmd.MarkFlagRequired("domain-name") 112 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) 113 | createCmd.Flags().String("git-protocol", "https", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) 114 | createCmd.Flags().String("github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") 115 | createCmd.Flags().String("gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") 116 | createCmd.Flags().String("gitops-template-branch", "", "the branch to clone for the gitops-template repository") 117 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") 118 | createCmd.Flags().String("install-catalog-apps", "", "comma separated values to install after provision") 119 | createCmd.Flags().Bool("use-telemetry", true, "whether to emit telemetry") 120 | createCmd.Flags().Bool("install-kubefirst-pro", true, "whether or not to install kubefirst pro") 121 | 122 | return createCmd 123 | } 124 | 125 | func Destroy() *cobra.Command { 126 | destroyCmd := &cobra.Command{ 127 | Use: "destroy", 128 | Short: "destroy the kubefirst platform", 129 | Long: "destroy the kubefirst platform running in akamai and remove all resources", 130 | RunE: common.Destroy, 131 | } 132 | 133 | return destroyCmd 134 | } 135 | 136 | func RootCredentials() *cobra.Command { 137 | authCmd := &cobra.Command{ 138 | Use: "root-credentials", 139 | Short: "retrieve root authentication information for platform components", 140 | Long: "retrieve root authentication information for platform components", 141 | RunE: common.GetRootCredentials, 142 | } 143 | 144 | authCmd.Flags().Bool("argocd", false, "copy the ArgoCD password to the clipboard (optional)") 145 | authCmd.Flags().Bool("kbot", false, "copy the kbot password to the clipboard (optional)") 146 | authCmd.Flags().Bool("vault", false, "copy the vault password to the clipboard (optional)") 147 | 148 | return authCmd 149 | } 150 | -------------------------------------------------------------------------------- /cmd/akamai/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package akamai 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | func ValidateProvidedFlags(gitProvider, dnsProvider string) error { 18 | if os.Getenv("LINODE_TOKEN") == "" { 19 | return fmt.Errorf("your LINODE_TOKEN is not set - please set and re-run your last command") 20 | } 21 | 22 | if dnsProvider == "cloudflare" { 23 | if os.Getenv("CF_API_TOKEN") == "" { 24 | return fmt.Errorf("your CF_API_TOKEN environment variable is not set. Please set and try again") 25 | } 26 | } 27 | 28 | switch gitProvider { 29 | case "github": 30 | key, err := internalssh.GetHostKey("github.com") 31 | if err != nil { 32 | return fmt.Errorf("failed to fetch github host key: %w", err) 33 | } 34 | log.Info().Msgf("%q %s", "github.com", key.Type()) 35 | case "gitlab": 36 | key, err := internalssh.GetHostKey("gitlab.com") 37 | if err != nil { 38 | return fmt.Errorf("failed to fetch gitlab host key: %w", err) 39 | } 40 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/aws/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package aws 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "os" 13 | "slices" 14 | 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/service/ec2" 17 | ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 18 | "github.com/aws/aws-sdk-go-v2/service/ssm" 19 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 20 | "github.com/rs/zerolog/log" 21 | ) 22 | 23 | func ValidateProvidedFlags(ctx context.Context, cfg aws.Config, gitProvider, amiType, nodeType string) error { 24 | // Validate required environment variables for dns provider 25 | if dnsProviderFlag == "cloudflare" { 26 | if os.Getenv("CF_API_TOKEN") == "" { 27 | return fmt.Errorf("your CF_API_TOKEN environment variable is not set. Please set and try again") 28 | } 29 | } 30 | 31 | switch gitProvider { 32 | case "github": 33 | key, err := internalssh.GetHostKey("github.com") 34 | if err != nil { 35 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy: %w", err) 36 | } 37 | log.Info().Msgf("%q %s", "github.com", key.Type()) 38 | case "gitlab": 39 | key, err := internalssh.GetHostKey("gitlab.com") 40 | if err != nil { 41 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy: %w", err) 42 | } 43 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 44 | } 45 | 46 | ssmClient := ssm.NewFromConfig(cfg) 47 | ec2Client := ec2.NewFromConfig(cfg) 48 | paginator := ec2.NewDescribeInstanceTypesPaginator(ec2Client, &ec2.DescribeInstanceTypesInput{}) 49 | 50 | if err := validateAMIType(ctx, amiType, nodeType, ssmClient, ec2Client, paginator); err != nil { 51 | return fmt.Errorf("failed to validate ami type for node group: %w", err) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func getSessionCredentials(ctx context.Context, cp aws.CredentialsProvider) (*aws.Credentials, error) { 58 | // Retrieve credentials 59 | creds, err := cp.Retrieve(ctx) 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to retrieve AWS credentials: %w", err) 62 | } 63 | 64 | return &creds, nil 65 | } 66 | 67 | func validateAMIType(ctx context.Context, amiType, nodeType string, ssmClient ssmClienter, ec2Client ec2Clienter, paginator paginator) error { 68 | ssmParameterName, ok := supportedAMITypes[amiType] 69 | if !ok { 70 | return fmt.Errorf("not a valid ami type: %q", amiType) 71 | } 72 | 73 | amiID, err := getLatestAMIFromSSM(ctx, ssmClient, ssmParameterName) 74 | if err != nil { 75 | return fmt.Errorf("failed to get AMI ID from SSM: %w", err) 76 | } 77 | 78 | architecture, err := getAMIArchitecture(ctx, ec2Client, amiID) 79 | if err != nil { 80 | return fmt.Errorf("failed to get AMI architecture: %w", err) 81 | } 82 | 83 | instanceTypes, err := getSupportedInstanceTypes(ctx, paginator, architecture) 84 | if err != nil { 85 | return fmt.Errorf("failed to get supported instance types: %w", err) 86 | } 87 | 88 | for _, instanceType := range instanceTypes { 89 | if instanceType == nodeType { 90 | return nil 91 | } 92 | } 93 | 94 | return fmt.Errorf("node type %q not supported for %q\nSupported instance types: %s", nodeType, amiType, instanceTypes) 95 | } 96 | 97 | type ssmClienter interface { 98 | GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) 99 | } 100 | 101 | func getLatestAMIFromSSM(ctx context.Context, ssmClient ssmClienter, parameterName string) (string, error) { 102 | input := &ssm.GetParameterInput{ 103 | Name: aws.String(parameterName), 104 | } 105 | output, err := ssmClient.GetParameter(ctx, input) 106 | if err != nil { 107 | return "", fmt.Errorf("failure when fetching parameters: %w", err) 108 | } 109 | 110 | if output == nil || output.Parameter == nil || output.Parameter.Value == nil { 111 | return "", fmt.Errorf("invalid parameter value found for %q", parameterName) 112 | } 113 | 114 | return *output.Parameter.Value, nil 115 | } 116 | 117 | type ec2Clienter interface { 118 | DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error) 119 | } 120 | 121 | func getAMIArchitecture(ctx context.Context, ec2Client ec2Clienter, amiID string) (string, error) { 122 | input := &ec2.DescribeImagesInput{ 123 | ImageIds: []string{amiID}, 124 | } 125 | output, err := ec2Client.DescribeImages(ctx, input) 126 | if err != nil { 127 | return "", fmt.Errorf("failed to describe images: %w", err) 128 | } 129 | 130 | if len(output.Images) == 0 { 131 | return "", fmt.Errorf("no images found for AMI ID: %s", amiID) 132 | } 133 | 134 | return string(output.Images[0].Architecture), nil 135 | } 136 | 137 | type paginator interface { 138 | HasMorePages() bool 139 | NextPage(ctx context.Context, optFns ...func(*ec2.Options)) (*ec2.DescribeInstanceTypesOutput, error) 140 | } 141 | 142 | func getSupportedInstanceTypes(ctx context.Context, p paginator, architecture string) ([]string, error) { 143 | var instanceTypes []string 144 | for p.HasMorePages() { 145 | page, err := p.NextPage(ctx) 146 | if err != nil { 147 | return nil, fmt.Errorf("failed to load next pages for instance types: %w", err) 148 | } 149 | 150 | for _, instanceType := range page.InstanceTypes { 151 | if slices.Contains(instanceType.ProcessorInfo.SupportedArchitectures, ec2Types.ArchitectureType(architecture)) { 152 | instanceTypes = append(instanceTypes, string(instanceType.InstanceType)) 153 | } 154 | } 155 | } 156 | return instanceTypes, nil 157 | } 158 | -------------------------------------------------------------------------------- /cmd/aws/quota.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package aws 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "strings" 13 | 14 | awsinternal "github.com/konstructio/kubefirst-api/pkg/aws" 15 | "github.com/konstructio/kubefirst-api/pkg/reports" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | // printAwsQuotaWarning provides visual output detailing quota health for aws 20 | func printAwsQuotaWarning(messageHeader string, output map[string][]awsinternal.QuotaDetailResponse) string { 21 | var buf bytes.Buffer 22 | 23 | fmt.Fprintln(&buf, strings.Repeat("-", 70)) 24 | fmt.Fprintln(&buf, messageHeader) 25 | fmt.Fprintln(&buf, strings.Repeat("-", 70)) 26 | fmt.Fprintln(&buf, "") 27 | 28 | for service, quotas := range output { 29 | fmt.Fprintln(&buf, service) 30 | fmt.Fprintln(&buf, strings.Repeat("-", 35)) 31 | fmt.Fprintln(&buf, "") 32 | 33 | for _, thing := range quotas { 34 | fmt.Fprintf(&buf, "%s: %v\n", thing.QuotaName, thing.QuotaValue) 35 | } 36 | fmt.Fprintln(&buf, "") 37 | } 38 | 39 | // Write to logs, but also output to stdout 40 | return buf.String() 41 | } 42 | 43 | // evalAwsQuota provides an interface to the command-line 44 | func evalAwsQuota(cmd *cobra.Command, _ []string) error { 45 | cloudRegionFlag, err := cmd.Flags().GetString("cloud-region") 46 | if err != nil { 47 | return fmt.Errorf("failed to get cloud region flag: %w", err) 48 | } 49 | 50 | config, err := awsinternal.NewAwsV2(cloudRegionFlag) 51 | if err != nil { 52 | return fmt.Errorf("failed to create new aws config: %w", err) 53 | } 54 | 55 | awsClient := &awsinternal.Configuration{Config: config} 56 | quotaDetails, err := awsClient.GetServiceQuotas([]string{"eks", "vpc"}) 57 | if err != nil { 58 | return fmt.Errorf("failed to get service quotas: %w", err) 59 | } 60 | 61 | messageHeader := fmt.Sprintf( 62 | "AWS Quota Health\nRegion: %s\n\nIf you encounter issues deploying your Kubefirst cluster, check these quotas and determine if you need to request a limit increase.", 63 | cloudRegionFlag, 64 | ) 65 | result := printAwsQuotaWarning(messageHeader, quotaDetails) 66 | 67 | // Write to logs, but also output to stdout 68 | fmt.Println(reports.StyleMessage(result)) 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /cmd/azure/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2024, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | 8 | package azure 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/konstructio/kubefirst-api/pkg/constants" 14 | "github.com/konstructio/kubefirst/internal/catalog" 15 | "github.com/konstructio/kubefirst/internal/cluster" 16 | "github.com/konstructio/kubefirst/internal/common" 17 | "github.com/konstructio/kubefirst/internal/provision" 18 | "github.com/konstructio/kubefirst/internal/step" 19 | "github.com/konstructio/kubefirst/internal/utilities" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | var ( 24 | // Supported providers 25 | supportedDNSProviders = []string{"azure", "cloudflare"} 26 | supportedGitProviders = []string{"github", "gitlab"} 27 | 28 | // Supported git providers 29 | supportedGitProtocolOverride = []string{"https", "ssh"} 30 | ) 31 | 32 | func NewCommand() *cobra.Command { 33 | azureCmd := &cobra.Command{ 34 | Use: "azure", 35 | Short: "Kubefirst Azure installation", 36 | Long: "Kubefirst Azure", 37 | Run: func(_ *cobra.Command, _ []string) { 38 | fmt.Println("To learn more about azure in kubefirst, run:") 39 | fmt.Println(" kubefirst azure --help") 40 | }, 41 | SilenceErrors: true, 42 | SilenceUsage: true, 43 | } 44 | 45 | // wire up new commands 46 | azureCmd.AddCommand(Create(), Destroy(), RootCredentials()) 47 | 48 | return azureCmd 49 | } 50 | 51 | func Create() *cobra.Command { 52 | createCmd := &cobra.Command{ 53 | Use: "create", 54 | Short: "create the kubefirst platform running on Azure kubernetes", 55 | TraverseChildren: true, 56 | RunE: func(cmd *cobra.Command, _ []string) error { 57 | cloudProvider := "azure" 58 | estimatedDurationMin := 20 59 | ctx := cmd.Context() 60 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 61 | 62 | stepper.DisplayLogHints(cloudProvider, estimatedDurationMin) 63 | 64 | stepper.NewProgressStep("Validate Configuration") 65 | cliFlags, err := utilities.GetFlags(cmd, cloudProvider) 66 | if err != nil { 67 | wrerr := fmt.Errorf("failed to get flags: %w", err) 68 | stepper.FailCurrentStep(wrerr) 69 | return wrerr 70 | } 71 | 72 | isValid, catalogApps, err := catalog.ValidateCatalogApps(ctx, cliFlags.InstallCatalogApps) 73 | if !isValid { 74 | wrerr := fmt.Errorf("invalid catalog apps: %w", err) 75 | stepper.FailCurrentStep(wrerr) 76 | return wrerr 77 | } 78 | 79 | err = ValidateProvidedFlags(cliFlags.GitProvider) 80 | if err != nil { 81 | wrerr := fmt.Errorf("failed to validate provided flags: %w", err) 82 | stepper.FailCurrentStep(wrerr) 83 | return wrerr 84 | } 85 | 86 | stepper.CompleteCurrentStep() 87 | 88 | clusterClient := cluster.Client{} 89 | provision := provision.NewProvisioner(provision.NewProvisionWatcher(cliFlags.ClusterName, &clusterClient), stepper) 90 | 91 | if err := provision.ProvisionManagementCluster(ctx, cliFlags, catalogApps); err != nil { 92 | return fmt.Errorf("failed to create Azure management cluster: %w", err) 93 | } 94 | 95 | return nil 96 | }, 97 | } 98 | 99 | azureDefaults := constants.GetCloudDefaults().Azure 100 | 101 | // todo review defaults and update descriptions 102 | createCmd.Flags().String("alerts-email", "", "email address for let's encrypt certificate notifications (required)") 103 | createCmd.MarkFlagRequired("alerts-email") 104 | createCmd.Flags().Bool("ci", false, "if running kubefirst in ci, set this flag to disable interactive features") 105 | createCmd.Flags().String("cloud-region", "eastus", "the Azure region to provision infrastructure in") 106 | createCmd.Flags().String("cluster-name", "kubefirst", "the name of the cluster to create") 107 | createCmd.Flags().String("cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") 108 | createCmd.Flags().String("node-count", azureDefaults.NodeCount, "the node count for the cluster") 109 | createCmd.Flags().String("node-type", azureDefaults.InstanceSize, "the instance size of the cluster to create") 110 | createCmd.Flags().String("dns-provider", "azure", fmt.Sprintf("the dns provider - one of: %s", supportedDNSProviders)) 111 | createCmd.Flags().String("dns-azure-resource-group", "", "the name of the resource group where the DNS Zone exists. If not set, the first matching zone will be used") 112 | createCmd.Flags().String("subdomain", "", "the subdomain to use for DNS records (Cloudflare)") 113 | createCmd.Flags().String("domain-name", "", "the Azure/Cloudflare DNS hosted zone name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") 114 | createCmd.MarkFlagRequired("domain-name") 115 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("the git provider - one of: %s", supportedGitProviders)) 116 | createCmd.Flags().String("git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %s", supportedGitProtocolOverride)) 117 | createCmd.Flags().String("github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") 118 | createCmd.Flags().String("gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") 119 | createCmd.Flags().String("gitops-template-branch", "", "the branch to clone for the gitops-template repository") 120 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") 121 | createCmd.Flags().String("install-catalog-apps", "", "comma separated values to install after provision") 122 | createCmd.Flags().Bool("use-telemetry", true, "whether to emit telemetry") 123 | createCmd.Flags().Bool("force-destroy", false, "allows force destruction on objects (helpful for test environments, defaults to false)") 124 | createCmd.Flags().Bool("install-kubefirst-pro", true, "whether or not to install kubefirst pro") 125 | 126 | return createCmd 127 | } 128 | 129 | func Destroy() *cobra.Command { 130 | destroyCmd := &cobra.Command{ 131 | Use: "destroy", 132 | Short: "destroy the kubefirst platform", 133 | Long: "destroy the kubefirst platform running in Azure and remove all resources", 134 | RunE: common.Destroy, 135 | } 136 | 137 | return destroyCmd 138 | } 139 | 140 | func RootCredentials() *cobra.Command { 141 | authCmd := &cobra.Command{ 142 | Use: "root-credentials", 143 | Short: "retrieve root authentication information for platform components", 144 | Long: "retrieve root authentication information for platform components", 145 | RunE: common.GetRootCredentials, 146 | } 147 | 148 | authCmd.Flags().Bool("argocd", false, "copy the argocd password to the clipboard (optional)") 149 | authCmd.Flags().Bool("kbot", false, "copy the kbot password to the clipboard (optional)") 150 | authCmd.Flags().Bool("vault", false, "copy the vault password to the clipboard (optional)") 151 | 152 | return authCmd 153 | } 154 | -------------------------------------------------------------------------------- /cmd/azure/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2024, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package azure 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | // Environment variables required for authentication. This should be a 18 | // service principal - the Terraform provider docs detail how to create 19 | // one 20 | // @link https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret.html 21 | var envvarSecrets = []string{ 22 | "ARM_CLIENT_ID", 23 | "ARM_CLIENT_SECRET", 24 | "ARM_TENANT_ID", 25 | "ARM_SUBSCRIPTION_ID", 26 | } 27 | 28 | func ValidateProvidedFlags(gitProvider string) error { 29 | for _, env := range envvarSecrets { 30 | if os.Getenv(env) == "" { 31 | return fmt.Errorf("your %s is not set - please set and re-run your last command", env) 32 | } 33 | } 34 | 35 | switch gitProvider { 36 | case "github": 37 | key, err := internalssh.GetHostKey("github.com") 38 | if err != nil { 39 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy") 40 | } else { 41 | log.Info().Msgf("%s %s\n", "github.com", key.Type()) 42 | } 43 | case "gitlab": 44 | key, err := internalssh.GetHostKey("gitlab.com") 45 | if err != nil { 46 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy") 47 | } else { 48 | log.Info().Msgf("%s %s\n", "gitlab.com", key.Type()) 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /cmd/civo/backup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package civo 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | "github.com/konstructio/kubefirst-api/pkg/providerConfigs" 14 | "github.com/konstructio/kubefirst-api/pkg/ssl" 15 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 16 | "github.com/rs/zerolog/log" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | func backupCivoSSL(_ *cobra.Command, _ []string) error { 22 | utils.DisplayLogHints() 23 | 24 | clusterName := viper.GetString("flags.cluster-name") 25 | domainName := viper.GetString("flags.domain-name") 26 | gitProvider := viper.GetString("flags.git-provider") 27 | gitProtocol := viper.GetString("flags.git-protocol") 28 | 29 | // Switch based on git provider, set params 30 | var cGitOwner string 31 | switch gitProvider { 32 | case "github": 33 | cGitOwner = viper.GetString("flags.github-owner") 34 | case "gitlab": 35 | cGitOwner = viper.GetString("flags.gitlab-owner") 36 | default: 37 | return fmt.Errorf("invalid git provider option: %q", gitProvider) 38 | } 39 | 40 | config, err := providerConfigs.GetConfig( 41 | clusterName, 42 | domainName, 43 | gitProvider, 44 | cGitOwner, 45 | gitProtocol, 46 | os.Getenv("CF_API_TOKEN"), 47 | os.Getenv("CF_ORIGIN_CA_ISSUER_API_TOKEN"), 48 | ) 49 | if err != nil { 50 | return fmt.Errorf("failed to get config: %w", err) 51 | } 52 | 53 | if _, err := os.Stat(config.SSLBackupDir + "/certificates"); os.IsNotExist(err) { 54 | // path/to/whatever does not exist 55 | paths := []string{config.SSLBackupDir + "/certificates", config.SSLBackupDir + "/clusterissuers", config.SSLBackupDir + "/secrets"} 56 | 57 | for _, path := range paths { 58 | if _, err := os.Stat(path); os.IsNotExist(err) { 59 | log.Info().Msgf("checking path: %q", path) 60 | err := os.MkdirAll(path, os.ModePerm) 61 | if err != nil { 62 | log.Info().Msg("directory already exists, continuing") 63 | } 64 | } 65 | } 66 | } 67 | 68 | err = ssl.Backup(config.SSLBackupDir, config.Kubeconfig) 69 | if err != nil { 70 | return fmt.Errorf("error backing up SSL resources: %w", err) 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /cmd/civo/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package civo 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | func ValidateProvidedFlags(gitProvider, dnsProvider string) error { 18 | if os.Getenv("CIVO_TOKEN") == "" { 19 | return fmt.Errorf("your CIVO_TOKEN is not set - please set and re-run your last command") 20 | } 21 | 22 | // Validate required environment variables for dns provider 23 | if dnsProvider == "cloudflare" { 24 | if os.Getenv("CF_API_TOKEN") == "" { 25 | return fmt.Errorf("your CF_API_TOKEN environment variable is not set. Please set and try again") 26 | } 27 | } 28 | 29 | switch gitProvider { 30 | case "github": 31 | key, err := internalssh.GetHostKey("github.com") 32 | if err != nil { 33 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy") 34 | } 35 | log.Info().Msgf("github.com %q", key.Type()) 36 | case "gitlab": 37 | key, err := internalssh.GetHostKey("gitlab.com") 38 | if err != nil { 39 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy") 40 | } 41 | log.Info().Msgf("gitlab.com %q", key.Type()) 42 | } 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /cmd/digitalocean/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package digitalocean 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/constants" 13 | "github.com/konstructio/kubefirst/internal/catalog" 14 | "github.com/konstructio/kubefirst/internal/cluster" 15 | "github.com/konstructio/kubefirst/internal/common" 16 | "github.com/konstructio/kubefirst/internal/provision" 17 | "github.com/konstructio/kubefirst/internal/step" 18 | "github.com/konstructio/kubefirst/internal/utilities" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var ( 23 | // Supported providers 24 | supportedDNSProviders = []string{"digitalocean", "cloudflare"} 25 | supportedGitProviders = []string{"github", "gitlab"} 26 | // Supported git protocols 27 | supportedGitProtocolOverride = []string{"https", "ssh"} 28 | ) 29 | 30 | func NewCommand() *cobra.Command { 31 | digitaloceanCmd := &cobra.Command{ 32 | Use: "digitalocean", 33 | Short: "Kubefirst DigitalOcean installation", 34 | Long: "Kubefirst DigitalOcean", 35 | Run: func(_ *cobra.Command, _ []string) { 36 | fmt.Println("To learn more about DigitalOcean in Kubefirst, run:") 37 | fmt.Println(" kubefirst digitalocean --help") 38 | }, 39 | } 40 | 41 | // on error, doesnt show helper/usage 42 | digitaloceanCmd.SilenceUsage = true 43 | 44 | // wire up new commands 45 | digitaloceanCmd.AddCommand(Create(), Destroy(), RootCredentials()) 46 | 47 | return digitaloceanCmd 48 | } 49 | 50 | func Create() *cobra.Command { 51 | createCmd := &cobra.Command{ 52 | Use: "create", 53 | Short: "create the Kubefirst platform running on DigitalOcean Kubernetes", 54 | TraverseChildren: true, 55 | RunE: func(cmd *cobra.Command, _ []string) error { 56 | cloudProvider := "digitalocean" 57 | estimatedTimeMin := 20 58 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 59 | ctx := cmd.Context() 60 | 61 | stepper.DisplayLogHints(cloudProvider, estimatedTimeMin) 62 | 63 | stepper.NewProgressStep("Validate Configuration") 64 | 65 | cliFlags, err := utilities.GetFlags(cmd, "digitalocean") 66 | if err != nil { 67 | wrerr := fmt.Errorf("failed to get flags: %w", err) 68 | stepper.FailCurrentStep(wrerr) 69 | return wrerr 70 | } 71 | 72 | _, catalogApps, err := catalog.ValidateCatalogApps(ctx, cliFlags.InstallCatalogApps) 73 | if err != nil { 74 | wrerr := fmt.Errorf("failed to validate catalog apps: %w", err) 75 | stepper.FailCurrentStep(wrerr) 76 | return wrerr 77 | } 78 | 79 | err = ValidateProvidedFlags(cliFlags.GitProvider, cliFlags.DNSProvider) 80 | if err != nil { 81 | wrerr := fmt.Errorf("failed to validate provided flags: %w", err) 82 | stepper.FailCurrentStep(wrerr) 83 | return wrerr 84 | } 85 | 86 | stepper.CompleteCurrentStep() 87 | clusterClient := cluster.Client{} 88 | 89 | provision := provision.NewProvisioner(provision.NewProvisionWatcher(cliFlags.ClusterName, &clusterClient), stepper) 90 | 91 | if err := provision.ProvisionManagementCluster(ctx, cliFlags, catalogApps); err != nil { 92 | return fmt.Errorf("failed to create DigitalOcean management cluster: %w", err) 93 | } 94 | 95 | return nil 96 | }, 97 | // PreRun: common.CheckDocker, 98 | } 99 | 100 | doDefaults := constants.GetCloudDefaults().DigitalOcean 101 | 102 | // todo review defaults and update descriptions 103 | createCmd.Flags().String("alerts-email", "", "email address for let's encrypt certificate notifications (required)") 104 | createCmd.MarkFlagRequired("alerts-email") 105 | createCmd.Flags().Bool("ci", false, "if running Kubefirst in CI, set this flag to disable interactive features") 106 | createCmd.Flags().String("cloud-region", "nyc3", "the DigitalOcean region to provision infrastructure in") 107 | createCmd.Flags().String("cluster-name", "kubefirst", "the name of the cluster to create") 108 | createCmd.Flags().String("cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") 109 | createCmd.Flags().String("node-count", doDefaults.NodeCount, "the node count for the cluster") 110 | createCmd.Flags().String("node-type", doDefaults.InstanceSize, "the instance size of the cluster to create") 111 | createCmd.Flags().String("dns-provider", "digitalocean", fmt.Sprintf("the dns provider - one of: %q", supportedDNSProviders)) 112 | createCmd.Flags().String("subdomain", "", "the subdomain to use for DNS records (Cloudflare)") 113 | createCmd.Flags().String("domain-name", "", "the DigitalOcean DNS Name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") 114 | createCmd.MarkFlagRequired("domain-name") 115 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) 116 | createCmd.Flags().String("git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) 117 | createCmd.Flags().String("github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using GitHub") 118 | createCmd.Flags().String("gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using GitLab") 119 | createCmd.Flags().String("gitops-template-branch", "", "the branch to clone for the gitops-template repository") 120 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") 121 | createCmd.Flags().String("install-catalog-apps", "", "comma separated values to install after provision") 122 | createCmd.Flags().Bool("use-telemetry", true, "whether to emit telemetry") 123 | createCmd.Flags().Bool("install-kubefirst-pro", true, "whether or not to install Kubefirst Pro") 124 | 125 | return createCmd 126 | } 127 | 128 | func Destroy() *cobra.Command { 129 | destroyCmd := &cobra.Command{ 130 | Use: "destroy", 131 | Short: "destroy the Kubefirst platform", 132 | Long: "destroy the Kubefirst platform running in DigitalOcean and remove all resources", 133 | RunE: common.Destroy, 134 | // PreRun: common.CheckDocker, 135 | } 136 | 137 | return destroyCmd 138 | } 139 | 140 | func RootCredentials() *cobra.Command { 141 | authCmd := &cobra.Command{ 142 | Use: "root-credentials", 143 | Short: "retrieve root authentication information for platform components", 144 | Long: "retrieve root authentication information for platform components", 145 | RunE: common.GetRootCredentials, 146 | } 147 | 148 | authCmd.Flags().Bool("argocd", false, "copy the ArgoCD password to the clipboard (optional)") 149 | authCmd.Flags().Bool("kbot", false, "copy the kbot password to the clipboard (optional)") 150 | authCmd.Flags().Bool("vault", false, "copy the vault password to the clipboard (optional)") 151 | 152 | return authCmd 153 | } 154 | -------------------------------------------------------------------------------- /cmd/digitalocean/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package digitalocean 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | func ValidateProvidedFlags(gitProvider, dnsProvider string) error { 18 | // Validate required environment variables for dns provider 19 | if dnsProvider == "cloudflare" { 20 | if os.Getenv("CF_API_TOKEN") == "" { 21 | return fmt.Errorf("your CF_API_TOKEN environment variable is not set. Please set and try again") 22 | } 23 | } 24 | 25 | for _, env := range []string{"DO_TOKEN", "DO_SPACES_KEY", "DO_SPACES_SECRET"} { 26 | if os.Getenv(env) == "" { 27 | return fmt.Errorf("your %q variable is unset - please set it before continuing", env) 28 | } 29 | } 30 | 31 | switch gitProvider { 32 | case "github": 33 | key, err := internalssh.GetHostKey("github.com") 34 | if err != nil { 35 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy") 36 | } 37 | log.Info().Msgf("%q %s", "github.com", key.Type()) 38 | case "gitlab": 39 | key, err := internalssh.GetHostKey("gitlab.com") 40 | if err != nil { 41 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy") 42 | } 43 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2025, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | 13 | "github.com/konstructio/kubefirst/internal/generate" 14 | "github.com/konstructio/kubefirst/internal/step" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func GenerateCommand() *cobra.Command { 19 | generateCommand := &cobra.Command{ 20 | Use: "generate", 21 | Short: "code generator helpers", 22 | } 23 | 24 | // wire up new commands 25 | generateCommand.AddCommand(generateApp()) 26 | 27 | return generateCommand 28 | } 29 | 30 | func generateApp() *cobra.Command { 31 | var name string 32 | var environments []string 33 | var outputPath string 34 | 35 | appScaffoldCmd := &cobra.Command{ 36 | Use: "app-scaffold", 37 | Short: "scaffold the gitops application repo", 38 | TraverseChildren: true, 39 | RunE: func(cmd *cobra.Command, _ []string) error { 40 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 41 | 42 | stepper.NewProgressStep("Create App Scaffold") 43 | 44 | if err := generate.AppScaffold(name, environments, outputPath); err != nil { 45 | wrerr := fmt.Errorf("error scaffolding app: %w", err) 46 | stepper.FailCurrentStep(wrerr) 47 | return wrerr 48 | } 49 | 50 | stepper.CompleteCurrentStep() 51 | 52 | stepper.InfoStepString(fmt.Sprintf("App successfully scaffolded: %s", name)) 53 | 54 | return nil 55 | }, 56 | } 57 | 58 | appScaffoldCmd.Flags().StringVarP(&name, "name", "n", "", "name of the app") 59 | appScaffoldCmd.MarkFlagRequired("name") 60 | appScaffoldCmd.Flags().StringSliceVar(&environments, "environments", []string{"development", "staging", "production"}, "environment names to create") 61 | appScaffoldCmd.Flags().StringVar(&outputPath, "output-path", filepath.Join(".", "registry", "environments"), "location to save generated files") 62 | 63 | return appScaffoldCmd 64 | } 65 | -------------------------------------------------------------------------------- /cmd/google/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package google 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/constants" 13 | "github.com/konstructio/kubefirst/internal/catalog" 14 | "github.com/konstructio/kubefirst/internal/cluster" 15 | "github.com/konstructio/kubefirst/internal/common" 16 | "github.com/konstructio/kubefirst/internal/provision" 17 | "github.com/konstructio/kubefirst/internal/step" 18 | "github.com/konstructio/kubefirst/internal/utilities" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var ( 23 | // Supported providers 24 | supportedDNSProviders = []string{"google", "cloudflare"} 25 | supportedGitProviders = []string{"github", "gitlab"} 26 | 27 | // Supported git providers 28 | supportedGitProtocolOverride = []string{"https", "ssh"} 29 | ) 30 | 31 | func NewCommand() *cobra.Command { 32 | googleCmd := &cobra.Command{ 33 | Use: "google", 34 | Short: "kubefirst Google installation", 35 | Long: "kubefirst google", 36 | Run: func(_ *cobra.Command, _ []string) { 37 | fmt.Println("To learn more about google in kubefirst, run:") 38 | fmt.Println(" kubefirst beta google --help") 39 | }, 40 | } 41 | 42 | // on error, doesnt show helper/usage 43 | googleCmd.SilenceUsage = true 44 | 45 | // wire up new commands 46 | googleCmd.AddCommand(Create(), Destroy(), RootCredentials()) 47 | 48 | return googleCmd 49 | } 50 | 51 | func Create() *cobra.Command { 52 | createCmd := &cobra.Command{ 53 | Use: "create", 54 | Short: "create the kubefirst platform running on GCP Kubernetes", 55 | TraverseChildren: true, 56 | RunE: func(cmd *cobra.Command, _ []string) error { 57 | cloudProvider := "google" 58 | estimatedTimeMin := 20 59 | ctx := cmd.Context() 60 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 61 | 62 | stepper.DisplayLogHints(cloudProvider, estimatedTimeMin) 63 | 64 | stepper.NewProgressStep("Validate Configuration") 65 | 66 | cliFlags, err := utilities.GetFlags(cmd, cloudProvider) 67 | if err != nil { 68 | wrerr := fmt.Errorf("failed to get flags: %w", err) 69 | stepper.FailCurrentStep(wrerr) 70 | return wrerr 71 | } 72 | 73 | _, catalogApps, err := catalog.ValidateCatalogApps(ctx, cliFlags.InstallCatalogApps) 74 | if err != nil { 75 | wrerr := fmt.Errorf("failed to validate catalog apps: %w", err) 76 | stepper.FailCurrentStep(wrerr) 77 | return wrerr 78 | } 79 | 80 | err = ValidateProvidedFlags(cliFlags.GitProvider) 81 | if err != nil { 82 | wrerr := fmt.Errorf("failed to validate provided flags: %w", err) 83 | stepper.FailCurrentStep(wrerr) 84 | return wrerr 85 | } 86 | 87 | stepper.CompleteCurrentStep() 88 | clusterClient := cluster.Client{} 89 | 90 | provision := provision.NewProvisioner(provision.NewProvisionWatcher(cliFlags.ClusterName, &clusterClient), stepper) 91 | 92 | if err := provision.ProvisionManagementCluster(ctx, cliFlags, catalogApps); err != nil { 93 | return fmt.Errorf("failed to create google management cluster: %w", err) 94 | } 95 | 96 | return nil 97 | }, 98 | // PreRun: common.CheckDocker, 99 | } 100 | 101 | googleDefaults := constants.GetCloudDefaults().Google 102 | 103 | // todo review defaults and update descriptions 104 | createCmd.Flags().String("alerts-email", "", "email address for let's encrypt certificate notifications (required)") 105 | createCmd.MarkFlagRequired("alerts-email") 106 | createCmd.Flags().Bool("ci", false, "if running kubefirst in ci, set this flag to disable interactive features") 107 | createCmd.Flags().String("cloud-region", "us-east1", "the GCP region to provision infrastructure in") 108 | createCmd.Flags().String("cluster-name", "kubefirst", "the name of the cluster to create") 109 | createCmd.Flags().String("cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") 110 | createCmd.Flags().String("node-count", googleDefaults.NodeCount, "the node count for the cluster") 111 | createCmd.Flags().String("node-type", googleDefaults.InstanceSize, "the instance size of the cluster to create") 112 | createCmd.Flags().String("dns-provider", "google", fmt.Sprintf("the dns provider - one of: %q", supportedDNSProviders)) 113 | createCmd.Flags().String("subdomain", "", "the subdomain to use for DNS records (Cloudflare)") 114 | createCmd.Flags().String("domain-name", "", "the GCP DNS Name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") 115 | createCmd.MarkFlagRequired("domain-name") 116 | createCmd.Flags().String("google-project", "", "google project id (required)") 117 | createCmd.MarkFlagRequired("google-project") 118 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) 119 | createCmd.Flags().String("git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) 120 | createCmd.Flags().String("github-org", "", "the GitHub organization for the new gitops and metaphor repositories - required if using github") 121 | createCmd.Flags().String("gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") 122 | createCmd.Flags().String("gitops-template-branch", "", "the branch to clone for the gitops-template repository") 123 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") 124 | createCmd.Flags().String("install-catalog-apps", "", "comma separated values to install after provision") 125 | createCmd.Flags().Bool("use-telemetry", true, "whether to emit telemetry") 126 | createCmd.Flags().Bool("force-destroy", false, "allows force destruction on objects (helpful for test environments, defaults to false)") 127 | createCmd.Flags().Bool("install-kubefirst-pro", true, "whether or not to install kubefirst pro") 128 | 129 | return createCmd 130 | } 131 | 132 | func Destroy() *cobra.Command { 133 | destroyCmd := &cobra.Command{ 134 | Use: "destroy", 135 | Short: "destroy the kubefirst platform", 136 | Long: "destroy the kubefirst platform running in Google and remove all resources", 137 | RunE: common.Destroy, 138 | // PreRun: common.CheckDocker, 139 | } 140 | 141 | return destroyCmd 142 | } 143 | 144 | func RootCredentials() *cobra.Command { 145 | authCmd := &cobra.Command{ 146 | Use: "root-credentials", 147 | Short: "retrieve root authentication information for platform components", 148 | Long: "retrieve root authentication information for platform components", 149 | RunE: common.GetRootCredentials, 150 | } 151 | 152 | authCmd.Flags().Bool("argocd", false, "copy the ArgoCD password to the clipboard (optional)") 153 | authCmd.Flags().Bool("kbot", false, "copy the kbot password to the clipboard (optional)") 154 | authCmd.Flags().Bool("vault", false, "copy the vault password to the clipboard (optional)") 155 | 156 | return authCmd 157 | } 158 | -------------------------------------------------------------------------------- /cmd/google/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package google 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | _ "k8s.io/client-go/plugin/pkg/client/auth" // required for authentication 16 | ) 17 | 18 | func ValidateProvidedFlags(gitProvider string) error { 19 | if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") == "" { 20 | return fmt.Errorf("your GOOGLE_APPLICATION_CREDENTIALS is not set - please set and re-run your last command") 21 | } 22 | 23 | _, err := os.Open(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) 24 | if err != nil { 25 | return fmt.Errorf("could not open GOOGLE_APPLICATION_CREDENTIALS file: %w", err) 26 | } 27 | 28 | switch gitProvider { 29 | case "github": 30 | key, err := internalssh.GetHostKey("github.com") 31 | if err != nil { 32 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy: %w", err) 33 | } 34 | log.Info().Msgf("%q %s", "github.com", key.Type()) 35 | case "gitlab": 36 | key, err := internalssh.GetHostKey("gitlab.com") 37 | if err != nil { 38 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy: %w", err) 39 | } 40 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /cmd/info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "runtime" 13 | "text/tabwriter" 14 | 15 | "github.com/konstructio/kubefirst-api/pkg/configs" 16 | "github.com/konstructio/kubefirst/internal/step" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | func InfoCommand() *cobra.Command { 21 | infoCmd := &cobra.Command{ 22 | Use: "info", 23 | Short: "provides general Kubefirst setup data", 24 | Long: `Provides machine data, files and folders paths`, 25 | RunE: func(cmd *cobra.Command, _ []string) error { 26 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 27 | config, err := configs.ReadConfig() 28 | if err != nil { 29 | wrerr := fmt.Errorf("failed to read config: %w", err) 30 | stepper.InfoStep(step.EmojiError, wrerr.Error()) 31 | return wrerr 32 | } 33 | 34 | var buf bytes.Buffer 35 | 36 | tw := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', tabwriter.Debug) 37 | 38 | fmt.Fprintln(&buf, "") 39 | fmt.Fprintln(&buf, "Info summary") 40 | fmt.Fprintln(&buf, "") 41 | 42 | fmt.Fprintf(tw, "Name\tValue\n") 43 | fmt.Fprintf(tw, "---\t---\n") 44 | fmt.Fprintf(tw, "Operational System\t%s\n", config.LocalOs) 45 | fmt.Fprintf(tw, "Architecture\t%s\n", config.LocalArchitecture) 46 | fmt.Fprintf(tw, "Golang version\t%s\n", runtime.Version()) 47 | fmt.Fprintf(tw, "Kubefirst config file\t%s\n", config.KubefirstConfigFilePath) 48 | fmt.Fprintf(tw, "Kubefirst config folder\t%s\n", config.K1FolderPath) 49 | fmt.Fprintf(tw, "Kubefirst Version\t%s\n", configs.K1Version) 50 | tw.Flush() 51 | 52 | stepper.InfoStepString(buf.String()) 53 | return nil 54 | }, 55 | } 56 | 57 | return infoCmd 58 | } 59 | -------------------------------------------------------------------------------- /cmd/k3d/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3d 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst/internal/progress" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ( 17 | // Supported git providers 18 | supportedGitProviders = []string{"github", "gitlab"} 19 | 20 | // Supported git protocols 21 | supportedGitProtocolOverride = []string{"https", "ssh"} 22 | ) 23 | 24 | func NewCommand() *cobra.Command { 25 | k3dCmd := &cobra.Command{ 26 | Use: "k3d", 27 | Short: "kubefirst k3d installation", 28 | Long: "kubefirst k3d", 29 | Run: func(_ *cobra.Command, _ []string) { 30 | fmt.Println("To learn more about k3d in kubefirst, run:") 31 | fmt.Println(" kubefirst k3d --help") 32 | 33 | if progress.Progress != nil { 34 | progress.Progress.Quit() 35 | } 36 | }, 37 | } 38 | 39 | // wire up new commands 40 | k3dCmd.AddCommand(Create(), Destroy(), MkCert(), RootCredentials(), UnsealVault()) 41 | 42 | return k3dCmd 43 | } 44 | 45 | func LocalCommandAlias() *cobra.Command { 46 | localCmd := &cobra.Command{ 47 | Use: "local", 48 | Short: "kubefirst local installation with k3d", 49 | Long: "kubefirst local installation with k3d", 50 | } 51 | 52 | // wire up new commands 53 | localCmd.AddCommand(Create(), Destroy(), MkCert(), RootCredentials(), UnsealVault()) 54 | 55 | return localCmd 56 | } 57 | 58 | func Create() *cobra.Command { 59 | createCmd := &cobra.Command{ 60 | Use: "create", 61 | Short: "create the kubefirst platform running in k3d on your localhost", 62 | TraverseChildren: true, 63 | RunE: runK3d, 64 | } 65 | 66 | // todo review defaults and update descriptions 67 | createCmd.Flags().Bool("ci", false, "if running kubefirst in ci, set this flag to disable interactive features") 68 | createCmd.Flags().String("cluster-name", "kubefirst", "the name of the cluster to create") 69 | createCmd.Flags().String("cluster-type", "mgmt", "the type of cluster to create (i.e. mgmt|workload)") 70 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("the git provider - one of: %q", supportedGitProviders)) 71 | createCmd.Flags().String("git-protocol", "ssh", fmt.Sprintf("the git protocol - one of: %q", supportedGitProtocolOverride)) 72 | createCmd.Flags().String("github-org", "", "the GitHub organization for the new gitops and metaphor repositories") 73 | createCmd.Flags().String("gitlab-group", "", "the GitLab group for the new gitops and metaphor projects - required if using gitlab") 74 | createCmd.Flags().String("gitops-template-branch", "", "the branch to clone for the gitops-template repository") 75 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "the fully qualified url to the gitops-template repository to clone") 76 | createCmd.Flags().String("install-catalog-apps", "", "comma separated values of catalog apps to install after provision") 77 | createCmd.Flags().Bool("use-telemetry", true, "whether to emit telemetry") 78 | 79 | return createCmd 80 | } 81 | 82 | func Destroy() *cobra.Command { 83 | destroyCmd := &cobra.Command{ 84 | Use: "destroy", 85 | Short: "destroy the kubefirst platform", 86 | Long: "deletes the GitHub resources, k3d resources, and local content to re-provision", 87 | RunE: destroyK3d, 88 | } 89 | 90 | return destroyCmd 91 | } 92 | 93 | func MkCert() *cobra.Command { 94 | mkCertCmd := &cobra.Command{ 95 | Use: "mkcert", 96 | Short: "create a single ssl certificate for a local application", 97 | Long: "create a single ssl certificate for a local application", 98 | RunE: mkCert, 99 | } 100 | 101 | mkCertCmd.Flags().String("application", "", "the name of the application (required)") 102 | mkCertCmd.MarkFlagRequired("application") 103 | mkCertCmd.Flags().String("namespace", "", "the application namespace (required)") 104 | mkCertCmd.MarkFlagRequired("namespace") 105 | 106 | return mkCertCmd 107 | } 108 | 109 | func RootCredentials() *cobra.Command { 110 | authCmd := &cobra.Command{ 111 | Use: "root-credentials", 112 | Short: "retrieve root authentication information for platform components", 113 | Long: "retrieve root authentication information for platform components", 114 | RunE: getK3dRootCredentials, 115 | } 116 | 117 | authCmd.Flags().Bool("argocd", false, "copy the ArgoCD password to the clipboard (optional)") 118 | authCmd.Flags().Bool("kbot", false, "copy the kbot password to the clipboard (optional)") 119 | authCmd.Flags().Bool("vault", false, "copy the vault password to the clipboard (optional)") 120 | 121 | return authCmd 122 | } 123 | 124 | func UnsealVault() *cobra.Command { 125 | unsealVaultCmd := &cobra.Command{ 126 | Use: "unseal-vault", 127 | Short: "check to see if an existing vault instance is sealed and, if so, unseal it", 128 | Long: "check to see if an existing vault instance is sealed and, if so, unseal it", 129 | RunE: unsealVault, 130 | } 131 | 132 | return unsealVaultCmd 133 | } 134 | -------------------------------------------------------------------------------- /cmd/k3d/mkcert.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3d 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/k3d" 13 | "github.com/konstructio/kubefirst-api/pkg/k8s" 14 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 15 | "github.com/konstructio/kubefirst/internal/progress" 16 | log "github.com/sirupsen/logrus" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | // mkCert creates a single certificate for a host for k3d 22 | func mkCert(cmd *cobra.Command, _ []string) error { 23 | utils.DisplayLogHints() 24 | 25 | appNameFlag, err := cmd.Flags().GetString("application") 26 | if err != nil { 27 | return fmt.Errorf("failed to get application flag: %w", err) 28 | } 29 | 30 | appNamespaceFlag, err := cmd.Flags().GetString("namespace") 31 | if err != nil { 32 | return fmt.Errorf("failed to get namespace flag: %w", err) 33 | } 34 | 35 | flags := utils.GetClusterStatusFlags() 36 | if !flags.SetupComplete { 37 | return fmt.Errorf("there doesn't appear to be an active k3d cluster") 38 | } 39 | 40 | config, err := k3d.GetConfig( 41 | viper.GetString("flags.cluster-name"), 42 | flags.GitProvider, 43 | viper.GetString(fmt.Sprintf("flags.%s-owner", flags.GitProvider)), 44 | flags.GitProtocol, 45 | ) 46 | if err != nil { 47 | return fmt.Errorf("failed to get config: %w", err) 48 | } 49 | 50 | kcfg, err := k8s.CreateKubeConfig(false, config.Kubeconfig) 51 | if err != nil { 52 | return fmt.Errorf("failed to create kubeconfig: %w", err) 53 | } 54 | 55 | log.Infof("Generating certificate for %s.%s...", appNameFlag, k3d.DomainName) 56 | 57 | err = k3d.GenerateSingleTLSSecret(kcfg.Clientset, *config, appNameFlag, appNamespaceFlag) 58 | if err != nil { 59 | return fmt.Errorf("error generating certificate for %s/%s: %w", appNameFlag, appNamespaceFlag, err) 60 | } 61 | 62 | log.Infof("Certificate generated. You can use it with an app by setting `tls.secretName: %s-tls` on a Traefik IngressRoute.", appNameFlag) 63 | progress.Progress.Quit() 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /cmd/k3d/root-credentials.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3d 8 | 9 | import ( 10 | "errors" 11 | "fmt" 12 | 13 | "github.com/konstructio/kubefirst-api/pkg/credentials" 14 | "github.com/konstructio/kubefirst-api/pkg/k3d" 15 | "github.com/konstructio/kubefirst-api/pkg/k8s" 16 | "github.com/konstructio/kubefirst/internal/progress" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | func getK3dRootCredentials(cmd *cobra.Command, _ []string) error { 22 | domainName := k3d.DomainName 23 | clusterName := viper.GetString("flags.cluster-name") 24 | gitProvider := viper.GetString("flags.git-provider") 25 | gitProtocol := viper.GetString("flags.git-protocol") 26 | gitOwner := viper.GetString(fmt.Sprintf("flags.%s-owner", gitProvider)) 27 | 28 | // Parse flags 29 | a, err := cmd.Flags().GetBool("argocd") 30 | if err != nil { 31 | return fmt.Errorf("failed to get ArgoCD flag: %w", err) 32 | } 33 | k, err := cmd.Flags().GetBool("kbot") 34 | if err != nil { 35 | return fmt.Errorf("failed to get kbot flag: %w", err) 36 | } 37 | v, err := cmd.Flags().GetBool("vault") 38 | if err != nil { 39 | return fmt.Errorf("failed to get vault flag: %w", err) 40 | } 41 | opts := credentials.CredentialOptions{ 42 | CopyArgoCDPasswordToClipboard: a, 43 | CopyKbotPasswordToClipboard: k, 44 | CopyVaultPasswordToClipboard: v, 45 | } 46 | 47 | // Determine if there are eligible installs 48 | _, err = credentials.EvalAuth(k3d.CloudProvider, gitProvider) 49 | if err != nil { 50 | return fmt.Errorf("failed to evaluate auth: %w", err) 51 | } 52 | 53 | // Determine if the Kubernetes cluster is available 54 | if !viper.GetBool("kubefirst-checks.create-k3d-cluster") { 55 | return errors.New("it looks like a Kubernetes cluster has not been created yet - try again") 56 | } 57 | 58 | // Instantiate kubernetes client 59 | config, err := k3d.GetConfig(clusterName, gitProvider, gitOwner, gitProtocol) 60 | if err != nil { 61 | return fmt.Errorf("failed to get config: %w", err) 62 | } 63 | 64 | kcfg, err := k8s.CreateKubeConfig(false, config.Kubeconfig) 65 | if err != nil { 66 | return fmt.Errorf("failed to create kubeconfig: %w", err) 67 | } 68 | 69 | err = credentials.ParseAuthData(kcfg.Clientset, k3d.CloudProvider, domainName, &opts) 70 | if err != nil { 71 | return fmt.Errorf("failed to parse auth data: %w", err) 72 | } 73 | 74 | progress.Progress.Quit() 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /cmd/k3d/vault.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3d 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "fmt" 13 | "strings" 14 | "time" 15 | 16 | "github.com/hashicorp/vault/api" 17 | "github.com/konstructio/kubefirst-api/pkg/k3d" 18 | "github.com/konstructio/kubefirst-api/pkg/k8s" 19 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 20 | "github.com/konstructio/kubefirst/internal/progress" 21 | "github.com/rs/zerolog/log" 22 | "github.com/spf13/cobra" 23 | "github.com/spf13/viper" 24 | "k8s.io/client-go/kubernetes" 25 | ) 26 | 27 | const ( 28 | // Name for the Secret that gets created that contains root auth data 29 | vaultSecretName = "vault-unseal-secret" 30 | // Namespace that Vault runs in 31 | vaultNamespace = "vault" 32 | // number of secret threshold Vault unseal 33 | secretThreshold = 3 34 | ) 35 | 36 | func unsealVault(_ *cobra.Command, _ []string) error { 37 | flags := utils.GetClusterStatusFlags() 38 | if !flags.SetupComplete { 39 | return fmt.Errorf("failed to unseal vault: there doesn't appear to be an active k3d cluster") 40 | } 41 | config, err := k3d.GetConfig( 42 | viper.GetString("flags.cluster-name"), 43 | flags.GitProvider, 44 | viper.GetString(fmt.Sprintf("flags.%s-owner", flags.GitProvider)), 45 | flags.GitProtocol, 46 | ) 47 | if err != nil { 48 | return fmt.Errorf("failed to get config: %w", err) 49 | } 50 | 51 | kcfg, err := k8s.CreateKubeConfig(false, config.Kubeconfig) 52 | if err != nil { 53 | return fmt.Errorf("failed to create kubeconfig: %w", err) 54 | } 55 | 56 | vaultClient, err := api.NewClient(&api.Config{ 57 | Address: "https://vault.kubefirst.dev", 58 | }) 59 | if err != nil { 60 | return fmt.Errorf("failed to create vault client: %w", err) 61 | } 62 | vaultClient.CloneConfig().ConfigureTLS(&api.TLSConfig{ 63 | Insecure: true, 64 | }) 65 | 66 | health, err := vaultClient.Sys().Health() 67 | if err != nil { 68 | return fmt.Errorf("failed to check vault health: %w", err) 69 | } 70 | 71 | if health.Sealed { 72 | node := "vault-0" 73 | existingInitResponse, err := parseExistingVaultInitSecret(kcfg.Clientset) 74 | if err != nil { 75 | return fmt.Errorf("failed to parse existing vault init secret: %w", err) 76 | } 77 | 78 | sealStatusTracking := 0 79 | for i, shard := range existingInitResponse.Keys { 80 | if i < secretThreshold { 81 | log.Info().Msgf("passing unseal shard %d to %q", i+1, node) 82 | deadline := time.Now().Add(60 * time.Second) 83 | ctx, cancel := context.WithDeadline(context.Background(), deadline) 84 | defer cancel() 85 | for j := 0; j < 5; j++ { 86 | _, err := vaultClient.Sys().UnsealWithContext(ctx, shard) 87 | if err != nil { 88 | if errors.Is(err, context.DeadlineExceeded) { 89 | continue 90 | } 91 | return fmt.Errorf("error passing unseal shard %d to %q: %w", i+1, node, err) 92 | } 93 | } 94 | for j := 0; j < 10; j++ { 95 | sealStatus, err := vaultClient.Sys().SealStatus() 96 | if err != nil { 97 | return fmt.Errorf("error retrieving health of %q: %w", node, err) 98 | } 99 | if sealStatus.Progress > sealStatusTracking || !sealStatus.Sealed { 100 | log.Info().Msg("shard accepted") 101 | sealStatusTracking++ 102 | break 103 | } 104 | log.Info().Msgf("waiting for node %q to accept unseal shard", node) 105 | time.Sleep(6 * time.Second) 106 | } 107 | } 108 | } 109 | 110 | log.Printf("vault unsealed") 111 | } else { 112 | return fmt.Errorf("failed to unseal vault: vault is already unsealed") 113 | } 114 | 115 | progress.Progress.Quit() 116 | 117 | return nil 118 | } 119 | 120 | func parseExistingVaultInitSecret(clientset kubernetes.Interface) (*api.InitResponse, error) { 121 | secret, err := k8s.ReadSecretV2(clientset, vaultNamespace, vaultSecretName) 122 | if err != nil { 123 | return nil, fmt.Errorf("failed to read secret: %w", err) 124 | } 125 | 126 | var rkSlice []string 127 | for key, value := range secret { 128 | if strings.Contains(key, "root-unseal-key-") { 129 | rkSlice = append(rkSlice, value) 130 | } 131 | } 132 | 133 | existingInitResponse := &api.InitResponse{ 134 | Keys: rkSlice, 135 | RootToken: secret["root-token"], 136 | } 137 | return existingInitResponse, nil 138 | } 139 | -------------------------------------------------------------------------------- /cmd/k3s/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3s 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/rs/zerolog/log" 13 | 14 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 15 | _ "k8s.io/client-go/plugin/pkg/client/auth" // required for k8s authentication 16 | ) 17 | 18 | func ValidateProvidedFlags(gitProvider string) error { 19 | switch gitProvider { 20 | case "github": 21 | key, err := internalssh.GetHostKey("github.com") 22 | if err != nil { 23 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy") 24 | } 25 | log.Info().Msgf("%q %s", "github.com", key.Type()) 26 | case "gitlab": 27 | key, err := internalssh.GetHostKey("gitlab.com") 28 | if err != nil { 29 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy") 30 | } 31 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /cmd/launch.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "text/tabwriter" 13 | 14 | "github.com/konstructio/kubefirst/internal/cluster" 15 | "github.com/konstructio/kubefirst/internal/launch" 16 | "github.com/konstructio/kubefirst/internal/step" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | // additionalHelmFlags can optionally pass user-supplied flags to helm 21 | var additionalHelmFlags []string 22 | 23 | func LaunchCommand() *cobra.Command { 24 | launchCommand := &cobra.Command{ 25 | Use: "launch", 26 | Short: "create a local k3d cluster and launch the Kubefirst console and API in it", 27 | Long: "create a local k3d cluster and launch the Kubefirst console and API in it", 28 | } 29 | 30 | // wire up new commands 31 | launchCommand.AddCommand(launchUp(), launchDown(), launchCluster()) 32 | 33 | return launchCommand 34 | } 35 | 36 | // launchUp creates a new k3d cluster with Kubefirst console and API 37 | func launchUp() *cobra.Command { 38 | launchUpCmd := &cobra.Command{ 39 | Use: "up", 40 | Short: "launch new console and api instance", 41 | TraverseChildren: true, 42 | RunE: func(cmd *cobra.Command, _ []string) error { 43 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 44 | 45 | stepper.DisplayLogHints("", 5) 46 | 47 | stepper.NewProgressStep("Launching Console and API") 48 | 49 | if err := launch.Up(cmd.Context(), additionalHelmFlags, false, true); err != nil { 50 | stepper.FailCurrentStep(err) 51 | return fmt.Errorf("failed to launch console and api: %w", err) 52 | } 53 | 54 | stepper.CompleteCurrentStep() 55 | 56 | stepper.InfoStep(step.EmojiTada, "Your kubefirst platform provisioner has been created.") 57 | 58 | return nil 59 | }, 60 | } 61 | 62 | launchUpCmd.Flags().StringSliceVar(&additionalHelmFlags, "helm-flag", []string{}, "additional helm flag to pass to the launch up command - can be used any number of times") 63 | 64 | return launchUpCmd 65 | } 66 | 67 | // launchDown destroys a k3d cluster for Kubefirst console and API 68 | func launchDown() *cobra.Command { 69 | launchDownCmd := &cobra.Command{ 70 | Use: "down", 71 | Short: "remove console and api instance", 72 | TraverseChildren: true, 73 | RunE: func(cmd *cobra.Command, _ []string) error { 74 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 75 | 76 | stepper.NewProgressStep("Destroying Console and API") 77 | 78 | if err := launch.Down(false); err != nil { 79 | wrerr := fmt.Errorf("failed to remove console and api: %w", err) 80 | stepper.FailCurrentStep(wrerr) 81 | return wrerr 82 | } 83 | 84 | stepper.CompleteCurrentStep() 85 | 86 | stepper.InfoStep(step.EmojiTada, "Your kubefirst platform provisioner has been destroyed.") 87 | 88 | return nil 89 | }, 90 | } 91 | 92 | return launchDownCmd 93 | } 94 | 95 | // launchCluster 96 | func launchCluster() *cobra.Command { 97 | launchClusterCmd := &cobra.Command{ 98 | Use: "cluster", 99 | Short: "interact with clusters created by the Kubefirst console", 100 | TraverseChildren: true, 101 | } 102 | 103 | launchClusterCmd.AddCommand(launchListClusters(), launchDeleteCluster()) 104 | 105 | return launchClusterCmd 106 | } 107 | 108 | // launchListClusters makes a request to the console API to list created clusters 109 | func launchListClusters() *cobra.Command { 110 | launchListClustersCmd := &cobra.Command{ 111 | Use: "list", 112 | Short: "list clusters created by the Kubefirst console", 113 | TraverseChildren: true, 114 | RunE: func(cmd *cobra.Command, _ []string) error { 115 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 116 | 117 | clusters, err := cluster.GetClusters() 118 | if err != nil { 119 | return fmt.Errorf("error getting clusters: %w", err) 120 | } 121 | 122 | var buf bytes.Buffer 123 | tw := tabwriter.NewWriter(&buf, 0, 0, 1, ' ', tabwriter.Debug) 124 | 125 | fmt.Fprint(tw, "NAME\tCREATED AT\tSTATUS\tTYPE\tPROVIDER\n") 126 | for _, cluster := range clusters { 127 | fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", 128 | cluster.ClusterName, 129 | cluster.CreationTimestamp, 130 | cluster.Status, 131 | cluster.ClusterType, 132 | cluster.CloudProvider) 133 | } 134 | 135 | stepper.InfoStepString(buf.String()) 136 | 137 | return nil 138 | }, 139 | } 140 | 141 | return launchListClustersCmd 142 | } 143 | 144 | // launchDeleteCluster makes a request to the console API to delete a single cluster 145 | func launchDeleteCluster() *cobra.Command { 146 | launchDeleteClusterCmd := &cobra.Command{ 147 | Use: "delete", 148 | Short: "delete a cluster created by the Kubefirst console", 149 | TraverseChildren: true, 150 | Args: func(cmd *cobra.Command, args []string) error { 151 | if err := cobra.ExactArgs(1)(cmd, args); err != nil { 152 | return fmt.Errorf("you must provide a cluster name as the only argument to this command") 153 | } 154 | return nil 155 | }, 156 | RunE: func(cmd *cobra.Command, args []string) error { 157 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 158 | 159 | stepper.NewProgressStep("Deleting Cluster") 160 | 161 | if len(args) != 1 { 162 | wrerr := fmt.Errorf("expected 1 argument (cluster name)") 163 | stepper.FailCurrentStep(wrerr) 164 | return wrerr 165 | } 166 | 167 | managedClusterName := args[0] 168 | 169 | err := cluster.DeleteCluster(managedClusterName) 170 | if err != nil { 171 | wrerr := fmt.Errorf("failed to delete cluster: %w", err) 172 | stepper.FailCurrentStep(wrerr) 173 | return wrerr 174 | } 175 | 176 | deleteMessage := ` 177 | Submitted request to delete cluster` + fmt.Sprintf("`%s`", managedClusterName) + ` 178 | Follow progress with ` + fmt.Sprintf("`%s`", "kubefirst launch cluster list") + ` 179 | ` 180 | stepper.InfoStepString(deleteMessage) 181 | 182 | return nil 183 | }, 184 | } 185 | 186 | return launchDeleteClusterCmd 187 | } 188 | -------------------------------------------------------------------------------- /cmd/letsencrypt.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/certificates" 13 | "github.com/konstructio/kubefirst/internal/step" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // Certificate check 18 | var domainNameFlag string 19 | 20 | func LetsEncryptCommand() *cobra.Command { 21 | letsEncryptCommand := &cobra.Command{ 22 | Use: "letsencrypt", 23 | Short: "interact with LetsEncrypt certificates for a domain", 24 | Long: "interact with LetsEncrypt certificates for a domain", 25 | } 26 | 27 | // wire up new commands 28 | letsEncryptCommand.AddCommand(status()) 29 | 30 | return letsEncryptCommand 31 | } 32 | 33 | func status() *cobra.Command { 34 | statusCmd := &cobra.Command{ 35 | Use: "status", 36 | Short: "check the usage statistics for a LetsEncrypt certificate", 37 | TraverseChildren: true, 38 | RunE: func(cmd *cobra.Command, _ []string) error { 39 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 40 | if err := certificates.CheckCertificateUsage(domainNameFlag); err != nil { 41 | wrerr := fmt.Errorf("failed to check certificate usage for domain %q: %w", domainNameFlag, err) 42 | stepper.InfoStep(step.EmojiError, wrerr.Error()) 43 | return wrerr 44 | } 45 | 46 | return nil 47 | }, 48 | } 49 | 50 | // todo review defaults and update descriptions 51 | statusCmd.Flags().StringVar(&domainNameFlag, "domain-name", "", "the domain to check certificates for (i.e. your-domain.com|subdomain.your-domain.com) (required)") 52 | statusCmd.MarkFlagRequired("domain-name") 53 | 54 | return statusCmd 55 | } 56 | -------------------------------------------------------------------------------- /cmd/logs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst/internal/progress" 13 | "github.com/konstructio/kubefirst/internal/provisionLogs" 14 | "github.com/nxadm/tail" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | func LogsCommand() *cobra.Command { 20 | logsCmd := &cobra.Command{ 21 | Use: "logs", 22 | Short: "kubefirst real time logs", 23 | Long: `kubefirst real time logs`, 24 | RunE: func(_ *cobra.Command, _ []string) error { 25 | provisionLogs.InitializeProvisionLogsTerminal() 26 | 27 | go func() { 28 | t, err := tail.TailFile(viper.GetString("k1-paths.log-file"), tail.Config{Follow: true, ReOpen: true}) 29 | if err != nil { 30 | fmt.Printf("Error tailing log file: %v\n", err) 31 | progress.Progress.Quit() 32 | return 33 | } 34 | 35 | for line := range t.Lines { 36 | provisionLogs.AddLog(line.Text) 37 | } 38 | }() 39 | 40 | if _, err := provisionLogs.ProvisionLogs.Run(); err != nil { 41 | return fmt.Errorf("failed to run provision logs: %w", err) 42 | } 43 | 44 | return nil 45 | }, 46 | } 47 | 48 | return logsCmd 49 | } 50 | -------------------------------------------------------------------------------- /cmd/reset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "time" 13 | 14 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 15 | "github.com/konstructio/kubefirst/internal/step" 16 | "github.com/rs/zerolog/log" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | func ResetCommand() *cobra.Command { 22 | resetCmd := &cobra.Command{ 23 | Use: "reset", 24 | Short: "removes local kubefirst content to provision a new platform", 25 | Long: "removes local kubefirst content to provision a new platform", 26 | RunE: func(cmd *cobra.Command, _ []string) error { 27 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 28 | 29 | homePath, err := os.UserHomeDir() 30 | if err != nil { 31 | wrerr := fmt.Errorf("unable to get user home directory: %w", err) 32 | stepper.InfoStep(step.EmojiError, wrerr.Error()) 33 | return wrerr 34 | } 35 | 36 | if err := runReset(homePath); err != nil { 37 | wrerr := fmt.Errorf("failed to reset kubefirst platform: %w", err) 38 | stepper.InfoStep(step.EmojiError, wrerr.Error()) 39 | return wrerr 40 | } 41 | 42 | stepper.InfoStep(step.EmojiTada, "Successfully reset kubefirst platform") 43 | 44 | return nil 45 | }, 46 | } 47 | 48 | return resetCmd 49 | } 50 | 51 | // runReset carries out the reset function 52 | func runReset(homePath string) error { 53 | log.Info().Msg("removing previous platform content") 54 | 55 | k1Dir := fmt.Sprintf("%s/.k1", homePath) 56 | kubefirstConfig := fmt.Sprintf("%s/.kubefirst", homePath) 57 | 58 | if err := utils.ResetK1Dir(k1Dir); err != nil { 59 | return fmt.Errorf("error resetting k1 directory: %w", err) 60 | } 61 | log.Info().Msg("previous platform content removed") 62 | 63 | log.Info().Msg("resetting $HOME/.kubefirst config") 64 | viper.Set("argocd", "") 65 | viper.Set("github", "") 66 | viper.Set("gitlab", "") 67 | viper.Set("components", "") 68 | viper.Set("kbot", "") 69 | viper.Set("kubefirst-checks", "") 70 | viper.Set("kubefirst", "") 71 | viper.Set("secrets", "") 72 | if err := viper.WriteConfig(); err != nil { 73 | return fmt.Errorf("error writing viper config: %w", err) 74 | } 75 | 76 | if err := os.RemoveAll(k1Dir); err != nil { 77 | return fmt.Errorf("unable to delete %q folder, error: %w", k1Dir, err) 78 | } 79 | 80 | if err := os.RemoveAll(kubefirstConfig); err != nil { 81 | return fmt.Errorf("unable to remove %q, error: %w", kubefirstConfig, err) 82 | } 83 | 84 | time.Sleep(time.Second * 2) 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | "github.com/konstructio/kubefirst-api/pkg/configs" 14 | "github.com/konstructio/kubefirst/cmd/akamai" 15 | "github.com/konstructio/kubefirst/cmd/aws" 16 | "github.com/konstructio/kubefirst/cmd/azure" 17 | "github.com/konstructio/kubefirst/cmd/civo" 18 | "github.com/konstructio/kubefirst/cmd/digitalocean" 19 | "github.com/konstructio/kubefirst/cmd/google" 20 | "github.com/konstructio/kubefirst/cmd/k3d" 21 | "github.com/konstructio/kubefirst/cmd/k3s" 22 | "github.com/konstructio/kubefirst/cmd/vultr" 23 | "github.com/konstructio/kubefirst/internal/common" 24 | "github.com/konstructio/kubefirst/internal/step" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | // Execute adds all child commands to the root command and sets flags appropriately. 29 | // This is called by main.main(). It only needs to happen once to the rootCmd. 30 | func Execute() { 31 | rootCmd := &cobra.Command{ 32 | Use: "kubefirst", 33 | Short: "kubefirst management cluster installer base command", 34 | Long: `kubefirst management cluster installer provisions an 35 | open source application delivery platform in under an hour. 36 | checkout the docs at https://kubefirst.konstruct.io/docs/.`, 37 | PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { 38 | // wire viper config for flags for all commands 39 | return configs.InitializeViperConfig(cmd) 40 | }, 41 | Run: func(_ *cobra.Command, _ []string) { 42 | fmt.Println("To learn more about kubefirst, run:") 43 | fmt.Println(" kubefirst help") 44 | }, 45 | SilenceErrors: true, 46 | SilenceUsage: true, 47 | } 48 | 49 | output := rootCmd.ErrOrStderr() 50 | 51 | rootCmd.AddCommand( 52 | aws.NewCommand(), 53 | azure.NewCommand(), 54 | civo.NewCommand(), 55 | digitalocean.NewCommand(), 56 | k3d.NewCommand(), 57 | k3d.LocalCommandAlias(), 58 | k3s.NewCommand(), 59 | google.NewCommand(), 60 | vultr.NewCommand(), 61 | akamai.NewCommand(), 62 | GenerateCommand(), 63 | LaunchCommand(), 64 | LetsEncryptCommand(), 65 | TerraformCommand(), 66 | ResetCommand(), 67 | VersionCommand(), 68 | LogsCommand(), 69 | InfoCommand(), 70 | ) 71 | 72 | // This will allow all child commands to have informUser available for free. 73 | // Refers: https://github.com/konstructio/runtime/issues/525 74 | // Before removing next line, please read ticket above. 75 | common.CheckForVersionUpdate() 76 | if err := rootCmd.Execute(); err != nil { 77 | fmt.Println() 78 | fmt.Fprintln(output, step.EmojiError, "Error:", err) 79 | fmt.Fprintln(output, "If a detailed error message was available, please make the necessary corrections before retrying.") 80 | fmt.Fprintln(output, "You can re-run the last command to try the operation again.") 81 | os.Exit(0) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/terraform.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/vault" 13 | "github.com/konstructio/kubefirst/internal/step" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var ( 18 | vaultURLFlag string 19 | vaultTokenFlag string 20 | outputFileFlag string 21 | ) 22 | 23 | func TerraformCommand() *cobra.Command { 24 | terraformCommand := &cobra.Command{ 25 | Use: "terraform", 26 | Short: "interact with terraform", 27 | Long: "interact with terraform", 28 | } 29 | 30 | // wire up new commands 31 | terraformCommand.AddCommand(terraformSetEnv()) 32 | 33 | return terraformCommand 34 | } 35 | 36 | // terraformSetEnv retrieves Vault secrets and formats them for export in the local 37 | // shell for use with terraform commands 38 | func terraformSetEnv() *cobra.Command { 39 | terraformSetCmd := &cobra.Command{ 40 | Use: "set-env", 41 | Short: "retrieve data from a target vault secret and format it for use in the local shell via environment variables", 42 | TraverseChildren: true, 43 | RunE: func(cmd *cobra.Command, _ []string) error { 44 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 45 | 46 | v := vault.Configuration{ 47 | Config: vault.NewVault(), 48 | } 49 | 50 | err := v.IterSecrets(vaultURLFlag, vaultTokenFlag, outputFileFlag) 51 | if err != nil { 52 | wrerr := fmt.Errorf("error during vault read: %w", err) 53 | stepper.InfoStep(step.EmojiError, wrerr.Error()) 54 | return wrerr 55 | } 56 | 57 | message := ` 58 | Generated env file at` + fmt.Sprintf("`%s`", outputFileFlag) + ` 59 | 60 | :bulb: Run` + fmt.Sprintf("`source %s`", outputFileFlag) + ` to set environment variables 61 | 62 | ` 63 | stepper.InfoStepString(message) 64 | 65 | return nil 66 | }, 67 | } 68 | 69 | terraformSetCmd.Flags().StringVar(&vaultURLFlag, "vault-url", "", "the URL of the vault instance (required)") 70 | terraformSetCmd.Flags().StringVar(&vaultTokenFlag, "vault-token", "", "the vault token (required)") 71 | terraformSetCmd.Flags().StringVar(&outputFileFlag, "output-file", ".env", "the file that will be created in the local directory containing secrets (.env by default)") 72 | 73 | return terraformSetCmd 74 | } 75 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package cmd 8 | 9 | import ( 10 | "github.com/konstructio/kubefirst-api/pkg/configs" 11 | "github.com/konstructio/kubefirst/internal/step" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | func VersionCommand() *cobra.Command { 16 | versionCmd := &cobra.Command{ 17 | Use: "version", 18 | Short: "print the version number for kubefirst-cli", 19 | Long: `All software has versions. This is kubefirst's`, 20 | Run: func(cmd *cobra.Command, _ []string) { 21 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 22 | versionMsg := "\n kubefirst-cli golang utility version: " + configs.K1Version 23 | 24 | stepper.InfoStepString(versionMsg) 25 | }, 26 | } 27 | return versionCmd 28 | } 29 | -------------------------------------------------------------------------------- /cmd/vultr/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package vultr 8 | 9 | import ( 10 | "fmt" 11 | 12 | "github.com/konstructio/kubefirst-api/pkg/constants" 13 | "github.com/konstructio/kubefirst/internal/catalog" 14 | "github.com/konstructio/kubefirst/internal/cluster" 15 | "github.com/konstructio/kubefirst/internal/common" 16 | "github.com/konstructio/kubefirst/internal/provision" 17 | "github.com/konstructio/kubefirst/internal/step" 18 | "github.com/konstructio/kubefirst/internal/utilities" 19 | "github.com/spf13/cobra" 20 | ) 21 | 22 | var ( 23 | // Supported providers 24 | supportedDNSProviders = []string{"vultr", "cloudflare"} 25 | supportedGitProviders = []string{"github", "gitlab"} 26 | // Supported git protocols 27 | supportedGitProtocolOverride = []string{"https", "ssh"} 28 | ) 29 | 30 | func NewCommand() *cobra.Command { 31 | vultrCmd := &cobra.Command{ 32 | Use: "vultr", 33 | Short: "Kubefirst Vultr installation", 34 | Long: "kubefirst vultr", 35 | Run: func(_ *cobra.Command, _ []string) { 36 | fmt.Println("To learn more about Vultr in Kubefirst, run:") 37 | fmt.Println(" kubefirst vultr --help") 38 | }, 39 | } 40 | 41 | // on error, doesnt show helper/usage 42 | vultrCmd.SilenceUsage = true 43 | 44 | // wire up new commands 45 | vultrCmd.AddCommand(Create(), Destroy(), RootCredentials()) 46 | 47 | return vultrCmd 48 | } 49 | 50 | func Create() *cobra.Command { 51 | createCmd := &cobra.Command{ 52 | Use: "create", 53 | Short: "Create the Kubefirst platform running on Vultr Kubernetes", 54 | TraverseChildren: true, 55 | RunE: func(cmd *cobra.Command, _ []string) error { 56 | cloudProvider := "vultr" 57 | estimatedTimeMinutes := 15 58 | ctx := cmd.Context() 59 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 60 | 61 | stepper.DisplayLogHints(cloudProvider, estimatedTimeMinutes) 62 | 63 | stepper.NewProgressStep("Validate Configuration") 64 | 65 | cliFlags, err := utilities.GetFlags(cmd, cloudProvider) 66 | if err != nil { 67 | wrerr := fmt.Errorf("failed to get flags: %w", err) 68 | stepper.FailCurrentStep(wrerr) 69 | return wrerr 70 | } 71 | 72 | _, catalogApps, err := catalog.ValidateCatalogApps(ctx, cliFlags.InstallCatalogApps) 73 | if err != nil { 74 | wrerr := fmt.Errorf("catalog validation failed: %w", err) 75 | stepper.FailCurrentStep(wrerr) 76 | return wrerr 77 | } 78 | 79 | err = ValidateProvidedFlags(cliFlags.GitProvider, cliFlags.DNSProvider) 80 | if err != nil { 81 | wrerr := fmt.Errorf("failed to validate provided flags: %w", err) 82 | stepper.FailCurrentStep(wrerr) 83 | return wrerr 84 | } 85 | 86 | stepper.CompleteCurrentStep() 87 | clusterClient := cluster.Client{} 88 | 89 | provision := provision.NewProvisioner(provision.NewProvisionWatcher(cliFlags.ClusterName, &clusterClient), stepper) 90 | 91 | if err := provision.ProvisionManagementCluster(ctx, cliFlags, catalogApps); err != nil { 92 | return fmt.Errorf("failed to create vultr management cluster: %w", err) 93 | } 94 | 95 | return nil 96 | }, 97 | // PreRun: common.CheckDocker, 98 | } 99 | 100 | vultrDefaults := constants.GetCloudDefaults().Vultr 101 | 102 | // todo review defaults and update descriptions 103 | createCmd.Flags().String("alerts-email", "", "Email address for Let's Encrypt certificate notifications (required)") 104 | createCmd.MarkFlagRequired("alerts-email") 105 | createCmd.Flags().Bool("ci", false, "If running Kubefirst in CI, set this flag to disable interactive features") 106 | createCmd.Flags().String("cloud-region", "ewr", "The Vultr region to provision infrastructure in") 107 | createCmd.Flags().String("cluster-name", "kubefirst", "The name of the cluster to create") 108 | createCmd.Flags().String("cluster-type", "mgmt", "The type of cluster to create (i.e. mgmt|workload)") 109 | createCmd.Flags().String("node-count", vultrDefaults.NodeCount, "The node count for the cluster") 110 | createCmd.Flags().String("node-type", vultrDefaults.InstanceSize, "The instance size of the cluster to create") 111 | createCmd.Flags().String("dns-provider", "vultr", fmt.Sprintf("The DNS provider - one of: %s", supportedDNSProviders)) 112 | createCmd.Flags().String("subdomain", "", "The subdomain to use for DNS records (Cloudflare)") 113 | createCmd.Flags().String("domain-name", "", "The Vultr DNS name to use for DNS records (i.e. your-domain.com|subdomain.your-domain.com) (required)") 114 | createCmd.MarkFlagRequired("domain-name") 115 | createCmd.Flags().String("git-provider", "github", fmt.Sprintf("The Git provider - one of: %s", supportedGitProviders)) 116 | createCmd.Flags().String("git-protocol", "ssh", fmt.Sprintf("The Git protocol - one of: %s", supportedGitProtocolOverride)) 117 | createCmd.Flags().String("github-org", "", "The GitHub organization for the new GitOps and metaphor repositories - required if using GitHub") 118 | createCmd.Flags().String("gitlab-group", "", "The GitLab group for the new GitOps and metaphor projects - required if using GitLab") 119 | createCmd.Flags().String("gitops-template-branch", "", "The branch to clone for the GitOps template repository") 120 | createCmd.Flags().String("gitops-template-url", "https://github.com/konstructio/gitops-template.git", "The fully qualified URL to the GitOps template repository to clone") 121 | createCmd.Flags().String("install-catalog-apps", "", "Comma separated values to install after provision") 122 | createCmd.Flags().Bool("use-telemetry", true, "Whether to emit telemetry") 123 | createCmd.Flags().Bool("install-kubefirst-pro", true, "Whether or not to install Kubefirst Pro") 124 | 125 | return createCmd 126 | } 127 | 128 | func Destroy() *cobra.Command { 129 | destroyCmd := &cobra.Command{ 130 | Use: "destroy", 131 | Short: "Destroy the Kubefirst platform", 132 | Long: "Destroy the Kubefirst platform running in Vultr and remove all resources", 133 | RunE: common.Destroy, 134 | // PreRun: common.CheckDocker, 135 | } 136 | 137 | return destroyCmd 138 | } 139 | 140 | func RootCredentials() *cobra.Command { 141 | authCmd := &cobra.Command{ 142 | Use: "root-credentials", 143 | Short: "Retrieve root authentication information for platform components", 144 | Long: "Retrieve root authentication information for platform components", 145 | RunE: common.GetRootCredentials, 146 | } 147 | 148 | authCmd.Flags().Bool("argocd", false, "Copy the ArgoCD password to the clipboard (optional)") 149 | authCmd.Flags().Bool("kbot", false, "Copy the kbot password to the clipboard (optional)") 150 | authCmd.Flags().Bool("vault", false, "Copy the vault password to the clipboard (optional)") 151 | 152 | return authCmd 153 | } 154 | -------------------------------------------------------------------------------- /cmd/vultr/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package vultr 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | 13 | internalssh "github.com/konstructio/kubefirst-api/pkg/ssh" 14 | "github.com/rs/zerolog/log" 15 | ) 16 | 17 | func ValidateProvidedFlags(gitProvider, dnsProvider string) error { 18 | if os.Getenv("VULTR_API_KEY") == "" { 19 | return fmt.Errorf("your VULTR_API_KEY variable is unset - please set it before continuing") 20 | } 21 | 22 | if dnsProvider == "cloudflare" { 23 | if os.Getenv("CF_API_TOKEN") == "" { 24 | return fmt.Errorf("your CF_API_TOKEN environment variable is not set. Please set and try again") 25 | } 26 | } 27 | 28 | switch gitProvider { 29 | case "github": 30 | key, err := internalssh.GetHostKey("github.com") 31 | if err != nil { 32 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan github.com >> ~/.ssh/known_hosts` to remedy: %w", err) 33 | } 34 | log.Info().Msgf("%q %s", "github.com", key.Type()) 35 | case "gitlab": 36 | key, err := internalssh.GetHostKey("gitlab.com") 37 | if err != nil { 38 | return fmt.Errorf("known_hosts file does not exist - please run `ssh-keyscan gitlab.com >> ~/.ssh/known_hosts` to remedy: %w", err) 39 | } 40 | log.Info().Msgf("%q %s", "gitlab.com", key.Type()) 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # The name of the server to show in the TUI. 2 | name: Soft Serve 3 | 4 | # The host and port to display in the TUI. You may want to change this if your 5 | # server is accessible from a different host and/or port that what it's 6 | # actually listening on (for example, if it's behind a reverse proxy). 7 | host: localhost 8 | port: 23231 9 | 10 | # Access level for anonymous users. Options are: admin-access, read-write, 11 | # read-only, and no-access. 12 | anon-access: read-write 13 | 14 | # You can grant read-only access to users without private keys. Any password 15 | # will be accepted. 16 | allow-keyless: true 17 | 18 | # Customize repo display in the menu. 19 | repos: 20 | - name: Home 21 | repo: config 22 | private: true 23 | note: "Configuration and content repo for this server" 24 | readme: README.md -------------------------------------------------------------------------------- /images/kubefirst-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructio/kubefirst/6c46321d1a282323a4cbba8e1462964046ff1abd/images/kubefirst-arch.png -------------------------------------------------------------------------------- /images/provisioning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstructio/kubefirst/6c46321d1a282323a4cbba8e1462964046ff1abd/images/provisioning.png -------------------------------------------------------------------------------- /internal/catalog/catalog.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package catalog 8 | 9 | import ( 10 | "context" 11 | "fmt" 12 | "io" 13 | "os" 14 | "strings" 15 | 16 | git "github.com/google/go-github/v52/github" 17 | 18 | apiTypes "github.com/konstructio/kubefirst-api/pkg/types" 19 | 20 | "github.com/rs/zerolog/log" 21 | "gopkg.in/yaml.v3" 22 | ) 23 | 24 | const ( 25 | KubefirstGitHubOrganization = "kubefirst" 26 | KubefirstGitopsCatalogRepository = "gitops-catalog" 27 | basePath = "/" 28 | ) 29 | 30 | type GitHubClient struct { 31 | Client *git.Client 32 | } 33 | 34 | // NewGitHub instantiates an unauthenticated GitHub client 35 | func NewGitHub() *git.Client { 36 | return git.NewClient(nil) 37 | } 38 | 39 | func ReadActiveApplications(ctx context.Context) (apiTypes.GitopsCatalogApps, error) { 40 | gh := GitHubClient{ 41 | Client: NewGitHub(), 42 | } 43 | 44 | activeContent, err := gh.ReadGitopsCatalogRepoContents(ctx) 45 | if err != nil { 46 | return apiTypes.GitopsCatalogApps{}, fmt.Errorf("error retrieving gitops catalog repository content: %w", err) 47 | } 48 | 49 | index, err := gh.ReadGitopsCatalogIndex(ctx, activeContent) 50 | if err != nil { 51 | return apiTypes.GitopsCatalogApps{}, fmt.Errorf("error retrieving gitops catalog index content: %w", err) 52 | } 53 | 54 | var out apiTypes.GitopsCatalogApps 55 | 56 | err = yaml.Unmarshal(index, &out) 57 | if err != nil { 58 | return apiTypes.GitopsCatalogApps{}, fmt.Errorf("error retrieving gitops catalog applications: %w", err) 59 | } 60 | 61 | return out, nil 62 | } 63 | 64 | func ValidateCatalogApps(ctx context.Context, catalogApps string) (bool, []apiTypes.GitopsCatalogApp, error) { 65 | items := strings.Split(catalogApps, ",") 66 | 67 | gitopsCatalogapps := []apiTypes.GitopsCatalogApp{} 68 | if catalogApps == "" { 69 | return true, gitopsCatalogapps, nil 70 | } 71 | 72 | apps, err := ReadActiveApplications(ctx) 73 | if err != nil { 74 | log.Error().Msgf("error getting gitops catalog applications: %s", err) 75 | return false, gitopsCatalogapps, err 76 | } 77 | 78 | for _, app := range items { 79 | found := false 80 | for _, catalogApp := range apps.Apps { 81 | if app == catalogApp.Name { 82 | found = true 83 | 84 | if catalogApp.SecretKeys != nil { 85 | for _, secret := range catalogApp.SecretKeys { 86 | secretValue := os.Getenv(secret.Env) 87 | 88 | if secretValue == "" { 89 | return false, gitopsCatalogapps, fmt.Errorf("your %q environment variable is not set for %q catalog application. Please set and try again", secret.Env, app) 90 | } 91 | 92 | secret.Value = secretValue 93 | } 94 | } 95 | 96 | if catalogApp.ConfigKeys != nil { 97 | for _, config := range catalogApp.ConfigKeys { 98 | configValue := os.Getenv(config.Env) 99 | if configValue == "" { 100 | return false, gitopsCatalogapps, fmt.Errorf("your %q environment variable is not set for %q catalog application. Please set and try again", config.Env, app) 101 | } 102 | config.Value = configValue 103 | } 104 | } 105 | 106 | gitopsCatalogapps = append(gitopsCatalogapps, catalogApp) 107 | 108 | break 109 | } 110 | } 111 | if !found { 112 | return false, gitopsCatalogapps, fmt.Errorf("catalog app is not supported: %q", app) 113 | } 114 | } 115 | 116 | return true, gitopsCatalogapps, nil 117 | } 118 | 119 | func (gh *GitHubClient) ReadGitopsCatalogRepoContents(ctx context.Context) ([]*git.RepositoryContent, error) { 120 | _, directoryContent, _, err := gh.Client.Repositories.GetContents( 121 | ctx, 122 | KubefirstGitHubOrganization, 123 | KubefirstGitopsCatalogRepository, 124 | basePath, 125 | nil, 126 | ) 127 | if err != nil { 128 | return nil, fmt.Errorf("error retrieving gitops catalog repository contents: %w", err) 129 | } 130 | 131 | return directoryContent, nil 132 | } 133 | 134 | // ReadGitopsCatalogIndex reads the gitops catalog repository index 135 | func (gh *GitHubClient) ReadGitopsCatalogIndex(ctx context.Context, contents []*git.RepositoryContent) ([]byte, error) { 136 | for _, content := range contents { 137 | if *content.Type == "file" && *content.Name == "index.yaml" { 138 | b, err := gh.readFileContents(ctx, content) 139 | if err != nil { 140 | return nil, fmt.Errorf("error reading index.yaml file: %w", err) 141 | } 142 | return b, nil 143 | } 144 | } 145 | 146 | return nil, fmt.Errorf("index.yaml not found in gitops catalog repository") 147 | } 148 | 149 | // readFileContents parses the contents of a file in a GitHub repository 150 | func (gh *GitHubClient) readFileContents(ctx context.Context, content *git.RepositoryContent) ([]byte, error) { 151 | rc, _, err := gh.Client.Repositories.DownloadContents( 152 | ctx, 153 | KubefirstGitHubOrganization, 154 | KubefirstGitopsCatalogRepository, 155 | *content.Path, 156 | nil, 157 | ) 158 | if err != nil { 159 | return nil, fmt.Errorf("error downloading contents of %q: %w", *content.Path, err) 160 | } 161 | defer rc.Close() 162 | 163 | b, err := io.ReadAll(rc) 164 | if err != nil { 165 | return nil, fmt.Errorf("error reading contents of %q: %w", *content.Path, err) 166 | } 167 | 168 | return b, nil 169 | } 170 | -------------------------------------------------------------------------------- /internal/common/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package common 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "os" 14 | "regexp" 15 | "runtime" 16 | "strings" 17 | 18 | "github.com/konstructio/kubefirst-api/pkg/configs" 19 | "github.com/konstructio/kubefirst-api/pkg/providerConfigs" 20 | "github.com/konstructio/kubefirst/internal/cluster" 21 | "github.com/konstructio/kubefirst/internal/launch" 22 | "github.com/konstructio/kubefirst/internal/progress" 23 | "github.com/konstructio/kubefirst/internal/step" 24 | "github.com/rs/zerolog/log" 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/viper" 27 | ) 28 | 29 | type CheckResponse struct { 30 | // Current is current latest version on source. 31 | Current string 32 | 33 | // Outdate is true when target version is less than Current on source. 34 | Outdated bool 35 | 36 | // Latest is true when target version is equal to Current on source. 37 | Latest bool 38 | 39 | // New is true when target version is greater than Current on source. 40 | New bool 41 | } 42 | 43 | // CheckForVersionUpdate determines whether or not there is a new cli version available 44 | func CheckForVersionUpdate() { 45 | if configs.K1Version != configs.DefaultK1Version { 46 | res, skip := versionCheck() 47 | if !skip { 48 | if res.Outdated { 49 | switch runtime.GOOS { 50 | case "darwin": 51 | fmt.Printf("A newer version (v%s) is available! Please upgrade with: \"brew update && brew upgrade kubefirst\"\n", res.Current) 52 | default: 53 | fmt.Printf("A newer version (v%s) is available! \"https://github.com/konstructio/kubefirst/blob/main/build/README.md\"\n", res.Current) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | // versionCheck compares local to remote version 61 | func versionCheck() (*CheckResponse, bool) { 62 | var latestVersion string 63 | flatVersion := strings.ReplaceAll(configs.K1Version, "v", "") 64 | 65 | resp, err := http.Get("https://raw.githubusercontent.com/Homebrew/homebrew-core/master/Formula/k/kubefirst.rb") 66 | if err != nil { 67 | fmt.Printf("checking for a newer version failed (cannot get Homebrew formula) with: %s", err) 68 | return nil, true 69 | } 70 | defer resp.Body.Close() 71 | 72 | if resp.StatusCode != http.StatusOK { 73 | fmt.Printf("checking for a newer version failed (HTTP error) with: %s", err) 74 | return nil, true 75 | } 76 | 77 | bodyBytes, err := io.ReadAll(resp.Body) 78 | if err != nil { 79 | fmt.Printf("checking for a newer version failed (cannot read the file) with: %s", err) 80 | return nil, true 81 | } 82 | 83 | bodyString := string(bodyBytes) 84 | if !strings.Contains(bodyString, "url \"https://github.com/konstructio/kubefirst/archive/refs/tags/") { 85 | fmt.Printf("checking for a newer version failed (no reference to kubefirst release) with: %s", err) 86 | return nil, true 87 | } 88 | 89 | re := regexp.MustCompile(`.*/v(.*).tar.gz"`) 90 | matches := re.FindStringSubmatch(bodyString) 91 | if len(matches) < 2 { 92 | fmt.Println("checking for a newer version failed (no version match)") 93 | return nil, true 94 | } 95 | latestVersion = matches[1] 96 | 97 | return &CheckResponse{ 98 | Current: flatVersion, 99 | Outdated: latestVersion < flatVersion, 100 | Latest: latestVersion == flatVersion, 101 | New: flatVersion > latestVersion, 102 | }, false 103 | } 104 | 105 | func GetRootCredentials(cmd *cobra.Command, _ []string) error { 106 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 107 | 108 | stepper.NewProgressStep("Fetching Credentials") 109 | 110 | clusterName := viper.GetString("flags.cluster-name") 111 | 112 | cluster, err := cluster.GetCluster(clusterName) 113 | if err != nil { 114 | wrerr := fmt.Errorf("failed to get cluster: %w", err) 115 | stepper.FailCurrentStep(wrerr) 116 | return wrerr 117 | } 118 | 119 | stepper.CompleteCurrentStep() 120 | 121 | header := ` 122 | ## 123 | # Root Credentials 124 | 125 | ### :bulb: Keep this data secure. These passwords can be used to access the following applications in your platform 126 | 127 | ## ArgoCD Admin Password 128 | ##### ` + cluster.ArgoCDPassword + ` 129 | 130 | ## KBot User Password 131 | ##### ` + cluster.VaultAuth.KbotPassword + ` 132 | 133 | ## Vault Root Token 134 | ##### ` + cluster.VaultAuth.RootToken + ` 135 | ` 136 | stepper.InfoStep(step.EmojiBulb, progress.RenderMessage(header)) 137 | 138 | return nil 139 | } 140 | 141 | func Destroy(cmd *cobra.Command, _ []string) error { 142 | stepper := step.NewStepFactory(cmd.ErrOrStderr()) 143 | // Determine if there are active installs 144 | gitProvider := viper.GetString("flags.git-provider") 145 | gitProtocol := viper.GetString("flags.git-protocol") 146 | cloudProvider := viper.GetString("kubefirst.cloud-provider") 147 | 148 | log.Info().Msg("destroying kubefirst platform") 149 | 150 | clusterName := viper.GetString("flags.cluster-name") 151 | domainName := viper.GetString("flags.domain-name") 152 | 153 | // Switch based on git provider, set params 154 | var cGitOwner string 155 | switch gitProvider { 156 | case "github": 157 | cGitOwner = viper.GetString("flags.github-owner") 158 | case "gitlab": 159 | cGitOwner = viper.GetString("flags.gitlab-owner") 160 | default: 161 | return fmt.Errorf("invalid git provider: %q", gitProvider) 162 | } 163 | 164 | // Instantiate aws config 165 | config, err := providerConfigs.GetConfig( 166 | clusterName, 167 | domainName, 168 | gitProvider, 169 | cGitOwner, 170 | gitProtocol, 171 | os.Getenv("CF_API_TOKEN"), 172 | os.Getenv("CF_ORIGIN_CA_ISSUER_API_TOKEN"), 173 | ) 174 | if err != nil { 175 | return fmt.Errorf("failed to get config: %w", err) 176 | } 177 | 178 | stepper.NewProgressStep("Destroying k3d") 179 | 180 | if err := launch.Down(true); err != nil { 181 | wrerr := fmt.Errorf("failed to destroy k3d: %w", err) 182 | stepper.FailCurrentStep(wrerr) 183 | return wrerr 184 | } 185 | 186 | stepper.NewProgressStep("Cleaning up environment") 187 | 188 | log.Info().Msg("resetting `$HOME/.kubefirst` config") 189 | viper.Set("argocd", "") 190 | viper.Set(gitProvider, "") 191 | viper.Set("components", "") 192 | viper.Set("kbot", "") 193 | viper.Set("kubefirst-checks", "") 194 | viper.Set("launch", "") 195 | viper.Set("kubefirst", "") 196 | viper.Set("flags", "") 197 | viper.Set("k1-paths", "") 198 | if err := viper.WriteConfig(); err != nil { 199 | wrerr := fmt.Errorf("failed to write viper config: %w", err) 200 | stepper.FailCurrentStep(wrerr) 201 | return wrerr 202 | } 203 | 204 | if _, err := os.Stat(config.K1Dir + "/kubeconfig"); !os.IsNotExist(err) { 205 | if err := os.Remove(config.K1Dir + "/kubeconfig"); err != nil { 206 | wrerr := fmt.Errorf("failed to delete kubeconfig: %w", err) 207 | stepper.FailCurrentStep(wrerr) 208 | return wrerr 209 | } 210 | } 211 | 212 | successMessage := ` 213 | ### 214 | #### :tada: Success` + "`Your k3d kubefirst platform has been destroyed.`" + ` 215 | 216 | ### :blue_book: To delete a management cluster please see documentation: 217 | https://kubefirst.konstruct.io/docs/` + cloudProvider + `/deprovision 218 | ` 219 | 220 | progress.Success(successMessage) 221 | 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /internal/generate/files.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Files struct { 11 | data map[string]bytes.Buffer 12 | } 13 | 14 | func (f *Files) Add(file string, content bytes.Buffer) { 15 | if f.data == nil { 16 | f.data = map[string]bytes.Buffer{} 17 | } 18 | 19 | f.data[file] = content 20 | } 21 | 22 | func (f *Files) Save(filePrefix string) error { 23 | for file, content := range f.data { 24 | name := filepath.Join(filePrefix, file) 25 | 26 | if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil { 27 | return fmt.Errorf("failed to create directory: %w", err) 28 | } 29 | 30 | if err := os.WriteFile(name, content.Bytes(), 0o644); err != nil { 31 | return fmt.Errorf("failed to write file: %w", err) 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/generate/scaffold.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "io/fs" 8 | "path/filepath" 9 | "strings" 10 | "text/template" 11 | ) 12 | 13 | //go:embed scaffold 14 | var scaffoldFS embed.FS 15 | 16 | type ScaffoldData struct { 17 | AppName string 18 | DeploymentName string 19 | Description string 20 | Environment string 21 | Namespace string 22 | } 23 | 24 | func AppScaffold(appName string, environments []string, outputPath string) error { 25 | for _, env := range environments { 26 | files, err := generateAppScaffoldEnvironmentFiles(appName, env) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | if err := files.Save(filepath.Join(outputPath, env)); err != nil { 32 | return err 33 | } 34 | } 35 | return nil 36 | } 37 | 38 | func generateAppScaffoldEnvironmentFiles(appName, environment string) (*Files, error) { 39 | rootDir := "scaffold/" 40 | 41 | tpl, err := template.New("tpl").ParseFS(scaffoldFS, rootDir+"*.yaml", rootDir+"**/*.yaml") 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to create template: %w", err) 44 | } 45 | 46 | data := ScaffoldData{ 47 | AppName: appName, 48 | DeploymentName: fmt.Sprintf("%s-environment-%s", environment, appName), 49 | Description: fmt.Sprintf("%s example application", appName), 50 | Environment: environment, 51 | Namespace: environment, 52 | } 53 | 54 | files := &Files{} 55 | err = fs.WalkDir(scaffoldFS, ".", func(path string, d fs.DirEntry, err error) error { 56 | if err != nil { 57 | return fmt.Errorf("error walking directory: %w", err) 58 | } 59 | 60 | if d.IsDir() { 61 | return nil 62 | } 63 | 64 | // Get the file name without the root path 65 | file, _ := strings.CutPrefix(path, rootDir) 66 | 67 | // Parse any template variables in the file name 68 | fileTpl, err := tpl.Parse(file) 69 | if err != nil { 70 | return fmt.Errorf("error parsing file name: %w", err) 71 | } 72 | 73 | var fileNameOutput bytes.Buffer 74 | if err := fileTpl.Execute(&fileNameOutput, data); err != nil { 75 | return fmt.Errorf("error executing file name: %w", err) 76 | } 77 | 78 | // Parse the contents of the file 79 | var fileContent bytes.Buffer 80 | if err := tpl.ExecuteTemplate(&fileContent, d.Name(), data); err != nil { 81 | return fmt.Errorf("error executing template: %w", err) 82 | } 83 | 84 | // Now store everything for output 85 | files.Add(fileNameOutput.String(), fileContent) 86 | 87 | return nil 88 | }) 89 | if err != nil { 90 | return nil, fmt.Errorf("error walking directory: %w", err) 91 | } 92 | 93 | return files, nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/generate/scaffold/{{ .AppName }}.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: "{{ .DeploymentName }}" 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | annotations: 9 | argocd.argoproj.io/sync-wave: '45' 10 | spec: 11 | project: default 12 | source: 13 | repoURL: 14 | path: "registry/environments/{{ .Environment }}/{{ .AppName }}" 15 | targetRevision: HEAD 16 | destination: 17 | name: in-cluster 18 | namespace: "{{ .Namespace }}" 19 | syncPolicy: 20 | automated: 21 | prune: true 22 | selfHeal: true 23 | syncOptions: 24 | - CreateNamespace=true 25 | -------------------------------------------------------------------------------- /internal/generate/scaffold/{{ .AppName }}/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: "{{ .Description }}" 3 | name: "{{ .AppName }}" 4 | type: application 5 | version: 1.0.0 6 | dependencies: 7 | - name: "{{ .AppName }}" 8 | repository: http://chartmuseum.chartmuseum.svc.cluster.local:8080 9 | version: 0.0.1-rc.awaiting-ci 10 | -------------------------------------------------------------------------------- /internal/generate/scaffold/{{ .AppName }}/values.yaml: -------------------------------------------------------------------------------- 1 | # This is a generated file. These values may not correspond to your own chart's values 2 | 3 | "{{ .AppName }}": 4 | annotations: | 5 | linkerd.io/inject: "enabled" 6 | labels: | 7 | mirror.linkerd.io/exported: "true" 8 | image: 9 | repository: "/{{ .AppName }}" 10 | imagePullSecrets: 11 | - name: docker-config 12 | ingress: 13 | className: nginx 14 | enabled: true 15 | annotations: 16 | 17 | 18 | 19 | 20 | nginx.ingress.kubernetes.io/service-upstream: "true" 21 | hosts: 22 | - host: "{{ .AppName }}-{{ .Environment }}." 23 | paths: 24 | - path: / 25 | pathType: Prefix 26 | tls: 27 | - secretName: "{{ .AppName }}-tls" 28 | hosts: 29 | - "{{ .AppName }}-{{ .Environment }}." 30 | -------------------------------------------------------------------------------- /internal/generate/scaffold_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_generateAppScaffoldEnvironmentFiles(t *testing.T) { 15 | tests := []struct { 16 | Name string 17 | Environment string 18 | Error error 19 | }{ 20 | { 21 | Name: "app", 22 | Environment: "development", 23 | }, 24 | { 25 | Name: "metaphor", 26 | Environment: "production", 27 | }, 28 | { 29 | Name: "some-app", 30 | Environment: "some-environment", 31 | }, 32 | } 33 | 34 | for _, test := range tests { 35 | goldenDir := filepath.Join(".", "testdata", "scaffold", test.Environment) 36 | 37 | fileData, err := generateAppScaffoldEnvironmentFiles(test.Name, test.Environment) 38 | require.Equal(t, test.Error, err) 39 | 40 | expectedFiles := []string{} 41 | for k := range fileData.data { 42 | expectedFiles = append(expectedFiles, k) 43 | } 44 | 45 | actualFiles := make([]string, 0) 46 | err = filepath.WalkDir(goldenDir, func(path string, d fs.DirEntry, err error) error { 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if d.IsDir() { 52 | return nil 53 | } 54 | 55 | f, _ := strings.CutPrefix(path, goldenDir+string(filepath.Separator)) 56 | actualFiles = append(actualFiles, f) 57 | 58 | actualFileContent, err := os.ReadFile(path) 59 | require.Nil(t, err) 60 | 61 | expectedFileContent, ok := fileData.data[f] 62 | require.True(t, ok) 63 | 64 | require.Equal(t, expectedFileContent.String(), string(actualFileContent)) 65 | 66 | return nil 67 | }) 68 | require.Nil(t, err) 69 | 70 | slices.Sort(expectedFiles) 71 | slices.Sort(actualFiles) 72 | require.Equal(t, expectedFiles, actualFiles) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/development/app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: "development-environment-app" 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | annotations: 9 | argocd.argoproj.io/sync-wave: '45' 10 | spec: 11 | project: default 12 | source: 13 | repoURL: 14 | path: "registry/environments/development/app" 15 | targetRevision: HEAD 16 | destination: 17 | name: in-cluster 18 | namespace: "development" 19 | syncPolicy: 20 | automated: 21 | prune: true 22 | selfHeal: true 23 | syncOptions: 24 | - CreateNamespace=true 25 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/development/app/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: "app example application" 3 | name: "app" 4 | type: application 5 | version: 1.0.0 6 | dependencies: 7 | - name: "app" 8 | repository: http://chartmuseum.chartmuseum.svc.cluster.local:8080 9 | version: 0.0.1-rc.awaiting-ci 10 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/development/app/values.yaml: -------------------------------------------------------------------------------- 1 | # This is a generated file. These values may not correspond to your own chart's values 2 | 3 | "app": 4 | annotations: | 5 | linkerd.io/inject: "enabled" 6 | labels: | 7 | mirror.linkerd.io/exported: "true" 8 | image: 9 | repository: "/app" 10 | imagePullSecrets: 11 | - name: docker-config 12 | ingress: 13 | className: nginx 14 | enabled: true 15 | annotations: 16 | 17 | 18 | 19 | 20 | nginx.ingress.kubernetes.io/service-upstream: "true" 21 | hosts: 22 | - host: "app-development." 23 | paths: 24 | - path: / 25 | pathType: Prefix 26 | tls: 27 | - secretName: "app-tls" 28 | hosts: 29 | - "app-development." 30 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/production/metaphor.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: "production-environment-metaphor" 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | annotations: 9 | argocd.argoproj.io/sync-wave: '45' 10 | spec: 11 | project: default 12 | source: 13 | repoURL: 14 | path: "registry/environments/production/metaphor" 15 | targetRevision: HEAD 16 | destination: 17 | name: in-cluster 18 | namespace: "production" 19 | syncPolicy: 20 | automated: 21 | prune: true 22 | selfHeal: true 23 | syncOptions: 24 | - CreateNamespace=true 25 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/production/metaphor/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: "metaphor example application" 3 | name: "metaphor" 4 | type: application 5 | version: 1.0.0 6 | dependencies: 7 | - name: "metaphor" 8 | repository: http://chartmuseum.chartmuseum.svc.cluster.local:8080 9 | version: 0.0.1-rc.awaiting-ci 10 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/production/metaphor/values.yaml: -------------------------------------------------------------------------------- 1 | # This is a generated file. These values may not correspond to your own chart's values 2 | 3 | "metaphor": 4 | annotations: | 5 | linkerd.io/inject: "enabled" 6 | labels: | 7 | mirror.linkerd.io/exported: "true" 8 | image: 9 | repository: "/metaphor" 10 | imagePullSecrets: 11 | - name: docker-config 12 | ingress: 13 | className: nginx 14 | enabled: true 15 | annotations: 16 | 17 | 18 | 19 | 20 | nginx.ingress.kubernetes.io/service-upstream: "true" 21 | hosts: 22 | - host: "metaphor-production." 23 | paths: 24 | - path: / 25 | pathType: Prefix 26 | tls: 27 | - secretName: "metaphor-tls" 28 | hosts: 29 | - "metaphor-production." 30 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/some-environment/some-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: "some-environment-environment-some-app" 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | annotations: 9 | argocd.argoproj.io/sync-wave: '45' 10 | spec: 11 | project: default 12 | source: 13 | repoURL: 14 | path: "registry/environments/some-environment/some-app" 15 | targetRevision: HEAD 16 | destination: 17 | name: in-cluster 18 | namespace: "some-environment" 19 | syncPolicy: 20 | automated: 21 | prune: true 22 | selfHeal: true 23 | syncOptions: 24 | - CreateNamespace=true 25 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/some-environment/some-app/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | description: "some-app example application" 3 | name: "some-app" 4 | type: application 5 | version: 1.0.0 6 | dependencies: 7 | - name: "some-app" 8 | repository: http://chartmuseum.chartmuseum.svc.cluster.local:8080 9 | version: 0.0.1-rc.awaiting-ci 10 | -------------------------------------------------------------------------------- /internal/generate/testdata/scaffold/some-environment/some-app/values.yaml: -------------------------------------------------------------------------------- 1 | # This is a generated file. These values may not correspond to your own chart's values 2 | 3 | "some-app": 4 | annotations: | 5 | linkerd.io/inject: "enabled" 6 | labels: | 7 | mirror.linkerd.io/exported: "true" 8 | image: 9 | repository: "/some-app" 10 | imagePullSecrets: 11 | - name: docker-config 12 | ingress: 13 | className: nginx 14 | enabled: true 15 | annotations: 16 | 17 | 18 | 19 | 20 | nginx.ingress.kubernetes.io/service-upstream: "true" 21 | hosts: 22 | - host: "some-app-some-environment." 23 | paths: 24 | - path: / 25 | pathType: Prefix 26 | tls: 27 | - secretName: "some-app-tls" 28 | hosts: 29 | - "some-app-some-environment." 30 | -------------------------------------------------------------------------------- /internal/gitShim/containerRegistryAuth.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package gitShim //nolint:revive // allowed during refactoring 8 | 9 | import ( 10 | "encoding/base64" 11 | "fmt" 12 | 13 | "github.com/konstructio/kubefirst-api/pkg/gitlab" 14 | "github.com/konstructio/kubefirst-api/pkg/k8s" 15 | v1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/api/errors" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/client-go/kubernetes" 19 | ) 20 | 21 | const secretName = "container-registry-auth" 22 | 23 | type ContainerRegistryAuth struct { 24 | GitProvider string 25 | GitUser string 26 | GitToken string 27 | GitlabGroupFlag string 28 | GithubOwner string 29 | ContainerRegistryHost string 30 | 31 | Clientset kubernetes.Interface 32 | } 33 | 34 | // CreateContainerRegistrySecret 35 | func CreateContainerRegistrySecret(obj *ContainerRegistryAuth) (string, error) { 36 | // Handle secret creation for container registry authentication 37 | switch obj.GitProvider { 38 | // GitHub docker auth secret 39 | // kaniko requires a specific format for Docker auth created as a secret 40 | // For GitHub, this becomes the provided token (pat) 41 | case "github": 42 | usernamePasswordString := fmt.Sprintf("%s:%s", obj.GitUser, obj.GitToken) 43 | usernamePasswordStringB64 := base64.StdEncoding.EncodeToString([]byte(usernamePasswordString)) 44 | dockerConfigString := fmt.Sprintf(`{"auths": {"%s": {"username": %q, "password": %q, "email": %q, "auth": %q}}}`, 45 | obj.ContainerRegistryHost, 46 | obj.GithubOwner, 47 | obj.GitToken, 48 | "k-bot@example.com", 49 | usernamePasswordStringB64, 50 | ) 51 | 52 | data := map[string][]byte{"config.json": []byte(dockerConfigString)} 53 | argoDeployTokenSecret := &v1.Secret{ 54 | ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: "argo"}, 55 | Data: data, 56 | Type: "Opaque", 57 | } 58 | err := k8s.CreateSecretV2(obj.Clientset, argoDeployTokenSecret) 59 | if errors.IsAlreadyExists(err) { 60 | if err := k8s.UpdateSecretV2(obj.Clientset, "argo", secretName, data); err != nil { 61 | return "", fmt.Errorf("error while updating secret for GitHub container registry auth: %w", err) 62 | } 63 | } 64 | 65 | if err != nil && !errors.IsAlreadyExists(err) { 66 | return "", fmt.Errorf("error while creating secret for GitHub container registry auth: %w", err) 67 | } 68 | 69 | case "gitlab": 70 | gitlabClient, err := gitlab.NewGitLabClient(obj.GitToken, obj.GitlabGroupFlag) 71 | if err != nil { 72 | return "", fmt.Errorf("error while creating GitLab client: %w", err) 73 | } 74 | 75 | p := gitlab.DeployTokenCreateParameters{ 76 | Name: secretName, 77 | Username: secretName, 78 | Scopes: []string{"read_registry", "write_registry"}, 79 | } 80 | token, err := gitlabClient.CreateGroupDeployToken(0, &p) 81 | if err != nil { 82 | return "", fmt.Errorf("error while creating GitLab group deploy token: %w", err) 83 | } 84 | 85 | return token, nil 86 | } 87 | 88 | return "", nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/helm/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package helm 8 | 9 | type Repo struct { 10 | Name string `yaml:"name"` 11 | URL string `yaml:"url"` 12 | } 13 | 14 | type Release struct { 15 | AppVersion string `yaml:"app_version"` 16 | Chart string `yaml:"chart"` 17 | Name string `yaml:"name"` 18 | Namespace string `yaml:"namespace"` 19 | Revision string `yaml:"revision"` 20 | Status string `yaml:"status"` 21 | Updated string `yaml:"updated"` 22 | } 23 | -------------------------------------------------------------------------------- /internal/k3d/menu.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package k3d 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "log" 13 | "strings" 14 | 15 | "github.com/charmbracelet/bubbles/list" 16 | tea "github.com/charmbracelet/bubbletea" 17 | "github.com/charmbracelet/lipgloss" 18 | ) 19 | 20 | const ( 21 | ListHeight = 14 22 | DefaultWidth = 20 23 | ) 24 | 25 | var ( 26 | TitleStyle = lipgloss.NewStyle().MarginLeft(2) 27 | ItemStyle = lipgloss.NewStyle().PaddingLeft(4) 28 | SelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("170")) 29 | PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) 30 | HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) 31 | QuitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) 32 | ) 33 | 34 | type Item string 35 | 36 | func (i Item) FilterValue() string { return "" } 37 | 38 | type ItemDelegate struct{} 39 | 40 | func (d ItemDelegate) Height() int { return 1 } 41 | func (d ItemDelegate) Spacing() int { return 0 } 42 | func (d ItemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } 43 | func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { 44 | i, ok := listItem.(Item) 45 | if !ok { 46 | return 47 | } 48 | 49 | str := fmt.Sprintf("%d. %s", index+1, i) 50 | 51 | fn := ItemStyle.Render 52 | if index == m.Index() { 53 | fn = func(s ...string) string { 54 | return SelectedItemStyle.Render("> " + strings.Join(s, " ")) 55 | } 56 | } 57 | 58 | fmt.Fprint(w, fn(str)) 59 | } 60 | 61 | type Model struct { 62 | List list.Model 63 | Choice string 64 | Quitting bool 65 | } 66 | 67 | func (m Model) Init() tea.Cmd { 68 | return nil 69 | } 70 | 71 | func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 72 | switch msg := msg.(type) { 73 | case tea.WindowSizeMsg: 74 | m.List.SetWidth(msg.Width) 75 | return m, nil 76 | 77 | case tea.KeyMsg: 78 | switch keypress := msg.String(); keypress { 79 | case "ctrl+c": 80 | m.Quitting = true 81 | return m, tea.Quit 82 | 83 | case "enter": 84 | i, ok := m.List.SelectedItem().(Item) 85 | if ok { 86 | m.Choice = string(i) 87 | } 88 | return m, tea.Quit 89 | } 90 | } 91 | 92 | var cmd tea.Cmd 93 | m.List, cmd = m.List.Update(msg) 94 | return m, cmd 95 | } 96 | 97 | func (m Model) View() string { 98 | if m.Choice != "" { 99 | return QuitTextStyle.Render(m.Choice) 100 | } 101 | if m.Quitting { 102 | return QuitTextStyle.Render("Quitting.") 103 | } 104 | return "\n" + m.List.View() 105 | } 106 | 107 | func MongoDestinationChooser(inCluster bool) (string, error) { 108 | if inCluster { 109 | return "in-cluster", nil 110 | } 111 | 112 | items := []list.Item{ 113 | Item("in-cluster"), 114 | Item("atlas"), 115 | } 116 | 117 | l := list.New(items, ItemDelegate{}, DefaultWidth, ListHeight) 118 | l.Title = "Where will you be running MongoDB?" 119 | l.SetShowStatusBar(false) 120 | l.SetFilteringEnabled(false) 121 | l.Styles.Title = TitleStyle 122 | l.Styles.PaginationStyle = PaginationStyle 123 | l.Styles.HelpStyle = HelpStyle 124 | 125 | m := Model{List: l} 126 | 127 | model, err := tea.NewProgram(m).Run() 128 | if err != nil { 129 | log.Printf("Error running program: %v", err) 130 | return "", fmt.Errorf("failed to run the program: %w", err) 131 | } 132 | 133 | if strings.Contains(model.View(), "atlas") { 134 | return "atlas", nil 135 | } 136 | if strings.Contains(model.View(), "in-cluster") { 137 | return "in-cluster", nil 138 | } 139 | return "error", nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/launch/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package launch 8 | 9 | const ( 10 | consoleURL = "https://console.kubefirst.dev" 11 | helmChartName = "kubefirst" 12 | helmChartRepoName = "konstruct" 13 | helmChartRepoURL = "https://charts.konstruct.io" 14 | helmChartVersion = "2.8.4" 15 | namespace = "kubefirst" 16 | secretName = "kubefirst-initial-secrets" 17 | ) 18 | -------------------------------------------------------------------------------- /internal/progress/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package progress 8 | 9 | import ( 10 | "log" 11 | "time" 12 | 13 | tea "github.com/charmbracelet/bubbletea" 14 | "github.com/konstructio/kubefirst-api/pkg/types" 15 | "github.com/konstructio/kubefirst/internal/cluster" 16 | ) 17 | 18 | // Commands 19 | func GetClusterInterval(clusterName string) tea.Cmd { 20 | return tea.Every(time.Second*10, func(_ time.Time) tea.Msg { 21 | provisioningCluster, err := cluster.GetCluster(clusterName) 22 | if err != nil { 23 | log.Printf("failed to get cluster %q: %v", clusterName, err) 24 | return nil 25 | } 26 | 27 | return CusterProvisioningMsg(provisioningCluster) 28 | }) 29 | } 30 | 31 | func AddSuccesMessage(cluster types.Cluster) tea.Cmd { 32 | return tea.Tick(0, func(_ time.Time) tea.Msg { 33 | successMessage := DisplaySuccessMessage(cluster) 34 | 35 | return successMessage 36 | }) 37 | } 38 | 39 | func BuildCompletedSteps(cluster types.Cluster) ([]string, string) { 40 | completedSteps := []string{} 41 | nextStep := "" 42 | if cluster.InstallToolsCheck { 43 | completedSteps = append(completedSteps, CompletedStepsLabels.installToolsCheck) 44 | nextStep = CompletedStepsLabels.domainLivenessCheck 45 | } 46 | if cluster.DomainLivenessCheck { 47 | completedSteps = append(completedSteps, CompletedStepsLabels.domainLivenessCheck) 48 | nextStep = CompletedStepsLabels.kbotSetupCheck 49 | } 50 | if cluster.KbotSetupCheck { 51 | completedSteps = append(completedSteps, CompletedStepsLabels.kbotSetupCheck) 52 | nextStep = CompletedStepsLabels.gitInitCheck 53 | } 54 | if cluster.GitInitCheck { 55 | completedSteps = append(completedSteps, CompletedStepsLabels.gitInitCheck) 56 | nextStep = CompletedStepsLabels.gitopsReadyCheck 57 | } 58 | if cluster.GitopsReadyCheck { 59 | completedSteps = append(completedSteps, CompletedStepsLabels.gitopsReadyCheck) 60 | nextStep = CompletedStepsLabels.gitTerraformApplyCheck 61 | } 62 | if cluster.GitTerraformApplyCheck { 63 | completedSteps = append(completedSteps, CompletedStepsLabels.gitTerraformApplyCheck) 64 | nextStep = CompletedStepsLabels.gitopsPushedCheck 65 | } 66 | if cluster.GitopsPushedCheck { 67 | completedSteps = append(completedSteps, CompletedStepsLabels.gitopsPushedCheck) 68 | nextStep = CompletedStepsLabels.cloudTerraformApplyCheck 69 | } 70 | if cluster.CloudTerraformApplyCheck { 71 | completedSteps = append(completedSteps, CompletedStepsLabels.cloudTerraformApplyCheck) 72 | nextStep = CompletedStepsLabels.clusterSecretsCreatedCheck 73 | } 74 | if cluster.ClusterSecretsCreatedCheck { 75 | completedSteps = append(completedSteps, CompletedStepsLabels.clusterSecretsCreatedCheck) 76 | nextStep = CompletedStepsLabels.argoCDInstallCheck 77 | } 78 | if cluster.ArgoCDInstallCheck { 79 | completedSteps = append(completedSteps, CompletedStepsLabels.argoCDInstallCheck) 80 | nextStep = CompletedStepsLabels.argoCDInitializeCheck 81 | } 82 | if cluster.ArgoCDInitializeCheck { 83 | completedSteps = append(completedSteps, CompletedStepsLabels.argoCDInitializeCheck) 84 | nextStep = CompletedStepsLabels.vaultInitializedCheck 85 | } 86 | if cluster.VaultInitializedCheck { 87 | completedSteps = append(completedSteps, CompletedStepsLabels.vaultInitializedCheck) 88 | nextStep = CompletedStepsLabels.vaultTerraformApplyCheck 89 | } 90 | if cluster.VaultTerraformApplyCheck { 91 | completedSteps = append(completedSteps, CompletedStepsLabels.vaultTerraformApplyCheck) 92 | nextStep = CompletedStepsLabels.usersTerraformApplyCheck 93 | } 94 | if cluster.UsersTerraformApplyCheck { 95 | completedSteps = append(completedSteps, CompletedStepsLabels.usersTerraformApplyCheck) 96 | nextStep = "Wrapping up" 97 | } 98 | 99 | return completedSteps, nextStep 100 | } 101 | -------------------------------------------------------------------------------- /internal/progress/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package progress 8 | 9 | var CompletedStepsLabels = ProvisionSteps{ 10 | installToolsCheck: "Installing tools", 11 | domainLivenessCheck: "Domain liveness check", 12 | kbotSetupCheck: "Kbot setup", 13 | gitInitCheck: "Initializing Git", 14 | gitopsReadyCheck: "Initializing GitOps", 15 | gitTerraformApplyCheck: "Git Terraform apply", 16 | gitopsPushedCheck: "GitOps repos pushed", 17 | cloudTerraformApplyCheck: "Cloud Terraform apply", 18 | clusterSecretsCreatedCheck: "Creating cluster secrets", 19 | argoCDInstallCheck: "Installing ArgoCD", 20 | argoCDInitializeCheck: "Initializing ArgoCD", 21 | vaultInitializedCheck: "Initializing Vault", 22 | vaultTerraformApplyCheck: "Vault Terraform apply", 23 | usersTerraformApplyCheck: "Users Terraform apply", 24 | } 25 | -------------------------------------------------------------------------------- /internal/progress/message.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | 7 | Emojis definition https://github.com/yuin/goldmark-emoji/blob/master/definition/github.go 8 | Color definition https://www.ditig.com/256-colors-cheat-sheet 9 | */ 10 | package progress 11 | 12 | import ( 13 | "fmt" 14 | "log" 15 | "strconv" 16 | 17 | "github.com/charmbracelet/glamour" 18 | "github.com/konstructio/kubefirst-api/pkg/types" 19 | "github.com/spf13/viper" 20 | ) 21 | 22 | func RenderMessage(message string) string { 23 | r, _ := glamour.NewTermRenderer( 24 | glamour.WithStyles(StyleConfig), 25 | glamour.WithEmoji(), 26 | ) 27 | 28 | out, err := r.Render(message) 29 | if err != nil { 30 | log.Println(err.Error()) 31 | return err.Error() 32 | } 33 | return out 34 | } 35 | 36 | func createStep(message string) addStep { 37 | out := RenderMessage(message) 38 | 39 | return addStep{ 40 | message: out, 41 | } 42 | } 43 | 44 | func createErrorLog(message string) errorMsg { 45 | out := RenderMessage(fmt.Sprintf("##### :no_entry_sign: Error: %s", message)) 46 | 47 | return errorMsg{ 48 | message: out, 49 | } 50 | } 51 | 52 | // Public Progress Functions 53 | func DisplayLogHints(estimatedTime int) { 54 | logFile := viper.GetString("k1-paths.log-file") 55 | cloudProvider := viper.GetString("kubefirst.cloud-provider") 56 | 57 | documentationLink := "https://kubefirst.konstruct.io/docs/" 58 | if cloudProvider != "" { 59 | documentationLink += cloudProvider + `/quick-start/install/cli` 60 | } 61 | 62 | header := ` 63 | ## 64 | # Welcome to Kubefirst 65 | 66 | ### :bulb: To view verbose logs run below command in new terminal: 67 | ` + fmt.Sprintf("##### **tail -f -n +1 %s**", logFile) + ` 68 | ### :blue_book: Documentation: ` + documentationLink + ` 69 | 70 | ### :alarm_clock: Estimated time:` + fmt.Sprintf("`%s minutes` \n\n", strconv.Itoa(estimatedTime)) 71 | 72 | headerMessage := RenderMessage(header) 73 | 74 | Progress.Send(headerMsg{ 75 | message: headerMessage, 76 | }) 77 | } 78 | 79 | //nolint:revive // will be fixed in the future 80 | func DisplaySuccessMessage(cluster types.Cluster) string { 81 | cloudCliKubeconfig := "" 82 | 83 | gitProviderLabel := "GitHub" 84 | if cluster.GitProvider == "gitlab" { 85 | gitProviderLabel = "GitLab" 86 | } 87 | 88 | switch cluster.CloudProvider { 89 | case "aws": 90 | cloudCliKubeconfig = fmt.Sprintf("aws eks update-kubeconfig --name %q --region %q", cluster.ClusterName, cluster.CloudRegion) 91 | case "azure": 92 | cloudCliKubeconfig = fmt.Sprintf("az aks get-credentials --resource-group %q --name %q", cluster.ClusterName, cluster.ClusterName) 93 | case "civo": 94 | cloudCliKubeconfig = fmt.Sprintf("civo kubernetes config %q --save", cluster.ClusterName) 95 | case "digitalocean": 96 | cloudCliKubeconfig = "doctl kubernetes cluster kubeconfig save " + cluster.ClusterName 97 | case "google": 98 | cloudCliKubeconfig = fmt.Sprintf("gcloud container clusters get-credentials %q --region=%q", cluster.ClusterName, cluster.CloudRegion) 99 | case "vultr": 100 | cloudCliKubeconfig = fmt.Sprintf("vultr-cli kubernetes config %q", cluster.ClusterName) 101 | case "k3s": 102 | cloudCliKubeconfig = "use the kubeconfig file outputted from terraform to access the cluster" 103 | } 104 | 105 | var fullDomainName string 106 | if cluster.SubdomainName != "" { 107 | fullDomainName = fmt.Sprintf("%s.%s", cluster.SubdomainName, cluster.DomainName) 108 | } else { 109 | fullDomainName = cluster.DomainName 110 | } 111 | 112 | success := ` 113 | ## 114 | #### :tada: Success` + "`Cluster " + cluster.ClusterName + " is now up and running`" + ` 115 | 116 | # Cluster ` + cluster.ClusterName + ` details: 117 | 118 | ### :bulb: To retrieve root credentials for your Kubefirst platform run: 119 | ##### kubefirst ` + cluster.CloudProvider + ` root-credentials 120 | 121 | ## ` + fmt.Sprintf("`%s `", gitProviderLabel) + ` 122 | ### Git Owner ` + fmt.Sprintf("`%s`", cluster.GitAuth.Owner) + ` 123 | ### Repos ` + fmt.Sprintf("`https://%s.com/%s/gitops` \n\n", cluster.GitProvider, cluster.GitAuth.Owner) + 124 | fmt.Sprintf("` https://%s.com/%s/metaphor`", cluster.GitProvider, cluster.GitAuth.Owner) + ` 125 | ## Kubefirst Console 126 | ### URL ` + fmt.Sprintf("`https://kubefirst.%s`", fullDomainName) + ` 127 | ## Argo CD 128 | ### URL ` + fmt.Sprintf("`https://argocd.%s`", fullDomainName) + ` 129 | ## Vault 130 | ### URL ` + fmt.Sprintf("`https://vault.%s`", fullDomainName) + ` 131 | 132 | 133 | ### :bulb: Quick start examples: 134 | 135 | ### To connect to your new Kubernetes cluster run: 136 | ##### ` + cloudCliKubeconfig + ` 137 | 138 | ### To view all cluster pods run: 139 | ##### kubectl get pods -A 140 | ` 141 | 142 | return success 143 | } 144 | 145 | func AddStep(message string) { 146 | renderedMessage := createStep(fmt.Sprintf("%s %s", ":dizzy:", message)) 147 | Progress.Send(renderedMessage) 148 | } 149 | 150 | func CompleteStep(message string) { 151 | Progress.Send(completeStep{ 152 | message: message, 153 | }) 154 | } 155 | 156 | func Success(success string) { 157 | successMessage := RenderMessage(success) 158 | 159 | Progress.Send( 160 | successMsg{ 161 | message: successMessage, 162 | }) 163 | } 164 | 165 | func Error(message string) { 166 | renderedMessage := createErrorLog(message) 167 | Progress.Send(renderedMessage) 168 | } 169 | 170 | func StartProvisioning(clusterName string) { 171 | provisioningMessage := startProvision{ 172 | clusterName: clusterName, 173 | } 174 | 175 | Progress.Send(provisioningMessage) 176 | } 177 | -------------------------------------------------------------------------------- /internal/progress/progress.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package progress 8 | 9 | import ( 10 | "fmt" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/konstructio/kubefirst-api/pkg/types" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | var Progress *tea.Program 18 | 19 | //nolint:revive // will be removed after refactoring 20 | func NewModel() progressModel { 21 | return progressModel{ 22 | isProvisioned: false, 23 | } 24 | } 25 | 26 | // Bubbletea functions 27 | func InitializeProgressTerminal() { 28 | Progress = tea.NewProgram(NewModel()) 29 | } 30 | 31 | func (m progressModel) Init() tea.Cmd { 32 | return nil 33 | } 34 | 35 | func (m progressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 36 | switch msg := msg.(type) { 37 | case tea.KeyMsg: 38 | switch msg.String() { 39 | case "ctrl+c": 40 | return m, tea.Quit 41 | default: 42 | return m, nil 43 | } 44 | 45 | case headerMsg: 46 | m.header = msg.message 47 | return m, nil 48 | 49 | case addStep: 50 | m.nextStep = msg.message 51 | return m, nil 52 | 53 | case completeStep: 54 | m.completedSteps = append(m.completedSteps, msg.message) 55 | m.nextStep = "" 56 | return m, nil 57 | 58 | case errorMsg: 59 | m.error = msg.message 60 | return m, tea.Quit 61 | 62 | case successMsg: 63 | m.successMessage = msg.message + "\n\n" 64 | return m, tea.Quit 65 | 66 | case startProvision: 67 | m.clusterName = msg.clusterName 68 | return m, GetClusterInterval(m.clusterName) 69 | 70 | case CusterProvisioningMsg: 71 | m.provisioningCluster = types.Cluster(msg) 72 | completedSteps, nextStep := BuildCompletedSteps(types.Cluster(msg)) 73 | m.completedSteps = append(m.completedSteps, completedSteps...) 74 | m.nextStep = RenderMessage(fmt.Sprintf(":dizzy: %s", nextStep)) 75 | 76 | if m.provisioningCluster.Status == "error" { 77 | errorMessage := createErrorLog(m.provisioningCluster.LastCondition) 78 | m.error = errorMessage.message 79 | return m, tea.Quit 80 | } 81 | 82 | if m.provisioningCluster.Status == "provisioned" { 83 | m.isProvisioned = true 84 | m.nextStep = "" 85 | viper.Set("kubefirst-checks.cluster-install-complete", true) 86 | viper.WriteConfig() 87 | 88 | return m, AddSuccesMessage(m.provisioningCluster) 89 | } 90 | 91 | return m, GetClusterInterval(m.clusterName) 92 | 93 | default: 94 | return m, nil 95 | } 96 | } 97 | 98 | func (m progressModel) View() string { 99 | if !m.isProvisioned && m.successMessage == "" { 100 | index := 0 101 | 102 | if len(m.completedSteps) > 5 { 103 | index = len(m.completedSteps) - 5 104 | } 105 | 106 | completedSteps := "" 107 | for i := index; i < len(m.completedSteps); i++ { 108 | completedSteps += RenderMessage(fmt.Sprintf(":white_check_mark: %s", m.completedSteps[i])) 109 | } 110 | 111 | if m.header != "" { 112 | return m.header + "\n\n" + 113 | completedSteps + 114 | m.nextStep + "\n\n" + 115 | m.error + "\n\n" 116 | } 117 | } 118 | 119 | return m.successMessage 120 | } 121 | -------------------------------------------------------------------------------- /internal/progress/styles.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "github.com/charmbracelet/glamour/ansi" 5 | ) 6 | 7 | var StyleConfig = ansi.StyleConfig{ 8 | Document: ansi.StyleBlock{ 9 | StylePrimitive: ansi.StylePrimitive{ 10 | BlockPrefix: "", 11 | BlockSuffix: "", 12 | Color: stringPtr("252"), 13 | }, 14 | Margin: uintPtr(2), 15 | }, 16 | BlockQuote: ansi.StyleBlock{ 17 | Indent: uintPtr(1), 18 | IndentToken: stringPtr("│ "), 19 | }, 20 | List: ansi.StyleList{ 21 | LevelIndent: 2, 22 | }, 23 | Heading: ansi.StyleBlock{ 24 | StylePrimitive: ansi.StylePrimitive{ 25 | BlockSuffix: "\n", 26 | Color: stringPtr("39"), 27 | Bold: boolPtr(true), 28 | }, 29 | }, 30 | H1: ansi.StyleBlock{ 31 | StylePrimitive: ansi.StylePrimitive{ 32 | Prefix: " ", 33 | Suffix: " ", 34 | Color: stringPtr("288"), 35 | BackgroundColor: stringPtr("63"), 36 | Bold: boolPtr(true), 37 | }, 38 | }, 39 | H2: ansi.StyleBlock{ 40 | StylePrimitive: ansi.StylePrimitive{ 41 | Prefix: "", 42 | Color: stringPtr("99"), 43 | }, 44 | }, 45 | H3: ansi.StyleBlock{ 46 | StylePrimitive: ansi.StylePrimitive{ 47 | Prefix: "", 48 | Color: stringPtr("244"), 49 | Bold: boolPtr(true), 50 | }, 51 | Margin: uintPtr(0), 52 | }, 53 | H4: ansi.StyleBlock{ 54 | StylePrimitive: ansi.StylePrimitive{ 55 | Prefix: "", 56 | Color: stringPtr("70"), 57 | }, 58 | }, 59 | H5: ansi.StyleBlock{ 60 | StylePrimitive: ansi.StylePrimitive{ 61 | Prefix: "", 62 | Color: stringPtr("15"), 63 | }, 64 | Margin: uintPtr(0), 65 | }, 66 | H6: ansi.StyleBlock{ 67 | StylePrimitive: ansi.StylePrimitive{ 68 | Prefix: "###### ", 69 | Color: stringPtr("35"), 70 | Bold: boolPtr(false), 71 | }, 72 | }, 73 | Strikethrough: ansi.StylePrimitive{ 74 | CrossedOut: boolPtr(true), 75 | }, 76 | Emph: ansi.StylePrimitive{ 77 | Italic: boolPtr(true), 78 | }, 79 | Strong: ansi.StylePrimitive{ 80 | Bold: boolPtr(true), 81 | }, 82 | HorizontalRule: ansi.StylePrimitive{ 83 | Color: stringPtr("240"), 84 | Format: "\n--------\n", 85 | }, 86 | Item: ansi.StylePrimitive{ 87 | BlockPrefix: "• ", 88 | }, 89 | Enumeration: ansi.StylePrimitive{ 90 | BlockPrefix: ". ", 91 | Color: stringPtr("#8be9fd"), 92 | }, 93 | Task: ansi.StyleTask{ 94 | StylePrimitive: ansi.StylePrimitive{}, 95 | Ticked: "[✓] ", 96 | Unticked: "[ ] ", 97 | }, 98 | Link: ansi.StylePrimitive{ 99 | Color: stringPtr("15"), 100 | Underline: boolPtr(false), 101 | }, 102 | LinkText: ansi.StylePrimitive{ 103 | Color: stringPtr("35"), 104 | Bold: boolPtr(true), 105 | }, 106 | Image: ansi.StylePrimitive{ 107 | Color: stringPtr("212"), 108 | Underline: boolPtr(true), 109 | }, 110 | ImageText: ansi.StylePrimitive{ 111 | Color: stringPtr("243"), 112 | Format: "Image: {{.text}} →", 113 | }, 114 | Code: ansi.StyleBlock{ 115 | StylePrimitive: ansi.StylePrimitive{ 116 | Color: stringPtr("15"), 117 | Prefix: " ", 118 | Suffix: " ", 119 | Bold: boolPtr(true), 120 | BackgroundColor: stringPtr(""), 121 | }, 122 | }, 123 | CodeBlock: ansi.StyleCodeBlock{ 124 | StyleBlock: ansi.StyleBlock{ 125 | StylePrimitive: ansi.StylePrimitive{ 126 | Color: stringPtr("244"), 127 | }, 128 | Margin: uintPtr(2), 129 | }, 130 | Chroma: &ansi.Chroma{ 131 | Text: ansi.StylePrimitive{ 132 | Color: stringPtr("#f8f8f2"), 133 | }, 134 | Error: ansi.StylePrimitive{ 135 | Color: stringPtr("#f8f8f2"), 136 | BackgroundColor: stringPtr("#ff5555"), 137 | }, 138 | Comment: ansi.StylePrimitive{ 139 | Color: stringPtr("#6272A4"), 140 | }, 141 | CommentPreproc: ansi.StylePrimitive{ 142 | Color: stringPtr("#ff79c6"), 143 | }, 144 | Keyword: ansi.StylePrimitive{ 145 | Color: stringPtr("#ff79c6"), 146 | }, 147 | KeywordReserved: ansi.StylePrimitive{ 148 | Color: stringPtr("#ff79c6"), 149 | }, 150 | KeywordNamespace: ansi.StylePrimitive{ 151 | Color: stringPtr("#ff79c6"), 152 | }, 153 | KeywordType: ansi.StylePrimitive{ 154 | Color: stringPtr("#8be9fd"), 155 | }, 156 | Operator: ansi.StylePrimitive{ 157 | Color: stringPtr("#ff79c6"), 158 | }, 159 | Punctuation: ansi.StylePrimitive{ 160 | Color: stringPtr("#f8f8f2"), 161 | }, 162 | Name: ansi.StylePrimitive{ 163 | Color: stringPtr("#8be9fd"), 164 | }, 165 | NameBuiltin: ansi.StylePrimitive{ 166 | Color: stringPtr("#8be9fd"), 167 | }, 168 | NameTag: ansi.StylePrimitive{ 169 | Color: stringPtr("#ff79c6"), 170 | }, 171 | NameAttribute: ansi.StylePrimitive{ 172 | Color: stringPtr("#50fa7b"), 173 | }, 174 | NameClass: ansi.StylePrimitive{ 175 | Color: stringPtr("#8be9fd"), 176 | }, 177 | NameConstant: ansi.StylePrimitive{ 178 | Color: stringPtr("#bd93f9"), 179 | }, 180 | NameDecorator: ansi.StylePrimitive{ 181 | Color: stringPtr("#50fa7b"), 182 | }, 183 | NameFunction: ansi.StylePrimitive{ 184 | Color: stringPtr("#50fa7b"), 185 | }, 186 | LiteralNumber: ansi.StylePrimitive{ 187 | Color: stringPtr("#6EEFC0"), 188 | }, 189 | LiteralString: ansi.StylePrimitive{ 190 | Color: stringPtr("#f1fa8c"), 191 | }, 192 | LiteralStringEscape: ansi.StylePrimitive{ 193 | Color: stringPtr("#ff79c6"), 194 | }, 195 | GenericDeleted: ansi.StylePrimitive{ 196 | Color: stringPtr("#ff5555"), 197 | }, 198 | GenericEmph: ansi.StylePrimitive{ 199 | Color: stringPtr("#f1fa8c"), 200 | Italic: boolPtr(true), 201 | }, 202 | GenericInserted: ansi.StylePrimitive{ 203 | Color: stringPtr("#50fa7b"), 204 | }, 205 | GenericStrong: ansi.StylePrimitive{ 206 | Color: stringPtr("#ffb86c"), 207 | Bold: boolPtr(true), 208 | }, 209 | GenericSubheading: ansi.StylePrimitive{ 210 | Color: stringPtr("#bd93f9"), 211 | }, 212 | Background: ansi.StylePrimitive{ 213 | BackgroundColor: stringPtr("#282a36"), 214 | }, 215 | }, 216 | }, 217 | Table: ansi.StyleTable{ 218 | StyleBlock: ansi.StyleBlock{ 219 | StylePrimitive: ansi.StylePrimitive{}, 220 | }, 221 | CenterSeparator: stringPtr("┼"), 222 | ColumnSeparator: stringPtr("│"), 223 | RowSeparator: stringPtr("─"), 224 | }, 225 | DefinitionDescription: ansi.StylePrimitive{ 226 | BlockPrefix: "\n🠶 ", 227 | }, 228 | } 229 | 230 | func boolPtr(b bool) *bool { return &b } 231 | func stringPtr(s string) *string { return &s } 232 | func uintPtr(u uint) *uint { return &u } 233 | -------------------------------------------------------------------------------- /internal/progress/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package progress 8 | 9 | import ( 10 | "github.com/konstructio/kubefirst-api/pkg/types" 11 | ) 12 | 13 | // Terminal model 14 | type progressModel struct { 15 | // Terminal 16 | error string 17 | isProvisioned bool 18 | 19 | header string 20 | 21 | // Provisioning fields 22 | clusterName string 23 | provisioningCluster types.Cluster 24 | completedSteps []string 25 | nextStep string 26 | successMessage string 27 | } 28 | 29 | // Bubbletea messages 30 | 31 | type CusterProvisioningMsg types.Cluster 32 | 33 | type startProvision struct { 34 | clusterName string 35 | } 36 | 37 | type addStep struct { 38 | message string 39 | } 40 | 41 | type completeStep struct { 42 | message string 43 | } 44 | 45 | type errorMsg struct { 46 | message string 47 | } 48 | 49 | type headerMsg struct { 50 | message string 51 | } 52 | 53 | type successMsg struct { 54 | message string 55 | } 56 | 57 | // Custom 58 | 59 | type ProvisionSteps struct { 60 | installToolsCheck string 61 | domainLivenessCheck string 62 | kbotSetupCheck string 63 | gitInitCheck string 64 | gitopsReadyCheck string 65 | gitTerraformApplyCheck string 66 | gitopsPushedCheck string 67 | cloudTerraformApplyCheck string 68 | clusterSecretsCreatedCheck string 69 | argoCDInstallCheck string 70 | argoCDInitializeCheck string 71 | vaultInitializedCheck string 72 | vaultTerraformApplyCheck string 73 | usersTerraformApplyCheck string 74 | } 75 | -------------------------------------------------------------------------------- /internal/provision/provision.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package provision 8 | 9 | import ( 10 | "context" 11 | "errors" 12 | "fmt" 13 | "os" 14 | "strings" 15 | "time" 16 | 17 | apiTypes "github.com/konstructio/kubefirst-api/pkg/types" 18 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 19 | "github.com/konstructio/kubefirst/internal/cluster" 20 | "github.com/konstructio/kubefirst/internal/gitShim" 21 | "github.com/konstructio/kubefirst/internal/launch" 22 | "github.com/konstructio/kubefirst/internal/progress" 23 | "github.com/konstructio/kubefirst/internal/step" 24 | "github.com/konstructio/kubefirst/internal/types" 25 | "github.com/konstructio/kubefirst/internal/utilities" 26 | "github.com/rs/zerolog/log" 27 | "github.com/spf13/viper" 28 | ) 29 | 30 | func CreateMgmtClusterRequest(gitAuth apiTypes.GitAuth, cliFlags types.CliFlags, catalogApps []apiTypes.GitopsCatalogApp) error { 31 | clusterRecord, err := utilities.CreateClusterDefinitionRecordFromRaw( 32 | gitAuth, 33 | cliFlags, 34 | catalogApps, 35 | ) 36 | if err != nil { 37 | return fmt.Errorf("error creating cluster definition record: %w", err) 38 | } 39 | 40 | clusterCreated, err := cluster.GetCluster(clusterRecord.ClusterName) 41 | if err != nil && !errors.Is(err, cluster.ErrNotFound) { 42 | log.Printf("error retrieving cluster %q: %v", clusterRecord.ClusterName, err) 43 | return fmt.Errorf("error retrieving cluster: %w", err) 44 | } 45 | 46 | if errors.Is(err, cluster.ErrNotFound) { 47 | if err := cluster.CreateCluster(*clusterRecord); err != nil { 48 | return fmt.Errorf("error creating cluster: %w", err) 49 | } 50 | } 51 | 52 | if clusterCreated.Status == "error" { 53 | cluster.ResetClusterProgress(clusterRecord.ClusterName) 54 | if err := cluster.CreateCluster(*clusterRecord); err != nil { 55 | return fmt.Errorf("error re-creating cluster after error state: %w", err) 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | type Provisioner struct { 63 | watcher *Watcher 64 | stepper step.Stepper 65 | } 66 | 67 | func NewProvisioner(watcher *Watcher, stepper step.Stepper) *Provisioner { 68 | return &Provisioner{ 69 | watcher: watcher, 70 | stepper: stepper, 71 | } 72 | } 73 | 74 | func (p *Provisioner) ProvisionManagementCluster(ctx context.Context, cliFlags *types.CliFlags, catalogApps []apiTypes.GitopsCatalogApp) error { 75 | p.stepper.NewProgressStep("Initialize Configuration") 76 | 77 | clusterSetupComplete := viper.GetBool("kubefirst-checks.cluster-install-complete") 78 | if clusterSetupComplete { 79 | p.stepper.InfoStep(step.EmojiCheck, "Cluster already successfully provisioned") 80 | 81 | return nil 82 | } 83 | 84 | utilities.CreateK1ClusterDirectory(cliFlags.ClusterName) 85 | 86 | p.stepper.NewProgressStep("Validate Git Credentials") 87 | 88 | gitAuth, err := gitShim.ValidateGitCredentials(cliFlags.GitProvider, cliFlags.GithubOrg, cliFlags.GitlabGroup) 89 | if err != nil { 90 | return fmt.Errorf("failed to validate git credentials: %w", err) 91 | } 92 | 93 | // Validate git 94 | executionControl := viper.GetBool(fmt.Sprintf("kubefirst-checks.%s-credentials", cliFlags.GitProvider)) 95 | if !executionControl { 96 | newRepositoryNames := []string{"gitops", "metaphor"} 97 | newTeamNames := []string{"admins", "developers"} 98 | 99 | initGitParameters := gitShim.GitInitParameters{ 100 | GitProvider: cliFlags.GitProvider, 101 | GitToken: gitAuth.Token, 102 | GitOwner: gitAuth.Owner, 103 | Repositories: newRepositoryNames, 104 | Teams: newTeamNames, 105 | } 106 | 107 | err = gitShim.InitializeGitProvider(&initGitParameters) 108 | if err != nil { 109 | return fmt.Errorf("failed to initialize Git provider: %w", err) 110 | } 111 | } 112 | viper.Set(fmt.Sprintf("kubefirst-checks.%s-credentials", cliFlags.GitProvider), true) 113 | if err = viper.WriteConfig(); err != nil { 114 | wrerr := fmt.Errorf("failed to write viper config: %w", err) 115 | p.stepper.FailCurrentStep(wrerr) 116 | return wrerr 117 | } 118 | 119 | p.stepper.NewProgressStep("Setup k3d Cluster") 120 | 121 | k3dClusterCreationComplete := viper.GetBool("launch.deployed") 122 | isK1Debug := strings.ToLower(os.Getenv("K1_LOCAL_DEBUG")) == "true" 123 | 124 | if !k3dClusterCreationComplete && !isK1Debug { 125 | if err := launch.Up(ctx, nil, true, cliFlags.UseTelemetry); err != nil { 126 | return fmt.Errorf("failed to launch k3d cluster: %w", err) 127 | } 128 | } 129 | 130 | err = utils.IsAppAvailable(fmt.Sprintf("%s/api/proxyHealth", cluster.GetConsoleIngressURL()), "kubefirst api") 131 | if err != nil { 132 | return fmt.Errorf("API availability check failed: %w", err) 133 | } 134 | 135 | p.stepper.NewProgressStep("Create Management Cluster") 136 | 137 | if err := CreateMgmtClusterRequest(gitAuth, *cliFlags, catalogApps); err != nil { 138 | return fmt.Errorf("failed to request management cluster creation: %w", err) 139 | } 140 | 141 | for !p.watcher.IsComplete() { 142 | p.stepper.NewProgressStep(p.watcher.GetCurrentStep()) 143 | if err := p.watcher.UpdateProvisionProgress(); err != nil { 144 | return fmt.Errorf("failed to provision management cluster: %w", err) 145 | } 146 | 147 | time.Sleep(5 * time.Second) 148 | } 149 | 150 | p.stepper.CompleteCurrentStep() 151 | 152 | p.stepper.InfoStep(step.EmojiTada, "Your kubefirst platform has been provisioned!") 153 | 154 | clusterInfo, err := cluster.GetCluster(cliFlags.ClusterName) 155 | if err != nil { 156 | return fmt.Errorf("failed to get management cluster: %w", err) 157 | } 158 | p.stepper.InfoStep(step.EmojiMagic, progress.RenderMessage(progress.DisplaySuccessMessage(clusterInfo))) 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /internal/provision/provisionWatcher.go: -------------------------------------------------------------------------------- 1 | package provision 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | apiTypes "github.com/konstructio/kubefirst-api/pkg/types" 8 | "github.com/konstructio/kubefirst/internal/cluster" 9 | ) 10 | 11 | const ( 12 | InstallToolsCheck = "Install Tools" 13 | DomainLivenessCheck = "Domain Liveness" 14 | KBotSetupCheck = "KBot Setup" 15 | GitInitCheck = "Git Init" 16 | GitOpsReadyCheck = "GitOps Ready" 17 | GitTerraformApplyCheck = "Git Terraform Apply" 18 | GitOpsPushedCheck = "GitOps Pushed" 19 | CloudTerraformApplyCheck = "Cloud Terraform Apply" 20 | ClusterSecretsCreatedCheck = "Cluster Secrets Created" 21 | ArgoCDInstallCheck = "ArgoCD Install" 22 | ArgoCDInitializeCheck = "ArgoCD Initialize" 23 | VaultInitializedCheck = "Vault Initialized" 24 | VaultTerraformApplyCheck = "Vault Terraform Apply" 25 | UsersTerraformApplyCheck = "Users Terraform Apply" 26 | FinalCheck = "Final Check" 27 | ProvisionComplete = "Provision Complete" 28 | ) 29 | 30 | type ClusterClient interface { 31 | GetCluster(clusterName string) (*apiTypes.Cluster, error) 32 | CreateCluster(cluster apiTypes.ClusterDefinition) error 33 | ResetClusterProgress(clusterName string) error 34 | } 35 | 36 | type Watcher struct { 37 | clusterName string 38 | installSteps []installStep 39 | client ClusterClient 40 | } 41 | 42 | type installStep struct { 43 | StepName string 44 | } 45 | 46 | func NewProvisionWatcher(clusterName string, client ClusterClient) *Watcher { 47 | return &Watcher{ 48 | clusterName: clusterName, 49 | installSteps: []installStep{ 50 | {StepName: InstallToolsCheck}, 51 | {StepName: DomainLivenessCheck}, 52 | {StepName: KBotSetupCheck}, 53 | {StepName: GitInitCheck}, 54 | {StepName: GitOpsReadyCheck}, 55 | {StepName: GitTerraformApplyCheck}, 56 | {StepName: GitOpsPushedCheck}, 57 | {StepName: CloudTerraformApplyCheck}, 58 | {StepName: ClusterSecretsCreatedCheck}, 59 | {StepName: ArgoCDInstallCheck}, 60 | {StepName: ArgoCDInitializeCheck}, 61 | {StepName: VaultInitializedCheck}, 62 | {StepName: VaultTerraformApplyCheck}, 63 | {StepName: UsersTerraformApplyCheck}, 64 | {StepName: FinalCheck}, 65 | }, 66 | client: client, 67 | } 68 | } 69 | 70 | func (c *Watcher) GetClusterName() string { 71 | return c.clusterName 72 | } 73 | 74 | func (c *Watcher) SetClusterName(clusterName string) { 75 | c.clusterName = clusterName 76 | } 77 | 78 | func (c *Watcher) IsComplete() bool { 79 | return len(c.installSteps) == 0 80 | } 81 | 82 | func (c *Watcher) GetCurrentStep() string { 83 | return c.installSteps[0].StepName 84 | } 85 | 86 | func (c *Watcher) popStep() string { 87 | if len(c.installSteps) == 0 { 88 | return ProvisionComplete 89 | } 90 | 91 | step := c.installSteps[0] 92 | c.installSteps = c.installSteps[1:] 93 | return step.StepName 94 | } 95 | 96 | func (c *Watcher) UpdateProvisionProgress() error { 97 | provisionedCluster, err := c.client.GetCluster(c.clusterName) 98 | if err != nil { 99 | if errors.Is(err, cluster.ErrNotFound) { 100 | return nil 101 | } 102 | 103 | return fmt.Errorf("error retrieving cluster %q: %w", c.clusterName, err) 104 | } 105 | 106 | if provisionedCluster.Status == "error" { 107 | return fmt.Errorf("cluster in error state: %s", provisionedCluster.LastCondition) 108 | } 109 | 110 | clusterStepStatus := c.mapClusterStepStatus(provisionedCluster) 111 | 112 | if clusterStepStatus[c.GetCurrentStep()] { 113 | c.popStep() 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (*Watcher) mapClusterStepStatus(provisionedCluster *apiTypes.Cluster) map[string]bool { 120 | clusterStepStatus := map[string]bool{ 121 | InstallToolsCheck: provisionedCluster.InstallToolsCheck, 122 | DomainLivenessCheck: provisionedCluster.DomainLivenessCheck, 123 | KBotSetupCheck: provisionedCluster.KbotSetupCheck, 124 | GitInitCheck: provisionedCluster.GitInitCheck, 125 | GitOpsReadyCheck: provisionedCluster.GitopsReadyCheck, 126 | GitTerraformApplyCheck: provisionedCluster.GitTerraformApplyCheck, 127 | GitOpsPushedCheck: provisionedCluster.GitopsPushedCheck, 128 | CloudTerraformApplyCheck: provisionedCluster.CloudTerraformApplyCheck, 129 | ClusterSecretsCreatedCheck: provisionedCluster.ClusterSecretsCreatedCheck, 130 | ArgoCDInstallCheck: provisionedCluster.ArgoCDInstallCheck, 131 | ArgoCDInitializeCheck: provisionedCluster.ArgoCDInitializeCheck, 132 | VaultInitializedCheck: provisionedCluster.VaultInitializedCheck, 133 | VaultTerraformApplyCheck: provisionedCluster.VaultTerraformApplyCheck, 134 | UsersTerraformApplyCheck: provisionedCluster.UsersTerraformApplyCheck, 135 | FinalCheck: provisionedCluster.FinalCheck, 136 | } 137 | return clusterStepStatus 138 | } 139 | -------------------------------------------------------------------------------- /internal/provision/provisionWatcher_test.go: -------------------------------------------------------------------------------- 1 | package provision 2 | 3 | import ( 4 | "testing" 5 | 6 | apiTypes "github.com/konstructio/kubefirst-api/pkg/types" 7 | "github.com/konstructio/kubefirst/internal/cluster" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type MockClusterClient struct { 13 | clusters map[string]apiTypes.Cluster 14 | } 15 | 16 | func (m *MockClusterClient) GetCluster(clusterName string) (*apiTypes.Cluster, error) { 17 | foundCluster, exists := m.clusters[clusterName] 18 | if !exists { 19 | return nil, cluster.ErrNotFound 20 | } 21 | return &foundCluster, nil 22 | } 23 | 24 | func (m *MockClusterClient) CreateCluster(cluster apiTypes.ClusterDefinition) error { 25 | return nil 26 | } 27 | 28 | func (m *MockClusterClient) ResetClusterProgress(clusterName string) error { 29 | return nil 30 | } 31 | 32 | func TestClusterProvision(t *testing.T) { 33 | t.Run("should have checks after initialized", func(t *testing.T) { 34 | client := &MockClusterClient{} 35 | cp := NewProvisionWatcher("test-cluster", client) 36 | 37 | assert.Equal(t, "test-cluster", cp.clusterName) 38 | assert.Equal(t, InstallToolsCheck, cp.installSteps[0].StepName) 39 | }) 40 | 41 | t.Run("should have InstallTools check first", func(t *testing.T) { 42 | client := &MockClusterClient{} 43 | cp := NewProvisionWatcher("test-cluster", client) 44 | 45 | require.Greater(t, len(cp.installSteps), 0) 46 | assert.Equal(t, InstallToolsCheck, cp.installSteps[0].StepName) 47 | }) 48 | 49 | t.Run("should be complete if there are no more install steps", func(t *testing.T) { 50 | client := &MockClusterClient{} 51 | cp := NewProvisionWatcher("test-cluster", client) 52 | 53 | assert.False(t, cp.IsComplete()) 54 | 55 | for range cp.installSteps { 56 | cp.popStep() 57 | } 58 | 59 | assert.True(t, cp.IsComplete()) 60 | }) 61 | 62 | t.Run("should continue to show complete if attempting to update the process", func(t *testing.T) { 63 | client := &MockClusterClient{} 64 | cp := NewProvisionWatcher("test-cluster", client) 65 | 66 | for range cp.installSteps { 67 | cp.popStep() 68 | } 69 | 70 | step := cp.popStep() 71 | assert.Equal(t, ProvisionComplete, step) 72 | }) 73 | 74 | t.Run("should move to the next step when popped", func(t *testing.T) { 75 | client := &MockClusterClient{} 76 | cp := NewProvisionWatcher("test-cluster", client) 77 | 78 | assert.Equal(t, InstallToolsCheck, cp.GetCurrentStep()) 79 | cp.popStep() 80 | assert.Equal(t, DomainLivenessCheck, cp.GetCurrentStep()) 81 | }) 82 | 83 | t.Run("should change the current step if the top is popped", func(t *testing.T) { 84 | client := &MockClusterClient{} 85 | cp := NewProvisionWatcher("test-cluster", client) 86 | 87 | step := cp.popStep() 88 | assert.Equal(t, InstallToolsCheck, step) 89 | assert.Equal(t, DomainLivenessCheck, cp.GetCurrentStep()) 90 | }) 91 | 92 | t.Run("should only update one step at a time even if all checks are complete", func(t *testing.T) { 93 | client := &MockClusterClient{ 94 | clusters: map[string]apiTypes.Cluster{ 95 | "test-cluster": { 96 | ClusterName: "test-cluster", 97 | InstallToolsCheck: true, 98 | DomainLivenessCheck: true, 99 | KbotSetupCheck: true, 100 | GitInitCheck: true, 101 | GitopsReadyCheck: true, 102 | GitTerraformApplyCheck: true, 103 | GitopsPushedCheck: true, 104 | CloudTerraformApplyCheck: true, 105 | ClusterSecretsCreatedCheck: true, 106 | ArgoCDInstallCheck: true, 107 | ArgoCDInitializeCheck: true, 108 | VaultInitializedCheck: true, 109 | VaultTerraformApplyCheck: true, 110 | UsersTerraformApplyCheck: true, 111 | }, 112 | }, 113 | } 114 | cp := NewProvisionWatcher("test-cluster", client) 115 | 116 | err := cp.UpdateProvisionProgress() 117 | assert.NoError(t, err) 118 | assert.Equal(t, DomainLivenessCheck, cp.GetCurrentStep()) 119 | }) 120 | 121 | t.Run("should return an error if the cluster is in an error state", func(t *testing.T) { 122 | client := &MockClusterClient{ 123 | clusters: map[string]apiTypes.Cluster{ 124 | "test-cluster": { 125 | ClusterName: "test-cluster", 126 | Status: "error", 127 | LastCondition: "some error", 128 | }, 129 | }, 130 | } 131 | cp := NewProvisionWatcher("test-cluster", client) 132 | 133 | err := cp.UpdateProvisionProgress() 134 | assert.Error(t, err) 135 | }) 136 | 137 | t.Run("should not return an error if the cluster isn't ready", func(t *testing.T) { 138 | client := &MockClusterClient{ 139 | clusters: map[string]apiTypes.Cluster{}, 140 | } 141 | cp := NewProvisionWatcher("test-cluster", client) 142 | 143 | err := cp.UpdateProvisionProgress() 144 | assert.NoError(t, err) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /internal/provisionLogs/command.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package provisionLogs //nolint:revive // allowed during refactoring 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "strings" 13 | "time" 14 | 15 | "github.com/muesli/termenv" 16 | ) 17 | 18 | type Log struct { 19 | Level string `bson:"level" json:"level"` 20 | Time string `bson:"time" json:"time"` 21 | Message string `bson:"message" json:"message"` 22 | } 23 | 24 | var ( 25 | color = termenv.EnvColorProfile().Color 26 | infoStyle = termenv.Style{}.Foreground(color("27")).Styled 27 | errorStyle = termenv.Style{}.Foreground(color("196")).Styled 28 | timeStyle = termenv.Style{}.Foreground(color("245")).Bold().Styled 29 | textStyle = termenv.Style{}.Foreground(color("15")).Styled 30 | ) 31 | 32 | func AddLog(logMsg string) { 33 | var log Log 34 | var formatterMsg string 35 | 36 | err := json.Unmarshal([]byte(logMsg), &log) 37 | if err != nil { 38 | formatterMsg = textStyle(logMsg) 39 | } else { 40 | parsedTime, err := time.Parse(time.RFC3339, log.Time) 41 | if err != nil { 42 | fmt.Printf("error parsing date: %v\n", err) 43 | return 44 | } 45 | 46 | formattedDateStr := parsedTime.Format("2006-01-02 15:04:05") 47 | 48 | timeLog := timeStyle(formattedDateStr) 49 | level := infoStyle(strings.ToUpper(log.Level)) 50 | 51 | if log.Level == "error" { 52 | level = errorStyle(strings.ToUpper(log.Level)) 53 | } 54 | 55 | message := textStyle(log.Message) 56 | 57 | formatterMsg = fmt.Sprintf("%s %s: %s", timeLog, level, message) 58 | } 59 | 60 | renderedMessage := formatterMsg 61 | 62 | ProvisionLogs.Send(logMessage{message: renderedMessage}) 63 | } 64 | -------------------------------------------------------------------------------- /internal/provisionLogs/provisionLogs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package provisionLogs //nolint:revive // allowed during refactoring 8 | 9 | import ( 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | ) 13 | 14 | var quitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Render 15 | 16 | var ProvisionLogs *tea.Program 17 | 18 | //nolint:revive // will be removed after refactoring 19 | func NewModel() provisionLogsModel { 20 | return provisionLogsModel{} 21 | } 22 | 23 | // Bubbletea functions 24 | func InitializeProvisionLogsTerminal() { 25 | ProvisionLogs = tea.NewProgram(NewModel()) 26 | } 27 | 28 | func (m provisionLogsModel) Init() tea.Cmd { 29 | return nil 30 | } 31 | 32 | func (m provisionLogsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 33 | switch msg := msg.(type) { 34 | case tea.KeyMsg: 35 | switch msg.String() { 36 | case "ctrl+c": 37 | return m, tea.Quit 38 | default: 39 | return m, nil 40 | } 41 | 42 | case logMessage: 43 | m.logs = append(m.logs, msg.message) 44 | return m, nil 45 | 46 | default: 47 | return m, nil 48 | } 49 | } 50 | 51 | func (m provisionLogsModel) View() string { 52 | logs := "" 53 | for i := 0; i < len(m.logs); i++ { 54 | logs += m.logs[i] + "\n" 55 | } 56 | 57 | return logs + "\n" + quitStyle("ctrl+c to quit") + "\n" 58 | } 59 | -------------------------------------------------------------------------------- /internal/provisionLogs/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package provisionLogs //nolint:revive // allowed during refactoring 8 | 9 | // Terminal model 10 | type provisionLogsModel struct { 11 | logs []string 12 | } 13 | 14 | // Bubbletea messages 15 | type logMessage struct { 16 | message string 17 | } 18 | -------------------------------------------------------------------------------- /internal/segment/segment.go: -------------------------------------------------------------------------------- 1 | package segment 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/denisbrodbeck/machineid" 8 | "github.com/konstructio/kubefirst-api/pkg/configs" 9 | "github.com/konstructio/kubefirst-api/pkg/k3d" 10 | "github.com/kubefirst/metrics-client/pkg/telemetry" 11 | ) 12 | 13 | const ( 14 | kubefirstClient string = "api" 15 | ) 16 | 17 | func InitClient(clusterID, clusterType, gitProvider string) (telemetry.TelemetryEvent, error) { 18 | machineID, err := machineid.ID() 19 | if err != nil { 20 | return telemetry.TelemetryEvent{}, fmt.Errorf("failed to get machine ID: %w", err) 21 | } 22 | 23 | c := telemetry.TelemetryEvent{ 24 | CliVersion: configs.K1Version, 25 | CloudProvider: k3d.CloudProvider, 26 | ClusterID: clusterID, 27 | ClusterType: clusterType, 28 | DomainName: k3d.DomainName, 29 | GitProvider: gitProvider, 30 | InstallMethod: "kubefirst-launch", 31 | KubefirstClient: kubefirstClient, 32 | KubefirstTeam: os.Getenv("KUBEFIRST_TEAM"), 33 | KubefirstTeamInfo: os.Getenv("KUBEFIRST_TEAM_INFO"), 34 | MachineID: machineID, 35 | ErrorMessage: "", 36 | MetricName: telemetry.ClusterInstallCompleted, 37 | UserId: machineID, 38 | } 39 | 40 | return c, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/step/stepper.go: -------------------------------------------------------------------------------- 1 | package step 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/konstructio/cli-utils/stepper" 8 | ) 9 | 10 | const ( 11 | EmojiCheck = "✅" 12 | EmojiError = "🔴" 13 | EmojiMagic = "✨" 14 | EmojiHead = "🤕" 15 | EmojiNoEntry = "⛔" 16 | EmojiTada = "🎉" 17 | EmojiAlarm = "⏰" 18 | EmojiBug = "🐛" 19 | EmojiBulb = "💡" 20 | EmojiWarning = "⚠️" 21 | EmojiWrench = "🔧" 22 | EmojiBook = "📘" 23 | ) 24 | 25 | type Stepper interface { 26 | NewProgressStep(stepName string) 27 | FailCurrentStep(err error) 28 | CompleteCurrentStep() 29 | InfoStep(emoji, message string) 30 | InfoStepString(message string) 31 | DisplayLogHints(cloudProvider string, estimatedTime int) 32 | } 33 | 34 | type Factory struct { 35 | writer io.Writer 36 | currentStep *stepper.Step 37 | } 38 | 39 | func NewStepFactory(writer io.Writer) *Factory { 40 | return &Factory{writer: writer} 41 | } 42 | 43 | func (s *Factory) NewProgressStep(stepName string) { 44 | if s.currentStep == nil { 45 | s.currentStep = stepper.New(s.writer, stepName) 46 | } else if s.currentStep != nil && s.currentStep.GetName() != stepName { 47 | s.currentStep.Complete(nil) 48 | s.currentStep = stepper.New(s.writer, stepName) 49 | } 50 | } 51 | 52 | func (s *Factory) FailCurrentStep(err error) { 53 | s.currentStep.Complete(err) 54 | } 55 | 56 | func (s *Factory) CompleteCurrentStep() { 57 | s.currentStep.Complete(nil) 58 | } 59 | 60 | func (s *Factory) GetCurrentStep() string { 61 | return s.currentStep.GetName() 62 | } 63 | 64 | func (s *Factory) InfoStep(emoji, message string) { 65 | fmt.Fprintf(s.writer, "%s %s\n", emoji, message) 66 | } 67 | 68 | func (s *Factory) InfoStepString(message string) { 69 | fmt.Fprintf(s.writer, "%s\n", message) 70 | } 71 | 72 | func (s *Factory) DisplayLogHints(cloudProvider string, estimatedTime int) { 73 | documentationLink := "https://kubefirst.konstruct.io/docs/" 74 | 75 | if cloudProvider != "" { 76 | documentationLink += cloudProvider + `/quick-start/install/cli` 77 | } 78 | 79 | header := "\n Welcome to Kubefirst \n\n" 80 | 81 | verboseLogs := fmt.Sprintf("%s To view verbose logs run below command in new terminal: \"kubefirst logs\"\n%s Documentation: %s\n\n", EmojiBulb, EmojiBook, documentationLink) 82 | 83 | estimatedTimeMsg := fmt.Sprintf("%s Estimated time: %d minutes\n\n", EmojiAlarm, estimatedTime) 84 | 85 | s.InfoStepString(fmt.Sprintf("%s%s%s", header, verboseLogs, estimatedTimeMsg)) 86 | } 87 | -------------------------------------------------------------------------------- /internal/step/stepper_test.go: -------------------------------------------------------------------------------- 1 | package step 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestStepFactory_NewStep(t *testing.T) { 15 | t.Run("creates new step with name", func(t *testing.T) { 16 | stepName := "test step" 17 | buf := &bytes.Buffer{} 18 | sf := NewStepFactory(buf) 19 | 20 | sf.NewProgressStep(stepName) 21 | 22 | assert.Equal(t, stepName, sf.GetCurrentStep()) 23 | }) 24 | 25 | t.Run("should create new step if provided step name", func(t *testing.T) { 26 | stepName := "test step" 27 | buf := &bytes.Buffer{} 28 | sf := NewStepFactory(buf) 29 | 30 | sf.NewProgressStep(stepName) 31 | 32 | assert.Equal(t, stepName, sf.GetCurrentStep()) 33 | 34 | newStepName := "another step" 35 | sf.NewProgressStep(newStepName) 36 | 37 | assert.Equal(t, newStepName, sf.GetCurrentStep()) 38 | 39 | assert.Eventually(t, func() bool { return assert.Contains(t, buf.String(), stepName) }, 1*time.Second, 100*time.Millisecond) 40 | assert.Eventually(t, func() bool { return assert.Contains(t, buf.String(), newStepName) }, 1*time.Second, 100*time.Millisecond) 41 | }) 42 | 43 | t.Run("should not change current step if provided same name", func(t *testing.T) { 44 | stepName := "test step" 45 | buf := &bytes.Buffer{} 46 | sf := NewStepFactory(buf) 47 | 48 | sf.NewProgressStep(stepName) 49 | 50 | assert.Equal(t, stepName, sf.GetCurrentStep()) 51 | 52 | sf.NewProgressStep(stepName) 53 | 54 | assert.Equal(t, stepName, sf.GetCurrentStep()) 55 | }) 56 | } 57 | 58 | func TestStepFactory_FailCurrentStep(t *testing.T) { 59 | t.Run("should fail current step", func(t *testing.T) { 60 | stepName := "test step" 61 | errorMessage := "test error" 62 | buf := &bytes.Buffer{} 63 | sf := NewStepFactory(buf) 64 | 65 | sf.NewProgressStep(stepName) 66 | 67 | sf.FailCurrentStep(fmt.Errorf("%s", errorMessage)) 68 | 69 | assert.Contains(t, buf.String(), errorMessage) 70 | }) 71 | } 72 | 73 | func TestStepFactory_CompleteCurrentStep(t *testing.T) { 74 | t.Run("should complete current step", func(t *testing.T) { 75 | stepName := "test step" 76 | buf := &bytes.Buffer{} 77 | sf := NewStepFactory(buf) 78 | 79 | sf.NewProgressStep(stepName) 80 | 81 | sf.CompleteCurrentStep() 82 | 83 | assert.Eventually(t, func() bool { return assert.Contains(t, buf.String(), EmojiCheck) }, 1*time.Second, 100*time.Millisecond) 84 | }) 85 | } 86 | 87 | func TestStepFactory_DisplayLogHints(t *testing.T) { 88 | type fields struct { 89 | writer io.Writer 90 | } 91 | type args struct { 92 | cloudProvider string 93 | estimatedTime int 94 | } 95 | tests := []struct { 96 | name string 97 | fields fields 98 | args args 99 | }{ 100 | { 101 | name: "displays log hints without blowing up", 102 | fields: fields{ 103 | writer: os.Stderr, 104 | }, 105 | args: args{ 106 | cloudProvider: "test", 107 | estimatedTime: 10, 108 | }, 109 | }, 110 | } 111 | for _, tt := range tests { 112 | t.Run(tt.name, func(t *testing.T) { 113 | sf := &Factory{ 114 | writer: tt.fields.writer, 115 | } 116 | sf.DisplayLogHints(tt.args.cloudProvider, tt.args.estimatedTime) 117 | 118 | // no assertions, just make sure it doesn't blow up 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /internal/types/flags.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package types 8 | 9 | type CliFlags struct { 10 | AlertsEmail string 11 | Ci bool 12 | CloudRegion string 13 | CloudProvider string 14 | ClusterName string 15 | ClusterType string 16 | DNSProvider string 17 | DNSAzureRG string 18 | DomainName string 19 | SubDomainName string 20 | GitProvider string 21 | GitProtocol string 22 | GithubOrg string 23 | GitlabGroup string 24 | GitopsTemplateBranch string 25 | GitopsTemplateURL string 26 | GoogleProject string 27 | UseTelemetry bool 28 | ECR bool 29 | NodeType string 30 | NodeCount string 31 | InstallCatalogApps string 32 | K3sSSHUser string 33 | K3sSSHPrivateKey string 34 | K3sServersPrivateIPs []string 35 | K3sServersPublicIPs []string 36 | K3sServersArgs []string 37 | InstallKubefirstPro bool 38 | AMIType string 39 | } 40 | -------------------------------------------------------------------------------- /internal/types/proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package types 8 | 9 | import ( 10 | apiTypes "github.com/konstructio/kubefirst-api/pkg/types" 11 | ) 12 | 13 | type ProxyCreateClusterRequest struct { 14 | Body apiTypes.ClusterDefinition `bson:"body" json:"body"` 15 | URL string `bson:"url" json:"url"` 16 | } 17 | 18 | type ProxyResetClusterRequest struct { 19 | URL string `bson:"url" json:"url"` 20 | } 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2021-2023, Kubefirst 3 | 4 | This program is licensed under MIT. 5 | See the LICENSE file for more details. 6 | */ 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | stdLog "log" 12 | "os" 13 | "time" 14 | 15 | "github.com/konstructio/kubefirst-api/pkg/configs" 16 | utils "github.com/konstructio/kubefirst-api/pkg/utils" 17 | "github.com/konstructio/kubefirst/cmd" 18 | "github.com/konstructio/kubefirst/internal/progress" 19 | zeroLog "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | "github.com/spf13/viper" 22 | "golang.org/x/exp/slices" 23 | ) 24 | 25 | func main() { 26 | argsWithProg := os.Args 27 | 28 | bubbleTeaAllowlist := []string{"k3d"} 29 | needsBubbleTea := false 30 | 31 | for _, arg := range argsWithProg { 32 | if slices.Contains(bubbleTeaAllowlist, arg) { 33 | needsBubbleTea = true 34 | } 35 | } 36 | 37 | config, err := configs.ReadConfig() 38 | if err != nil { 39 | log.Error().Msgf("failed to read config: %v", err) 40 | return 41 | } 42 | 43 | if err := utils.SetupViper(config, true); err != nil { 44 | log.Error().Msgf("failed to setup Viper: %v", err) 45 | return 46 | } 47 | 48 | now := time.Now() 49 | epoch := now.Unix() 50 | logfileName := fmt.Sprintf("log_%d.log", epoch) 51 | 52 | isProvision := slices.Contains(argsWithProg, "create") 53 | isLogs := slices.Contains(argsWithProg, "logs") 54 | 55 | // don't create a new log file for logs, using the previous one 56 | if isLogs { 57 | logfileName = viper.GetString("k1-paths.log-file-name") 58 | } 59 | 60 | // use cluster name as filename 61 | if isProvision { 62 | clusterName := fmt.Sprint(epoch) 63 | for i := 1; i < len(os.Args); i++ { 64 | arg := os.Args[i] 65 | 66 | // Check if the argument is "--cluster-name" 67 | if arg == "--cluster-name" && i+1 < len(os.Args) { 68 | // Get the value of the cluster name 69 | clusterName = os.Args[i+1] 70 | break 71 | } 72 | } 73 | 74 | logfileName = fmt.Sprintf("log_%s.log", clusterName) 75 | } 76 | 77 | homePath, err := os.UserHomeDir() 78 | if err != nil { 79 | log.Error().Msgf("failed to get user home directory: %v", err) 80 | return 81 | } 82 | 83 | k1Dir := fmt.Sprintf("%s/.k1", homePath) 84 | 85 | // * create k1Dir if it doesn't exist 86 | if _, err := os.Stat(k1Dir); os.IsNotExist(err) { 87 | if err := os.MkdirAll(k1Dir, os.ModePerm); err != nil { 88 | log.Error().Msgf("error creating directory %q: %v", k1Dir, err) 89 | return 90 | } 91 | } 92 | 93 | // * create log directory if it doesn't exist 94 | logsFolder := fmt.Sprintf("%s/logs", k1Dir) 95 | if _, err := os.Stat(logsFolder); os.IsNotExist(err) { 96 | if err := os.Mkdir(logsFolder, 0o700); err != nil { 97 | log.Error().Msgf("error creating logs directory: %v", err) 98 | return 99 | } 100 | } 101 | 102 | // * create session log file 103 | logfile := fmt.Sprintf("%s/%s", logsFolder, logfileName) 104 | logFileObj, err := utils.OpenLogFile(logfile) 105 | if err != nil { 106 | log.Error().Msgf("unable to store log location, error is: %v - please verify the current user has write access to this directory", err) 107 | return 108 | } 109 | 110 | // handle file close request 111 | defer func(logFileObj *os.File) { 112 | if err := logFileObj.Close(); err != nil { 113 | log.Error().Msgf("error closing log file: %v", err) 114 | } 115 | }(logFileObj) 116 | 117 | // setup default logging 118 | // this Go standard log is active to keep compatibility with current code base 119 | stdLog.SetOutput(logFileObj) 120 | stdLog.SetPrefix("LOG: ") 121 | stdLog.SetFlags(stdLog.Ldate) 122 | 123 | log.Logger = zeroLog.New(logFileObj).With().Timestamp().Logger() 124 | 125 | viper.Set("k1-paths.logs-dir", logsFolder) 126 | viper.Set("k1-paths.log-file", logfile) 127 | viper.Set("k1-paths.log-file-name", logfileName) 128 | 129 | if err := viper.WriteConfig(); err != nil { 130 | log.Error().Msgf("failed to write config: %v", err) 131 | return 132 | } 133 | 134 | if needsBubbleTea { 135 | progress.InitializeProgressTerminal() 136 | 137 | go func() { 138 | cmd.Execute() 139 | }() 140 | 141 | progress.Progress.Run() 142 | } else { 143 | cmd.Execute() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tools/aws-assume-role.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # This script helps you assume the AssumedAdmin role you either created manually or using the Terraform plan from ws-create-role.tf 5 | # 6 | # Requirement: aws-cli installed (see https://github.com/aws/aws-cli) 7 | # 8 | # Replace the AWS account ID `111111111111` in the `ROLE` variable with yours. If you give the admin a different name than `AssumedAdmin`, please update it also. 9 | # 10 | # Ensure that the default values fit your needs (i.e., role session name, duration of assume role...) 11 | # 12 | # Before running the script, ensure you have credentials. configure with the AWS CLI. To do so, run 13 | # aws configure 14 | # 15 | # To run this script 16 | # ./aws-assume-role.sh 17 | # 18 | 19 | # 20 | # Change the AWS account ID & role name 21 | # 22 | ROLE="arn:aws:iam::111111111111:role/AssumedAdmin" 23 | 24 | # 25 | # You can leave the rest of thre script as is 26 | # 27 | 28 | # An identifier for the assumed role session: you can change it if you want. 29 | ROLE_SESSION_NAME="AssumedAdmin-kubefirst" 30 | 31 | # Colors for formatting 32 | YELLOW="\033[1;93m" 33 | NOFORMAT="\033[0m" 34 | BOLD="\033[1m" 35 | 36 | # Backup the previous credentials 37 | if [ -f "~/.aws/credentials" ] 38 | then 39 | mv ~/.aws/credentials ~/.aws/credentials.bak 40 | fi 41 | 42 | # Unset previously set AWS access environment variables 43 | unset AWS_ACCESS_KEY_ID 44 | unset AWS_SECRET_ACCESS_KEY 45 | unset AWS_SESSION_TOKEN 46 | 47 | # Retrieving the connected user 48 | USER=$(aws sts get-caller-identity | jq -r .Arn | cut -d'/' -f 2) 49 | 50 | if [ $(echo "$USER" | grep -v "Unable to locate credentials") ] 51 | then 52 | # Assuming the new role for 12 hours. You can change the `--duration-seconds` to shorter timeout for security reason. 53 | JSON=$(aws sts assume-role --role-arn "${ROLE}" --role-session-name "${ROLE_SESSION_NAME}" --duration-seconds 43200) 54 | export AWS_ACCESS_KEY_ID=$(echo $JSON | jq -r .Credentials.AccessKeyId) 55 | export AWS_SECRET_ACCESS_KEY=$(echo $JSON | jq -r .Credentials.SecretAccessKey) 56 | export AWS_SESSION_TOKEN=$(echo $JSON | jq -r .Credentials.SessionToken) 57 | unset JSON 58 | 59 | # Display useful information for UI installation 60 | echo -e "\n${YELLOW}Started session for user ${NOFORMAT}${BOLD}${USER}${NOFORMAT}${YELLOW} assuming ${NOFORMAT}${BOLD}${ROLE}${NOFORMAT}\n" 61 | echo -e "${BOLD}AWS_ACCESS_KEY_ID: ${NOFORMAT} ${AWS_ACCESS_KEY_ID}" 62 | echo -e "${BOLD}AWS_SECRET_ACCESS_KEY:${NOFORMAT} ${AWS_SECRET_ACCESS_KEY}" 63 | echo -e "${BOLD}AWS_SESSION_TOKEN: ${NOFORMAT} ${AWS_SESSION_TOKEN}" 64 | else 65 | # The script wasn't successful 66 | exit 1 67 | fi 68 | -------------------------------------------------------------------------------- /tools/aws-create-role.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Terraform plan to create the administrator role that will be assumed 3 | # 4 | # Please read the comment within the file (not just this one) carefully to prevent any security issues within your organization! 5 | # 6 | # Replace the AWS account ID `111111111111` with yours. 7 | # 8 | # Ensure that the default values fit your needs (i.e., AWS region, role permission...) 9 | # 10 | # To run this plan: 11 | # terraform init 12 | # terraform apply 13 | # 14 | 15 | terraform { 16 | required_providers { 17 | aws = { 18 | source = "hashicorp/aws" 19 | version = "4.67.0" 20 | } 21 | } 22 | } 23 | 24 | provider "aws" { 25 | region = "us-east-1" 26 | } 27 | 28 | resource "aws_iam_role" "assumed_admin" { 29 | 30 | # The role name 31 | name = "KubernetesAdmin" 32 | 33 | # The default session time is 1 hour, this set it to 12 hours for convenience. It's less annoying, but less secure, feel free to remove or change! 34 | max_session_duration = 43200 35 | 36 | # 37 | # Below is a permissive role not intended for long-term use. 38 | # 39 | # It grants all IAM users of the AWS account the ability to assume the role `KubernetesAdmin`, which we created and give the `AdministratorAccess` policy. 40 | # 41 | # The value `:root` grants assume to the whole account but you can replace it with your individual IAM ARN, or your role if appropriate. 42 | # 43 | # As a reminder, the value `111111111111` below should be replaced with your AWS account ID. 44 | # 45 | # Anyone with IAM can assume the role while it's in place like this. You can scope it down to your specific user, or across accounts, or whatever you need. 46 | # 47 | assume_role_policy = <