├── .gitattributes ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Jenkinsfile ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── _cicd ├── Makefile ├── functions.sh └── gcloud │ └── Dockerfile ├── cmd └── lenses-cli │ └── main.go ├── doc.go ├── example └── main.go ├── go.mod ├── go.sum ├── pkg ├── acl │ ├── commands.go │ └── commands_test.go ├── alert │ ├── channels_commands.go │ ├── commands.go │ ├── commands_test.go │ ├── helpers.go │ ├── helpers_test.go │ └── payloads.go ├── api │ ├── alert_channel_templates_client.go │ ├── alert_client.go │ ├── as_code.go │ ├── audit_channel_templates_client.go │ ├── channels_client.go │ ├── client.go │ ├── client_authentication.go │ ├── client_test.go │ ├── config.go │ ├── config_json.go │ ├── config_json_test.go │ ├── config_test.go │ ├── config_yaml.go │ ├── config_yaml_test.go │ ├── connection_client.go │ ├── connection_client_dyn_gen.go │ ├── connection_client_gen.go │ ├── conntemplate_client.go │ ├── consumers_client.go │ ├── datasets_client.go │ ├── datasets_client_gen.go │ ├── datasets_dto_gen.go │ ├── datasets_dto_test.go │ ├── elasticsearch.go │ ├── elasticsearch_test.go │ ├── files.go │ ├── group_client.go │ ├── lenses.go │ ├── license.go │ ├── provisioning_client.go │ ├── schemas_client.go │ ├── service_account_client.go │ ├── topicsettings_client.go │ ├── user_client.go │ └── wizard_gen.go ├── ascode │ └── resource_commands.go ├── audit │ ├── channels_commands.go │ └── commands.go ├── configs │ ├── commands.go │ └── configuration.go ├── connection │ ├── aws.go │ ├── commands.go │ ├── common.go │ ├── datadog.go │ ├── doc.go │ ├── elastic.go │ ├── elastic_test.go │ ├── flagmapper.go │ ├── flagmapper_test.go │ ├── generic.go │ ├── generic_test.go │ ├── glue.go │ ├── glue_test.go │ ├── kafka.go │ ├── kafka_test.go │ ├── kafkaconnect.go │ ├── kerberos.go │ ├── pagerduty.go │ ├── postgres.go │ ├── prometheus.go │ ├── schemaregistry.go │ ├── slack.go │ ├── splunk.go │ ├── webhook.go │ └── zookeeper.go ├── connector │ └── commands.go ├── conntemplate │ ├── commands.go │ └── commands_test.go ├── constants.go ├── consumers │ ├── commands.go │ └── commands_test.go ├── dataset │ ├── commands.go │ └── commands_test.go ├── elasticsearch │ ├── commands.go │ ├── commands_test.go │ ├── payloads.go │ └── payloads_test.go ├── export │ ├── acl_commands.go │ ├── alert_channels_commands.go │ ├── alert_commands.go │ ├── audit_channels_commands.go │ ├── channels.go │ ├── commands.go │ ├── connection_commands.go │ ├── connection_commands_test.go │ ├── connector_commands.go │ ├── fileWriter.go │ ├── group_commands.go │ ├── policy_commands.go │ ├── processors_commands.go │ ├── quota_commands.go │ ├── repo_commands.go │ ├── schemas_commands.go │ ├── service_account_commands.go │ ├── topic_commands.go │ └── topicsettings_commands.go ├── import │ ├── acl_commands.go │ ├── alert_channels_commands.go │ ├── alert_commands.go │ ├── audit_channels_commands.go │ ├── channels.go │ ├── connection_commands.go │ ├── connector_commands.go │ ├── group_commands.go │ ├── import_group_commands.go │ ├── policy_commands.go │ ├── processor_commands.go │ ├── quota_commands.go │ ├── schemas_commands.go │ ├── service_account_commands.go │ ├── topic_commands.go │ └── topicsettings_commands.go ├── initcontainer │ ├── command.go │ └── commands_test.go ├── license │ ├── commands.go │ └── commands_test.go ├── logs │ └── commands.go ├── management │ ├── group_commands.go │ ├── group_commands_test.go │ ├── service_account_commands.go │ ├── service_account_commands_test.go │ ├── user_commands.go │ ├── user_commands_test.go │ └── utils.go ├── policy │ ├── commands.go │ └── commands_test.go ├── processor │ ├── commands.go │ └── commands_test.go ├── provision │ ├── commands.go │ ├── provision.go │ ├── provision_test.go │ └── testing │ │ ├── my-file.txt │ │ └── my-lic.json ├── quota │ └── commands.go ├── schemas │ └── commands.go ├── secret │ ├── commands.go │ └── secrets.go ├── shell │ └── shell_command.go ├── sql │ ├── commands.go │ ├── completer.go │ └── executor.go ├── topic │ ├── commands.go │ └── payloads.go ├── topicsettings │ ├── commands.go │ └── commands_test.go ├── user │ ├── configure_user_command.go │ ├── configure_user_command_test.go │ └── user_profile_command.go ├── utils │ ├── colors.go │ ├── utils.go │ └── utils_test.go └── websocket │ └── ws.go ├── publish-docker └── test ├── command_helper.go ├── config_manager.go └── testing.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go linguist-language=Go 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | *.exe 3 | .idea/ 4 | .vscode/ 5 | /demo 6 | connector.props 7 | secrets.props 8 | worker.props 9 | cover.out 10 | cover.html 11 | _cicd/local.env 12 | alert-settings/ 13 | alert-channels/ 14 | connections/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | language: go 4 | os: 5 | - linux 6 | cache: 7 | directories: 8 | - $GOPATH/.cache/go-build 9 | - $GOPATH/pkg/mod 10 | go: 11 | - go1.13 12 | go_import_path: github.com/lensesio/lenses-go 13 | install: 14 | go get golang.org/x/lint/golint 15 | 16 | jobs: 17 | include: 18 | - stage: Build 19 | script: REVISION=${TRAVIS_COMMIT} VERSION=${TRAVIS_COMMIT} make cross-build 20 | 21 | - stage: Lint 22 | script: make lint 23 | 24 | - stage: Tests 25 | script: make test 26 | 27 | - stage: Build docker 28 | deploy: 29 | on: 30 | tags: false 31 | provider: script 32 | script: REVISION=${TRAVIS_COMMIT} make build-linux && VERSION=${TRAVIS_COMMIT} make build-docker 33 | 34 | - stage: Publish docker 35 | branches: 36 | only: 37 | - master 38 | deploy: 39 | on: 40 | tags: true 41 | provider: script 42 | script: REVISION=${TRAVIS_COMMIT} VERSION=${TRAVIS_TAG} make build-linux && make publish 43 | 44 | - stage: Release 45 | branches: 46 | only: 47 | - master 48 | before_deploy: REVISION=${TRAVIS_COMMIT} VERSION=${TRAVIS_TAG} make cross-build; cd bin; for i in *; do tar -czf $i.tar.gz $i; done; ls -1; cd ../; 49 | deploy: 50 | on: 51 | tags: true 52 | provider: releases 53 | name: Release ${TRAVIS_TAG} 54 | api_key: 55 | secure: q+kT2K6ZMh4qtiveEmsuC8t/T5IZTZd9zbHeOBQn1YoFt38ym9za94Ehq13jffEvsHOGKX1oc7FxpUkDInDllSXQrYXC2QNEhnA9ZmzijLzyVEKBNir7ts0g0cNXXlEhu+o86z+3nZi14x77NujF+u9ALSOqpLmfo6HmHtCows/Bkf9WC8LQ1wWla4ovh8BkCLe00P+G36tBzpm7khi1In0DaIgQdyG9rUCLQkfKy2smzfq+Cj9R2beXRGP6Zx7so5lwuGCl3xQL7s1LUct/4GyNyTOIpCfZtllJddghw6mY+pJAsurfjP/qdSrGCVj+WAYWh6ZtH0XqqNQe0QnH0NrBC9JCc7oixC/quhz3DEn7ufbUzNxqTOoHWw6wYcJoP2XaUxCXOsQ7KmLmAEa+WtUWxpalHrSScTz6NlxTBFlLGYL1Gl2OxpB4vmOVOS7/5HE1rFJAGYZkpgv4qiD9REoSkD8Qt3eG/BCv9gE7HVgeeiftGD4fEiVxNQw4lt09J/49RZ4ZrGBGcMMd9PGW0cOTYaFoTW+Sy/wKifsUPJSEfm7Avw/LOfuxhDytmbiUIRaFfchfEm8u5UwkOpas8ooGOtwK7bAUG83Le6lndeiAxQyA+0a0vTMkkRqSI1sxN6uV+tflVQZd6ULG3WrdvL/Bqq/ji9gQsIIEDwCavI4= 56 | file_glob: true 57 | file: bin/*.tar.gz 58 | skip_cleanup: true 59 | overwrite: true 60 | env: 61 | global: 62 | - GO111MODULE=on 63 | - secure: II8qslnN2wn164YxEEynMJyFmIbNi6IfEX+RRByA2f2VgWaFW1AHeDaSIH6WkXBnQtYm00c2Wx6Rq6TkPcpkkWnGw5jV66jiJ028/mjZSKCHqPWi8QNxvk76ZMEqTYw1Ly2vaM6HMDqQ5gUSFJSuyv56GVc3RkaT8/AltOCppIcoeqQSgjc3A8lU3NrUmx+2N7scAGCNZ/UVckcAghBnjIUW6wnWMQodN6NkKq5RVugt+BPJY+KRBQGD2k7FasixpK2Yru6DEotDqbCWpqBX7SUf0IJh6qok1/BipkHBghK6l2ajF6Rfbmi3WnXhjzm5gT+IsGAZYJvhcc3cwhrlir4BwymGxip57EBMeg/Y2rxVEQB3TVmIvvstuwUyD5BcBa2tqP96cPRcf0G1A//O6EEvh1aW4aE7bhDVWF+rKB4UV6x7QCGR3nwLDkhBxS9dsAEuAoFmIBeUldJJCPivxHyHoMDV7nk1aKna11iJXx9cWlYnPzM8S91mGMSJqRa6c15xOQFfkMMJ1qynCen3xA/ECsLWsCMvuQ6hk7OnsfTRiKPf0GOmM8Y1VCHSPPDcjwjwhfkRdsUnTjyRoYKz1xxtz3BGR+7mhHcNEgphDQM7KwnoleW7TysRL2BKkHbD5Z9lQfMQiS9TQjEwqVw2VDoZj8kV0eqh7eCBV9ViBZc= 64 | - secure: G7lHGM9M92Bf+DbjUupRpOXes0VGUkzmCV+yngz+4YfFbVlimIEd1H1DFRDZ6Vl/SjBTSQ6xyogiTOUA4FHgdaIp7bF7BnwekJutfZHnbTntT9Em2KfXyLZKTgPfHOQhD+lOqtPpi/UVa5xjn6qYSdWB1D7naoTg8ihSRYn/rJC/P2QByOUBUwd5Ik9YTks0skJRGxjOAmLew5Ym4MWvtlTmLtkVnmWXDgJdhiVq/0aeg31fsYRptoBG95kYvKcmQMza/OdP+0umj0fWujCEZTQvwZK51aJXy/qdAMI/+LEvECe+/fotsiSvns83Po5AnlhX4l/1xGNkeNMUSMCRSLBRn8mZ1k/Gi28W2r22p2cplTvg0Hfdklshl9ob5sl9jAKPmA5lr6JzRjxKU2a2vIWJbtLSM5iHMgFA1l5ACJ3Q1+V9GvVnhEMU59ALjmzXnr+a+fgckABbWo7o20/VfE8ZdDGpryIH3TTVpso8AO9dZMgZ3W2UFqj2I1EK8pB90TLf/H9HC6+iwHNS1VycYV+vNCViqycQ/NnGtxB9A8hcV/o7D7yHCl/N38zYKy3nyTgoOoeOivAMcgYZR4tiUlnUMqoqw947UmUClaDHHbG8Lz9o8gOLQHvZbeHKkve/zG8Id8/tmeYgHYBszJXJzIP33MruTZWlJaTBiZJJNyg= 65 | 66 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at https://lensesio.slack.com. 59 | All complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please make sure that you've read our [Code of Conduct](https://github.com/lensesio/lenses-go/blob/master/CODE_OF_CONDUCT.md) first. 4 | 5 | ## PR 6 | 7 | 1. Raise an issue at https://github.com/lensesio/lenses-go/issues. 8 | 2. Describe the issue, what did you expected to see and what you saw instead. 9 | 3. Link the issue in your PR. 10 | 4. Wait for response, we might request code changes before accept it. 11 | 12 | ## Documentation 13 | 14 | https://lenses.stream/using-lenses/cli/index.html -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | ARG TARGETARCH TARGETOS 3 | 4 | RUN apk add --no-cache bash jq curl gettext 5 | ADD bin/lenses-cli-linux-${TARGETARCH} /opt/lenses/lenses-cli 6 | ENV PATH /opt/lenses/:$PATH 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build clean 2 | 3 | PKG_LIST:=$(shell go list ./... | grep -v /vendor/) 4 | EXECUTABLE:=lenses-cli 5 | OUTPUT:=bin 6 | LDFLAGS:= -ldflags "-s -w -X main.buildVersion=${VERSION} -X main.buildRevision=${REVISION} -X main.buildTime=$(shell date +%s)" 7 | 8 | help: 9 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 10 | 11 | build: dep ## Build for development only 12 | go build -o ./${OUTPUT}/${EXECUTABLE} ./cmd/${EXECUTABLE} 13 | 14 | build-fast: ## Build for development only without verifying dependencies 15 | go build -o ./${OUTPUT}/${EXECUTABLE} ./cmd/${EXECUTABLE} 16 | 17 | build-linux: dep ## Build binary for linux 18 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-linux-amd64 ./cmd/${EXECUTABLE} 19 | 20 | build-docker: ## Builds Docker with linux lenses-cli 21 | docker build -t lensesio/lenses-cli:${VERSION} . 22 | 23 | dep: ## Ensure dependencies 24 | go mod verify 25 | 26 | clean: dep ## Clean 27 | go clean 28 | rm -r bin/ 29 | rm -f cover.out 30 | 31 | cross-build: dep ## Build the app for multiple os/arch 32 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-darwin-amd64 ./cmd/${EXECUTABLE} 33 | GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-darwin-arm64 ./cmd/${EXECUTABLE} 34 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-linux-amd64 ./cmd/${EXECUTABLE} 35 | GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-linux-arm64 ./cmd/${EXECUTABLE} 36 | GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ${LDFLAGS} -o ${OUTPUT}/${EXECUTABLE}-windows-amd64.exe ./cmd/${EXECUTABLE} 37 | 38 | format-check: ## Check format of source code according to Go's best practices 39 | @goimports -l cmd/ pkg/ test/ 40 | 41 | publish: ## Publish lenses CLI as docker 42 | bash -c "./publish-docker" 43 | 44 | race: dep ## Run data race detector 45 | go test -race -short ${PKG_LIST} 46 | 47 | setup: ## Get all the necessary dependencies 48 | go get -u golang.org/x/lint/golint 49 | go get -u golang.org/x/tools/cmd/goimports 50 | 51 | test: dep ## Run tests 52 | go test -coverprofile=cover.out ./... 53 | go tool cover -func=cover.out 54 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Third Party libraries and components 2 | ------------------------------------ 3 | 4 | uuid : MIT - https://github.com/satori/go.uuid 5 | survey : MIT - https://github.com/kataras/survey 6 | websocket : BSD-2 - https://github.com/gorilla/websocket 7 | golog : BSD-3 - https://github.com/kataras/golog 8 | cobra : Apache 2.0 - https://github.com/spf13/cobra 9 | yaml : Apache 2.0 - https://github.com/go-yaml/yaml 10 | godotenv : MIT - https://github.com/joho/godotenv 11 | gokrb5 : Apache 2.0 - https://github.com/jcmturner/gokrb5 12 | go-prompt : MIT - https://github.com/c-bata/go-prompt -------------------------------------------------------------------------------- /_cicd/Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash -o errexit -o nounset -o pipefail 2 | .PHONY: * 3 | 4 | # thanks to https://gist.github.com/mpneuried/0594963ad38e68917ef189b4e6a269db 5 | # ENV_FILE ?= _build/local.env 6 | # include $(ENV_FILE) 7 | 8 | # JENKINS_URL= 9 | # JENKINS_USER_ID= 10 | # JENKINS_API_TOKEN= 11 | # JENKINS_CLI_PATH= 12 | # JENKINS_JOB= 13 | # JENKINS_JOB_PARAMS= 14 | JENKINS_CLI = java -jar ${JENKINS_CLI_PATH} 15 | 16 | # thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 17 | help: ## This help 18 | @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / \ 19 | {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) 20 | 21 | jenkins-cli-help: ## 22 | ${JENKINS_CLI} help 23 | @printf "\033[36m%-30s\033[0m %s\n" "'$@' finished successfully!" 24 | 25 | lint: ## 26 | ${JENKINS_CLI} declarative-linter < ../Jenkinsfile 27 | @printf "\033[36m%-30s\033[0m %s\n" "'$@' finished successfully!" 28 | 29 | build: ## 30 | @# e.g. make build JENKINS_JOB_PARAMS="DOCKER_GO_IMG=golang:1.11" 31 | @if [[ -n "${JENKINS_JOB_PARAMS}" ]]; then\ 32 | ${JENKINS_CLI} build ${JENKINS_JOB} -v -f -p ${JENKINS_JOB_PARAMS};\ 33 | fi 34 | ${JENKINS_CLI} build ${JENKINS_JOB} -v -f 35 | 36 | replay: ## 37 | ${JENKINS_CLI} replay-pipeline ${JENKINS_JOB} < ../Jenkinsfile 38 | 39 | restart: ## 40 | @# JENKINS_STAGE is case sensitive 41 | ${JENKINS_CLI} restart-from-stage --job ${JENKINS_JOB} \ 42 | --stage "${JENKINS_STAGE}" 43 | 44 | console: ## 45 | @# $JENKINS_JOB should be in the form of for mb 46 | ${JENKINS_CLI} console ${JENKINS_JOB} 47 | 48 | watch-console: ## 49 | watch -d -n 0.1 -c "${JENKINS_CLI} console ${JENKINS_JOB} -f | tail -50" 50 | 51 | docker-build: ## 52 | docker run --rm \ 53 | --volume $(shell dirname $(shell pwd)):/src --workdir /src \ 54 | --volume /tmp:/tmp \ 55 | --user $(shell id --user):$(shell id --group) \ 56 | --env HOME=/tmp/cli-cache/home \ 57 | --env GOPATH=/tmp/cli-cache/go/ \ 58 | golang:1.19 /src/_cicd/functions.sh build 59 | 60 | docker-cross-build: ## 61 | docker run --rm --volume $(shell dirname $(shell pwd)):/src --workdir /src \ 62 | --volume /tmp:/tmp \ 63 | --user $(shell id --user):$(shell id --group) \ 64 | --env HOME=/tmp/cli-cache/home \ 65 | --env GOPATH=/tmp/cli-cache/go \ 66 | golang:1.19 /src/_cicd/functions.sh cross-build 67 | 68 | docker-build-shell: ## 69 | docker run --user $(shell id --user):$(shell id --group) -it \ 70 | --volume $(shell dirname $(shell pwd)):/src \ 71 | --volume /tmp:/tmp \ 72 | --env HOME=/tmp/cli-cache/home \ 73 | --env GOPATH=/tmp/cli-cache/go \ 74 | --workdir /src --rm \ 75 | golang:1.19 bash 76 | 77 | docker-gcloud-build: ## 78 | docker run -it --volume $(shell cd .. && pwd):/src \ 79 | --env GCLOUD_SA_KEY=$(shell echo {GCLOUD_SA_KEY} | sed 's/{/\\{/g' | sed 's/}/\\}/g' | sed 's/,/\\,/g') \ 80 | --workdir /src --rm \ 81 | --volume ~/Downloads:/tmp/downloads \ 82 | --user $(shell id --user):$(shell id --group) \ 83 | --env HOME=/tmp/cli-cache/home \ 84 | google/cloud-sdk:289.0.0 /src/_cicd/functions.sh archive 85 | docker-gcloud-shell: ## 86 | docker run -it --volume $(shell cd .. && pwd):/src \ 87 | --env HOME=/tmp/cli-cache/home \ 88 | --volume ~/Downloads:/tmp/downloads \ 89 | --workdir /src --rm \ 90 | google/cloud-sdk:289.0.0 bash 91 | -------------------------------------------------------------------------------- /_cicd/gcloud/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GCLOUD_DOCKER_IMAGE 2 | FROM ${GCLOUD_DOCKER_IMAGE} 3 | 4 | RUN apt-get update && apt-get install -y \ 5 | zip 6 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | lenses "github.com/lensesio/lenses-go/v5/pkg/api" 7 | ) 8 | 9 | func main() { 10 | // Prepare authentication using raw Username and Password. 11 | auth := lenses.BasicAuthentication{Username: "user", Password: "pass"} 12 | // Or authenticate using one of those three Kerberos built'n supported authentication methods. 13 | /* 14 | kerberosAuthWithPass := lenses.KerberosAuthentication{ 15 | ConfFile: "/etc/krb5.conf", 16 | Method: lenses.KerberosWithPassword{Realm: "my.realm or default if empty", Username: "user", Password: "pass"}, 17 | } 18 | 19 | kerberosAuthWithKeytab := lenses.KerberosAuthentication{ 20 | ConfFile: "/etc/krb5.conf", 21 | Method: lenses.KerberosWithKeytab{KeytabFile: "/home/me/krb5_my_keytab.txt"}, 22 | } 23 | 24 | kerberosFromCCacheAuth := lenses.KerberosAuthentication{ 25 | ConfFile: "/etc/krb5.conf", 26 | Method: lenses.KerberosFromCCache{CCacheFile: "/tmp/krb5_my_cache_file.conf"}, 27 | } 28 | 29 | Custom auth can be implement as well: `Authenticate(client *lenses.Client) error` 30 | */ 31 | 32 | // Prepare the client's configuration based on the host and the authentication above. 33 | clientConfig := lenses.ClientConfig{Host: "lenses.example", Authentication: auth, Timeout: "15s", Debug: true} 34 | 35 | // Creating the client using the configuration. 36 | client, err := lenses.OpenConnection(clientConfig) // or (config, lenses.UsingClient(customClient)/UsingToken(ready token string)) 37 | if err != nil { 38 | // handle error. 39 | panic(err) 40 | } 41 | 42 | // Using a client's method to do API calls. 43 | // All lenses-go methods return a typed value based on the call 44 | // and an error as second output so you can catch any error coming from backend or client, forget panics. 45 | // Go types are first class citizens here, we will not confuse you or let you work based on luck! 46 | topics, err := client.GetTopics() 47 | // Example on how deeply we make the difference here: 48 | // `Client#GetTopics` returns `[]lenses.Topic`, so you can work safely. 49 | // topics[index].ConsumersGroup[index].Coordinator.Host 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | // Print the length of the topics we've just received from our Lenses Box. 55 | print(len(topics)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/acl/commands_test.go: -------------------------------------------------------------------------------- 1 | package acl 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | "github.com/spf13/cobra" 11 | "github.com/stretchr/testify/require" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | var validACL = api.ACL{ 16 | PermissionType: api.ACLPermissionType("allow"), 17 | Principal: "user:bob", 18 | Operation: "read", 19 | ResourceType: "topic", 20 | PatternType: "literal", 21 | ResourceName: "transactions", 22 | Host: "acme.com", 23 | } 24 | 25 | var invalidACL = api.ACL{ 26 | PermissionType: api.ACLPermissionType("allow"), 27 | Principal: "user:bob", 28 | Operation: "read", 29 | ResourceName: "transactions", 30 | Host: "acme.com", 31 | } 32 | 33 | func Test_populateACL(t *testing.T) { 34 | var yamlFiles = []struct { 35 | path string 36 | payload api.ACL 37 | }{ 38 | { 39 | path: "/tmp/validACL.yaml", 40 | payload: validACL, 41 | }, 42 | { 43 | // Missing a couple required params 44 | path: "/tmp/invalidACL.yaml", 45 | payload: invalidACL, 46 | }, 47 | } 48 | 49 | // Let's create the yaml files to be used for testing and delete them when finished 50 | for _, file := range yamlFiles { 51 | 52 | out, _ := yaml.Marshal(file.payload) 53 | err := ioutil.WriteFile(file.path, out, 0644) 54 | require.NoError(t, err) 55 | defer os.Remove(file.path) 56 | } 57 | 58 | type args struct { 59 | cmd *cobra.Command 60 | args []string 61 | } 62 | tests := []struct { 63 | name string 64 | args args 65 | want api.ACL 66 | wantErr bool 67 | }{ 68 | { 69 | name: "'acl set' with no arguments or flags should fail", 70 | args: args{ 71 | cmd: NewCreateOrUpdateACLCommand(), 72 | args: []string{}, 73 | }, 74 | want: api.ACL{}, 75 | wantErr: true, 76 | }, 77 | { 78 | name: "'acl set' with valid ACL yaml file", 79 | args: args{ 80 | cmd: NewCreateOrUpdateACLCommand(), 81 | args: []string{"/tmp/validACL.yaml"}, 82 | }, 83 | want: validACL, 84 | wantErr: false, 85 | }, 86 | { 87 | name: "'acl set' with invalid param", 88 | args: args{ 89 | cmd: NewCreateOrUpdateACLCommand(), 90 | args: []string{"hello.txt"}, 91 | }, 92 | want: api.ACL{}, 93 | wantErr: true, 94 | }, 95 | { 96 | name: "'acl set' with invalid param", 97 | args: args{ 98 | cmd: NewCreateOrUpdateACLCommand(), 99 | args: []string{"hello.yaml"}, 100 | }, 101 | want: api.ACL{}, 102 | wantErr: true, 103 | }, 104 | { 105 | name: "'acl set' with invalid ACL yaml file", 106 | args: args{ 107 | cmd: NewCreateOrUpdateACLCommand(), 108 | args: []string{"/tmp/invalidACL.yaml"}, 109 | }, 110 | want: invalidACL, 111 | wantErr: true, 112 | }, 113 | } 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | got, err := populateACL(tt.args.cmd, tt.args.args) 117 | if (err != nil) != tt.wantErr { 118 | t.Errorf("populateACL() error = %v, wantErr %v", err, tt.wantErr) 119 | return 120 | } 121 | if !reflect.DeepEqual(got, tt.want) { 122 | t.Errorf("populateACL() = %v, want %v", got, tt.want) 123 | } 124 | }) 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /pkg/alert/payloads.go: -------------------------------------------------------------------------------- 1 | package alert 2 | 3 | // SettingConditionPayloads is the payload for creating alert setttings 4 | type SettingConditionPayloads struct { 5 | AlertID int `json:"alert" yaml:"alert"` 6 | Conditions []string `json:"conditions" yaml:"conditions"` 7 | } 8 | 9 | // SettingConditionPayload is the payload for creating alert setttings 10 | type SettingConditionPayload struct { 11 | AlertID int `json:"alert" yaml:"alert"` 12 | ConditionID string `json:"conditionID,omitempty" yaml:"conditionID"` 13 | Channels []string `json:"channels" yaml:"channels"` 14 | Topic string `yaml:"topic,omitempty"` 15 | Group string `yaml:"group,omitempty"` 16 | Threshold int `yaml:"threshold,omitempty"` 17 | Mode string `yaml:"mode,omitempty"` 18 | MoreThan int `yaml:"more-than,omitempty"` 19 | LessThan int `yaml:"less-than,omitempty"` 20 | Duration string `yaml:"duration,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/alert_channel_templates_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/lensesio/lenses-go/v5/pkg" 7 | ) 8 | 9 | // ChannelTemplate payload struct used for alert and audit 10 | type ChannelTemplate struct { 11 | ID int `json:"id" yaml:"id"` 12 | Name string `json:"name" yaml:"name" header:"name"` 13 | TemplateVersion int `json:"templateVersion,omitempty" yaml:"templateVersion" header:"Template Version"` 14 | Version string `json:"version" yaml:"version" header:"version"` 15 | Enabled bool `json:"enabled" yaml:"enabled" header:"enabled"` 16 | BuiltIn bool `json:"builtIn" yaml:"builtin" header:"builtin"` 17 | Metadata struct { 18 | Author string `json:"author"` 19 | Description string `json:"description"` 20 | } `json:"metadata"` 21 | Configuration []struct { 22 | ID int `json:"id"` 23 | Key string `json:"key"` 24 | DisplayName string `json:"displayName"` 25 | Placeholder string `json:"placeholder"` 26 | Description string `json:"description"` 27 | Type struct { 28 | Name string `json:"name"` 29 | DisplayName string `json:"displayName"` 30 | EnumValues interface{} `json:"enumValues"` 31 | } `json:"type"` 32 | Required bool `json:"required"` 33 | Provided bool `json:"provided"` 34 | } `json:"configuration"` 35 | SuitableConnections []struct { 36 | TemplateName string `json:"templateName"` 37 | Name string `json:"name"` 38 | } `json:"suitableConnections"` 39 | } 40 | 41 | // GetAlertChannelTemplates returns all alert channel templates 42 | func (c *Client) GetAlertChannelTemplates() (response []ChannelTemplate, err error) { 43 | resp, err := c.Do(http.MethodGet, pkg.AlertChannelTemplatesPath, contentTypeJSON, nil) 44 | if err != nil { 45 | return 46 | } 47 | 48 | if err = c.ReadJSON(resp, &response); err != nil { 49 | return 50 | } 51 | 52 | return 53 | } 54 | -------------------------------------------------------------------------------- /pkg/api/as_code.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | const resourcePath = "api/v1/resource" 9 | const resourceKafkaPath = resourcePath + "/kafka" 10 | const resourceConnectBasePath = resourceKafkaPath + "/connect" //api/v1/resource/kafka/connect/{connect-cluster-name}/connector/{connector-name}" 11 | 12 | // Returns the connector as code 13 | func (c *Client) GetConnectorAsCode(cluster string, connector string) (yaml string, err error) { 14 | path := fmt.Sprintf("%s/%s/connector/%s", resourceConnectBasePath, cluster, connector) 15 | //c.Do handles the error handling 16 | resp, err := c.Do(http.MethodGet, path, contentTypeYaml, nil) 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | defer resp.Body.Close() 22 | 23 | bodyBytes, err := c.ReadResponseBody(resp) 24 | if err != nil { 25 | return "", fmt.Errorf("failed to read response body: %w", err) 26 | } 27 | 28 | yaml = string(bodyBytes) 29 | 30 | return yaml, nil 31 | } 32 | 33 | func (c *Client) ImportResource(yaml string) error { 34 | // c.Do handles the error handling 35 | response, err := c.Do(http.MethodPut, resourcePath, contentTypeYaml, []byte(yaml)) 36 | if err != nil { 37 | return err 38 | } 39 | response.Body.Close() 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/api/audit_channel_templates_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/lensesio/lenses-go/v5/pkg" 7 | ) 8 | 9 | // GetAuditChannelTemplates returns all audit channel templates 10 | func (c *Client) GetAuditChannelTemplates() (response []ChannelTemplate, err error) { 11 | resp, err := c.Do(http.MethodGet, pkg.AuditChannelTemplatesPath, contentTypeJSON, nil) 12 | if err != nil { 13 | return 14 | } 15 | 16 | if err = c.ReadJSON(resp, &response); err != nil { 17 | return 18 | } 19 | 20 | return 21 | } 22 | -------------------------------------------------------------------------------- /pkg/api/client_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestAPIConfig(t *testing.T) { 9 | apiConfigTests := []struct { 10 | name string 11 | httpResponse string 12 | expectValue int 13 | }{ 14 | { 15 | "Empty string must unmarshal to 0 int value", 16 | `{"lenses.jmx.port":""}`, 17 | 0, 18 | }, 19 | { 20 | "Any integer should deserialize to int", 21 | `{"lenses.jmx.port":6}`, 22 | 6, 23 | }, 24 | { 25 | "Any int passed as string should deserialize to int", 26 | `{"lenses.jmx.port":"69"}`, 27 | 69, 28 | }, 29 | { 30 | "null should deserialize to 0 int value", 31 | `{"lenses.jmx.port":null}`, 32 | 0, 33 | }, 34 | } 35 | 36 | for _, tt := range apiConfigTests { 37 | var cfg BoxConfig 38 | 39 | err := json.Unmarshal([]byte(tt.httpResponse), &cfg) 40 | if err != nil { 41 | t.Error("failed to unmarshal: ", err) 42 | return 43 | } 44 | 45 | if tt.expectValue != int(cfg.JMXPort) { 46 | t.Error(tt.name) 47 | t.Errorf("got `%v`, want `%v`", cfg.JMXPort, tt.expectValue) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/api/conntemplate_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | ) 9 | 10 | // Connection Templates API 11 | 12 | // ConnectionTemplateMetadata type 13 | type ConnectionTemplateMetadata struct { 14 | Author string `json:"author" yaml:"author" header:"Author,text"` 15 | Description string `json:"description" yaml:"description" header:"Description,text"` 16 | DocURL string `json:"docUrl" yaml:"docUrl" header:"Doc Url,text"` 17 | GitRepo string `json:"gitRepo" yaml:"gitRepo" header:"Git Repo,text"` 18 | GitCommit string `json:"gitCommit" yaml:"gitCommit" header:"Git Commit,text"` 19 | Image string `json:"image" yaml:"image" header:"Image,text"` 20 | ImageTag string `json:"imageTag" yaml:"imageTag" header:"Image Tag,text"` 21 | } 22 | 23 | // ConnectionTemplateConfigType type 24 | type ConnectionTemplateConfigType struct { 25 | Name string `json:"name" yaml:"name" header:"Name,text"` 26 | DisplayName string `json:"displayName" yaml:"DisplayName" header:"Display Name,text"` 27 | } 28 | 29 | // ConnectionTemplateConfig type 30 | type ConnectionTemplateConfig struct { 31 | Key string `json:"key" yaml:"key" header:"key,text"` 32 | DisplayName string `json:"displayName" yaml:"displayName" header:"Display Name,text"` 33 | Placeholder string `json:"placeholder" yaml:"placeholder" header:"Placeholder,text"` 34 | Description string `json:"description" yaml:"description" header:"Description,text"` 35 | Required bool `json:"required" yaml:"required" header:"Required,text"` 36 | Mounted bool `json:"mounted" yaml:"mounted" header:"Mounted,text"` 37 | Type ConnectionTemplateConfigType `json:"type" yaml:"type" header:"Type,text"` 38 | } 39 | 40 | // ConnectionTemplate type 41 | type ConnectionTemplate struct { 42 | Name string `json:"name,omitempty" yaml:"name" header:"Name,text"` 43 | TemplateVersion int `json:"templateVersion,omitempty" yaml:"templateVersion" header:"Template Version"` 44 | Version string `json:"version,omitempty" yaml:"version" header:"Version,text"` 45 | BuiltIn bool `json:"builtIn,omitempty" yaml:"buildIn" header:"BuiltIn,text"` 46 | Enabled bool `json:"enabled,omitempty" yaml:"enabled" header:"Enabled,text"` 47 | Category string `json:"category,omitempty" yaml:"category"` 48 | Type string `json:"type,omitempty" yaml:"type"` 49 | Metadata ConnectionTemplateMetadata `json:"metadata,omitempty" yaml:"metadata"` 50 | Config []ConnectionTemplateConfig `json:"configuration,omitempty" yaml:"configuration"` 51 | } 52 | 53 | // GetConnectionTemplates returns all connections 54 | func (c *Client) GetConnectionTemplates() (response []ConnectionTemplate, err error) { 55 | path := fmt.Sprintf("api/%s", pkg.ConnectionTemplatesAPIPath) 56 | 57 | resp, err := c.Do(http.MethodGet, path, contentTypeJSON, nil) 58 | if err != nil { 59 | return 60 | } 61 | 62 | if err = c.ReadJSON(resp, &response); err != nil { 63 | return 64 | } 65 | 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /pkg/api/consumers_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | ) 10 | 11 | // SingleTopicOffset represent the payload structure 12 | // of the API for updating a single partition of a single topic. 13 | type SingleTopicOffset struct { 14 | Type string `json:"type" yaml:"type"` 15 | Offset int `json:"offset,omitempty" yaml:"offset"` 16 | } 17 | 18 | // MultipleTopicOffsets represent the payload structure 19 | // of the API for updating all partitions of multiple topics. 20 | type MultipleTopicOffsets struct { 21 | Type string `json:"type" yaml:"type"` 22 | Target string `json:"target,omitempty" yaml:"type"` 23 | Topics []string `json:"topics,omitempty" yaml:"topics"` 24 | } 25 | 26 | // UpdateSingleTopicOffset handles the API call to update 27 | // a signle partition of a topic. 28 | func (c *Client) UpdateSingleTopicOffset(groupID, topic, partitionID, offsetType string, offset int) error { 29 | if offsetType == "" { 30 | return errRequired("field `type` is missing") 31 | } 32 | 33 | path := fmt.Sprintf("%s/%s/offsets/topics/%s/partitions/%s", pkg.ConsumersGroupPath, groupID, topic, partitionID) 34 | singleTopic := SingleTopicOffset{Type: offsetType, Offset: offset} 35 | payload, err := json.Marshal(singleTopic) 36 | 37 | resp, err := c.Do(http.MethodPut, path, contentTypeJSON, payload) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | defer resp.Body.Close() 43 | 44 | return nil 45 | } 46 | 47 | // UpdateMultipleTopicsOffset handles the Lenses API call to update 48 | // all partitions of multiple topics of a consumer group. 49 | func (c *Client) UpdateMultipleTopicsOffset(groupID, offsetType, target string, topics []string) error { 50 | path := fmt.Sprintf("%s/%s/offsets", pkg.ConsumersGroupPath, groupID) 51 | multipleTopics := MultipleTopicOffsets{Type: offsetType, Target: target, Topics: topics} 52 | payload, err := json.Marshal(multipleTopics) 53 | 54 | _, err = c.Do(http.MethodPut, path, contentTypeJSON, payload) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // DeleteSingleTopicOffset handles the API call to delete consumer group offsets for 63 | // a signle partition of a topic. 64 | func (c *Client) DeleteSingleTopicOffset(groupID, topic, partitionID string) error { 65 | 66 | path := fmt.Sprintf("%s/%s/topics/%s/partitions/%s/offsets", pkg.ConsumersGroupPath, groupID, topic, partitionID) 67 | 68 | resp, err := c.Do(http.MethodDelete, path, contentTypeJSON, nil) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | defer resp.Body.Close() 74 | 75 | return nil 76 | } 77 | 78 | // DeleteMultipleTopicsOffset handles the Lenses API call to delete 79 | // all partitions of multiple topics of a consumer group. 80 | func (c *Client) DeleteMultipleTopicsOffset(groupID string, topics []string) error { 81 | path := fmt.Sprintf("%s/%s/offsets/delete", pkg.ConsumersGroupPath, groupID) 82 | multipleTopics := MultipleTopicOffsets{Topics: topics} 83 | payload, err := json.Marshal(multipleTopics) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | resp, err := c.Do(http.MethodPost, path, contentTypeJSON, payload) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | defer resp.Body.Close() 94 | 95 | return nil 96 | } 97 | 98 | // DeleteConsumerGroup handles the Lenses API call to delete a specific consumer group 99 | func (c *Client) DeleteConsumerGroup(groupID string) error { 100 | path := fmt.Sprintf("%s/%s", pkg.ConsumersGroupPath, groupID) 101 | 102 | resp, err := c.Do(http.MethodDelete, path, contentTypeJSON, nil) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | defer resp.Body.Close() 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/api/datasets_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | ) 12 | 13 | // UpdateDatasetDescription Struct 14 | type UpdateDatasetDescription struct { 15 | Description string `json:"description,omitempty" yaml:"description,omitempty"` 16 | } 17 | 18 | // UpdateDatasetTags struct 19 | type UpdateDatasetTags struct { 20 | Tags []DatasetTag `json:"tags" yaml:"tags"` 21 | } 22 | 23 | // updateDatasetsDescrciption creates new dataset metadata json payload 24 | func updateDatasetDescription(description string) (jsonPayload []byte, err error) { 25 | payload := UpdateDatasetDescription{ 26 | Description: description, 27 | } 28 | 29 | jsonPayload, err = json.Marshal(payload) 30 | 31 | return 32 | } 33 | 34 | // UpdateDatasetDescription validates that the supplied parameters are not empty 35 | // note: we intenionally allow here description to be empty as that is needed in order to remove it 36 | func (c *Client) UpdateDatasetDescription(connection, name, description string) (err error) { 37 | if len(strings.TrimSpace(connection)) == 0 { 38 | return errors.New("Required argument --connection not given or blank") 39 | } 40 | 41 | if len(strings.TrimSpace(name)) == 0 { 42 | return errors.New("Required argument --name not given or blank") 43 | } 44 | 45 | jsonPayload, err := updateDatasetDescription(description) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | path := fmt.Sprintf("api/%s/%s/%s/description", pkg.DatasetsAPIPath, connection, name) 51 | 52 | resp, err := c.Do(http.MethodPut, path, contentTypeJSON, jsonPayload) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | defer resp.Body.Close() 58 | return nil 59 | } 60 | 61 | // UpdateDatasetTags sets the dataset tags from the supplied list 62 | func (c *Client) UpdateDatasetTags(connection, name string, tags []string) (err error) { 63 | if len(strings.TrimSpace(connection)) == 0 { 64 | return errors.New("Required argument --connection not given or blank") 65 | } 66 | 67 | if len(strings.TrimSpace(name)) == 0 { 68 | return errors.New("Required argument --name not given or blank") 69 | } 70 | 71 | datasetTags := []DatasetTag{} 72 | for _, tag := range tags { 73 | if len(tag) == 0 || len(tag) > 255 { 74 | return errors.New("tags contain blank characters, or contain strings longer than 256 characters") 75 | } 76 | datasetTags = append(datasetTags, DatasetTag{Name: tag}) 77 | } 78 | payload := UpdateDatasetTags{ 79 | Tags: datasetTags, 80 | } 81 | 82 | jsonPayload, err := json.Marshal(payload) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | path := fmt.Sprintf("api/%s/%s/%s/tags", pkg.DatasetsAPIPath, connection, name) 88 | 89 | resp, err := c.Do(http.MethodPut, path, contentTypeJSON, jsonPayload) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | defer resp.Body.Close() 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/api/datasets_dto_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | // JSON as obtained from the Lenses v5 API. 12 | const listDatasetsJSON = `{ 13 | "datasets": { 14 | "values": [ 15 | { 16 | "name": "shipsaggs", 17 | "highlights": [ 18 | { 19 | "fieldName": "name", 20 | "startIndex": 0, 21 | "endIndex": 4, 22 | "arrayIndex": 0 23 | } 24 | ], 25 | "records": 347666906, 26 | "recordsPerSecond": 0, 27 | "keyType": "TWLONG", 28 | "valueType": "AVRO", 29 | "connectionName": "kafka", 30 | "replication": 1, 31 | "consumers": 0, 32 | "partitions": 10, 33 | "fields": { 34 | "key": [], 35 | "value": [] 36 | }, 37 | "isSystemEntity": false, 38 | "isMarkedForDeletion": false, 39 | "isCompacted": true, 40 | "sizeBytes": 7667899465, 41 | "policies": [], 42 | "permissions": [ 43 | "ShowTopic", 44 | "CreateTopic", 45 | "RequestTopicCreation", 46 | "DropTopic", 47 | "ConfigureTopic", 48 | "QueryTopic", 49 | "InsertData", 50 | "DeleteData", 51 | "UpdateSchema", 52 | "ViewSchema", 53 | "UpdateMetadata" 54 | ], 55 | "description": "test description", 56 | "tags": [ 57 | { 58 | "name": "iot" 59 | }, 60 | { 61 | "name": "Testing" 62 | } 63 | ], 64 | "retentionMs": 604800000, 65 | "retentionBytes": -1, 66 | "sourceType": "Kafka" 67 | }, 68 | { 69 | "name": "fastships_index", 70 | "highlights": [ 71 | { 72 | "fieldName": "name", 73 | "startIndex": 4, 74 | "endIndex": 8, 75 | "arrayIndex": 0 76 | } 77 | ], 78 | "sizeBytes": 12078914278, 79 | "records": 90099514, 80 | "connectionName": "ESOne", 81 | "replicas": 0, 82 | "shard": 5, 83 | "fields": { 84 | "key": [], 85 | "value": [] 86 | }, 87 | "isSystemEntity": false, 88 | "policies": [ 89 | { 90 | "policyId": "542c7269-c184-4c39-83a0-912428936957", 91 | "policyName": "mask MMSI", 92 | "policyCategory": "PII", 93 | "obfuscation": "First-3", 94 | "matchingKeyFields": [], 95 | "matchingValueFields": [ 96 | { 97 | "name": "MMSI", 98 | "parents": [] 99 | } 100 | ] 101 | } 102 | ], 103 | "permissions": [ 104 | "ShowIndex", 105 | "QueryIndex", 106 | "ViewSchema", 107 | "UpdateMetadata" 108 | ], 109 | "description": null, 110 | "tags": [], 111 | "sourceType": "Elastic" 112 | } 113 | ], 114 | "pagesAmount": 1, 115 | "totalCount": 2 116 | }, 117 | "sourceTypes": [ 118 | "Kafka", 119 | "Elastic" 120 | ] 121 | } 122 | ` 123 | 124 | // TestListDatasetsUnmarshalling especially focuses on the custom unmarshaller 125 | // in PageDatasetMatch. 126 | func TestListDatasetsUnmarshalling(t *testing.T) { 127 | var r Results 128 | err := json.Unmarshal([]byte(listDatasetsJSON), &r) 129 | require.NoError(t, err) 130 | 131 | // Type correctness. 132 | require.Len(t, r.Datasets.Values, 2) 133 | k, ok := r.Datasets.Values[0].(Kafka) 134 | require.True(t, ok) 135 | e, ok := r.Datasets.Values[1].(Elastic) 136 | require.True(t, ok) 137 | 138 | // Per-type unique properties. 139 | assert.Equal(t, 5, e.Shard) 140 | assert.Equal(t, "TWLONG", k.KeyType) 141 | } 142 | 143 | type polyX struct { 144 | Type string 145 | Common *int `json:",omitempty"` 146 | X int 147 | } 148 | 149 | type polyY struct { 150 | Type string 151 | Common *int `json:",omitempty"` 152 | Y int 153 | } 154 | 155 | func TestPolyTypeObjUnmarshaller(t *testing.T) { 156 | expect := []any{ 157 | polyX{Type: "x", Common: genPtr(5), X: 1}, 158 | polyX{Type: "x", Common: genPtr(2), X: 2}, 159 | polyY{Type: "y", Common: genPtr(42), Y: 1337}, 160 | polyY{Type: "y", Common: nil, Y: 31337}, 161 | } 162 | var raws []json.RawMessage 163 | for _, o := range expect { 164 | bs, err := json.Marshal(o) 165 | require.NoError(t, err) 166 | raws = append(raws, bs) 167 | } 168 | 169 | p := polyTypeObjUnmarshaller[any, string]{ 170 | discriminatorKey: "Type", 171 | type2ptr: func(s string) any { 172 | return map[string]any{ 173 | "x": &polyX{}, 174 | "y": &polyY{}, 175 | }[s] 176 | }, 177 | } 178 | got, err := p.unmarshalSlice(raws) 179 | require.NoError(t, err) 180 | 181 | assert.Equal(t, expect, got) 182 | } 183 | 184 | func genPtr[T any](v T) *T { 185 | return &v 186 | } 187 | -------------------------------------------------------------------------------- /pkg/api/elasticsearch.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strconv" 8 | 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | ) 11 | 12 | // Shard type for elasticsearch shards 13 | type Shard struct { 14 | Shard string `json:"shard"` 15 | Records int `json:"records"` 16 | Replicas int `json:"replicas"` 17 | AvailableReplicas int `json:"availableReplicas"` 18 | } 19 | 20 | // Index is Elasticsearch index type 21 | type Index struct { 22 | IndexName string `json:"indexName" header:"Name"` 23 | ConnectionName string `json:"connectionName" header:"Connection"` 24 | KeyType string `json:"keyType"` 25 | ValueType string `json:"valueType"` 26 | KeySchema string `json:"keySchema,omitempty"` 27 | ValueSchema string `json:"valueSchema,omitempty"` 28 | Size int `json:"size" header:"Size"` 29 | TotalRecords int `json:"totalMessages" header:"Records"` 30 | Description string `json:"description" yaml:"description"` 31 | Tags []DatasetTag `json:"tags" yaml:"tags"` 32 | Status string `json:"status" header:"Status"` 33 | Shards []Shard `json:"shards"` 34 | ShardsCount int `json:"shardsCount" header:"Shards"` 35 | Replicas int `json:"replicas" header:"Replicas"` 36 | Permission []string `json:"permissions"` 37 | } 38 | 39 | // GetIndexes returns the list of elasticsearch indexes. 40 | func (c *Client) GetIndexes(connectionName string, includeSystemIndexes bool) (indexes []Index, err error) { 41 | // # List of indexes 42 | // GET /api/elastic/indexes?connectionName=$x&includeSystemIndexes=$y 43 | url, err := url.Parse(pkg.ElasticsearchIndexesPath) 44 | q := url.Query() 45 | 46 | q.Add("includeSystemIndexes", strconv.FormatBool(includeSystemIndexes)) 47 | 48 | if connectionName != "" { 49 | q.Add("connectionName", connectionName) 50 | } 51 | url.RawQuery = q.Encode() 52 | 53 | resp, respErr := c.Do(http.MethodGet, url.String(), "", nil) 54 | if respErr != nil { 55 | err = respErr 56 | return 57 | } 58 | 59 | err = c.ReadJSON(resp, &indexes) 60 | 61 | return 62 | } 63 | 64 | // GetIndex fetches stuff about an index 65 | func (c *Client) GetIndex(connectionName string, indexName string) (index Index, err error) { 66 | // List of indexes 67 | // GET /api/elastic/indexes/connectionName/indexName 68 | path := fmt.Sprintf("%s/%s/%s", pkg.ElasticsearchIndexesPath, connectionName, indexName) 69 | 70 | resp, respErr := c.Do(http.MethodGet, path, "", nil) 71 | if respErr != nil { 72 | err = respErr 73 | return 74 | } 75 | 76 | err = c.ReadJSON(resp, &index) 77 | 78 | return 79 | } 80 | 81 | // GetAvailableReplicas returns the sum of all shards' available replicas 82 | func GetAvailableReplicas(esIndex Index) int { 83 | availableReplicas := 0 84 | 85 | for _, shard := range esIndex.Shards { 86 | availableReplicas = availableReplicas + shard.AvailableReplicas 87 | } 88 | 89 | return availableReplicas 90 | } 91 | -------------------------------------------------------------------------------- /pkg/api/elasticsearch_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetAvailableReplicas(t *testing.T) { 10 | shard := Shard{Shard: "1", Records: 2, Replicas: 1, AvailableReplicas: 1} 11 | 12 | index := Index{ 13 | Shards: []Shard{shard, shard}, 14 | } 15 | 16 | assert.Equal(t, 2, GetAvailableReplicas(index)) 17 | } 18 | 19 | func TestGetAvailableReplicasEmpty(t *testing.T) { 20 | index := Index{ 21 | Shards: []Shard{}, 22 | } 23 | 24 | assert.Equal(t, 0, GetAvailableReplicas(index)) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/files.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "mime/multipart" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/lensesio/lenses-go/v5/pkg" 14 | ) 15 | 16 | type FileMetadataEntity struct { 17 | Filename string `json:"filename"` // Required. 18 | ID uuid.UUID `json:"id"` // Required. 19 | Size int `json:"size"` // Required. 20 | UploadedAt time.Time `json:"uploadedAt"` // Required. 21 | UploadedBy string `json:"uploadedBy"` // Required. 22 | ContentType *string `json:"contentType,omitempty"` // Optional. 23 | } 24 | 25 | func (c *Client) UploadFile(fileName string) (uuid.UUID, error) { 26 | f, err := os.Open(fileName) 27 | if err != nil { 28 | return uuid.Nil, err 29 | } 30 | defer f.Close() 31 | return c.UploadFileFromReader(fileName, f) 32 | } 33 | 34 | func (c *Client) UploadFileFromReader(fileName string, r io.Reader) (uuid.UUID, error) { 35 | body := &bytes.Buffer{} 36 | writer := multipart.NewWriter(body) 37 | part, err := writer.CreateFormFile("file", fileName) 38 | if err != nil { 39 | return uuid.Nil, err 40 | } 41 | if _, err := io.Copy(part, r); err != nil { 42 | return uuid.Nil, err 43 | } 44 | if err := writer.Close(); err != nil { 45 | return uuid.Nil, err 46 | } 47 | fileUploadBody := body.Bytes() 48 | 49 | resp, err := c.Do(http.MethodPost, pkg.FileUploadPath, writer.FormDataContentType(), fileUploadBody) 50 | if err != nil { 51 | return uuid.Nil, err 52 | } 53 | message, err := c.ReadResponseBody(resp) 54 | if err != nil { 55 | return uuid.Nil, err 56 | } 57 | 58 | var fileResp FileMetadataEntity 59 | if err := json.Unmarshal(message, &fileResp); err != nil { 60 | return uuid.Nil, err 61 | } 62 | return fileResp.ID, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/api/group_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | const groupPath = "api/v1/group" 10 | 11 | // Namespace the payload object for namespaces 12 | type Namespace struct { 13 | Wildcards []string `json:"wildcards" yaml:"wildcards" header:"Wildcards"` 14 | Permissions []string `json:"permissions" yaml:"permissions" header:"Permissions"` 15 | Connection string `json:"connection" yaml:"connection" header:"connection"` 16 | } 17 | 18 | // Group the payload object 19 | type Group struct { 20 | Name string `json:"name" yaml:"name" header:"Name"` 21 | Description string `json:"description,omitempty" yaml:"description" header:"Description"` 22 | Namespaces []Namespace `json:"namespaces" yaml:"dataNamespaces" header:"Namespaces,count"` 23 | ScopedPermissions []string `json:"scopedPermissions" yaml:"applicationPermissions" header:"Application Permissions,count"` 24 | AdminPermissions []string `json:"adminPermissions" yaml:"adminPermissions" header:"Admin Permissions,count"` 25 | UserAccountsCount int `json:"userAccounts" yaml:"userAccounts" header:"User Accounts"` 26 | ServiceAccountsCount int `json:"serviceAccounts" yaml:"serviceAccounts" header:"Service Accounts"` 27 | ConnectClustersPermissions []string `json:"connectClustersPermissions" yaml:"connectClustersPermissions" header:"Connect clusters access"` 28 | } 29 | 30 | // GetGroups returns the list of groups 31 | func (c *Client) GetGroups() (groups []Group, err error) { 32 | resp, err := c.Do(http.MethodGet, groupPath, contentTypeJSON, nil) 33 | if err != nil { 34 | return 35 | } 36 | err = c.ReadJSON(resp, &groups) 37 | return 38 | } 39 | 40 | // GetGroup returns the group by the provided name 41 | func (c *Client) GetGroup(name string) (group Group, err error) { 42 | if name == "" { 43 | err = errRequired("name") 44 | return 45 | } 46 | 47 | path := fmt.Sprintf("%s/%s", groupPath, name) 48 | resp, err := c.Do(http.MethodGet, path, contentTypeJSON, nil) 49 | if err != nil { 50 | return 51 | } 52 | 53 | err = c.ReadJSON(resp, &group) 54 | return 55 | } 56 | 57 | // CreateGroup creates a group 58 | func (c *Client) CreateGroup(group *Group) error { 59 | if group.Name == "" { 60 | return errRequired("name") 61 | } 62 | if group.Namespaces == nil { 63 | group.Namespaces = make([]Namespace, 0) 64 | } 65 | 66 | payload, err := json.Marshal(group) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | _, err = c.Do(http.MethodPost, groupPath, contentTypeJSON, payload) 72 | if err != nil { 73 | return err 74 | } 75 | return err 76 | } 77 | 78 | // DeleteGroup deletes a group 79 | func (c *Client) DeleteGroup(name string) error { 80 | if name == "" { 81 | return errRequired("name") 82 | } 83 | 84 | path := fmt.Sprintf("%s/%s", groupPath, name) 85 | _, err := c.Do(http.MethodDelete, path, contentTypeJSON, nil) 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | 92 | // UpdateGroup updates a group 93 | func (c *Client) UpdateGroup(group *Group) error { 94 | if group.Name == "" { 95 | return errRequired("name") 96 | } 97 | 98 | payload, err := json.Marshal(group) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | path := fmt.Sprintf("%s/%s", groupPath, group.Name) 104 | _, err = c.Do(http.MethodPut, path, contentTypeJSON, payload) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | // CloneGroup clones a group 112 | func (c *Client) CloneGroup(currentName string, newName string) error { 113 | if currentName == "" { 114 | return errRequired("name") 115 | } 116 | if newName == "" { 117 | return errRequired("newName") 118 | } 119 | 120 | path := fmt.Sprintf("%s/%s/clone/%s", groupPath, currentName, newName) 121 | _, err := c.Do(http.MethodPost, path, contentTypeJSON, nil) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | -------------------------------------------------------------------------------- /pkg/api/license.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | ) 10 | 11 | // LicenseInfo describes the data received from the `GetLicenseInfo`. 12 | type LicenseInfo struct { 13 | ClientID string `json:"clientId" header:"ID,text"` 14 | IsRespected bool `json:"isRespected" header:"Respected"` 15 | MaxBrokers int `json:"maxBrokers" header:"Max Brokers"` 16 | MaxMessages int `json:"maxMessages,omitempty" header:"/ Messages"` 17 | Expiry int64 `json:"expiry" header:"Expires,timestamp(ms|02 Jan 2006 15:04)"` 18 | 19 | // no-payload data. 20 | 21 | // ExpiresAt is the time.Time expiration datetime (unix). 22 | ExpiresAt time.Time `json:"-"` 23 | 24 | // ExpiresDur is the duration that expires from now. 25 | ExpiresDur time.Duration `json:"-"` 26 | 27 | // YearsToExpire is the length of years that expires from now. 28 | YearsToExpire int `json:"yearsToExpire,omitempty"` 29 | // MonthsToExpire is the length of months that expires from now. 30 | MonthsToExpire int `json:"monthsToExpire,omitempty"` 31 | // DaysToExpire is the length of days that expires from now. 32 | DaysToExpire int `json:"daysToExpire,omitempty"` 33 | } 34 | 35 | // License is the JSON payload for updating a license. 36 | type License struct { 37 | Source string `json:"source"` 38 | ClientID string `json:"clientId"` 39 | Details string `json:"details"` 40 | Key string `json:"key"` 41 | } 42 | 43 | // GetLicenseInfo returns the license information for the connected lenses box. 44 | func (c *Client) GetLicenseInfo() (LicenseInfo, error) { 45 | var lc LicenseInfo 46 | 47 | resp, err := c.Do(http.MethodGet, pkg.LicensePath, "", nil) 48 | if err != nil { 49 | return lc, err 50 | } 51 | 52 | if err = c.ReadJSON(resp, &lc); err != nil { 53 | return lc, err 54 | } 55 | 56 | lc.ExpiresAt = time.Unix(lc.Expiry/1000, 0) 57 | lc.ExpiresDur = lc.ExpiresAt.Sub(time.Now()) 58 | lc.DaysToExpire = int(lc.ExpiresDur.Hours() / 24) 59 | lc.MonthsToExpire = int(lc.DaysToExpire / 30) 60 | lc.YearsToExpire = int(lc.MonthsToExpire / 12) 61 | 62 | if lc.YearsToExpire > 0 { 63 | lc.DaysToExpire = 0 64 | lc.MonthsToExpire = 0 65 | } else if lc.MonthsToExpire > 0 { 66 | lc.DaysToExpire = 0 67 | } 68 | 69 | return lc, nil 70 | } 71 | 72 | // UpdateLicense handles the `PUT` API call to update a license at runtime 73 | func (c *Client) UpdateLicense(license License) error { 74 | 75 | payload, err := json.Marshal(license) 76 | resp, err := c.Do(http.MethodPut, pkg.LicensePath, contentTypeJSON, payload) 77 | if err != nil { 78 | return err 79 | 80 | } 81 | return resp.Body.Close() 82 | } 83 | -------------------------------------------------------------------------------- /pkg/api/provisioning_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | ) 9 | 10 | func (c *Client) GetConnectionsState() (yaml string, err error) { 11 | resp, err := c.Do(http.MethodGet, pkg.ProvisionedConnectionsPath, contentTypeYaml, nil) 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | defer resp.Body.Close() 17 | 18 | bodyBytes, err := c.ReadResponseBody(resp) 19 | if err != nil { 20 | return "", fmt.Errorf("failed to read response body: %w", err) 21 | } 22 | 23 | yaml = string(bodyBytes) 24 | 25 | return yaml, nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/api/service_account_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | ) 8 | 9 | const serviceAccountPath = "api/v1/serviceaccount" 10 | 11 | // ServiceAccount the service account data transfer object 12 | type ServiceAccount struct { 13 | Name string `json:"name" yaml:"name" header:"Name"` 14 | Owner string `json:"owner,omitempty" yaml:"owner,omitempty" header:"Owner"` 15 | Groups []string `json:"groups" yaml:"groups" header:"Groups"` 16 | Token string `json:"token,omitempty" yaml:"token,omitempty"` 17 | } 18 | 19 | // CreateSvcAccPayload the data transfer object when we create a new service account 20 | type CreateSvcAccPayload struct { 21 | Token string `json:"token,omitempty"` 22 | } 23 | 24 | // GetServiceAccounts returns the list of service accounts 25 | func (c *Client) GetServiceAccounts() (serviceAccounts []ServiceAccount, err error) { 26 | resp, err := c.Do(http.MethodGet, serviceAccountPath, contentTypeJSON, nil) 27 | if err != nil { 28 | return 29 | } 30 | err = c.ReadJSON(resp, &serviceAccounts) 31 | return 32 | } 33 | 34 | // GetServiceAccount returns the service account by the provided name 35 | func (c *Client) GetServiceAccount(name string) (serviceAccount ServiceAccount, err error) { 36 | if name == "" { 37 | err = errRequired("name") 38 | return 39 | } 40 | 41 | path := fmt.Sprintf("%s/%s", serviceAccountPath, name) 42 | resp, err := c.Do(http.MethodGet, path, contentTypeJSON, nil) 43 | if err != nil { 44 | return 45 | } 46 | 47 | err = c.ReadJSON(resp, &serviceAccount) 48 | return 49 | } 50 | 51 | // CreateServiceAccount creates a service account 52 | func (c *Client) CreateServiceAccount(serviceAccount *ServiceAccount) (token CreateSvcAccPayload, err error) { 53 | if serviceAccount.Name == "" { 54 | err = errRequired("name") 55 | return 56 | } 57 | if len(serviceAccount.Groups) == 0 { 58 | err = errRequired("groups") 59 | return 60 | } 61 | 62 | payload, err := json.Marshal(serviceAccount) 63 | if err != nil { 64 | return 65 | } 66 | 67 | resp, err := c.Do(http.MethodPost, serviceAccountPath, contentTypeJSON, payload) 68 | if err != nil { 69 | return 70 | } 71 | err = c.ReadJSON(resp, &token) 72 | return 73 | } 74 | 75 | // DeleteServiceAccount deletes a service account 76 | func (c *Client) DeleteServiceAccount(name string) error { 77 | if name == "" { 78 | return errRequired("name") 79 | } 80 | 81 | path := fmt.Sprintf("%s/%s", serviceAccountPath, name) 82 | _, err := c.Do(http.MethodDelete, path, contentTypeJSON, nil) 83 | if err != nil { 84 | return err 85 | } 86 | return nil 87 | } 88 | 89 | // UpdateServiceAccount updates a service account 90 | func (c *Client) UpdateServiceAccount(serviceAccount *ServiceAccount) error { 91 | if serviceAccount.Name == "" { 92 | return errRequired("name") 93 | } 94 | if len(serviceAccount.Groups) == 0 { 95 | return errRequired("groups") 96 | } 97 | 98 | payload, err := json.Marshal(serviceAccount) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | path := fmt.Sprintf("%s/%s", serviceAccountPath, serviceAccount.Name) 104 | _, err = c.Do(http.MethodPut, path, contentTypeJSON, payload) 105 | if err != nil { 106 | return err 107 | } 108 | return nil 109 | } 110 | 111 | // RevokeServiceAccountToken returns the service account token for the provided name 112 | func (c *Client) RevokeServiceAccountToken(name string, newToken string) (token CreateSvcAccPayload, err error) { 113 | if name == "" { 114 | err = errRequired("name") 115 | return 116 | } 117 | payload, err := json.Marshal(CreateSvcAccPayload{Token: newToken}) 118 | if err != nil { 119 | return 120 | } 121 | 122 | path := fmt.Sprintf("%s/%s/revoke", serviceAccountPath, name) 123 | resp, err := c.Do(http.MethodPut, path, contentTypeJSON, payload) 124 | if err != nil { 125 | return 126 | } 127 | err = c.ReadJSON(resp, &token) 128 | return 129 | } 130 | -------------------------------------------------------------------------------- /pkg/api/topicsettings_client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // MinMax contains Min and Max keys 12 | type MinMax struct { 13 | Min int `json:"min" yaml:"min"` 14 | Max int `json:"max,omitempty" yaml:"max,omitempty"` 15 | } 16 | 17 | // DefaultMax contains Default and Max keys 18 | type DefaultMax struct { 19 | Default int64 `json:"default,omitempty" yaml:"default,omitempty"` 20 | Max int64 `json:"max" yaml:"max"` 21 | } 22 | 23 | // Retention contains Size and Time keys 24 | type Retention struct { 25 | Size DefaultMax `json:"size" yaml:"size"` 26 | Time DefaultMax `json:"time" yaml:"time"` 27 | } 28 | 29 | // TopicConfiguration contains Partitions, Replication and Retention keys 30 | type TopicConfiguration struct { 31 | Partitions MinMax `json:"partitions" yaml:"partitions"` 32 | Replication MinMax `json:"replication" yaml:"replication"` 33 | Retention Retention `json:"retention" yaml:"retention"` 34 | } 35 | 36 | // Naming contains Description and Pattern 37 | type Naming struct { 38 | Description string `json:"description" yaml:"description"` 39 | Pattern string `json:"pattern" yaml:"pattern"` 40 | } 41 | 42 | // TopicSettingsResponse contains Config, Naming and IsApplicable keys 43 | type TopicSettingsResponse struct { 44 | Config TopicConfiguration `json:"config" yaml:"config"` 45 | Naming *Naming `json:"naming,omitempty" yaml:"naming,omitempty"` 46 | IsApplicable bool `json:"isApplicable,omitempty" yaml:"isApplicable,omitempty"` 47 | } 48 | 49 | // TopicSettingsRequest contains Config and Naming keys 50 | type TopicSettingsRequest struct { 51 | Config TopicConfiguration `json:"config" yaml:"config"` 52 | Naming *Naming `json:"naming" yaml:"naming"` 53 | } 54 | 55 | const path = "api/v1/kafka/topic/policy" 56 | 57 | // GetTopicSettings from the API 58 | func (c *Client) GetTopicSettings() (settings TopicSettingsResponse, err error) { 59 | resp, err := c.Do(http.MethodGet, path, contentTypeJSON, nil) 60 | if err != nil { 61 | return 62 | } 63 | 64 | err = c.ReadJSON(resp, &settings) 65 | return 66 | } 67 | 68 | // UpdateTopicSettings from the API 69 | func (c *Client) UpdateTopicSettings(settings TopicSettingsRequest) error { 70 | if settings.Config.Partitions.Min < 1 { 71 | return fmt.Errorf("Partitions cannot have negative value") 72 | } 73 | 74 | if settings.Config.Replication.Min < 1 { 75 | return fmt.Errorf("Replication cannot have negative value") 76 | } 77 | 78 | if settings.Config.Retention.Size.Default < -1 { 79 | return fmt.Errorf("Retention size cannot have value lower than -1") 80 | } 81 | 82 | if settings.Config.Retention.Size.Max < -1 { 83 | return fmt.Errorf("Retention size cannot have value lower than -1") 84 | } 85 | 86 | if settings.Config.Retention.Time.Default < -1 { 87 | return fmt.Errorf("Retention time cannot have value lower than -1") 88 | } 89 | 90 | if settings.Config.Retention.Time.Max < -1 { 91 | return fmt.Errorf("Retention time cannot have value lower than -1") 92 | } 93 | 94 | payload, err := json.Marshal(settings) 95 | 96 | if err != nil { 97 | return errors.Wrap(err, "Failed to read settings from input") 98 | } 99 | 100 | _, err = c.Do(http.MethodPut, path, contentTypeJSON, payload) 101 | return err 102 | } 103 | -------------------------------------------------------------------------------- /pkg/api/wizard_gen.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "net/http" 4 | 5 | type SetupStatus struct { 6 | IsCompleted bool `json:"isCompleted"` // Required. 7 | IsLicensed bool `json:"isLicensed"` // Required. 8 | } 9 | 10 | // Returns the setup stage status. 11 | // Tags: Internal. 12 | func (c *Client) GetSetupStatus() (resp SetupStatus, err error) { 13 | err = c.do( 14 | http.MethodGet, 15 | "/api/v1/setup", 16 | nil, // request 17 | &resp, // response 18 | ) 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /pkg/ascode/resource_commands.go: -------------------------------------------------------------------------------- 1 | package ascode 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/kataras/golog" 9 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 10 | "github.com/spf13/cobra" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // ResourceImporter defines the interface for importing resources 15 | type resourceImporter interface { 16 | ImportResource(yaml string) error 17 | } 18 | 19 | // ApplyResourceCmd creates a Cobra command for applying a resource from a file 20 | func ApplyResourceCmd() *cobra.Command { 21 | cmd := &cobra.Command{ 22 | Use: "apply", 23 | Short: "Imports a resource from a file or a folder", 24 | Example: `apply resource.yaml`, 25 | SilenceErrors: true, 26 | TraverseChildren: true, 27 | Args: cobra.ExactArgs(1), 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | fileOrFolderPath := args[0] 30 | if _, err := os.Stat(fileOrFolderPath); os.IsNotExist(err) { 31 | return fmt.Errorf("file or folder %s does not exist", fileOrFolderPath) 32 | } 33 | 34 | importer := config.Client 35 | 36 | if err := applyResource(importer, fileOrFolderPath); err != nil { 37 | return fmt.Errorf("failed to apply resource: %w", err) 38 | } 39 | 40 | return nil 41 | }, 42 | } 43 | 44 | return cmd 45 | } 46 | 47 | func applyResource(importer resourceImporter, path string) error { 48 | fileInfo, err := os.Stat(path) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if fileInfo.IsDir() { 54 | return applyResourcesFromFolder(importer, path) 55 | } 56 | 57 | return applyResourceFile(importer, path) 58 | } 59 | 60 | func applyResourceFile(importer resourceImporter, filePath string) error { 61 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 62 | return fmt.Errorf("file %s does not exist", filePath) 63 | } 64 | 65 | content, err := os.ReadFile(filePath) 66 | if err != nil { 67 | return fmt.Errorf("failed to read file: %s", err) 68 | } 69 | 70 | fileContent := string(content) 71 | var resource Resource 72 | if err := yaml.Unmarshal(content, &resource); err != nil { 73 | return fmt.Errorf("invalid YAML file content: %s", err) 74 | } 75 | 76 | apiVersion := resource.APIVersion 77 | kind := resource.Kind 78 | 79 | if got, expect := apiVersion, "lenses.io/v0beta"; got != expect { 80 | return fmt.Errorf("unsupported API version: %q; only supporting %q", got, expect) 81 | } 82 | 83 | switch kind { 84 | case "KafkaConnector": 85 | if err := importer.ImportResource(fileContent); err != nil { 86 | return fmt.Errorf("failed to import KafkaConnector resource: %s", err) 87 | } 88 | default: 89 | return fmt.Errorf("unsupported kind: %s. Supported kind: KafkaConnector", kind) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func applyResourcesFromFolder(importer resourceImporter, folderPath string) error { 96 | err := filepath.Walk(folderPath, func(file string, info os.FileInfo, err error) error { 97 | if err != nil { 98 | return err 99 | } 100 | if file == folderPath { 101 | return nil 102 | } 103 | if !info.IsDir() { 104 | golog.Infof("Processing file:%s", file) 105 | 106 | if err := applyResourceFile(importer, file); err != nil { 107 | return fmt.Errorf("failed to apply resource from file %s: %w", file, err) 108 | } 109 | } 110 | return nil 111 | }) 112 | 113 | if err != nil { 114 | return fmt.Errorf("error walking through folder %s: %s", folderPath, err) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // Resource struct definition 121 | type Resource struct { 122 | APIVersion string `yaml:"apiVersion"` 123 | Kind string `yaml:"kind"` 124 | // Add other fields as needed 125 | } 126 | -------------------------------------------------------------------------------- /pkg/configs/commands.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | const commandModeName = "mode" 11 | 12 | // NewGetConfigsCommand creates the `configs` command 13 | func NewGetConfigsCommand() *cobra.Command { 14 | cmd := &cobra.Command{ 15 | Use: "configs", 16 | Aliases: []string{"config"}, 17 | Short: "Print the whole lenses box configs", 18 | Example: "configs", 19 | TraverseChildren: true, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | if len(args) > 0 { 22 | configEntryName := args[0] 23 | if configEntryName == commandModeName { 24 | // means that something like `config mode` called, 25 | // let's support it here as well, although 26 | // mode has its own command `mode` because it's super important 27 | // and users should call that instead. 28 | return NewGetModeCommand().Execute() 29 | } 30 | 31 | var value interface{} 32 | err := Client.GetConfigEntry(&value, configEntryName) 33 | if err != nil { 34 | return fmt.Errorf("retrieve config value [%s] failed: [%v]", configEntryName, err) 35 | } 36 | 37 | return bite.PrintJSON(cmd, value) // keep json. 38 | } 39 | 40 | config, err := Client.GetConfig() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return bite.PrintObject(cmd, config) 46 | }, 47 | } 48 | 49 | bite.CanPrintJSON(cmd) 50 | 51 | return cmd 52 | } 53 | 54 | // NewGetModeCommand creates the `mode` command 55 | func NewGetModeCommand() *cobra.Command { 56 | return &cobra.Command{ 57 | Use: commandModeName, 58 | Short: "Print the configuration's execution mode", 59 | Example: commandModeName, 60 | DisableFlagParsing: true, 61 | DisableFlagsInUseLine: true, 62 | DisableSuggestions: true, 63 | TraverseChildren: false, 64 | RunE: func(cmd *cobra.Command, args []string) error { 65 | mode, err := Client.GetExecutionMode() 66 | if err != nil { 67 | return err 68 | } 69 | _, err = fmt.Fprintln(cmd.OutOrStdout(), string(mode)) 70 | return err 71 | }, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/connection/aws.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewAWSGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "aws", 11 | Short: "Manage Lenses AWS connections", 12 | SilenceErrors: true, 13 | TraverseChildren: true, 14 | } 15 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectAWS]("AWS", gen, up, FlagMapperOpts{ 16 | Descriptions: map[string]string{ 17 | "AccessKeyId": "Access key ID of an AWS IAM account.", 18 | "SecretAccessKey": "Secret access key of an AWS IAM account.", 19 | "Region": "AWS region to connect to. If not provided, this is deferred to client configuration.", 20 | "SessionToken": "Specifies the session token value that is required if you are using temporary security credentials that you retrieved directly from AWS STS operations.", 21 | }, 22 | })...) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /pkg/connection/commands.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | // NewConnectionGroupCommand creates `connection` command 9 | func NewConnectionGroupCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "connections", 12 | Short: `Manage Lenses external connections`, 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | 17 | cmd.AddCommand(NewGenericConnectionGroupCommand()) 18 | 19 | // Maintain the generic connection manipulation commands where they used to 20 | // be for compatibility, but hidden and marked as deprecated. 21 | cmd.AddCommand(hideAndDeprecate(`this command has been moved under the "generic" connections command`, 22 | NewGenericConnectionGetCommand(), 23 | NewGenericConnectionCreateCommand(), 24 | NewGenericConnectionDeleteCommand(), 25 | NewGenericConnectionUpdateCommand(), 26 | NewGenericConnectionListCommand(), 27 | )...) 28 | 29 | cmd.AddCommand( 30 | // Those commands use the specific endpoints. 31 | NewKafkaGroupCommand(config.Client, upload), 32 | NewKafkaConnectGroupCommand(upload), 33 | NewKerberosGroupCommand(upload), 34 | NewSchemaRegistryGroupCommand(upload), 35 | NewZookeeperGroupCommand(upload), 36 | // Those commands use the generic endpoints. 37 | NewAWSGroupCommand(config.Client, upload), 38 | NewDataDogGroupCommand(config.Client, upload), 39 | NewElasticsearchGroupCommand(config.Client, upload), 40 | NewGlueGroupCommand(config.Client, upload), 41 | NewPagerDutyGroupCommand(config.Client, upload), 42 | NewPostgreSQLGroupCommand(config.Client, upload), 43 | NewPrometheusAlertmanagerGroupCommand(config.Client, upload), 44 | NewSlackGroupCommand(config.Client, upload), 45 | NewSplunkGroupCommand(config.Client, upload), 46 | NewWebhookGroupCommand(config.Client, upload), 47 | ) 48 | 49 | return cmd 50 | } 51 | 52 | func hideAndDeprecate(deprecate string, cs ...*cobra.Command) []*cobra.Command { 53 | for _, c := range cs { 54 | c.Hidden = true 55 | c.Deprecated = deprecate 56 | } 57 | return cs 58 | } 59 | -------------------------------------------------------------------------------- /pkg/connection/datadog.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewDataDogGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "data-dog", 11 | Aliases: []string{"dd"}, 12 | Short: "Manage Lenses DataDog connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectDataDog]("DataDog", gen, up, FlagMapperOpts{ 17 | Descriptions: map[string]string{ 18 | "APIKey": "The Datadog API key.", 19 | "Site": "The Datadog site, e.g. EU or US.", 20 | "ApplicationKey": "The Datadog application key.", 21 | }, 22 | })...) 23 | return cmd 24 | } 25 | -------------------------------------------------------------------------------- /pkg/connection/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | The Lenses API has two groups of endpoints to manage connections: a "generic" 3 | one and several "specific" ones. The generic one accepts all possible 4 | connection objects. Such an object has a "templateName" which is the type of the 5 | object, e.g.: PostgreSQL, Elasticsearch, etc. There are also a few specific 6 | endpoints that accept one connection "flavour". Examples are Kafka, 7 | KafkaConnect, etc. The generic API is a superset of the specific ones: every 8 | specific connection can be administered via the generic API, only a handful 9 | specific connections exist. 10 | 11 | The generic endpoint is: /api/v1/connection/connections; 12 | An example of a specific endpoint is: /api/v1/connection/connection-templates/KafkaConnect. 13 | */ 14 | package connection 15 | -------------------------------------------------------------------------------- /pkg/connection/elastic.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewElasticsearchGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "elasticsearch", 11 | Aliases: []string{"elastic", "es"}, 12 | Short: "Manage Lenses Elasticsearch connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectElasticsearch]("Elasticsearch", gen, up, FlagMapperOpts{ 17 | Rename: map[string]string{ 18 | "User": "es-user", // clashes with persistent flag. 19 | "Password": "es-password", // for consistency. 20 | }, 21 | Descriptions: map[string]string{ 22 | "Nodes": "The nodes of the Elasticsearch cluster to connect to, e.g. https://hostname:port.", 23 | "Password": "The password to connect to the Elasticsearch service.", 24 | "User": "The username to connect to the Elasticsearch service.", 25 | }, 26 | })...) 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /pkg/connection/elastic_test.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lensesio/lenses-go/v5/pkg/api" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestElasticList(t *testing.T) { 12 | m := genConnMock{} 13 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 14 | c.SetArgs([]string{"list"}) 15 | err := c.Execute() 16 | require.NoError(t, err) 17 | assert.True(t, m.listCalled) 18 | } 19 | 20 | func TestElasticUpdate(t *testing.T) { 21 | m := genConnMock{} 22 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 23 | c.SetArgs([]string{"update", "the-name", "--nodes", "x", "--nodes", "y", "--es-user", "u", "--es-password", "p"}) 24 | err := c.Execute() 25 | require.NoError(t, err) 26 | assert.Equal(t, ptrTo("the-name"), m.upName) 27 | assert.Equal(t, &api.UpsertConnectionAPIRequest{ 28 | ConfigurationObject: api.ConfigurationObjectElasticsearch{ 29 | Nodes: []string{"x", "y"}, 30 | Password: ptrTo("p"), 31 | User: ptrTo("u"), 32 | }, 33 | Tags: []string{}, 34 | TemplateName: ptrTo("Elasticsearch"), 35 | }, m.upReq) 36 | } 37 | 38 | func TestElasticGet(t *testing.T) { 39 | m := genConnMock{} 40 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 41 | c.SetArgs([]string{"get", "the-name"}) 42 | err := c.Execute() 43 | require.NoError(t, err) 44 | assert.Equal(t, ptrTo("the-name"), m.getName) 45 | } 46 | 47 | func TestElasticGetNoDefault(t *testing.T) { 48 | m := genConnMock{} 49 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 50 | c.SetArgs([]string{"get"}) 51 | err := c.Execute() 52 | require.Error(t, err) 53 | } 54 | 55 | func TestElasticDelete(t *testing.T) { 56 | m := genConnMock{} 57 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 58 | c.SetArgs([]string{"delete", "the-name"}) 59 | err := c.Execute() 60 | require.NoError(t, err) 61 | assert.Equal(t, ptrTo("the-name"), m.delName) 62 | } 63 | 64 | func TestElasticTest(t *testing.T) { 65 | m := genConnMock{} 66 | c := NewElasticsearchGroupCommand(&m, noUpload(t)) 67 | c.SetArgs([]string{"test", "the-name", "--nodes", "x", "--nodes", "y", "--es-user", "u", "--es-password", "p"}) 68 | err := c.Execute() 69 | require.NoError(t, err) 70 | assert.Equal(t, &api.TestConnectionAPIRequest{ 71 | Name: "the-name", 72 | ConfigurationObject: api.ConfigurationObjectElasticsearch{ 73 | Nodes: []string{"x", "y"}, 74 | Password: ptrTo("p"), 75 | User: ptrTo("u"), 76 | }, 77 | TemplateName: "Elasticsearch", 78 | }, m.testReq) 79 | } 80 | 81 | type genConnMock struct { 82 | getName *string 83 | getResp api.ConnectionJsonResponse 84 | 85 | upName *string 86 | upReq *api.UpsertConnectionAPIRequest 87 | upReqV2 *api.UpsertConnectionAPIRequestV2 88 | upResp api.AddConnectionResponse 89 | 90 | delName *string 91 | 92 | testReq *api.TestConnectionAPIRequest 93 | testReqV2 *api.TestConnectionAPIRequestV2 94 | 95 | listCalled bool 96 | listResp []api.ConnectionSummaryResponse 97 | 98 | genErr error 99 | } 100 | 101 | func (g *genConnMock) GetConnection1(name string) (resp api.ConnectionJsonResponse, err error) { 102 | g.getName = &name 103 | return g.getResp, g.genErr 104 | } 105 | func (g *genConnMock) ListConnections() (resp []api.ConnectionSummaryResponse, err error) { 106 | g.listCalled = true 107 | return g.listResp, g.genErr 108 | } 109 | func (g *genConnMock) TestConnection(reqBody api.TestConnectionAPIRequest) (err error) { 110 | g.testReq = &reqBody 111 | return g.genErr 112 | } 113 | func (g *genConnMock) TestConnectionV2(reqBody api.TestConnectionAPIRequestV2) (err error) { 114 | g.testReqV2 = &reqBody 115 | return g.genErr 116 | } 117 | func (g *genConnMock) UpdateConnectionV1(name string, reqBody api.UpsertConnectionAPIRequest) (resp api.AddConnectionResponse, err error) { 118 | g.upName = &name 119 | g.upReq = &reqBody 120 | return g.upResp, g.genErr 121 | } 122 | func (g *genConnMock) UpdateConnectionV2(name string, reqBody api.UpsertConnectionAPIRequestV2) (resp api.AddConnectionResponse, err error) { 123 | g.upName = &name 124 | g.upReqV2 = &reqBody 125 | return g.upResp, g.genErr 126 | } 127 | func (g *genConnMock) DeleteConnection1(name string) (err error) { 128 | g.delName = &name 129 | return g.genErr 130 | } 131 | -------------------------------------------------------------------------------- /pkg/connection/flagmapper_test.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/uuid" 7 | cobra "github.com/spf13/cobra" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type nested struct { 13 | AnOptInt *int `json:",omitempty"` 14 | Bool bool 15 | OptBool *bool `json:",omitempty"` 16 | GetsDefault string 17 | } 18 | 19 | type omgStruct struct { 20 | Nest nested 21 | OneString string 22 | OptString *string `json:",omitempty"` 23 | MultiString []string 24 | OptMultiString []string `json:",omitempty"` 25 | AnInt int 26 | KeyValues1 map[string]string 27 | FileID struct { 28 | FileId uuid.UUID `json:"fileId"` 29 | } 30 | OptFileID *struct { 31 | FileId uuid.UUID `json:"fileId"` 32 | } `json:",omitempty"` 33 | Hidden string 34 | } 35 | 36 | func ptrTo[T any](v T) *T { 37 | return &v 38 | } 39 | 40 | // TestFlagMapperSmoke lazily tests all bells and whistles in one go. 41 | func TestFlagMapperSmoke(t *testing.T) { 42 | u0 := uuid.MustParse("64fb0f9e-a6ed-4a4b-8167-bf579fb0d138") 43 | u1 := uuid.MustParse("0c85c282-942d-45e1-825b-b40fc23a51f5") 44 | cmd := &cobra.Command{ 45 | Use: "test", 46 | } 47 | var dest omgStruct 48 | m := NewFlagMapper(cmd, &dest, func(s string) (uuid.UUID, error) { 49 | assert.Equal(t, "my-file", s) 50 | return u0, nil 51 | }, FlagMapperOpts{ 52 | Defaults: map[string]string{"GetsDefault": "good"}, 53 | Descriptions: map[string]string{"AnInt": "sweet"}, 54 | Hide: []string{"Hidden"}, 55 | Rename: map[string]string{"KeyValues1": "key-values"}, 56 | }) 57 | assert.Contains(t, cmd.Flags().Lookup("an-int").Usage, "sweet") 58 | assert.Nil(t, cmd.Flags().Lookup("hidden")) 59 | cmd.Run = func(cmd *cobra.Command, args []string) { 60 | err := m.MapFlags() 61 | require.NoError(t, err) 62 | 63 | assert.Equal(t, omgStruct{ 64 | OneString: "one", 65 | OptString: ptrTo("two"), 66 | MultiString: []string{"item-1", "item-2"}, 67 | AnInt: 42, 68 | KeyValues1: map[string]string{"a": "b", "c": "d"}, 69 | FileID: struct { 70 | FileId uuid.UUID "json:\"fileId\"" 71 | }{u0}, 72 | OptFileID: &struct { 73 | FileId uuid.UUID `json:"fileId"` 74 | }{u1}, 75 | Nest: nested{ 76 | AnOptInt: ptrTo(31337), 77 | Bool: true, 78 | OptBool: ptrTo(false), 79 | GetsDefault: "good", 80 | }, 81 | }, dest) 82 | } 83 | cmd.SetArgs([]string{"test", 84 | "--one-string", "one", 85 | "--opt-string", "two", 86 | "--multi-string", "item-1", 87 | "--multi-string", "item-2", 88 | "--an-int", "42", 89 | "--an-opt-int", "31337", 90 | "--bool", 91 | "--opt-bool=false", 92 | "--key-values", "a=b", 93 | "--key-values", "c=d", 94 | "--file-id", "@my-file", 95 | "--opt-file-id", u1.String()}) 96 | cmd.Execute() 97 | 98 | } 99 | -------------------------------------------------------------------------------- /pkg/connection/glue_test.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/lensesio/lenses-go/v5/pkg/api" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGlueUpdateMinimal(t *testing.T) { 13 | m := genConnMock{} 14 | c := NewGlueGroupCommand(&m, noUpload(t)) 15 | c.SetArgs([]string{"update", "the-name", "--aws-connection", "test-aws-conn", "--glue-registry-arn", "test-arn"}) 16 | err := c.Execute() 17 | require.NoError(t, err) 18 | assert.Equal(t, ptrTo("the-name"), m.upName) 19 | assert.Equal(t, &api.UpsertConnectionAPIRequestV2{ 20 | Configuration: configurationObjectAWSGlueSchemaRegistryV2{ 21 | GlueRegistryArn: v2val[string]{"test-arn"}, 22 | AccessKeyId: v2ref{"test-aws-conn"}, 23 | SecretAccessKey: v2ref{"test-aws-conn"}, 24 | }, 25 | Tags: []string{}, 26 | TemplateName: ptrTo("AWSGlueSchemaRegistry"), 27 | }, m.upReqV2) 28 | } 29 | 30 | func TestGlueUpdateExtraBells(t *testing.T) { 31 | m := genConnMock{} 32 | c := NewGlueGroupCommand(&m, noUpload(t)) 33 | c.SetArgs([]string{"update", "the-name", "--aws-connection", "test-aws-conn", "--glue-registry-arn", "test-arn", "--glue-registry-cache-ttl", "123", "--glue-registry-cache-size", "31337", "--tags", "okay", "--tags", "cool"}) 34 | err := c.Execute() 35 | require.NoError(t, err) 36 | assert.Equal(t, ptrTo("the-name"), m.upName) 37 | assert.Equal(t, &api.UpsertConnectionAPIRequestV2{ 38 | Configuration: configurationObjectAWSGlueSchemaRegistryV2{ 39 | GlueRegistryArn: v2val[string]{"test-arn"}, 40 | AccessKeyId: v2ref{"test-aws-conn"}, 41 | SecretAccessKey: v2ref{"test-aws-conn"}, 42 | GlueRegistryCacheTtl: &v2val[int]{123}, 43 | GlueRegistryCacheSize: &v2val[int]{31337}, 44 | }, 45 | Tags: []string{"okay", "cool"}, 46 | TemplateName: ptrTo("AWSGlueSchemaRegistry"), 47 | }, m.upReqV2) 48 | } 49 | 50 | func TestGlueTest(t *testing.T) { 51 | // The update flag is a "tri-state boolean", let's cover three cases. 52 | for name, upd := range map[string]*bool{"True": ptrTo(true), "False": ptrTo(false), "Absent": nil} { 53 | t.Run("UpdateFlag"+name, func(t *testing.T) { 54 | m := genConnMock{} 55 | c := NewGlueGroupCommand(&m, noUpload(t)) 56 | args := []string{"test", "the-name", "--aws-connection", "test-aws-conn", "--glue-registry-arn", "test-arn"} 57 | if upd != nil { 58 | args = append(args, fmt.Sprintf("--update=%t", *upd)) 59 | } 60 | c.SetArgs(args) 61 | err := c.Execute() 62 | require.NoError(t, err) 63 | assert.Equal(t, &api.TestConnectionAPIRequestV2{ 64 | Name: "the-name", 65 | Configuration: configurationObjectAWSGlueSchemaRegistryV2{ 66 | GlueRegistryArn: v2val[string]{"test-arn"}, 67 | AccessKeyId: v2ref{"test-aws-conn"}, 68 | SecretAccessKey: v2ref{"test-aws-conn"}, 69 | }, 70 | TemplateName: "AWSGlueSchemaRegistry", 71 | Update: upd, 72 | }, m.testReqV2) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/connection/kafka.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | type kafkaClient interface { 9 | GetKafkaConnection(name string) (resp api.KafkaConnectionResponse, err error) 10 | UpdateKafkaConnection(name string, reqBody api.KafkaConnectionUpsertRequest) (resp api.AddConnectionResponse, err error) 11 | DeleteKafkaConnection(name string) (err error) 12 | TestKafkaConnection(reqBody api.KafkaConnectionTestRequest) (err error) 13 | ListKafkaConnections() (resp []api.ConnectionSummaryResponse, err error) 14 | } 15 | 16 | func NewKafkaGroupCommand(cl kafkaClient, up uploadFunc) *cobra.Command { 17 | cmd := &cobra.Command{ 18 | Use: "kafka", 19 | Short: "Manage Lenses Kafka connections", 20 | SilenceErrors: true, 21 | TraverseChildren: true, 22 | } 23 | 24 | var testReq api.KafkaConnectionTestRequest 25 | var upsertReq api.KafkaConnectionUpsertRequest 26 | cmd.AddCommand(connCrudsToCobra("Kafka", up, 27 | connCrud{ 28 | use: "get", 29 | defaultArg: "kafka", 30 | runWithArgRet: func(arg string) (interface{}, error) { return cl.GetKafkaConnection(arg) }, 31 | }, connCrud{ 32 | use: "list", 33 | runNoArgRet: func() (interface{}, error) { return cl.ListKafkaConnections() }, 34 | }, connCrud{ 35 | use: "test", 36 | defaultArg: "kafka", 37 | opts: FlagMapperOpts{ 38 | Descriptions: kdescs, 39 | Hide: []string{"Name", "MetricsCustomPortMappings"}, 40 | }, 41 | onto: &testReq, 42 | runWithargNoRet: func(arg string) error { 43 | testReq.Name = arg 44 | return cl.TestKafkaConnection(testReq) 45 | }, 46 | }, connCrud{ 47 | use: "upsert", 48 | defaultArg: "kafka", 49 | opts: FlagMapperOpts{ 50 | Descriptions: kdescs, 51 | Hide: []string{"MetricsCustomPortMappings"}, 52 | }, 53 | onto: &upsertReq, 54 | runWithArgRet: func(arg string) (interface{}, error) { 55 | return cl.UpdateKafkaConnection(arg, upsertReq) 56 | }, 57 | }, connCrud{ 58 | use: "delete", 59 | runWithargNoRet: cl.DeleteKafkaConnection, 60 | })...) 61 | 62 | return cmd 63 | } 64 | 65 | var kdescs = map[string]string{ 66 | "KafkaBootstrapServers": "Comma separated list of protocol://host:port to use for initial connection to Kafka.", 67 | "AdditionalProperties": "Any other additional properties.", 68 | "Keytab": "Kerberos keytab file.", 69 | "MetricsCustomPortMappings": "DEPRECATED.", 70 | "MetricsCustomURLMappings": "Mapping from node URL to metrics URL, allows overriding metrics target on a per-node basis.", 71 | "MetricsHTTPSuffix": "HTTP URL suffix for Jolokia or AWS metrics.", 72 | "MetricsHTTPTimeout": "HTTP Request timeout (ms) for Jolokia or AWS metrics.", 73 | "MetricsPassword": "The password for metrics connections.", 74 | "MetricsPort": "Default port number for metrics connection (JMX and JOLOKIA).", 75 | "MetricsSsl": "Flag to enable SSL for metrics connections.", 76 | "MetricsType": "Metrics type.", 77 | "MetricsUsername": "The username for metrics connections.", 78 | "Protocol": "Kafka security protocol.", 79 | "SaslJaasConfig": "JAAS Login module configuration for SASL.", 80 | "SaslMechanism": "Mechanism to use when authenticated using SASL.", 81 | "SslKeyPassword": "Key password for the keystore.", 82 | "SslKeystore": "SSL keystore file.", 83 | "SslKeystorePassword": "Password to the keystore.", 84 | "SslTruststore": "SSL truststore file.", 85 | "SslTruststorePassword": "Password to the truststore.", 86 | "Update": "Set to true if testing an update to an existing connection, false if testing a new connection.", 87 | "Tags": "Any tags to add to the connection's metadata.", 88 | } 89 | -------------------------------------------------------------------------------- /pkg/connection/kafkaconnect.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | cobra "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewKafkaConnectGroupCommand(up uploadFunc) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "kafka-connect", 12 | Aliases: []string{"kc"}, 13 | Short: "Manage Lenses Kafka Connect connections", 14 | SilenceErrors: true, 15 | TraverseChildren: true, 16 | } 17 | 18 | var testReq api.KafkaConnectConnectionTestRequest 19 | var upsertReq api.KafkaConnectConnectionUpsertRequest 20 | cmd.AddCommand(connCrudsToCobra("Kafka Connect", up, 21 | connCrud{ 22 | use: "get", 23 | runWithArgRet: func(arg string) (interface{}, error) { return config.Client.GetKafkaConnectConnection(arg) }, 24 | }, connCrud{ 25 | use: "list", 26 | runNoArgRet: func() (interface{}, error) { return config.Client.ListKafkaConnectConnections() }, 27 | }, connCrud{ 28 | use: "test", 29 | opts: FlagMapperOpts{ 30 | Descriptions: kcdescs, 31 | Hide: []string{"Name", "MetricsCustomPortMappings"}, 32 | }, 33 | onto: &testReq, 34 | runWithargNoRet: func(arg string) error { 35 | testReq.Name = arg 36 | return config.Client.TestKafkaConnectConnection(testReq) 37 | }, 38 | }, connCrud{ 39 | use: "upsert", 40 | opts: FlagMapperOpts{ 41 | Descriptions: kcdescs, 42 | Hide: []string{"MetricsCustomPortMappings"}, 43 | }, 44 | onto: &upsertReq, 45 | runWithArgRet: func(arg string) (interface{}, error) { 46 | return config.Client.UpdateKafkaConnectConnection(arg, upsertReq) 47 | }, 48 | }, connCrud{ 49 | use: "delete", 50 | runWithargNoRet: config.Client.DeleteKafkaConnectConnection, 51 | })...) 52 | 53 | return cmd 54 | } 55 | 56 | var kcdescs = map[string]string{ 57 | "Workers": "List of Kafka Connect worker URLs.", 58 | "Aes256Key": "AES256 Key used to encrypt secret properties when deploying Connectors to this ConnectCluster.", 59 | "MetricsCustomPortMappings": "DEPRECATED.", 60 | "MetricsCustomURLMappings": "Mapping from node URL to metrics URL, allows overriding metrics target on a per-node basis.", 61 | "MetricsHTTPSuffix": "HTTP URL suffix for Jolokia metrics.", 62 | "MetricsHTTPTimeout": "HTTP Request timeout (ms) for Jolokia metrics.", 63 | "MetricsPassword": "The password for metrics connections.", 64 | "MetricsPort": "Default port number for metrics connection (JMX and JOLOKIA).", 65 | "MetricsSsl": "Flag to enable SSL for metrics connections.", 66 | "MetricsType": "Metrics type.", 67 | "MetricsUsername": "The username for metrics connections.", 68 | "Password": "Password for HTTP Basic Authentication.", 69 | "SslAlgorithm": "Name of the ssl algorithm. If empty default one will be used (X509).", 70 | "SslKeyPassword": "Key password for the keystore.", 71 | "SslKeystore": "SSL keystore file.", 72 | "SslKeystorePassword": "Password to the keystore.", 73 | "SslTruststore": "SSL truststore file.", 74 | "SslTruststorePassword": "Password to the truststore.", 75 | "Username": "Username for HTTP Basic Authentication.", 76 | "Update": "Set to true if testing an update to an existing connection, false if testing a new connection.", 77 | "Tags": "Any tags to add to the connection's metadata.", 78 | } 79 | -------------------------------------------------------------------------------- /pkg/connection/kerberos.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | cobra "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewKerberosGroupCommand(up uploadFunc) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "kerberos", 12 | Aliases: []string{"kerb"}, 13 | Short: "Manage Lenses Kerberos connections", 14 | SilenceErrors: true, 15 | TraverseChildren: true, 16 | } 17 | 18 | var testReq api.KerberosConnectionTestRequest 19 | var upsertReq api.KerberosConnectionUpsertRequest 20 | cmd.AddCommand(connCrudsToCobra("Kerberos", up, 21 | connCrud{ 22 | use: "get", 23 | runWithArgRet: func(arg string) (interface{}, error) { return config.Client.GetKerberosConnection(arg) }, 24 | }, connCrud{ 25 | use: "list", 26 | runNoArgRet: func() (interface{}, error) { return config.Client.ListKerberosConnections() }, 27 | }, connCrud{ 28 | use: "test", 29 | defaultArg: "kerberos", 30 | opts: FlagMapperOpts{ 31 | Descriptions: kerbdescs, 32 | Hide: []string{"Name"}, 33 | }, 34 | onto: &testReq, 35 | runWithargNoRet: func(arg string) error { 36 | testReq.Name = arg 37 | return config.Client.TestKerberosConnection(testReq) 38 | }, 39 | }, connCrud{ 40 | use: "upsert", 41 | defaultArg: "kerberos", 42 | opts: FlagMapperOpts{ 43 | Descriptions: kerbdescs, 44 | }, 45 | onto: &upsertReq, 46 | runWithArgRet: func(arg string) (interface{}, error) { 47 | return config.Client.UpdateKerberosConnection(arg, upsertReq) 48 | }, 49 | }, connCrud{ 50 | use: "delete", 51 | runWithargNoRet: config.Client.DeleteKerberosConnection, 52 | })...) 53 | 54 | return cmd 55 | } 56 | 57 | var kerbdescs = map[string]string{ 58 | "KerberosKrb5": "Kerberos krb5.conf file.", 59 | "Update": "Set to true if testing an update to an existing connection, false if testing a new connection.", 60 | "Tags": "Any tags to add to the connection's metadata.", 61 | } 62 | -------------------------------------------------------------------------------- /pkg/connection/pagerduty.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewPagerDutyGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "pager-duty", 11 | Aliases: []string{"pd"}, 12 | Short: "Manage Lenses PagerDuty connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectPagerDuty]("PagerDuty", gen, up, FlagMapperOpts{ 17 | Descriptions: map[string]string{ 18 | "IntegrationKey": "An Integration Key for PagerDuty's service with Events API v2 integration type.", 19 | }, 20 | })...) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /pkg/connection/postgres.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewPostgreSQLGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "postgresql", 11 | Aliases: []string{"pg"}, 12 | Short: "Manage Lenses PostgreSQL connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectPostgreSQL]("PostgreSQL", gen, up, FlagMapperOpts{ 17 | Descriptions: map[string]string{ 18 | "Database": "The database to connect to.", 19 | "Host": "The Postgres hostname.", 20 | "Port": "The port number.", 21 | "SslMode": "The SSL connection mode as detailed in https://jdbc.postgresql.org/documentation/head/ssl-client.html.", 22 | "Username": "The user name.", 23 | "Password": "The password.", 24 | }, 25 | Rename: map[string]string{ 26 | "Host": "pg-host", // clashes. 27 | }, 28 | })...) 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /pkg/connection/prometheus.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewPrometheusAlertmanagerGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "prometheus", 11 | Aliases: []string{"prom", "alert-manager"}, 12 | Short: "Manage Lenses PrometheusAlertmanager connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectPrometheusAlertmanager]("PrometheusAlertmanager", gen, up, FlagMapperOpts{ 17 | Descriptions: map[string]string{ 18 | "Endpoints": "List of Alert Manager endpoints.", 19 | }, 20 | })...) 21 | return cmd 22 | } 23 | -------------------------------------------------------------------------------- /pkg/connection/schemaregistry.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | cobra "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewSchemaRegistryGroupCommand(up uploadFunc) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "schema-registry", 12 | Aliases: []string{"sr"}, 13 | Short: "Manage Lenses Schema Registry connections", 14 | SilenceErrors: true, 15 | TraverseChildren: true, 16 | } 17 | 18 | var testReq api.SchemaRegistryConnectionTestRequest 19 | var upsertReq api.SchemaRegistryConnectionUpsertRequest 20 | cmd.AddCommand(connCrudsToCobra("Schema Registry", up, 21 | connCrud{ 22 | use: "get", 23 | defaultArg: "schema-registry", 24 | runWithArgRet: func(arg string) (interface{}, error) { return config.Client.GetSchemaRegistryConnection(arg) }, 25 | }, connCrud{ 26 | use: "list", 27 | runNoArgRet: func() (interface{}, error) { return config.Client.ListSchemaRegistryConnections() }, 28 | }, connCrud{ 29 | use: "test", 30 | defaultArg: "schema-registry", 31 | opts: FlagMapperOpts{ 32 | Descriptions: srdescs, 33 | Hide: []string{"Name", "MetricsCustomPortMappings"}, 34 | }, 35 | onto: &testReq, 36 | runWithargNoRet: func(arg string) error { 37 | testReq.Name = arg 38 | return config.Client.TestSchemaRegistryConnection(testReq) 39 | }, 40 | }, connCrud{ 41 | use: "upsert", 42 | defaultArg: "schema-registry", 43 | opts: FlagMapperOpts{ 44 | Descriptions: srdescs, 45 | Hide: []string{"MetricsCustomPortMappings"}, 46 | }, 47 | onto: &upsertReq, 48 | runWithArgRet: func(arg string) (interface{}, error) { 49 | return config.Client.UpdateSchemaRegistryConnection(arg, upsertReq) 50 | }, 51 | }, connCrud{ 52 | use: "delete", 53 | runWithargNoRet: config.Client.DeleteSchemaRegistryConnection, 54 | })...) 55 | 56 | return cmd 57 | } 58 | 59 | var srdescs = map[string]string{ 60 | "SchemaRegistryURLs": "List of schema registry urls.", 61 | "AdditionalProperties": "Any other additional properties.", 62 | "MetricsCustomPortMappings": "DEPRECATED.", 63 | "MetricsCustomURLMappings": "Mapping from node URL to metrics URL, allows overriding metrics target on a per-node basis.", 64 | "MetricsHTTPSuffix": "HTTP URL suffix for Jolokia metrics.", 65 | "MetricsHTTPTimeout": "HTTP Request timeout (ms) for Jolokia metrics.", 66 | "MetricsPassword": "The password for metrics connections.", 67 | "MetricsPort": "Default port number for metrics connection (JMX and JOLOKIA).", 68 | "MetricsSsl": "Flag to enable SSL for metrics connections.", 69 | "MetricsType": "Metrics type.", 70 | "MetricsUsername": "The username for metrics connections.", 71 | "Password": "Password for HTTP Basic Authentication.", 72 | "SslKeyPassword": "Key password for the keystore.", 73 | "SslKeystore": "SSL keystore.", 74 | "SslTruststore": "SSL truststore.", 75 | "SslKeystorePassword": "Password to the keystore.", 76 | "SslTruststorePassword": "Password to the truststore.", 77 | "Username": "Username for HTTP Basic Authentication.", 78 | "Update": "Set to true if testing an update to an existing connection, false if testing a new connection.", 79 | "Tags": "Any tags to add to the connection's metadata.", 80 | } 81 | -------------------------------------------------------------------------------- /pkg/connection/slack.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewSlackGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "slack", 11 | Short: "Manage Lenses Slack connections", 12 | SilenceErrors: true, 13 | TraverseChildren: true, 14 | } 15 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectSlack]("Slack", gen, up, FlagMapperOpts{ 16 | Descriptions: map[string]string{ 17 | "WebhookURL": "The Slack endpoint to send the alert to.", 18 | }, 19 | })...) 20 | return cmd 21 | } 22 | -------------------------------------------------------------------------------- /pkg/connection/splunk.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewSplunkGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "splunk", 11 | Short: "Manage Lenses Splunk connections", 12 | SilenceErrors: true, 13 | TraverseChildren: true, 14 | } 15 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectSplunk]("Splunk", gen, up, FlagMapperOpts{ 16 | Descriptions: map[string]string{ 17 | "Host": "The host name for the HTTP Event Collector API of the Splunk instance.", 18 | "Insecure": "This is not encouraged but is required for a Splunk Cloud Trial instance.", 19 | "Token": "HTTP event collector authorization token.", 20 | "UseHTTPs": "Use SSL.", 21 | "Port": "The port number for the HTTP Event Collector API of the Splunk instance.", 22 | }, 23 | Rename: map[string]string{ 24 | "Host": "pg-host", // clashes. 25 | "Insecure": "pg-insecure", // clashes. 26 | "Token": "pg-token", // clashes. 27 | }, 28 | })...) 29 | return cmd 30 | } 31 | -------------------------------------------------------------------------------- /pkg/connection/webhook.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | cobra "github.com/spf13/cobra" 6 | ) 7 | 8 | func NewWebhookGroupCommand(gen genericConnectionClient, up uploadFunc) *cobra.Command { 9 | cmd := &cobra.Command{ 10 | Use: "webhook", 11 | Aliases: []string{"wh"}, 12 | Short: "Manage Lenses Webhook connections", 13 | SilenceErrors: true, 14 | TraverseChildren: true, 15 | } 16 | cmd.AddCommand(newGenericAPICommand[api.ConfigurationObjectWebhook]("Webhook", gen, up, FlagMapperOpts{ 17 | Descriptions: map[string]string{ 18 | "Host": "The host name.", 19 | "UseHTTPs": "Set to true in order to set the URL scheme to `https`. Will otherwise default to `http`.", 20 | "Creds": "An array of (secret) strings to be passed over to alert channel plugins.", 21 | "Port": "An optional port number to be appended to the the hostname.", 22 | }, 23 | Rename: map[string]string{ 24 | "Host": "wh-host", // clashes. 25 | }, 26 | })...) 27 | return cmd 28 | } 29 | -------------------------------------------------------------------------------- /pkg/connection/zookeeper.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | cobra "github.com/spf13/cobra" 7 | ) 8 | 9 | func NewZookeeperGroupCommand(up uploadFunc) *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "zookeeper", 12 | Aliases: []string{"zk"}, 13 | Short: "Manage Lenses Zookeeper connections", 14 | SilenceErrors: true, 15 | TraverseChildren: true, 16 | } 17 | 18 | var testReq api.ZookeeperConnectionTestRequest 19 | var upsertReq api.ZookeeperConnectionUpsertRequest 20 | cmd.AddCommand(connCrudsToCobra("Zookeeper", up, 21 | connCrud{ 22 | use: "get", 23 | defaultArg: "zookeeper", 24 | runWithArgRet: func(arg string) (interface{}, error) { return config.Client.GetZookeeperConnection(arg) }, 25 | }, connCrud{ 26 | use: "list", 27 | runNoArgRet: func() (interface{}, error) { return config.Client.ListZookeeperConnections() }, 28 | }, connCrud{ 29 | use: "test", 30 | defaultArg: "zookeeper", 31 | opts: FlagMapperOpts{ 32 | Descriptions: zkdescs, 33 | Hide: []string{"Name", "MetricsCustomPortMappings"}, 34 | }, 35 | onto: &testReq, 36 | runWithargNoRet: func(arg string) error { 37 | testReq.Name = arg 38 | return config.Client.TestZookeeperConnection(testReq) 39 | }, 40 | }, connCrud{ 41 | use: "upsert", 42 | defaultArg: "zookeeper", 43 | opts: FlagMapperOpts{ 44 | Descriptions: zkdescs, 45 | Hide: []string{"MetricsCustomPortMappings"}, 46 | }, 47 | onto: &upsertReq, 48 | runWithArgRet: func(arg string) (interface{}, error) { 49 | return config.Client.UpdateZookeeperConnection(arg, upsertReq) 50 | }, 51 | }, connCrud{ 52 | use: "delete", 53 | runWithargNoRet: config.Client.DeleteZookeeperConnection, 54 | })...) 55 | 56 | return cmd 57 | } 58 | 59 | var zkdescs = map[string]string{ 60 | "ZookeeperConnectionTimeout": "Zookeeper connection timeout.", 61 | "ZookeeperSessionTimeout": "Zookeeper connection session timeout.", 62 | "ZookeeperURLs": "List of zookeeper urls.", 63 | "MetricsCustomPortMappings": "DEPRECATED.", 64 | "MetricsCustomURLMappings": "Mapping from node URL to metrics URL, allows overriding metrics target on a per-node basis.", 65 | "MetricsHTTPSuffix": "HTTP URL suffix for Jolokia metrics.", 66 | "MetricsHTTPTimeout": "HTTP Request timeout (ms) for Jolokia metrics.", 67 | "MetricsPassword": "The password for metrics connections.", 68 | "MetricsPort": "Default port number for metrics connection (JMX and JOLOKIA).", 69 | "MetricsSsl": "Flag to enable SSL for metrics connections.", 70 | "MetricsType": "Metrics type.", 71 | "MetricsUsername": "The username for metrics connections.", 72 | "ZookeeperChrootPath": "Zookeeper /znode path.", 73 | "Update": "Set to true if testing an update to an existing connection, false if testing a new connection.", 74 | "Tags": "Any tags to add to the connection's metadata.", 75 | } 76 | -------------------------------------------------------------------------------- /pkg/conntemplate/commands.go: -------------------------------------------------------------------------------- 1 | package conntemplate 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 9 | cobra "github.com/spf13/cobra" 10 | ) 11 | 12 | // NewConnectionTemplateGroupCommand creates `connection-templates` command 13 | func NewConnectionTemplateGroupCommand() *cobra.Command { 14 | 15 | cmd := &cobra.Command{ 16 | Use: "connection-templates", 17 | Short: `List the connection templates`, 18 | Example: ` 19 | connection-templates 20 | `, 21 | SilenceErrors: true, 22 | TraverseChildren: true, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | connectionTemplates, err := config.Client.GetConnectionTemplates() 25 | if err != nil { 26 | golog.Errorf("Failed to retrieve connection templates. [%s]", err.Error()) 27 | return err 28 | } 29 | 30 | outputFlagValue := strings.ToUpper(bite.GetOutPutFlag(cmd)) 31 | if outputFlagValue != "JSON" && outputFlagValue != "YAML" { 32 | bite.PrintInfo(cmd, "Info: use JSON or YAML output to get the complete object\n\n") 33 | } 34 | 35 | return bite.PrintObject(cmd, connectionTemplates) 36 | }, 37 | } 38 | 39 | bite.CanPrintJSON(cmd) 40 | 41 | return cmd 42 | } 43 | -------------------------------------------------------------------------------- /pkg/constants.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | // ERRORS 4 | const ( 5 | ErrResourceNotFoundMessage = 404 6 | ErrResourceNotAccessibleMessage = 403 7 | ErrResourceNotGoodMessage = 400 8 | ErrResourceInternal = 500 9 | ) 10 | 11 | // Paths 12 | const ( 13 | SQLPath = "apps/sql" 14 | ConnectorsPath = "apps/connectors" 15 | 16 | GroupsPath = "groups" 17 | UsersPath = "users" 18 | ServiceAccountsPath = "service-accounts" 19 | 20 | AclsPath = "kafka/acls" 21 | TopicsPath = "kafka/topics" 22 | QuotasPath = "kafka/quotas" 23 | 24 | SchemasPath = "schemas" 25 | AlertSettingsPath = "alert-settings" 26 | PoliciesPath = "policies" 27 | TopicSettingsPath = "topic-settings" 28 | 29 | ConnectionsFilePath = "connections" 30 | ConnectionsAPIPath = "v1/connection/connections" 31 | DatasetsAPIPath = "v1/datasets" 32 | ConnectionTemplatesAPIPath = "v1/connection/connection-templates" 33 | ConsumersGroupPath = "api/consumers" 34 | ElasticsearchIndexesPath = "/api/elastic/indexes" 35 | AlertChannelsPath = "api/v1/alert/channels" 36 | AuditChannelsPath = "api/v1/audit/channels" 37 | AlertChannelTemplatesPath = "api/v1/alert/channel-templates" 38 | AuditChannelTemplatesPath = "api/v1/audit/channel-templates" 39 | AlertsSettingsPathV1 = "api/v1/alert/settings" 40 | AlertEventsPath = "api/v1/alert/events" 41 | MetadataTopicsPath = "api/v1/metadata/topics" 42 | 43 | LicensePath = "api/v1/license" 44 | FileUploadPath = "api/v1/files" 45 | SetupPath = "api/v1/setup" // aka Wizard mode 46 | ProvisioningPath = "api/v1/state" 47 | ProvisionedConnectionsPath = ProvisioningPath + "/connections" 48 | ) 49 | -------------------------------------------------------------------------------- /pkg/elasticsearch/commands.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // IndexesCommand displays available indexes 12 | func IndexesCommand() *cobra.Command { 13 | var connectionName string 14 | var includeSystemIndexes bool 15 | cmd := &cobra.Command{ 16 | Use: "elasticsearch-indexes", 17 | Short: "List all available elasticsearch indexes", 18 | Example: `elasticsearch-indexes --connection="es-default" --include-system-indexes`, 19 | SilenceErrors: true, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | client := config.Client 22 | 23 | indexes, err := client.GetIndexes(connectionName, includeSystemIndexes) 24 | 25 | if err != nil { 26 | return fmt.Errorf("Failed to retrieve indexes. Error: [%s]", err.Error()) 27 | } 28 | return bite.PrintObject(cmd, indexes) 29 | }, 30 | } 31 | 32 | cmd.Flags().StringVar(&connectionName, "connection", "", "Connection to use") 33 | cmd.Flags().BoolVar(&includeSystemIndexes, "include-system-indexes", false, "Show system indexes") 34 | 35 | bite.CanPrintJSON(cmd) 36 | return cmd 37 | } 38 | 39 | // IndexCommand displays index data 40 | func IndexCommand() *cobra.Command { 41 | var connectionName string 42 | var indexName string 43 | 44 | cmd := &cobra.Command{ 45 | Use: "elasticsearch-index", 46 | Short: "Fetch an elasticsearch index", 47 | Example: `elasticsearch-index --connection="es-default" --name="index"`, 48 | SilenceErrors: true, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | client := config.Client 51 | 52 | index, err := client.GetIndex(connectionName, indexName) 53 | 54 | if err != nil { 55 | return fmt.Errorf("Failed to retrieve index. Error: [%s]", err.Error()) 56 | } 57 | 58 | indexview := MakeIndexView(index) 59 | 60 | return bite.PrintObject(cmd, indexview) 61 | }, 62 | } 63 | 64 | cmd.Flags().StringVar(&connectionName, "connection", "", "Connection to use") 65 | cmd.Flags().StringVar(&indexName, "name", "", "Index to look for") 66 | 67 | bite.CanPrintJSON(cmd) 68 | 69 | return cmd 70 | } 71 | -------------------------------------------------------------------------------- /pkg/elasticsearch/payloads.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | ) 6 | 7 | // IndexView type 8 | type IndexView struct { 9 | api.Index `header:"inline"` 10 | AvailableReplicas int `header:"Available replicas"` 11 | } 12 | 13 | // MakeIndexView creates a presentation for later consumption 14 | func MakeIndexView(esIndex api.Index) IndexView { 15 | availableReplicas := api.GetAvailableReplicas(esIndex) 16 | 17 | esIndex.ShardsCount = len(esIndex.Shards) 18 | view := IndexView{esIndex, availableReplicas} 19 | 20 | return view 21 | } 22 | -------------------------------------------------------------------------------- /pkg/elasticsearch/payloads_test.go: -------------------------------------------------------------------------------- 1 | package elasticsearch 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lensesio/lenses-go/v5/pkg/api" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMakeIndexView(t *testing.T) { 11 | shard := api.Shard{Shard: "1", Records: 2, Replicas: 1, AvailableReplicas: 1} 12 | 13 | apiResponse := api.Index{ 14 | Shards: []api.Shard{shard, shard}, 15 | } 16 | 17 | indexView := MakeIndexView(apiResponse) 18 | 19 | assert.Equal(t, 2, indexView.ShardsCount) 20 | } 21 | 22 | func TestMakeIndexViewNoShards(t *testing.T) { 23 | apiResponse := api.Index{} 24 | indexView := MakeIndexView(apiResponse) 25 | 26 | assert.Equal(t, 0, indexView.ShardsCount) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/export/acl_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kataras/golog" 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportAclsCommand creates `export acls` command 17 | func NewExportAclsCommand() *cobra.Command { 18 | 19 | cmd := &cobra.Command{ 20 | Use: "acls", 21 | Short: "export acls", 22 | Example: `export acls`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | checkFileFlags(cmd) 27 | 28 | if err := writeACLs(cmd, config.Client); err != nil { 29 | golog.Errorf("Error writing ACLS. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 37 | cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract dependencies, topics, acls, quotas, alerts") 38 | bite.CanBeSilent(cmd) 39 | bite.CanPrintJSON(cmd) 40 | return cmd 41 | } 42 | 43 | func writeACLs(cmd *cobra.Command, client *api.Client) error { 44 | 45 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 46 | fileName := fmt.Sprintf("acls.%s", strings.ToLower(output)) 47 | 48 | acls, err := client.GetACLs() 49 | 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if len(acls) == 0 { 55 | fmt.Fprintf(cmd.OutOrStdout(), "no available ACLs for export\n") 56 | return nil 57 | } 58 | 59 | if err := utils.WriteFile(landscapeDir, pkg.AclsPath, fileName, output, acls); err != nil { 60 | return err 61 | } 62 | 63 | exportPath := fmt.Sprintf("%s/%s/%s", landscapeDir, pkg.AclsPath, fileName) 64 | fmt.Fprintf(cmd.OutOrStdout(), "ACLs have been successfully exported at %s\n", exportPath) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/export/alert_channels_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // NewExportAlertChannelsCommand creates `export alert-channels` command 12 | func NewExportAlertChannelsCommand() *cobra.Command { 13 | var alertChannelName string 14 | 15 | cmd := &cobra.Command{ 16 | Use: "alert-channels", 17 | Short: "export alert-channels", 18 | Example: `export alert-channels --resource-name=my-alert`, 19 | SilenceErrors: true, 20 | TraverseChildren: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | checkFileFlags(cmd) 23 | if err := writeChannels(cmd, pkg.AlertChannelsPath, "alert", alertChannelName); err != nil { 24 | return fmt.Errorf("failed to export alert channels from server: [%v]", err) 25 | } 26 | return nil 27 | }, 28 | } 29 | 30 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 31 | cmd.Flags().StringVar(&alertChannelName, "resource-name", "", "The name of the alert channel to export") 32 | //cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract alert channel dependencies, e.g. connections") 33 | bite.CanBeSilent(cmd) 34 | bite.CanPrintJSON(cmd) 35 | return cmd 36 | } 37 | -------------------------------------------------------------------------------- /pkg/export/audit_channels_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // NewExportAuditChannelsCommand creates `export audit-channels` command 12 | func NewExportAuditChannelsCommand() *cobra.Command { 13 | var auditChannelName string 14 | 15 | cmd := &cobra.Command{ 16 | Use: "audit-channels", 17 | Short: "export audit-channels", 18 | Example: `export audit-channels --resource-name=my-audit`, 19 | SilenceErrors: true, 20 | TraverseChildren: true, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | checkFileFlags(cmd) 23 | if err := writeChannels(cmd, pkg.AuditChannelsPath, "audit", auditChannelName); err != nil { 24 | return fmt.Errorf("failed to export audit channels from server: [%v]", err) 25 | } 26 | return nil 27 | }, 28 | } 29 | 30 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 31 | cmd.Flags().StringVar(&auditChannelName, "resource-name", "", "The name of the audit channel to export") 32 | //cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract audit channel dependencies, e.g. connections") 33 | bite.CanBeSilent(cmd) 34 | bite.CanPrintJSON(cmd) 35 | return cmd 36 | } 37 | -------------------------------------------------------------------------------- /pkg/export/channels.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "hash/fnv" 7 | "strings" 8 | 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func writeChannels(cmd *cobra.Command, channelsPath, channelType, channelName string) error { 17 | channels, err := config.Client.GetChannels(channelsPath, 1, 99999, "name", "asc", "", "") 18 | if err != nil { 19 | return fmt.Errorf("failed to retrieve channels from server: [%v]", err) 20 | } 21 | 22 | if channelName != "" { 23 | for _, channel := range channels.Values { 24 | if channelName == channel.Name { 25 | // fileName := fmt.Sprintf("%s-channel-%s.%s", channelType, strings.ToLower(channel.Name), strings.ToLower(bite.GetOutPutFlag(cmd))) 26 | // subDir := channelType+"-channels" 27 | 28 | // utils.WriteFile(landscapeDir, subDir, fileName, strings.ToUpper(bite.GetOutPutFlag(cmd)), channel) 29 | // fmt.Fprintf(cmd.OutOrStdout(), "exporting [%s] %s channel to base directory [%s]\n", channelName, channelType, landscapeDir) 30 | 31 | // return nil 32 | 33 | writeChannelToFile(cmd, channelType, channel.Name, channel) 34 | } 35 | } 36 | 37 | return fmt.Errorf("%s channel with name [%s] was not found", channelType, channelName) 38 | } 39 | 40 | var channelsForExport []api.ChannelPayload 41 | for _, chann := range channels.Values { 42 | var channForExport api.ChannelPayload 43 | channΑsJSON, _ := json.Marshal(chann) 44 | json.Unmarshal(channΑsJSON, &channForExport) 45 | channelsForExport = append(channelsForExport, channForExport) 46 | } 47 | 48 | for _, channelForExport := range channelsForExport { 49 | writeChannelToFile(cmd, channelType, channelForExport.Name, channelForExport) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func writeChannelToFile(cmd *cobra.Command, channelType, channelName string, channel interface{}) error { 56 | h := fnv.New64a() 57 | h.Write([]byte(channelName)) 58 | fileName := fmt.Sprintf("%s-channel-%s-%08x.%s", channelType, strings.ToLower(channelName), uint32(h.Sum64()), strings.ToLower(bite.GetOutPutFlag(cmd))) 59 | subDir := channelType + "-channels" 60 | 61 | utils.WriteFile(landscapeDir, subDir, fileName, strings.ToUpper(bite.GetOutPutFlag(cmd)), channel) 62 | 63 | fmt.Fprintf(cmd.OutOrStdout(), "exported %s channel [%s] to [%s]\n", channelType, channelName, fileName) 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/export/fileWriter.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | ) 7 | 8 | type OSFileWriter struct{} 9 | 10 | // MkdirAll creates the landscape dir with the specified mode 11 | func (w OSFileWriter) MkdirAll(landscape string, mode fs.FileMode) error { 12 | return os.MkdirAll(landscapeDir, mode) 13 | } 14 | 15 | // WriteFile writes the content to a file with the specified filename and mode. 16 | func (w OSFileWriter) WriteFile(filePath string, content string, mode fs.FileMode) error { 17 | return os.WriteFile(filePath, []byte(content), mode) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/export/group_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "strings" 7 | 8 | "github.com/kataras/golog" 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportGroupsCommand creates `export users` 17 | func NewExportGroupsCommand() *cobra.Command { 18 | var name string 19 | cmd := &cobra.Command{ 20 | Use: "groups", 21 | Short: "export groups", 22 | Example: `export groups`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | checkFileFlags(cmd) 27 | if err := writeGroups(cmd, name); err != nil { 28 | golog.Errorf("Error writing Users. [%s]", err.Error()) 29 | return err 30 | } 31 | return nil 32 | }, 33 | } 34 | 35 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 36 | cmd.Flags().StringVar(&name, "name", "", "The group name to extract") 37 | bite.CanBeSilent(cmd) 38 | bite.CanPrintJSON(cmd) 39 | return cmd 40 | } 41 | 42 | func writeGroups(cmd *cobra.Command, groupName string) error { 43 | 44 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 45 | 46 | if groupName != "" { 47 | group, err := config.Client.GetGroup(groupName) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | // Since groups can differ in case, we use a hash to ensure uniqueness 53 | // and avoid writing one file over another with the same name 54 | h := fnv.New64a() 55 | h.Write([]byte(group.Name)) 56 | 57 | fileName := strings.ToLower(fmt.Sprintf("groups-%s-%08x.%s", group.Name, uint32(h.Sum64()), output)) 58 | return utils.WriteFile(landscapeDir, pkg.GroupsPath, fileName, output, group) 59 | } 60 | groups, err := config.Client.GetGroups() 61 | if err != nil { 62 | return err 63 | } 64 | 65 | // Since groups can differ in case, we use a hash to ensure uniqueness 66 | // and avoid writing one file over another with the same name 67 | h := fnv.New64a() 68 | 69 | for _, group := range groups { 70 | h.Write([]byte(group.Name)) 71 | 72 | fileName := strings.ToLower(fmt.Sprintf("groups-%s-%08x.%s", group.Name, uint32(h.Sum64()), output)) 73 | 74 | if groupName != "" && group.Name == groupName { 75 | return utils.WriteFile(landscapeDir, pkg.GroupsPath, fileName, output, group) 76 | } 77 | 78 | err := utils.WriteFile(landscapeDir, pkg.GroupsPath, fileName, output, group) 79 | if err != nil { 80 | return err 81 | } 82 | } 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/export/policy_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kataras/golog" 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportPoliciesCommand creates `export policies` command 17 | func NewExportPoliciesCommand() *cobra.Command { 18 | var name, ID string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "policies", 22 | Short: "export policies", 23 | Example: `export policies --resource-name my-policy`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | client := config.Client 28 | 29 | setExecutionMode(client) 30 | checkFileFlags(cmd) 31 | if err := writePolicies(cmd, client, name, ID); err != nil { 32 | golog.Errorf("Error writing policies. [%s]", err.Error()) 33 | return err 34 | } 35 | return nil 36 | }, 37 | } 38 | 39 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 40 | cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract dependencies, topics, acls, quotas, alerts") 41 | cmd.Flags().StringVar(&name, "resource-name", "", "The resource name to export") 42 | cmd.Flags().StringVar(&ID, "id", "", "The policy id to extract") 43 | bite.CanPrintJSON(cmd) 44 | bite.CanBeSilent(cmd) 45 | return cmd 46 | } 47 | 48 | func writePolicies(cmd *cobra.Command, client *api.Client, name string, ID string) error { 49 | golog.Infof("Writing policies to [%s]", landscapeDir) 50 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 51 | 52 | if ID != "" { 53 | policy, err := client.GetPolicy(ID) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | fileName := fmt.Sprintf("policies-%s-%s.%s", strings.ToLower(policy.Name), strings.ToLower(policy.ID), strings.ToLower(output)) 59 | request := client.PolicyAsRequest(policy) 60 | return utils.WriteFile(landscapeDir, pkg.PoliciesPath, fileName, output, request) 61 | } 62 | 63 | policies, err := client.GetPolicies() 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, policy := range policies { 69 | fileName := fmt.Sprintf("policies-%s-%s.%s", strings.ToLower(policy.Name), strings.ToLower(policy.ID), strings.ToLower(output)) 70 | if name != "" && policy.Name == name { 71 | return utils.WriteFile(landscapeDir, pkg.PoliciesPath, fileName, output, policy) 72 | } 73 | 74 | err := utils.WriteFile(landscapeDir, pkg.PoliciesPath, fileName, output, policy) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/export/processors_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kataras/golog" 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportProcessorsCommand creates `export processors` command 17 | func NewExportProcessorsCommand() *cobra.Command { 18 | var name, cluster, namespace, id string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "processors", 22 | Short: "export processors", 23 | Example: `export processors --resource-name my-processor`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | client := config.Client 28 | 29 | setExecutionMode(client) 30 | checkFileFlags(cmd) 31 | if err := writeProcessors(cmd, client, id, cluster, namespace, name); err != nil { 32 | golog.Errorf("Error writing processors. [%s]", err.Error()) 33 | return err 34 | } 35 | return nil 36 | }, 37 | } 38 | 39 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 40 | cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract dependencies, topics, acls, quotas, alerts") 41 | cmd.Flags().StringVar(&name, "resource-name", "", "The processor name to export") 42 | cmd.Flags().StringVar(&cluster, "cluster-name", "", "Select by cluster name, available only in CONNECT and KUBERNETES mode") 43 | cmd.Flags().StringVar(&namespace, "namespace", "", "Select by namespace, available only in KUBERNETES mode") 44 | cmd.Flags().StringVar(&id, "id", "", "ID of the processor to export") 45 | cmd.Flags().StringVar(&prefix, "prefix", "", "Processor with the prefix in the name only") 46 | 47 | bite.CanBeSilent(cmd) 48 | bite.CanPrintJSON(cmd) 49 | return cmd 50 | } 51 | 52 | func writeProcessors(cmd *cobra.Command, client *api.Client, id, cluster, namespace, name string) error { 53 | 54 | if mode == api.ExecutionModeInProcess { 55 | cluster = "IN-PROC" 56 | namespace = "Lenses" 57 | } 58 | processors, err := client.GetProcessors() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | for _, processor := range processors.Streams { 64 | if id != "" && id != processor.ID { 65 | continue 66 | } else { 67 | if cluster != "" && cluster != processor.ClusterName { 68 | continue 69 | } 70 | 71 | if namespace != "" && namespace != processor.Namespace { 72 | continue 73 | } 74 | 75 | if name != "" && name != processor.Name { 76 | continue 77 | } 78 | 79 | if prefix != "" && !strings.HasPrefix(processor.Name, prefix) { 80 | continue 81 | } 82 | } 83 | request := processor.ProcessorAsFile() 84 | 85 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 86 | 87 | if output == "TABLE" { 88 | output = "YAML" 89 | } 90 | 91 | var fileName string 92 | 93 | // Appends the processor id to avoid file name conflicts when the processor name is 94 | // the same but the id is different, or the processor name differs in case sensitivity. 95 | if mode == api.ExecutionModeInProcess { 96 | fileName = fmt.Sprintf("processor-%s-%s.%s", strings.ToLower(processor.Name), strings.ToLower(processor.ProcessorID), strings.ToLower(output)) 97 | } else if mode == api.ExecutionModeConnect { 98 | fileName = fmt.Sprintf("processor-%s-%s-%s.%s", strings.ToLower(processor.ClusterName), strings.ToLower(processor.Name), strings.ToLower(processor.ProcessorID), strings.ToLower(output)) 99 | } else { 100 | fileName = fmt.Sprintf("processor-%s-%s-%s-%s.%s", strings.ToLower(processor.ClusterName), strings.ToLower(processor.Namespace), strings.ToLower(processor.Name), strings.ToLower(processor.ProcessorID), strings.ToLower(output)) 101 | } 102 | 103 | // trim so the yaml is a multiline string 104 | request.SQL = strings.TrimSpace(request.SQL) 105 | request.SQL = strings.Replace(request.SQL, "\t", " ", -1) 106 | request.SQL = strings.Replace(request.SQL, " \n", "\n", -1) 107 | 108 | if err := utils.WriteFile(landscapeDir, pkg.SQLPath, fileName, output, request); err != nil { 109 | return err 110 | } 111 | if dependents { 112 | handleDependents(cmd, client, processor.ID) 113 | } 114 | 115 | exportPath := fmt.Sprintf("%s/%s/%s", landscapeDir, pkg.SQLPath, fileName) 116 | fmt.Printf("processor '%s' has been successfully exported at %s\n", request.Name, exportPath) 117 | } 118 | 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /pkg/export/quota_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/kataras/golog" 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportQuotasCommand creates `export quotas` command 17 | func NewExportQuotasCommand() *cobra.Command { 18 | 19 | cmd := &cobra.Command{ 20 | Use: "quotas", 21 | Short: "export quotas", 22 | Example: `export quoats`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | checkFileFlags(cmd) 27 | 28 | if err := writeQuotas(cmd, config.Client); err != nil { 29 | golog.Errorf("Error writing quotas. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 37 | cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract dependencies, topics, acls, quotas, alerts") 38 | bite.CanPrintJSON(cmd) 39 | bite.CanBeSilent(cmd) 40 | return cmd 41 | } 42 | 43 | func writeQuotas(cmd *cobra.Command, client *api.Client) error { 44 | 45 | quotas, err := client.GetQuotas() 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | var requests []api.CreateQuotaPayload 52 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 53 | fileName := fmt.Sprintf("quotas.%s", strings.ToLower(output)) 54 | 55 | for _, q := range quotas { 56 | requests = append(requests, q.GetQuotaAsRequest()) 57 | } 58 | 59 | return utils.WriteFile(landscapeDir, pkg.QuotasPath, fileName, output, requests) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/export/repo_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/go-git/go-git/v5" 8 | "github.com/go-git/go-git/v5/config" 9 | "github.com/kataras/golog" 10 | "github.com/lensesio/bite" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // InitRepoCommand creates the `init-repo` command 15 | func InitRepoCommand() *cobra.Command { 16 | var gitURL string 17 | var gitSupport bool 18 | 19 | cmd := &cobra.Command{ 20 | Use: "init-repo", 21 | Short: "Initialise a git repo to hold a landscape", 22 | Example: `init-repo --git-url git@gitlab.com:landoop/demo-landscape.git`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | if gitSupport { 27 | if err := addGitSupport(cmd, gitURL); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&landscapeDir, "name", "", "Directory name to repo in") 37 | cmd.Flags().BoolVar(&gitSupport, "git", false, "Initialize a git repo") 38 | cmd.Flags().StringVar(&gitURL, "git-url", "", "-Remote url to set for the repo") 39 | cmd.MarkFlagRequired("name") 40 | 41 | return cmd 42 | } 43 | 44 | func addGitSupport(cmd *cobra.Command, gitURL string) error { 45 | _, err := git.PlainOpen("") 46 | 47 | if err == nil { 48 | pwd, _ := os.Getwd() 49 | golog.Error(fmt.Sprintf("Git repo already exists in directory [%s]", pwd)) 50 | return err 51 | } 52 | 53 | // initialise the git 54 | repo, initErr := git.PlainInit("", false) 55 | 56 | if initErr != nil { 57 | golog.Error("A repo already exists") 58 | } 59 | 60 | file, err := os.OpenFile( 61 | ".gitignore", 62 | os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 63 | 0o666, 64 | ) 65 | if err != nil { 66 | golog.Fatal(err) 67 | } 68 | defer file.Close() 69 | 70 | readme, err := os.OpenFile( 71 | "README.md", 72 | os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 73 | 0o666, 74 | ) 75 | if err != nil { 76 | golog.Fatal(err) 77 | } 78 | defer readme.Close() 79 | 80 | // write readme 81 | readme.WriteString(`# Lenses Landscape 82 | 83 | This repo contains Lenses landscape resource descriptions described in yaml files 84 | `) 85 | 86 | wt, err := repo.Worktree() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | wt.Add(".gitignore") 92 | wt.Add("landscape") 93 | wt.Add("README.md") 94 | 95 | bite.PrintInfo(cmd, "Landscape directory structure created") 96 | 97 | if gitURL != "" { 98 | bite.PrintInfo(cmd, "Setting remote to ["+gitURL+"]") 99 | repo.CreateRemote(&config.RemoteConfig{ 100 | Name: "origin", 101 | URLs: []string{gitURL}, 102 | }) 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /pkg/export/schemas_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/MakeNowJust/heredoc" 10 | "github.com/kataras/golog" 11 | "github.com/lensesio/bite" 12 | "github.com/lensesio/lenses-go/v5/pkg" 13 | "github.com/lensesio/lenses-go/v5/pkg/api" 14 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 15 | "github.com/lensesio/lenses-go/v5/pkg/utils" 16 | "github.com/pkg/errors" 17 | "github.com/spf13/cobra" 18 | ) 19 | 20 | // NewExportSchemasCmd to export schemas to yaml 21 | func NewExportSchemasCmd() *cobra.Command { 22 | var name string 23 | 24 | cmd := &cobra.Command{ 25 | Use: "schemas", 26 | Long: heredoc.Doc(` 27 | Export Schemas 28 | 29 | The schemas can be exported to different Kafka Cluster to allow for best GitOps practices. 30 | `), 31 | Example: heredoc.Doc(` 32 | $ lenses-cli export schemas --name="" 33 | `), 34 | SilenceErrors: true, 35 | TraverseChildren: true, 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | client := config.Client 38 | 39 | setExecutionMode(client) 40 | checkFileFlags(cmd) 41 | 42 | err := WriteSchemas(cmd, client, name) 43 | return errors.WithStack(err) 44 | }, 45 | } 46 | 47 | cmd.Flags().StringVarP(&landscapeDir, "dir", "D", ".", "Base directory to export to") 48 | cmd.Flags().StringVarP(&name, "name", "N", "", "Schema Name") 49 | bite.CanPrintJSON(cmd) 50 | bite.CanBeSilent(cmd) 51 | 52 | return cmd 53 | } 54 | 55 | // WriteSchemas to a file 56 | func WriteSchemas(cmd *cobra.Command, client *api.Client, name string) error { 57 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 58 | if name != "" { 59 | return writeSchema(output, client, name) 60 | } 61 | 62 | subjects, err := client.GetSubjects() 63 | if err != nil { 64 | return err 65 | } 66 | 67 | for _, sub := range subjects { 68 | if err := writeSchema(output, client, sub.Name); err != nil { 69 | return err 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func writeSchema(outputFormat string, client *api.Client, name string) error { 76 | 77 | schema, err := client.GetSchema(name) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | // Since schema names can differ in case, we use a hash to ensure uniqueness 83 | // and avoid writing one file over another with the same name 84 | h := fnv.New64a() 85 | h.Write([]byte(schema.Schema)) 86 | 87 | fileName := fmt.Sprintf("%s-%s-%08x.%s", 88 | strings.ToLower(schema.Name), 89 | strings.ToLower(schema.SchemaID), 90 | uint32(h.Sum64()), 91 | strings.ToLower(outputFormat)) 92 | 93 | filePath := filepath.Join(landscapeDir, pkg.SchemasPath, fileName) 94 | 95 | if err := utils.WriteFile(landscapeDir, pkg.SchemasPath, fileName, outputFormat, schema); err != nil { 96 | return err 97 | } 98 | golog.Infof("exported to file '%s'", filePath) 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/export/service_account_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "strings" 7 | 8 | "github.com/kataras/golog" 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewExportServiceAccountsCommand creates `export serviceaccounts` 17 | func NewExportServiceAccountsCommand() *cobra.Command { 18 | var name string 19 | cmd := &cobra.Command{ 20 | Use: "serviceaccounts", 21 | Short: "export serviceaccounts", 22 | Example: `export serviceaccounts`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | checkFileFlags(cmd) 27 | 28 | if err := writeServiceAccounts(cmd, name); err != nil { 29 | golog.Errorf("Error writing service accounts. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 37 | cmd.Flags().StringVar(&name, "name", "", "The service account name to extract") 38 | bite.CanBeSilent(cmd) 39 | bite.CanPrintJSON(cmd) 40 | return cmd 41 | } 42 | 43 | func writeServiceAccounts(cmd *cobra.Command, accountName string) error { 44 | 45 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 46 | if accountName != "" { 47 | svcAcc, err := config.Client.GetServiceAccount(accountName) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | fileName := fmt.Sprintf("svc-accounts-%s.%s", strings.ToLower(svcAcc.Name), strings.ToLower(output)) 53 | return utils.WriteFile(landscapeDir, pkg.ServiceAccountsPath, fileName, output, svcAcc) 54 | } 55 | svcaccs, err := config.Client.GetServiceAccounts() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | // Since service accounts name can differ in case, we use a hash to ensure uniqueness 61 | // and avoid writing one file over another with the same name 62 | h := fnv.New64a() 63 | for _, svcAcc := range svcaccs { 64 | h.Write([]byte(svcAcc.Name)) 65 | 66 | lowerSvcAccName := strings.ToLower(svcAcc.Name) 67 | fileName := fmt.Sprintf("svc-accounts-%s-%08x.%s", lowerSvcAccName, uint32(h.Sum64()), strings.ToLower(output)) 68 | 69 | if accountName != "" && svcAcc.Name == accountName { 70 | return utils.WriteFile(landscapeDir, pkg.ServiceAccountsPath, fileName, output, svcAcc) 71 | } 72 | 73 | err = utils.WriteFile(landscapeDir, pkg.ServiceAccountsPath, fileName, output, svcAcc) 74 | if err != nil { 75 | return err 76 | } 77 | } 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/export/topic_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "strings" 7 | 8 | "github.com/kataras/golog" 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | "github.com/lensesio/lenses-go/v5/pkg/api" 12 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 13 | "github.com/lensesio/lenses-go/v5/pkg/utils" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | // NewExportTopicsCommand creates `export topics` command 18 | func NewExportTopicsCommand() *cobra.Command { 19 | var name string 20 | cmd := &cobra.Command{ 21 | Use: "topics", 22 | Short: "export topics", 23 | Example: `export topics --resource-name my-topic`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | checkFileFlags(cmd) 28 | if err := writeTopics(cmd, config.Client, name); err != nil { 29 | golog.Errorf("Error writing topics. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&landscapeDir, "dir", ".", "Base directory to export to") 37 | cmd.Flags().BoolVar(&dependents, "dependents", false, "Extract dependencies, topics, acls, quotas, alerts") 38 | cmd.Flags().StringVar(&name, "resource-name", "", "The topic name to export") 39 | cmd.Flags().StringVar(&topicExclusions, "exclude", "", "Topics to exclude") 40 | cmd.Flags().StringVar(&prefix, "prefix", "", "Topics with the prefix only") 41 | bite.CanBeSilent(cmd) 42 | bite.CanPrintJSON(cmd) 43 | return cmd 44 | } 45 | 46 | func writeTopics(cmd *cobra.Command, client *api.Client, topicName string) error { 47 | var requests []api.CreateTopicPayload 48 | 49 | raw, err := client.GetTopics() 50 | 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, topic := range raw { 56 | 57 | // don't export control topics 58 | excluded := false 59 | for _, exclude := range systemTopicExclusions { 60 | if strings.HasPrefix(topic.TopicName, exclude) || 61 | strings.Contains(topic.TopicName, "KSTREAM-") || 62 | strings.Contains(topic.TopicName, "_agg_") || 63 | strings.Contains(topic.TopicName, "_sql_store_") { 64 | excluded = true 65 | break 66 | } 67 | } 68 | 69 | if excluded { 70 | continue 71 | } 72 | 73 | // exclude any user defined 74 | excluded = false 75 | for _, exclude := range strings.Split(topicExclusions, ",") { 76 | if topic.TopicName == exclude { 77 | excluded = true 78 | break 79 | } 80 | } 81 | 82 | if excluded { 83 | continue 84 | } 85 | 86 | if prefix != "" && !strings.HasPrefix(topic.TopicName, prefix) { 87 | continue 88 | } 89 | 90 | if topicName != "" && topicName == topic.TopicName { 91 | overrides := getTopicConfigOverrides(topic.Configs) 92 | request := topic.GetTopicAsRequest(overrides) 93 | return writeTopicsAsRequest(cmd, []api.CreateTopicPayload{request}) 94 | } 95 | 96 | overrides := getTopicConfigOverrides(topic.Configs) 97 | requests = append(requests, topic.GetTopicAsRequest(overrides)) 98 | } 99 | 100 | return writeTopicsAsRequest(cmd, requests) 101 | } 102 | 103 | func writeTopicsAsRequest(cmd *cobra.Command, requests []api.CreateTopicPayload) error { 104 | // write topics 105 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 106 | 107 | // Since topics can differ in case, we use a hash to ensure uniqueness 108 | // and avoid writing one file over another with the same name 109 | h := fnv.New64a() 110 | for _, topic := range requests { 111 | 112 | h.Write([]byte(topic.TopicName)) 113 | 114 | fileName := strings.ToLower( 115 | fmt.Sprintf("topic-%s-%08x.%s", 116 | topic.TopicName, 117 | uint32(h.Sum64()), 118 | output)) 119 | 120 | if err := utils.WriteFile(landscapeDir, pkg.TopicsPath, fileName, output, topic); err != nil { 121 | return err 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func getTopicConfigOverrides(configs []api.KV) api.KV { 129 | overrides := make(api.KV) 130 | 131 | for _, kv := range configs { 132 | if val, ok := kv["isDefault"]; ok { 133 | if val.(bool) == false { 134 | var name, value string 135 | 136 | if val, ok := kv["name"]; ok { 137 | name = val.(string) 138 | } 139 | 140 | if val, ok := kv["originalValue"]; ok { 141 | value = val.(string) 142 | } 143 | overrides[name] = value 144 | } 145 | } 146 | } 147 | 148 | return overrides 149 | } 150 | -------------------------------------------------------------------------------- /pkg/export/topicsettings_commands.go: -------------------------------------------------------------------------------- 1 | package export 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/MakeNowJust/heredoc" 8 | "github.com/kataras/golog" 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | "github.com/lensesio/lenses-go/v5/pkg/api" 12 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 13 | "github.com/lensesio/lenses-go/v5/pkg/utils" 14 | "github.com/pkg/errors" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // NewExportTopicSettingsCmd to export topic-settings to yaml 19 | func NewExportTopicSettingsCmd() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "topic-settings", 22 | Long: heredoc.Doc(` 23 | Export Topic Settings 24 | 25 | The settings can be exported to different Kafka Cluster to allow for best GitOps practices. 26 | `), 27 | Example: heredoc.Doc(` 28 | $ lenses-cli export 29 | `), 30 | SilenceErrors: true, 31 | TraverseChildren: true, 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | client := config.Client 34 | 35 | setExecutionMode(client) 36 | checkFileFlags(cmd) 37 | 38 | err := WriteTopicSettings(cmd, client) 39 | return errors.WithStack(err) 40 | 41 | }, 42 | } 43 | 44 | cmd.Flags().StringVarP(&landscapeDir, "dir", "D", ".", "Base directory to export to") 45 | bite.CanPrintJSON(cmd) 46 | bite.CanBeSilent(cmd) 47 | 48 | return cmd 49 | } 50 | 51 | // WriteTopicSettings to a file 52 | func WriteTopicSettings(cmd *cobra.Command, client *api.Client) error { 53 | golog.Infof("Writing topic-settings to [%s]", landscapeDir) 54 | 55 | settings, err := client.GetTopicSettings() 56 | 57 | if err != nil { 58 | return errors.WithStack(err) 59 | } 60 | 61 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 62 | fileName := fmt.Sprintf("topic-settings.%s", strings.ToLower(output)) 63 | return utils.WriteFile(landscapeDir, pkg.TopicSettingsPath, fileName, output, settings) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/import/acl_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/kataras/golog" 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewImportAclsCommand creates `import acls` command 17 | func NewImportAclsCommand() *cobra.Command { 18 | var path string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "acls", 22 | Short: "acls", 23 | Example: `import acls --dir /my-landscape`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | path = fmt.Sprintf("%s/%s", path, pkg.AclsPath) 28 | if err := loadAcls(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to load acls. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 37 | 38 | bite.CanPrintJSON(cmd) 39 | bite.CanBeSilent(cmd) 40 | cmd.Flags().Set("silent", "true") 41 | return cmd 42 | } 43 | 44 | func loadAcls(client *api.Client, cmd *cobra.Command, loadpath string) error { 45 | golog.Infof("Loading acls from [%s]", loadpath) 46 | files, err := utils.FindFiles(loadpath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | knownACLs, err := client.GetACLs() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | for _, file := range files { 57 | var candidateACLs []api.ACL 58 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &candidateACLs); err != nil { 59 | golog.Errorf("Error loading file [%s]", loadpath) 60 | return err 61 | } 62 | 63 | var imported bool 64 | // Import only new ACLs 65 | ImportACLs: 66 | for _, candidateACL := range candidateACLs { 67 | for _, knownACL := range knownACLs { 68 | if reflect.DeepEqual(knownACL, candidateACL) { 69 | continue ImportACLs 70 | } 71 | } 72 | 73 | if err := client.CreateOrUpdateACL(candidateACL); err != nil { 74 | return fmt.Errorf("error creating/updating acl from [%s] [%s]", loadpath, err.Error()) 75 | } 76 | fmt.Fprintf(cmd.OutOrStdout(), "imported ACL [%s] successfully\n", candidateACL) 77 | 78 | imported = true 79 | } 80 | 81 | importFilePath := fmt.Sprintf("%s/%s", loadpath, file.Name()) 82 | if !imported { 83 | fmt.Fprintf(cmd.OutOrStdout(), "no new ACLs have been found for import from %s\n", importFilePath) 84 | } 85 | } 86 | return nil 87 | } 88 | -------------------------------------------------------------------------------- /pkg/import/alert_channels_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // NewImportAlertChannelsCommand handles the CLI sub-command 'import alert-channels' 13 | func NewImportAlertChannelsCommand() *cobra.Command { 14 | var path string 15 | 16 | cmd := &cobra.Command{ 17 | Use: "alert-channels", 18 | Short: "alert-channels", 19 | Example: `import alert-channels --dir `, 20 | SilenceErrors: true, 21 | TraverseChildren: true, 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | 24 | path = fmt.Sprintf("%s/%s", path, "alert-channels") 25 | 26 | err := importChannels(config.Client, cmd, path, "alert", pkg.AlertChannelsPath) 27 | if err != nil { 28 | return fmt.Errorf("error importing alert channels. [%v]", err) 29 | } 30 | return nil 31 | }, 32 | } 33 | 34 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 35 | 36 | bite.CanPrintJSON(cmd) 37 | bite.CanBeSilent(cmd) 38 | cmd.Flags().Set("silent", "true") 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /pkg/import/audit_channels_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // NewImportAuditChannelsCommand handles the CLI sub-command 'import audit-channels' 13 | func NewImportAuditChannelsCommand() *cobra.Command { 14 | var path string 15 | 16 | cmd := &cobra.Command{ 17 | Use: "audit-channels", 18 | Short: "audit-channels", 19 | Example: `import audit-channels --dir `, 20 | SilenceErrors: true, 21 | TraverseChildren: true, 22 | RunE: func(cmd *cobra.Command, args []string) error { 23 | 24 | path = fmt.Sprintf("%s/%s", path, "audit-channels") 25 | 26 | err := importChannels(config.Client, cmd, path, "audit", pkg.AuditChannelsPath) 27 | if err != nil { 28 | return fmt.Errorf("error importing audit channels. [%v]", err) 29 | } 30 | return nil 31 | }, 32 | } 33 | 34 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 35 | 36 | bite.CanPrintJSON(cmd) 37 | bite.CanBeSilent(cmd) 38 | cmd.Flags().Set("silent", "true") 39 | return cmd 40 | } 41 | -------------------------------------------------------------------------------- /pkg/import/channels.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/lensesio/bite" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | "github.com/lensesio/lenses-go/v5/pkg/utils" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func importChannels(client *api.Client, cmd *cobra.Command, loadpath, channelType, channelsPath string) error { 15 | fmt.Fprintf(cmd.OutOrStdout(), "loading %s channels from [%s] directory\n", channelType, loadpath) 16 | 17 | var targetChannels []api.ChannelPayload 18 | files, err := utils.FindFiles(loadpath) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | for _, file := range files { 24 | var targetChannel api.ChannelPayload 25 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &targetChannel); err != nil { 26 | return fmt.Errorf("error loading file [%s]", loadpath) 27 | } 28 | targetChannels = append(targetChannels, targetChannel) 29 | } 30 | 31 | channels, err := client.GetChannels(channelsPath, 1, 99999, "name", "asc", "", "") 32 | if err != nil { 33 | return err 34 | } 35 | 36 | var sourceChannels []api.ChannelPayload 37 | for _, chann := range channels.Values { 38 | var channForExport api.ChannelPayload 39 | channΑsJSON, _ := json.Marshal(chann) 40 | json.Unmarshal(channΑsJSON, &channForExport) 41 | sourceChannels = append(sourceChannels, channForExport) 42 | } 43 | 44 | // Check for duplicates lacking server-side implementation 45 | for _, targetChannel := range targetChannels { 46 | found := false 47 | 48 | for _, sourceChannel := range sourceChannels { 49 | if reflect.DeepEqual(targetChannel, sourceChannel) { 50 | found = true 51 | } 52 | } 53 | 54 | if found { 55 | continue 56 | } 57 | 58 | if err := client.CreateChannel(targetChannel, channelsPath); err != nil { 59 | return fmt.Errorf("error importing %s channel [%v]", channelType, targetChannel) 60 | } 61 | fmt.Fprintf(cmd.OutOrStdout(), "%s channel [%s] successfully imported\n", channelType, targetChannel.Name) 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/import/connection_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewImportConnectionsCommand creates `import connections` command 16 | func NewImportConnectionsCommand() *cobra.Command { 17 | var path string 18 | 19 | cmd := &cobra.Command{ 20 | Use: "connections", 21 | Short: "Import from a directory named connections", 22 | Example: `import connections --dir lenses_export`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | 27 | path = fmt.Sprintf("%s/%s", path, pkg.ConnectionsFilePath) 28 | if err := loadConnections(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to import connections. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import from") 37 | 38 | bite.CanPrintJSON(cmd) 39 | _ = bite.CanBeSilent(cmd) 40 | return cmd 41 | } 42 | 43 | func loadConnections(client *api.Client, cmd *cobra.Command, loadpath string) error { 44 | golog.Infof("Loading connections from [%s]", loadpath) 45 | 46 | currentConnections, err := client.GetConnections() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | files, err := utils.FindFiles(loadpath) 52 | if err != nil { 53 | return err 54 | } 55 | connTemplates, err := config.Client.GetConnectionTemplates() 56 | if err != nil { 57 | golog.Errorf("Error getting connection templates [%s]", err.Error()) 58 | return err 59 | } 60 | 61 | for _, file := range files { 62 | var connection api.Connection 63 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &connection); err != nil { 64 | golog.Errorf("Error loading file [%s]", loadpath) 65 | return err 66 | } 67 | 68 | found := false 69 | for _, currentConn := range currentConnections { 70 | if currentConn.Name == connection.Name { 71 | found = true 72 | golog.Infof("Updating connection [%s]", connection.Name) 73 | if err := config.Client.UpdateConnection(currentConn.Name, connection.Name, "", connection.Configuration, connection.Tags); err != nil { 74 | golog.Errorf("Error updating connection [%s]. [%s]", connection.Name, err.Error()) 75 | return err 76 | } 77 | golog.Infof("Updated connection [%s]", connection.Name) 78 | continue 79 | } 80 | } 81 | if !found { 82 | golog.Infof("Creating new connection [%s]", file.Name()) 83 | var connTemplateName string 84 | for _, connTemplate := range connTemplates { 85 | if connTemplate.Name == connection.TemplateName { 86 | connTemplateName = connTemplate.Name 87 | break 88 | } 89 | } 90 | if connTemplateName == "" { 91 | golog.Errorf("Connection template %s for connection %s not found [%s]", connection.TemplateName, connection.Name, err.Error()) 92 | return err 93 | } 94 | if err := config.Client.CreateConnection(connection.Name, connTemplateName, "", connection.Configuration, connection.Tags); err != nil { 95 | golog.Errorf("Error creating connection [%s] from [%s] [%s]", connection.Name, loadpath, err.Error()) 96 | return err 97 | } 98 | golog.Infof("Created connection [%s]", connection.Name) 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/import/connector_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/lensesio/bite" 10 | "github.com/lensesio/lenses-go/v5/pkg" 11 | "github.com/lensesio/lenses-go/v5/pkg/api" 12 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 13 | "github.com/lensesio/lenses-go/v5/pkg/utils" 14 | "github.com/matryer/try" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // NewImportConnectorsCommand create `import connectors` 19 | func NewImportConnectorsCommand() *cobra.Command { 20 | var path string 21 | var interval string 22 | var retries int 23 | 24 | cmd := &cobra.Command{ 25 | Use: "connectors", 26 | Short: "connectors", 27 | Example: `import connectors --dir /my-landscape`, 28 | SilenceErrors: true, 29 | TraverseChildren: true, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | path = fmt.Sprintf("%s/%s", path, pkg.ConnectorsPath) 32 | if err := loadConnectors(config.Client, cmd, path, interval, retries); err != nil { 33 | return fmt.Errorf("failed to load connectors. [%s]", err.Error()) 34 | } 35 | return nil 36 | }, 37 | } 38 | 39 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 40 | cmd.Flags().StringVar(&interval, "interval", "0s", "Time between importing two connectors") 41 | cmd.Flags().IntVar(&retries, "retries", 5, "Number of HTTP retries before exiting") 42 | 43 | bite.CanPrintJSON(cmd) 44 | bite.CanBeSilent(cmd) 45 | cmd.Flags().Set("silent", "true") 46 | return cmd 47 | } 48 | 49 | func loadConnectors(client *api.Client, cmd *cobra.Command, loadpath, interval string, retries int) error { 50 | intervalDuration, err := time.ParseDuration(interval) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | fmt.Fprintf(cmd.OutOrStdout(), "loading connectors from [%s]\n", loadpath) 56 | files, err := utils.FindFiles(loadpath) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | for _, file := range files { 62 | var connector api.CreateUpdateConnectorPayload 63 | if err := load(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &connector); err != nil { 64 | return err 65 | } 66 | 67 | connectors, err := client.GetConnectors(connector.ClusterName) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | existsOrUpdated := false 73 | for _, name := range connectors { 74 | if name == connector.Name { 75 | var c api.Connector 76 | err := try.Do(func(attempt int) (bool, error) { 77 | var err error 78 | c, err = client.GetConnector(connector.ClusterName, connector.Name) 79 | if err != nil { 80 | time.Sleep(intervalDuration) 81 | } 82 | return attempt < retries, err 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if !reflect.DeepEqual(c.Config, connector.Config) { 89 | err := try.Do(func(attempt int) (bool, error) { 90 | var err error 91 | _, err = client.UpdateConnector(connector.ClusterName, connector.Name, connector.Config) 92 | if err != nil { 93 | fmt.Fprintf(cmd.OutOrStdout(), "failed to update connector '%s' [attempt num. %s]\n", connector.Name, strconv.Itoa(attempt)) 94 | time.Sleep(intervalDuration) 95 | } 96 | return attempt < retries, err 97 | }) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | fmt.Fprintf(cmd.OutOrStdout(), "updated connector config for cluster [%s], connector [%s]\n", connector.ClusterName, connector.Name) 103 | 104 | if err != nil { 105 | return err 106 | } 107 | } 108 | 109 | existsOrUpdated = true 110 | break 111 | } 112 | } 113 | 114 | if existsOrUpdated { 115 | continue 116 | } 117 | 118 | err = try.Do(func(attempt int) (bool, error) { 119 | var err error 120 | _, err = client.CreateConnector(connector.ClusterName, connector.Name, connector.Config) 121 | if err != nil { 122 | fmt.Fprintf(cmd.OutOrStdout(), "failed to create connector '%s' [attempt num. %s]\n", connector.Name, strconv.Itoa(attempt)) 123 | time.Sleep(intervalDuration) 124 | } 125 | return attempt < retries, err 126 | }) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | fmt.Fprintf(cmd.OutOrStdout(), "created connector [%s] successfully!\n", connector.Name) 132 | time.Sleep(intervalDuration) 133 | } 134 | 135 | return nil 136 | } 137 | -------------------------------------------------------------------------------- /pkg/import/group_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewImportGroupsCommand creates `import groups` command 16 | func NewImportGroupsCommand() *cobra.Command { 17 | var path string 18 | 19 | cmd := &cobra.Command{ 20 | Use: "groups", 21 | Short: "groups", 22 | Example: `import groups --dir groups`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | 27 | path = fmt.Sprintf("%s/%s", path, pkg.GroupsPath) 28 | if err := loadGroups(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to load user groups. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 37 | 38 | bite.CanPrintJSON(cmd) 39 | bite.CanBeSilent(cmd) 40 | cmd.Flags().Set("silent", "true") 41 | return cmd 42 | } 43 | 44 | func loadGroups(client *api.Client, cmd *cobra.Command, loadpath string) error { 45 | golog.Infof("Loading user groups from [%s]", loadpath) 46 | files, err := utils.FindFiles(loadpath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | currentGroups, err := client.GetGroups() 52 | 53 | if err != nil { 54 | return err 55 | } 56 | for _, file := range files { 57 | 58 | var group api.Group 59 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &group); err != nil { 60 | golog.Errorf("Error loading file [%s]", loadpath) 61 | return err 62 | } 63 | 64 | found := false 65 | for _, g := range currentGroups { 66 | if g.Name == group.Name { 67 | found = true 68 | payload := &api.Group{ 69 | Name: group.Name, 70 | Description: group.Description, 71 | Namespaces: group.Namespaces, 72 | ScopedPermissions: group.ScopedPermissions, 73 | AdminPermissions: group.AdminPermissions, 74 | ConnectClustersPermissions: group.ConnectClustersPermissions, 75 | } 76 | 77 | if err := config.Client.UpdateGroup(payload); err != nil { 78 | golog.Errorf("Error updating user group [%s]. [%s]", group.Name, err.Error()) 79 | return err 80 | } 81 | golog.Infof("Updated group [%s]", group.Name) 82 | } 83 | } 84 | 85 | if found { 86 | continue 87 | } 88 | 89 | if err := client.CreateGroup(&group); err != nil { 90 | golog.Errorf("Error creating user group [%s] from [%s] [%s]", group.Name, loadpath, err.Error()) 91 | return err 92 | } 93 | golog.Infof("Created user group [%s]", group.Name) 94 | 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/import/import_group_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "github.com/lensesio/bite" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // NewImportGroupCommand creates `import` command 9 | func NewImportGroupCommand() *cobra.Command { 10 | cmd := &cobra.Command{ 11 | Use: "import", 12 | Short: "import a landscape", 13 | Example: ` 14 | import acls --dir my-acls-dir 15 | import alert-settings --dir my-acls-dir 16 | import connectors --dir my-acls-dir 17 | import connections --dir my-acls-dir 18 | import processors --dir my-acls-dir 19 | import quota --dir my-acls-dir 20 | import schemas --dir my-acls-dir 21 | import topics --dir my-acls-dir 22 | import policies --dir my-acls-dir 23 | import groups --dir groups 24 | import topic-settings --dir topic-settings 25 | import serviceaccounts --dir serviceaccounts`, 26 | SilenceErrors: true, 27 | TraverseChildren: true, 28 | } 29 | 30 | cmd.AddCommand(NewImportAclsCommand()) 31 | cmd.AddCommand(NewImportAlertSettingsCommand()) 32 | cmd.AddCommand(NewImportConnectionsCommand()) 33 | cmd.AddCommand(NewImportConnectorsCommand()) 34 | cmd.AddCommand(NewImportProcessorsCommand()) 35 | cmd.AddCommand(NewImportQuotasCommand()) 36 | cmd.AddCommand(NewImportTopicsCommand()) 37 | cmd.AddCommand(NewImportPoliciesCommand()) 38 | cmd.AddCommand(NewImportGroupsCommand()) 39 | cmd.AddCommand(NewImportServiceAccountsCommand()) 40 | cmd.AddCommand(NewImportAlertChannelsCommand()) 41 | cmd.AddCommand(ImportTopicSettingsCmd()) 42 | cmd.AddCommand(NewImportAuditChannelsCommand()) 43 | cmd.AddCommand(NewImportSchemasCmd()) 44 | 45 | return cmd 46 | } 47 | 48 | func load(cmd *cobra.Command, path string, data interface{}) error { 49 | return bite.TryReadFile(path, data) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/import/policy_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewImportPoliciesCommand creates `import policies` ommand 16 | func NewImportPoliciesCommand() *cobra.Command { 17 | var path string 18 | 19 | cmd := &cobra.Command{ 20 | Use: "policies", 21 | Short: "policies", 22 | Example: `import policies --dir /my-landscape`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | path = fmt.Sprintf("%s/%s", path, pkg.PoliciesPath) 27 | if err := loadPolicies(config.Client, cmd, path); err != nil { 28 | golog.Errorf("Failed to load policies. [%s]", err.Error()) 29 | return err 30 | } 31 | return nil 32 | }, 33 | } 34 | 35 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 36 | 37 | bite.CanPrintJSON(cmd) 38 | bite.CanBeSilent(cmd) 39 | cmd.Flags().Set("silent", "true") 40 | return cmd 41 | } 42 | 43 | func loadPolicies(client *api.Client, cmd *cobra.Command, loadpath string) error { 44 | golog.Infof("Loading data policies from [%s]", loadpath) 45 | files, err := utils.FindFiles(loadpath) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | polices, err := client.GetPolicies() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, file := range files { 56 | 57 | var policy api.DataPolicyRequest 58 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &policy); err != nil { 59 | return err 60 | } 61 | 62 | found := false 63 | 64 | for _, p := range polices { 65 | if p.Name == policy.Name { 66 | found = true 67 | 68 | payload := api.DataPolicyUpdateRequest{ 69 | ID: p.ID, 70 | Name: p.Name, 71 | Category: p.Category, 72 | ImpactType: p.ImpactType, 73 | Obfuscation: p.Obfuscation, 74 | Fields: p.Fields, 75 | } 76 | 77 | if err := client.UpdatePolicy(payload); err != nil { 78 | golog.Errorf("Error updating data policy [%s]. [%s]", p.Name, err.Error()) 79 | return err 80 | } 81 | golog.Infof("Updated policy [%s]", p.Name) 82 | } 83 | } 84 | 85 | if !found { 86 | if err := client.CreatePolicy(policy); err != nil { 87 | golog.Errorf("Error creating data policy [%s]. [%s]", policy.Name, err.Error()) 88 | return err 89 | } 90 | golog.Infof("Created data policy [%s]", policy.Name) 91 | } 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/import/processor_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lensesio/bite" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | "github.com/lensesio/lenses-go/v5/pkg/api" 9 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 10 | "github.com/lensesio/lenses-go/v5/pkg/utils" 11 | 12 | "github.com/kataras/golog" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewImportProcessorsCommand import processors command 17 | func NewImportProcessorsCommand() *cobra.Command { 18 | var path string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "processors", 22 | Short: "processors", 23 | Example: `import processors --dir /my-landscape`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | path = fmt.Sprintf("%s/%s", path, pkg.SQLPath) 28 | if err := loadProcessors(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to load processors. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 37 | 38 | bite.CanPrintJSON(cmd) 39 | bite.CanBeSilent(cmd) 40 | cmd.Flags().Set("silent", "true") 41 | return cmd 42 | } 43 | 44 | func loadProcessors(client *api.Client, cmd *cobra.Command, loadpath string) error { 45 | golog.Infof("Loading processors from [%s]", loadpath) 46 | files, err := utils.FindFiles(loadpath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | processors, err := client.GetProcessors() 52 | if err != nil { 53 | golog.Errorf("Failed to retrieve processors. [%s]", err.Error()) 54 | } 55 | 56 | IterateImportFiles: 57 | for _, file := range files { 58 | 59 | var processor api.CreateProcessorFilePayload 60 | 61 | if err := load(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &processor); err != nil { 62 | return err 63 | } 64 | 65 | for _, p := range processors.Streams { 66 | if processor.Name != p.Name || 67 | processor.ClusterName != p.ClusterName || 68 | processor.Namespace != p.Namespace { 69 | continue 70 | } 71 | 72 | if processor.Runners == p.Runners { 73 | golog.Warnf("Processor [%s] from file [%s/%s] already exists", p.ID, loadpath, file.Name()) 74 | // Iterate next file from 'files' 75 | continue IterateImportFiles 76 | } 77 | // scale 78 | if err := client.UpdateProcessorRunners(p.ID, processor.Runners); err != nil { 79 | golog.Errorf("Error scaling processor [%s] from file [%s/%s]. [%s]", p.ID, loadpath, file.Name(), err.Error()) 80 | return err 81 | } 82 | golog.Infof("Scaled processor [%s] from file [%s/%s] from [%d] to [%d]", p.ID, loadpath, file.Name(), p.Runners, processor.Runners) 83 | return nil 84 | 85 | } 86 | 87 | if err := client.CreateProcessor( 88 | processor.Name, 89 | processor.SQL, 90 | processor.Runners, 91 | processor.ClusterName, 92 | processor.Namespace, 93 | processor.Pipeline, 94 | processor.ProcessorID); err != nil { 95 | 96 | golog.Errorf("Error creating processor from file [%s/%s]. [%s]", loadpath, file.Name(), err.Error()) 97 | return err 98 | } 99 | 100 | golog.Infof("Created processor from [%s/%s]", loadpath, file.Name()) 101 | } 102 | 103 | return nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/import/quota_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | quotapkg "github.com/lensesio/lenses-go/v5/pkg/quota" 12 | "github.com/lensesio/lenses-go/v5/pkg/utils" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // NewImportQuotasCommand creates `import quotas` command 17 | func NewImportQuotasCommand() *cobra.Command { 18 | var path string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "quotas", 22 | Short: "quotas", 23 | Example: `import quotas --dir /my-landscape`, 24 | SilenceErrors: true, 25 | TraverseChildren: true, 26 | RunE: func(cmd *cobra.Command, args []string) error { 27 | path = fmt.Sprintf("%s/%s", path, pkg.QuotasPath) 28 | if err := loadQuotas(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to load quotas. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 37 | 38 | bite.CanPrintJSON(cmd) 39 | bite.CanBeSilent(cmd) 40 | cmd.Flags().Set("silent", "true") 41 | return cmd 42 | } 43 | 44 | func loadQuotas(client *api.Client, cmd *cobra.Command, loadpath string) error { 45 | golog.Infof("Loading quotas from [%s]", loadpath) 46 | files, err := utils.FindFiles(loadpath) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | lensesQuotas, err := client.GetQuotas() 52 | var lensesReq []api.CreateQuotaPayload 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | for _, lq := range lensesQuotas { 59 | lensesReq = append(lensesReq, lq.GetQuotaAsRequest()) 60 | } 61 | 62 | for _, file := range files { 63 | var quotas []api.CreateQuotaPayload 64 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), "as); err != nil { 65 | golog.Errorf("Error loading file [%s]", loadpath) 66 | return err 67 | } 68 | 69 | for _, quota := range quotas { 70 | 71 | found := false 72 | for _, lq := range lensesReq { 73 | if quota.ClientID == lq.ClientID && 74 | quota.QuotaType == lq.QuotaType && 75 | quota.User == lq.User && 76 | quota.Config.ConsumerByteRate == lq.Config.ConsumerByteRate && 77 | quota.Config.ProducerByteRate == lq.Config.ProducerByteRate && 78 | quota.Config.RequestPercentage == lq.Config.RequestPercentage { 79 | found = true 80 | } 81 | } 82 | 83 | if found { 84 | continue 85 | } 86 | 87 | if quota.QuotaType == string(api.QuotaEntityClient) || 88 | quota.QuotaType == string(api.QuotaEntityClients) || 89 | quota.QuotaType == string(api.QuotaEntityClientsDefault) { 90 | if err := quotapkg.CreateQuotaForClients(cmd, client, quota); err != nil { 91 | golog.Errorf("Error creating/updating quota type [%s], client [%s], user [%s] from [%s]. [%s]", 92 | quota.QuotaType, quota.ClientID, quota.User, loadpath, err.Error()) 93 | return err 94 | } 95 | 96 | golog.Infof("Created/updated quota type [%s], client [%s], user [%s] from [%s]", 97 | quota.QuotaType, quota.ClientID, quota.User, loadpath) 98 | continue 99 | 100 | } 101 | 102 | if err := quotapkg.CreateQuotaForUsers(cmd, client, quota); err != nil { 103 | golog.Errorf("Error creating/updating quota type [%s], client [%s], user [%s] from [%s]. [%s]", 104 | quota.QuotaType, quota.ClientID, quota.User, loadpath, err.Error()) 105 | return err 106 | } 107 | 108 | golog.Infof("Created/updated quota type [%s], client [%s], user [%s] from [%s]", 109 | quota.QuotaType, quota.ClientID, quota.User, loadpath) 110 | } 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/import/schemas_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/lenses-go/v5/pkg" 8 | "github.com/lensesio/lenses-go/v5/pkg/api" 9 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 10 | "github.com/lensesio/lenses-go/v5/pkg/utils" 11 | 12 | "github.com/pkg/errors" 13 | 14 | "github.com/MakeNowJust/heredoc" 15 | "github.com/lensesio/bite" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | // NewImportSchemasCmd to read schemas from files 20 | func NewImportSchemasCmd() *cobra.Command { 21 | var path string 22 | var name string 23 | 24 | cmd := &cobra.Command{ 25 | Use: "schemas", 26 | Long: heredoc.Doc(` 27 | Import Schemas 28 | 29 | The settings can be import from a different Kafka Cluster to allow for best GitOps practices. 30 | `), 31 | Example: heredoc.Doc(` 32 | $ lenses-cli import schemas --dir /directory 33 | `), 34 | SilenceErrors: true, 35 | TraverseChildren: true, 36 | RunE: func(cmd *cobra.Command, args []string) error { 37 | path = fmt.Sprintf("%s/%s/", path, pkg.SchemasPath) 38 | err := ReadSchemas(config.Client, cmd, path) 39 | return errors.Wrapf(err, "Failed to read schemas") 40 | }, 41 | } 42 | 43 | cmd.Flags().StringVarP(&path, "dir", "D", ".", "Base directory to import") 44 | cmd.Flags().StringVarP(&name, "name", "N", "", "Imported Schema Name") 45 | 46 | bite.CanPrintJSON(cmd) 47 | bite.CanBeSilent(cmd) 48 | cmd.Flags().Set("silent", "true") 49 | 50 | return cmd 51 | } 52 | 53 | // ReadSchemas to read the files and import one by one 54 | func ReadSchemas(client *api.Client, cmd *cobra.Command, filePath string) error { 55 | files, err := utils.FindFiles(filePath) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | for _, file := range files { 61 | var schema struct { 62 | Name string `yaml:"name"` 63 | Format string `json:"format" yaml:"format"` 64 | Schema string `json:"schema" yaml:"schema"` 65 | } 66 | 67 | fileName := file.Name() 68 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", filePath, file.Name()), &schema); err != nil { 69 | return errors.Wrapf(err, "Could not load file [%s]", fileName) 70 | } 71 | 72 | if err := client.WriteSchema(schema.Name, api.WriteSchemaReq{ 73 | Format: schema.Format, 74 | Schema: schema.Schema, 75 | }); err != nil { 76 | return errors.Wrapf(err, "Could not import Schemas [%s]", fileName) 77 | } 78 | golog.Infof("imported schema from file '%s'", filePath) 79 | } 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /pkg/import/service_account_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewImportServiceAccountsCommand creates `import serviceaccounts` command 16 | func NewImportServiceAccountsCommand() *cobra.Command { 17 | var path string 18 | 19 | cmd := &cobra.Command{ 20 | Use: "serviceaccounts", 21 | Short: "serviceaccounts", 22 | Example: `import serviceaccounts --dir users`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | 27 | path = fmt.Sprintf("%s/%s", path, pkg.ServiceAccountsPath) 28 | if err := loadServiceAccounts(config.Client, cmd, path); err != nil { 29 | golog.Errorf("Failed to load user groups. [%s]", err.Error()) 30 | return err 31 | } 32 | return nil 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 37 | 38 | bite.CanPrintJSON(cmd) 39 | return cmd 40 | } 41 | 42 | func loadServiceAccounts(client *api.Client, cmd *cobra.Command, loadpath string) error { 43 | golog.Infof("Loading service accounts from [%s]", loadpath) 44 | files, err := utils.FindFiles(loadpath) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | currentSvcAccs, err := client.GetServiceAccounts() 50 | 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, file := range files { 56 | 57 | var svcacc api.ServiceAccount 58 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &svcacc); err != nil { 59 | golog.Errorf("Error loading file [%s]", loadpath) 60 | return err 61 | } 62 | 63 | found := false 64 | for _, sva := range currentSvcAccs { 65 | if sva.Name == svcacc.Name { 66 | found = true 67 | 68 | payload := &api.ServiceAccount{ 69 | Name: svcacc.Name, 70 | Owner: svcacc.Owner, 71 | Groups: svcacc.Groups, 72 | } 73 | 74 | if err := config.Client.UpdateServiceAccount(payload); err != nil { 75 | golog.Errorf("Error updating service account [%s]. [%s]", svcacc.Name, err.Error()) 76 | return err 77 | } 78 | golog.Infof("Updated service account [%s]", svcacc.Name) 79 | } 80 | } 81 | 82 | if found { 83 | continue 84 | } 85 | 86 | payload, err := client.CreateServiceAccount(&svcacc) 87 | if err != nil { 88 | golog.Errorf("Error creating service account [%s] from [%s] [%s]", svcacc.Name, loadpath, err.Error()) 89 | return err 90 | } 91 | golog.Infof("Created service account [%s], Token:[%s]", svcacc.Name, payload.Token) 92 | } 93 | 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/import/topic_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/golog" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewImportTopicsCommand creates `import topics` command 16 | func NewImportTopicsCommand() *cobra.Command { 17 | var path string 18 | 19 | cmd := &cobra.Command{ 20 | Use: "topics", 21 | Short: "topics", 22 | Example: `import topics --dir /my-landscape`, 23 | SilenceErrors: true, 24 | TraverseChildren: true, 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | path = fmt.Sprintf("%s/%s", path, pkg.TopicsPath) 27 | if err := loadTopics(config.Client, cmd, path); err != nil { 28 | return err 29 | } 30 | return nil 31 | }, 32 | } 33 | 34 | cmd.Flags().StringVar(&path, "dir", ".", "Base directory to import") 35 | 36 | bite.CanPrintJSON(cmd) 37 | bite.CanBeSilent(cmd) 38 | cmd.Flags().Set("silent", "true") 39 | return cmd 40 | } 41 | 42 | func loadTopics(client *api.Client, cmd *cobra.Command, loadpath string) error { 43 | golog.Infof("Loading topics from [%s]", loadpath) 44 | 45 | remoteTopics, err := client.GetTopics() 46 | if err != nil { 47 | golog.Errorf("Error retrieving topics [%s]", err.Error()) 48 | return err 49 | } 50 | 51 | // create a map out of Lenses existing topics where key is topic name 52 | // and value is a custom object of the topic's partition num and configs that is simplyfied for comparing purposes 53 | // i.e. from a slice of maps to a single map where key is the config name and value is its original value 54 | // (all other keys are disregarded, e.g. "defaultValue", "documentation", "isDefault", etc.) 55 | type simplyfiedTopicPayload struct { 56 | partitions int 57 | configs map[string]string 58 | } 59 | simplyfiedRemoteTopics := make(map[string]simplyfiedTopicPayload) 60 | 61 | for _, topic := range remoteTopics { 62 | configMap := make(map[string]string) 63 | for _, conf := range topic.Configs { 64 | configMap[fmt.Sprintf("%v", conf["name"])] = fmt.Sprintf("%v", conf["originalValue"]) 65 | } 66 | simplyfiedRemoteTopics[topic.TopicName] = simplyfiedTopicPayload{ 67 | topic.Partitions, 68 | configMap, 69 | } 70 | } 71 | 72 | files, err := utils.FindFiles(loadpath) 73 | if err != nil { 74 | return err 75 | } 76 | for _, file := range files { 77 | var topicFromFile api.CreateTopicPayload 78 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", loadpath, file.Name()), &topicFromFile); err != nil { 79 | return err 80 | } 81 | 82 | // if target imported topic exists then compare it with the instance found on the server 83 | if topicValue, ok := simplyfiedRemoteTopics[topicFromFile.TopicName]; ok { 84 | 85 | // compare the partition values 86 | if topicValue.partitions != topicFromFile.Partitions { 87 | if err := client.UpdateTopicPartitions(topicFromFile.TopicName, topicFromFile.Partitions); err != nil { 88 | return err 89 | } 90 | 91 | golog.Infof("Updated topic '%s' partitions with new value '%v'", topicFromFile.TopicName, topicFromFile.Partitions) 92 | } 93 | 94 | // compare the config from imported file with the config on the remote server 95 | for k, v := range topicFromFile.Configs { 96 | // If at least one config value is different then perform a single PUT on all config 97 | if v != topicValue.configs[k] { 98 | if err := client.UpdateTopicConfig(topicFromFile.TopicName, []api.KV{topicFromFile.Configs}); err != nil { 99 | return err 100 | } 101 | 102 | golog.Infof("Updated topic '%s' config", topicFromFile.TopicName) 103 | break 104 | } 105 | } 106 | } else { 107 | // If topic doesn't exist on the remote server then import it as new 108 | if err := client.CreateTopic(topicFromFile.TopicName, topicFromFile.Replication, topicFromFile.Partitions, topicFromFile.Configs); err != nil { 109 | return err 110 | } 111 | 112 | golog.Infof("Created topic [%s]", topicFromFile.TopicName) 113 | } 114 | } 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /pkg/import/topicsettings_commands.go: -------------------------------------------------------------------------------- 1 | package imports 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/MakeNowJust/heredoc" 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg" 9 | "github.com/lensesio/lenses-go/v5/pkg/api" 10 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 11 | "github.com/lensesio/lenses-go/v5/pkg/utils" 12 | "github.com/pkg/errors" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // ImportTopicSettingsCmd to read topic-settings from files 17 | func ImportTopicSettingsCmd() *cobra.Command { 18 | var path string 19 | 20 | cmd := &cobra.Command{ 21 | Use: "topic-settings", 22 | Long: heredoc.Doc(` 23 | Import Topic Settings 24 | 25 | The settings can be import from a different Kafka Cluster to allow for best GitOps practices. 26 | `), 27 | Example: heredoc.Doc(` 28 | $ lenses-cli import topic-settings --dir /directory 29 | `), 30 | SilenceErrors: true, 31 | TraverseChildren: true, 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | path = fmt.Sprintf("%s/%s", path, pkg.TopicSettingsPath) 34 | err := ReadTopicSettings(config.Client, cmd, path) 35 | return errors.Wrapf(err, utils.RED("Failed to read topic-settings")) 36 | }, 37 | } 38 | 39 | cmd.Flags().StringVarP(&path, "dir", "D", ".", "Base directory to import") 40 | 41 | bite.CanPrintJSON(cmd) 42 | bite.CanBeSilent(cmd) 43 | cmd.Flags().Set("silent", "true") 44 | 45 | return cmd 46 | } 47 | 48 | // ReadTopicSettings to read for each file and pass the topic-settings 49 | func ReadTopicSettings(client *api.Client, cmd *cobra.Command, filePath string) error { 50 | files, err := utils.FindFiles(filePath) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, file := range files { 56 | var settings api.TopicSettingsRequest 57 | var fileName = file.Name() 58 | if err := bite.LoadFile(cmd, fmt.Sprintf("%s/%s", filePath, file.Name()), &settings); err != nil { 59 | return errors.Wrapf(err, utils.RED("Could not load file [%s]"), fileName) 60 | } 61 | 62 | if err := client.UpdateTopicSettings(settings); err != nil { 63 | return errors.Wrapf(err, utils.RED("Could not update Topic Settings [%s]"), fileName) 64 | } 65 | fmt.Printf(utils.GREEN("✓ Imported Topic Settings from [%s]\n"), fileName) 66 | } 67 | 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/license/commands.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | 11 | "github.com/kataras/golog" 12 | "github.com/lensesio/bite" 13 | "github.com/lensesio/lenses-go/v5/pkg/api" 14 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | // NewLicenseGroupCommand creates the `license` command 19 | func NewLicenseGroupCommand() *cobra.Command { 20 | cmd := &cobra.Command{ 21 | Use: "license", 22 | Short: "View or update Lenses license", 23 | Example: `lenses-cli license get 24 | lenses-cli license update --license-file `, 25 | } 26 | 27 | cmd.AddCommand(NewLicenseGetCommand()) 28 | cmd.AddCommand(NewLicenseUpdateCommand()) 29 | return cmd 30 | } 31 | 32 | // NewLicenseGetCommand creates the `license get` subcommand 33 | func NewLicenseGetCommand() *cobra.Command { 34 | cmd := &cobra.Command{ 35 | Use: "get", 36 | Short: "Print information about the active Lenses license", 37 | Example: `lenses-cli license get`, 38 | RunE: func(cmd *cobra.Command, args []string) error { 39 | lc, err := config.Client.GetLicenseInfo() 40 | if err != nil { 41 | return err 42 | } 43 | 44 | return bite.PrintObject(cmd, lc) 45 | }, 46 | } 47 | 48 | bite.CanPrintJSON(cmd) 49 | 50 | return cmd 51 | } 52 | 53 | // NewLicenseUpdateCommand creates the `license update` subcommand 54 | func NewLicenseUpdateCommand() *cobra.Command { 55 | var licenseFilePath string 56 | 57 | cmd := &cobra.Command{ 58 | Use: "update", 59 | Short: "Update Lenses license by passing a JSON file", 60 | Example: `lenses-cli license update --file my-license.json`, 61 | RunE: func(cmd *cobra.Command, args []string) error { 62 | 63 | license, err := LoadLicenseFile(licenseFilePath) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | err = config.Client.UpdateLicense(license) 69 | if err != nil { 70 | return err 71 | } 72 | fmt.Fprintln(cmd.OutOrStdout(), "License updated") 73 | return nil 74 | }, 75 | } 76 | 77 | cmd.Flags().StringVar(&licenseFilePath, "file", "", "The file path of the license file") 78 | cmd.MarkFlagRequired("file") 79 | return cmd 80 | } 81 | 82 | // LoadLicenseFile loads a file from filesystem and pass it for parsing 83 | func LoadLicenseFile(licenseFilePath string) (api.License, error) { 84 | 85 | licenseFile, err := os.Open(licenseFilePath) 86 | defer licenseFile.Close() 87 | if err != nil { 88 | golog.Errorf("Failed to load license file", err.Error()) 89 | return api.License{}, err 90 | } 91 | return ParseLicenseFile(licenseFile) 92 | } 93 | 94 | // ParseLicenseFile unmarshalls the license file into a known struct 95 | func ParseLicenseFile(licenseFile io.Reader) (api.License, error) { 96 | var license api.License 97 | licenseFileAsBytes, _ := ioutil.ReadAll(licenseFile) 98 | err := json.Unmarshal(licenseFileAsBytes, &license) 99 | if err != nil { 100 | invalidLicenseErr := errors.New("invalid Lenses license JSON file") 101 | golog.Errorf(invalidLicenseErr.Error(), err.Error()) 102 | return license, invalidLicenseErr 103 | } 104 | 105 | if (license == api.License{}) { 106 | emptyLicenseErr := errors.New("empty Lenses license file") 107 | return license, emptyLicenseErr 108 | } 109 | return license, nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/logs/commands.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "github.com/lensesio/bite" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | "github.com/lensesio/lenses-go/v5/pkg/utils" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // NewLogsCommandGroup creates `logs` command 11 | func NewLogsCommandGroup() *cobra.Command { 12 | 13 | root := &cobra.Command{ 14 | Use: "logs", 15 | Short: "List the info or metrics logs", 16 | Example: `logs info`, 17 | SilenceErrors: true, 18 | TraverseChildren: true, 19 | } 20 | 21 | asObjects := root.PersistentFlags().Bool("no-text", false, "no-text will print as objects, defaults to false") 22 | logsInfoSubComamnd := NewGetLogsInfoCommand(asObjects) 23 | // if not `info $subCommand` passed then by-default show the info logs. 24 | root.RunE = logsInfoSubComamnd.RunE 25 | 26 | root.AddCommand(logsInfoSubComamnd) 27 | root.AddCommand(NewGetLogsMetricsCommand(asObjects)) 28 | return root 29 | } 30 | 31 | // NewGetLogsInfoCommand creates `logs info` command 32 | func NewGetLogsInfoCommand(asObjects *bool) *cobra.Command { 33 | cmd := &cobra.Command{ 34 | Use: "info", 35 | Short: "List the latest (512) INFO logs", 36 | Example: `logs info`, 37 | SilenceErrors: true, 38 | TraverseChildren: true, 39 | RunE: func(cmd *cobra.Command, args []string) error { 40 | logs, err := config.Client.GetLogsInfo() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | if *asObjects { 46 | return bite.PrintObject(cmd, logs) 47 | } 48 | 49 | return utils.PrintLogLines(logs) 50 | }, 51 | } 52 | 53 | return cmd 54 | } 55 | 56 | // NewGetLogsMetricsCommand creates `logs metrics` command 57 | func NewGetLogsMetricsCommand(asObjects *bool) *cobra.Command { 58 | cmd := &cobra.Command{ 59 | Use: "metrics", 60 | Short: "List the latest (512) METRICS logs", 61 | Example: `logs metrics`, 62 | SilenceErrors: true, 63 | TraverseChildren: true, 64 | RunE: func(cmd *cobra.Command, args []string) error { 65 | logs, err := config.Client.GetLogsMetrics() 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if *asObjects { 71 | return bite.PrintObject(cmd, logs) 72 | } 73 | 74 | return utils.PrintLogLines(logs) 75 | }, 76 | } 77 | 78 | return cmd 79 | } 80 | -------------------------------------------------------------------------------- /pkg/management/utils.go: -------------------------------------------------------------------------------- 1 | package management 2 | 3 | import "github.com/lensesio/lenses-go/v5/pkg/api" 4 | 5 | // GroupView the view model for group to be printed 6 | type GroupView struct { 7 | Name string `json:"name" yaml:"name" header:"name"` 8 | Namespaces []api.Namespace `json:"namespaces,omitempty" yaml:"namespaces" header:"Namespaces,count"` 9 | ScopedPermissions []string `json:"scopedPermissions" yaml:"scopedPermissions" header:"Scoped Permissions"` 10 | AdminPermissions []string `json:"adminPermissions" yaml:"adminPermissions" header:"Admin Permissions"` 11 | UserAccountsCount int `json:"userAccounts" yaml:"userAccounts" header:"User Accounts"` 12 | ServiceAccountsCount int `json:"serviceAccounts" yaml:"serviceAccounts" header:"Service Accounts"` 13 | ConnectClustersPermissions []string `json:"connectClustersPermissions" yaml:"connectClustersPermissions" header:"Connect clusters access"` 14 | } 15 | 16 | // PrintGroup returns a group for table printing 17 | func PrintGroup(g api.Group) GroupView { 18 | return GroupView{ 19 | Name: g.Name, 20 | Namespaces: g.Namespaces, 21 | ScopedPermissions: g.ScopedPermissions, 22 | AdminPermissions: g.AdminPermissions, 23 | ConnectClustersPermissions: g.ConnectClustersPermissions, 24 | } 25 | } 26 | 27 | // TokenView the view model for token to be printed 28 | type TokenView struct { 29 | Name string `json:"name" yaml:"name" header:"Service Account"` 30 | Token string `json:"token" yaml:"token" header:"Token"` 31 | } 32 | 33 | // PrintToken returns token for table printing 34 | func PrintToken(name, token string) TokenView { 35 | return TokenView{ 36 | Name: name, 37 | Token: token, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /pkg/provision/commands.go: -------------------------------------------------------------------------------- 1 | package provision 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var ( 16 | configMode string 17 | setupModeFlag bool 18 | ) 19 | 20 | // NewProvisionCommand is the 'beta provision' commmand 21 | func NewProvisionCommand() *cobra.Command { 22 | cmdShortDesc := "Provision Lenses with a YAML config file to setup license, connections, etc." 23 | cmdLongDesc := `Provision Lenses with a YAML config file to setup license, connections, etc.. 24 | If --mode flag set to 'sidecar' (for k8s purposes) then keep CLI running.` 25 | 26 | cmd := &cobra.Command{ 27 | Use: "provision [--mode {normal,sidecar}] [--setup-mode]", 28 | Short: cmdShortDesc, 29 | Long: cmdLongDesc, 30 | Example: "provision wizard.yml --mode=sidecar", 31 | RunE: func(cmd *cobra.Command, args []string) error { 32 | if len(args) == 0 { 33 | return errors.New("missing provisioning file, refer to `provision --help` for more info") 34 | } 35 | 36 | // If '--setup-mode' flag is set and setup has completed then skip provisioning 37 | status, err := config.Client.GetSetupStatus() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if !(setupModeFlag && status.IsCompleted) { 43 | yamlFileAsBytes, err := os.ReadFile(args[0]) 44 | if err != nil { 45 | return err 46 | } 47 | if err := provision(yamlFileAsBytes, config.Client, http.DefaultClient); err != nil { 48 | return err 49 | } 50 | } else { 51 | fmt.Fprintln(cmd.OutOrStdout(), "skipping provisioning as Lenses setup has already been completed") 52 | } 53 | 54 | if configMode == "sidecar" { 55 | fmt.Fprintln(cmd.OutOrStdout(), "lenses-cli will stay in idle state") 56 | // If we're a sidecar and our job is done, do not terminate 57 | // (otherwise k8s will restart us). Wait for either context 58 | // cancellation or a SIGINT/SIGTERM. 59 | sigs := make(chan os.Signal, 1) 60 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 61 | select { 62 | case <-sigs: 63 | case <-cmd.Context().Done(): 64 | } 65 | } 66 | return nil 67 | }, 68 | } 69 | 70 | cmd.PersistentFlags().StringVar(&configMode, "mode", "normal", "[normal,sidecar]") 71 | cmd.PersistentFlags().BoolVar(&setupModeFlag, "setup-mode", false, "When set will perform the provision only if Lenses is still in setup/wizard mode") 72 | return cmd 73 | } 74 | -------------------------------------------------------------------------------- /pkg/provision/testing/my-file.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /pkg/provision/testing/my-lic.json: -------------------------------------------------------------------------------- 1 | {"key":"test"} -------------------------------------------------------------------------------- /pkg/shell/shell_command.go: -------------------------------------------------------------------------------- 1 | package shell 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/c-bata/go-prompt" 9 | "github.com/kataras/golog" 10 | "github.com/lensesio/bite" 11 | "github.com/lensesio/lenses-go/v5/pkg/api" 12 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 13 | "github.com/lensesio/lenses-go/v5/pkg/sql" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var sqlHistoryPath = fmt.Sprintf("%s/history", api.DefaultConfigurationHomeDir) 18 | 19 | // NewInteractiveCommand creates `shell` command 20 | func NewInteractiveCommand() *cobra.Command { 21 | 22 | cmd := &cobra.Command{ 23 | Use: "shell", 24 | Short: "shell", 25 | Example: `shell`, 26 | SilenceErrors: true, 27 | TraverseChildren: true, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | client := config.Client 30 | 31 | sql.InteractiveShell = true 32 | 33 | fmt.Printf(` 34 | __ ________ ____ 35 | / / ___ ____ ________ _____ / ____/ / / _/ 36 | / / / _ \/ __ \/ ___/ _ \/ ___/ / / / / / / 37 | / /___/ __/ / / (__ ) __(__ ) / /___/ /____/ / 38 | /_____/\___/_/ /_/____/\___/____/ \____/_____/___/ 39 | Docs at https://docs.lenses.io 40 | Connected to [%s] as [%s], context [%s] 41 | Use "!" to set output options [!keys|!keysOnly|!stats|!meta|!pretty] 42 | Crtl+D to exit 43 | 44 | `, client.Config.Host, client.User.Name, config.Manager.Config.CurrentContext) 45 | 46 | var histories []string 47 | 48 | if _, err := os.Stat(sqlHistoryPath); os.IsExist(err) { 49 | file, err := os.Open(sqlHistoryPath) 50 | if err != nil { 51 | golog.Warnf("Unable to open command history. [%s]", err.Error()) 52 | } 53 | defer file.Close() 54 | 55 | scanner := bufio.NewScanner(file) 56 | for scanner.Scan() { 57 | histories = append(histories, scanner.Text()) 58 | } 59 | 60 | if err := scanner.Err(); err != nil { 61 | golog.Fatal(err) 62 | } 63 | } 64 | executor := sql.NewExecutor(cmd, client, sqlHistoryPath) 65 | 66 | p := prompt.New( 67 | executor.Execute, 68 | sql.Completer, 69 | prompt.OptionTitle(fmt.Sprintf("lenses: connected to [%s] ", client.Config.Host)), 70 | prompt.OptionPrefix("lenses-sql> "), 71 | prompt.OptionLivePrefix(executor.ChangeLivePrefix), 72 | prompt.OptionInputTextColor(prompt.Turquoise), 73 | prompt.OptionPrefixTextColor(prompt.White), 74 | prompt.OptionHistory(histories), 75 | ) 76 | 77 | p.Run() 78 | 79 | return nil 80 | 81 | }, 82 | } 83 | bite.CanPrintJSON(cmd) 84 | 85 | return cmd 86 | } 87 | -------------------------------------------------------------------------------- /pkg/sql/completer.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/c-bata/go-prompt" 9 | "github.com/kataras/golog" 10 | "github.com/lensesio/lenses-go/v5/pkg/api" 11 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 12 | ) 13 | 14 | func checkValidation(validation api.SQLValidationResponse) bool { 15 | for _, lint := range validation.Lints { 16 | var val = strings.ToLower(lint.Type) 17 | if val == "error" || val == "warning" { 18 | for _, innerLint := range validation.Lints { 19 | golog.Errorf("sError: text [%s]", innerLint.Text) 20 | } 21 | return false 22 | } 23 | } 24 | 25 | return true 26 | } 27 | 28 | // Completer sql completer 29 | func Completer(d prompt.Document) []prompt.Suggest { 30 | 31 | if strings.HasPrefix(d.GetWordBeforeCursor(), "!") { 32 | return prompt.FilterHasPrefix(optionSuggestions(), d.GetWordBeforeCursor(), true) 33 | } 34 | 35 | sql := fmt.Sprintf("%s%s", sqlQuery, d.CurrentLine()) 36 | caret := d.CursorPositionCol() + len(sqlQuery) 37 | 38 | keywords, err := config.Client.ValidateSQL(strings.Replace(sql, " ", " ", 0), caret) 39 | if err != nil { 40 | golog.Error(err) 41 | os.Exit(1) 42 | } 43 | 44 | if d.TextBeforeCursor() == "" { 45 | return []prompt.Suggest{} 46 | } 47 | 48 | var suggestions []prompt.Suggest 49 | 50 | for _, s := range keywords.Suggestions { 51 | suggestions = append(suggestions, prompt.Suggest{Text: s.Display, Description: s.Text}) 52 | } 53 | 54 | return prompt.FilterHasPrefix(suggestions, d.GetWordBeforeCursor(), true) 55 | } 56 | 57 | func optionSuggestions() []prompt.Suggest { 58 | return []prompt.Suggest{ 59 | {Text: "!keys", Description: "Toggle printing message keys"}, 60 | {Text: "!keys-only", Description: "Toggle printing keys only from message, no value"}, 61 | {Text: "!live-stream", Description: "Toggle continuous query mode"}, 62 | {Text: "!meta", Description: "Toggle printing message metadata"}, 63 | {Text: "!stats", Description: "Toggle printing query stats"}, 64 | {Text: "!options", Description: "Print current options"}, 65 | {Text: "!pretty", Description: "Toggle pretty printing query output"}, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/topic/payloads.go: -------------------------------------------------------------------------------- 1 | package topic 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/lensesio/bite" 8 | "github.com/lensesio/lenses-go/v5/pkg/api" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type topicMetadataView struct { 13 | api.TopicMetadata `yaml:",inline" header:"inline"` 14 | ValueSchema json.RawMessage `json:"valueSchema" yaml:"-"` // for view-only. 15 | KeySchema json.RawMessage `json:"keySchema" yaml:"-"` // for view-only. 16 | } 17 | 18 | func newTopicView(cmd *cobra.Command, client *api.Client, topic api.Topic) (t topicView) { 19 | t.Topic = topic 20 | output := strings.ToUpper(bite.GetOutPutFlag(cmd)) 21 | 22 | // don't spend time here if we are not in the machine-friendly mode, table mode does not show so much details and couldn't be, schemas are big. 23 | if output != "JSON" && output != "YAML" { 24 | return 25 | } 26 | 27 | if topic.KeySchema != "" { 28 | rawJSON, err := api.JSONAvroSchema(topic.KeySchema) 29 | if err != nil { 30 | return 31 | } 32 | 33 | if err = json.Unmarshal(rawJSON, &t.KeySchema); err != nil { 34 | return 35 | } 36 | } 37 | 38 | if topic.ValueSchema != "" { 39 | rawJSON, err := api.JSONAvroSchema(topic.ValueSchema) 40 | if err != nil { 41 | return 42 | } 43 | 44 | if err = json.Unmarshal(rawJSON, &t.ValueSchema); err != nil { 45 | return 46 | } 47 | } 48 | 49 | return 50 | } 51 | 52 | func newTopicMetadataView(m api.TopicMetadata) (topicMetadataView, error) { 53 | viewM := topicMetadataView{m, nil, nil} 54 | 55 | if len(m.ValueSchemaRaw) > 0 { 56 | rawJSON, err := api.JSONAvroSchema(m.ValueSchemaRaw) 57 | if err != nil { 58 | return viewM, err 59 | } 60 | 61 | if err = json.Unmarshal(rawJSON, &viewM.ValueSchema); err != nil { 62 | return viewM, err 63 | } 64 | 65 | // clear raw (avro) values and keep only the jsoned(ValueSchema, KeySchema). 66 | viewM.ValueSchemaRaw = "" 67 | } 68 | 69 | if len(m.KeySchemaRaw) > 0 { 70 | rawJSON, err := api.JSONAvroSchema(m.KeySchemaRaw) 71 | if err != nil { 72 | return viewM, err 73 | } 74 | 75 | if err = json.Unmarshal(rawJSON, &viewM.KeySchema); err != nil { 76 | return viewM, err 77 | } 78 | 79 | viewM.KeySchemaRaw = "" 80 | } 81 | 82 | return viewM, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/user/configure_user_command_test.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/lensesio/lenses-go/v5/pkg/api" 9 | test "github.com/lensesio/lenses-go/v5/test" 10 | ) 11 | 12 | const contextOutput = "[master] [valid, current]\n{\n \"host\": \"http://domain.com:80\",\n \"token\": \"****\",\n \"timeout\": \"15s\",\n \"debug\": true,\n \"basic\": {\n \"username\": \"user\",\n \"password\": \"****\"\n }\n}\n" 13 | 14 | func TestContextCommands(t *testing.T) { 15 | scenarios := make(map[string]test.CommandTest) 16 | 17 | scenarios["Command 'context' should return the 'master' context when 'master' context exists"] = 18 | test.CommandTest{ 19 | Setup: test.SetupMasterContext, 20 | Teardown: test.ResetConfigManager, 21 | Cmd: NewConfigurationContextCommand, 22 | ProcessOutput: func(t *testing.T, output string) { 23 | assert.Equal(t, output, contextOutput) 24 | }, 25 | ShouldContain: []string{ 26 | "master", 27 | "http://domain.com:80", 28 | "[master] [valid, current]", 29 | contextOutput, 30 | }, 31 | ShouldNotContain: []string{ 32 | "second", 33 | "http://example.com:80", 34 | }, 35 | } 36 | 37 | scenarios["Command 'context' should return err when no context exists"] = 38 | test.CommandTest{ 39 | Setup: test.SetupConfigManager, 40 | Teardown: test.ResetConfigManager, 41 | Cmd: NewConfigurationContextCommand, 42 | ShouldContainErrors: []string{ 43 | "current context does not exist, please use the `configure` command first", 44 | }, 45 | ShouldNotContain: []string{ 46 | "master", 47 | "http://domain.com:80", 48 | }, 49 | } 50 | 51 | scenarios["Command 'contexts' should return 'master' and 'second' contexts when both exists"] = 52 | test.CommandTest{ 53 | Setup: func() { 54 | secondAuth := api.BasicAuthentication{ 55 | Username: "user", 56 | Password: "pass", 57 | } 58 | secondClientConfig := api.ClientConfig{ 59 | Authentication: secondAuth, 60 | Debug: false, 61 | Host: "example.com", 62 | Timeout: "30s", 63 | Token: "secret", 64 | } 65 | test.SetupContext("second", secondClientConfig, secondAuth) 66 | test.SetupMasterContext() 67 | }, 68 | Teardown: test.ResetConfigManager, 69 | Cmd: NewGetConfigurationContextsCommand, 70 | ShouldContain: []string{ 71 | "master", 72 | "http://domain.com:80", 73 | "second", 74 | "http://example.com:80", 75 | contextOutput, 76 | }, 77 | ShouldNotContain: []string{ 78 | "third", 79 | "http://foo.com:80", 80 | }, 81 | } 82 | 83 | scenarios["Command 'contexts' should return no contexts when none exists"] = 84 | test.CommandTest{ 85 | Setup: test.SetupConfigManager, 86 | Teardown: test.ResetConfigManager, 87 | Cmd: NewGetConfigurationContextsCommand, 88 | ShouldNotContain: []string{ 89 | "master", 90 | "http://domain.com:80", 91 | "second", 92 | "http://example.com:80", 93 | "third", 94 | "http://foo.com:80", 95 | }, 96 | } 97 | 98 | test.RunCommandTests(t, scenarios) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/user/user_profile_command.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/kataras/golog" 5 | 6 | "github.com/lensesio/bite" 7 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 8 | "github.com/lensesio/lenses-go/v5/pkg/utils" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // NewUserGroupCommand creates `user` command 13 | func NewUserGroupCommand() *cobra.Command { 14 | root := &cobra.Command{ 15 | Use: "user", 16 | Short: "List information about the authenticated logged user such as the given roles given by the lenses administrator or manage the user's profile", 17 | Example: "user", 18 | TraverseChildren: true, 19 | SilenceErrors: true, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | if user := config.Client.User; user.Name != "" { 22 | // if logged in using the user password, then we have those info, 23 | // let's print it as well. 24 | return bite.PrintObject(cmd, user) 25 | } 26 | return nil 27 | }, 28 | } 29 | 30 | bite.CanPrintJSON(root) 31 | 32 | root.AddCommand(NewUserProfileGroupCommand()) 33 | 34 | return root 35 | } 36 | 37 | // NewUserProfileGroupCommand creates `users profile` command 38 | func NewUserProfileGroupCommand() *cobra.Command { 39 | rootSub := &cobra.Command{ 40 | Use: "profile", 41 | Short: "List the user-specific favourites, if any", 42 | Example: "user profile", 43 | TraverseChildren: true, 44 | SilenceErrors: true, 45 | RunE: func(cmd *cobra.Command, args []string) error { 46 | profile, err := config.Client.GetUserProfile() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if bite.ExpectsFeedback(cmd) && len(profile.Topics)+len(profile.Schemas)+len(profile.Transformers) == 0 { 52 | // do not throw error, it's not an error. 53 | return bite.PrintInfo(cmd, "No user profile available.") 54 | } 55 | 56 | return bite.PrintObject(cmd, profile) 57 | }, 58 | } 59 | 60 | bite.CanBeSilent(rootSub) 61 | bite.CanPrintJSON(rootSub) 62 | 63 | rootSub.AddCommand(NewCreateUserProfilePropertyValueCommand()) 64 | rootSub.AddCommand(NewDeleteUserProfilePropertyValueCommand()) 65 | 66 | return rootSub 67 | } 68 | 69 | // NewCreateUserProfilePropertyValueCommand creates `profile user set` command 70 | func NewCreateUserProfilePropertyValueCommand() *cobra.Command { 71 | cmd := &cobra.Command{ 72 | Use: "set", 73 | Aliases: []string{"add", "insert", "create", "update"}, 74 | Short: `Add a "value" to the user profile "property" entries`, 75 | Example: "user profile set newProperty newValueToTheProperty", 76 | TraverseChildren: true, 77 | SilenceErrors: true, 78 | RunE: func(cmd *cobra.Command, args []string) error { 79 | 80 | return utils.WalkPropertyValueFromArgs(args, func(property, value string) error { 81 | if err := config.Client.CreateUserProfilePropertyValue(property, value); err != nil { 82 | golog.Errorf("Failed to add the user profile value [%s] from property [%s]. [%s]", value, property, err.Error()) 83 | return err 84 | } 85 | 86 | return bite.PrintInfo(cmd, "User profile value: [%s] for property: [%s] added", value, property) 87 | }) 88 | }, 89 | } 90 | 91 | bite.CanBeSilent(cmd) 92 | 93 | return cmd 94 | } 95 | 96 | // NewDeleteUserProfilePropertyValueCommand creates `profile user delete` command 97 | func NewDeleteUserProfilePropertyValueCommand() *cobra.Command { 98 | cmd := &cobra.Command{ 99 | Use: "delete", 100 | Aliases: []string{"remove"}, 101 | Short: `Remove the "value" from the user profile "property" entries`, 102 | Example: "user profile delete existingProperty existingValueFromProperty", 103 | TraverseChildren: true, 104 | SilenceErrors: true, 105 | RunE: func(cmd *cobra.Command, args []string) error { 106 | return utils.WalkPropertyValueFromArgs(args, func(property, value string) error { 107 | if err := config.Client.DeleteUserProfilePropertyValue(property, value); err != nil { 108 | golog.Errorf("Failed to remove the user profile value [%s] from property [%s]. [%s]", value, property, err.Error()) 109 | return err 110 | } 111 | 112 | return bite.PrintInfo(cmd, "User profile value: [%s] from property: [%s] removed", value, property) 113 | }) 114 | }, 115 | } 116 | 117 | bite.CanBeSilent(cmd) 118 | 119 | return cmd 120 | } 121 | -------------------------------------------------------------------------------- /pkg/utils/colors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/mgutz/ansi" 5 | ) 6 | 7 | // Variables for colors. Expand the color pallete here 8 | var ( 9 | RED = ansi.ColorFunc("red") 10 | YELLOW = ansi.ColorFunc("yellow") 11 | GREEN = ansi.ColorFunc("green") 12 | GRAY = ansi.ColorFunc("black+h") 13 | ) 14 | 15 | // Red wrapper for a string 16 | func Red(format string) string { 17 | return RED(format) 18 | } 19 | 20 | // Yellow wrapper for a string 21 | func Yellow(format string) string { 22 | return YELLOW(format) 23 | } 24 | 25 | // Green wrapper for a string 26 | func Green(format string) string { 27 | return GREEN(format) 28 | } 29 | 30 | // Gray wrapper for a string 31 | func Gray(format string) string { 32 | return GRAY(format) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func Test_isValidImportFile(t *testing.T) { 6 | type args struct { 7 | fileName string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want bool 13 | }{ 14 | {"hidden file", args{".file"}, false}, 15 | {"hidden folder", args{".folder/"}, false}, 16 | {"non yaml/yml file", args{"hello.txt"}, false}, 17 | {"non yaml/yml file", args{"foobar"}, false}, 18 | {"valid file", args{"topic.yml"}, true}, 19 | {"valid file", args{"topic.yaml"}, true}, 20 | {"invalid dir entry", args{"folder/"}, false}, 21 | } 22 | for _, tt := range tests { 23 | t.Run(tt.name, func(t *testing.T) { 24 | if got := isValidImportFile(tt.args.fileName); got != tt.want { 25 | t.Errorf("isValidImportFile() = %v, want %v", got, tt.want) 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /publish-docker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Major versions detected:" 4 | git tag -l | grep -E "^([0-9]+\.){2,2}[0-9]+$" | grep -v 99 | cut -f 1,2 -d . | sort -ur 5 | echo 6 | 7 | echo "Valid tags detected:" 8 | git tag -l | grep -E "^([0-9]+\.){2,2}[0-9]+$" | grep -v 99 | sort -t . -n -r -k 1,1 -k 2,2 -k 3,3 9 | echo 10 | 11 | # if we are not on a tag, skip. 12 | if ! git describe --tags | grep -Esq "^([0-9]+\.){2,2}[0-9]+$"; then 13 | echo "Not a commit with a valid tag. Will not release." 14 | exit 1 15 | fi 16 | 17 | # Current major version 18 | CUR_MAJOR="$(git describe --tags | cut -f 1,2 -d .)" 19 | echo "Current major is $CUR_MAJOR" 20 | 21 | # Latest major version in repo 22 | REPO_MAJOR="$(git tag -l | grep -E "^([0-9]+\.){2,2}[0-9]+$" | grep -v 99 | cut -f 1,2 -d . | sort -ur | head -1)" 23 | echo "Repo highest major is $REPO_MAJOR" 24 | 25 | # Current minor version 26 | CUR_MINOR="$(git describe --tags)" 27 | echo "Current tag is $CUR_MINOR" 28 | 29 | # Highest minor version for the current major 30 | HIGH_MINOR="$(git tag -l | grep -E "^([0-9]+\.){2,2}[0-9]+$" | grep -E "^${CUR_MAJOR}\." | sort -t . -n -r -k 3,3 | head -1)" 31 | echo "Highest patch for current version is $HIGH_MINOR" 32 | 33 | # Publish docker 34 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 35 | docker build -t lensesio/lenses-cli:${CUR_MINOR} . 36 | 37 | echo "Publishing lensesio/lenses-cli:${CUR_MINOR}" 38 | docker push lensesio/lenses-cli:${CUR_MINOR} 39 | 40 | 41 | # If we have the highest patch (minor) for our version, then we need to tag 42 | # with our version too (e.g 2.2) 43 | if [[ $CUR_MINOR == $HIGH_MINOR ]]; then 44 | echo "Publishing lensesio/lenses-cli:${CUR_MAJOR}" 45 | docker tag lensesio/lenses-cli:${CUR_MINOR} lensesio/lenses-cli:${CUR_MAJOR} 46 | 47 | docker push lensesio/lenses-cli:${CUR_MAJOR} 48 | 49 | # If we have the highest version too, then we need to tag with latest too 50 | if [[ $REPO_MAJOR == $CUR_MAJOR ]]; then 51 | echo "Publishing lensesio/lenses-cli:latest" 52 | docker tag lensesio/lenses-cli:${CUR_MINOR} lensesio/lenses-cli:latest 53 | 54 | docker push lensesio/lenses-cli:latest 55 | fi 56 | fi 57 | 58 | docker logout 59 | -------------------------------------------------------------------------------- /test/command_helper.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/cobra" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | // CommandTest struct to define a test scenario 12 | type CommandTest struct { 13 | Setup func() 14 | Teardown func() 15 | Cmd func() *cobra.Command 16 | CmdArgs []string 17 | ProcessOutput func(t *testing.T, s string) 18 | ShouldContainErrors []string 19 | HasCustomError error 20 | ShouldContain []string 21 | ShouldNotContain []string 22 | } 23 | 24 | // RunCommandTests runs all set test scenarios 25 | // *** USAGE *** 26 | // 27 | // func TestFooCommands(t *testing.T) { 28 | // scenarios := make(map[string]test.CommandTest) 29 | // scenarios["'Foo' command should throw error without args"] = 30 | // test.CommandTest{ 31 | // Cmd: NewRootCommand, 32 | // CmdArgs: []string{"foo"},11 33 | // ShouldContainErrors: []string{`No args set!`}, 34 | // } 35 | // scenarios["'Foo' command run successfully with args"] = 36 | // test.CommandTest{ 37 | // Cmd: NewRootCommand, 38 | // CmdArgs: []string{"foo","bar"},11 39 | // ShouldContain: []string{`Run smoothly!`}, 40 | // } 41 | // 42 | // // Both tests will run 43 | // 44 | // test.RunCommandTests(t, scenarios) 45 | // } 46 | func RunCommandTests(t *testing.T, cmdTests map[string]CommandTest) { 47 | for description, cmdTest := range cmdTests { 48 | t.Run(description, func(t *testing.T) { 49 | runCommandTest(t, cmdTest) 50 | }) 51 | } 52 | } 53 | 54 | func runCommandTest(t *testing.T, v CommandTest) { 55 | // Arrange 56 | if v.Setup != nil { 57 | v.Setup() 58 | } 59 | 60 | // Act 61 | if v.Cmd == nil { 62 | t.Error("Cmd attribute not found") 63 | } 64 | cmd := v.Cmd() 65 | var outputValue string 66 | cmd.PersistentFlags().StringVar(&outputValue, "output", "json", "") 67 | output, err := ExecuteCommand(cmd, v.CmdArgs...) 68 | if v.ProcessOutput != nil { 69 | v.ProcessOutput(t, output) 70 | } 71 | 72 | // Assert 73 | if len(v.ShouldContainErrors) == 0 && v.HasCustomError != nil { 74 | assert.NoError(t, err) 75 | } 76 | if len(v.ShouldContainErrors) != 0 { 77 | assert.Error(t, err) 78 | for _, msg := range v.ShouldContainErrors { 79 | assert.EqualError(t, err, msg) 80 | } 81 | } 82 | if v.HasCustomError != nil { 83 | assert.True(t, errors.Is(err, v.HasCustomError)) 84 | } 85 | 86 | for _, expectedString := range v.ShouldContain { 87 | assert.Contains(t, output, expectedString) 88 | } 89 | for _, unexpectedString := range v.ShouldNotContain { 90 | assert.NotContains(t, output, unexpectedString) 91 | } 92 | 93 | if v.Teardown != nil { 94 | v.Teardown() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/config_manager.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/lensesio/lenses-go/v5/pkg/api" 5 | config "github.com/lensesio/lenses-go/v5/pkg/configs" 6 | ) 7 | 8 | // SetupMasterContext add a new context named "master" in Config 9 | func SetupMasterContext() { 10 | SetupContext(api.DefaultContextKey, ClientConfig, auth) 11 | } 12 | 13 | // SetupContext add a new context in Config 14 | func SetupContext(contextName string, clientConfig api.ClientConfig, basicAuth api.BasicAuthentication) { 15 | SetupConfigManager() 16 | config.Manager.Config.AddContext(contextName, &clientConfig) 17 | config.Manager.Config.SetCurrent(contextName) 18 | config.Manager.Config.GetCurrent().Authentication = basicAuth 19 | } 20 | 21 | // SetupConfigManager setup a new empty config manager if not done already 22 | func SetupConfigManager() { 23 | if config.Manager == nil { 24 | config.Manager = config.NewEmptyConfigManager() 25 | } 26 | } 27 | 28 | // ResetConfigManager reset the config manager 29 | func ResetConfigManager() { 30 | config.Manager = nil 31 | } 32 | -------------------------------------------------------------------------------- /test/testing.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/lensesio/lenses-go/v5/pkg/api" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/pflag" 16 | ) 17 | 18 | var ( 19 | auth = api.BasicAuthentication{ 20 | Username: "user", 21 | Password: "pass", 22 | } 23 | // ClientConfig mocked for testing 24 | ClientConfig = api.ClientConfig{ 25 | Authentication: auth, 26 | Debug: true, 27 | Host: "domain.com", 28 | Timeout: "15s", 29 | Token: "secret", 30 | } 31 | ) 32 | 33 | // CheckStringContains check if string contains the expected value 34 | func CheckStringContains(t *testing.T, got, expected string) { 35 | if !strings.Contains(got, expected) { 36 | t.Errorf("Expected to contain: \n %v\nGot:\n %v\n", expected, got) 37 | } 38 | } 39 | 40 | // CheckStringOmits check if string doesn't contain the expected value 41 | func CheckStringOmits(t *testing.T, got, expected string) { 42 | if strings.Contains(got, expected) { 43 | t.Errorf("Expected to not contain: \n %v\nGot: %v", expected, got) 44 | } 45 | } 46 | 47 | // EmptyRun an empty run 48 | func EmptyRun(*cobra.Command, []string) {} 49 | 50 | // ExecuteCommand execute a command 51 | func ExecuteCommand(root *cobra.Command, args ...string) (output string, err error) { 52 | _, output, err = executeCommandC(root, args...) 53 | return output, err 54 | } 55 | 56 | // ResetCommandLineFlagSet resets the flagset 57 | func ResetCommandLineFlagSet() { 58 | pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) 59 | } 60 | 61 | // TestingHTTPClient starts a [httptest.Server] that serves the provided handler 62 | // and returns an http.Client that always dials to this server. 63 | func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { 64 | s := httptest.NewServer(handler) 65 | 66 | cli := &http.Client{ 67 | Transport: &http.Transport{ 68 | DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { 69 | return net.Dial(network, s.Listener.Addr().String()) 70 | }, 71 | }, 72 | } 73 | 74 | return cli, s.Close 75 | } 76 | 77 | func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { 78 | buf := new(bytes.Buffer) 79 | root.SetOutput(buf) 80 | root.SetArgs(args) 81 | 82 | c, err = root.ExecuteC() 83 | 84 | return c, buf.String(), err 85 | } 86 | --------------------------------------------------------------------------------