├── .github ├── codecov.yaml ├── pr-title-checker-config.json └── workflows │ ├── ci.yaml │ ├── license-checker.yaml │ ├── pr-title-checker.yaml │ └── release.yaml ├── .gitignore ├── .golangci.yaml ├── .typos.toml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gtctl │ ├── cluster.go │ ├── cluster_connect.go │ ├── cluster_create.go │ ├── cluster_delete.go │ ├── cluster_get.go │ ├── cluster_list.go │ ├── cluster_scale.go │ ├── main.go │ ├── playground.go │ └── version.go ├── docs └── images │ └── screenshot.png ├── examples └── bare-metal │ ├── cluster-with-local-artifacts.yaml │ ├── cluster-with-s3-storage.datanode.toml │ ├── cluster-with-s3-storage.yaml │ └── cluster.yaml ├── go.mod ├── go.sum ├── hack ├── e2e │ └── setup-e2e-env.sh ├── install.sh └── version.sh ├── licenserc.toml ├── pkg ├── artifacts │ ├── constants.go │ ├── manager.go │ └── manager_test.go ├── cluster │ ├── baremetal │ │ ├── cluster.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── get.go │ │ ├── get_test.go │ │ ├── not_implemented.go │ │ └── testdata │ │ │ └── pids │ │ │ ├── a │ │ │ └── pid │ │ │ ├── b │ │ │ └── pid │ │ │ └── c │ │ │ └── pid │ ├── kubernetes │ │ ├── cluster.go │ │ ├── connect.go │ │ ├── create.go │ │ ├── delete.go │ │ ├── get.go │ │ ├── list.go │ │ └── scale.go │ └── types.go ├── components │ ├── datanode.go │ ├── etcd.go │ ├── frontend.go │ ├── metasrv.go │ ├── run.go │ ├── types.go │ └── utils.go ├── config │ ├── baremetal.go │ ├── kubernetes.go │ ├── kubernetes_test.go │ ├── testdata │ │ └── validate │ │ │ ├── invalid_artifact.yaml │ │ │ ├── invalid_hostname_port.yaml │ │ │ ├── invalid_replicas.yaml │ │ │ └── valid_config.yaml │ ├── validate.go │ └── validate_test.go ├── connector │ ├── mysql.go │ └── postgres.go ├── helm │ ├── loader.go │ ├── loader_test.go │ ├── testdata │ │ ├── db-manifests.yaml │ │ ├── db-values.yaml │ │ ├── merged-values.yaml │ │ └── values.yaml │ ├── values.go │ └── values_test.go ├── kube │ └── client.go ├── logger │ └── logger.go ├── metadata │ ├── manager.go │ └── manager_test.go ├── plugins │ ├── manager.go │ ├── manager_test.go │ └── testdata │ │ └── gtctl-foo-plugin ├── status │ └── status.go ├── utils │ ├── file │ │ ├── file.go │ │ ├── file_test.go │ │ ├── testdata │ │ │ ├── test-tar-gz.tar.gz │ │ │ ├── test-tgz.tgz │ │ │ └── test-zip.zip │ │ ├── yaml.go │ │ └── yaml_test.go │ └── semver │ │ ├── semver.go │ │ └── semver_test.go └── version │ └── version.go └── tests └── e2e ├── greptimedbcluster_test.go └── suite_test.go /.github/codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: on 4 | patch: on 5 | -------------------------------------------------------------------------------- /.github/pr-title-checker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LABEL": { 3 | "name": "Invalid PR Title", 4 | "color": "B60205" 5 | }, 6 | "CHECKS": { 7 | "regexp": "^(feat|fix|test|refactor|chore|style|docs|perf|build|ci|revert)(\\(.*\\))?:.*", 8 | "ignoreLabels" : ["ignore-title"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | GO_VERSION: "1.21" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | typos-check: 13 | name: Check typos 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: crate-ci/typos@master 18 | 19 | lint: 20 | name: Check linters 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version: ${{ env.GO_VERSION }} 27 | - name: golangci-lint 28 | uses: golangci/golangci-lint-action@v3 29 | with: 30 | version: v1.54 31 | # '-v' flag is required to show the output of golangci-lint. 32 | args: -v 33 | 34 | unit-test: 35 | name: Unit test coverage 36 | runs-on: ubuntu-latest 37 | needs: [ lint ] 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-go@v5 41 | with: 42 | go-version: ${{ env.GO_VERSION }} 43 | - name: Unit test 44 | run: make coverage 45 | - name: Upload coverage to Codecov 46 | uses: codecov/codecov-action@v4 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | fail_ci_if_error: true 50 | files: ./coverage.xml 51 | name: codecov-gtctl 52 | verbose: true 53 | e2e: 54 | name: End to End tests 55 | runs-on: ubuntu-latest 56 | needs: [ lint ] 57 | steps: 58 | - uses: actions/checkout@v4 59 | - uses: actions/setup-go@v5 60 | with: 61 | go-version: ${{ env.GO_VERSION }} 62 | - name: Run End to End tests 63 | run: make e2e 64 | -------------------------------------------------------------------------------- /.github/workflows/license-checker.yaml: -------------------------------------------------------------------------------- 1 | name: License Checker 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | jobs: 10 | license-header-check: 11 | runs-on: ubuntu-latest 12 | name: license-header-check 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Check License Header 16 | uses: korandoru/hawkeye@v5 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-checker.yaml: -------------------------------------------------------------------------------- 1 | name: PR Title Checker 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | - labeled 10 | - unlabeled 11 | 12 | jobs: 13 | check: 14 | name: pr-title-check 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: thehanimo/pr-title-checker@v1.4.0 18 | with: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | pass_on_octokit_error: false 21 | configuration_path: ".github/pr-title-checker-config.json" 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | inputs: 9 | release-install-script: 10 | type: boolean 11 | description: "Release install script to AWS-CN S3 bucket" 12 | required: false 13 | default: true 14 | 15 | env: 16 | GO_VERSION: "1.21" 17 | MAX_UPLOAD_RETRY_TIMES: 20 18 | UPLOAD_RETRY_TIMEOUT: 10 # minutes 19 | 20 | jobs: 21 | build: 22 | name: build-binary 23 | if: ${{ github.event_name == 'push' }} 24 | strategy: 25 | matrix: 26 | # The file format is gtctl-- 27 | include: 28 | - os: ubuntu-latest 29 | file: gtctl-linux-amd64 30 | goos: linux 31 | goarch: amd64 32 | - os: ubuntu-latest 33 | file: gtctl-linux-arm64 34 | goos: linux 35 | goarch: arm64 36 | - os: macos-latest 37 | file: gtctl-darwin-arm64 38 | goos: darwin 39 | goarch: arm64 40 | - os: macos-latest 41 | file: gtctl-darwin-amd64 42 | goos: darwin 43 | goarch: amd64 44 | runs-on: ${{ matrix.os }} 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/setup-go@v5 48 | with: 49 | go-version: ${{ env.GO_VERSION }} 50 | - name: Build project 51 | run: make 52 | env: 53 | GOOS: ${{ matrix.goos }} 54 | GOARCH: ${{ matrix.goarch }} 55 | 56 | - name: Calculate checksum and rename binary 57 | shell: bash 58 | run: | 59 | cd bin 60 | chmod +x gtctl 61 | tar -zcvf ${{ matrix.file }}.tgz gtctl 62 | echo $(shasum -a 256 ${{ matrix.file }}.tgz | cut -f1 -d' ') > ${{ matrix.file }}.sha256sum 63 | 64 | - name: Upload artifacts 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: ${{ matrix.file }} 68 | path: bin/${{ matrix.file }}.tgz 69 | - name: Upload checksum of artifacts 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: ${{ matrix.file }}.sha256sum 73 | path: bin/${{ matrix.file }}.sha256sum 74 | - name: Upload artifacts to S3 75 | uses: nick-invision/retry@v3 76 | env: 77 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_CN_ACCESS_KEY_ID }} 78 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_CN_SECRET_ACCESS_KEY}} 79 | AWS_DEFAULT_REGION: ${{ vars.AWS_CN_RELEASE_BUCKET_REGION }} 80 | with: 81 | max_attempts: ${{ env.MAX_UPLOAD_RETRY_TIMES }} 82 | timeout_minutes: ${{ env.UPLOAD_RETRY_TIMEOUT }} 83 | command: | 84 | aws s3 cp \ 85 | bin/${{ matrix.file }}.tgz \ 86 | s3://${{ vars.AWS_CN_RELEASE_BUCKET }}/releases/gtctl/${{ github.ref_name }}/${{ matrix.file }}.tgz && \ 87 | aws s3 cp \ 88 | bin/${{ matrix.file }}.sha256sum \ 89 | s3://${{ vars.AWS_CN_RELEASE_BUCKET }}/releases/gtctl/${{ github.ref_name }}/${{ matrix.file }}.sha256sum && \ 90 | aws s3 cp \ 91 | bin/${{ matrix.file }}.tgz \ 92 | s3://${{ vars.AWS_CN_RELEASE_BUCKET }}/releases/gtctl/latest/${{ matrix.file }}.tgz && \ 93 | aws s3 cp \ 94 | bin/${{ matrix.file }}.sha256sum \ 95 | s3://${{ vars.AWS_CN_RELEASE_BUCKET }}/releases/gtctl/latest/${{ matrix.file }}.sha256sum 96 | 97 | release: 98 | name: release-artifacts 99 | needs: [ build ] 100 | runs-on: ubuntu-latest 101 | steps: 102 | - name: Checkout sources 103 | uses: actions/checkout@v4 104 | 105 | - name: Download artifacts 106 | uses: actions/download-artifact@v4 107 | 108 | - name: Publish release 109 | uses: ncipollo/release-action@v1 110 | with: 111 | name: "Release ${{ github.ref_name }}" 112 | prerelease: false 113 | make_release: true 114 | generateReleaseNotes: true 115 | allowUpdates: true 116 | tag: ${{ github.ref_name }} 117 | artifacts: | 118 | **/gtctl-* 119 | 120 | release-install-script: 121 | name: release-install-script 122 | if: ${{ inputs.release-install-script }} 123 | runs-on: ubuntu-latest 124 | steps: 125 | - uses: actions/checkout@v4 126 | - name: Upload install.sh to S3 127 | uses: nick-invision/retry@v4 128 | env: 129 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_CN_ACCESS_KEY_ID }} 130 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_CN_SECRET_ACCESS_KEY}} 131 | AWS_DEFAULT_REGION: ${{ vars.AWS_CN_RELEASE_BUCKET_REGION }} 132 | with: 133 | max_attempts: ${{ env.MAX_UPLOAD_RETRY_TIMES }} 134 | timeout_minutes: ${{ env.UPLOAD_RETRY_TIMEOUT }} 135 | command: | 136 | aws s3 cp hack/install.sh s3://${{ vars.AWS_CN_RELEASE_BUCKET }}/releases/scripts/gtctl/install.sh 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | testbin/* 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | .DS_Store 23 | *.swp 24 | *.swo 25 | *~ 26 | .vscode 27 | 28 | .gtctl 29 | coverage.xml 30 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # Options for analysis running. 2 | run: 3 | # Timeout for analysis, e.g. 30s, 5m. 4 | # Default: 1m 5 | timeout: 10m 6 | 7 | # The default concurrency value is the number of available CPU. 8 | concurrency: 4 9 | 10 | # Which dirs to skip: issues from them won't be reported. 11 | # Can use regexp here: `generated.*`, regexp is applied on full path, 12 | # including the path prefix if one is set. 13 | # Default value is empty list, 14 | # but default dirs are skipped independently of this option's value (see skip-dirs-use-default). 15 | # "/" will be replaced by current OS file path separator to properly work on Windows. 16 | skip-dirs: 17 | - bin 18 | - docs 19 | - examples 20 | - hack 21 | 22 | # output configuration options. 23 | output: 24 | # Format: colored-line-number|line-number|json|colored-tab|tab|checkstyle|code-climate|junit-xml|github-actions|teamcity 25 | # 26 | # Multiple can be specified by separating them by comma, output can be provided 27 | # for each of them by separating format name and path by colon symbol. 28 | # Output path can be either `stdout`, `stderr` or path to the file to write to. 29 | # Example: "checkstyle:report.xml,json:stdout,colored-line-number" 30 | # 31 | # Default: colored-line-number 32 | format: colored-line-number 33 | 34 | # Print lines of code with issue. 35 | # Default: true 36 | print-issued-lines: true 37 | 38 | # Print linter name in the end of issue text. 39 | # Default: true 40 | print-linter-lines: true 41 | 42 | linters: 43 | # Disable all linters. 44 | disable-all: true 45 | 46 | # Enable specific linter 47 | # https://golangci-lint.run/usage/linters/#enabled-by-default 48 | enable: 49 | # Errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases. 50 | - errcheck 51 | 52 | # Linter for Go source code that specializes in simplifying code. 53 | - gosimple 54 | 55 | # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string. 56 | - govet 57 | 58 | # Detects when assignments to existing variables are not used. 59 | - ineffassign 60 | 61 | # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. 62 | # The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. 63 | - staticcheck 64 | 65 | # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode. 66 | - goimports 67 | 68 | # Checks whether HTTP response body is closed successfully. 69 | - bodyclose 70 | 71 | # Provides diagnostics that check for bugs, performance and style issues. 72 | # Extensible without recompilation through dynamic rules. 73 | # Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion. 74 | - gocritic 75 | 76 | # Gofmt checks whether code was gofmt-ed. By default, this tool runs with -s option to check for code simplification. 77 | - gofmt 78 | 79 | # Finds repeated strings that could be replaced by a constant. 80 | - goconst 81 | 82 | # Finds commonly misspelled English words in comments. 83 | - misspell 84 | 85 | # Finds naked returns in functions greater than a specified function length. 86 | - nakedret 87 | 88 | linters-settings: 89 | goimports: 90 | # A comma-separated list of prefixes, which, if set, checks import paths 91 | # with the given prefixes are grouped after 3rd-party packages. 92 | # Default: "" 93 | local-prefixes: github.com/GreptimeTeam/gtctl 94 | linters-settings: 95 | min-occurrences: 3 96 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | # See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos 2 | [default.extend-words] 3 | ue = "ue" 4 | [files] 5 | extend-exclude = ["go.mod"] 6 | -------------------------------------------------------------------------------- /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 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | info@greptime.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Greptime Team 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | REPO_ROOT:=$(CURDIR) 16 | OUT_DIR=$(REPO_ROOT)/bin 17 | BIN_NAME?=gtctl 18 | CLUSTER:=e2e-cluster 19 | LDFLAGS:=$(shell ./hack/version.sh) 20 | BUILD_FLAGS?=-trimpath -ldflags="-buildid= -w $(LDFLAGS)" 21 | MAIN_PKG:=$(REPO_ROOT)/cmd/gtctl 22 | INSTALL_DIR?=/usr/local/bin 23 | 24 | ##@ Build 25 | 26 | .PHONY: update-modules gtctl 27 | gtctl: ## Build gtctl binary(default). 28 | GO111MODULE=on CGO_ENABLED=0 go build -o "$(OUT_DIR)/$(BIN_NAME)" $(BUILD_FLAGS) $(MAIN_PKG) 29 | 30 | .PHONY: update-modules 31 | update-modules: ## Update Go modules. 32 | GO111MODULE=on go get -u ./... 33 | GO111MODULE=on go mod tidy 34 | 35 | .PHONY: install 36 | install: gtctl ## Install gtctl binary. 37 | sudo cp $(OUT_DIR)/$(BIN_NAME) $(INSTALL_DIR)/$(BIN_NAME) 38 | 39 | .PHONY: clean 40 | clean: ## Clean build files. 41 | rm -r $(OUT_DIR) 42 | 43 | ##@ Development 44 | 45 | .PHONY: setup-e2e 46 | setup-e2e: ## Setup e2e test environment. 47 | ./hack/e2e/setup-e2e-env.sh 48 | 49 | .PHONY: e2e 50 | e2e: gtctl setup-e2e ## Run e2e. 51 | go test -timeout 10m -v ./tests/e2e/... && kind delete clusters $(CLUSTER) 52 | 53 | .PHONY: lint 54 | lint: golangci-lint gtctl ## Run lint. 55 | golangci-lint run -v ./... 56 | 57 | .PHONY: test 58 | test: ## Run unit test. 59 | go test -timeout 1m -v ./pkg/... 60 | 61 | .PHONY: coverage 62 | coverage: ## Run unit test with coverage. 63 | go test ./pkg/... -race -coverprofile=coverage.xml -covermode=atomic 64 | 65 | .PHONY: fix-license-header 66 | fix-license-header: license-eye ## Fix license header. 67 | license-eye -c .licenserc.yaml header fix 68 | 69 | ##@ Tools Installation 70 | 71 | .PHONY: license-eye 72 | license-eye: ## Install license-eye. 73 | @which license-eye || go install github.com/apache/skywalking-eyes/cmd/license-eye@latest 74 | 75 | .PHONY: golangci-lint 76 | golangci-lint: ## Install golangci-lint. 77 | @which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 78 | 79 | ##@ General 80 | 81 | # The help target prints out all targets with their descriptions organized 82 | # beneath their categories. The categories are represented by '##@' and the 83 | # target descriptions by '##'. The awk commands is responsible for reading the 84 | # entire set of makefiles included in this invocation, looking for lines of the 85 | # file as xyz: ## something, and then pretty-format the target and help. Then, 86 | # if there's a line with ##@ something, that gets pretty-printed as a category. 87 | # More info on the usage of ANSI control characters for terminal formatting: 88 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 89 | # More info on the awk command: 90 | # https://linuxcommand.org/lc3_adv_awk.php 91 | 92 | .PHONY: help 93 | help: ## Display help messages. 94 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gtctl 2 | 3 | [![codecov](https://codecov.io/github/GreptimeTeam/gtctl/branch/develop/graph/badge.svg?token=287NUSEH5D)](https://app.codecov.io/github/GreptimeTeam/gtctl/tree/develop) 4 | [![license](https://img.shields.io/github/license/GreptimeTeam/gtctl)](https://github.com/GreptimeTeam/gtctl/blob/develop/LICENSE) 5 | [![report](https://goreportcard.com/badge/github.com/GreptimeTeam/gtctl)](https://goreportcard.com/report/github.com/GreptimeTeam/gtctl) 6 | 7 | ## Overview 8 | 9 | `gtctl`(`g-t-control`) is a command-line tool for managing the [GreptimeDB](https://github.com/GrepTimeTeam/greptimedb) cluster. `gtctl` is the **All-in-One** binary that integrates multiple operations of the GreptimeDB cluster. 10 | 11 |

12 | screenshot 13 |

14 | 15 | ## Installation 16 | 17 | Install `gtctl` using the following **oneliner installation** command: 18 | 19 | ```shell 20 | curl -fsSL https://raw.githubusercontent.com/greptimeteam/gtctl/develop/hack/install.sh | sh 21 | ``` 22 | 23 | In case of `go` is available, you can alternatively install `gtctl` with: 24 | 25 | ```shell 26 | go install github.com/GreptimeTeam/gtctl/cmd/gtctl@develop 27 | ``` 28 | 29 | ## Quickstart 30 | 31 | The **fastest** way to experience the GreptimeDB cluster is to use the playground: 32 | 33 | ```shell 34 | gtctl playground 35 | ``` 36 | 37 | The `playground` will deploy the minimal GreptimeDB cluster on your environment in bare-metal mode. 38 | 39 | ## Documentation 40 | 41 | * [More](https://docs.greptime.com/reference/gtctl) features and usage about `gtctl` 42 | 43 | ## License 44 | 45 | `gtctl` uses the [Apache 2.0 license](LICENSE) to strike a balance between open contributions and allowing you to use the software however you want. 46 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/GreptimeTeam/gtctl/pkg/logger" 25 | ) 26 | 27 | func NewClusterCommand(l logger.Logger) *cobra.Command { 28 | cmd := &cobra.Command{ 29 | Args: cobra.NoArgs, 30 | Use: "cluster", 31 | Short: "Manage GreptimeDB cluster", 32 | Long: `Manage GreptimeDB cluster in Kubernetes`, 33 | RunE: func(cmd *cobra.Command, args []string) error { 34 | if err := cmd.Help(); err != nil { 35 | return err 36 | } 37 | 38 | return errors.New("subcommand is required") 39 | }, 40 | } 41 | 42 | cmd.AddCommand(NewCreateClusterCommand(l)) 43 | cmd.AddCommand(NewDeleteClusterCommand(l)) 44 | cmd.AddCommand(NewScaleClusterCommand(l)) 45 | cmd.AddCommand(NewGetClusterCommand(l)) 46 | cmd.AddCommand(NewListClustersCommand(l)) 47 | cmd.AddCommand(NewConnectCommand(l)) 48 | 49 | return cmd 50 | } 51 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster_connect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 26 | "github.com/GreptimeTeam/gtctl/pkg/cluster/kubernetes" 27 | "github.com/GreptimeTeam/gtctl/pkg/logger" 28 | ) 29 | 30 | type clusterConnectCliOptions struct { 31 | Namespace string 32 | Protocol string 33 | } 34 | 35 | func NewConnectCommand(l logger.Logger) *cobra.Command { 36 | var options clusterConnectCliOptions 37 | 38 | cmd := &cobra.Command{ 39 | Use: "connect", 40 | Short: "Connect to a GreptimeDB cluster", 41 | Long: `Connect to a GreptimeDB cluster`, 42 | RunE: func(cmd *cobra.Command, args []string) error { 43 | if len(args) == 0 { 44 | return fmt.Errorf("cluster name should be set") 45 | } 46 | 47 | var ( 48 | ctx = context.TODO() 49 | clusterName = args[0] 50 | protocol opt.ConnectProtocol 51 | ) 52 | 53 | cluster, err := kubernetes.NewCluster(l) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | switch options.Protocol { 59 | case "mysql": 60 | protocol = opt.MySQL 61 | case "pg", "psql", "postgres": 62 | protocol = opt.Postgres 63 | default: 64 | return fmt.Errorf("unsupported connection protocol: %s", options.Protocol) 65 | } 66 | connectOptions := &opt.ConnectOptions{ 67 | Namespace: options.Namespace, 68 | Name: clusterName, 69 | Protocol: protocol, 70 | } 71 | 72 | return cluster.Connect(ctx, connectOptions) 73 | }, 74 | } 75 | 76 | cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") 77 | cmd.Flags().StringVarP(&options.Protocol, "protocol", "p", "mysql", "Specify a database protocol, like mysql or pg.") 78 | 79 | return cmd 80 | } 81 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster_delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/spf13/cobra" 24 | 25 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 26 | "github.com/GreptimeTeam/gtctl/pkg/cluster/baremetal" 27 | "github.com/GreptimeTeam/gtctl/pkg/cluster/kubernetes" 28 | "github.com/GreptimeTeam/gtctl/pkg/logger" 29 | ) 30 | 31 | type clusterDeleteOptions struct { 32 | Namespace string 33 | TearDownEtcd bool 34 | 35 | // The options for deleting GreptimeDB cluster in bare-metal. 36 | BareMetal bool 37 | } 38 | 39 | func NewDeleteClusterCommand(l logger.Logger) *cobra.Command { 40 | var options clusterDeleteOptions 41 | 42 | cmd := &cobra.Command{ 43 | Use: "delete", 44 | Short: "Delete a GreptimeDB cluster", 45 | Long: `Delete a GreptimeDB cluster`, 46 | RunE: func(cmd *cobra.Command, args []string) error { 47 | if len(args) == 0 { 48 | return fmt.Errorf("cluster name should be set") 49 | } 50 | 51 | clusterName := args[0] 52 | var ( 53 | cluster opt.Operations 54 | err error 55 | ctx = context.TODO() 56 | ) 57 | 58 | if options.BareMetal { 59 | cluster, err = baremetal.NewCluster(l, clusterName, baremetal.WithCreateNoDirs()) 60 | } else { 61 | cluster, err = kubernetes.NewCluster(l) 62 | } 63 | if err != nil { 64 | return err 65 | } 66 | 67 | deleteOptions := &opt.DeleteOptions{ 68 | Namespace: options.Namespace, 69 | Name: clusterName, 70 | } 71 | return cluster.Delete(ctx, deleteOptions) 72 | }, 73 | } 74 | 75 | cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") 76 | cmd.Flags().BoolVar(&options.TearDownEtcd, "tear-down-etcd", false, "Tear down etcd cluster.") 77 | cmd.Flags().BoolVar(&options.BareMetal, "bare-metal", false, "Get the greptimedb cluster on bare-metal environment.") 78 | 79 | return cmd 80 | } 81 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster_get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | 24 | "github.com/olekukonko/tablewriter" 25 | "github.com/spf13/cobra" 26 | 27 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 28 | "github.com/GreptimeTeam/gtctl/pkg/cluster/baremetal" 29 | "github.com/GreptimeTeam/gtctl/pkg/cluster/kubernetes" 30 | "github.com/GreptimeTeam/gtctl/pkg/logger" 31 | ) 32 | 33 | type clusterGetCliOptions struct { 34 | Namespace string 35 | 36 | // The options for getting GreptimeDB cluster in bare-metal. 37 | BareMetal bool 38 | } 39 | 40 | func NewGetClusterCommand(l logger.Logger) *cobra.Command { 41 | var options clusterGetCliOptions 42 | 43 | table := tablewriter.NewWriter(os.Stdout) 44 | 45 | cmd := &cobra.Command{ 46 | Use: "get", 47 | Short: "Get GreptimeDB cluster", 48 | Long: `Get GreptimeDB cluster`, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | if len(args) == 0 { 51 | return fmt.Errorf("cluster name should be set") 52 | } 53 | 54 | var ( 55 | ctx = context.TODO() 56 | err error 57 | cluster opt.Operations 58 | clusterName = args[0] 59 | ) 60 | 61 | if options.BareMetal { 62 | cluster, err = baremetal.NewCluster(l, clusterName, baremetal.WithCreateNoDirs()) 63 | } else { 64 | cluster, err = kubernetes.NewCluster(l) 65 | } 66 | if err != nil { 67 | return err 68 | } 69 | 70 | getOptions := &opt.GetOptions{ 71 | Namespace: options.Namespace, 72 | Name: clusterName, 73 | Table: table, 74 | } 75 | return cluster.Get(ctx, getOptions) 76 | }, 77 | } 78 | 79 | cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") 80 | cmd.Flags().BoolVar(&options.BareMetal, "bare-metal", false, "Get the greptimedb cluster on bare-metal environment.") 81 | 82 | return cmd 83 | } 84 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster_list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "os" 22 | 23 | "github.com/olekukonko/tablewriter" 24 | "github.com/spf13/cobra" 25 | 26 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 27 | "github.com/GreptimeTeam/gtctl/pkg/cluster/kubernetes" 28 | "github.com/GreptimeTeam/gtctl/pkg/logger" 29 | ) 30 | 31 | func NewListClustersCommand(l logger.Logger) *cobra.Command { 32 | table := tablewriter.NewWriter(os.Stdout) 33 | 34 | cmd := &cobra.Command{ 35 | Use: "list", 36 | Short: "List all GreptimeDB clusters", 37 | Long: `List all GreptimeDB clusters`, 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | var ctx = context.Background() 40 | 41 | cluster, err := kubernetes.NewCluster(l) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return cluster.List(ctx, &opt.ListOptions{ 47 | GetOptions: opt.GetOptions{ 48 | Table: table, 49 | }, 50 | }) 51 | }, 52 | } 53 | 54 | return cmd 55 | } 56 | -------------------------------------------------------------------------------- /cmd/gtctl/cluster_scale.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "time" 23 | 24 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 25 | "github.com/spf13/cobra" 26 | 27 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 28 | "github.com/GreptimeTeam/gtctl/pkg/cluster/kubernetes" 29 | "github.com/GreptimeTeam/gtctl/pkg/logger" 30 | ) 31 | 32 | type clusterScaleCliOptions struct { 33 | Namespace string 34 | ComponentType string 35 | Replicas int32 36 | Timeout int 37 | } 38 | 39 | func (s clusterScaleCliOptions) validate() error { 40 | if s.ComponentType == "" { 41 | return fmt.Errorf("component type is required") 42 | } 43 | 44 | if s.ComponentType != string(greptimedbclusterv1alpha1.FrontendComponentKind) && 45 | s.ComponentType != string(greptimedbclusterv1alpha1.DatanodeComponentKind) && 46 | s.ComponentType != string(greptimedbclusterv1alpha1.MetaComponentKind) { 47 | return fmt.Errorf("component type is invalid") 48 | } 49 | 50 | if s.Replicas < 0 { 51 | return fmt.Errorf("replicas should be equal or greater than 0") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func NewScaleClusterCommand(l logger.Logger) *cobra.Command { 58 | var options clusterScaleCliOptions 59 | 60 | cmd := &cobra.Command{ 61 | Use: "scale", 62 | Short: "Scale GreptimeDB cluster", 63 | Long: `Scale GreptimeDB cluster`, 64 | RunE: func(cmd *cobra.Command, args []string) error { 65 | if len(args) == 0 { 66 | return fmt.Errorf("cluster name should be set") 67 | } 68 | 69 | if err := options.validate(); err != nil { 70 | return err 71 | } 72 | 73 | var ( 74 | ctx = context.Background() 75 | cancel context.CancelFunc 76 | ) 77 | 78 | if options.Timeout > 0 { 79 | ctx, cancel = context.WithTimeout(ctx, time.Duration(options.Timeout)*time.Second) 80 | defer cancel() 81 | } 82 | 83 | cluster, err := kubernetes.NewCluster(l) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | scaleOptions := &opt.ScaleOptions{ 89 | Name: args[0], 90 | Namespace: options.Namespace, 91 | NewReplicas: options.Replicas, 92 | ComponentType: greptimedbclusterv1alpha1.ComponentKind(options.ComponentType), 93 | } 94 | return cluster.Scale(ctx, scaleOptions) 95 | }, 96 | } 97 | 98 | cmd.Flags().StringVarP(&options.ComponentType, "component", "c", "", "Component of GreptimeDB cluster, can be 'frontend', 'datanode' and 'meta'.") 99 | cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "default", "Namespace of GreptimeDB cluster.") 100 | cmd.Flags().Int32Var(&options.Replicas, "replicas", 0, "The replicas of component of GreptimeDB cluster.") 101 | cmd.Flags().IntVar(&options.Timeout, "timeout", 300, "Timeout in seconds for the command to complete, default is no timeout.") 102 | 103 | return cmd 104 | } 105 | -------------------------------------------------------------------------------- /cmd/gtctl/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | "sigs.k8s.io/kind/pkg/log" 25 | 26 | "github.com/GreptimeTeam/gtctl/pkg/logger" 27 | "github.com/GreptimeTeam/gtctl/pkg/plugins" 28 | "github.com/GreptimeTeam/gtctl/pkg/version" 29 | ) 30 | 31 | func NewRootCommand() *cobra.Command { 32 | const GtctlTextBanner = ` 33 | __ __ __ 34 | ____ _/ /______/ /_/ / 35 | / __ '/ __/ ___/ __/ / 36 | / /_/ / /_/ /__/ /_/ / 37 | \__, /\__/\___/\__/_/ 38 | /____/` 39 | 40 | var ( 41 | verbosity int32 42 | 43 | l = logger.New(os.Stdout, log.Level(verbosity), logger.WithColored()) 44 | ) 45 | 46 | cmd := &cobra.Command{ 47 | Use: "gtctl", 48 | Short: "gtctl is a command-line tool for managing GreptimeDB cluster.", 49 | Long: fmt.Sprintf("%s\ngtctl is a command-line tool for managing GreptimeDB cluster.", GtctlTextBanner), 50 | Version: version.Get().String(), 51 | SilenceUsage: true, 52 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 53 | type verboser interface { 54 | SetVerbosity(log.Level) 55 | } 56 | if v, ok := l.(verboser); ok { 57 | v.SetVerbosity(log.Level(verbosity)) 58 | return nil 59 | } 60 | 61 | return fmt.Errorf("logger does not implement SetVerbosity") 62 | }, 63 | } 64 | 65 | cmd.PersistentFlags().Int32VarP(&verbosity, "verbosity", "v", 0, "info log verbosity, higher value produces more output") 66 | 67 | // Add all top level subcommands. 68 | cmd.AddCommand(NewVersionCommand(l)) 69 | cmd.AddCommand(NewClusterCommand(l)) 70 | cmd.AddCommand(NewPlaygroundCommand(l)) 71 | 72 | return cmd 73 | } 74 | 75 | func main() { 76 | pm, err := plugins.NewManager() 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | if len(os.Args) > 1 && pm.ShouldRun(os.Args[1]) { 82 | if err = pm.Run(os.Args[1:]); err != nil { 83 | fmt.Println(err) 84 | os.Exit(1) 85 | } 86 | os.Exit(0) 87 | } 88 | 89 | if err = NewRootCommand().Execute(); err != nil { 90 | fmt.Println(err) 91 | os.Exit(1) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cmd/gtctl/playground.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/lucasepe/codename" 21 | "github.com/spf13/cobra" 22 | 23 | "github.com/GreptimeTeam/gtctl/pkg/logger" 24 | ) 25 | 26 | func NewPlaygroundCommand(l logger.Logger) *cobra.Command { 27 | return &cobra.Command{ 28 | Use: "playground", 29 | Short: "Starts a GreptimeDB cluster playground", 30 | Long: "Starts a GreptimeDB cluster playground in bare-metal mode", 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | rng, err := codename.DefaultRNG() 33 | if err != nil { 34 | return nil 35 | } 36 | 37 | playgroundName := codename.Generate(rng, 0) 38 | playgroundOptions := &clusterCreateCliOptions{ 39 | BareMetal: true, 40 | Timeout: 900, // 15min 41 | EnableCache: false, 42 | } 43 | 44 | return NewCluster([]string{playgroundName}, playgroundOptions, l) 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cmd/gtctl/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/GreptimeTeam/gtctl/pkg/logger" 23 | "github.com/GreptimeTeam/gtctl/pkg/version" 24 | ) 25 | 26 | func NewVersionCommand(l logger.Logger) *cobra.Command { 27 | return &cobra.Command{ 28 | Use: "version", 29 | Short: "Print the version of gtctl and exit", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | l.V(0).Infof("%s", version.Get()) 32 | }, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/gtctl/9dc5c7f6f6401bfd72fb6d720c767739d0e855d6/docs/images/screenshot.png -------------------------------------------------------------------------------- /examples/bare-metal/cluster-with-local-artifacts.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | local: "/path/to/greptime" 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 0.0.0.0:3002 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: 20 | local: "/path/to/etcd" 21 | -------------------------------------------------------------------------------- /examples/bare-metal/cluster-with-s3-storage.datanode.toml: -------------------------------------------------------------------------------- 1 | # More options for storage: https://docs.greptime.com/user-guide/deployments/configuration#storage-options 2 | 3 | [storage] 4 | type = "S3" 5 | bucket = "test_greptimedb" 6 | root = "/greptimedb" 7 | access_key_id = "" 8 | secret_access_key = "" 9 | -------------------------------------------------------------------------------- /examples/bare-metal/cluster-with-s3-storage.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: latest 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | config: 'examples/bare-metal/cluster-with-s3-storage.datanode.toml' 13 | meta: 14 | replicas: 1 15 | storeAddr: 127.0.0.1:2379 16 | serverAddr: 0.0.0.0:3002 17 | httpAddr: 0.0.0.0:14001 18 | 19 | etcd: 20 | artifact: 21 | version: v3.5.7 22 | -------------------------------------------------------------------------------- /examples/bare-metal/cluster.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: latest 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 0.0.0.0:3002 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: 20 | version: v3.5.7 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GreptimeTeam/gtctl 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/GreptimeTeam/greptimedb-operator v0.1.0-alpha.9 7 | github.com/Masterminds/semver/v3 v3.2.1 8 | github.com/briandowns/spinner v1.19.0 9 | github.com/fatih/color v1.13.0 10 | github.com/go-pg/pg/v10 v10.11.1 11 | github.com/go-playground/validator/v10 v10.14.1 12 | github.com/go-sql-driver/mysql v1.6.0 13 | github.com/google/go-github/v53 v53.2.0 14 | github.com/lucasepe/codename v0.2.0 15 | github.com/olekukonko/tablewriter v0.0.5 16 | github.com/onsi/ginkgo/v2 v2.4.0 17 | github.com/onsi/gomega v1.23.0 18 | github.com/spf13/cobra v1.6.1 19 | github.com/stretchr/testify v1.8.2 20 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 21 | gopkg.in/yaml.v3 v3.0.1 22 | helm.sh/helm/v3 v3.11.1 23 | k8s.io/api v0.26.0 24 | k8s.io/apiextensions-apiserver v0.26.0 25 | k8s.io/apimachinery v0.26.0 26 | k8s.io/cli-runtime v0.26.0 27 | k8s.io/client-go v0.26.0 28 | k8s.io/klog/v2 v2.80.1 29 | sigs.k8s.io/kind v0.17.0 30 | sigs.k8s.io/yaml v1.3.0 31 | ) 32 | 33 | require ( 34 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 35 | github.com/BurntSushi/toml v1.2.1 // indirect 36 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 37 | github.com/Masterminds/goutils v1.1.1 // indirect 38 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 39 | github.com/Masterminds/squirrel v1.5.3 // indirect 40 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 41 | github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect 42 | github.com/beorn7/perks v1.0.1 // indirect 43 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 44 | github.com/chai2010/gettext-go v1.0.2 // indirect 45 | github.com/cloudflare/circl v1.3.3 // indirect 46 | github.com/containerd/containerd v1.6.18 // indirect 47 | github.com/cyphar/filepath-securejoin v0.2.3 // indirect 48 | github.com/davecgh/go-spew v1.1.1 // indirect 49 | github.com/docker/cli v20.10.21+incompatible // indirect 50 | github.com/docker/distribution v2.8.2+incompatible // indirect 51 | github.com/docker/docker v20.10.21+incompatible // indirect 52 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 53 | github.com/docker/go-connections v0.4.0 // indirect 54 | github.com/docker/go-metrics v0.0.1 // indirect 55 | github.com/docker/go-units v0.4.0 // indirect 56 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 57 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 58 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 59 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 60 | github.com/go-errors/errors v1.0.1 // indirect 61 | github.com/go-gorp/gorp/v3 v3.1.0 // indirect 62 | github.com/go-logr/logr v1.2.3 // indirect 63 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 64 | github.com/go-openapi/jsonreference v0.20.0 // indirect 65 | github.com/go-openapi/swag v0.19.14 // indirect 66 | github.com/go-pg/zerochecker v0.2.0 // indirect 67 | github.com/go-playground/locales v0.14.1 // indirect 68 | github.com/go-playground/universal-translator v0.18.1 // indirect 69 | github.com/gobwas/glob v0.2.3 // indirect 70 | github.com/gogo/protobuf v1.3.2 // indirect 71 | github.com/golang/protobuf v1.5.3 // indirect 72 | github.com/google/btree v1.0.1 // indirect 73 | github.com/google/gnostic v0.5.7-v3refs // indirect 74 | github.com/google/go-cmp v0.5.9 // indirect 75 | github.com/google/go-querystring v1.1.0 // indirect 76 | github.com/google/gofuzz v1.2.0 // indirect 77 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 78 | github.com/google/uuid v1.3.0 // indirect 79 | github.com/gorilla/mux v1.8.0 // indirect 80 | github.com/gosuri/uitable v0.0.4 // indirect 81 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 82 | github.com/huandu/xstrings v1.3.3 // indirect 83 | github.com/imdario/mergo v0.3.13 // indirect 84 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 85 | github.com/jinzhu/inflection v1.0.0 // indirect 86 | github.com/jmoiron/sqlx v1.3.5 // indirect 87 | github.com/josharian/intern v1.0.0 // indirect 88 | github.com/json-iterator/go v1.1.12 // indirect 89 | github.com/klauspost/compress v1.13.6 // indirect 90 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 91 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 92 | github.com/leodido/go-urn v1.2.4 // indirect 93 | github.com/lib/pq v1.10.7 // indirect 94 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 95 | github.com/mailru/easyjson v0.7.6 // indirect 96 | github.com/mattn/go-colorable v0.1.13 // indirect 97 | github.com/mattn/go-isatty v0.0.16 // indirect 98 | github.com/mattn/go-runewidth v0.0.9 // indirect 99 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 100 | github.com/mitchellh/copystructure v1.2.0 // indirect 101 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 102 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 103 | github.com/moby/locker v1.0.1 // indirect 104 | github.com/moby/spdystream v0.2.0 // indirect 105 | github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect 106 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 107 | github.com/modern-go/reflect2 v1.0.2 // indirect 108 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 109 | github.com/morikuni/aec v1.0.0 // indirect 110 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 111 | github.com/opencontainers/go-digest v1.0.0 // indirect 112 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 113 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 114 | github.com/pkg/errors v0.9.1 // indirect 115 | github.com/pmezard/go-difflib v1.0.0 // indirect 116 | github.com/prometheus/client_golang v1.14.0 // indirect 117 | github.com/prometheus/client_model v0.3.0 // indirect 118 | github.com/prometheus/common v0.37.0 // indirect 119 | github.com/prometheus/procfs v0.8.0 // indirect 120 | github.com/rubenv/sql-migrate v1.2.0 // indirect 121 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 122 | github.com/shopspring/decimal v1.2.0 // indirect 123 | github.com/sirupsen/logrus v1.9.0 // indirect 124 | github.com/spf13/cast v1.4.1 // indirect 125 | github.com/spf13/pflag v1.0.5 // indirect 126 | github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect 127 | github.com/vmihailenco/bufpool v0.1.11 // indirect 128 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 129 | github.com/vmihailenco/tagparser v0.1.2 // indirect 130 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 131 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 132 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 133 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 134 | github.com/xlab/treeprint v1.1.0 // indirect 135 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 136 | golang.org/x/crypto v0.12.0 // indirect 137 | golang.org/x/net v0.10.0 // indirect 138 | golang.org/x/oauth2 v0.8.0 // indirect 139 | golang.org/x/sync v0.1.0 // indirect 140 | golang.org/x/sys v0.12.0 // indirect 141 | golang.org/x/term v0.11.0 // indirect 142 | golang.org/x/text v0.12.0 // indirect 143 | golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect 144 | google.golang.org/appengine v1.6.7 // indirect 145 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 146 | google.golang.org/grpc v1.56.3 // indirect 147 | google.golang.org/protobuf v1.30.0 // indirect 148 | gopkg.in/inf.v0 v0.9.1 // indirect 149 | gopkg.in/yaml.v2 v2.4.0 // indirect 150 | k8s.io/apiserver v0.26.0 // indirect 151 | k8s.io/component-base v0.26.0 // indirect 152 | k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect 153 | k8s.io/kubectl v0.26.0 // indirect 154 | k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect 155 | mellium.im/sasl v0.3.1 // indirect 156 | oras.land/oras-go v1.2.2 // indirect 157 | sigs.k8s.io/controller-runtime v0.12.3 // indirect 158 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect 159 | sigs.k8s.io/kustomize/api v0.12.1 // indirect 160 | sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect 161 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 162 | ) 163 | -------------------------------------------------------------------------------- /hack/e2e/setup-e2e-env.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023 Greptime Team 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o pipefail 19 | 20 | CLUSTER=e2e-cluster 21 | REGISTRY_NAME=kind-registry 22 | REGISTRY_PORT=5001 23 | KIND_NODE_IMAGE=kindest/node:v1.24.15 24 | 25 | function check_prerequisites() { 26 | if ! hash docker 2>/dev/null; then 27 | echo "docker command is not found! You can download docker here: https://docs.docker.com/get-docker/" 28 | exit 29 | fi 30 | 31 | if ! hash kind 2>/dev/null; then 32 | echo "kind command is not found! You can download kind here: https://kind.sigs.k8s.io/docs/user/quick-start/#installing-from-release-binaries" 33 | exit 34 | fi 35 | 36 | if ! hash kubectl 2>/dev/null; then 37 | echo "kubectl command is not found! You can download kubectl here: https://kubernetes.io/docs/tasks/tools/" 38 | exit 39 | fi 40 | } 41 | 42 | function start_local_registry() { 43 | # create registry container unless it already exists 44 | if [ "$(docker inspect -f '{{.State.Running}}' "${REGISTRY_NAME}" 2>/dev/null || true)" != 'true' ]; then 45 | docker run \ 46 | -d --restart=always -p "127.0.0.1:${REGISTRY_PORT}:5000" --name "${REGISTRY_NAME}" \ 47 | registry:2 48 | fi 49 | } 50 | 51 | function create_kind_cluster() { 52 | # check cluster 53 | for cluster in $(kind get clusters); do 54 | if [ "$cluster" = "${CLUSTER}" ]; then 55 | echo "Use the existed cluster $cluster" 56 | kubectl config use-context kind-"$cluster" 57 | return 58 | fi 59 | done 60 | 61 | # create a cluster with the local registry enabled in containerd 62 | cat <] [-v ] [-o ] [-i]" 30 | echo "Options:" 31 | echo " -s Download source. Options: github, aws. Default: github." 32 | echo " -v Version of the binary to install. Default: latest." 33 | echo " -o Organization of the repository. Default: GreptimeTeam." 34 | echo " -r Repository name. Default: gtctl." 35 | echo " -i Install to /usr/local/bin." 36 | } 37 | 38 | download_from_github() { 39 | if [ "${VERSION}" = "latest" ]; then 40 | wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/latest/download/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz" 41 | wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/latest/download/${BIN}-${OS_TYPE}-${ARCH_TYPE}.sha256sum" 42 | else 43 | wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/download/${VERSION}/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz" 44 | wget "https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/download/${VERSION}/${BIN}-${OS_TYPE}-${ARCH_TYPE}.sha256sum" 45 | fi 46 | } 47 | 48 | download_from_aws() { 49 | wget "$GREPTIME_AWS_CN_RELEASE_BUCKET/${GITHUB_REPO}/${VERSION}/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz" 50 | wget "$GREPTIME_AWS_CN_RELEASE_BUCKET/${GITHUB_REPO}/${VERSION}/${BIN}-${OS_TYPE}-${ARCH_TYPE}.sha256sum" 51 | } 52 | 53 | verify_sha256sum() { 54 | command -v shasum >/dev/null 2>&1 || { echo "WARN: shasum command not found, skip sha256sum verification."; return; } 55 | 56 | ARTIFACT_FILE="$1" 57 | SUM_FILE="$2" 58 | 59 | # Calculate sha256sum of the downloaded file. 60 | CALCULATE_SUM=$(shasum -a 256 "$ARTIFACT_FILE" | cut -f1 -d' ') 61 | 62 | if [ "${CALCULATE_SUM}" != "$(cat "$SUM_FILE")" ]; then 63 | echo "ERROR: sha256sum verification failed for $ARTIFACT_FILE" 64 | exit 1 65 | else 66 | echo "sha256sum verification succeeded for $ARTIFACT_FILE, checksum: $CALCULATE_SUM" 67 | fi 68 | } 69 | 70 | install_binary() { 71 | tar xvf "${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz" 72 | if [ "${INSTALL_DIR}" = "/usr/local/bin/" ]; then 73 | sudo mv "${BIN}" "${INSTALL_DIR}" 74 | echo "Run '${BIN} --help' to get started" 75 | else 76 | echo "Run './${BIN} --help' to get started" 77 | fi 78 | 79 | # Clean the download package. 80 | rm "${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz" 81 | rm "${BIN}-${OS_TYPE}-${ARCH_TYPE}.sha256sum" 82 | } 83 | 84 | get_os_type() { 85 | case "$(uname -s)" in 86 | Darwin) OS_TYPE=darwin;; 87 | Linux) OS_TYPE=linux;; 88 | *) echo "Error: Unknown OS type"; exit 1;; 89 | esac 90 | } 91 | 92 | get_arch_type() { 93 | case "$(uname -m)" in 94 | arm64|aarch64) ARCH_TYPE=arm64;; 95 | x86_64|amd64) ARCH_TYPE=amd64;; 96 | *) echo "Error: Unknown CPU type"; exit 1;; 97 | esac 98 | } 99 | 100 | do_download() { 101 | echo "Downloading '${BIN}' from '${DOWNLOAD_SOURCE}', OS: '${OS_TYPE}', Arch: '${ARCH_TYPE}', Version: '${VERSION}'" 102 | 103 | case "${DOWNLOAD_SOURCE}" in 104 | github) download_from_github;; 105 | aws) download_from_aws;; 106 | *) echo "ERROR: Unknown download source"; exit 1;; 107 | esac 108 | } 109 | 110 | # Check required commands 111 | command -v wget >/dev/null 2>&1 || { echo "ERROR: wget command not found. Please install wget."; exit 1; } 112 | 113 | while getopts "s:v:o:r:i" opt; do 114 | case "$opt" in 115 | s) DOWNLOAD_SOURCE="$OPTARG";; 116 | v) VERSION="$OPTARG";; 117 | o) GITHUB_ORG="$OPTARG";; 118 | r) GITHUB_REPO="$OPTARG";; 119 | i) INSTALL_DIR="/usr/local/bin/";; 120 | *) usage; exit 1;; 121 | esac 122 | done 123 | 124 | # Main 125 | get_os_type 126 | get_arch_type 127 | do_download 128 | verify_sha256sum "$(pwd)"/${BIN}-${OS_TYPE}-${ARCH_TYPE}.tgz "$(pwd)"/${BIN}-${OS_TYPE}-${ARCH_TYPE}.sha256sum 129 | install_binary 130 | -------------------------------------------------------------------------------- /hack/version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2023 Greptime Team 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | function ldflag() { 17 | local key=${1} 18 | local val=${2} 19 | 20 | echo "-X 'github.com/GreptimeTeam/gtctl/pkg/version.${key}=${val}'" 21 | } 22 | 23 | # parse the current git commit hash 24 | GitCommit=$(git rev-parse HEAD) 25 | 26 | # check if the current commit has a matching tag 27 | GitVersion=$(git describe --exact-match --abbrev=0 --tags "${GitCommit}" 2> /dev/null || true) 28 | 29 | # check for changed files (not untracked files) 30 | if [ -n "${GitVersion}" ] && [ -n "$(git diff --shortstat 2> /dev/null | tail -n1)" ]; then 31 | GitVersion+="${GitVersion}-dirty" 32 | fi 33 | 34 | ldflags+=($(ldflag "gitCommit" "${GitCommit}")) 35 | ldflags+=($(ldflag "gitVersion" "${GitVersion}")) 36 | ldflags+=($(ldflag "buildDate" "$(date ${buildDate} -u +'%Y-%m-%dT%H:%M:%SZ')")) 37 | 38 | echo "${ldflags[*]-}" 39 | -------------------------------------------------------------------------------- /licenserc.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Greptime Team 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | headerPath = "Apache-2.0.txt" 16 | 17 | includes = [ 18 | "*.go", 19 | "*.sh", 20 | ] 21 | 22 | [properties] 23 | inceptionYear = 2023 24 | copyrightOwner = "Greptime Team" 25 | -------------------------------------------------------------------------------- /pkg/artifacts/constants.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package artifacts 18 | 19 | const ( 20 | // GreptimeChartIndexURL is the URL of the Greptime chart index. 21 | GreptimeChartIndexURL = "https://raw.githubusercontent.com/GreptimeTeam/helm-charts/gh-pages/index.yaml" 22 | 23 | // GreptimeChartReleaseDownloadURL is the URL of the Greptime charts that stored in the GitHub release. 24 | GreptimeChartReleaseDownloadURL = "https://github.com/GreptimeTeam/helm-charts/releases/download" 25 | 26 | // GreptimeReleaseBucketCN releases bucket public endpoint in CN region. 27 | GreptimeReleaseBucketCN = "https://downloads.greptime.cn/releases" 28 | 29 | // GreptimeCNCharts is the URL of the Greptime charts that stored in the S3 bucket of the CN region. 30 | GreptimeCNCharts = GreptimeReleaseBucketCN + "/charts" 31 | 32 | // GreptimeDBCNBinaries is the URL of the GreptimeDB binaries that stored in the S3 bucket of the CN region. 33 | GreptimeDBCNBinaries = GreptimeReleaseBucketCN + "/greptimedb" 34 | 35 | // EtcdCNBinaries is the URL of the etcd binaries that stored in the S3 bucket of the CN region. 36 | EtcdCNBinaries = GreptimeReleaseBucketCN + "/etcd" 37 | 38 | // LatestVersionTag is the tag of the latest version. 39 | LatestVersionTag = "latest" 40 | 41 | // EtcdOCIRegistry is the OCI registry of the etcd chart. 42 | EtcdOCIRegistry = "oci://registry-1.docker.io/bitnamicharts/etcd" 43 | 44 | // GreptimeGitHubOrg is the GitHub organization of Greptime. 45 | GreptimeGitHubOrg = "GreptimeTeam" 46 | 47 | // GreptimeDBGithubRepo is the GitHub repository of GreptimeDB. 48 | GreptimeDBGithubRepo = "greptimedb" 49 | 50 | // EtcdGitHubOrg is the GitHub organization of etcd. 51 | EtcdGitHubOrg = "etcd-io" 52 | 53 | // EtcdGithubRepo is the GitHub repository of etcd. 54 | EtcdGithubRepo = "etcd" 55 | 56 | // GreptimeBinName is the artifact name of greptime. 57 | GreptimeBinName = "greptime" 58 | 59 | // EtcdBinName is the artifact name of etcd. 60 | EtcdBinName = "etcd" 61 | 62 | // GreptimeDBClusterChartName is the chart name of GreptimeDB. 63 | GreptimeDBClusterChartName = "greptimedb-cluster" 64 | 65 | // GreptimeDBOperatorChartName is the chart name of GreptimeDB operator. 66 | GreptimeDBOperatorChartName = "greptimedb-operator" 67 | 68 | // EtcdChartName is the chart name of etcd. 69 | EtcdChartName = "etcd" 70 | 71 | // DefaultEtcdChartVersion is the default etcd chart version. 72 | DefaultEtcdChartVersion = "9.2.0" 73 | 74 | // DefaultEtcdBinVersion is the default etcd binary version. 75 | DefaultEtcdBinVersion = "v3.5.7" 76 | ) 77 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package baremetal 18 | 19 | import ( 20 | "context" 21 | "os/signal" 22 | "sync" 23 | "syscall" 24 | 25 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 26 | "github.com/GreptimeTeam/gtctl/pkg/cluster" 27 | "github.com/GreptimeTeam/gtctl/pkg/components" 28 | "github.com/GreptimeTeam/gtctl/pkg/config" 29 | "github.com/GreptimeTeam/gtctl/pkg/logger" 30 | "github.com/GreptimeTeam/gtctl/pkg/metadata" 31 | ) 32 | 33 | type Cluster struct { 34 | config *config.BareMetalClusterConfig 35 | createNoDirs bool 36 | enableCache bool 37 | useMemoryMeta bool 38 | 39 | am artifacts.Manager 40 | mm metadata.Manager 41 | cc *ClusterComponents 42 | 43 | logger logger.Logger 44 | stop context.CancelFunc 45 | ctx context.Context 46 | wg sync.WaitGroup 47 | } 48 | 49 | // ClusterComponents describes all the components need to be deployed under bare-metal mode. 50 | type ClusterComponents struct { 51 | MetaSrv components.ClusterComponent 52 | Datanode components.ClusterComponent 53 | Frontend components.ClusterComponent 54 | Etcd components.ClusterComponent 55 | } 56 | 57 | func NewClusterComponents(config *config.BareMetalClusterComponentsConfig, workingDirs components.WorkingDirs, 58 | wg *sync.WaitGroup, logger logger.Logger, useMemoryMeta bool) *ClusterComponents { 59 | return &ClusterComponents{ 60 | MetaSrv: components.NewMetaSrv(config.MetaSrv, workingDirs, wg, logger, useMemoryMeta), 61 | Datanode: components.NewDataNode(config.Datanode, config.MetaSrv.ServerAddr, workingDirs, wg, logger), 62 | Frontend: components.NewFrontend(config.Frontend, config.MetaSrv.ServerAddr, workingDirs, wg, logger), 63 | Etcd: components.NewEtcd(workingDirs, wg, logger), 64 | } 65 | } 66 | 67 | type Option func(cluster *Cluster) 68 | 69 | // WithReplaceConfig replaces current cluster config with given config. 70 | func WithReplaceConfig(cfg *config.BareMetalClusterConfig) Option { 71 | return func(c *Cluster) { 72 | c.config = cfg 73 | } 74 | } 75 | 76 | func WithGreptimeVersion(version string) Option { 77 | return func(c *Cluster) { 78 | c.config.Cluster.Artifact.Version = version 79 | } 80 | } 81 | 82 | func WithEnableCache(enableCache bool) Option { 83 | return func(c *Cluster) { 84 | c.enableCache = enableCache 85 | } 86 | } 87 | 88 | func WithMetastore(useMemoryMeta bool) Option { 89 | return func(c *Cluster) { 90 | c.useMemoryMeta = useMemoryMeta 91 | } 92 | } 93 | 94 | func WithCreateNoDirs() Option { 95 | return func(c *Cluster) { 96 | c.createNoDirs = true 97 | } 98 | } 99 | 100 | func NewCluster(l logger.Logger, clusterName string, opts ...Option) (cluster.Operations, error) { 101 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 102 | 103 | c := &Cluster{ 104 | logger: l, 105 | config: config.DefaultBareMetalConfig(), 106 | ctx: ctx, 107 | stop: stop, 108 | } 109 | 110 | for _, opt := range opts { 111 | if opt != nil { 112 | opt(c) 113 | } 114 | } 115 | 116 | if err := config.ValidateConfig(c.config); err != nil { 117 | return nil, err 118 | } 119 | 120 | // Configure Metadata Manager 121 | mm, err := metadata.New("") 122 | if err != nil { 123 | return nil, err 124 | } 125 | c.mm = mm 126 | 127 | // Configure Artifact Manager. 128 | am, err := artifacts.NewManager(l) 129 | if err != nil { 130 | return nil, err 131 | } 132 | c.am = am 133 | 134 | // Configure Cluster Components. 135 | mm.AllocateClusterScopeDirs(clusterName) 136 | if !c.createNoDirs { 137 | if err = mm.CreateClusterScopeDirs(c.config); err != nil { 138 | return nil, err 139 | } 140 | } 141 | csd := mm.GetClusterScopeDirs() 142 | c.cc = NewClusterComponents(c.config.Cluster, components.WorkingDirs{ 143 | DataDir: csd.DataDir, 144 | LogsDir: csd.LogsDir, 145 | PidsDir: csd.PidsDir, 146 | }, &c.wg, c.logger, c.useMemoryMeta) 147 | 148 | return c, nil 149 | } 150 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package baremetal 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "syscall" 24 | 25 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 26 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 27 | ) 28 | 29 | func (c *Cluster) Delete(ctx context.Context, options *opt.DeleteOptions) error { 30 | cluster, err := c.get(ctx, &opt.GetOptions{Name: options.Name}) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | running, ferr, serr := c.isClusterRunning(cluster.ForegroundPid) 36 | if ferr != nil { 37 | return fmt.Errorf("error checking whether cluster '%s' is running: %v", options.Name, ferr) 38 | } 39 | if running || serr == nil { 40 | return fmt.Errorf("cluster '%s' is running, please stop it before deleting", options.Name) 41 | } 42 | 43 | csd := c.mm.GetClusterScopeDirs() 44 | c.logger.V(0).Infof("Deleting cluster configurations and runtime directories in %s", csd.BaseDir) 45 | if err = c.delete(ctx, csd.BaseDir); err != nil { 46 | return err 47 | } 48 | c.logger.V(0).Info("Deleted!") 49 | 50 | return nil 51 | } 52 | 53 | func (c *Cluster) delete(_ context.Context, baseDir string) error { 54 | return fileutils.DeleteDirIfExists(baseDir) 55 | } 56 | 57 | // isClusterRunning checks the current status of cluster by sending signal to process. 58 | func (c *Cluster) isClusterRunning(pid int) (runs bool, f error, s error) { 59 | p, f := os.FindProcess(pid) 60 | if f != nil { 61 | return false, f, nil 62 | } 63 | 64 | s = p.Signal(syscall.Signal(0)) 65 | if s != nil { 66 | return false, nil, s 67 | } 68 | 69 | return true, nil, nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package baremetal 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "io/fs" 23 | "os" 24 | "path" 25 | "path/filepath" 26 | 27 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 28 | "github.com/olekukonko/tablewriter" 29 | "gopkg.in/yaml.v3" 30 | 31 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 32 | cfg "github.com/GreptimeTeam/gtctl/pkg/config" 33 | "github.com/GreptimeTeam/gtctl/pkg/metadata" 34 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 35 | ) 36 | 37 | func (c *Cluster) Get(ctx context.Context, options *opt.GetOptions) error { 38 | cluster, err := c.get(ctx, options) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | c.renderGetView(options.Table, cluster) 44 | 45 | return nil 46 | } 47 | 48 | func (c *Cluster) get(_ context.Context, options *opt.GetOptions) (*cfg.BareMetalClusterMetadata, error) { 49 | csd := c.mm.GetClusterScopeDirs() 50 | _, err := os.Stat(csd.BaseDir) 51 | if os.IsNotExist(err) { 52 | return nil, fmt.Errorf("cluster %s is not exist", options.Name) 53 | } 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | ok, err := fileutils.IsFileExists(csd.ConfigPath) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if !ok { 63 | return nil, fmt.Errorf("cluster %s is not exist", options.Name) 64 | } 65 | 66 | var cluster cfg.BareMetalClusterMetadata 67 | in, err := os.ReadFile(csd.ConfigPath) 68 | if err != nil { 69 | return nil, err 70 | } 71 | if err = yaml.Unmarshal(in, &cluster); err != nil { 72 | return nil, err 73 | } 74 | 75 | return &cluster, nil 76 | } 77 | 78 | func (c *Cluster) configGetView(table *tablewriter.Table) { 79 | table.SetAutoMergeCells(true) 80 | table.SetRowLine(true) 81 | } 82 | 83 | func (c *Cluster) renderGetView(table *tablewriter.Table, data *cfg.BareMetalClusterMetadata) { 84 | c.configGetView(table) 85 | 86 | headers, footers, bulk := collectClusterInfoFromBareMetal(data) 87 | table.SetHeader(headers) 88 | table.AppendBulk(bulk) 89 | table.Render() 90 | 91 | for _, footer := range footers { 92 | c.logger.V(0).Info(footer) 93 | } 94 | } 95 | 96 | func collectClusterInfoFromBareMetal(data *cfg.BareMetalClusterMetadata) ( 97 | headers, footers []string, bulk [][]string) { 98 | headers = []string{"COMPONENT", "PID"} 99 | 100 | pidsDir := path.Join(data.ClusterDir, metadata.ClusterPidsDir) 101 | pidsMap := collectPidsForBareMetal(pidsDir) 102 | 103 | var ( 104 | date = data.CreationDate.String() 105 | rows = func(name string, replicas int) { 106 | for i := 0; i < replicas; i++ { 107 | key := fmt.Sprintf("%s.%d", name, i) 108 | pid := "N/A" 109 | if val, ok := pidsMap[key]; ok { 110 | pid = fmt.Sprintf(".%d: %s", i, val) 111 | } 112 | bulk = append(bulk, []string{name, pid}) 113 | } 114 | } 115 | ) 116 | 117 | rows(string(greptimedbclusterv1alpha1.FrontendComponentKind), data.Config.Cluster.Frontend.Replicas) 118 | rows(string(greptimedbclusterv1alpha1.DatanodeComponentKind), data.Config.Cluster.Datanode.Replicas) 119 | rows(string(greptimedbclusterv1alpha1.MetaComponentKind), data.Config.Cluster.MetaSrv.Replicas) 120 | 121 | bulk = append(bulk, []string{"etcd", pidsMap["etcd"]}) 122 | 123 | config, err := yaml.Marshal(data.Config) 124 | footers = []string{ 125 | fmt.Sprintf("CREATION-DATE: %s", date), 126 | fmt.Sprintf("GREPTIMEDB-VERSION: %s", data.Config.Cluster.Artifact.Version), 127 | fmt.Sprintf("ETCD-VERSION: %s", data.Config.Etcd.Artifact.Version), 128 | fmt.Sprintf("CLUSTER-DIR: %s", data.ClusterDir), 129 | } 130 | if err != nil { 131 | footers = append(footers, fmt.Sprintf("CLUSTER-CONFIG: error retrieving cluster config: %v", err)) 132 | } else { 133 | footers = append(footers, fmt.Sprintf("CLUSTER-CONFIG:\n%s", string(config))) 134 | } 135 | 136 | return headers, footers, bulk 137 | } 138 | 139 | // collectPidsForBareMetal returns the pid of each component. 140 | func collectPidsForBareMetal(pidsDir string) map[string]string { 141 | ret := make(map[string]string) 142 | 143 | if err := filepath.WalkDir(pidsDir, func(path string, d fs.DirEntry, err error) error { 144 | if d.IsDir() { 145 | if d.Name() == metadata.ClusterPidsDir { 146 | return nil 147 | } 148 | 149 | pidPath := filepath.Join(path, "pid") 150 | pid, err := os.ReadFile(pidPath) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | ret[d.Name()] = string(pid) 156 | } 157 | return nil 158 | }); err != nil { 159 | return ret 160 | } 161 | 162 | return ret 163 | } 164 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/get_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package baremetal 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func TestCollectPidsForBareMetal(t *testing.T) { 27 | pidsPath := filepath.Join("testdata", "pids") 28 | want := map[string]string{ 29 | "a": "123", 30 | "b": "456", 31 | "c": "789", 32 | } 33 | 34 | ret := collectPidsForBareMetal(pidsPath) 35 | 36 | assert.Equal(t, want, ret) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/not_implemented.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package baremetal 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 24 | ) 25 | 26 | func (c *Cluster) List(ctx context.Context, options *opt.ListOptions) error { 27 | return fmt.Errorf("do not support") 28 | } 29 | 30 | func (c *Cluster) Scale(ctx context.Context, options *opt.ScaleOptions) error { 31 | return fmt.Errorf("do not support") 32 | } 33 | 34 | func (c *Cluster) Connect(ctx context.Context, options *opt.ConnectOptions) error { 35 | return fmt.Errorf("do not support") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/cluster/baremetal/testdata/pids/a/pid: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /pkg/cluster/baremetal/testdata/pids/b/pid: -------------------------------------------------------------------------------- 1 | 456 -------------------------------------------------------------------------------- /pkg/cluster/baremetal/testdata/pids/c/pid: -------------------------------------------------------------------------------- 1 | 789 -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/cluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/GreptimeTeam/gtctl/pkg/cluster" 23 | "github.com/GreptimeTeam/gtctl/pkg/helm" 24 | "github.com/GreptimeTeam/gtctl/pkg/kube" 25 | "github.com/GreptimeTeam/gtctl/pkg/logger" 26 | ) 27 | 28 | type Cluster struct { 29 | helmLoader *helm.Loader 30 | client *kube.Client 31 | logger logger.Logger 32 | 33 | timeout time.Duration 34 | dryRun bool 35 | } 36 | 37 | type Option func(cluster *Cluster) 38 | 39 | // WithDryRun enables Cluster to dry run. 40 | func WithDryRun(dryRun bool) Option { 41 | return func(c *Cluster) { 42 | c.dryRun = dryRun 43 | } 44 | } 45 | 46 | // WithTimeout enables Cluster to have a timeout. 47 | func WithTimeout(timeout time.Duration) Option { 48 | return func(c *Cluster) { 49 | c.timeout = timeout 50 | } 51 | } 52 | 53 | func NewCluster(l logger.Logger, opts ...Option) (cluster.Operations, error) { 54 | hl, err := helm.NewLoader(l) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | c := &Cluster{ 60 | helmLoader: hl, 61 | logger: l, 62 | } 63 | for _, opt := range opts { 64 | opt(c) 65 | } 66 | 67 | var client *kube.Client 68 | if !c.dryRun { 69 | client, err = kube.NewClient("") 70 | if err != nil { 71 | return nil, err 72 | } 73 | } 74 | c.client = client 75 | 76 | return c, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/connect.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "strconv" 23 | 24 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | 27 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 28 | "github.com/GreptimeTeam/gtctl/pkg/connector" 29 | ) 30 | 31 | func (c *Cluster) Connect(ctx context.Context, options *opt.ConnectOptions) error { 32 | cluster, err := c.get(ctx, &opt.GetOptions{ 33 | Namespace: options.Namespace, 34 | Name: options.Name, 35 | }) 36 | if err != nil && errors.IsNotFound(err) { 37 | c.logger.V(0).Infof("cluster %s in %s not found", options.Name, options.Namespace) 38 | return nil 39 | } 40 | 41 | switch options.Protocol { 42 | case opt.MySQL: 43 | if err = c.connectMySQL(cluster); err != nil { 44 | return fmt.Errorf("error connecting to mysql: %v", err) 45 | } 46 | case opt.Postgres: 47 | if err = c.connectPostgres(cluster); err != nil { 48 | return fmt.Errorf("error connecting to postgres: %v", err) 49 | } 50 | default: 51 | return fmt.Errorf("unsupported connect protocol type") 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (c *Cluster) connectMySQL(cluster *greptimedbclusterv1alpha1.GreptimeDBCluster) error { 58 | return connector.Mysql(strconv.Itoa(int(cluster.Spec.MySQLServicePort)), cluster.Name, c.logger) 59 | } 60 | 61 | func (c *Cluster) connectPostgres(cluster *greptimedbclusterv1alpha1.GreptimeDBCluster) error { 62 | return connector.PostgresSQL(strconv.Itoa(int(cluster.Spec.PostgresServicePort)), cluster.Name, c.logger) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 24 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 25 | "github.com/GreptimeTeam/gtctl/pkg/helm" 26 | ) 27 | 28 | const ( 29 | AliCloudRegistry = "greptime-registry.cn-hangzhou.cr.aliyuncs.com" 30 | 31 | disableRBACConfig = "auth.rbac.create=false,auth.rbac.token.enabled=false," 32 | ) 33 | 34 | func (c *Cluster) Create(ctx context.Context, options *opt.CreateOptions) error { 35 | spinner := options.Spinner 36 | 37 | withSpinner := func(target string, f func(context.Context, *opt.CreateOptions) error) error { 38 | if !c.dryRun && spinner != nil { 39 | spinner.Start(fmt.Sprintf("Installing %s...", target)) 40 | } 41 | 42 | if err := f(ctx, options); err != nil { 43 | if spinner != nil { 44 | spinner.Stop(false, fmt.Sprintf("Installing %s failed", target)) 45 | } 46 | return err 47 | } 48 | 49 | if !c.dryRun { 50 | if spinner != nil { 51 | spinner.Stop(true, fmt.Sprintf("Installing %s successfully 🎉", target)) 52 | } 53 | } 54 | return nil 55 | } 56 | 57 | if err := withSpinner("GreptimeDB Operator", c.createOperator); err != nil { 58 | return err 59 | } 60 | if err := withSpinner("Etcd cluster", c.createEtcdCluster); err != nil { 61 | return err 62 | } 63 | if err := withSpinner("GreptimeDB cluster", c.createCluster); err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | 70 | // createOperator creates GreptimeDB Operator. 71 | func (c *Cluster) createOperator(ctx context.Context, options *opt.CreateOptions) error { 72 | if options.Operator == nil { 73 | return fmt.Errorf("missing create greptimedb operator options") 74 | } 75 | operatorOpt := options.Operator 76 | resourceName, resourceNamespace := OperatorName(), options.Namespace 77 | 78 | if operatorOpt.UseGreptimeCNArtifacts && len(operatorOpt.ImageRegistry) == 0 { 79 | operatorOpt.ConfigValues += fmt.Sprintf("image.registry=%s,", AliCloudRegistry) 80 | } 81 | 82 | opts := &helm.LoadOptions{ 83 | ReleaseName: resourceName, 84 | Namespace: resourceNamespace, 85 | ChartName: artifacts.GreptimeDBOperatorChartName, 86 | ChartVersion: operatorOpt.GreptimeDBOperatorChartVersion, 87 | FromCNRegion: operatorOpt.UseGreptimeCNArtifacts, 88 | ValuesOptions: *operatorOpt, 89 | EnableCache: true, 90 | ValuesFile: operatorOpt.ValuesFile, 91 | } 92 | manifests, err := c.helmLoader.LoadAndRenderChart(ctx, opts) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | if c.dryRun { 98 | c.logger.V(0).Info(string(manifests)) 99 | return nil 100 | } 101 | 102 | if err = c.client.Apply(ctx, manifests); err != nil { 103 | return err 104 | } 105 | 106 | return c.client.WaitForDeploymentReady(ctx, resourceName, resourceNamespace, c.timeout) 107 | } 108 | 109 | // createCluster creates GreptimeDB cluster. 110 | func (c *Cluster) createCluster(ctx context.Context, options *opt.CreateOptions) error { 111 | if options.Cluster == nil { 112 | return fmt.Errorf("missing create greptimedb cluster options") 113 | } 114 | clusterOpt := options.Cluster 115 | resourceName, resourceNamespace := options.Name, options.Namespace 116 | 117 | if clusterOpt.UseGreptimeCNArtifacts && len(clusterOpt.ImageRegistry) == 0 { 118 | clusterOpt.ConfigValues += fmt.Sprintf("image.registry=%s,initializer.registry=%s,", AliCloudRegistry, AliCloudRegistry) 119 | } 120 | 121 | opts := &helm.LoadOptions{ 122 | ReleaseName: resourceName, 123 | Namespace: resourceNamespace, 124 | ChartName: artifacts.GreptimeDBClusterChartName, 125 | ChartVersion: clusterOpt.GreptimeDBChartVersion, 126 | FromCNRegion: clusterOpt.UseGreptimeCNArtifacts, 127 | ValuesOptions: *clusterOpt, 128 | EnableCache: true, 129 | ValuesFile: clusterOpt.ValuesFile, 130 | } 131 | manifests, err := c.helmLoader.LoadAndRenderChart(ctx, opts) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | if c.dryRun { 137 | c.logger.V(0).Info(string(manifests)) 138 | return nil 139 | } 140 | 141 | if err = c.client.Apply(ctx, manifests); err != nil { 142 | return err 143 | } 144 | 145 | return c.client.WaitForClusterReady(ctx, resourceName, resourceNamespace, c.timeout) 146 | } 147 | 148 | // createEtcdCluster creates Etcd cluster. 149 | func (c *Cluster) createEtcdCluster(ctx context.Context, options *opt.CreateOptions) error { 150 | if options.Etcd == nil { 151 | return fmt.Errorf("missing create etcd cluster options") 152 | } 153 | etcdOpt := options.Etcd 154 | resourceName, resourceNamespace := EtcdClusterName(options.Name), options.Namespace 155 | 156 | etcdOpt.ConfigValues += disableRBACConfig 157 | if etcdOpt.UseGreptimeCNArtifacts && len(etcdOpt.ImageRegistry) == 0 { 158 | etcdOpt.ConfigValues += fmt.Sprintf("image.registry=%s,", AliCloudRegistry) 159 | } 160 | 161 | opts := &helm.LoadOptions{ 162 | ReleaseName: resourceName, 163 | Namespace: resourceNamespace, 164 | ChartName: artifacts.EtcdChartName, 165 | ChartVersion: artifacts.DefaultEtcdChartVersion, 166 | FromCNRegion: etcdOpt.UseGreptimeCNArtifacts, 167 | ValuesOptions: *etcdOpt, 168 | EnableCache: true, 169 | ValuesFile: etcdOpt.ValuesFile, 170 | } 171 | manifests, err := c.helmLoader.LoadAndRenderChart(ctx, opts) 172 | if err != nil { 173 | return fmt.Errorf("error while loading helm chart: %v", err) 174 | } 175 | 176 | if c.dryRun { 177 | c.logger.V(0).Info(string(manifests)) 178 | return nil 179 | } 180 | 181 | if err = c.client.Apply(ctx, manifests); err != nil { 182 | return fmt.Errorf("error while applying helm chart: %v", err) 183 | } 184 | 185 | return c.client.WaitForEtcdReady(ctx, resourceName, resourceNamespace, c.timeout) 186 | } 187 | 188 | func EtcdClusterName(clusterName string) string { 189 | return fmt.Sprintf("%s-etcd", clusterName) 190 | } 191 | 192 | func OperatorName() string { 193 | return "greptimedb-operator" 194 | } 195 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/delete.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | 24 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 25 | ) 26 | 27 | func (c *Cluster) Delete(ctx context.Context, options *opt.DeleteOptions) error { 28 | cluster, err := c.get(ctx, &opt.GetOptions{ 29 | Namespace: options.Namespace, 30 | Name: options.Name, 31 | }) 32 | if errors.IsNotFound(err) || cluster == nil { 33 | c.logger.V(0).Infof("Cluster '%s' in '%s' not found", options.Name, options.Namespace) 34 | return nil 35 | } 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // TODO: should wait cluster to be terminated? 41 | c.logger.V(0).Infof("Deleting cluster '%s' in namespace '%s'...", options.Name, options.Namespace) 42 | if err = c.deleteCluster(ctx, options); err != nil { 43 | return err 44 | } 45 | c.logger.V(0).Infof("Cluster '%s' in namespace '%s' is deleted!", options.Name, options.Namespace) 46 | 47 | if options.TearDownEtcd { 48 | c.logger.V(0).Infof("Deleting etcd cluster in namespace '%s'...", options.Namespace) 49 | if err = c.deleteEtcdCluster(ctx, &opt.DeleteOptions{ 50 | Namespace: options.Namespace, 51 | Name: EtcdClusterName(options.Name), 52 | }); err != nil { 53 | return err 54 | } 55 | c.logger.V(0).Infof("Etcd cluster in namespace '%s' is deleted!", options.Namespace) 56 | } 57 | return nil 58 | } 59 | 60 | func (c *Cluster) deleteCluster(ctx context.Context, options *opt.DeleteOptions) error { 61 | return c.client.DeleteCluster(ctx, options.Name, options.Namespace) 62 | } 63 | 64 | func (c *Cluster) deleteEtcdCluster(ctx context.Context, options *opt.DeleteOptions) error { 65 | return c.client.DeleteEtcdCluster(ctx, options.Name, options.Namespace) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | 26 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 27 | ) 28 | 29 | func (c *Cluster) Get(ctx context.Context, options *opt.GetOptions) error { 30 | cluster, err := c.get(ctx, options) 31 | if err != nil && !errors.IsNotFound(err) { 32 | return err 33 | } 34 | if errors.IsNotFound(err) || cluster == nil { 35 | return fmt.Errorf("cluster not found") 36 | } 37 | 38 | c.logger.V(0).Infof("Cluster '%s' in '%s' namespace is running, create at %s\n", 39 | options.Name, options.Namespace, cluster.CreationTimestamp) 40 | 41 | return nil 42 | } 43 | 44 | func (c *Cluster) get(ctx context.Context, options *opt.GetOptions) (*greptimedbclusterv1alpha1.GreptimeDBCluster, error) { 45 | cluster, err := c.client.GetCluster(ctx, options.Name, options.Namespace) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return cluster, nil 50 | } 51 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 24 | "github.com/olekukonko/tablewriter" 25 | "k8s.io/apimachinery/pkg/api/errors" 26 | 27 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 28 | ) 29 | 30 | func (c *Cluster) List(ctx context.Context, options *opt.ListOptions) error { 31 | clusters, err := c.list(ctx) 32 | if err != nil && !errors.IsNotFound(err) { 33 | return err 34 | } 35 | if errors.IsNotFound(err) || clusters == nil { 36 | return fmt.Errorf("clusters not found") 37 | } 38 | 39 | c.renderListView(options.Table, clusters) 40 | 41 | return nil 42 | } 43 | 44 | func (c *Cluster) list(ctx context.Context) (*greptimedbclusterv1alpha1.GreptimeDBClusterList, error) { 45 | clusters, err := c.client.ListClusters(ctx) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return clusters, nil 51 | } 52 | 53 | func (c *Cluster) configListView(table *tablewriter.Table) { 54 | table.SetAutoWrapText(false) 55 | table.SetAutoFormatHeaders(true) 56 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 57 | table.SetAlignment(tablewriter.ALIGN_LEFT) 58 | table.SetCenterSeparator("") 59 | table.SetColumnSeparator("") 60 | table.SetRowSeparator("") 61 | table.SetHeaderLine(false) 62 | table.SetBorder(false) 63 | table.SetTablePadding("\t") 64 | table.SetNoWhiteSpace(true) 65 | } 66 | 67 | func (c *Cluster) renderListView(table *tablewriter.Table, data *greptimedbclusterv1alpha1.GreptimeDBClusterList) { 68 | c.configListView(table) 69 | 70 | table.SetHeader([]string{"Name", "Namespace", "Creation Date"}) 71 | defer table.Render() 72 | 73 | for _, cluster := range data.Items { 74 | table.Append([]string{ 75 | cluster.Name, 76 | cluster.Namespace, 77 | cluster.CreationTimestamp.String(), 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/cluster/kubernetes/scale.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kubernetes 18 | 19 | import ( 20 | "context" 21 | 22 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 23 | 24 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 25 | ) 26 | 27 | func (c *Cluster) Scale(ctx context.Context, options *opt.ScaleOptions) error { 28 | cluster, err := c.get(ctx, &opt.GetOptions{ 29 | Namespace: options.Namespace, 30 | Name: options.Name, 31 | }) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | c.scale(options, cluster) 37 | c.logger.V(0).Infof("Scaling cluster %s in %s from %d to %d\n", 38 | options.Name, options.Namespace, options.OldReplicas, options.NewReplicas) 39 | 40 | if err = c.client.UpdateCluster(ctx, options.Namespace, cluster); err != nil { 41 | return err 42 | } 43 | 44 | return c.client.WaitForClusterReady(ctx, options.Name, options.Namespace, c.timeout) 45 | } 46 | 47 | func (c *Cluster) scale(options *opt.ScaleOptions, cluster *greptimedbclusterv1alpha1.GreptimeDBCluster) { 48 | switch options.ComponentType { 49 | case greptimedbclusterv1alpha1.FrontendComponentKind: 50 | options.OldReplicas = cluster.Spec.Frontend.Replicas 51 | cluster.Spec.Frontend.Replicas = options.NewReplicas 52 | case greptimedbclusterv1alpha1.DatanodeComponentKind: 53 | options.OldReplicas = cluster.Spec.Datanode.Replicas 54 | cluster.Spec.Datanode.Replicas = options.NewReplicas 55 | case greptimedbclusterv1alpha1.MetaComponentKind: 56 | options.OldReplicas = cluster.Spec.Meta.Replicas 57 | cluster.Spec.Meta.Replicas = options.NewReplicas 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/cluster/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package cluster 18 | 19 | import ( 20 | "context" 21 | 22 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 23 | "github.com/olekukonko/tablewriter" 24 | 25 | "github.com/GreptimeTeam/gtctl/pkg/status" 26 | ) 27 | 28 | type Operations interface { 29 | // Get gets the current cluster profile. 30 | Get(ctx context.Context, options *GetOptions) error 31 | 32 | // List lists all cluster profiles. 33 | List(ctx context.Context, options *ListOptions) error 34 | 35 | // Scale scales the current cluster according to NewReplicas in ScaleOptions, 36 | // and refill the OldReplicas in ScaleOptions. 37 | Scale(ctx context.Context, options *ScaleOptions) error 38 | 39 | // Create creates a new cluster. 40 | Create(ctx context.Context, options *CreateOptions) error 41 | 42 | // Delete deletes a specific cluster. 43 | Delete(ctx context.Context, options *DeleteOptions) error 44 | 45 | // Connect connects to a specific cluster. 46 | Connect(ctx context.Context, options *ConnectOptions) error 47 | } 48 | 49 | type GetOptions struct { 50 | Namespace string 51 | Name string 52 | 53 | // Table view render. 54 | Table *tablewriter.Table 55 | } 56 | 57 | type ListOptions struct { 58 | GetOptions 59 | } 60 | 61 | type ScaleOptions struct { 62 | NewReplicas int32 63 | OldReplicas int32 64 | Namespace string 65 | Name string 66 | ComponentType greptimedbclusterv1alpha1.ComponentKind 67 | } 68 | 69 | type DeleteOptions struct { 70 | Namespace string 71 | Name string 72 | TearDownEtcd bool 73 | } 74 | 75 | type CreateOptions struct { 76 | Namespace string 77 | Name string 78 | 79 | Cluster *CreateClusterOptions 80 | Operator *CreateOperatorOptions 81 | Etcd *CreateEtcdOptions 82 | 83 | Spinner *status.Spinner 84 | } 85 | 86 | // CreateClusterOptions is the options to create a GreptimeDB cluster. 87 | type CreateClusterOptions struct { 88 | GreptimeDBChartVersion string 89 | UseGreptimeCNArtifacts bool 90 | ValuesFile string 91 | 92 | ImageRegistry string `helm:"image.registry"` 93 | InitializerImageRegistry string `helm:"initializer.registry"` 94 | DatanodeStorageClassName string `helm:"datanode.storage.storageClassName"` 95 | DatanodeStorageSize string `helm:"datanode.storage.storageSize"` 96 | DatanodeStorageRetainPolicy string `helm:"datanode.storage.storageRetainPolicy"` 97 | EtcdEndPoints string `helm:"meta.etcdEndpoints"` 98 | ConfigValues string `helm:"*"` 99 | } 100 | 101 | // CreateOperatorOptions is the options to create a GreptimeDB operator. 102 | type CreateOperatorOptions struct { 103 | GreptimeDBOperatorChartVersion string 104 | UseGreptimeCNArtifacts bool 105 | ValuesFile string 106 | 107 | ImageRegistry string `helm:"image.registry"` 108 | ConfigValues string `helm:"*"` 109 | } 110 | 111 | // CreateEtcdOptions is the options to create an etcd cluster. 112 | type CreateEtcdOptions struct { 113 | EtcdChartVersion string 114 | UseGreptimeCNArtifacts bool 115 | ValuesFile string 116 | 117 | // The parameters reference: https://artifacthub.io/packages/helm/bitnami/etcd. 118 | EtcdClusterSize string `helm:"replicaCount"` 119 | ImageRegistry string `helm:"image.registry"` 120 | EtcdStorageClassName string `helm:"persistence.storageClass"` 121 | EtcdStorageSize string `helm:"persistence.size"` 122 | ConfigValues string `helm:"*"` 123 | } 124 | 125 | type ConnectProtocol int 126 | 127 | const ( 128 | MySQL ConnectProtocol = iota 129 | Postgres 130 | ) 131 | 132 | type ConnectOptions struct { 133 | Namespace string 134 | Name string 135 | Protocol ConnectProtocol 136 | } 137 | -------------------------------------------------------------------------------- /pkg/components/datanode.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | "net/http" 24 | "path" 25 | "sync" 26 | "time" 27 | 28 | greptimev1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 29 | 30 | "github.com/GreptimeTeam/gtctl/pkg/config" 31 | "github.com/GreptimeTeam/gtctl/pkg/logger" 32 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 33 | ) 34 | 35 | const ( 36 | dataHomeDir = "home" 37 | dataWalDir = "wal" 38 | ) 39 | 40 | type datanode struct { 41 | config *config.Datanode 42 | metaSrvAddr string 43 | 44 | workingDirs WorkingDirs 45 | wg *sync.WaitGroup 46 | logger logger.Logger 47 | 48 | dataHomeDirs []string 49 | allocatedDirs 50 | } 51 | 52 | func NewDataNode(config *config.Datanode, metaSrvAddr string, workingDirs WorkingDirs, 53 | wg *sync.WaitGroup, logger logger.Logger) ClusterComponent { 54 | return &datanode{ 55 | config: config, 56 | metaSrvAddr: metaSrvAddr, 57 | workingDirs: workingDirs, 58 | wg: wg, 59 | logger: logger, 60 | } 61 | } 62 | 63 | func (d *datanode) Name() string { 64 | return string(greptimev1alpha1.DatanodeComponentKind) 65 | } 66 | 67 | func (d *datanode) Start(ctx context.Context, stop context.CancelFunc, binary string) error { 68 | for i := 0; i < d.config.Replicas; i++ { 69 | dirName := fmt.Sprintf("%s.%d", d.Name(), i) 70 | 71 | homeDir := path.Join(d.workingDirs.DataDir, dirName, dataHomeDir) 72 | if err := fileutils.EnsureDir(homeDir); err != nil { 73 | return err 74 | } 75 | d.dataHomeDirs = append(d.dataHomeDirs, homeDir) 76 | 77 | datanodeLogDir := path.Join(d.workingDirs.LogsDir, dirName) 78 | if err := fileutils.EnsureDir(datanodeLogDir); err != nil { 79 | return err 80 | } 81 | d.logsDirs = append(d.logsDirs, datanodeLogDir) 82 | 83 | datanodePidDir := path.Join(d.workingDirs.PidsDir, dirName) 84 | if err := fileutils.EnsureDir(datanodePidDir); err != nil { 85 | return err 86 | } 87 | d.pidsDirs = append(d.pidsDirs, datanodePidDir) 88 | 89 | walDir := path.Join(d.workingDirs.DataDir, dirName, dataWalDir) 90 | if err := fileutils.EnsureDir(walDir); err != nil { 91 | return err 92 | } 93 | d.dataDirs = append(d.dataDirs, path.Join(d.workingDirs.DataDir, dirName)) 94 | 95 | option := &RunOptions{ 96 | Binary: binary, 97 | Name: dirName, 98 | logDir: datanodeLogDir, 99 | pidDir: datanodePidDir, 100 | args: d.BuildArgs(i, walDir, homeDir), 101 | } 102 | if err := runBinary(ctx, stop, option, d.wg, d.logger); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | // Checking component running status with intervals. 108 | ticker := time.NewTicker(500 * time.Millisecond) 109 | defer ticker.Stop() 110 | 111 | CHECKER: 112 | for { 113 | select { 114 | case <-ticker.C: 115 | if d.IsRunning(ctx) { 116 | break CHECKER 117 | } 118 | case <-ctx.Done(): 119 | return fmt.Errorf("status checking failed: %v", ctx.Err()) 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | 126 | func (d *datanode) BuildArgs(params ...interface{}) []string { 127 | logLevel := d.config.LogLevel 128 | if logLevel == "" { 129 | logLevel = DefaultLogLevel 130 | } 131 | 132 | nodeID_, _, homeDir := params[0], params[1], params[2] 133 | nodeID := nodeID_.(int) 134 | 135 | args := []string{ 136 | fmt.Sprintf("--log-level=%s", logLevel), 137 | d.Name(), "start", 138 | fmt.Sprintf("--node-id=%d", nodeID), 139 | fmt.Sprintf("--metasrv-addrs=%s", d.metaSrvAddr), 140 | fmt.Sprintf("--data-home=%s", homeDir), 141 | } 142 | args = GenerateAddrArg("--http-addr", d.config.HTTPAddr, nodeID, args) 143 | args = GenerateAddrArg("--rpc-addr", d.config.RPCAddr, nodeID, args) 144 | 145 | if len(d.config.Config) > 0 { 146 | args = append(args, fmt.Sprintf("-c=%s", d.config.Config)) 147 | } 148 | 149 | return args 150 | } 151 | 152 | func (d *datanode) IsRunning(_ context.Context) bool { 153 | for i := 0; i < d.config.Replicas; i++ { 154 | addr := FormatAddrArg(d.config.HTTPAddr, i) 155 | _, httpPort, err := net.SplitHostPort(addr) 156 | if err != nil { 157 | d.logger.V(5).Infof("failed to split host port in %s: %s", d.Name(), err) 158 | return false 159 | } 160 | 161 | rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) 162 | if err != nil { 163 | d.logger.V(5).Infof("failed to get %s health: %s", d.Name(), err) 164 | return false 165 | } 166 | 167 | if rsp.StatusCode != http.StatusOK { 168 | return false 169 | } 170 | 171 | if err = rsp.Body.Close(); err != nil { 172 | return false 173 | } 174 | } 175 | 176 | return true 177 | } 178 | -------------------------------------------------------------------------------- /pkg/components/etcd.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | "path" 22 | "sync" 23 | 24 | "github.com/GreptimeTeam/gtctl/pkg/logger" 25 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 26 | ) 27 | 28 | type etcd struct { 29 | workingDirs WorkingDirs 30 | wg *sync.WaitGroup 31 | logger logger.Logger 32 | 33 | allocatedDirs 34 | } 35 | 36 | func NewEtcd(workingDirs WorkingDirs, wg *sync.WaitGroup, logger logger.Logger) ClusterComponent { 37 | return &etcd{ 38 | workingDirs: workingDirs, 39 | wg: wg, 40 | logger: logger, 41 | } 42 | } 43 | 44 | func (e *etcd) Name() string { 45 | return "etcd" 46 | } 47 | 48 | func (e *etcd) Start(ctx context.Context, stop context.CancelFunc, binary string) error { 49 | var ( 50 | etcdDataDir = path.Join(e.workingDirs.DataDir, e.Name()) 51 | etcdLogDir = path.Join(e.workingDirs.LogsDir, e.Name()) 52 | etcdPidDir = path.Join(e.workingDirs.PidsDir, e.Name()) 53 | etcdDirs = []string{etcdDataDir, etcdLogDir, etcdPidDir} 54 | ) 55 | for _, dir := range etcdDirs { 56 | if err := fileutils.EnsureDir(dir); err != nil { 57 | return err 58 | } 59 | } 60 | e.dataDirs = append(e.dataDirs, etcdDataDir) 61 | e.logsDirs = append(e.logsDirs, etcdLogDir) 62 | e.pidsDirs = append(e.pidsDirs, etcdPidDir) 63 | 64 | option := &RunOptions{ 65 | Binary: binary, 66 | Name: e.Name(), 67 | logDir: etcdLogDir, 68 | pidDir: etcdPidDir, 69 | args: e.BuildArgs(etcdDataDir), 70 | } 71 | if err := runBinary(ctx, stop, option, e.wg, e.logger); err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (e *etcd) BuildArgs(params ...interface{}) []string { 79 | return []string{"--data-dir", params[0].(string)} 80 | } 81 | 82 | func (e *etcd) IsRunning(_ context.Context) bool { 83 | // Have not implemented the healthy checker now. 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /pkg/components/frontend.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | "path" 24 | "sync" 25 | 26 | greptimedbclusterv1alpha1 "github.com/GreptimeTeam/greptimedb-operator/apis/v1alpha1" 27 | 28 | "github.com/GreptimeTeam/gtctl/pkg/config" 29 | "github.com/GreptimeTeam/gtctl/pkg/logger" 30 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 31 | ) 32 | 33 | type frontend struct { 34 | config *config.Frontend 35 | metaSrvAddr string 36 | 37 | workingDirs WorkingDirs 38 | wg *sync.WaitGroup 39 | logger logger.Logger 40 | 41 | allocatedDirs 42 | } 43 | 44 | func NewFrontend(config *config.Frontend, metaSrvAddr string, workingDirs WorkingDirs, 45 | wg *sync.WaitGroup, logger logger.Logger) ClusterComponent { 46 | return &frontend{ 47 | config: config, 48 | metaSrvAddr: metaSrvAddr, 49 | workingDirs: workingDirs, 50 | wg: wg, 51 | logger: logger, 52 | } 53 | } 54 | 55 | func (f *frontend) Name() string { 56 | return string(greptimedbclusterv1alpha1.FrontendComponentKind) 57 | } 58 | 59 | func (f *frontend) Start(ctx context.Context, stop context.CancelFunc, binary string) error { 60 | for i := 0; i < f.config.Replicas; i++ { 61 | dirName := fmt.Sprintf("%s.%d", f.Name(), i) 62 | 63 | frontendLogDir := path.Join(f.workingDirs.LogsDir, dirName) 64 | if err := fileutils.EnsureDir(frontendLogDir); err != nil { 65 | return err 66 | } 67 | f.logsDirs = append(f.logsDirs, frontendLogDir) 68 | 69 | frontendPidDir := path.Join(f.workingDirs.PidsDir, dirName) 70 | if err := fileutils.EnsureDir(frontendPidDir); err != nil { 71 | return err 72 | } 73 | f.pidsDirs = append(f.pidsDirs, frontendPidDir) 74 | 75 | option := &RunOptions{ 76 | Binary: binary, 77 | Name: dirName, 78 | logDir: frontendLogDir, 79 | pidDir: frontendPidDir, 80 | args: f.BuildArgs(i), 81 | } 82 | if err := runBinary(ctx, stop, option, f.wg, f.logger); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (f *frontend) BuildArgs(params ...interface{}) []string { 91 | logLevel := f.config.LogLevel 92 | if logLevel == "" { 93 | logLevel = DefaultLogLevel 94 | } 95 | 96 | nodeId := params[0].(int) 97 | 98 | args := []string{ 99 | fmt.Sprintf("--log-level=%s", logLevel), 100 | f.Name(), "start", 101 | fmt.Sprintf("--metasrv-addrs=%s", f.metaSrvAddr), 102 | } 103 | 104 | args = GenerateAddrArg("--http-addr", f.config.HTTPAddr, nodeId, args) 105 | args = GenerateAddrArg("--rpc-addr", f.config.GRPCAddr, nodeId, args) 106 | args = GenerateAddrArg("--mysql-addr", f.config.MysqlAddr, nodeId, args) 107 | args = GenerateAddrArg("--postgres-addr", f.config.PostgresAddr, nodeId, args) 108 | 109 | if len(f.config.Config) > 0 { 110 | args = append(args, fmt.Sprintf("-c=%s", f.config.Config)) 111 | } 112 | if len(f.config.UserProvider) > 0 { 113 | args = append(args, fmt.Sprintf("--user-provider=%s", f.config.UserProvider)) 114 | } 115 | return args 116 | } 117 | 118 | func (f *frontend) IsRunning(_ context.Context) bool { 119 | for i := 0; i < f.config.Replicas; i++ { 120 | addr := FormatAddrArg(f.config.HTTPAddr, i) 121 | healthy := fmt.Sprintf("http://%s/health", addr) 122 | 123 | resp, err := http.Get(healthy) 124 | if err != nil { 125 | f.logger.V(5).Infof("Failed to get %s healthy: %s", f.Name(), err) 126 | return false 127 | } 128 | 129 | if resp.StatusCode != http.StatusOK { 130 | f.logger.V(5).Infof("%s is not healthy: %s", f.Name(), resp) 131 | return false 132 | } 133 | 134 | if err = resp.Body.Close(); err != nil { 135 | f.logger.V(5).Infof("%s is not healthy: %s, err: %s", f.Name(), resp, err) 136 | return false 137 | } 138 | } 139 | return true 140 | } 141 | -------------------------------------------------------------------------------- /pkg/components/metasrv.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | "net/http" 24 | "path" 25 | "strconv" 26 | "sync" 27 | "time" 28 | 29 | "github.com/GreptimeTeam/gtctl/pkg/config" 30 | "github.com/GreptimeTeam/gtctl/pkg/logger" 31 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 32 | ) 33 | 34 | type metaSrv struct { 35 | config *config.MetaSrv 36 | 37 | workingDirs WorkingDirs 38 | wg *sync.WaitGroup 39 | logger logger.Logger 40 | useMemoryMeta bool 41 | 42 | allocatedDirs 43 | } 44 | 45 | func NewMetaSrv(config *config.MetaSrv, workingDirs WorkingDirs, 46 | wg *sync.WaitGroup, logger logger.Logger, useMemoryMeta bool) ClusterComponent { 47 | return &metaSrv{ 48 | config: config, 49 | workingDirs: workingDirs, 50 | wg: wg, 51 | logger: logger, 52 | useMemoryMeta: useMemoryMeta, 53 | } 54 | } 55 | 56 | func (m *metaSrv) Name() string { 57 | return "metasrv" 58 | } 59 | 60 | func (m *metaSrv) Start(ctx context.Context, stop context.CancelFunc, binary string) error { 61 | // Default bind address for meta srv. 62 | bindAddr := net.JoinHostPort("127.0.0.1", "3002") 63 | if len(m.config.BindAddr) > 0 { 64 | bindAddr = m.config.BindAddr 65 | } 66 | 67 | for i := 0; i < m.config.Replicas; i++ { 68 | dirName := fmt.Sprintf("%s.%d", m.Name(), i) 69 | 70 | metaSrvLogDir := path.Join(m.workingDirs.LogsDir, dirName) 71 | if err := fileutils.EnsureDir(metaSrvLogDir); err != nil { 72 | return err 73 | } 74 | m.logsDirs = append(m.logsDirs, metaSrvLogDir) 75 | 76 | metaSrvPidDir := path.Join(m.workingDirs.PidsDir, dirName) 77 | if err := fileutils.EnsureDir(metaSrvPidDir); err != nil { 78 | return err 79 | } 80 | m.pidsDirs = append(m.pidsDirs, metaSrvPidDir) 81 | option := &RunOptions{ 82 | Binary: binary, 83 | Name: dirName, 84 | logDir: metaSrvLogDir, 85 | pidDir: metaSrvPidDir, 86 | args: m.BuildArgs(i, bindAddr), 87 | } 88 | if err := runBinary(ctx, stop, option, m.wg, m.logger); err != nil { 89 | return err 90 | } 91 | } 92 | 93 | // Checking component running status with intervals. 94 | ticker := time.NewTicker(500 * time.Millisecond) 95 | defer ticker.Stop() 96 | 97 | CHECKER: 98 | for { 99 | select { 100 | case <-ticker.C: 101 | if m.IsRunning(ctx) { 102 | break CHECKER 103 | } 104 | case <-ctx.Done(): 105 | return fmt.Errorf("status checking failed: %v", ctx.Err()) 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (m *metaSrv) BuildArgs(params ...interface{}) []string { 113 | logLevel := m.config.LogLevel 114 | if logLevel == "" { 115 | logLevel = DefaultLogLevel 116 | } 117 | 118 | nodeID_, bindAddr_ := params[0], params[1] 119 | nodeID := nodeID_.(int) 120 | bindAddr := bindAddr_.(string) 121 | 122 | args := []string{ 123 | fmt.Sprintf("--log-level=%s", logLevel), 124 | m.Name(), "start", 125 | fmt.Sprintf("--store-addr=%s", m.config.StoreAddr), 126 | fmt.Sprintf("--server-addr=%s", m.config.ServerAddr), 127 | } 128 | args = GenerateAddrArg("--http-addr", m.config.HTTPAddr, nodeID, args) 129 | args = GenerateAddrArg("--bind-addr", bindAddr, nodeID, args) 130 | 131 | if m.useMemoryMeta { 132 | useMemoryMeta := strconv.FormatBool(m.useMemoryMeta) 133 | args = GenerateAddrArg("--use-memory-store", useMemoryMeta, nodeID, args) 134 | } 135 | 136 | if len(m.config.Config) > 0 { 137 | args = append(args, fmt.Sprintf("-c=%s", m.config.Config)) 138 | } 139 | 140 | return args 141 | } 142 | 143 | func (m *metaSrv) IsRunning(_ context.Context) bool { 144 | for i := 0; i < m.config.Replicas; i++ { 145 | addr := FormatAddrArg(m.config.HTTPAddr, i) 146 | _, httpPort, err := net.SplitHostPort(addr) 147 | if err != nil { 148 | m.logger.V(5).Infof("failed to split host port in %s: %s", m.Name(), err) 149 | return false 150 | } 151 | 152 | rsp, err := http.Get(fmt.Sprintf("http://localhost:%s/health", httpPort)) 153 | if err != nil { 154 | m.logger.V(5).Infof("failed to get %s health: %s", m.Name(), err) 155 | return false 156 | } 157 | 158 | if rsp.StatusCode != http.StatusOK { 159 | return false 160 | } 161 | 162 | if err = rsp.Body.Close(); err != nil { 163 | return false 164 | } 165 | } 166 | 167 | return true 168 | } 169 | -------------------------------------------------------------------------------- /pkg/components/run.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "bufio" 21 | "context" 22 | "os" 23 | "os/exec" 24 | "path" 25 | "strconv" 26 | "sync" 27 | "syscall" 28 | 29 | "github.com/GreptimeTeam/gtctl/pkg/logger" 30 | ) 31 | 32 | // RunOptions contains all the options for one component to run on bare-metal. 33 | type RunOptions struct { 34 | Binary string 35 | Name string 36 | 37 | pidDir string 38 | logDir string 39 | args []string 40 | } 41 | 42 | func runBinary(ctx context.Context, stop context.CancelFunc, 43 | option *RunOptions, wg *sync.WaitGroup, logger logger.Logger) error { 44 | cmd := exec.CommandContext(ctx, option.Binary, option.args...) 45 | 46 | // output to binary. 47 | logFile := path.Join(option.logDir, "log") 48 | outputFile, err := os.Create(logFile) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | outputFileWriter := bufio.NewWriter(outputFile) 54 | cmd.Stdout = outputFileWriter 55 | cmd.Stderr = outputFileWriter 56 | 57 | if err = cmd.Start(); err != nil { 58 | return err 59 | } 60 | 61 | pid := strconv.Itoa(cmd.Process.Pid) 62 | logger.V(3).Infof("run '%s' binary '%s' with args: '%v', log: '%s', pid: '%s'", 63 | option.Name, option.Binary, option.args, option.logDir, pid) 64 | 65 | pidFile := path.Join(option.pidDir, "pid") 66 | f, err := os.Create(pidFile) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | _, err = f.Write([]byte(pid)) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | go func() { 77 | defer wg.Done() 78 | wg.Add(1) 79 | if err := cmd.Wait(); err != nil { 80 | // Caught signal kill and interrupt error then ignore. 81 | if exit, ok := err.(*exec.ExitError); ok { 82 | if status, ok := exit.Sys().(syscall.WaitStatus); ok && status.Signaled() { 83 | if status.Signal() == syscall.SIGKILL || status.Signal() == syscall.SIGINT { 84 | return 85 | } 86 | } 87 | } 88 | logger.Errorf("component '%s' binary '%s' (pid '%s') exited with error: %v", option.Name, option.Binary, pid, err) 89 | logger.Errorf("args: '%v'", option.args) 90 | _ = outputFileWriter.Flush() 91 | 92 | // If one component has failed, stop the whole context. 93 | stop() 94 | } 95 | }() 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/components/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | const ( 24 | DefaultLogLevel = "info" 25 | ) 26 | 27 | // WorkingDirs include all the directories used in bare-metal mode. 28 | type WorkingDirs struct { 29 | DataDir string `yaml:"dataDir"` 30 | LogsDir string `yaml:"logsDir"` 31 | PidsDir string `yaml:"pidsDir"` 32 | } 33 | 34 | // allocatedDirs include all the directories that created during bare-metal mode. 35 | type allocatedDirs struct { 36 | dataDirs []string 37 | logsDirs []string 38 | pidsDirs []string 39 | } 40 | 41 | // ClusterComponent is the basic component of running GreptimeDB Cluster in bare-metal mode. 42 | type ClusterComponent interface { 43 | // Start starts cluster component by executing binary. 44 | Start(ctx context.Context, stop context.CancelFunc, binary string) error 45 | 46 | // BuildArgs build up args for cluster component. 47 | BuildArgs(params ...interface{}) []string 48 | 49 | // IsRunning returns the status of current cluster component. 50 | IsRunning(ctx context.Context) bool 51 | 52 | // Name return the name of component. 53 | Name() string 54 | } 55 | -------------------------------------------------------------------------------- /pkg/components/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package components 18 | 19 | import ( 20 | "fmt" 21 | "net" 22 | "strconv" 23 | ) 24 | 25 | // FormatAddrArg formats the given addr and nodeId to a valid socket string. 26 | // This function will return an empty string when the given addr is empty. 27 | func FormatAddrArg(addr string, nodeId int) string { 28 | // return empty result if the address is not specified 29 | if len(addr) == 0 { 30 | return addr 31 | } 32 | 33 | // The "addr" is validated when set. 34 | host, port, _ := net.SplitHostPort(addr) 35 | portInt, _ := strconv.Atoi(port) 36 | 37 | return net.JoinHostPort(host, strconv.Itoa(portInt+nodeId)) 38 | } 39 | 40 | // GenerateAddrArg pushes arg into args array, return the new args array. 41 | func GenerateAddrArg(config string, addr string, nodeId int, args []string) []string { 42 | socketAddr := FormatAddrArg(addr, nodeId) 43 | 44 | // don't generate param if the socket address is empty 45 | if len(socketAddr) == 0 { 46 | return args 47 | } 48 | 49 | return append(args, fmt.Sprintf("%s=%s", config, socketAddr)) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/config/baremetal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "time" 21 | 22 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 23 | ) 24 | 25 | // BareMetalClusterMetadata stores metadata of a GreptimeDB cluster. 26 | type BareMetalClusterMetadata struct { 27 | Config *BareMetalClusterConfig `yaml:"config"` 28 | CreationDate time.Time `yaml:"creationDate"` 29 | ClusterDir string `yaml:"clusterDir"` 30 | ForegroundPid int `yaml:"foregroundPid"` 31 | } 32 | 33 | // BareMetalClusterConfig is the desired state of a GreptimeDB cluster on bare metal. 34 | // 35 | // The field of BareMetalClusterConfig that with `validate` tag will be validated 36 | // against its requirement. Each filed has only one requirement. 37 | // 38 | // Each field of BareMetalClusterConfig can also have its own exported method `Validate`. 39 | type BareMetalClusterConfig struct { 40 | Cluster *BareMetalClusterComponentsConfig `yaml:"cluster" validate:"required"` 41 | Etcd *Etcd `yaml:"etcd" validate:"required"` 42 | } 43 | 44 | type BareMetalClusterComponentsConfig struct { 45 | Artifact *Artifact `yaml:"artifact" validate:"required"` 46 | Frontend *Frontend `yaml:"frontend" validate:"required"` 47 | MetaSrv *MetaSrv `yaml:"meta" validate:"required"` 48 | Datanode *Datanode `yaml:"datanode" validate:"required"` 49 | } 50 | 51 | type Artifact struct { 52 | // Local is the local path of binary(greptime or etcd). 53 | Local string `yaml:"local" validate:"omitempty,filepath"` 54 | 55 | // Version is the release version of binary(greptime or etcd). 56 | // Usually, it points to the version of binary of GitHub release. 57 | Version string `yaml:"version"` 58 | } 59 | 60 | type Datanode struct { 61 | NodeID int `yaml:"nodeID" validate:"gte=0"` 62 | RPCAddr string `yaml:"rpcAddr" validate:"required,hostname_port"` 63 | HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` 64 | DataDir string `yaml:"dataDir" validate:"omitempty,dirpath"` 65 | WalDir string `yaml:"walDir" validate:"omitempty,dirpath"` 66 | ProcedureDir string `yaml:"procedureDir" validate:"omitempty,dirpath"` 67 | 68 | Replicas int `yaml:"replicas" validate:"gt=0"` 69 | Config string `yaml:"config" validate:"omitempty,filepath"` 70 | LogLevel string `yaml:"logLevel"` 71 | } 72 | 73 | type Frontend struct { 74 | GRPCAddr string `yaml:"grpcAddr" validate:"omitempty,hostname_port"` 75 | HTTPAddr string `yaml:"httpAddr" validate:"omitempty,hostname_port"` 76 | PostgresAddr string `yaml:"postgresAddr" validate:"omitempty,hostname_port"` 77 | MetaAddr string `yaml:"metaAddr" validate:"omitempty,hostname_port"` 78 | MysqlAddr string `yaml:"mysqlAddr" validate:"omitempty,hostname_port"` 79 | 80 | Replicas int `yaml:"replicas" validate:"gt=0"` 81 | Config string `yaml:"config" validate:"omitempty,filepath"` 82 | LogLevel string `yaml:"logLevel"` 83 | UserProvider string `yaml:"userProvider"` 84 | } 85 | 86 | type MetaSrv struct { 87 | StoreAddr string `yaml:"storeAddr" validate:"hostname_port"` 88 | ServerAddr string `yaml:"serverAddr" validate:"hostname_port"` 89 | BindAddr string `yaml:"bindAddr" validate:"omitempty,hostname_port"` 90 | HTTPAddr string `yaml:"httpAddr" validate:"required,hostname_port"` 91 | 92 | Replicas int `yaml:"replicas" validate:"gt=0"` 93 | Config string `yaml:"config" validate:"omitempty,filepath"` 94 | LogLevel string `yaml:"logLevel"` 95 | } 96 | 97 | type Etcd struct { 98 | Artifact *Artifact `yaml:"artifact" validate:"required"` 99 | } 100 | 101 | func DefaultBareMetalConfig() *BareMetalClusterConfig { 102 | return &BareMetalClusterConfig{ 103 | Cluster: &BareMetalClusterComponentsConfig{ 104 | Artifact: &Artifact{ 105 | Version: artifacts.LatestVersionTag, 106 | }, 107 | Frontend: &Frontend{ 108 | Replicas: 1, 109 | HTTPAddr: "0.0.0.0:4000", 110 | GRPCAddr: "0.0.0.0:4001", 111 | MysqlAddr: "0.0.0.0:4002", 112 | PostgresAddr: "0.0.0.0:4003", 113 | }, 114 | MetaSrv: &MetaSrv{ 115 | Replicas: 1, 116 | StoreAddr: "127.0.0.1:2379", 117 | ServerAddr: "0.0.0.0:3002", 118 | HTTPAddr: "0.0.0.0:14001", 119 | }, 120 | Datanode: &Datanode{ 121 | Replicas: 3, 122 | RPCAddr: "0.0.0.0:14100", 123 | HTTPAddr: "0.0.0.0:14300", 124 | }, 125 | }, 126 | Etcd: &Etcd{ 127 | Artifact: &Artifact{ 128 | Version: artifacts.DefaultEtcdBinVersion, 129 | }, 130 | }, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pkg/config/kubernetes.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | ) 23 | 24 | const ( 25 | // Various of support config type 26 | configOperator = "operator" 27 | configCluster = "cluster" 28 | configEtcd = "etcd" 29 | ) 30 | 31 | type SetValues struct { 32 | RawConfig []string 33 | 34 | OperatorConfig string 35 | ClusterConfig string 36 | EtcdConfig string 37 | } 38 | 39 | // Parse parses raw config values and classify it to different 40 | // categories of config type by its prefix. 41 | func (c *SetValues) Parse() error { 42 | var ( 43 | operatorConfig []string 44 | clusterConfig []string 45 | etcdConfig []string 46 | ) 47 | 48 | for _, raw := range c.RawConfig { 49 | if len(raw) == 0 { 50 | return fmt.Errorf("cannot parse empty config values") 51 | } 52 | 53 | var configPrefix, configValue string 54 | values := strings.Split(raw, ",") 55 | 56 | for _, value := range values { 57 | value = strings.Trim(value, " ") 58 | cfg := strings.SplitN(value, ".", 2) 59 | configPrefix = cfg[0] 60 | if len(cfg) == 2 { 61 | configValue = cfg[1] 62 | } else { 63 | configValue = configPrefix 64 | } 65 | 66 | switch configPrefix { 67 | case configOperator: 68 | operatorConfig = append(operatorConfig, configValue) 69 | case configCluster: 70 | clusterConfig = append(clusterConfig, configValue) 71 | case configEtcd: 72 | etcdConfig = append(etcdConfig, configValue) 73 | default: 74 | clusterConfig = append(clusterConfig, value) 75 | } 76 | } 77 | } 78 | 79 | if len(operatorConfig) > 0 { 80 | c.OperatorConfig = strings.Join(operatorConfig, ",") 81 | } 82 | if len(clusterConfig) > 0 { 83 | c.ClusterConfig = strings.Join(clusterConfig, ",") 84 | } 85 | if len(etcdConfig) > 0 { 86 | c.EtcdConfig = strings.Join(etcdConfig, ",") 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/config/kubernetes_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestParseConfig(t *testing.T) { 26 | testCases := []struct { 27 | name string 28 | config []string 29 | expect SetValues 30 | err bool 31 | }{ 32 | { 33 | name: "all-with-prefix", 34 | config: []string{"cluster.foo=bar", "etcd.foo=bar", "operator.foo=bar"}, 35 | expect: SetValues{ 36 | ClusterConfig: "foo=bar", 37 | EtcdConfig: "foo=bar", 38 | OperatorConfig: "foo=bar", 39 | }, 40 | }, 41 | { 42 | name: "all-without-prefix", 43 | config: []string{"foo=bar", "foo.boo=bar", "foo.boo.coo=bar"}, 44 | expect: SetValues{ 45 | ClusterConfig: "foo=bar,foo.boo=bar,foo.boo.coo=bar", 46 | }, 47 | }, 48 | { 49 | name: "mix-with-prefix", 50 | config: []string{"etcd.foo=bar", "foo.boo=bar", "foo.boo.coo=bar"}, 51 | expect: SetValues{ 52 | ClusterConfig: "foo.boo=bar,foo.boo.coo=bar", 53 | EtcdConfig: "foo=bar", 54 | }, 55 | }, 56 | { 57 | name: "empty-values", 58 | config: []string{""}, 59 | err: true, 60 | }, 61 | { 62 | name: "empty-config", 63 | config: []string{}, 64 | expect: SetValues{}, 65 | }, 66 | } 67 | 68 | for _, tc := range testCases { 69 | t.Run(tc.name, func(t *testing.T) { 70 | actual := SetValues{RawConfig: tc.config} 71 | err := actual.Parse() 72 | 73 | if tc.err { 74 | assert.Error(t, err) 75 | return 76 | } 77 | 78 | tc.expect.RawConfig = tc.config 79 | assert.Equal(t, tc.expect, actual) 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/config/testdata/validate/invalid_artifact.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: v0.2.0-nightly-20230403 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 0.0.0.0:3002 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: # invalid artifact 20 | version: 21 | local: 22 | 23 | -------------------------------------------------------------------------------- /pkg/config/testdata/validate/invalid_hostname_port.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: v0.2.0-nightly-20230403 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:1438000 # invalid port 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 6870.0.0.0:3243002 # invalid hostname and port 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: 20 | version: v3.5.7 21 | -------------------------------------------------------------------------------- /pkg/config/testdata/validate/invalid_replicas.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: v0.2.0-nightly-20230403 5 | frontend: 6 | replicas: 0 # invalid replicas 7 | datanode: 8 | replicas: -3 # invalid replicas 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 0.0.0.0:3002 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: 20 | version: v3.5.7 21 | -------------------------------------------------------------------------------- /pkg/config/testdata/validate/valid_config.yaml: -------------------------------------------------------------------------------- 1 | cluster: 2 | name: mycluster # name of the cluster 3 | artifact: 4 | version: v0.2.0-nightly-20230403 5 | frontend: 6 | replicas: 1 7 | datanode: 8 | replicas: 3 9 | rpcAddr: 0.0.0.0:14100 10 | mysqlAddr: 0.0.0.0:14200 11 | httpAddr: 0.0.0.0:14300 12 | meta: 13 | replicas: 1 14 | storeAddr: 127.0.0.1:2379 15 | serverAddr: 0.0.0.0:3002 16 | httpAddr: 0.0.0.0:14001 17 | 18 | etcd: 19 | artifact: 20 | version: v3.5.7 21 | -------------------------------------------------------------------------------- /pkg/config/validate.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/go-playground/validator/v10" 23 | ) 24 | 25 | var validate *validator.Validate 26 | 27 | // ValidateConfig validate config in bare-metal mode. 28 | func ValidateConfig(config *BareMetalClusterConfig) error { 29 | if config == nil { 30 | return fmt.Errorf("no config to validate") 31 | } 32 | 33 | validate = validator.New() 34 | 35 | // Register custom validation method for Artifact. 36 | validate.RegisterStructValidation(ValidateArtifact, Artifact{}) 37 | 38 | err := validate.Struct(config) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func ValidateArtifact(sl validator.StructLevel) { 47 | artifact := sl.Current().Interface().(Artifact) 48 | if len(artifact.Version) == 0 && len(artifact.Local) == 0 { 49 | sl.ReportError(sl.Current().Interface(), "Artifact", "Version/Local", "", "") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/config/validate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package config 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | "gopkg.in/yaml.v3" 27 | ) 28 | 29 | func TestValidateConfig(t *testing.T) { 30 | testCases := []struct { 31 | name string 32 | expect bool 33 | errKey []string 34 | }{ 35 | { 36 | name: "valid_config", 37 | expect: true, 38 | }, 39 | { 40 | name: "invalid_hostname_port", 41 | expect: false, 42 | errKey: []string{ 43 | "Config.Cluster.MetaSrv.ServerAddr", 44 | "Config.Cluster.Datanode.HTTPAddr", 45 | }, 46 | }, 47 | { 48 | name: "invalid_replicas", 49 | expect: false, 50 | errKey: []string{ 51 | "Config.Cluster.Frontend.Replicas", 52 | "Config.Cluster.Datanode.Replicas", 53 | }, 54 | }, 55 | { 56 | name: "invalid_artifact", 57 | expect: false, 58 | errKey: []string{ 59 | "Config.Etcd.Artifact.Artifact", 60 | }, 61 | }, 62 | } 63 | 64 | for _, tc := range testCases { 65 | t.Run(tc.name, func(t *testing.T) { 66 | var actual BareMetalClusterConfig 67 | if err := loadConfig(filepath.Join("testdata", "validate", 68 | fmt.Sprintf("%s.yaml", tc.name)), &actual); err != nil { 69 | t.Errorf("error while loading %s file: %v", tc.name, err) 70 | } 71 | 72 | err := ValidateConfig(&actual) 73 | if tc.expect { 74 | assert.NoError(t, err) 75 | } else { 76 | assert.Error(t, err) 77 | for _, key := range tc.errKey { 78 | assert.Contains(t, err.Error(), key) 79 | } 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func loadConfig(path string, ret *BareMetalClusterConfig) error { 86 | configs, err := os.ReadFile(path) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if err = yaml.Unmarshal(configs, ret); err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/connector/mysql.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package connector 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "fmt" 23 | "net" 24 | "os" 25 | "os/exec" 26 | "sync" 27 | "syscall" 28 | 29 | "github.com/go-sql-driver/mysql" 30 | 31 | "github.com/GreptimeTeam/gtctl/pkg/logger" 32 | ) 33 | 34 | const ( 35 | mySQLDriver = "mysql" 36 | mySQLDefaultAddr = "127.0.0.1" 37 | mySQLDefaultNet = "tcp" 38 | 39 | mySQLPortArg = "-P" 40 | mySQLHostArg = "-h" 41 | 42 | kubectl = "kubectl" 43 | portForward = "port-forward" 44 | ) 45 | 46 | // Mysql connects to a GreptimeDB cluster using mysql protocol. 47 | func Mysql(port, clusterName string, l logger.Logger) error { 48 | waitGroup := sync.WaitGroup{} 49 | 50 | // TODO: is there any elegant way to enable port-forward? 51 | cmd := exec.CommandContext(context.Background(), kubectl, portForward, "-n", "default", "svc/"+clusterName+"-frontend", fmt.Sprintf("%s:%s", port, port)) 52 | if err := cmd.Start(); err != nil { 53 | l.Errorf("Error starting port-forwarding: %v", err) 54 | return err 55 | } 56 | 57 | waitGroup.Add(1) 58 | go func() { 59 | defer waitGroup.Done() 60 | if err := cmd.Wait(); err != nil { 61 | // exit status 1 62 | exitError, ok := err.(*exec.ExitError) 63 | if !ok { 64 | l.Errorf("Error waiting for port-forwarding to finish: %v", err) 65 | return 66 | } 67 | if exitError.Sys().(syscall.WaitStatus).ExitStatus() == 1 { 68 | return 69 | } 70 | } 71 | }() 72 | 73 | for { 74 | cfg := mysql.Config{ 75 | Net: mySQLDefaultNet, 76 | Addr: net.JoinHostPort(mySQLDefaultAddr, port), 77 | User: "", 78 | Passwd: "", 79 | DBName: "", 80 | AllowNativePasswords: true, 81 | } 82 | 83 | db, err := sql.Open(mySQLDriver, cfg.FormatDSN()) 84 | if err != nil { 85 | continue 86 | } 87 | 88 | if _, err = db.Conn(context.Background()); err != nil { 89 | continue 90 | } 91 | 92 | if err = db.Close(); err != nil { 93 | if err == os.ErrProcessDone { 94 | return nil 95 | } 96 | return err 97 | } 98 | 99 | break 100 | } 101 | 102 | cmd = mysqlCommand(port) 103 | cmd.Stdout = os.Stdout 104 | cmd.Stderr = os.Stderr 105 | cmd.Stdin = os.Stdin 106 | if err := cmd.Start(); err != nil { 107 | l.Errorf("Error starting mysql client: %v", err) 108 | return err 109 | } 110 | 111 | if err := cmd.Wait(); err != nil { 112 | l.Errorf("Error waiting for mysql client to finish: %v", err) 113 | return err 114 | } 115 | 116 | // gracefully stop port-forwarding 117 | if err := cmd.Process.Kill(); err != nil { 118 | if err == os.ErrProcessDone { 119 | l.V(1).Info("Shutting down port-forwarding successfully") 120 | return nil 121 | } 122 | return err 123 | } 124 | 125 | waitGroup.Wait() 126 | return nil 127 | } 128 | 129 | func mysqlCommand(port string) *exec.Cmd { 130 | return exec.Command(mySQLDriver, mySQLHostArg, mySQLDefaultAddr, mySQLPortArg, port) 131 | } 132 | -------------------------------------------------------------------------------- /pkg/connector/postgres.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package connector 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | "os" 24 | "os/exec" 25 | "sync" 26 | "syscall" 27 | 28 | "github.com/go-pg/pg/v10" 29 | 30 | "github.com/GreptimeTeam/gtctl/pkg/logger" 31 | ) 32 | 33 | const ( 34 | postgresSQLDriver = "psql" 35 | postgresSQLDatabaseName = "public" 36 | postgresSQLDefaultAddr = "127.0.0.1" 37 | postgresSQLDefaultNet = "tcp" 38 | 39 | postgresSQLHostArg = "-h" 40 | postgresSQLPortArg = "-p" 41 | postgresSQLDatabaseArg = "-d" 42 | ) 43 | 44 | // PostgresSQL connects to a GreptimeDB cluster using postgres protocol. 45 | func PostgresSQL(port, clusterName string, l logger.Logger) error { 46 | waitGroup := sync.WaitGroup{} 47 | 48 | // TODO: is there any elegant way to enable port-forward? 49 | cmd := exec.CommandContext(context.Background(), kubectl, portForward, "-n", "default", "svc/"+clusterName+"-frontend", fmt.Sprintf("%s:%s", port, port)) 50 | if err := cmd.Start(); err != nil { 51 | l.Errorf("Error starting port-forwarding: %v", err) 52 | return err 53 | } 54 | 55 | defer func() { 56 | if recover() != nil { 57 | if err := cmd.Process.Kill(); err != nil { 58 | l.Errorf("Error killing port-forwarding process: %v", err) 59 | } 60 | } 61 | }() 62 | 63 | waitGroup.Add(1) 64 | go func() { 65 | defer waitGroup.Done() 66 | if err := cmd.Wait(); err != nil { 67 | // exit status 1 68 | exitError, ok := err.(*exec.ExitError) 69 | if !ok { 70 | l.Errorf("Error waiting for port-forwarding to finish: %v", err) 71 | return 72 | } 73 | if exitError.Sys().(syscall.WaitStatus).ExitStatus() == 1 { 74 | return 75 | } 76 | } 77 | }() 78 | 79 | for { 80 | opt := &pg.Options{ 81 | Addr: net.JoinHostPort(postgresSQLDefaultAddr, port), 82 | Network: postgresSQLDefaultNet, 83 | Database: postgresSQLDatabaseName, 84 | } 85 | db := pg.Connect(opt) 86 | if _, err := db.Exec("SELECT 1"); err == nil { 87 | break 88 | } 89 | } 90 | 91 | cmd = postgresSQLCommand(port) 92 | cmd.Stdout = os.Stdout 93 | cmd.Stderr = os.Stderr 94 | cmd.Stdin = os.Stdin 95 | if err := cmd.Start(); err != nil { 96 | l.Errorf("Error starting pg: %v", err) 97 | return err 98 | } 99 | 100 | if err := cmd.Wait(); err != nil { 101 | l.Errorf("Error waiting for pg client to finish: %v", err) 102 | return err 103 | } 104 | 105 | // gracefully stop port-forwarding 106 | if err := cmd.Process.Kill(); err != nil { 107 | if err == os.ErrProcessDone { 108 | l.V(1).Info("Shutting down port-forwarding successfully") 109 | return nil 110 | } 111 | return err 112 | } 113 | 114 | waitGroup.Wait() 115 | return nil 116 | } 117 | 118 | func postgresSQLCommand(port string) *exec.Cmd { 119 | return exec.Command(postgresSQLDriver, postgresSQLHostArg, postgresSQLDefaultAddr, 120 | postgresSQLPortArg, port, postgresSQLDatabaseArg, postgresSQLDatabaseName) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/helm/loader.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package helm 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "os" 24 | "strings" 25 | 26 | "helm.sh/helm/v3/pkg/action" 27 | "helm.sh/helm/v3/pkg/chart" 28 | "helm.sh/helm/v3/pkg/chart/loader" 29 | "helm.sh/helm/v3/pkg/chartutil" 30 | 31 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 32 | "github.com/GreptimeTeam/gtctl/pkg/logger" 33 | "github.com/GreptimeTeam/gtctl/pkg/metadata" 34 | ) 35 | 36 | var ( 37 | // KubeVersion is the target version of the kubernetes. 38 | KubeVersion = "v1.20.0" 39 | ) 40 | 41 | // Loader is the Helm charts loader. The implementation is based on Helm SDK. 42 | // The main purpose of Loader is: 43 | // 1. Load the chart from remote charts and save them in cache directory. 44 | // 2. Generate the manifests from the chart with the values. 45 | type Loader struct { 46 | // logger is the logger for the Loader. 47 | logger logger.Logger 48 | 49 | // am is the artifacts manager to manage charts. 50 | am artifacts.Manager 51 | 52 | // mm is the metadata manager to manage the metadata. 53 | mm metadata.Manager 54 | } 55 | 56 | type Option func(*Loader) 57 | 58 | func NewLoader(l logger.Logger, opts ...Option) (*Loader, error) { 59 | r := &Loader{logger: l} 60 | 61 | am, err := artifacts.NewManager(l) 62 | if err != nil { 63 | return nil, err 64 | } 65 | r.am = am 66 | 67 | mm, err := metadata.New("") 68 | if err != nil { 69 | return nil, err 70 | } 71 | r.mm = mm 72 | 73 | for _, opt := range opts { 74 | opt(r) 75 | } 76 | 77 | return r, nil 78 | } 79 | 80 | func WithHomeDir(dir string) Option { 81 | return func(r *Loader) { 82 | mm, err := metadata.New(dir) 83 | if err != nil { 84 | r.logger.Errorf("failed to create metadata manager: %v", err) 85 | os.Exit(1) 86 | } 87 | r.mm = mm 88 | } 89 | } 90 | 91 | // LoadOptions is the options for running LoadAndRenderChart. 92 | type LoadOptions struct { 93 | // ReleaseName is the name of the release. 94 | ReleaseName string 95 | 96 | // Namespace is the namespace of the release. 97 | Namespace string 98 | 99 | // ChartName is the name of the chart. 100 | ChartName string 101 | 102 | // ChartVersion is the version of the chart. 103 | ChartVersion string 104 | 105 | // FromCNRegion indicates whether to use the artifacts from CN region. 106 | FromCNRegion bool 107 | 108 | // ValuesOptions is the options for generating the helm values. 109 | ValuesOptions interface{} 110 | 111 | // ValuesFile is the path to the values file. 112 | ValuesFile string 113 | 114 | // EnableCache indicates whether to enable the cache. 115 | EnableCache bool 116 | } 117 | 118 | // LoadAndRenderChart loads the chart from the remote charts and render the manifests with the values. 119 | func (r *Loader) LoadAndRenderChart(ctx context.Context, opts *LoadOptions) ([]byte, error) { 120 | values, err := ToHelmValues(opts.ValuesOptions, opts.ValuesFile) 121 | if err != nil { 122 | return nil, err 123 | } 124 | r.logger.V(3).Infof("create '%s' with values: %v", opts.ReleaseName, values) 125 | 126 | if opts.ChartVersion == "" { 127 | opts.ChartVersion = artifacts.LatestVersionTag 128 | } 129 | 130 | src, err := r.am.NewSource(opts.ChartName, opts.ChartVersion, artifacts.ArtifactTypeChart, opts.FromCNRegion) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | destDir, err := r.mm.AllocateArtifactFilePath(src, false) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | chartFile, err := r.am.DownloadTo(ctx, src, destDir, &artifacts.DownloadOptions{EnableCache: opts.EnableCache}) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | data, err := os.ReadFile(chartFile) 146 | if err != nil { 147 | return nil, err 148 | } 149 | helmChart, err := loader.LoadArchive(bytes.NewReader(data)) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | manifests, err := r.generateManifests(ctx, opts.ReleaseName, opts.Namespace, helmChart, values) 155 | if err != nil { 156 | return nil, err 157 | } 158 | r.logger.V(3).Infof("create '%s' with manifests: %s", opts.ReleaseName, string(manifests)) 159 | 160 | return manifests, nil 161 | } 162 | 163 | func (r *Loader) generateManifests(ctx context.Context, releaseName, namespace string, chart *chart.Chart, values map[string]interface{}) ([]byte, error) { 164 | client, err := r.newHelmClient(releaseName, namespace) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | rel, err := client.RunWithContext(ctx, chart, values) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | var manifests bytes.Buffer 175 | _, err = fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | return manifests.Bytes(), nil 181 | } 182 | 183 | func (r *Loader) newHelmClient(releaseName, namespace string) (*action.Install, error) { 184 | kubeVersion, err := chartutil.ParseKubeVersion(KubeVersion) 185 | if err != nil { 186 | return nil, fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err) 187 | } 188 | 189 | helmClient := action.NewInstall(new(action.Configuration)) 190 | helmClient.DryRun = true 191 | helmClient.ReleaseName = releaseName 192 | helmClient.Replace = true 193 | helmClient.ClientOnly = true 194 | helmClient.IncludeCRDs = true 195 | helmClient.Namespace = namespace 196 | helmClient.KubeVersion = kubeVersion 197 | 198 | return helmClient, nil 199 | } 200 | -------------------------------------------------------------------------------- /pkg/helm/loader_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package helm 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "testing" 23 | 24 | "sigs.k8s.io/kind/pkg/log" 25 | 26 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 27 | opt "github.com/GreptimeTeam/gtctl/pkg/cluster" 28 | "github.com/GreptimeTeam/gtctl/pkg/logger" 29 | ) 30 | 31 | const ( 32 | testMetadataDir = "/tmp/gtctl-test" 33 | ) 34 | 35 | func TestLoadAndRenderChart(t *testing.T) { 36 | r, err := NewLoader(logger.New(os.Stdout, log.Level(4), logger.WithColored()), WithHomeDir(testMetadataDir)) 37 | if err != nil { 38 | t.Errorf("failed to create render: %v", err) 39 | } 40 | defer cleanMetadataDir() 41 | 42 | opts := &LoadOptions{ 43 | ReleaseName: "gtctl-ut", 44 | Namespace: "default", 45 | ChartName: artifacts.GreptimeDBClusterChartName, 46 | ChartVersion: "0.1.29", 47 | FromCNRegion: false, 48 | ValuesOptions: opt.CreateClusterOptions{ 49 | ImageRegistry: "registry.cn-hangzhou.aliyuncs.com", 50 | DatanodeStorageClassName: "ebs-sc", 51 | DatanodeStorageSize: "11Gi", 52 | DatanodeStorageRetainPolicy: "Delete", 53 | EtcdEndPoints: "mycluster-etcd.default:2379", 54 | InitializerImageRegistry: "registry.cn-hangzhou.aliyuncs.com", 55 | ConfigValues: "meta.replicas=3", 56 | }, 57 | ValuesFile: "./testdata/db-values.yaml", 58 | EnableCache: false, 59 | } 60 | 61 | ctx := context.Background() 62 | manifests, err := r.LoadAndRenderChart(ctx, opts) 63 | if err != nil { 64 | t.Fatalf("failed to load and render chart: %v", err) 65 | } 66 | 67 | wantedManifests, err := os.ReadFile("./testdata/db-manifests.yaml") 68 | if err != nil { 69 | t.Fatalf("failed to read wanted manifests: %v", err) 70 | } 71 | 72 | if string(wantedManifests) != string(manifests) { 73 | t.Errorf("expected %s, got %s", string(wantedManifests), string(manifests)) 74 | } 75 | } 76 | 77 | func cleanMetadataDir() { 78 | os.RemoveAll(testMetadataDir) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/helm/testdata/db-manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: greptimedb-cluster/templates/cluster.yaml 3 | apiVersion: greptime.io/v1alpha1 4 | kind: GreptimeDBCluster 5 | metadata: 6 | name: gtctl-ut 7 | namespace: default 8 | spec: 9 | base: 10 | main: 11 | image: 'registry.cn-hangzhou.aliyuncs.com/greptime/greptimedb:v0.8.0' 12 | resources: 13 | limits: {} 14 | requests: {} 15 | frontend: 16 | replicas: 3 17 | template: 18 | main: 19 | resources: 20 | requests: 21 | {} 22 | limits: 23 | {} 24 | meta: 25 | replicas: 3 26 | etcdEndpoints: 27 | - mycluster-etcd.default:2379 28 | template: 29 | main: 30 | resources: 31 | requests: 32 | {} 33 | limits: 34 | {} 35 | datanode: 36 | replicas: 3 37 | template: 38 | main: 39 | resources: 40 | requests: 41 | {} 42 | limits: 43 | {} 44 | storage: 45 | storageClassName: ebs-sc 46 | storageSize: 11Gi 47 | storageRetainPolicy: Delete 48 | dataHome: /data/greptimedb 49 | walDir: /data/greptimedb/wal 50 | httpServicePort: 4000 51 | grpcServicePort: 4001 52 | mysqlServicePort: 4002 53 | postgresServicePort: 4003 54 | initializer: 55 | image: 'registry.cn-hangzhou.aliyuncs.com/greptime/greptimedb-initializer:0.1.0-alpha.25' 56 | objectStorage: 57 | {} 58 | -------------------------------------------------------------------------------- /pkg/helm/testdata/db-values.yaml: -------------------------------------------------------------------------------- 1 | meta: 2 | replicas: 1 3 | 4 | frontend: 5 | replicas: 3 6 | -------------------------------------------------------------------------------- /pkg/helm/testdata/merged-values.yaml: -------------------------------------------------------------------------------- 1 | fullnameOverride: "" 2 | image: 3 | imagePullPolicy: IfNotPresent 4 | pullSecrets: [] 5 | registry: greptime-registry.cn-hangzhou.cr.aliyuncs.com 6 | repository: greptime/greptimedb-operator 7 | tag: v0.1.0 8 | nameOverride: "" 9 | nodeSelector: {} 10 | rbac: 11 | create: true 12 | replicas: 1 13 | resources: 14 | limits: 15 | cpu: 100m 16 | memory: 256Mi 17 | requests: 18 | cpu: 250m 19 | memory: 64Mi 20 | serviceAccount: 21 | annotations: {} 22 | create: true 23 | name: "" 24 | -------------------------------------------------------------------------------- /pkg/helm/testdata/values.yaml: -------------------------------------------------------------------------------- 1 | fullnameOverride: "" 2 | image: 3 | imagePullPolicy: IfNotPresent 4 | pullSecrets: [] 5 | registry: docker.io 6 | repository: greptime/greptimedb-operator 7 | tag: latest 8 | nameOverride: "" 9 | nodeSelector: {} 10 | rbac: 11 | create: true 12 | replicas: 1 13 | resources: 14 | limits: 15 | cpu: 500m 16 | memory: 128Mi 17 | requests: 18 | cpu: 250m 19 | memory: 64Mi 20 | serviceAccount: 21 | annotations: {} 22 | create: true 23 | name: "" 24 | -------------------------------------------------------------------------------- /pkg/helm/values.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package helm 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "reflect" 23 | "strings" 24 | 25 | "helm.sh/helm/v3/pkg/strvals" 26 | "sigs.k8s.io/yaml" 27 | ) 28 | 29 | const ( 30 | FieldTag = "helm" 31 | ) 32 | 33 | // Values is a map of helm values. 34 | type Values map[string]interface{} 35 | 36 | // NewFromFile creates a new Values from a local yaml values file. 37 | func NewFromFile(filename string) (Values, error) { 38 | data, err := os.ReadFile(filename) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | var values Values 44 | if err := yaml.Unmarshal(data, &values); err != nil { 45 | return nil, err 46 | } 47 | 48 | return values, nil 49 | } 50 | 51 | // ToHelmValues converts the input struct that contains special helm annotations `helm:"values"` and the local yaml values file to a map that can be used as helm values. 52 | // If there is the same key in both the input struct and the local yaml values file, the value in the input struct will be used. 53 | // valuesFile can be empty. 54 | func ToHelmValues(input interface{}, valuesFile string) (Values, error) { 55 | var ( 56 | base Values 57 | err error 58 | ) 59 | 60 | if len(valuesFile) > 0 { 61 | base, err = NewFromFile(valuesFile) 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | 67 | vals, err := struct2Values(input) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return mergeMaps(base, vals), nil 73 | } 74 | 75 | // OutputValues returns the values as a yaml byte array. 76 | func (v Values) OutputValues() ([]byte, error) { 77 | data, err := yaml.Marshal(v) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return data, nil 82 | } 83 | 84 | // mergeMaps merges two maps recursively. 85 | // The function is copied from 'helm/helm/pkg/cli/values/options.go'. 86 | func mergeMaps(a, b map[string]interface{}) map[string]interface{} { 87 | out := make(map[string]interface{}, len(a)) 88 | for k, v := range a { 89 | out[k] = v 90 | } 91 | for k, v := range b { 92 | if v, ok := v.(map[string]interface{}); ok { 93 | if bv, ok := out[k]; ok { 94 | if bv, ok := bv.(map[string]interface{}); ok { 95 | out[k] = mergeMaps(bv, v) 96 | continue 97 | } 98 | } 99 | } 100 | out[k] = v 101 | } 102 | return out 103 | } 104 | 105 | // struct2Values converts the input struct to a helm values map. 106 | func struct2Values(input interface{}) (Values, error) { 107 | var rawArgs []string 108 | valueOf := reflect.ValueOf(input) 109 | 110 | // Make sure we are handling with a struct here. 111 | if valueOf.Kind() != reflect.Struct { 112 | return nil, fmt.Errorf("invalid input type, should be struct") 113 | } 114 | 115 | typeOf := reflect.TypeOf(input) 116 | for i := 0; i < valueOf.NumField(); i++ { 117 | helmValueKey := typeOf.Field(i).Tag.Get(FieldTag) 118 | if len(helmValueKey) > 0 && valueOf.Field(i).Len() > 0 { 119 | // If the struct annotation is `helm:"*"`, the value will be added to the rawArgs directly. 120 | // Otherwise, the value will be added to the rawArgs with the key `helmValueKey`. 121 | if helmValueKey == "*" { 122 | rawArgs = append(rawArgs, valueOf.Field(i).String()) 123 | } else { 124 | rawArgs = append(rawArgs, fmt.Sprintf("%s=%s", helmValueKey, valueOf.Field(i))) 125 | } 126 | } 127 | } 128 | 129 | if len(rawArgs) > 0 { 130 | values := make(map[string]interface{}) 131 | if err := strvals.ParseInto(strings.Join(rawArgs, ","), values); err != nil { 132 | return nil, err 133 | } 134 | return values, nil 135 | } 136 | 137 | return nil, nil 138 | } 139 | -------------------------------------------------------------------------------- /pkg/helm/values_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package helm 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | ) 23 | 24 | func TestNewFromFile(t *testing.T) { 25 | v, err := NewFromFile("testdata/values.yaml") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | output, err := v.OutputValues() 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | original, err := os.ReadFile("testdata/values.yaml") 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | if string(original) != string(output) { 41 | t.Errorf("expected %s, got %s", string(original), string(output)) 42 | } 43 | } 44 | 45 | func TestToHelmValues(t *testing.T) { 46 | inputVals := struct { 47 | ImageRegistry string `helm:"image.registry"` 48 | Version string `helm:"image.tag"` 49 | ConfigValues string `helm:"*"` 50 | }{ 51 | ImageRegistry: "greptime-registry.cn-hangzhou.cr.aliyuncs.com", 52 | Version: "v0.1.0", 53 | ConfigValues: "resources.limits.cpu=100m,resources.limits.memory=256Mi", 54 | } 55 | 56 | v, err := ToHelmValues(inputVals, "testdata/values.yaml") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | output, err := v.OutputValues() 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | original, err := os.ReadFile("testdata/merged-values.yaml") 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if string(original) != string(output) { 72 | t.Errorf("expected %s, got %s", string(original), string(output)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package logger 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | "io" 23 | "runtime" 24 | "strings" 25 | "sync" 26 | "sync/atomic" 27 | 28 | "github.com/fatih/color" 29 | "sigs.k8s.io/kind/pkg/log" 30 | ) 31 | 32 | // Logger is the new interface and base on log.Logger that from kind project. 33 | type Logger interface { 34 | log.Logger 35 | } 36 | 37 | // logger is an implementation of Logger interface. 38 | // The implementation of logger is based on the 'kind/pkg/internal/cli/logger.go' file. 39 | type logger struct { 40 | writer io.Writer 41 | writerMu sync.Mutex 42 | verbosity log.Level 43 | bufferPool *bufferPool 44 | colored bool 45 | } 46 | 47 | var _ Logger = &logger{} 48 | 49 | type Option func(*logger) 50 | 51 | func Bold(s string) string { 52 | return color.New(color.FgHiWhite, color.Bold).SprintfFunc()(s) 53 | } 54 | 55 | // New returns a new logger with the given verbosity. 56 | func New(writer io.Writer, verbosity log.Level, opts ...Option) Logger { 57 | l := &logger{ 58 | writer: writer, 59 | verbosity: verbosity, 60 | bufferPool: newBufferPool(), 61 | } 62 | 63 | for _, opt := range opts { 64 | opt(l) 65 | } 66 | 67 | return l 68 | } 69 | 70 | func WithColored() Option { 71 | return func(l *logger) { 72 | l.colored = true 73 | } 74 | } 75 | 76 | // Warn is part of the log.logger interface. 77 | func (l *logger) Warn(message string) { 78 | if l.colored { 79 | // Output in yellow. 80 | message = fmt.Sprintf("\x1b[33m%s\x1b[0m", message) 81 | } 82 | l.print(message) 83 | } 84 | 85 | // Warnf is part of the log.logger interface. 86 | func (l *logger) Warnf(format string, args ...interface{}) { 87 | if l.colored { 88 | // Output in yellow. 89 | format = fmt.Sprintf("\x1b[33m%s\x1b[0m", format) 90 | } 91 | l.printf(format, args...) 92 | } 93 | 94 | // Error is part of the log.logger interface. 95 | func (l *logger) Error(message string) { 96 | if l.colored { 97 | // Output in red. 98 | message = fmt.Sprintf("\x1b[31m%s\x1b[0m", message) 99 | } 100 | l.print(message) 101 | } 102 | 103 | // Errorf is part of the log.logger interface. 104 | func (l *logger) Errorf(format string, args ...interface{}) { 105 | if l.colored { 106 | // Output in red. 107 | format = fmt.Sprintf("\x1b[31m%s\x1b[0m", format) 108 | } 109 | l.printf(format, args...) 110 | } 111 | 112 | // V is part of the log.logger interface. 113 | func (l *logger) V(level log.Level) log.InfoLogger { 114 | return infoLogger{ 115 | logger: l, 116 | level: level, 117 | enabled: level <= l.getVerbosity(), 118 | } 119 | } 120 | 121 | // SetVerbosity sets the loggers verbosity. 122 | func (l *logger) SetVerbosity(verbosity log.Level) { 123 | atomic.StoreInt32((*int32)(&l.verbosity), int32(verbosity)) 124 | } 125 | 126 | // infoLogger implements log.InfoLogger for logger. 127 | type infoLogger struct { 128 | logger *logger 129 | level log.Level 130 | enabled bool 131 | } 132 | 133 | // Enabled is part of the log.InfoLogger interface. 134 | func (i infoLogger) Enabled() bool { 135 | return i.enabled 136 | } 137 | 138 | // Info is part of the log.InfoLogger interface. 139 | func (i infoLogger) Info(message string) { 140 | if !i.enabled { 141 | return 142 | } 143 | // for > 0, we are writing debug messages, include extra info 144 | if i.level > 0 { 145 | i.logger.debug(message) 146 | } else { 147 | i.logger.print(message) 148 | } 149 | } 150 | 151 | // Infof is part of the log.InfoLogger interface. 152 | func (i infoLogger) Infof(format string, args ...interface{}) { 153 | if !i.enabled { 154 | return 155 | } 156 | // for > 0, we are writing debug messages, include extra info. 157 | if i.level > 0 { 158 | i.logger.debugf(format, args...) 159 | } else { 160 | i.logger.printf(format, args...) 161 | } 162 | } 163 | 164 | // synchronized write to the inner writer 165 | func (l *logger) write(p []byte) (n int, err error) { 166 | l.writerMu.Lock() 167 | defer l.writerMu.Unlock() 168 | return l.writer.Write(p) 169 | } 170 | 171 | // writeBuffer writes buf with write, ensuring there is a trailing newline. 172 | func (l *logger) writeBuffer(buf *bytes.Buffer) { 173 | // ensure trailing newline 174 | if buf.Len() == 0 || buf.Bytes()[buf.Len()-1] != '\n' { 175 | buf.WriteByte('\n') 176 | } 177 | // TODO: should we handle this somehow?? 178 | // Who logs for the logger? 🤔 179 | _, _ = l.write(buf.Bytes()) 180 | } 181 | 182 | // print writes a simple string to the log writer. 183 | func (l *logger) print(message string) { 184 | buf := bytes.NewBufferString(message) 185 | l.writeBuffer(buf) 186 | } 187 | 188 | // printf is roughly fmt.Fprintf against the log writer. 189 | func (l *logger) printf(format string, args ...interface{}) { 190 | buf := l.bufferPool.Get() 191 | fmt.Fprintf(buf, format, args...) 192 | l.writeBuffer(buf) 193 | l.bufferPool.Put(buf) 194 | } 195 | 196 | // debug is like print but with a debug log header. 197 | func (l *logger) debug(message string) { 198 | buf := l.bufferPool.Get() 199 | l.addDebugHeader(buf) 200 | if l.colored { 201 | // Output in blue. 202 | message = fmt.Sprintf("\x1b[34m%s\x1b[0m", message) 203 | } 204 | buf.WriteString(message) 205 | l.writeBuffer(buf) 206 | l.bufferPool.Put(buf) 207 | } 208 | 209 | // debugf is like printf but with a debug log header. 210 | func (l *logger) debugf(format string, args ...interface{}) { 211 | buf := l.bufferPool.Get() 212 | l.addDebugHeader(buf) 213 | if l.colored { 214 | // Output in blue. 215 | format = fmt.Sprintf("\x1b[34m%s\x1b[0m", format) 216 | } 217 | fmt.Fprintf(buf, format, args...) 218 | l.writeBuffer(buf) 219 | l.bufferPool.Put(buf) 220 | } 221 | 222 | // addDebugHeader inserts the debug line header to buf. 223 | func (l *logger) addDebugHeader(buf *bytes.Buffer) { 224 | _, file, line, ok := runtime.Caller(3) 225 | // lifted from klog 226 | if !ok { 227 | file = "???" 228 | line = 1 229 | } else { 230 | if slash := strings.LastIndex(file, "/"); slash >= 0 { 231 | path := file 232 | file = path[slash+1:] 233 | if dirsep := strings.LastIndex(path[:slash], "/"); dirsep >= 0 { 234 | file = path[dirsep+1:] 235 | } 236 | } 237 | } 238 | buf.Grow(len(file) + 11) // we know at least this many bytes are needed 239 | if l.colored { 240 | // Output in blue. 241 | buf.WriteString("\x1b[34m") 242 | } 243 | buf.WriteString("DEBUG: ") 244 | buf.WriteString(file) 245 | buf.WriteByte(':') 246 | fmt.Fprintf(buf, "%d", line) 247 | buf.WriteByte(']') 248 | buf.WriteByte(' ') 249 | if l.colored { 250 | // Reset color. 251 | buf.WriteString("\x1b[0m") 252 | } 253 | } 254 | 255 | func (l *logger) getVerbosity() log.Level { 256 | return log.Level(atomic.LoadInt32((*int32)(&l.verbosity))) 257 | } 258 | 259 | // bufferPool is a type safe sync.Pool of *byte.Buffer, guaranteed to be Reset. 260 | type bufferPool struct { 261 | sync.Pool 262 | } 263 | 264 | // newBufferPool returns a new bufferPool 265 | func newBufferPool() *bufferPool { 266 | return &bufferPool{ 267 | sync.Pool{ 268 | New: func() interface{} { 269 | // The Pool's New function should generally only return pointer 270 | // types, since a pointer can be put into the return interface 271 | // value without an allocation. 272 | return new(bytes.Buffer) 273 | }, 274 | }, 275 | } 276 | } 277 | 278 | // Get obtains a buffer from the pool. 279 | func (b *bufferPool) Get() *bytes.Buffer { 280 | return b.Pool.Get().(*bytes.Buffer) 281 | } 282 | 283 | // Put returns a buffer to the pool, resetting it first. 284 | func (b *bufferPool) Put(x *bytes.Buffer) { 285 | // only store small buffers to avoid pointless allocation 286 | // avoid keeping arbitrarily large buffers 287 | if x.Len() > 256 { 288 | return 289 | } 290 | x.Reset() 291 | b.Pool.Put(x) 292 | } 293 | -------------------------------------------------------------------------------- /pkg/metadata/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package metadata 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "path" 23 | "path/filepath" 24 | "time" 25 | 26 | "gopkg.in/yaml.v3" 27 | 28 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 29 | "github.com/GreptimeTeam/gtctl/pkg/config" 30 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 31 | ) 32 | 33 | // Manager is the interface of the metadata manager. 34 | // The metadata manager is responsible for managing all the metadata of gtctl. 35 | type Manager interface { 36 | // AllocateArtifactFilePath allocates the file path of the artifact. 37 | AllocateArtifactFilePath(src *artifacts.Source, installBinary bool) (string, error) 38 | 39 | // AllocateClusterScopeDirs allocates the directories and config path of one cluster. 40 | AllocateClusterScopeDirs(clusterName string) 41 | 42 | // SetHomeDir sets the home directory of the metadata manager. 43 | SetHomeDir(dir string) error 44 | 45 | // GetWorkingDir returns the working directory of the metadata manager. 46 | // It should be ${HomeDir}/${BaseDir}. 47 | GetWorkingDir() string 48 | 49 | // CreateClusterScopeDirs creates cluster scope directories and config path that allocated by AllocateClusterScopeDirs. 50 | CreateClusterScopeDirs(cfg *config.BareMetalClusterConfig) error 51 | 52 | // GetClusterScopeDirs returns the cluster scope directory of current cluster. 53 | GetClusterScopeDirs() *ClusterScopeDirs 54 | 55 | // Clean cleans up all the metadata. It will remove the working directory. 56 | Clean() error 57 | } 58 | 59 | const ( 60 | // BaseDir is the working directory of gtctl and 61 | // all the metadata will be stored in ${HomeDir}/${BaseDir}. 62 | BaseDir = ".gtctl" 63 | 64 | ClusterLogsDir = "logs" 65 | ClusterDataDir = "data" 66 | ClusterPidsDir = "pids" 67 | ) 68 | 69 | type ClusterScopeDirs struct { 70 | BaseDir string 71 | LogsDir string 72 | DataDir string 73 | PidsDir string 74 | ConfigPath string 75 | } 76 | 77 | type manager struct { 78 | workingDir string 79 | 80 | clusterDir *ClusterScopeDirs 81 | } 82 | 83 | var _ Manager = &manager{} 84 | 85 | func New(homeDir string) (Manager, error) { 86 | m := &manager{} 87 | if homeDir == "" { 88 | dir, err := os.UserHomeDir() 89 | if err != nil { 90 | return nil, err 91 | } 92 | m.workingDir = filepath.Join(dir, BaseDir) 93 | } else { 94 | m.workingDir = filepath.Join(homeDir, BaseDir) 95 | } 96 | return m, nil 97 | } 98 | 99 | func (m *manager) AllocateClusterScopeDirs(clusterName string) { 100 | csd := &ClusterScopeDirs{ 101 | // ${HomeDir}/${BaseDir}${ClusterName} 102 | BaseDir: path.Join(m.workingDir, clusterName), 103 | } 104 | 105 | // ${HomeDir}/${BaseDir}/${ClusterName}/logs 106 | csd.LogsDir = path.Join(csd.BaseDir, ClusterLogsDir) 107 | // ${HomeDir}/${BaseDir}/${ClusterName}/data 108 | csd.DataDir = path.Join(csd.BaseDir, ClusterDataDir) 109 | // ${HomeDir}/${BaseDir}/${ClusterName}/pids 110 | csd.PidsDir = path.Join(csd.BaseDir, ClusterPidsDir) 111 | // ${HomeDir}/${BaseDir}/${ClusterName}/${ClusterName}.yaml 112 | csd.ConfigPath = filepath.Join(csd.BaseDir, fmt.Sprintf("%s.yaml", clusterName)) 113 | 114 | m.clusterDir = csd 115 | } 116 | 117 | func (m *manager) AllocateArtifactFilePath(src *artifacts.Source, installBinary bool) (string, error) { 118 | var filePath string 119 | switch src.Type { 120 | case artifacts.ArtifactTypeChart: 121 | filePath = filepath.Join(m.workingDir, "artifacts", "charts", src.Name, src.Version, "pkg") 122 | case artifacts.ArtifactTypeBinary: 123 | if installBinary { 124 | // TODO(zyy17): It seems that we need to call AllocateArtifactFilePath() twice to get the correct path. Can we make it easier? 125 | filePath = filepath.Join(m.workingDir, "artifacts", "binaries", src.Name, src.Version, "bin") 126 | } else { 127 | filePath = filepath.Join(m.workingDir, "artifacts", "binaries", src.Name, src.Version, "pkg") 128 | } 129 | default: 130 | return "", fmt.Errorf("unknown artifact type: %s", src.Type) 131 | } 132 | 133 | return filePath, nil 134 | } 135 | 136 | func (m *manager) CreateClusterScopeDirs(cfg *config.BareMetalClusterConfig) error { 137 | if m.clusterDir == nil { 138 | return fmt.Errorf("unallocated cluster dir, please initialize a metadata manager with cluster name provided") 139 | } 140 | 141 | dirs := []string{ 142 | m.clusterDir.BaseDir, 143 | m.clusterDir.LogsDir, 144 | m.clusterDir.DataDir, 145 | m.clusterDir.PidsDir, 146 | } 147 | 148 | for _, dir := range dirs { 149 | if err := fileutils.EnsureDir(dir); err != nil { 150 | return err 151 | } 152 | } 153 | 154 | f, err := os.Create(m.clusterDir.ConfigPath) 155 | if err != nil { 156 | return err 157 | } 158 | 159 | metaConfig := config.BareMetalClusterMetadata{ 160 | Config: cfg, 161 | CreationDate: time.Now(), 162 | ClusterDir: m.clusterDir.BaseDir, 163 | ForegroundPid: os.Getpid(), 164 | } 165 | 166 | out, err := yaml.Marshal(metaConfig) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | if _, err = f.Write(out); err != nil { 172 | return err 173 | } 174 | 175 | if err = f.Close(); err != nil { 176 | return err 177 | } 178 | 179 | return nil 180 | } 181 | 182 | func (m *manager) SetHomeDir(dir string) error { 183 | m.workingDir = filepath.Join(dir, BaseDir) 184 | return nil 185 | } 186 | 187 | func (m *manager) GetWorkingDir() string { 188 | return m.workingDir 189 | } 190 | 191 | func (m *manager) GetClusterScopeDirs() *ClusterScopeDirs { 192 | return m.clusterDir 193 | } 194 | 195 | func (m *manager) Clean() error { 196 | return os.RemoveAll(m.workingDir) 197 | } 198 | -------------------------------------------------------------------------------- /pkg/metadata/manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package metadata 18 | 19 | import ( 20 | "os" 21 | "path/filepath" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | "gopkg.in/yaml.v3" 26 | 27 | "github.com/GreptimeTeam/gtctl/pkg/artifacts" 28 | "github.com/GreptimeTeam/gtctl/pkg/config" 29 | ) 30 | 31 | func TestMetadataManager(t *testing.T) { 32 | tempDir, err := os.MkdirTemp("/tmp", "gtctl-ut-") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer os.RemoveAll(tempDir) 37 | 38 | m, err := New(tempDir) 39 | if err != nil { 40 | t.Fatalf("failed to create metadata manager: %v", err) 41 | } 42 | 43 | tests := []struct { 44 | src *artifacts.Source 45 | wantedDestDir string 46 | wantedInstallDir string 47 | }{ 48 | { 49 | src: &artifacts.Source{ 50 | Name: artifacts.GreptimeDBClusterChartName, 51 | Version: artifacts.LatestVersionTag, 52 | Type: artifacts.ArtifactTypeChart, 53 | }, 54 | wantedDestDir: filepath.Join(tempDir, BaseDir, "artifacts", "charts", artifacts.GreptimeDBClusterChartName, artifacts.LatestVersionTag, "pkg"), 55 | }, 56 | { 57 | src: &artifacts.Source{ 58 | Name: artifacts.EtcdBinName, 59 | Version: artifacts.DefaultEtcdBinVersion, 60 | Type: artifacts.ArtifactTypeBinary, 61 | }, 62 | wantedDestDir: filepath.Join(tempDir, BaseDir, "artifacts", "binaries", artifacts.EtcdBinName, artifacts.DefaultEtcdBinVersion, "pkg"), 63 | wantedInstallDir: filepath.Join(tempDir, BaseDir, "artifacts", "binaries", artifacts.EtcdBinName, artifacts.DefaultEtcdBinVersion, "bin"), 64 | }, 65 | } 66 | 67 | for _, tt := range tests { 68 | gotDestDir, err := m.AllocateArtifactFilePath(tt.src, false) 69 | if err != nil { 70 | t.Errorf("failed to allocate artifact file path: %v", err) 71 | } 72 | if gotDestDir != tt.wantedDestDir { 73 | t.Errorf("got %s, wanted %s", gotDestDir, tt.wantedDestDir) 74 | } 75 | 76 | if tt.src.Type == artifacts.ArtifactTypeBinary { 77 | gotInstallDir, err := m.AllocateArtifactFilePath(tt.src, true) 78 | if err != nil { 79 | t.Errorf("failed to allocate artifact file path: %v", err) 80 | } 81 | if gotInstallDir != tt.wantedInstallDir { 82 | t.Errorf("got %s, wanted %s", gotInstallDir, tt.wantedInstallDir) 83 | } 84 | } 85 | } 86 | 87 | // Clean() should remove the working directory. 88 | if err := m.Clean(); err != nil { 89 | t.Fatalf("failed to clean up metadata: %v", err) 90 | } 91 | 92 | if _, err := os.Stat(m.GetWorkingDir()); !os.IsNotExist(err) { 93 | t.Fatalf("working directory %s still exists", m.GetWorkingDir()) 94 | } 95 | 96 | // SetHomeDir() should change the working directory. 97 | testHomeDir := "/path/to/gtctl-ut" 98 | if err := m.SetHomeDir(testHomeDir); err != nil { 99 | t.Fatalf("failed to set home directory: %v", err) 100 | } 101 | wantedWorkingDir := filepath.Join(testHomeDir, BaseDir) 102 | if m.GetWorkingDir() != wantedWorkingDir { 103 | t.Errorf("got %s, wanted %s", m.GetWorkingDir(), wantedWorkingDir) 104 | } 105 | } 106 | 107 | func TestCreateMetadataManagerWithEmptyHomeDir(t *testing.T) { 108 | m, err := New("") 109 | if err != nil { 110 | t.Fatalf("failed to create metadata manager: %v", err) 111 | } 112 | 113 | dir, err := os.UserHomeDir() 114 | if err != nil { 115 | t.Fatalf("failed to get user home directory: %v", err) 116 | } 117 | 118 | wantedWorkingDir := filepath.Join(dir, BaseDir) 119 | if m.GetWorkingDir() != wantedWorkingDir { 120 | t.Fatalf("got %s, wanted %s", m.GetWorkingDir(), wantedWorkingDir) 121 | } 122 | } 123 | 124 | func TestMetadataManagerWithClusterConfigPath(t *testing.T) { 125 | m, err := New("/tmp") 126 | assert.NoError(t, err) 127 | 128 | expect := config.DefaultBareMetalConfig() 129 | m.AllocateClusterScopeDirs("test") 130 | err = m.CreateClusterScopeDirs(expect) 131 | assert.NoError(t, err) 132 | 133 | csd := m.GetClusterScopeDirs() 134 | assert.NotNil(t, csd) 135 | assert.NotEmpty(t, csd.ConfigPath) 136 | 137 | cnt, err := os.ReadFile(csd.ConfigPath) 138 | assert.NoError(t, err) 139 | 140 | var actual config.BareMetalClusterMetadata 141 | err = yaml.Unmarshal(cnt, &actual) 142 | assert.NoError(t, err) 143 | assert.Equal(t, expect, actual.Config) 144 | 145 | err = m.Clean() 146 | assert.NoError(t, err) 147 | } 148 | -------------------------------------------------------------------------------- /pkg/plugins/manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "strings" 25 | 26 | fileutils "github.com/GreptimeTeam/gtctl/pkg/utils/file" 27 | ) 28 | 29 | const ( 30 | // DefaultPluginPrefix is the default prefix for the plugin binary name. 31 | DefaultPluginPrefix = "gtctl-" 32 | 33 | // PluginSearchPathsEnvKey is the environment variable key for the plugin search paths. 34 | // If we set this variable, the plugin manager will search the paths provided by this variable. 35 | // If we don't set this variable, the plugin manager will search the current working directory and the $PATH. 36 | PluginSearchPathsEnvKey = "GTCTL_PLUGIN_PATHS" 37 | ) 38 | 39 | // Manager manages and executes the plugins. 40 | type Manager struct { 41 | prefix string 42 | searchPaths []string 43 | } 44 | 45 | func NewManager() (*Manager, error) { 46 | m := &Manager{ 47 | prefix: DefaultPluginPrefix, 48 | searchPaths: []string{}, 49 | } 50 | 51 | pluginSearchPaths := os.Getenv(PluginSearchPathsEnvKey) 52 | if len(pluginSearchPaths) > 0 { 53 | m.searchPaths = append(m.searchPaths, strings.Split(pluginSearchPaths, ":")...) 54 | } else { 55 | // Search the current working directory. 56 | pwd, err := os.Getwd() 57 | if err != nil { 58 | return nil, err 59 | } 60 | m.searchPaths = append(m.searchPaths, pwd) 61 | 62 | // Search the $PATH. 63 | pathEnv := os.Getenv("PATH") 64 | if len(pathEnv) > 0 { 65 | m.searchPaths = append(m.searchPaths, strings.Split(pathEnv, ":")...) 66 | } 67 | } 68 | 69 | return m, nil 70 | } 71 | 72 | // ShouldRun returns true whether you should run the plugin. 73 | func (m *Manager) ShouldRun(name string) bool { 74 | _, err := m.searchPlugins(name) 75 | return err == nil 76 | } 77 | 78 | // Run searches for the plugin and runs it. 79 | func (m *Manager) Run(args []string) error { 80 | if len(args) < 1 { 81 | return nil // No arguments provided, normal help message will be shown. 82 | } 83 | 84 | pluginPath, err := m.searchPlugins(args[0]) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | pluginCmd := exec.Command(pluginPath, args[1:]...) 90 | pluginCmd.Stdin = os.Stdin 91 | pluginCmd.Stdout = os.Stdout 92 | pluginCmd.Stderr = os.Stderr 93 | if err := pluginCmd.Run(); err != nil { 94 | return fmt.Errorf("failed to run plugin '%s': %v", pluginPath, err) 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (m *Manager) searchPlugins(name string) (string, error) { 101 | if len(m.searchPaths) == 0 { 102 | return "", fmt.Errorf("no plugin search paths provided") 103 | } 104 | 105 | // Construct plugin binary name gtctl-. 106 | pluginName := m.prefix + name 107 | for _, path := range m.searchPaths { 108 | pluginPath := filepath.Join(path, pluginName) 109 | if exist, _ := fileutils.IsFileExists(pluginPath); !exist { 110 | continue 111 | } 112 | 113 | return pluginPath, nil 114 | } 115 | 116 | return "", fmt.Errorf("error: unknown command %q for \"gtctl\"", name) 117 | } 118 | -------------------------------------------------------------------------------- /pkg/plugins/manager_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package plugins 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestPluginManager(t *testing.T) { 24 | t.Setenv(PluginSearchPathsEnvKey, "./testdata") 25 | pm, err := NewManager() 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | pluginName := "foo-plugin" 31 | if pm.ShouldRun(pluginName) { 32 | if err := pm.Run([]string{pluginName, "1", "2", "3"}); err != nil { 33 | t.Fatal(err) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/plugins/testdata/gtctl-foo-plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Just prints the args it receives. 4 | echo "Hello from gtctl-foo-plugin.sh, args: $*" 5 | -------------------------------------------------------------------------------- /pkg/status/status.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package status 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "github.com/briandowns/spinner" 24 | ) 25 | 26 | // The spinner frames is from kind project. 27 | var spinnerFrames = []string{ 28 | "⠈⠁", 29 | "⠈⠑", 30 | "⠈⠱", 31 | "⠈⡱", 32 | "⢀⡱", 33 | "⢄⡱", 34 | "⢄⡱", 35 | "⢆⡱", 36 | "⢎⡱", 37 | "⢎⡰", 38 | "⢎⡠", 39 | "⢎⡀", 40 | "⢎⠁", 41 | "⠎⠁", 42 | "⠊⠁", 43 | } 44 | 45 | const ( 46 | defaultDelay = 100 * time.Millisecond 47 | ) 48 | 49 | type Spinner struct { 50 | spinner *spinner.Spinner 51 | } 52 | 53 | func NewSpinner() (*Spinner, error) { 54 | s := spinner.New(spinnerFrames, defaultDelay) 55 | if err := s.Color("fgHiWhite", "bold"); err != nil { 56 | return nil, err 57 | } 58 | return &Spinner{ 59 | spinner: s, 60 | }, nil 61 | } 62 | 63 | func (s *Spinner) Start(status string) { 64 | s.spinner.Start() 65 | s.spinner.Suffix = fmt.Sprintf(" %s", status) 66 | } 67 | 68 | func (s *Spinner) Stop(success bool, status string) { 69 | if success { 70 | s.spinner.FinalMSG = fmt.Sprintf(" \x1b[32m✓\x1b[0m %s\n", status) 71 | } else { 72 | s.spinner.FinalMSG = fmt.Sprintf(" \x1b[31m✗\x1b[0m %s 😵‍💫\n", status) 73 | } 74 | s.spinner.Stop() 75 | } 76 | -------------------------------------------------------------------------------- /pkg/utils/file/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package file 18 | 19 | import ( 20 | "archive/tar" 21 | "archive/zip" 22 | "bytes" 23 | "compress/gzip" 24 | "fmt" 25 | "io" 26 | "os" 27 | "path" 28 | "path/filepath" 29 | ) 30 | 31 | // EnsureDir ensures the directory exists. 32 | func EnsureDir(dir string) error { 33 | // Check if the directory exists 34 | if _, err := os.Stat(dir); os.IsNotExist(err) { 35 | // Create the directory along with any necessary parents. 36 | return os.MkdirAll(dir, 0755) 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func DeleteDirIfExists(dir string) (err error) { 43 | if err := os.RemoveAll(dir); err != nil && !os.IsNotExist(err) { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | func IsFileExists(filepath string) (bool, error) { 50 | info, err := os.Stat(filepath) 51 | if os.IsNotExist(err) { 52 | // file does not exist 53 | return false, nil 54 | } 55 | 56 | if err != nil { 57 | // Other errors happened. 58 | return false, err 59 | } 60 | 61 | if info.IsDir() { 62 | // It's a directory. 63 | return false, fmt.Errorf("'%s' is directory, not file", filepath) 64 | } 65 | 66 | // The file exists. 67 | return true, nil 68 | } 69 | 70 | // CopyFile copies the file from src to dst. 71 | func CopyFile(src, dst string) error { 72 | r, err := os.Open(src) 73 | if err != nil { 74 | return err 75 | } 76 | defer r.Close() 77 | 78 | w, err := os.Create(dst) 79 | if err != nil { 80 | return err 81 | } 82 | defer w.Close() 83 | 84 | _, err = io.Copy(w, r) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | return w.Sync() 90 | } 91 | 92 | const ( 93 | ZipExtension = ".zip" 94 | TarGzExtension = ".tar.gz" 95 | TgzExtension = ".tgz" 96 | GzExtension = ".gz" 97 | TarExtension = ".tar" 98 | ) 99 | 100 | // Uncompress uncompresses the file to the destination directory. 101 | func Uncompress(file, dst string) error { 102 | fileType := path.Ext(file) 103 | switch fileType { 104 | case ZipExtension: 105 | return unzip(file, dst) 106 | case TgzExtension, GzExtension, TarGzExtension: 107 | return untar(file, dst) 108 | default: 109 | return fmt.Errorf("unsupported file type: %s", fileType) 110 | } 111 | } 112 | 113 | func unzip(file, dst string) error { 114 | archive, err := zip.OpenReader(file) 115 | if err != nil { 116 | return err 117 | } 118 | defer archive.Close() 119 | 120 | for _, f := range archive.File { 121 | filePath := filepath.Join(dst, f.Name) 122 | 123 | if f.FileInfo().IsDir() { 124 | if err := os.MkdirAll(filePath, os.ModePerm); err != nil { 125 | return err 126 | } 127 | continue 128 | } 129 | 130 | if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { 131 | return err 132 | } 133 | 134 | dstFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | fileInArchive, err := f.Open() 140 | if err != nil { 141 | return err 142 | } 143 | 144 | if _, err := io.Copy(dstFile, fileInArchive); err != nil { 145 | return err 146 | } 147 | 148 | if err := dstFile.Close(); err != nil { 149 | return err 150 | } 151 | 152 | if err := fileInArchive.Close(); err != nil { 153 | return err 154 | } 155 | } 156 | 157 | return nil 158 | } 159 | 160 | func untar(file, dst string) error { 161 | data, err := os.ReadFile(file) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | stream, err := gzip.NewReader(bytes.NewReader(data)) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | tarReader := tar.NewReader(stream) 172 | 173 | for { 174 | header, err := tarReader.Next() 175 | 176 | if err == io.EOF { 177 | break 178 | } 179 | 180 | if err != nil { 181 | return err 182 | } 183 | 184 | switch header.Typeflag { 185 | case tar.TypeReg: 186 | filePath := path.Join(dst, header.Name) 187 | outFile, err := os.Create(filePath) 188 | if err != nil && !os.IsExist(err) { 189 | return err 190 | } 191 | if _, err := io.Copy(outFile, tarReader); err != nil { 192 | return err 193 | } 194 | 195 | if err := os.Chmod(filePath, os.FileMode(header.Mode)); err != nil { 196 | return err 197 | } 198 | 199 | if err := outFile.Close(); err != nil { 200 | return err 201 | } 202 | case tar.TypeDir: 203 | if err := os.Mkdir(path.Join(dst, header.Name), 0755); err != nil && !os.IsExist(err) { 204 | return err 205 | } 206 | default: 207 | continue 208 | } 209 | } 210 | 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /pkg/utils/file/file_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package file 18 | 19 | import ( 20 | "os" 21 | "path" 22 | "testing" 23 | ) 24 | 25 | func TestUncompress(t *testing.T) { 26 | const ( 27 | testContent = "helloworld" 28 | outputDir = "testdata/output" 29 | ) 30 | 31 | tests := []struct { 32 | filename string 33 | path string 34 | dst string 35 | }{ 36 | {"test-zip", "testdata/test-zip.zip", outputDir}, 37 | {"test-tgz", "testdata/test-tgz.tgz", outputDir}, 38 | {"test-tgz", "testdata/test-tar-gz.tar.gz", outputDir}, 39 | } 40 | 41 | // Clean up output dir. 42 | defer func() { 43 | os.RemoveAll(outputDir) 44 | }() 45 | 46 | for _, test := range tests { 47 | if err := Uncompress(test.path, test.dst); err != nil { 48 | t.Errorf("uncompress file '%s': %v", test.path, err) 49 | } 50 | 51 | dataFile := path.Join(test.dst, test.filename, "data") 52 | data, err := os.ReadFile(dataFile) 53 | if err != nil { 54 | t.Errorf("read file '%s': %v", dataFile, err) 55 | } 56 | 57 | if string(data) != testContent { 58 | t.Errorf("file content is not '%s': %s", testContent, string(data)) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/utils/file/testdata/test-tar-gz.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/gtctl/9dc5c7f6f6401bfd72fb6d720c767739d0e855d6/pkg/utils/file/testdata/test-tar-gz.tar.gz -------------------------------------------------------------------------------- /pkg/utils/file/testdata/test-tgz.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/gtctl/9dc5c7f6f6401bfd72fb6d720c767739d0e855d6/pkg/utils/file/testdata/test-tgz.tgz -------------------------------------------------------------------------------- /pkg/utils/file/testdata/test-zip.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GreptimeTeam/gtctl/9dc5c7f6f6401bfd72fb6d720c767739d0e855d6/pkg/utils/file/testdata/test-zip.zip -------------------------------------------------------------------------------- /pkg/utils/file/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package file 18 | 19 | import ( 20 | "bytes" 21 | 22 | "golang.org/x/exp/maps" 23 | "gopkg.in/yaml.v3" 24 | ) 25 | 26 | // MergeYAML merges two yaml files from src to dst, the src yaml will override dst yaml if the same key exists. 27 | func MergeYAML(dst, src []byte) ([]byte, error) { 28 | map1 := map[string]interface{}{} 29 | map2 := map[string]interface{}{} 30 | 31 | if err := yaml.Unmarshal(src, &map1); err != nil { 32 | return nil, err 33 | } 34 | 35 | if err := yaml.Unmarshal(dst, &map2); err != nil { 36 | return nil, err 37 | } 38 | 39 | maps.Copy(map2, map1) 40 | 41 | buf := bytes.NewBuffer([]byte{}) 42 | encoder := yaml.NewEncoder(buf) 43 | encoder.SetIndent(2) 44 | 45 | if err := encoder.Encode(map2); err != nil { 46 | return nil, err 47 | } 48 | 49 | if err := encoder.Close(); err != nil { 50 | return nil, err 51 | } 52 | 53 | return buf.Bytes(), nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/utils/file/yaml_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package file 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | func TestMergeYAML(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | yaml1 string 28 | yaml2 string 29 | want string 30 | }{ 31 | { 32 | name: "test", 33 | yaml1: ` 34 | a: a 35 | b: 36 | c: 37 | d: d 38 | e: 39 | f: 40 | - g 41 | k: 42 | l: l 43 | `, 44 | yaml2: ` 45 | a: a1 46 | b: 47 | c: 48 | d: d1 49 | e: 50 | f: 51 | - h 52 | i: 53 | j: j 54 | `, 55 | want: `a: a1 56 | b: 57 | c: 58 | d: d1 59 | e: 60 | f: 61 | - h 62 | i: 63 | j: j 64 | k: 65 | l: l 66 | `, 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | got, err := MergeYAML([]byte(tt.yaml1), []byte(tt.yaml2)) 72 | if err != nil { 73 | t.Errorf("MergeYAML() err = %v", err) 74 | } 75 | 76 | actual := string(got) 77 | if !reflect.DeepEqual(actual, tt.want) { 78 | t.Errorf("MergeYAML() got = %v, want %v", actual, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pkg/utils/semver/semver.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package semver 18 | 19 | import ( 20 | "github.com/Masterminds/semver/v3" 21 | ) 22 | 23 | // Compare compares two semantic versions. 24 | // It returns true if v1 is greater than v2, otherwise false. 25 | func Compare(v1, v2 string) (bool, error) { 26 | semV1, err := semver.NewVersion(v1) 27 | if err != nil { 28 | return false, err 29 | } 30 | 31 | semV2, err := semver.NewVersion(v2) 32 | if err != nil { 33 | return false, err 34 | } 35 | 36 | return semV1.GreaterThan(semV2), nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/utils/semver/semver_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package semver 18 | 19 | import ( 20 | "testing" 21 | ) 22 | 23 | func TestCompare(t *testing.T) { 24 | tests := []struct { 25 | v1, v2 string 26 | want bool 27 | }{ 28 | {"v0.3.2", "v0.4.0-nightly-20230802", false}, 29 | {"v0.4.0-nightly-20230807", "0.4.0-nightly-20230802", true}, 30 | } 31 | 32 | for _, test := range tests { 33 | got, err := Compare(test.v1, test.v2) 34 | if err != nil { 35 | t.Errorf("compare '%s' and '%s': %v", test.v1, test.v2, err) 36 | } 37 | 38 | if got != test.want { 39 | t.Errorf("compare '%s' and '%s': got %v, want %v", test.v1, test.v2, got, test.want) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | import ( 20 | "fmt" 21 | "runtime" 22 | ) 23 | 24 | var ( 25 | gitCommit = "none" 26 | gitVersion = "none" 27 | buildDate = "none" 28 | ) 29 | 30 | type Version struct { 31 | GitCommit string 32 | GitVersion string 33 | GoVersion string 34 | Compiler string 35 | Platform string 36 | BuildDate string 37 | } 38 | 39 | func (v Version) String() string { 40 | format := "GitCommit: %s\n" + 41 | "GitVersion: %s\n" + 42 | "GoVersion: %s\n" + 43 | "Compiler: %s\n" + 44 | "Platform: %s\n" + 45 | "BuildDate: %s\n" 46 | return fmt.Sprintf(format, v.GitCommit, v.GitVersion, v.GoVersion, v.Compiler, v.Platform, v.BuildDate) 47 | } 48 | 49 | func Get() Version { 50 | return Version{ 51 | GitCommit: gitCommit, 52 | GitVersion: gitVersion, 53 | GoVersion: runtime.Version(), 54 | Compiler: runtime.Compiler, 55 | Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), 56 | BuildDate: buildDate, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/e2e/greptimedbcluster_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "context" 21 | "database/sql" 22 | "fmt" 23 | "os" 24 | "os/exec" 25 | "time" 26 | 27 | . "github.com/onsi/ginkgo/v2" 28 | . "github.com/onsi/gomega" 29 | 30 | "github.com/go-sql-driver/mysql" 31 | "k8s.io/klog/v2" 32 | ) 33 | 34 | const ( 35 | createTableSQL = `CREATE TABLE dist_table ( 36 | ts TIMESTAMP DEFAULT current_timestamp(), 37 | n INT, 38 | row_id INT, 39 | TIME INDEX (ts), 40 | PRIMARY KEY(n) 41 | ) 42 | PARTITION ON COLUMNS (n) ( 43 | n < 5, 44 | n >= 5 AND n < 9, 45 | n >= 9 46 | )` 47 | 48 | insertDataSQLStr = "INSERT INTO dist_table(n, row_id) VALUES (%d, %d)" 49 | 50 | selectDataSQL = `SELECT * FROM dist_table` 51 | 52 | testRowIDNum = 10 53 | ) 54 | 55 | var ( 56 | defaultQueryTimeout = 5 * time.Second 57 | ) 58 | 59 | // TestData is the schema of test data in SQL table. 60 | type TestData struct { 61 | timestamp string 62 | n int32 63 | rowID int32 64 | } 65 | 66 | var _ = Describe("Basic test of greptimedb cluster", func() { 67 | It("Bootstrap cluster", func() { 68 | var err error 69 | err = createCluster() 70 | Expect(err).NotTo(HaveOccurred(), "failed to create cluster") 71 | 72 | err = getCluster() 73 | Expect(err).NotTo(HaveOccurred(), "failed to get cluster") 74 | 75 | err = listCluster() 76 | Expect(err).NotTo(HaveOccurred(), "failed to list cluster") 77 | 78 | go func() { 79 | forwardRequest() 80 | }() 81 | 82 | By("Connecting GreptimeDB") 83 | var db *sql.DB 84 | var conn *sql.Conn 85 | 86 | Eventually(func() error { 87 | cfg := mysql.Config{ 88 | Net: "tcp", 89 | Addr: "127.0.0.1:4002", 90 | User: "", 91 | Passwd: "", 92 | DBName: "", 93 | AllowNativePasswords: true, 94 | } 95 | 96 | db, err = sql.Open("mysql", cfg.FormatDSN()) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | conn, err = db.Conn(context.TODO()) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | return nil 107 | }, 30*time.Second, time.Second).ShouldNot(HaveOccurred()) 108 | 109 | By("Execute SQL queries after connecting") 110 | 111 | ctx, cancel := context.WithTimeout(context.Background(), defaultQueryTimeout) 112 | defer cancel() 113 | 114 | _, err = conn.ExecContext(ctx, createTableSQL) 115 | Expect(err).NotTo(HaveOccurred(), "failed to create SQL table") 116 | 117 | ctx, cancel = context.WithTimeout(context.Background(), defaultQueryTimeout) 118 | defer cancel() 119 | for rowID := 1; rowID <= testRowIDNum; rowID++ { 120 | insertDataSQL := fmt.Sprintf(insertDataSQLStr, rowID, rowID) 121 | _, err = conn.ExecContext(ctx, insertDataSQL) 122 | Expect(err).NotTo(HaveOccurred(), "failed to insert data") 123 | } 124 | 125 | ctx, cancel = context.WithTimeout(context.Background(), defaultQueryTimeout) 126 | defer cancel() 127 | results, err := conn.QueryContext(ctx, selectDataSQL) 128 | Expect(err).NotTo(HaveOccurred(), "failed to get data") 129 | 130 | var data []TestData 131 | for results.Next() { 132 | var d TestData 133 | err = results.Scan(&d.timestamp, &d.n, &d.rowID) 134 | Expect(err).NotTo(HaveOccurred(), "failed to scan data that query from db") 135 | data = append(data, d) 136 | } 137 | Expect(len(data) == testRowIDNum).Should(BeTrue(), "get the wrong data from db") 138 | 139 | err = deleteCluster() 140 | Expect(err).NotTo(HaveOccurred(), "failed to delete cluster") 141 | }) 142 | }) 143 | 144 | func createCluster() error { 145 | cmd := exec.Command("../../bin/gtctl", "cluster", "create", "mydb", "--timeout", "300") 146 | cmd.Stdout = os.Stdout 147 | cmd.Stderr = os.Stderr 148 | if err := cmd.Run(); err != nil { 149 | return err 150 | } 151 | return nil 152 | } 153 | 154 | func getCluster() error { 155 | cmd := exec.Command("../../bin/gtctl", "cluster", "get", "mydb") 156 | cmd.Stdout = os.Stdout 157 | cmd.Stderr = os.Stderr 158 | if err := cmd.Run(); err != nil { 159 | return err 160 | } 161 | return nil 162 | } 163 | 164 | func listCluster() error { 165 | cmd := exec.Command("../../bin/gtctl", "cluster", "list") 166 | cmd.Stdout = os.Stdout 167 | cmd.Stderr = os.Stderr 168 | if err := cmd.Run(); err != nil { 169 | return err 170 | } 171 | return nil 172 | } 173 | 174 | func deleteCluster() error { 175 | cmd := exec.Command("../../bin/gtctl", "cluster", "delete", "mydb", "--tear-down-etcd") 176 | cmd.Stdout = os.Stdout 177 | cmd.Stderr = os.Stderr 178 | if err := cmd.Run(); err != nil { 179 | return err 180 | } 181 | return nil 182 | } 183 | 184 | func forwardRequest() { 185 | for { 186 | cmd := exec.Command("kubectl", "port-forward", "svc/mydb-frontend", "4002:4002") 187 | if err := cmd.Run(); err != nil { 188 | klog.Errorf("Failed to port forward: %v", err) 189 | return 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /tests/e2e/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Greptime Team 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo/v2" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestGtctl(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Gtctl Suite") 29 | } 30 | --------------------------------------------------------------------------------