├── .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 | 
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 | [](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 = <