diff --git a/api/v1alpha1/workspace_types.go b/api/v1alpha1/workspace_types.go index 03156b55..705ec45c 100644 --- a/api/v1alpha1/workspace_types.go +++ b/api/v1alpha1/workspace_types.go @@ -99,6 +99,16 @@ func (r *NetworkRule) UniqueKey() string { return r.HostPrefix() } +func GetNetworkRuleIndex(rules []NetworkRule, target NetworkRule) int { + index := -1 + for i, v := range rules { + if v.UniqueKey() == target.UniqueKey() { + index = i + } + } + return index +} + func MainRuleKey(cfg Config) string { return HTTPUniqueKey(cfg.ServiceMainPortName, "/") } diff --git a/cmd/cosmoctl/main.go b/cmd/cosmoctl/main.go index 1cc0e44a..df275c48 100644 --- a/cmd/cosmoctl/main.go +++ b/cmd/cosmoctl/main.go @@ -2,8 +2,16 @@ package main import ( "github.com/cosmo-workspace/cosmo/internal/cmd" + "github.com/cosmo-workspace/cosmo/pkg/cli" +) + +var ( + // goreleaser default https://goreleaser.com/customization/builds/ + version = "snapshot" + commit = "snapshot" + date = "snapshot" ) func main() { - cmd.Execute() + cmd.Execute(cli.VersionInfo{Version: version, Commit: commit, Date: date}) } diff --git a/config/user-addon/traefik-middleware/Makefile b/config/user-addon/traefik-middleware/Makefile index 085363df..379fb478 100644 --- a/config/user-addon/traefik-middleware/Makefile +++ b/config/user-addon/traefik-middleware/Makefile @@ -2,8 +2,8 @@ all: useraddon PHONY: useraddon useraddon: - cat cosmo-username-headers.yaml | cosmoctl template generate --name cosmo-username-headers \ - --user-addon \ + cat cosmo-username-headers.yaml | cosmoctl tmpl gen --name cosmo-username-headers \ + --useraddon \ --desc 'Traefik middleware for user authorization. DO NOT EDIT' \ - --set-default-user-addon \ + --useraddon-set-default \ --disable-nameprefix | grep -v "Generated by cosmoctl" > cosmo-username-headers-addon.yaml \ No newline at end of file diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md index a7431544..69293c8b 100644 --- a/docs/GETTING-STARTED.md +++ b/docs/GETTING-STARTED.md @@ -113,7 +113,7 @@ Download binary from [latest release](https://github.com/cosmo-workspace/cosmo/r Use cosmoctl to create first User. ```sh -cosmoctl user create admin --admin +cosmoctl user create admin --privileged ``` Output: diff --git a/docs/TEMPLATE-ENGINE.md b/docs/TEMPLATE-ENGINE.md index 66f2004a..604c0f77 100644 --- a/docs/TEMPLATE-ENGINE.md +++ b/docs/TEMPLATE-ENGINE.md @@ -124,7 +124,7 @@ In the example template, deployment name created by instance named `example` is > Note: > Currently, name prefix feature is not the same as kustomize, which change the name and the references. -> So the Template generated by `cosmoctl template gen` command use kustomize internally and have `{{INSTANCE}}-` prefix on all manifests by default. +> So the Template generated by `cosmoctl tmpl gen` command use kustomize internally and have `{{INSTANCE}}-` prefix on all manifests by default. In order not to prefix on resources, set `cosmo-workspace.github.io/disable-nameprefix: "true"` in annotation of Template. @@ -185,7 +185,7 @@ Template can be generated via `cosmoctl`. All you have to do is to prepare your own Kubernetes YAMLs that is deployable. -And pass them to `cosmoctl template gen` command by stdin. +And pass them to `cosmoctl tmpl gen` command by stdin. ```sh # kustomze diff --git a/docs/USER.md b/docs/USER.md index 4aa2ceb4..e46a0e5d 100644 --- a/docs/USER.md +++ b/docs/USER.md @@ -90,7 +90,7 @@ spec:
-UserAddon can be generated via `cosmoctl template gen` command, same as WorkspaceTemplate. +UserAddon can be generated via `cosmoctl tmpl gen` command, same as WorkspaceTemplate. ### Create UserAddon @@ -185,7 +185,7 @@ UserAddon can be generated via `cosmoctl template gen` command, same as Workspac 2. Generate WorkspaceTemplate - Pass kustomize-generated manifest to `cosmoctl template gen` command by stdin. + Pass kustomize-generated manifest to `cosmoctl tmpl gen` command by stdin. ```sh kustomize build . | cosmoctl tmpl gen --cluster-scope --useraddon -o addon.yaml @@ -205,7 +205,7 @@ UserAddons with the following annotations have special behavior. | Annotatio keys | Avairable values(default) | Description | cosmoctl option | |:--|:--|:--|:--| -| `useraddon.cosmo-workspace.github.io/default` | `["true", "false"]`("false") | UserAddon with this annotation is applied to all Users automatically | `--set-default-user-addon` | +| `useraddon.cosmo-workspace.github.io/default` | `["true", "false"]`("false") | UserAddon with this annotation is applied to all Users automatically | `--useraddon-set-default` | | `cosmo-workspace.github.io/disable-nameprefix` | `["true", "false"]`("false") | UserAddon with this annotation is applied to all Users automatically | `--disable-nameprefix` | | `cosmo-workspace.github.io/userroles` | comma-separated UserRoles(None) | User who use this Template must have all of the UserRoles specified in this annotation | `--userroles` | | `cosmo-workspace.github.io/required-useraddons` | comma-separated UserAddon names(None) | User who use this Template must be attached all of the UserAddons specified in this annotation | `--required-useraddons` | diff --git a/docs/WORKSPACE.md b/docs/WORKSPACE.md index a811eb30..f4b794ca 100644 --- a/docs/WORKSPACE.md +++ b/docs/WORKSPACE.md @@ -330,7 +330,7 @@ As you can see `# Generated by cosmoctl template command` top of the yaml, Works 2. Generate WorkspaceTemplate - Pass kustomize-generated manifest to `cosmoctl template gen` command by stdin. + Pass kustomize-generated manifest to `cosmoctl tmpl gen` command by stdin. ```sh kustomize build . | cosmoctl tmpl gen --workspace -o cosmo-template.yaml @@ -365,7 +365,7 @@ As you can see `# Generated by cosmoctl template command` top of the yaml, Works 2. Generate WorkspaceTemplate - Pass Helm-generated manifests to `cosmoctl template gen` command by stdin. + Pass Helm-generated manifests to `cosmoctl tmpl gen` command by stdin. ```sh helm template code-server-example cosmo/dev-code-server | cosmoctl tmpl gen --workspace -o cosmo-template.yaml diff --git a/go.mod b/go.mod index 80fdd447..3f6bee8f 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bufbuild/connect-go v1.10.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/evanphx/json-patch/v5 v5.9.0 + github.com/fatih/color v1.17.0 github.com/gkampitakis/go-snaps v0.5.4 github.com/go-ldap/ldap/v3 v3.4.8 github.com/go-logr/logr v1.4.1 @@ -26,6 +27,7 @@ require ( github.com/traefik/traefik/v3 v3.0.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.23.0 + golang.org/x/term v0.20.0 google.golang.org/protobuf v1.34.1 k8s.io/api v0.30.0 k8s.io/apimachinery v0.30.0 @@ -120,7 +122,6 @@ require ( golang.org/x/oauth2 v0.20.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.0 // indirect diff --git a/go.sum b/go.sum index 6f7d7c50..84a2f14f 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/hack/local-run-test/Makefile b/hack/local-run-test/Makefile index 65ff390b..4fa1e3e2 100644 --- a/hack/local-run-test/Makefile +++ b/hack/local-run-test/Makefile @@ -69,7 +69,7 @@ show-url: ## show-url create-all: create-cluster docker-build-all install-all apply-template add-user add-workspace ## Create all delete-all: delete-cluster ## Delete all -docker-build-all: docker-build-manager docker-build-dashboard ## Docker build all +docker-build-all: docker-build-manager docker-build-dashboard docker-build-traefik-plugins ## Docker build all install-all: install-cosmo ## Install cosmo resources. uninstall-all: uninstall-cosmo ## Uninstall cosmo resources. @@ -213,7 +213,7 @@ docker-build-dashboard: ## build & push cosmo dashboard image. docker push localhost:5000/cosmo-dashboard:$(DASHBOARD_IMAGE_TAG) docker rmi cosmo-dashboard:$(DASHBOARD_IMAGE_TAG) k3d image import localhost:5000/cosmo-dashboard:$(DASHBOARD_IMAGE_TAG) -c $(CLUSTER_NAME) - -kubectl rollout restart deploy -n cosmo-system cosmo-dashboard + -kubectl rollout restart deploy -n cosmo-system cosmo-dashboard docker-build-traefik-plugins: ## build & push cosmo traefik-plugins image. @echo ====== $@ ====== @@ -240,9 +240,9 @@ docker-cache-clear: ## docker cache clear. LOGLEVEL ?= info -install-cosmo: helm kubectl docker-build-manager docker-build-dashboard docker-build-traefik-plugins ## Install cosmo resources. +install-cosmo: #helm kubectl docker-build-manager docker-build-dashboard docker-build-traefik-plugins ## Install cosmo resources. @echo ====== $@ ====== - helm dependency update ../../charts/cosmo + helm dependency update ../../charts/cosmo helm upgrade --install cosmo ../../charts/cosmo \ -n cosmo-system --create-namespace \ --wait \ @@ -317,42 +317,44 @@ apply-template: kubectl cosmoctl ## Apply template. add-user: kubectl cosmoctl ## add user @echo ====== $@ ====== - -cosmoctl user create tom --admin 2> /dev/null - -cosmoctl user create gryffindor-dev --role "gryffindor" --addon resource-limitter --addon gryffindor-serviceaccount 2> /dev/null - -cosmoctl user create gryffindor-admin --role "gryffindor-admin" --addon resource-limitter --addon gryffindor-serviceaccount 2> /dev/null - -cosmoctl user create slytherin-dev --role "slytherin" 2> /dev/null - -cosmoctl user create slytherin-admin --role "slytherin-admin" 2> /dev/null - -cosmoctl user create grytherin --role "gryffindor,slytherin" --addon resource-limitter --addon gryffindor-serviceaccount 2> /dev/null - -cosmoctl user create ldapuser1 --admin --auth-type ldap 2> /dev/null - -cosmoctl user reset-password tom --password vvv - -cosmoctl user reset-password gryffindor-dev --password xxxxxxxx - -cosmoctl user reset-password gryffindor-admin --password xxxxxxxx - -cosmoctl user reset-password slytherin-dev --password xxxxxxxx - -cosmoctl user reset-password slytherin-admin --password xxxxxxxx - -cosmoctl user reset-password grytherin --password xxxxxxxx - + -cosmoctl -k user create tom --privileged --force 2> /dev/null + -cosmoctl -k user create gryffindor-dev --role "gryffindor" --addon resource-limitter --addon gryffindor-serviceaccount --force 2> /dev/null + -cosmoctl -k user create gryffindor-admin --role "gryffindor-admin" --addon resource-limitter --addon gryffindor-serviceaccount --force 2> /dev/null + -cosmoctl -k user create slytherin-dev --role "slytherin" --force 2> /dev/null + -cosmoctl -k user create slytherin-admin --role "slytherin-admin" --force 2> /dev/null + -cosmoctl -k user create grytherin --role "gryffindor,slytherin" --addon resource-limitter --addon gryffindor-serviceaccount --force 2> /dev/null + -cosmoctl -k user create ldapuser1 --privileged --auth-type ldap --force 2> /dev/null + -echo vvv | cosmoctl -k user change-password --password-stdin tom + -echo xxxxxxxx | cosmoctl -k user change-password --password-stdin gryffindor-dev + -echo xxxxxxxx | cosmoctl -k user change-password --password-stdin gryffindor-admin + -echo xxxxxxxx | cosmoctl -k user change-password --password-stdin slytherin-dev + -echo xxxxxxxx | cosmoctl -k user change-password --password-stdin slytherin-admin + -echo xxxxxxxx | cosmoctl -k user change-password --password-stdin grytherin + -echo vvv | cosmoctl login tom --password-stdin --dashboard-url $(DASHBOARD_URL) add-workspace: kubectl cosmoctl ## add workspace @echo ====== $@ ====== - -cosmoctl workspace create --user=tom --template=dev-code-server ws1 - -cosmoctl workspace create --user=ldapuser1 --template=dev-code-server ldapws1 + -cosmoctl workspace create --force --template=dev-code-server ws1 + -cosmoctl -k workspace create --force --user=ldapuser1 --template=dev-code-server ldapws1 sleep 5 - -cosmoctl networkrule add --user=tom --workspace=ws1 --port=7701 --host-prefix proxy1 --path / + -cosmoctl ws upsert-network ws1 --port=7701 --host-prefix proxy1 --path / sleep 1 - -cosmoctl networkrule add --user=tom --workspace=ws1 --port=7701 --host-prefix proxy1 --path /aaa + -cosmoctl ws upsert-network ws1 --port=7701 --host-prefix proxy1 --path /aaa sleep 1 - -cosmoctl networkrule add --user=tom --workspace=ws1 --port=7701 --host-prefix proxy1 --path /bbb --public + -cosmoctl ws upsert-network ws1 --port=7701 --host-prefix proxy1 --path /bbb --public sleep 1 - -cosmoctl networkrule add --user=tom --workspace=ws1 --port=7701 --path / + -cosmoctl ws upsert-network ws1 --port=7701 --path / delete-cosmo-crd: ## Delete cosmo crd. - -kubectl get crd | grep cosmo-workspace.github.io | awk '{print $$1}' | xargs kubectl delete crd + -kubectl get crd | grep cosmo-workspace.github.io | awk '{print $$1}' | xargs kubectl delete crd delete-cosmo-resources: -kubectl delete user --all -kubectl delete tmpl --all -kubectl delete ctmpl --all +create-cosmo-resources: apply-template add-user add-workspace cg + ##--------------------------------------------------------------------- ##@ Execute test ##--------------------------------------------------------------------- @@ -487,13 +489,13 @@ bin/argocd: ##--------------------------------------------------------------------- ##@ Utility ##--------------------------------------------------------------------- -console: ## Activate kubeconfig for local k8s. +console: ## Activate kubeconfig for local k8s. @bash -rcfile <(echo ". ~/.bashrc;PS1='\[\033[01;32m\]\u@test-env\[\033[00m\]:\[\033[01;35m\]\W\[\033[00m\]$$ '") helm-ls: ## helm list -@helm list -a -A -kg: ## Get k0s resources. +kg: ## Get k8s resources. -@kubectl get node --show-kind -@kubectl get po -A --show-kind -@kubectl get ing -A --show-kind @@ -502,3 +504,9 @@ kg: ## Get k0s resources. -@kubectl get svc -A --show-kind -@kubectl get ep -A --show-kind -@kubectl get application -A --show-kind + +cg: ## Get cosmo resources. + -@cosmoctl get user -k + -@cosmoctl get ws -A -k + -@cosmoctl get tmpl -k + -@cosmoctl get addon -k diff --git a/hack/local-run-test/templates/code-server-01/Makefile b/hack/local-run-test/templates/code-server-01/Makefile index c72706a3..bf7bab89 100644 --- a/hack/local-run-test/templates/code-server-01/Makefile +++ b/hack/local-run-test/templates/code-server-01/Makefile @@ -5,8 +5,8 @@ FROM_TAG=4.9.1 .PHONY: template template: cd kubernetes/ && kustomize edit set image codercom/code-server=cosmo.io:5000/my-code-server:latest - kustomize build kubernetes/ | cosmoctl tmpl generate -o cosmo-template.yaml --workspace \ - --required-vars CODE-SERVER_STORAGE_GB:20,DOCKER_STORAGE:20 + kustomize build kubernetes/ | cosmoctl tmpl gen ws -o cosmo-template.yaml --no-header \ + --var CODE-SERVER_STORAGE_GB:20 --var DOCKER_STORAGE:20 .PHONY: apply apply: template ## Apply template diff --git a/hack/local-run-test/templates/code-server-01/cosmo-template.yaml b/hack/local-run-test/templates/code-server-01/cosmo-template.yaml index f72745d9..a096c43a 100644 --- a/hack/local-run-test/templates/code-server-01/cosmo-template.yaml +++ b/hack/local-run-test/templates/code-server-01/cosmo-template.yaml @@ -1,13 +1,10 @@ -# Generated by cosmoctl - cosmo v0.8.0 cosmo-workspace 2023 apiVersion: cosmo-workspace.github.io/v1alpha1 kind: Template metadata: annotations: workspace.cosmo-workspace.github.io/deployment: workspace - workspace.cosmo-workspace.github.io/ingress: "" workspace.cosmo-workspace.github.io/service: workspace workspace.cosmo-workspace.github.io/service-main-port: main - workspace.cosmo-workspace.github.io/urlbase: "" creationTimestamp: null labels: cosmo-workspace.github.io/type: workspace diff --git a/hack/local-run-test/templates/dev-code-server/Makefile b/hack/local-run-test/templates/dev-code-server/Makefile index 0642c76a..8f8ce3ab 100644 --- a/hack/local-run-test/templates/dev-code-server/Makefile +++ b/hack/local-run-test/templates/dev-code-server/Makefile @@ -6,14 +6,12 @@ IMAGE_TAG=v0.0.2-4.13.0 .PHONY: template template: ## Create template cd kubernetes/ && kustomize edit set image $(IMAGE_REPO)/$(IMAGE_NAME):$(IMAGE_TAG) - kustomize build kubernetes/ | cosmoctl tmpl generate -o cosmo-template.yaml --workspace \ - --required-vars CODE-SERVER_STORAGE_GB:20,DOCKER_STORAGE:20 - kustomize build gryffindor | cosmoctl tmpl generate -o gryffindor-template.yaml --workspace \ - --name gryffindor-codeserver \ - --desc 'only for gryffindor' \ - --userroles 'gryffindor' \ - --required-useraddons gryffindor-serviceaccount \ - --required-vars CODE-SERVER_STORAGE_GB:20,DOCKER_STORAGE:20 + kustomize build kubernetes/ | cosmoctl tmpl gen ws -o cosmo-template.yaml --no-header \ + --var CODE-SERVER_STORAGE_GB:20 --var DOCKER_STORAGE:20 + kustomize build gryffindor | cosmoctl tmpl gen ws -o gryffindor-template.yaml --no-header \ + --name gryffindor-codeserver --desc 'only for gryffindor' \ + --userroles 'gryffindor' --required-useraddons gryffindor-serviceaccount \ + --var CODE-SERVER_STORAGE_GB:20 --var DOCKER_STORAGE:20 .PHONY: apply apply: template ## Apply template diff --git a/hack/local-run-test/templates/dev-code-server/cosmo-template.yaml b/hack/local-run-test/templates/dev-code-server/cosmo-template.yaml index 03420207..1b686836 100644 --- a/hack/local-run-test/templates/dev-code-server/cosmo-template.yaml +++ b/hack/local-run-test/templates/dev-code-server/cosmo-template.yaml @@ -1,4 +1,3 @@ -# Generated by cosmoctl - cosmo v1.0.0-rc5 cosmo-workspace 2023 apiVersion: cosmo-workspace.github.io/v1alpha1 kind: Template metadata: diff --git a/hack/local-run-test/templates/dev-code-server/gryffindor-template.yaml b/hack/local-run-test/templates/dev-code-server/gryffindor-template.yaml index 19e7aa96..bd9252c8 100644 --- a/hack/local-run-test/templates/dev-code-server/gryffindor-template.yaml +++ b/hack/local-run-test/templates/dev-code-server/gryffindor-template.yaml @@ -1,4 +1,3 @@ -# Generated by cosmoctl - cosmo v1.0.0-rc5 cosmo-workspace 2023 apiVersion: cosmo-workspace.github.io/v1alpha1 kind: Template metadata: diff --git a/internal/cmd/__snapshots__/netrule_test.snap b/internal/cmd/__snapshots__/netrule_test.snap deleted file mode 100644 index c73c19d1..00000000 --- a/internal/cmd/__snapshots__/netrule_test.snap +++ /dev/null @@ -1,855 +0,0 @@ -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --namespace cosmo-user-user1 --workspace ws1 --port 4000 --host-prefix nw12 --path /def 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --namespace cosmo-user-user1 --workspace ws1 --port 4000 --host-prefix nw12 --path /def 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --namespace cosmo-user-user1 --workspace ws1 --port 4000 --host-prefix nw12 --path /def 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"customHostPrefix\": \"nw12\", - \"httpPath\": \"/def\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw3\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 3000 --host-prefix nw11 --path /abc 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 3000 --host-prefix nw11 --path /abc 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 3000 --host-prefix nw11 --path /abc 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 3000, - \"customHostPrefix\": \"nw11\", - \"httpPath\": \"/abc\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw3\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix nw13 --path /def 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix nw13 --path /def 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix nw13 --path /def 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"customHostPrefix\": \"nw13\", - \"httpPath\": \"/def\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw3\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --path /def 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --path /def 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 --path /def 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"httpPath\": \"/def\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw3\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ✅ success in normal context: netrule create --user user1 --workspace ws1 --port 4000 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw3\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ❌ fail with an unexpected error at update: netrule create --workspace ws1 --user user1 --host-prefix nw99 --port 4000 --path /def 1'] -SnapShot = """ -Error: failed to upsert network rule: mock update error -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with an unexpected error at update: netrule create --workspace ws1 --user user1 --host-prefix nw99 --port 4000 --path /def 2'] -SnapShot = 'failed to upsert network rule: mock update error' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --namespace xxxxx --workspace ws1 --port 4000 1'] -SnapShot = """ -Error: invalid options: namespace xxxxx is not cosmo user's namespace -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --namespace xxxxx --workspace ws1 --port 4000 2'] -SnapShot = """ -invalid options: namespace xxxxx is not cosmo user's namespace""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 0 1'] -SnapShot = """ -Error: validation error: --port is required -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 0 2'] -SnapShot = 'validation error: --port is required' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 124000 1'] -SnapShot = """ -Error: failed to upsert network rule: admission webhook \"vworkspace.kb.io\" denied the request: network rules check failed: port validation failed: port=124000 -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 124000 2'] -SnapShot = 'failed to upsert network rule: admission webhook "vworkspace.kb.io" denied the request: network rules check failed: port validation failed: port=124000' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix main 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix main 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 --host-prefix main 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 1'] -SnapShot = """ -\u001B[32mSuccessfully add network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace ws1 --port 4000 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 4000, - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace xxx --port 4000 1'] -SnapShot = """ -Error: failed to get workspace: failed to get workspace: workspaces.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user user1 --workspace xxx --port 4000 2'] -SnapShot = 'failed to get workspace: failed to get workspace: workspaces.cosmo-workspace.github.io "xxx" not found' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user xxx --workspace ws1 --port 4000 1'] -SnapShot = """ -Error: failed to get workspace: failed to get user: users.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create --user xxx --workspace ws1 --port 4000 2'] -SnapShot = 'failed to get workspace: failed to get user: users.cosmo-workspace.github.io "xxx" not found' - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create 1'] -SnapShot = """ -Error: validation error: --workspace is required -Usage: - cosmoctl networkrule create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER [flags] - -Aliases: - create, add - -Flags: - -h, --help help for create - --host-prefix string custom host prefix - -n, --namespace string namespace - --path string path for Ingress path when using ingress (default \"/\") - --port int32 serivce port number (Required) - --public disable authentication for this port - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [create] ❌ fail with invalid args: netrule create 2'] -SnapShot = 'validation error: --workspace is required' - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 0 1'] -SnapShot = """ -\u001B[32mSuccessfully remove network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 0 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 0 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 1111, - \"customHostPrefix\": \"nw1\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw2\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 1 1'] -SnapShot = """ -\u001B[32mSuccessfully remove network rule for workspace 'ws1' -\u001B[0m""" - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 1 2'] -SnapShot = 'success' - -['cosmoctl [netrule] [delete] ✅ success in normal context: netrule delete --user user1 --workspace ws1 --index 1 3'] -SnapShot = """ -{ - \"metadata\": { - \"name\": \"ws1\", - \"namespace\": \"cosmo-user-user1\", - \"creationTimestamp\": null - }, - \"spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - }, - { - \"protocol\": \"http\", - \"portNumber\": 2222, - \"customHostPrefix\": \"nw2\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [netrule] [delete] ❌ fail with an unexpected error at update: netrule delete --user user1 --workspace ws1 --index 1 1'] -SnapShot = """ -Error: failed to remove network rule: mock update error -Usage: - cosmoctl networkrule delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME [flags] - -Aliases: - delete, rm - -Flags: - -h, --help help for delete - --index int network rule index (Required) (default -1) - -n, --namespace string namespace - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [delete] ❌ fail with an unexpected error at update: netrule delete --user user1 --workspace ws1 --index 1 2'] -SnapShot = 'failed to remove network rule: mock update error' - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace ws1 --index -1 1'] -SnapShot = """ -Error: index out of range -Usage: - cosmoctl networkrule delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME [flags] - -Aliases: - delete, rm - -Flags: - -h, --help help for delete - --index int network rule index (Required) (default -1) - -n, --namespace string namespace - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace ws1 --index -1 2'] -SnapShot = 'index out of range' - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace ws1 --index 3 1'] -SnapShot = """ -Error: index out of range -Usage: - cosmoctl networkrule delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME [flags] - -Aliases: - delete, rm - -Flags: - -h, --help help for delete - --index int network rule index (Required) (default -1) - -n, --namespace string namespace - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace ws1 --index 3 2'] -SnapShot = 'index out of range' - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace xxx --index 1 1'] -SnapShot = """ -Error: failed to get workspace: workspaces.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl networkrule delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME [flags] - -Aliases: - delete, rm - -Flags: - -h, --help help for delete - --index int network rule index (Required) (default -1) - -n, --namespace string namespace - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user user1 --workspace xxx --index 1 2'] -SnapShot = 'failed to get workspace: workspaces.cosmo-workspace.github.io "xxx" not found' - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user xxx --workspace ws1 --index 1 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl networkrule delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME [flags] - -Aliases: - delete, rm - -Flags: - -h, --help help for delete - --index int network rule index (Required) (default -1) - -n, --namespace string namespace - -u, --user string user name - --workspace string workspace name (Required) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [netrule] [delete] ❌ fail with invalid args: netrule delete --user xxx --workspace ws1 --index 1 2'] -SnapShot = 'failed to get user: users.cosmo-workspace.github.io "xxx" not found' diff --git a/internal/cmd/__snapshots__/root_cmd_test.snap b/internal/cmd/__snapshots__/root_cmd_test.snap new file mode 100644 index 00000000..d7c532b5 --- /dev/null +++ b/internal/cmd/__snapshots__/root_cmd_test.snap @@ -0,0 +1,36 @@ +['help should match snapshot 1'] +SnapShot = """ + +Command line tool for cosmo API +Complete documentation is available at http://github.com/cosmo-workspace/cosmo + +MIT 2024 cosmo-workspace/cosmo + +Usage: + cosmoctl [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + create Create cosmo resources + delete Delete cosmo resources + get Get cosmo resources + help Help about any command + login Login to COSMO Dashboard Server + resume Start stopped workspaces + suspend Suspend workspaces + template Manipulate Template resource + user Manipulate User resource + version Print the version number + workspace Manipulate Workspace resource + +Flags: + --config string cosmoctl config file path. env:COSMOCTL_CONFIG (default: $HOME/.config/cosmocfg) + --context string kube-context (default: current context) + --dashboard-url string COSMO Dashboard server endpoint URL. env:COSMOCTL_DASHBOARD_URL + -h, --help help for cosmoctl + -k, --kube use kubernetes API client instead of cosmo dashboard API client + --kubeconfig string kubeconfig file path. env:KUBECONFIG (default: $HOME/.kube/config) + -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL + +Use \"cosmoctl [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/__snapshots__/template_test.snap b/internal/cmd/__snapshots__/template_test.snap deleted file mode 100644 index 16e93b96..00000000 --- a/internal/cmd/__snapshots__/template_test.snap +++ /dev/null @@ -1,815 +0,0 @@ -['cosmoctl [template] [generate] ✅ success in normal context: template generate --user-addon --set-default-user-addon --cluster-scope --disable-nameprefix 1'] -SnapShot = """ -# Generated by cosmoctl - cosmo vX.X.X cosmo-workspace 2023 -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: ClusterTemplate -metadata: - annotations: - cosmo-workspace.github.io/disable-nameprefix: \"true\" - useraddon.cosmo-workspace.github.io/default: \"true\" - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: useraddon - name: cmd -spec: - rawYaml: | - apiVersion: v1 - kind: Service - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - ports: - - name: main - port: 3000 - protocol: TCP - selector: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - type: ClusterIP - --- - apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - replicas: 1 - selector: - matchLabels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - template: - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - spec: - containers: - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: workspace - port: - name: main - path: /* - pathType: Exact - -""" - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --user-addon --set-default-user-addon --cluster-scope --disable-nameprefix 2'] -SnapShot = 'success' - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --user-addon --set-default-user-addon --disable-nameprefix 1'] -SnapShot = """ -# Generated by cosmoctl - cosmo vX.X.X cosmo-workspace 2023 -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Template -metadata: - annotations: - cosmo-workspace.github.io/disable-nameprefix: \"true\" - useraddon.cosmo-workspace.github.io/default: \"true\" - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: useraddon - name: cmd -spec: - rawYaml: | - apiVersion: v1 - kind: Service - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - ports: - - name: main - port: 3000 - protocol: TCP - selector: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - type: ClusterIP - --- - apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - replicas: 1 - selector: - matchLabels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - template: - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - spec: - containers: - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: workspace - namespace: '{{NAMESPACE}}' - spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: workspace - port: - name: main - path: /* - pathType: Exact - -""" - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --user-addon --set-default-user-addon --disable-nameprefix 2'] -SnapShot = 'success' - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --userroles teama-* 1'] -SnapShot = """ -# Generated by cosmoctl - cosmo vX.X.X cosmo-workspace 2023 -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Template -metadata: - annotations: - cosmo-workspace.github.io/userroles: teama-* - workspace.cosmo-workspace.github.io/deployment: workspace - workspace.cosmo-workspace.github.io/service: workspace - workspace.cosmo-workspace.github.io/service-main-port: main - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: workspace - name: cmd -spec: - rawYaml: | - apiVersion: v1 - kind: Service - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - ports: - - name: main - port: 3000 - protocol: TCP - selector: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - type: ClusterIP - --- - apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - replicas: 1 - selector: - matchLabels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - template: - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - spec: - containers: - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: '{{INSTANCE}}-workspace' - port: - name: main - path: /* - pathType: Exact - -""" - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --userroles teama-* 2'] -SnapShot = 'success' - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --workspace-main-service-port-name main --required-vars HOGE:HOGEHOGE,FUGA:FUGAFUGA 1'] -SnapShot = """ -# Generated by cosmoctl - cosmo vX.X.X cosmo-workspace 2023 -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Template -metadata: - annotations: - workspace.cosmo-workspace.github.io/deployment: workspace - workspace.cosmo-workspace.github.io/service: workspace - workspace.cosmo-workspace.github.io/service-main-port: main - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: workspace - name: cmd -spec: - rawYaml: | - apiVersion: v1 - kind: Service - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - ports: - - name: main - port: 3000 - protocol: TCP - selector: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - type: ClusterIP - --- - apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - replicas: 1 - selector: - matchLabels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - template: - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - spec: - containers: - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: '{{INSTANCE}}-workspace' - port: - name: main - path: /* - pathType: Exact - requiredVars: - - default: HOGEHOGE - var: HOGE - - default: FUGAFUGA - var: FUGA - -""" - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --workspace-main-service-port-name main --required-vars HOGE:HOGEHOGE,FUGA:FUGAFUGA 2'] -SnapShot = 'success' - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --workspace-main-service-port-name main -o /tmp/test-cosmo-template 1'] -SnapShot = '' - -['cosmoctl [template] [generate] ✅ success in normal context: template generate --workspace --workspace-main-service-port-name main -o /tmp/test-cosmo-template 2'] -SnapShot = 'success' - -['cosmoctl [template] [generate] ❌ fail with invalid args: template generate --workspace --user-addon --workspace-main-service-port-name main 1'] -SnapShot = """ -Error: validation error: --workspace and --user-addon cannot be specified concurrently -Usage: - cosmoctl template generate --name TEMPLATE_NAME [< Input via Stdin or pipe] [flags] - -Aliases: - generate, gen - -Flags: - --cluster-scope generate ClusterTemplate (default generate namespaced Template) - --desc string template description - --disable-nameprefix disable adding instance name prefix on child resource name - -h, --help help for generate - -n, --name string template name (use directory name if not specified) - -o, --output string write output into file (default: Stdout) - --required-useraddons strings required user addons - --required-vars strings template custom vars to be replaced by instance. format --required-vars VAR1,VAR2:default-value - --set-default-user-addon set default user addon - --user-addon template as type useraddon - --useraddon template as type useraddon - --userroles strings user roles to show this template (e.g. 'teama-*', 'teamb-admin', etc.) - --workspace template as type workspace - --workspace-deployment-name string Deployment name for Workspace. use with --workspace (auto detected if not specified) - --workspace-main-service-port-name string ServicePort name for Workspace main container port. use with --workspace (auto detected if not specified) - --workspace-service-name string Service name for Workspace. use with --workspace (auto detected if not specified) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [generate] ❌ fail with invalid args: template generate --workspace --user-addon --workspace-main-service-port-name main 2'] -SnapShot = 'validation error: --workspace and --user-addon cannot be specified concurrently' - -['cosmoctl [template] [get] ✅ success in normal context: template get --workspace 1'] -SnapShot = """ -NAME REQUIRED_VARS USERROLE REQUIRED_ADDONS -template1 {{HOGE}},{{FUGA}} -template2 {{HOGE}},{{FUGA}} -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get --workspace 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get 1'] -SnapShot = """ -TYPE NAME CLUSTERSCOPE REQUIRED_VARS DEFAULT USERROLE REQUIRED_ADDONS -workspace template1 false {{HOGE}},{{FUGA}} true -workspace template2 false {{HOGE}},{{FUGA}} true -useraddon template3 false {{HOGE}},{{FUGA}} true -useraddon cluster-template1 true {{HOGE}},{{FUGA}} -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get notfound 1'] -SnapShot = """ -TYPE NAME CLUSTERSCOPE REQUIRED_VARS DEFAULT USERROLE REQUIRED_ADDONS -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get notfound 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 --workspace 1'] -SnapShot = """ -NAME REQUIRED_VARS USERROLE REQUIRED_ADDONS -template2 {{HOGE}},{{FUGA}} -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 --workspace 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 1'] -SnapShot = """ -TYPE NAME CLUSTERSCOPE REQUIRED_VARS DEFAULT USERROLE REQUIRED_ADDONS -workspace template2 false {{HOGE}},{{FUGA}} true -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 cluster-template1 notfound 1'] -SnapShot = """ -TYPE NAME CLUSTERSCOPE REQUIRED_VARS DEFAULT USERROLE REQUIRED_ADDONS -workspace template2 false {{HOGE}},{{FUGA}} true -useraddon cluster-template1 true {{HOGE}},{{FUGA}} -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 cluster-template1 notfound 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 template3 1'] -SnapShot = """ -TYPE NAME CLUSTERSCOPE REQUIRED_VARS DEFAULT USERROLE REQUIRED_ADDONS -workspace template2 false {{HOGE}},{{FUGA}} true -useraddon template3 false {{HOGE}},{{FUGA}} true -""" - -['cosmoctl [template] [get] ✅ success in normal context: template get template2 template3 2'] -SnapShot = 'success' - -['cosmoctl [template] [get] ❌ fail with an unexpected error at list users: template get --workspace 1'] -SnapShot = """ -Error: mock list error -Usage: - cosmoctl template get [flags] - -Flags: - -h, --help help for get - --useraddon show type useraddon template - --workspace show type workspace template - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [get] ❌ fail with an unexpected error at list users: template get --workspace 2'] -SnapShot = 'mock list error' - -['cosmoctl [template] [get] ❌ fail with an unexpected error at list users: template get 1'] -SnapShot = """ -Error: mock list error -Usage: - cosmoctl template get [flags] - -Flags: - -h, --help help for get - --useraddon show type useraddon template - --workspace show type workspace template - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [get] ❌ fail with an unexpected error at list users: template get 2'] -SnapShot = 'mock list error' - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file - --client -v 10 1'] -SnapShot = """ -APIVERSION KIND NAME RESULT MESSAGE -rbac.authorization.k8s.io/v1 RoleBinding cosmoctl-validate-XXXXXXXX-cosmo-auth-proxy-role OK -v1 Service cosmoctl-validate-XXXXXXXX-workspace OK -v1 PersistentVolumeClaim cosmoctl-validate-XXXXXXXX-workspace OK -apps/v1 Deployment cosmoctl-validate-XXXXXXXX-workspace OK -networking.k8s.io/v1 Ingress cosmoctl-validate-XXXXXXXX-workspace OK -""" - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file - --client -v 10 2'] -SnapShot = 'success' - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file - 1'] -SnapShot = """ -APIVERSION KIND NAME RESULT MESSAGE -rbac.authorization.k8s.io/v1 RoleBinding cosmoctl-validate-XXXXXXXX-cosmo-auth-proxy-role OK -v1 Service cosmoctl-validate-XXXXXXXX-workspace OK -v1 PersistentVolumeClaim cosmoctl-validate-XXXXXXXX-workspace OK -apps/v1 Deployment cosmoctl-validate-XXXXXXXX-workspace OK -networking.k8s.io/v1 Ingress cosmoctl-validate-XXXXXXXX-workspace NG dryrun failed: Ingress.networking.k8s.io \"cosmoctl-validate-XXXXXXXX-workspace\" is invalid: spec.rules[0].host: Invalid value: \"main-cosmoctl-validate-XXXXXXXX-default.{{DOMAIN}}\": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*') -""" - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file - 2'] -SnapShot = 'success' - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file /tmp/test-template.yaml --vars DOMAIN:example.com 1'] -SnapShot = """ -APIVERSION KIND NAME RESULT MESSAGE -rbac.authorization.k8s.io/v1 RoleBinding cosmoctl-validate-XXXXXXXX-cosmo-auth-proxy-role OK -v1 Service cosmoctl-validate-XXXXXXXX-workspace OK -v1 PersistentVolumeClaim cosmoctl-validate-XXXXXXXX-workspace OK -apps/v1 Deployment cosmoctl-validate-XXXXXXXX-workspace OK -networking.k8s.io/v1 Ingress cosmoctl-validate-XXXXXXXX-workspace OK -""" - -['cosmoctl [template] [validate] ✅ success in normal context: template validate --file /tmp/test-template.yaml --vars DOMAIN:example.com 2'] -SnapShot = 'success' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file - --vars HOGE 1'] -SnapShot = """ -Error: invalid options: vars format error: vars HOGE must be 'VAR:VAL' -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file - --vars HOGE 2'] -SnapShot = """ -invalid options: vars format error: vars HOGE must be 'VAR:VAL'""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/(xx*xx) 1'] -SnapShot = """ -Error: invalid options: failed to read input file: open /tmp/(xx*xx): no such file or directory -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/(xx*xx) 2'] -SnapShot = 'invalid options: failed to read input file: open /tmp/(xx*xx): no such file or directory' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-empty-template.yaml 1'] -SnapShot = """ -Error: invalid options: no input -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-empty-template.yaml 2'] -SnapShot = 'invalid options: no input' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-invalid-template.yaml 1'] -SnapShot = """ -Error: invalid options: failed to unmarshal yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v1alpha1.Template -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-invalid-template.yaml 2'] -SnapShot = 'invalid options: failed to unmarshal yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type v1alpha1.Template' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-user-addon-template.yaml 1'] -SnapShot = """ -Error: invalid options: required vars not given. set --var REQUIRED_VAR: -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file /tmp/test-user-addon-template.yaml 2'] -SnapShot = 'invalid options: required vars not given. set --var REQUIRED_VAR:' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file 1'] -SnapShot = """ -Error: flag needs an argument: --file -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate --file 2'] -SnapShot = 'flag needs an argument: --file' - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate 1'] -SnapShot = """ -Error: validation error: --file is required -Usage: - cosmoctl template validate --file FILE [flags] - -Aliases: - validate, valid, check - -Flags: - --client dry-run on client-side. kubectl is required to be executable in PATH - -f, --file string input COSMO Template file yaml path. when specified '-', input from Stdin - -h, --help help for validate - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [template] [validate] ❌ fail with invalid args: template validate 2'] -SnapShot = 'validation error: --file is required' diff --git a/internal/cmd/__snapshots__/user_test.snap b/internal/cmd/__snapshots__/user_test.snap deleted file mode 100644 index e1556b73..00000000 --- a/internal/cmd/__snapshots__/user_test.snap +++ /dev/null @@ -1,1001 +0,0 @@ -['cosmoctl [user] [all] ❌ fail with invalid arg: kubeconfig user create user1 --kubeconfig XXXX 1'] -SnapShot = """ -Error: invalid options: stat XXXX: no such file or directory -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [all] ❌ fail with invalid arg: kubeconfig user delete user1 --kubeconfig XXXX 1'] -SnapShot = """ -Error: invalid options: stat XXXX: no such file or directory -Usage: - cosmoctl user delete USER_NAME [flags] - -Aliases: - delete, del - -Flags: - -h, --help help for delete - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [all] ❌ fail with invalid arg: kubeconfig user get --kubeconfig XXXX 1'] -SnapShot = """ -Error: invalid options: stat XXXX: no such file or directory -Usage: - cosmoctl user get [flags] - -Flags: - --filter strings filter option. 'role' and 'addon' are available for now. e.g. 'role=x', 'addon=y' - -h, --help help for get - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [all] ❌ fail with invalid arg: kubeconfig user reset-password user1 --password XXXXXXXX --kubeconfig XXXX 1'] -SnapShot = """ -Error: invalid options: stat XXXX: no such file or directory -Usage: - cosmoctl user reset-password USER_NAME [flags] - -Flags: - -h, --help help for reset-password - --password string new password (default: random string) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 --cluster-addon user-clustertemplate1 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 --cluster-addon user-clustertemplate1 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 --cluster-addon user-clustertemplate1 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - }, - { - \"template\": { - \"name\": \"user-clustertemplate1\", - \"clusterScoped\": true - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1,HOGE: HOGE HOGE ,FUGA:FUGAF:UGA 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1,HOGE: HOGE HOGE ,FUGA:FUGAF:UGA 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --addon user-template1,HOGE: HOGE HOGE ,FUGA:FUGAF:UGA 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - }, - \"vars\": { - \"FUGA\": \"FUGAF:UGA\", - \"HOGE\": \" HOGE HOGE \" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --admin --role cosmo-admin 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --admin --role cosmo-admin 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --admin --role cosmo-admin 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"roles\": [ - { - \"name\": \"cosmo-admin\" - } - ], - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --admin --addon user-template1,HOGE:HOGEHOGE 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --admin --addon user-template1,HOGE:HOGEHOGE 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --admin --addon user-template1,HOGE:HOGEHOGE 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"create 1\", - \"roles\": [ - { - \"name\": \"cosmo-admin\" - } - ], - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - }, - \"vars\": { - \"HOGE\": \"HOGEHOGE\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --addon user-template1,HOGE:HOGEHOGE 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --addon user-template1,HOGE:HOGEHOGE 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --addon user-template1,HOGE:HOGEHOGE 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"create 1\", - \"roles\": [ - { - \"name\": \"cosmo-admin\" - } - ], - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - }, - \"vars\": { - \"HOGE\": \"HOGEHOGE\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type ldap user-template1,HOGE:HOGEHOGE 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0m""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type ldap user-template1,HOGE:HOGEHOGE 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type ldap user-template1,HOGE:HOGEHOGE 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"create 1\", - \"roles\": [ - { - \"name\": \"cosmo-admin\" - } - ], - \"authType\": \"ldap\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type password-secret user-template1,HOGE:HOGEHOGE 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type password-secret user-template1,HOGE:HOGEHOGE 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --name create 1 --role cosmo-admin --auth-type password-secret user-template1,HOGE:HOGEHOGE 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"create 1\", - \"roles\": [ - { - \"name\": \"cosmo-admin\" - } - ], - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --role xxx 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --role xxx 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create --role xxx 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"roles\": [ - { - \"name\": \"xxx\" - } - ], - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success in normal context: user create user-create 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"authType\": \"password-secret\", - \"addons\": [ - { - \"template\": { - \"name\": \"user-template1\" - } - } - ] - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success to create password immediately: user create user-create 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success to create password immediately: user create user-create 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success to create password immediately: user create user-create 3'] -SnapShot = """ -{ - \"Name\": \"user-create\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create\", - \"authType\": \"password-secret\" - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ✅ success to create password later: user create user-create-later 1'] -SnapShot = """ -\u001B[32mSuccessfully created user user-create-later -\u001B[0mDefault password: xxxxxxxx -""" - -['cosmoctl [user] [create] ✅ success to create password later: user create user-create-later 2'] -SnapShot = 'success' - -['cosmoctl [user] [create] ✅ success to create password later: user create user-create-later 3'] -SnapShot = """ -{ - \"Name\": \"user-create-later\", - \"Namespace\": \"\", - \"Spec\": { - \"displayName\": \"user-create-later\", - \"authType\": \"password-secret\" - }, - \"Status\": { - \"namespace\": {} - } -} -""" - -['cosmoctl [user] [create] ❌ fail to create password timeout user create user-create-timeout 1'] -SnapShot = """ -Error: reached to timeout in user creation -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail to create password timeout user create user-create-timeout 2'] -SnapShot = 'reached to timeout in user creation' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create --admin 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create --admin 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create TESTuser 1'] -SnapShot = """ -Error: failed to create user: User.cosmo-workspace.github.io \"TESTuser\" is invalid: metadata.name: Invalid value: \"TESTuser\": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*') -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create TESTuser 2'] -SnapShot = """ -failed to create user: User.cosmo-workspace.github.io \"TESTuser\" is invalid: metadata.name: Invalid value: \"TESTuser\": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon XXXXXXXXX,HOGE:yyy 1'] -SnapShot = """ -Error: failed to create user: admission webhook \"vuser.kb.io\" denied the request: failed to create addon XXXXXXXXX :templates.cosmo-workspace.github.io \"XXXXXXXXX\" not found -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon XXXXXXXXX,HOGE:yyy 2'] -SnapShot = 'failed to create user: admission webhook "vuser.kb.io" denied the request: failed to create addon XXXXXXXXX :templates.cosmo-workspace.github.io "XXXXXXXXX" not found' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon user-template1 ,HOGE:yyy 1'] -SnapShot = """ -Error: invalid options: invalid addon vars format: user-template1 ,HOGE:yyy -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon user-template1 ,HOGE:yyy 2'] -SnapShot = 'invalid options: invalid addon vars format: user-template1 ,HOGE:yyy' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon user-template1,HOGE :yyy 1'] -SnapShot = """ -Error: invalid options: invalid addon vars format: user-template1,HOGE :yyy -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --addon user-template1,HOGE :yyy 2'] -SnapShot = 'invalid options: invalid addon vars format: user-template1,HOGE :yyy' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --auth-type xxxx 1'] -SnapShot = """ -Error: validation error: invalid auth-type: xxxx -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --auth-type xxxx 2'] -SnapShot = 'validation error: invalid auth-type: xxxx' - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --cluster-addon user-clustertemplate1,HOGE : 1'] -SnapShot = """ -Error: invalid options: invalid addon vars format: user-clustertemplate1,HOGE : -Usage: - cosmoctl user create USER_NAME --role cosmo-admin [flags] - -Flags: - --addon stringArray user addons - format is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' - --admin user admin role - --auth-type string user auth type 'password-secret'(default),'ldap' (default \"password-secret\") - --cluster-addon stringArray user addons by ClusterTemplate - format is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' - --display-name string user display name (default: same as USER_NAME) - -h, --help help for create - --name string [DEPRICATED] use --display-name - --role strings user roles - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [create] ❌ fail with invalid args: user create user-create --cluster-addon user-clustertemplate1,HOGE : 2'] -SnapShot = 'invalid options: invalid addon vars format: user-clustertemplate1,HOGE :' - -['cosmoctl [user] [delete] ✅ success in normal context: user delete user-delete1 1'] -SnapShot = """ -\u001B[32mSuccessfully deleted user user-delete1 -\u001B[0m""" - -['cosmoctl [user] [delete] ✅ success in normal context: user delete user-delete1 2'] -SnapShot = 'success' - -['cosmoctl [user] [delete] ❌ fail with an unexpected error at delete: user delete user-delete1 1'] -SnapShot = """ -Error: failed to delete user: mock delete user error -Usage: - cosmoctl user delete USER_NAME [flags] - -Aliases: - delete, del - -Flags: - -h, --help help for delete - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [delete] ❌ fail with an unexpected error at delete: user delete user-delete1 2'] -SnapShot = 'failed to delete user: mock delete user error' - -['cosmoctl [user] [delete] ❌ fail with invalid args: user delete 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl user delete USER_NAME [flags] - -Aliases: - delete, del - -Flags: - -h, --help help for delete - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [delete] ❌ fail with invalid args: user delete 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [user] [delete] ❌ fail with invalid args: user delete XXXXX 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"XXXXX\" not found -Usage: - cosmoctl user delete USER_NAME [flags] - -Aliases: - delete, del - -Flags: - -h, --help help for delete - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [delete] ❌ fail with invalid args: user delete XXXXX 2'] -SnapShot = 'failed to get user: users.cosmo-workspace.github.io "XXXXX" not found' - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=*-admin --filter role=myteam-* 1'] -SnapShot = """ -NAME ROLES AUTHTYPE NAMESPACE PHASE ADDONS -user3 myteam-admin password-secret -""" - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=*-admin --filter role=myteam-* 2'] -SnapShot = 'success' - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=*-admin 1'] -SnapShot = """ -NAME ROLES AUTHTYPE NAMESPACE PHASE ADDONS -user2 cosmo-admin password-secret -user3 myteam-admin password-secret -""" - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=*-admin 2'] -SnapShot = 'success' - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=cosmo-admin 1'] -SnapShot = """ -NAME ROLES AUTHTYPE NAMESPACE PHASE ADDONS -user2 cosmo-admin password-secret -""" - -['cosmoctl [user] [get] ✅ success in normal context: user get --filter role=cosmo-admin 2'] -SnapShot = 'success' - -['cosmoctl [user] [get] ✅ success in normal context: user get 1'] -SnapShot = """ -NAME ROLES AUTHTYPE NAMESPACE PHASE ADDONS -user1 password-secret -user2 cosmo-admin password-secret -user3 myteam-admin password-secret -""" - -['cosmoctl [user] [get] ✅ success in normal context: user get 2'] -SnapShot = 'success' - -['cosmoctl [user] [get] ✅ success with empty user: user get 1'] -SnapShot = """ -NAME ROLES AUTHTYPE NAMESPACE PHASE ADDONS -""" - -['cosmoctl [user] [get] ✅ success with empty user: user get 2'] -SnapShot = 'success' - -['cosmoctl [user] [get] ❌ fail with an unexpected error at list: user get 1'] -SnapShot = """ -Error: failed to list users: mock user list error -Usage: - cosmoctl user get [flags] - -Flags: - --filter strings filter option. 'role' and 'addon' are available for now. e.g. 'role=x', 'addon=y' - -h, --help help for get - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [get] ❌ fail with an unexpected error at list: user get 2'] -SnapShot = 'failed to list users: mock user list error' - -['cosmoctl [user] [get] ❌ fail with invalid args: user get --filter x 1'] -SnapShot = """ -Error: invalid options: invalid filter expression: x -Usage: - cosmoctl user get [flags] - -Flags: - --filter strings filter option. 'role' and 'addon' are available for now. e.g. 'role=x', 'addon=y' - -h, --help help for get - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [get] ❌ fail with invalid args: user get --filter x 2'] -SnapShot = 'invalid options: invalid filter expression: x' - -['cosmoctl [user] [get] ❌ fail with invalid args: user get --filter x=x 1'] -SnapShot = """ -Error: invalid options: invalid filter expression: x=x -Usage: - cosmoctl user get [flags] - -Flags: - --filter strings filter option. 'role' and 'addon' are available for now. e.g. 'role=x', 'addon=y' - -h, --help help for get - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [get] ❌ fail with invalid args: user get --filter x=x 2'] -SnapShot = 'invalid options: invalid filter expression: x=x' - -['cosmoctl [user] [reset-password] ✅ success in normal context: user reset-password user1 --password XXXXXXXX 1'] -SnapShot = """ -\u001B[32mSuccessfully reset password: user user1 -\u001B[0m""" - -['cosmoctl [user] [reset-password] ✅ success in normal context: user reset-password user1 --password XXXXXXXX 2'] -SnapShot = 'success' - -['cosmoctl [user] [reset-password] ✅ success in normal context: user reset-password user1 1'] -SnapShot = """ -\u001B[32mSuccessfully reset password: user user1 -\u001B[0mNew password: xxxxxxxx -""" - -['cosmoctl [user] [reset-password] ✅ success in normal context: user reset-password user1 2'] -SnapShot = 'success' - -['cosmoctl [user] [reset-password] ❌ fail with an unexpected error at update: user reset-password user1 1'] -SnapShot = """ -\u001B[32mSuccessfully reset password: user user1 -\u001B[0mError: failed to get default password: failed to get password secret: mock get error -Usage: - cosmoctl user reset-password USER_NAME [flags] - -Flags: - -h, --help help for reset-password - --password string new password (default: random string) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [reset-password] ❌ fail with an unexpected error at update: user reset-password user1 2'] -SnapShot = 'failed to get default password: failed to get password secret: mock get error' - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl user reset-password USER_NAME [flags] - -Flags: - -h, --help help for reset-password - --password string new password (default: random string) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password XXXXXX 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"XXXXXX\" not found -Usage: - cosmoctl user reset-password USER_NAME [flags] - -Flags: - -h, --help help for reset-password - --password string new password (default: random string) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password XXXXXX 2'] -SnapShot = 'failed to get user: users.cosmo-workspace.github.io "XXXXXX" not found' - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password user1 --password 1'] -SnapShot = """ -\u001B[32mSuccessfully reset password: user user1 -\u001B[0mNew password: xxxxxxxx -""" - -['cosmoctl [user] [reset-password] ❌ fail with invalid args: user reset-password user1 --password 2'] -SnapShot = 'success' diff --git a/internal/cmd/__snapshots__/workspace_test.snap b/internal/cmd/__snapshots__/workspace_test.snap deleted file mode 100644 index 2c53ae8b..00000000 --- a/internal/cmd/__snapshots__/workspace_test.snap +++ /dev/null @@ -1,1276 +0,0 @@ -['cosmoctl [workspace] [create] ✅ success in normal context: workspace create ws1 --user user1 --template template1 --vars HOGE:HOGEHOGE 1'] -SnapShot = """ -\u001B[32mSuccessfully created workspace ws1 -\u001B[0m""" - -['cosmoctl [workspace] [create] ✅ success in normal context: workspace create ws1 --user user1 --template template1 --vars HOGE:HOGEHOGE 2'] -SnapShot = """ -{ - \"Name\": \"ws1\", - \"Namespace\": \"cosmo-user-user1\", - \"Spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"vars\": { - \"HOGE\": \"HOGEHOGE\" - }, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"Status\": { - \"instance\": {}, - \"config\": {} - } -} -""" - -['cosmoctl [workspace] [create] ✅ success in normal context: workspace create ws1 --user user1 --template template1 1'] -SnapShot = """ -\u001B[32mSuccessfully created workspace ws1 -\u001B[0m""" - -['cosmoctl [workspace] [create] ✅ success in normal context: workspace create ws1 --user user1 --template template1 2'] -SnapShot = """ -{ - \"Name\": \"ws1\", - \"Namespace\": \"cosmo-user-user1\", - \"Spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"Status\": { - \"instance\": {}, - \"config\": {} - } -} -""" - -['cosmoctl [workspace] [create] ✅ success with dry-run: workspace create ws1 --user user1 --template template1 --vars HOGE:HOGEHOGE --dry-run 1'] -SnapShot = """ -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Workspace -metadata: - creationTimestamp: xxxxxxxx - generation: 1 - managedFields: - - apiVersion: cosmo-workspace.github.io/v1alpha1 - fieldsType: FieldsV1 - fieldsV1: - f:spec: - .: {} - f:template: - .: {} - f:name: {} - f:vars: - .: {} - f:HOGE: {} - manager: cmd.test - operation: Update - time: xxxxxxxx - name: ws1 - namespace: cosmo-user-user1 - uid: xxxxxxxx -spec: - network: - - customHostPrefix: main - httpPath: / - portNumber: 18080 - protocol: http - public: false - replicas: 1 - template: - name: template1 - vars: - HOGE: HOGEHOGE -status: - config: - mainServicePortName: main - serviceName: workspace - instance: {} - phase: Pending - -\u001B[32mSuccessfully created workspace ws1 (dry-run) -\u001B[0m""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create --user user1 --template template1 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --namespace xxxx --template template1 1'] -SnapShot = """ -Error: invalid options: namespace xxxx is not cosmo user's namespace -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user --template template1 1'] -SnapShot = """ -Error: validation error: --template is required -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user user1 --namespace user1 --template template1 1'] -SnapShot = """ -Error: validation error: --user and --namespace connot be used at the same time -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user user1 --template 1'] -SnapShot = """ -Error: flag needs an argument: --template -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user user1 --template template1 --all-namespaces 1'] -SnapShot = """ -Error: unknown flag: --all-namespaces -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user user1 --template template1 --vars HOGE 1'] -SnapShot = """ -Error: invalid options: vars format error: vars HOGE must be 'VAR:VAL' -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user xxxxx --template template1 --dry-run 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxxxx\" not found -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [create] ❌ fail with invalid args: workspace create ws1 --user xxxxx --template template1 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxxxx\" not found -Usage: - cosmoctl workspace create WORKSPACE_NAME --template TEMPLATE_NAME [flags] - -Examples: -create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10 - -Flags: - --dry-run dry run - -h, --help help for create - -n, --namespace string namespace - -t, --template string template name - -u, --user string user name - --vars string template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2) - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ✅ success in normal context: workspace delete ws2 --namespace cosmo-user-user1 1'] -SnapShot = """ -\u001B[32mSuccessfully deleted workspace ws2 -\u001B[0m""" - -['cosmoctl [workspace] [delete] ✅ success in normal context: workspace delete ws2 --namespace cosmo-user-user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [delete] ✅ success in normal context: workspace delete ws2 --user user1 1'] -SnapShot = """ -\u001B[32mSuccessfully deleted workspace ws2 -\u001B[0m""" - -['cosmoctl [workspace] [delete] ✅ success in normal context: workspace delete ws2 --user user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [delete] ✅ success with dry-run: workspace delete ws2 --dry-run --namespace cosmo-user-user1 1'] -SnapShot = """ -\u001B[32mSuccessfully deleted workspace ws2 (dry-run) -\u001B[0m""" - -['cosmoctl [workspace] [delete] ✅ success with dry-run: workspace delete ws2 --dry-run --namespace cosmo-user-user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [delete] ✅ success with dry-run: workspace delete ws2 --dry-run --user user1 1'] -SnapShot = """ -\u001B[32mSuccessfully deleted workspace ws2 (dry-run) -\u001B[0m""" - -['cosmoctl [workspace] [delete] ✅ success with dry-run: workspace delete ws2 --dry-run --user user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [delete] ❌ fail with an unexpected error at delete: workspace delete ws1 --dry-run --user user1 1'] -SnapShot = """ -Error: failed to delete workspace: mock delete error -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with an unexpected error at delete: workspace delete ws1 --dry-run --user user1 2'] -SnapShot = 'failed to delete workspace: mock delete error' - -['cosmoctl [workspace] [delete] ❌ fail with an unexpected error at delete: workspace delete ws1 --user user1 1'] -SnapShot = """ -Error: failed to delete workspace: mock delete error -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with an unexpected error at delete: workspace delete ws1 --user user1 2'] -SnapShot = 'failed to delete workspace: mock delete error' - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --namespace cosmo-user-user1 --user user1 1'] -SnapShot = """ -Error: validation error: --user and --namespace connot be used at the same time -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --namespace cosmo-user-user1 --user user1 2'] -SnapShot = 'validation error: --user and --namespace connot be used at the same time' - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --namespace xxxx 1'] -SnapShot = """ -Error: invalid options: namespace xxxx is not cosmo user's namespace -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --namespace xxxx 2'] -SnapShot = """ -invalid options: namespace xxxx is not cosmo user's namespace""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --user user1 -A 1'] -SnapShot = """ -Error: unknown shorthand flag: 'A' in -A -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --user user1 -A 2'] -SnapShot = """ -unknown shorthand flag: 'A' in -A""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --user user1 xxx 1'] -SnapShot = """ -Error: failed to get workspace: workspaces.cosmo-workspace.github.io \"ws1\" not found -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete ws1 --user user1 xxx 2'] -SnapShot = 'failed to get workspace: workspaces.cosmo-workspace.github.io "ws1" not found' - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete xxxx --user user1 -A 1'] -SnapShot = """ -Error: unknown shorthand flag: 'A' in -A -Usage: - cosmoctl workspace delete WORKSPACE_NAME [flags] - -Aliases: - delete, del - -Flags: - --dry-run dry run - -h, --help help for delete - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [delete] ❌ fail with invalid args: workspace delete xxxx --user user1 -A 2'] -SnapShot = """ -unknown shorthand flag: 'A' in -A""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get --namespace cosmo-user-user1 1'] -SnapShot = """ -NAME TEMPLATE PHASE -ws1 template1 Pending -ws2 template1 Pending -""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get --namespace cosmo-user-user1 ws2 1'] -SnapShot = """ -NAME TEMPLATE PHASE -ws2 template1 Pending -""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get --user user1 1'] -SnapShot = """ -NAME TEMPLATE PHASE -ws1 template1 Pending -ws2 template1 Pending -""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get --user user1 ws2 1'] -SnapShot = """ -NAME TEMPLATE PHASE -ws2 template1 Pending -""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get -A --network 1'] -SnapShot = """ -USER NAME PORT URL PUBLIC -user1 ws1 18080 false -user1 ws2 18080 false -user1 ws2 1111 false -user1 ws2 2222 false -""" - -['cosmoctl [workspace] [get] ✅ success in normal context: workspace get -A 1'] -SnapShot = """ -USER NAME TEMPLATE PHASE -user1 ws1 template1 Pending -user1 ws2 template1 Pending -""" - -['cosmoctl [workspace] [get] ✅ success when workspace is empty: workspace get --all-namespaces 1'] -SnapShot = """ -USER NAME TEMPLATE PHASE -""" - -['cosmoctl [workspace] [get] ✅ success when workspace is empty: workspace get --namespace cosmo-user-user1 1'] -SnapShot = """ -NAME TEMPLATE PHASE -""" - -['cosmoctl [workspace] [get] ✅ success when workspace is empty: workspace get --user user1 1'] -SnapShot = """ -NAME TEMPLATE PHASE -""" - -['cosmoctl [workspace] [get] ✅ success when workspace is empty: workspace get -A --network 1'] -SnapShot = """ -USER NAME PORT URL PUBLIC -""" - -['cosmoctl [workspace] [get] ❌ fail with an unexpected error at list users: workspace get -A 1'] -SnapShot = """ -Error: failed to list users: mock listUsers error -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with an unexpected error at list workspace: workspace get --user user1 1'] -SnapShot = """ -Error: failed to list workspaces: mock listWorkspacesByUserName error -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with an unexpected error at list workspace: workspace get -A 1'] -SnapShot = """ -Error: failed to list workspaces: mock listWorkspacesByUserName error -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with invalid args: workspace get --namespace cosmo-user-user1 --user user1 1'] -SnapShot = """ -Error: validation error: --user and --namespace connot be used at the same time -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with invalid args: workspace get --namespace xxx 1'] -SnapShot = """ -Error: invalid options: namespace xxx is not cosmo user's namespace -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with invalid args: workspace get --user user1 xxx 1'] -SnapShot = """ -Error: failed to get workspace: workspaces.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with invalid args: workspace get --user xxxx 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxxx\" not found -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [get] ❌ fail with invalid args: workspace get -A --user user1 1'] -SnapShot = """ -Error: validation error: --all-namespaces connot be used with --namespace or --user -Usage: - cosmoctl workspace get [WORKSPACE_NAME] [flags] - -Flags: - -A, --all-namespaces all namespaces - -h, --help help for get - -n, --namespace string namespace - --network show workspace network - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ✅ success in normal context: workspace run-instance ws1 --user user1 1'] -SnapShot = """ -\u001B[32mSuccessfully run workspace ws1 -\u001B[0m""" - -['cosmoctl [workspace] [run-instance] ✅ success in normal context: workspace run-instance ws1 --user user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [run-instance] ✅ success in normal context: workspace run-instance ws1 --user user1 3'] -SnapShot = """ -{ - \"Name\": \"ws1\", - \"Namespace\": \"cosmo-user-user1\", - \"Spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 1, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"Status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with an unexpected error at update: workspace run-instance ws1 --user user1 1'] -SnapShot = """ -Error: failed to update workspace: mock update error -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with an unexpected error at update: workspace run-instance ws1 --user user1 2'] -SnapShot = 'failed to update workspace: mock update error' - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --namespace xxxxx 1'] -SnapShot = """ -Error: invalid options: namespace xxxxx is not cosmo user's namespace -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --namespace xxxxx 2'] -SnapShot = """ -invalid options: namespace xxxxx is not cosmo user's namespace""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user user1 --namespace cosmo-user-user1 1'] -SnapShot = """ -Error: validation error: --user and --namespace connot be used at the same time -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user user1 --namespace cosmo-user-user1 2'] -SnapShot = 'validation error: --user and --namespace connot be used at the same time' - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user user1 -A 1'] -SnapShot = """ -Error: unknown shorthand flag: 'A' in -A -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user user1 -A 2'] -SnapShot = """ -unknown shorthand flag: 'A' in -A""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user xxxxx 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxxxx\" not found -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws1 --user xxxxx 2'] -SnapShot = 'failed to get user: users.cosmo-workspace.github.io "xxxxx" not found' - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws2 --user user1 1'] -SnapShot = """ -Error: no change -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance ws2 --user user1 2'] -SnapShot = 'no change' - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance xxx --user user1 1'] -SnapShot = """ -Error: failed to get workspace: workspaces.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl workspace run-instance WORKSPACE_NAME [flags] - -Aliases: - run-instance, run - -Flags: - -h, --help help for run-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [run-instance] ❌ fail with invalid args: workspace run-instance xxx --user user1 2'] -SnapShot = 'failed to get workspace: workspaces.cosmo-workspace.github.io "xxx" not found' - -['cosmoctl [workspace] [stop-instance] ✅ success in normal context: workspace stop-instance ws1 --user user1 1'] -SnapShot = """ -\u001B[32mSuccessfully stopped workspace ws1 -\u001B[0m""" - -['cosmoctl [workspace] [stop-instance] ✅ success in normal context: workspace stop-instance ws1 --user user1 2'] -SnapShot = 'success' - -['cosmoctl [workspace] [stop-instance] ✅ success in normal context: workspace stop-instance ws1 --user user1 3'] -SnapShot = """ -{ - \"Name\": \"ws1\", - \"Namespace\": \"cosmo-user-user1\", - \"Spec\": { - \"template\": { - \"name\": \"template1\" - }, - \"replicas\": 0, - \"network\": [ - { - \"protocol\": \"http\", - \"portNumber\": 18080, - \"customHostPrefix\": \"main\", - \"httpPath\": \"/\", - \"public\": false - } - ] - }, - \"Status\": { - \"instance\": {}, - \"phase\": \"Pending\", - \"config\": { - \"serviceName\": \"workspace\", - \"mainServicePortName\": \"main\" - } - } -} -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with an unexpected error at update: workspace stop-instance ws1 --user user1 1'] -SnapShot = """ -Error: failed to update workspace: mock update error -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with an unexpected error at update: workspace stop-instance ws1 --user user1 2'] -SnapShot = 'failed to update workspace: mock update error' - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance 1'] -SnapShot = """ -Error: validation error: invalid args -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance 2'] -SnapShot = 'validation error: invalid args' - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --namespace xxxxx 1'] -SnapShot = """ -Error: invalid options: namespace xxxxx is not cosmo user's namespace -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --namespace xxxxx 2'] -SnapShot = """ -invalid options: namespace xxxxx is not cosmo user's namespace""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user user1 --namespace cosmo-user-user1 1'] -SnapShot = """ -Error: validation error: --user and --namespace connot be used at the same time -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user user1 --namespace cosmo-user-user1 2'] -SnapShot = 'validation error: --user and --namespace connot be used at the same time' - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user user1 -A 1'] -SnapShot = """ -Error: unknown shorthand flag: 'A' in -A -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user user1 -A 2'] -SnapShot = """ -unknown shorthand flag: 'A' in -A""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user xxxxx 1'] -SnapShot = """ -Error: failed to get user: users.cosmo-workspace.github.io \"xxxxx\" not found -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws1 --user xxxxx 2'] -SnapShot = 'failed to get user: users.cosmo-workspace.github.io "xxxxx" not found' - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws2 --user user1 1'] -SnapShot = """ -Error: no change -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance ws2 --user user1 2'] -SnapShot = 'no change' - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance xxx --user user1 1'] -SnapShot = """ -Error: failed to get workspace: workspaces.cosmo-workspace.github.io \"xxx\" not found -Usage: - cosmoctl workspace stop-instance WORKSPACE_NAME [flags] - -Aliases: - stop-instance, stop - -Flags: - -h, --help help for stop-instance - -n, --namespace string namespace - -u, --user string user name - -Global Flags: - --context string kube-context (default: current context) - --kubeconfig string kubeconfig file path (default: $HOME/.kube/config) - -v, --verbose int log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL (default -1) - -""" - -['cosmoctl [workspace] [stop-instance] ❌ fail with invalid args: workspace stop-instance xxx --user user1 2'] -SnapShot = 'failed to get workspace: workspaces.cosmo-workspace.github.io "xxx" not found' diff --git a/internal/cmd/create/__snapshots__/cmd_test.snap b/internal/cmd/create/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..af30aeb0 --- /dev/null +++ b/internal/cmd/create/__snapshots__/cmd_test.snap @@ -0,0 +1,17 @@ +['help should match snapshot 1'] +SnapShot = """ +Create cosmo resources + +Usage: + create [command] + +Available Commands: + network Upsert workspace network. Alias of 'cosmoctl workspace upsert-network' + user Create user. Alias of 'cosmoctl user create' + workspace Create workspace. Alias of 'cosmoctl workspace create' + +Flags: + -h, --help help for create + +Use \" create [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/create/cmd.go b/internal/cmd/create/cmd.go index 8dd1df1d..c2983f1e 100644 --- a/internal/cmd/create/cmd.go +++ b/internal/cmd/create/cmd.go @@ -1,38 +1,35 @@ package create import ( - "github.com/cosmo-workspace/cosmo/internal/cmd/netrule" "github.com/cosmo-workspace/cosmo/internal/cmd/user" "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/spf13/cobra" ) -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { createCmd := &cobra.Command{ Use: "create", Short: "Create cosmo resources", - Long: ` -Create cosmo resources -`, } - o := cmdutil.NewUserNamespacedCliOptions(co) + createCmd.AddCommand(user.CreateCmd(&cobra.Command{ + Use: "user USER_NAME", + Short: "Create user. Alias of 'cosmoctl user create'", + Aliases: []string{"us"}, + }, o)) createCmd.AddCommand(workspace.CreateCmd(&cobra.Command{ Use: "workspace WORKSPACE_NAME --template TEMPLATE_NAME", - Short: "Create workspace", - Example: "workspace my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10", + Short: "Create workspace. Alias of 'cosmoctl workspace create'", Aliases: []string{"ws"}, }, o)) - createCmd.AddCommand(user.CreateCmd(&cobra.Command{ - Use: "user USER_NAME --role cosmo-admin", - Short: "Create user", - }, o.CliOptions)) - createCmd.AddCommand(netrule.CreateCmd(&cobra.Command{ - Use: "networkrule --workspace WORKSPACE_NAME --port PORT_NUMBER", - Short: "Create or update workspace network rule", - Aliases: []string{"netrule", "net"}, + + createCmd.AddCommand(workspace.UpsertNetworkCmd(&cobra.Command{ + Use: "network WORKSPACE_NAME --port 8080", + Short: "Upsert workspace network. Alias of 'cosmoctl workspace upsert-network'", + Aliases: []string{"net", "workspace-network", "workspace-networks", "ws-net", "wsnet"}, }, o)) + cmd.AddCommand(createCmd) } diff --git a/internal/cmd/create/cmd_test.go b/internal/cmd/create/cmd_test.go new file mode 100644 index 00000000..16643c65 --- /dev/null +++ b/internal/cmd/create/cmd_test.go @@ -0,0 +1,31 @@ +package create + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandCreate(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl create suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"create", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/delete/__snapshots__/cmd_test.snap b/internal/cmd/delete/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..c01895f6 --- /dev/null +++ b/internal/cmd/delete/__snapshots__/cmd_test.snap @@ -0,0 +1,20 @@ +['help should match snapshot 1'] +SnapShot = """ +Delete cosmo resources + +Usage: + delete [command] + +Aliases: + delete, rm, remove + +Available Commands: + network Remove workspace network. Alias of 'cosmoctl workspace remove-network' + user Delete users. Alias of 'cosmoctl user delete' + workspace Delete workspaces. Alias of 'cosmoctl workspace delete' + +Flags: + -h, --help help for delete + +Use \" delete [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/delete/cmd.go b/internal/cmd/delete/cmd.go index 06da3c7d..b0c8ae4c 100644 --- a/internal/cmd/delete/cmd.go +++ b/internal/cmd/delete/cmd.go @@ -1,38 +1,33 @@ -package get +package delete import ( - "github.com/cosmo-workspace/cosmo/internal/cmd/netrule" "github.com/cosmo-workspace/cosmo/internal/cmd/user" "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/spf13/cobra" ) -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { deleteCmd := &cobra.Command{ - Use: "delete", - Short: "Delete cosmo resources", - Long: ` -Delete cosmo resources -`, + Use: "delete", + Short: "Delete cosmo resources", + Aliases: []string{"rm", "remove"}, } - o := cmdutil.NewUserNamespacedCliOptions(co) - + deleteCmd.AddCommand(user.DeleteCmd(&cobra.Command{ + Use: "user USER_NAME...", + Short: "Delete users. Alias of 'cosmoctl user delete'", + Aliases: []string{"us", "users"}, + }, o)) deleteCmd.AddCommand(workspace.DeleteCmd(&cobra.Command{ - Use: "workspace WORKSPACE_NAME", - Aliases: []string{"ws"}, - Short: "Delete workspace", + Use: "workspace WORKSPACE_NAME...", + Short: "Delete workspaces. Alias of 'cosmoctl workspace delete'", + Aliases: []string{"ws", "workspaces"}, }, o)) - deleteCmd.AddCommand(user.DeleteCmd(&cobra.Command{ - Use: "user USER_NAME", - Short: "Delete user", - }, o.CliOptions)) - deleteCmd.AddCommand(netrule.DeleteCmd(&cobra.Command{ - Use: "networkrule NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER", - Short: "Create or update workspace network rule", - Aliases: []string{"netrule", "net"}, + deleteCmd.AddCommand(workspace.RemoveNetworkCmd(&cobra.Command{ + Use: "network WORKSPACE_NAME --port 8080", + Short: "Remove workspace network. Alias of 'cosmoctl workspace remove-network'", + Aliases: []string{"net", "workspace-network", "workspace-networks", "ws-net", "wsnet"}, }, o)) - cmd.AddCommand(deleteCmd) } diff --git a/internal/cmd/delete/cmd_test.go b/internal/cmd/delete/cmd_test.go new file mode 100644 index 00000000..890b40a0 --- /dev/null +++ b/internal/cmd/delete/cmd_test.go @@ -0,0 +1,31 @@ +package delete + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandDelete(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl delete suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"delete", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/get/__snapshots__/cmd_test.snap b/internal/cmd/get/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..4dee9d0d --- /dev/null +++ b/internal/cmd/get/__snapshots__/cmd_test.snap @@ -0,0 +1,20 @@ +['help should match snapshot 1'] +SnapShot = """ +Get cosmo resources + +Usage: + get [command] + +Available Commands: + events Get events for user + network Get workspace networks + template Get workspace templates + user Get users. Alias of 'cosmoctl user get' + useraddon Get user addons. Alias of 'cosmoctl user get-addons' + workspace Get workspaces. Alias of 'cosmoctl workspace get' + +Flags: + -h, --help help for get + +Use \" get [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/get/cmd.go b/internal/cmd/get/cmd.go index e9881fbe..58ab774c 100644 --- a/internal/cmd/get/cmd.go +++ b/internal/cmd/get/cmd.go @@ -1,42 +1,47 @@ package get import ( - "github.com/cosmo-workspace/cosmo/internal/cmd/template" "github.com/cosmo-workspace/cosmo/internal/cmd/user" "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/spf13/cobra" ) -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { getCmd := &cobra.Command{ Use: "get", Short: "Get cosmo resources", - Long: ` -Get cosmo resources -`, } - o := cmdutil.NewUserNamespacedCliOptions(co) - - getCmd.PersistentFlags().StringVarP(&o.User, "user", "u", "", "user name") - getCmd.PersistentFlags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - getCmd.PersistentFlags().BoolVarP(&o.AllNamespace, "all-namespaces", "A", false, "all namespaces") - + getCmd.AddCommand(user.GetCmd(&cobra.Command{ + Use: "user [USER_NAME...]", + Short: "Get users. Alias of 'cosmoctl user get'", + Aliases: []string{"users"}, + }, o)) getCmd.AddCommand(workspace.GetCmd(&cobra.Command{ - Use: "workspace WORKSPACE_NAME", - Aliases: []string{"ws"}, - Short: "Get workspace", + Use: "workspace [WORKSPACE_NAME...]", + Short: "Get workspaces. Alias of 'cosmoctl workspace get'", + Aliases: []string{"workspaces", "ws"}, + }, o)) + getCmd.AddCommand(workspace.GetTemplatesCmd(&cobra.Command{ + Use: "template [TEMPLATE_NAME...]", + Short: "Get workspace templates", + Aliases: []string{"templates", "tmpl", "tmpls", "ws-tmpl", "ws-tmpls", "wstmpl", "wstmpls"}, + }, o)) + getCmd.AddCommand(user.GetAddonsCmd(&cobra.Command{ + Use: "useraddon [ADDON_NAME...]", + Short: "Get user addons. Alias of 'cosmoctl user get-addons'", + Aliases: []string{"useraddon", "useraddons", "addon", "addons", "user-addon", "user-addons"}, + }, o)) + getCmd.AddCommand(workspace.GetNetworkCmd(&cobra.Command{ + Use: "network WORKSPACE_NAME", + Short: "Get workspace networks", + Aliases: []string{"net", "workspace-networks", "workspace-network", "ws-net", "wsnet"}, + }, o)) + getCmd.AddCommand(user.GetEventsCmd(&cobra.Command{ + Use: "events [USER_NAME]", + Short: "Get events for user", + Aliases: []string{"event", "events"}, }, o)) - getCmd.AddCommand(user.GetCmd(&cobra.Command{ - Use: "user USER_NAME", - Short: "Get user", - }, o.CliOptions)) - getCmd.AddCommand(template.GetCmd(&cobra.Command{ - Use: "template TEMPLATE_NAME", - Aliases: []string{"tmpl"}, - Short: "Get template", - }, o.CliOptions)) - cmd.AddCommand(getCmd) } diff --git a/internal/cmd/get/cmd_test.go b/internal/cmd/get/cmd_test.go new file mode 100644 index 00000000..6103df0b --- /dev/null +++ b/internal/cmd/get/cmd_test.go @@ -0,0 +1,31 @@ +package get + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandGet(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl get suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"get", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/login/__snapshots__/cmd_test.snap b/internal/cmd/login/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..55ace7c2 --- /dev/null +++ b/internal/cmd/login/__snapshots__/cmd_test.snap @@ -0,0 +1,21 @@ +['help should match snapshot 1'] +SnapShot = """ +Login to COSMO Dashboard Server + +Usage: + login USER_NAME [flags] + +Examples: + + # interactive mode + cosmoctl login + + # non interactive mode + echo $PASSWORD | cosmoctl login USER_NAME --dashboard-url https://DASHBOARD_URL --password-stdin + + +Flags: + --again login again + -h, --help help for login + --password-stdin input new password from stdin pipe +""" diff --git a/internal/cmd/login/cmd.go b/internal/cmd/login/cmd.go new file mode 100644 index 00000000..8708ed89 --- /dev/null +++ b/internal/cmd/login/cmd.go @@ -0,0 +1,190 @@ +package login + +import ( + "context" + "fmt" + "time" + + "github.com/bufbuild/connect-go" + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { + loginCmd := &cobra.Command{ + Use: "login USER_NAME", + Short: "Login to COSMO Dashboard Server", + Example: ` + # interactive mode + cosmoctl login + + # non interactive mode + echo $PASSWORD | cosmoctl login USER_NAME --dashboard-url https://DASHBOARD_URL --password-stdin +`, + } + cmd.AddCommand(LoginCmd(loginCmd, o)) +} + +type LoginOption struct { + *cli.RootOptions + + UserName string + Password string + PasswordStdin bool + Again bool +} + +func LoginCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &LoginOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().BoolVar(&o.PasswordStdin, "password-stdin", false, "input new password from stdin pipe") + cmd.Flags().BoolVar(&o.Again, "again", false, "login again") + return cmd +} + +func (o *LoginOption) Validate(cmd *cobra.Command, args []string) error { + if o.UseKubeAPI { + return fmt.Errorf("login command does not support using Kubernetes API") + } + if !o.Again && o.PasswordStdin { + if o.DashboardURL == "" || len(args) == 0 { + return fmt.Errorf("dashboard URL and user name are required by args when using --password-stdin") + } + } + + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + return nil +} + +func (o *LoginOption) Complete(cmd *cobra.Command, args []string) error { + // check if config file already exists + cfgPath, _ := o.GetConfigFilePath() + previousLogin, _ := cli.NewOrLoadConfigFile(cfgPath) + + if o.Again { + if previousLogin.Endpoint == "" { + return fmt.Errorf("failed to get previous login state. please login without --again") + } + o.DashboardURL = previousLogin.Endpoint + o.UserName = previousLogin.User + + } else { + // 1. Ask Dashboard URL + if o.DashboardURL == "" { + prompt := "Dashboard URL: " + if previousLogin.Endpoint != "" { + prompt = fmt.Sprintf("Dashboard URL (%s): ", previousLogin.Endpoint) + } + input, err := cli.AskInput(prompt, false) + if err != nil { + return err + } + if input == "" { + o.DashboardURL = previousLogin.Endpoint + } else { + o.DashboardURL = input + } + } + + // 2. Ask UserName + if len(args) > 0 { + o.UserName = args[0] + } + if o.UserName == "" { + prompt := "User Name : " + if previousLogin.User != "" { + prompt = fmt.Sprintf("User Name (%s): ", previousLogin.User) + } + input, err := cli.AskInput(prompt, false) + if err != nil { + return err + } + if input == "" { + o.UserName = previousLogin.User + } else { + o.UserName = input + } + } + } + + // 3. Ask Password + if o.PasswordStdin { + input, err := cli.ReadFromPipedStdin() + if err != nil { + return fmt.Errorf("failed to read from stdin pipe: %w", err) + } + o.Password = input + } else { + prompt := "Password : " + if previousLogin.Endpoint == "" { + prompt = "Password: " + } + input, err := cli.AskInput(prompt, true) + if err != nil { + return err + } + o.Password = input + } + + o.RootOptions.DisableUseServiceAccount = true + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + o.Logr.Debug().Info("input", "dashboardURL", o.DashboardURL, "userName", o.UserName, "password", mask(o.Password)) + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func mask(s string) string { + if s == "" { + return "" + } + return "******" +} + +func (o *LoginOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + c := o.CosmoDashClient + res, err := c.AuthServiceClient.Login(ctx, connect.NewRequest(&dashv1alpha1.LoginRequest{UserName: o.UserName, Password: o.Password})) + if err != nil { + return fmt.Errorf("failed to login: %w", err) + } + o.CliConfig.Token = cli.ExtractSessionToken(res) + o.CliConfig.User = o.UserName + o.CliConfig.Endpoint = o.GetDashboardURL() + o.CliConfig.UseServiceAccount = false + + // reset cacert if endpoint is not in cluster + if o.CliConfig.Endpoint != cli.InClusterDashboardURL { + o.CliConfig.CACert = "" + } + + // save session + err = o.CliConfig.Save() + if err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully logined to %s as %s", o.CliConfig.Endpoint, o.CliConfig.User)) + + return nil + +} diff --git a/internal/cmd/login/cmd_test.go b/internal/cmd/login/cmd_test.go new file mode 100644 index 00000000..34e71322 --- /dev/null +++ b/internal/cmd/login/cmd_test.go @@ -0,0 +1,31 @@ +package login + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandLogin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl login suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"login", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/netrule/cmd.go b/internal/cmd/netrule/cmd.go deleted file mode 100644 index 16a7335a..00000000 --- a/internal/cmd/netrule/cmd.go +++ /dev/null @@ -1,32 +0,0 @@ -package netrule - -import ( - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/spf13/cobra" -) - -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { - netruleCmd := &cobra.Command{ - Use: "networkrule", - Short: "Manipulate NetworkRule of Workspace resource", - Long: ` -Workspace network rule utility command -`, - Aliases: []string{"netrule", "net"}, - } - - o := cmdutil.NewUserNamespacedCliOptions(co) - - netruleCmd.AddCommand(CreateCmd(&cobra.Command{ - Use: "create NETWORK_RULE_NAME --workspace WORKSPACE_NAME --port PORT_NUMBER", - Short: "Create or update workspace network rule", - Aliases: []string{"add"}, - }, o)) - netruleCmd.AddCommand(DeleteCmd(&cobra.Command{ - Use: "delete NETWORK_RULE_NAME --workspace WORKSPACE_NAME", - Short: "Delete workspace network rule", - Aliases: []string{"rm"}, - }, o)) - - cmd.AddCommand(netruleCmd) -} diff --git a/internal/cmd/netrule/create.go b/internal/cmd/netrule/create.go deleted file mode 100644 index fff486a9..00000000 --- a/internal/cmd/netrule/create.go +++ /dev/null @@ -1,109 +0,0 @@ -package netrule - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/spf13/cobra" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" -) - -type CreateOption struct { - *cmdutil.UserNamespacedCliOptions - - WorkspaceName string - CustomHostPrefix string - PortNumber int32 - HTTPPath string - Public bool - - rule cosmov1alpha1.NetworkRule -} - -func CreateCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &CreateOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - cmd.Flags().StringVar(&o.WorkspaceName, "workspace", "", "workspace name (Required)") - cmd.Flags().Int32Var(&o.PortNumber, "port", 0, "serivce port number (Required)") - cmd.Flags().StringVar(&o.CustomHostPrefix, "host-prefix", "", "custom host prefix") - cmd.Flags().StringVar(&o.HTTPPath, "path", "/", "path for Ingress path when using ingress") - cmd.Flags().BoolVar(&o.Public, "public", false, "disable authentication for this port") - - return cmd -} - -func (o *CreateOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *CreateOption) Validate(cmd *cobra.Command, args []string) error { - if o.AllNamespace { - return errors.New("--all-namespaces is not supported in this command") - } - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { - return err - } - if o.WorkspaceName == "" { - return errors.New("--workspace is required") - } - if o.PortNumber == 0 { - return errors.New("--port is required") - } - return nil -} - -func (o *CreateOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { - return err - } - - o.rule = cosmov1alpha1.NetworkRule{ - CustomHostPrefix: o.CustomHostPrefix, - PortNumber: o.PortNumber, - HTTPPath: o.HTTPPath, - Public: o.Public, - } - o.rule.Default() - return nil -} - -func (o *CreateOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - ctx = clog.IntoContext(ctx, o.Logr) - - c := o.Client - - ws, err := c.GetWorkspaceByUserName(ctx, o.WorkspaceName, o.User) - if err != nil { - return fmt.Errorf("failed to get workspace: %v", err) - } - index := -1 - for i, v := range ws.Spec.Network { - if v.UniqueKey() == o.rule.UniqueKey() { - index = i - } - } - - if _, err := c.AddNetworkRule(ctx, o.WorkspaceName, o.User, o.rule, index); err != nil { - return err - } - - cmdutil.PrintfColorInfo(o.Out, "Successfully add network rule for workspace '%s'\n", o.WorkspaceName) - return nil -} diff --git a/internal/cmd/netrule/delete.go b/internal/cmd/netrule/delete.go deleted file mode 100644 index a5b94193..00000000 --- a/internal/cmd/netrule/delete.go +++ /dev/null @@ -1,78 +0,0 @@ -package netrule - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/spf13/cobra" - - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" -) - -type DeleteOption struct { - *cmdutil.UserNamespacedCliOptions - - WorkspaceName string - - Index int -} - -func DeleteCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &DeleteOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - cmd.Flags().StringVar(&o.WorkspaceName, "workspace", "", "workspace name (Required)") - cmd.Flags().IntVar(&o.Index, "index", -1, "network rule index (Required)") - return cmd -} - -func (o *DeleteOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *DeleteOption) Validate(cmd *cobra.Command, args []string) error { - if o.AllNamespace { - return errors.New("--all-namespaces is not supported in this command") - } - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { - return err - } - if o.WorkspaceName == "" { - return errors.New("workspace name is required") - } - return nil -} - -func (o *DeleteOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { - return err - } - return nil -} - -func (o *DeleteOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - ctx = clog.IntoContext(ctx, o.Logr) - - c := o.Client - - if _, err := c.DeleteNetworkRule(ctx, o.WorkspaceName, o.User, o.Index); err != nil { - return err - } - - cmdutil.PrintfColorInfo(o.Out, "Successfully remove network rule for workspace '%s'\n", o.WorkspaceName) - return nil -} diff --git a/internal/cmd/netrule_test.go b/internal/cmd/netrule_test.go deleted file mode 100644 index c21872a8..00000000 --- a/internal/cmd/netrule_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "errors" - "io" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" - - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" - . "github.com/cosmo-workspace/cosmo/pkg/snap" -) - -var _ = Describe("cosmoctl [netrule]", func() { - - var ( - clientMock kubeutil.ClientMock - rootCmd *cobra.Command - options *cmdutil.CliOptions - outBuf *bytes.Buffer - ) - consoleOut := func() string { - out, _ := io.ReadAll(outBuf) - return string(out) - } - - BeforeEach(func() { - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme - - baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) - Expect(err).NotTo(HaveOccurred()) - clientMock = kubeutil.NewClientMock(baseclient) - klient := kosmo.NewClient(&clientMock) - - options = cmdutil.NewCliOptions() - options.Client = &klient - outBuf = bytes.NewBufferString("") - options.Out = outBuf - options.ErrOut = outBuf - options.Scheme = scheme - rootCmd = NewRootCmd(options) - - testUtil.CreateLoginUser("user2", "お名前", nil, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateLoginUser("user1", "アドミン", []cosmov1alpha1.UserRole{cosmov1alpha1.PrivilegedRole}, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeWorkspace, "template1") - By("---------------BeforeEach end----------------") - }) - - AfterEach(func() { - By("---------------AfterEach start---------------") - clientMock.Clear() - testUtil.DeleteWorkspaceAll() - testUtil.DeleteCosmoUserAll() - testUtil.DeleteTemplateAll() - }) - - //================================================================================== - desc := func(args ...string) string { return strings.Join(args, " ") } - - errSnap := func(err error) string { - if err == nil { - return "success" - } else { - return err.Error() - } - } - - //================================================================================== - Describe("[create]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - if err == nil { - wsv1Workspace, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[5], "user1") - Expect(err).NotTo(HaveOccurred()) - Ω(ObjectSnapshot(wsv1Workspace)).To(MatchSnapShot()) - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws1", "nw1", 1111, "/", false, -1) - testUtil.UpsertNetworkRule("user1", "ws1", "nw3", 2222, "/", false, -1) - run_test(args...) - }, - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "3000", "--host-prefix", "nw11", "--path", "/abc"), - Entry(desc, "netrule", "create", "--namespace", "cosmo-user-user1", "--workspace", "ws1", "--port", "4000", "--host-prefix", "nw12", "--path", "/def"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "4000", "--host-prefix", "nw13", "--path", "/def"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "4000"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "4000", "--path", "/def"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws1", "nw1", 1111, "/", false, -1) - run_test(args...) - }, - Entry(desc, "netrule", "create", "--user", "xxx", "--workspace", "ws1", "--port", "4000"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "xxx", "--port", "4000"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "0"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "124000"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "4000", "--host-prefix", "main"), - Entry(desc, "netrule", "create", "--user", "user1", "--workspace", "ws1", "--port", "4000"), - Entry(desc, "netrule", "create"), - Entry(desc, "netrule", "create", "--namespace", "xxxxx", "--workspace", "ws1", "--port", "4000"), - ) - - DescribeTable("❌ fail with an unexpected error at update:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - clientMock.UpdateMock = func(ctx context.Context, obj client.Object, opts ...client.UpdateOption) (mocked bool, err error) { - if clientMock.IsCallingFrom("\\.RunE$") { - return true, errors.New("mock update error") - } - return false, nil - } - run_test(args...) - }, - Entry(desc, "netrule", "create", "--workspace", "ws1", "--user", "user1", "--host-prefix", "nw99", "--port", "4000", "--path", "/def"), - ) - }) - - //================================================================================== - Describe("[delete]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - if err == nil { - wsv1Workspace, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[5], args[3]) - Expect(err).NotTo(HaveOccurred()) - Ω(ObjectSnapshot(wsv1Workspace)).To(MatchSnapShot()) - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws1", "nw1", 1111, "/", false, -1) - testUtil.UpsertNetworkRule("user1", "ws1", "nw2", 2222, "/", false, -1) - run_test(args...) - }, - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "ws1", "--index", "0"), - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "ws1", "--index", "1"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws1", "nw1", 1111, "/", false, -1) - run_test(args...) - }, - Entry(desc, "netrule", "delete", "--user", "xxx", "--workspace", "ws1", "--index", "1"), - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "xxx", "--index", "1"), - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "ws1", "--index", "-1"), - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "ws1", "--index", "3"), - ) - - DescribeTable("❌ fail with an unexpected error at update:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws1", "nw1", 1111, "/", false, -1) - clientMock.SetUpdateError("\\.RunE$", errors.New("mock update error")) - run_test(args...) - }, - Entry(desc, "netrule", "delete", "--user", "user1", "--workspace", "ws1", "--index", "1"), - ) - }) - - //================================================================================== -}) diff --git a/internal/cmd/resume/__snapshots__/cmd_test.snap b/internal/cmd/resume/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..37923abe --- /dev/null +++ b/internal/cmd/resume/__snapshots__/cmd_test.snap @@ -0,0 +1,18 @@ +['help should match snapshot 1'] +SnapShot = """ +Start stopped workspaces + +Usage: + resume [command] + +Aliases: + resume, start, run + +Available Commands: + workspace Resume workspaces. Alias of 'cosmoctl workspace resume' + +Flags: + -h, --help help for resume + +Use \" resume [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/resume/cmd.go b/internal/cmd/resume/cmd.go new file mode 100644 index 00000000..ee8c463b --- /dev/null +++ b/internal/cmd/resume/cmd.go @@ -0,0 +1,22 @@ +package resume + +import ( + "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/spf13/cobra" +) + +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { + resumeCmd := &cobra.Command{ + Use: "resume", + Short: "Start stopped workspaces", + Aliases: []string{"start", "run"}, + } + + resumeCmd.AddCommand(workspace.ResumeCmd(&cobra.Command{ + Use: "workspace WORKSPACE_NAME...", + Short: "Resume workspaces. Alias of 'cosmoctl workspace resume'", + Aliases: []string{"ws", "workspaces"}, + }, o)) + cmd.AddCommand(resumeCmd) +} diff --git a/internal/cmd/resume/cmd_test.go b/internal/cmd/resume/cmd_test.go new file mode 100644 index 00000000..08c5277a --- /dev/null +++ b/internal/cmd/resume/cmd_test.go @@ -0,0 +1,31 @@ +package resume + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandResume(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl resume suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"resume", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/root_cmd.go b/internal/cmd/root_cmd.go index 06148b30..dea3925f 100644 --- a/internal/cmd/root_cmd.go +++ b/internal/cmd/root_cmd.go @@ -1,5 +1,5 @@ /* -Copyright © 2022 NAME HERE +Copyright © 2024 cosmo-workspace */ package cmd @@ -7,65 +7,59 @@ import ( "fmt" "os" + "github.com/fatih/color" "github.com/spf13/cobra" "github.com/cosmo-workspace/cosmo/internal/cmd/create" del "github.com/cosmo-workspace/cosmo/internal/cmd/delete" "github.com/cosmo-workspace/cosmo/internal/cmd/get" - "github.com/cosmo-workspace/cosmo/internal/cmd/netrule" - "github.com/cosmo-workspace/cosmo/internal/cmd/run" - "github.com/cosmo-workspace/cosmo/internal/cmd/stop" + "github.com/cosmo-workspace/cosmo/internal/cmd/login" + "github.com/cosmo-workspace/cosmo/internal/cmd/resume" + "github.com/cosmo-workspace/cosmo/internal/cmd/suspend" "github.com/cosmo-workspace/cosmo/internal/cmd/template" "github.com/cosmo-workspace/cosmo/internal/cmd/user" "github.com/cosmo-workspace/cosmo/internal/cmd/version" "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" ) -func NewRootCmd(o *cmdutil.CliOptions) *cobra.Command { +func NewRootCmd(o *cli.RootOptions) *cobra.Command { rootCmd := &cobra.Command{ Use: "cosmoctl", - Short: "Command line tool to manipulate comso", + Short: "Command line tool for cosmo API", Long: ` -Command line tool to manipulate comso +Command line tool for cosmo API Complete documentation is available at http://github.com/cosmo-workspace/cosmo -MIT 2022 cosmo-workspace/cosmo +MIT 2024 cosmo-workspace/cosmo `, } - - rootCmd.SetIn(o.In) - rootCmd.SetOut(o.Out) - rootCmd.SetErr(o.ErrOut) - rootCmd.PersistentFlags().StringVar(&o.KubeConfigPath, "kubeconfig", "", "kubeconfig file path (default: $HOME/.kube/config)") - rootCmd.PersistentFlags().StringVar(&o.KubeContext, "context", "", "kube-context (default: current context)") - rootCmd.PersistentFlags().IntVarP(&o.LogLevel, "verbose", "v", -1, "log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL") + o.AddFlags(rootCmd) version.AddCommand(rootCmd, o) - template.AddCommand(rootCmd, o) - user.AddCommand(rootCmd, o) - workspace.AddCommand(rootCmd, o) - netrule.AddCommand(rootCmd, o) + login.AddCommand(rootCmd, o) create.AddCommand(rootCmd, o) get.AddCommand(rootCmd, o) del.AddCommand(rootCmd, o) - run.AddCommand(rootCmd, o) - stop.AddCommand(rootCmd, o) + resume.AddCommand(rootCmd, o) + suspend.AddCommand(rootCmd, o) + + user.AddCommand(rootCmd, o) + workspace.AddCommand(rootCmd, o) + template.AddCommand(rootCmd, o) return rootCmd } -func Execute() { - o := cmdutil.NewCliOptions() - o.In = os.Stdin - o.Out = os.Stdout - o.ErrOut = os.Stderr +func Execute(v cli.VersionInfo) { + o := cli.NewRootOptions() + o.Versions = v rootCmd := NewRootCmd(o) if err := rootCmd.Execute(); err != nil { - fmt.Fprintln(o.Out, err) + fmt.Fprintln(rootCmd.ErrOrStderr(), color.RedString("Error: %s", err)) os.Exit(1) } diff --git a/internal/cmd/root_cmd_test.go b/internal/cmd/root_cmd_test.go new file mode 100644 index 00000000..ac91b032 --- /dev/null +++ b/internal/cmd/root_cmd_test.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "bytes" + "testing" + + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/cosmo-workspace/cosmo/pkg/cli" +) + +func TestCommandRoot(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl root suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + o := cli.NewRootOptions() + rootCmd := NewRootCmd(o) + + out := bytes.Buffer{} + rootCmd.SetOut(&out) + rootCmd.SetArgs([]string{"--help"}) + err := rootCmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/run/cmd.go b/internal/cmd/run/cmd.go deleted file mode 100644 index 2ca27b15..00000000 --- a/internal/cmd/run/cmd.go +++ /dev/null @@ -1,27 +0,0 @@ -package run - -import ( - "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/spf13/cobra" -) - -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { - runCmd := &cobra.Command{ - Use: "run", - Short: "Run workload resources", - Long: ` -Run cosmo workload resources -`, - } - - o := cmdutil.NewUserNamespacedCliOptions(co) - - runCmd.AddCommand(workspace.RunInstanceCmd(&cobra.Command{ - Use: "workspace WORKSPACE_NAME", - Aliases: []string{"ws", "inst", "instance"}, - Short: "Run workspace instance", - }, o)) - - cmd.AddCommand(runCmd) -} diff --git a/internal/cmd/stop/cmd.go b/internal/cmd/stop/cmd.go deleted file mode 100644 index 882ea8b6..00000000 --- a/internal/cmd/stop/cmd.go +++ /dev/null @@ -1,27 +0,0 @@ -package stop - -import ( - "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/spf13/cobra" -) - -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { - runCmd := &cobra.Command{ - Use: "stop", - Short: "Stop workload resources", - Long: ` -Stop cosmo workload resources -`, - } - - o := cmdutil.NewUserNamespacedCliOptions(co) - - runCmd.AddCommand(workspace.StopInstanceCmd(&cobra.Command{ - Use: "workspace WORKSPACE_NAME", - Aliases: []string{"ws", "inst", "instance"}, - Short: "Stop workspace instance", - }, o)) - - cmd.AddCommand(runCmd) -} diff --git a/internal/cmd/suite_test.go b/internal/cmd/suite_test.go deleted file mode 100644 index ed7e71c5..00000000 --- a/internal/cmd/suite_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package cmd - -import ( - "context" - "path/filepath" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/metrics/server" - "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/internal/webhooks" - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" - "github.com/cosmo-workspace/cosmo/pkg/kosmo/test" - //+kubebuilder:scaffold:imports -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -var ( - cfg *rest.Config - k8sClient kosmo.Client - testEnv *envtest.Environment - testUtil test.TestUtil - ctx context.Context - cancel context.CancelFunc -) - -const DefaultURLBase = "https://{{NETRULE_GROUP}}-{{INSTANCE}}-{{USER_NAME}}.domain" - -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(clientgoscheme.Scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(clientgoscheme.Scheme)) - //+kubebuilder:scaffold:scheme -} - -func TestCommand(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Cosmoctl cmd Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - ctx, cancel = context.WithCancel(ctrl.SetupSignalHandler()) - - By("bootstrapping test environment") - - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - WebhookInstallOptions: envtest.WebhookInstallOptions{ - Paths: []string{filepath.Join("..", "..", "config", "webhook")}, - }, - } - - var err error - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - c, err := client.New(cfg, client.Options{Scheme: clientgoscheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - - k8sClient = kosmo.NewClient(c) - Expect(k8sClient).NotTo(BeNil()) - - testUtil = test.NewTestUtil(k8sClient) - - mgr, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: clientgoscheme.Scheme, - Metrics: server.Options{BindAddress: "0"}, - WebhookServer: webhook.NewServer(webhook.Options{ - CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir, - Port: testEnv.WebhookInstallOptions.LocalServingPort, - }), - }) - Expect(err).NotTo(HaveOccurred()) - - // webhook - (&webhooks.InstanceMutationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("InstanceMutationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - (&webhooks.InstanceValidationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("InstanceValidationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - FieldManager: "cosmo-instance-controller", - }).SetupWebhookWithManager(mgr) - - (&webhooks.WorkspaceMutationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("WorkspaceMutationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - (&webhooks.WorkspaceValidationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("WorkspaceValidationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - (&webhooks.UserMutationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("UserMutationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - (&webhooks.UserValidationWebhookHandler{ - Client: k8sClient, - Log: clog.NewLogger(ctrl.Log.WithName("UserValidationWebhookHandler")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - (&webhooks.TemplateValidationWebhookHandler{ - Client: mgr.GetClient(), - Log: clog.NewLogger(ctrl.Log.WithName("TemplateValidationWebhook")), - Decoder: admission.NewDecoder(mgr.GetScheme()), - }).SetupWebhookWithManager(mgr) - - go func() { - defer GinkgoRecover() - err := mgr.Start(ctx) - Expect(err).NotTo(HaveOccurred()) - }() - -}) - -var _ = AfterSuite(func() { - cancel() - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) -}) diff --git a/internal/cmd/suspend/__snapshots__/cmd_test.snap b/internal/cmd/suspend/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..64059ad3 --- /dev/null +++ b/internal/cmd/suspend/__snapshots__/cmd_test.snap @@ -0,0 +1,18 @@ +['help should match snapshot 1'] +SnapShot = """ +Suspend workspaces + +Usage: + suspend [command] + +Aliases: + suspend, stop + +Available Commands: + workspace Suspend workspaces. Alias of 'cosmoctl workspace suspend' + +Flags: + -h, --help help for suspend + +Use \" suspend [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/suspend/cmd.go b/internal/cmd/suspend/cmd.go new file mode 100644 index 00000000..61fe9cfb --- /dev/null +++ b/internal/cmd/suspend/cmd.go @@ -0,0 +1,22 @@ +package suspend + +import ( + "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/spf13/cobra" +) + +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { + suspendCmd := &cobra.Command{ + Use: "suspend", + Short: "Suspend workspaces", + Aliases: []string{"stop"}, + } + + suspendCmd.AddCommand(workspace.SuspendCmd(&cobra.Command{ + Use: "workspace WORKSPACE_NAME...", + Short: "Suspend workspaces. Alias of 'cosmoctl workspace suspend'", + Aliases: []string{"ws", "workspaces"}, + }, o)) + cmd.AddCommand(suspendCmd) +} diff --git a/internal/cmd/suspend/cmd_test.go b/internal/cmd/suspend/cmd_test.go new file mode 100644 index 00000000..54f2f94c --- /dev/null +++ b/internal/cmd/suspend/cmd_test.go @@ -0,0 +1,31 @@ +package suspend + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandSuspend(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl suspend suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"suspend", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/template/__snapshots__/cmd_test.snap b/internal/cmd/template/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..753c5618 --- /dev/null +++ b/internal/cmd/template/__snapshots__/cmd_test.snap @@ -0,0 +1,87 @@ +['help should match snapshot 1'] +SnapShot = """ + +Manipulate COSMO Workspace Template resource. + +\"Template\" is a set of Kubernetes resources for Workspace. + +Usage: + template [command] + +Aliases: + template, tmpl + +Available Commands: + generate Generate Template + get Get Templates + validate Validate Template by dry-run + +Flags: + -h, --help help for template + +Use \" template [command] --help\" for more information about a command. +""" + +['help template generate should match snapshot 1'] +SnapShot = """ +Generate Template + +Usage: + template generate [command] + +Aliases: + generate, gen + +Available Commands: + useraddon Generate UserAddon + workspace Generate WorkspaceTemplate + +Flags: + -h, --help help for generate + +Use \" template generate [command] --help\" for more information about a command. +""" + +['help template get should match snapshot 1'] +SnapShot = """ +Get Templates + +Usage: + template get [command] + +Aliases: + get, list + +Available Commands: + useraddons Get addons + workspace Get workspace templates in cluster + +Flags: + -h, --help help for get + +Use \" template get [command] --help\" for more information about a command. +""" + +['help template should match snapshot 1'] +SnapShot = """ + +Manipulate COSMO Workspace Template resource. + +\"Template\" is a set of Kubernetes resources for Workspace. + +Usage: + template [command] + +Aliases: + template, tmpl + +Available Commands: + generate Generate Template + get Get Templates + validate Validate Template by dry-run + +Flags: + -h, --help help for template + +Use \" template [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/template/cmd.go b/internal/cmd/template/cmd.go index 5121b707..73b3e043 100644 --- a/internal/cmd/template/cmd.go +++ b/internal/cmd/template/cmd.go @@ -1,55 +1,71 @@ package template import ( - cmdutil "github.com/cosmo-workspace/cosmo/pkg/cmdutil" "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/internal/cmd/user" + "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" + "github.com/cosmo-workspace/cosmo/pkg/cli" ) -func AddCommand(cmd *cobra.Command, o *cmdutil.CliOptions) { - tmplCmd := &cobra.Command{ - Use: "template", - Short: "Manipulate Template resource", +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { + templateCmd := &cobra.Command{ + Use: "template", + Short: "Manipulate Template resource", + Aliases: []string{"tmpl"}, Long: ` -Template utility command. +Manipulate COSMO Workspace Template resource. + +"Template" is a set of Kubernetes resources for Workspace. `, - Aliases: []string{"tmpl"}, } - - tmplCmd.AddCommand(generateCmd(&cobra.Command{ - Use: "generate --name TEMPLATE_NAME [< Input via Stdin or pipe]", - Aliases: []string{"gen"}, + generateCmd := &cobra.Command{ + Use: "generate [< Input via Stdin or pipe]", Short: "Generate Template", - Long: `Generate Template - -For create generated template, just do "kubectl create -f cosmo-template.yaml" + Aliases: []string{"gen"}, + } + generateCmd.AddCommand(generateWorkspaceCmd(&cobra.Command{ + Use: "workspace [< Input via Stdin or pipe]", + Short: "Generate WorkspaceTemplate", + Long: `Generate WorkspaceTemplate -Example: +For create generated Workspace Template, just do kubectl apply +`, + Example: ` * Pipe from kustomize build and apply to your cluster in a single line - kustomize build ./kubernetes/ | cosmoctl template generate --name TEMPLATE_NAME | kubectl apply -f - + kustomize build ./kubernetes/ | cosmoctl gen tmpl --name TEMPLATE_NAME | kubectl apply -f - * Input merged config file (kustomize build ... or helm template ... etc.) and save it to file - cosmoctl template generate --name TEMPLATE_NAME -o cosmo-template.yaml < merged.yaml + cosmoctl gen tmpl --name TEMPLATE_NAME -o cosmo-template.yaml < merged.yaml `, + Aliases: []string{"workspace", "ws", "workspace-template"}, }, o)) - tmplCmd.AddCommand(GetCmd(&cobra.Command{ - Use: "get", - Short: "Get templates", - Long: `Get Templates + generateCmd.AddCommand(generateUserAddonCmd(&cobra.Command{ + Use: "useraddon [< Input via Stdin or pipe]", + Short: "Generate UserAddon", + Long: `Generate UserAddon -Basically it is similar to "kubectl get template" +For create generated UserAddon Template, just do kubectl apply +`, + Example: ` + * Pipe from kustomize build and apply to your cluster in a single line + + kustomize build ./kubernetes/ | cosmoctl gen addon --name TEMPLATE_NAME | kubectl apply -f - -For type workspace template, use with --workspace flag to see more information. + * Input merged config file (kustomize build ... or helm template ... etc.) and save it to file + + cosmoctl gen addon --name TEMPLATE_NAME -o cosmo-template.yaml < merged.yaml `, + Aliases: []string{"addon", "useraddon", "user-addon"}, }, o)) - tmplCmd.AddCommand(validateCmd(&cobra.Command{ + + templateCmd.AddCommand(validateCmd(&cobra.Command{ Use: "validate --file FILE", Aliases: []string{"valid", "check"}, - Short: "Validate Template", - Long: `Validate Template by dry-run - -Usage: + Short: "Validate Template by dry-run", + Example: ` * Dry-run on server-side cosmoctl template validate -f cosmo-template.yaml @@ -64,5 +80,23 @@ Usage: `, }, o)) - cmd.AddCommand(tmplCmd) + getCmd := &cobra.Command{ + Use: "get", + Short: "Get Templates", + Aliases: []string{"list"}, + } + getCmd.AddCommand(workspace.GetTemplatesCmd(&cobra.Command{ + Use: "workspace [TEMPLATE_NAME...]", + Short: "Get workspace templates in cluster", + Aliases: []string{"workspaces", "workspace", "ws"}, + }, o)) + getCmd.AddCommand(user.GetAddonsCmd(&cobra.Command{ + Use: "useraddons [ADDON_NAME...]", + Short: "Get addons", + Aliases: []string{"useraddon", "addons", "addon", "user-addon"}, + }, o)) + + templateCmd.AddCommand(getCmd) + templateCmd.AddCommand(generateCmd) + cmd.AddCommand(templateCmd) } diff --git a/internal/cmd/template/cmd_test.go b/internal/cmd/template/cmd_test.go new file mode 100644 index 00000000..5820b8f4 --- /dev/null +++ b/internal/cmd/template/cmd_test.go @@ -0,0 +1,59 @@ +package template + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandTemplate(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl template suite") +} + +var _ = Describe("help", func() { + Context("template", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"template", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) + }) + + Context("template generate", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"template", "generate", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) + }) + + Context("template get", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"template", "get", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) + }) +}) diff --git a/internal/cmd/template/generate.go b/internal/cmd/template/generate.go deleted file mode 100644 index 178a93b4..00000000 --- a/internal/cmd/template/generate.go +++ /dev/null @@ -1,263 +0,0 @@ -package template - -import ( - "context" - "errors" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/mattn/go-isatty" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "sigs.k8s.io/yaml" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/internal/cmd/version" - cmdutil "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/template" - "github.com/cosmo-workspace/cosmo/pkg/workspace" -) - -type generateOption struct { - *cmdutil.CliOptions - wsConfig cosmov1alpha1.Config - - Name string - OutputFile string - RequiredVars []string - Desc string - - TypeWorkspace bool - TypeUserAddon bool - - SetDefaultUserAddon bool - DisableNamePrefix bool - ClusterScope bool - UserRoles []string - RequiredUserAddons []string - - tmpl cosmov1alpha1.TemplateObject -} - -func generateCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &generateOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.Name, "name", "n", "", "template name (use directory name if not specified)") - cmd.Flags().StringVarP(&o.OutputFile, "output", "o", "", "write output into file (default: Stdout)") - cmd.Flags().StringSliceVar(&o.RequiredVars, "required-vars", []string{}, "template custom vars to be replaced by instance. format --required-vars VAR1,VAR2:default-value") - cmd.Flags().StringVar(&o.Desc, "desc", "", "template description") - - cmd.Flags().BoolVar(&o.TypeWorkspace, "workspace", false, "template as type workspace") - cmd.Flags().StringVar(&o.wsConfig.DeploymentName, "workspace-deployment-name", "", "Deployment name for Workspace. use with --workspace (auto detected if not specified)") - cmd.Flags().StringVar(&o.wsConfig.ServiceName, "workspace-service-name", "", "Service name for Workspace. use with --workspace (auto detected if not specified)") - cmd.Flags().StringVar(&o.wsConfig.ServiceMainPortName, "workspace-main-service-port-name", "", "ServicePort name for Workspace main container port. use with --workspace (auto detected if not specified)") - - cmd.Flags().BoolVar(&o.TypeUserAddon, "user-addon", false, "template as type useraddon") - cmd.Flags().BoolVar(&o.TypeUserAddon, "useraddon", false, "template as type useraddon") - cmd.Flags().BoolVar(&o.SetDefaultUserAddon, "set-default-user-addon", false, "set default user addon") - cmd.Flags().BoolVar(&o.DisableNamePrefix, "disable-nameprefix", false, "disable adding instance name prefix on child resource name") - - cmd.Flags().BoolVar(&o.ClusterScope, "cluster-scope", false, "generate ClusterTemplate (default generate namespaced Template)") - cmd.Flags().StringSliceVar(&o.UserRoles, "userroles", []string{}, "user roles to show this template (e.g. 'teama-*', 'teamb-admin', etc.)") - - cmd.Flags().StringSliceVar(&o.RequiredUserAddons, "required-useraddons", []string{}, "required user addons") - - return cmd -} - -func (o *generateOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *generateOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { - return err - } - - if o.TypeWorkspace && o.TypeUserAddon { - return errors.New("--workspace and --user-addon cannot be specified concurrently") - } - - if o.TypeWorkspace && o.ClusterScope { - return errors.New("workspace template cannot be cluster-scoped") - } - - return nil -} - -func (o *generateOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { - return err - } - - if o.ClusterScope { - o.tmpl = &cosmov1alpha1.ClusterTemplate{} - } else { - o.tmpl = &cosmov1alpha1.Template{} - } - - if o.Name == "" { - dir, err := os.Getwd() - if err != nil { - return err - } - o.Name = filepath.Base(dir) - } - - if o.OutputFile != "" { - var err error - o.OutputFile, err = filepath.Abs(o.OutputFile) - if err != nil { - return err - } - } - - if len(o.RequiredVars) > 0 { - vars := make([]cosmov1alpha1.RequiredVarSpec, 0, len(o.RequiredVars)) - for _, v := range o.RequiredVars { - vcol := strings.Split(v, ":") - varSpec := cosmov1alpha1.RequiredVarSpec{Var: vcol[0]} - if len(vcol) > 1 { - varSpec.Default = vcol[1] - } - vars = append(vars, varSpec) - } - o.tmpl.GetSpec().RequiredVars = vars - } - - o.tmpl.SetName(o.Name) - o.tmpl.GetSpec().Description = o.Desc - - gvk, err := apiutil.GVKForObject(o.tmpl, o.Scheme) - if err != nil { - return err - } - o.tmpl.SetGroupVersionKind(gvk) - - // annotations - ann := o.tmpl.GetAnnotations() - if ann == nil { - ann = make(map[string]string) - } - - if o.TypeWorkspace { - template.SetTemplateType(o.tmpl, cosmov1alpha1.TemplateLabelEnumTypeWorkspace) - } else if o.TypeUserAddon { - template.SetTemplateType(o.tmpl, cosmov1alpha1.TemplateLabelEnumTypeUserAddon) - - if o.SetDefaultUserAddon { - ann[cosmov1alpha1.UserAddonTemplateAnnKeyDefaultUserAddon] = strconv.FormatBool(true) - } - if o.DisableNamePrefix { - ann[cosmov1alpha1.TemplateAnnKeyDisableNamePrefix] = strconv.FormatBool(true) - } - } - - if len(o.UserRoles) > 0 { - ann[cosmov1alpha1.TemplateAnnKeyUserRoles] = strings.Join(o.UserRoles, ",") - } - if len(o.RequiredUserAddons) > 0 { - ann[cosmov1alpha1.TemplateAnnKeyRequiredAddons] = strings.Join(o.RequiredUserAddons, ",") - } - - o.tmpl.SetAnnotations(ann) - - return nil -} - -func (o *generateOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - - if isatty.IsTerminal(os.Stdin.Fd()) { - return fmt.Errorf("no input via stdin") - } - - // input data from stdin - input, err := io.ReadAll(o.In) - if err != nil { - return fmt.Errorf("failed to read input file : %w", err) - } - if len(input) == 0 { - return fmt.Errorf("no input") - } - o.Logr.DebugAll().Info(string(input), "obj", "input k8s configs") - - // create tmp dir - tmpDir, err := ioutil.TempDir(os.TempDir(), "cosmoctl-*") - if err != nil { - return fmt.Errorf("failed to create temp dir : %w", err) - } - defer os.RemoveAll(tmpDir) - o.Logr.Debug().Info("tmpDir created", "path", tmpDir) - - // save it as "packaged" file - if err := cmdutil.CreateFile(tmpDir, DefaultPackagedFile, input); err != nil { - return err - } - o.Logr.Debug().Info(fmt.Sprintf("%s created", DefaultPackagedFile)) - - // if type workspace, validate and set label - o.Logr.Debug().Info("template type", "workspace", o.TypeWorkspace) - unsts, err := preTemplateBuild(string(input)) - if err != nil { - return fmt.Errorf("failed to pre-build template: %w", err) - } - - if o.TypeWorkspace { - if err := completeWorkspaceConfig(&o.wsConfig, unsts); err != nil { - return fmt.Errorf("type workspace validation failed: %w", err) - } - workspace.SetConfigOnTemplateAnnotations(o.tmpl, o.wsConfig) - } - - kust := NewKustomize(o.DisableNamePrefix) - - // run kustomize - out, err := cmdutil.ExecKustomize(ctx, tmpDir, kust) - if err != nil { - return err - } - o.Logr.Debug().Info(string(out), "obj", "updated k8s configs") - - o.tmpl.GetSpec().RawYaml = string(out) - - outtmpl, _ := yaml.Marshal(&o.tmpl) - - output := append([]byte("# Generated by "+version.Footprint+"\n"), outtmpl...) - - // output to Stdout or write the output to file given by option - if o.OutputFile == "" { - fmt.Fprintln(o.Out, string(output)) - } else { - if err := cmdutil.CreateFile(filepath.Dir(o.OutputFile), filepath.Base(o.OutputFile), output); err != nil { - return err - } - } - return nil -} - -func preTemplateBuild(rawTmpl string) ([]unstructured.Unstructured, error) { - var inst cosmov1alpha1.Instance - inst.SetName("dummy") - inst.SetNamespace("dummy") - - builder := template.NewRawYAMLBuilder(rawTmpl, &inst) - return builder.Build() -} diff --git a/internal/cmd/template/generate_useraddon.go b/internal/cmd/template/generate_useraddon.go new file mode 100644 index 00000000..71e36353 --- /dev/null +++ b/internal/cmd/template/generate_useraddon.go @@ -0,0 +1,124 @@ +package template + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/cli" +) + +type generateUserAddonOption struct { + *cli.RootOptions + + Name string + OutputFile string + RequiredVars []string + Desc string + NoHeader bool + UserRoles []string + RequiredUserAddons []string + SetDefault bool + SetUserNamePrefix bool + ClusterScope bool +} + +func generateUserAddonCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &generateUserAddonOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + + cmd.Flags().StringVarP(&o.Name, "name", "n", "", "template name (use directory name if not specified)") + cmd.Flags().StringVarP(&o.OutputFile, "output", "o", "", "write output into file (default: Stdout)") + cmd.Flags().StringSliceVar(&o.RequiredVars, "var", []string{}, "template custom vars. format --var=VAR1 --var=VAR2:default-value") + cmd.Flags().StringVar(&o.Desc, "desc", "", "template description") + cmd.Flags().BoolVar(&o.NoHeader, "no-header", false, "no output headers") + cmd.Flags().StringSliceVar(&o.UserRoles, "userroles", []string{}, "user roles only to show this template (e.g. 'teama-*', 'teamb-admin', etc.)") + cmd.Flags().StringSliceVar(&o.RequiredUserAddons, "required-useraddons", []string{}, "add dependency to use this useraddon") + + cmd.Flags().BoolVar(&o.SetDefault, "default", false, "set default. default user addon is applied to all users") + cmd.Flags().BoolVar(&o.SetUserNamePrefix, "user-prefix", false, "adding user name prefix on child resource name. default false but true if --cluster-scope is specified") + cmd.Flags().BoolVarP(&o.ClusterScope, "cluster-scope", "c", false, "include cluster-scoped resoure like ClusterRoleBindings, PersistentVolume etc.") + return cmd +} + +func (o *generateUserAddonOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + return nil +} + +func (o *generateUserAddonOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.CompleteWithoutClient(cmd, args); err != nil { + return err + } + if o.Name == "" { + dir, err := os.Getwd() + if err != nil { + return err + } + o.Name = filepath.Base(dir) + } + + if o.OutputFile != "" { + outFile, err := filepath.Abs(o.OutputFile) + if err != nil { + return err + } + o.OutputFile = outFile + } + + if o.ClusterScope { + o.SetUserNamePrefix = true + } + return nil +} + +func (o *generateUserAddonOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + input, err := cli.ReadFromPipedStdin() + if err != nil { + return err + } + o.Logr.Debug().Info(input) + + builder := NewTemplateObjectBuilder(o.ClusterScope). + Name(o.Name). + Description(o.Desc). + RequiredVars(o.RequiredVars). + SetRequiredAddons(o.RequiredUserAddons). + SetUserRoles(o.UserRoles). + TypeUserAddon(o.SetDefault). + Resources(input) + + if !o.SetUserNamePrefix { + builder = builder.DisableNamePrefix() + } + + if !o.NoHeader { + builder.SetHeader(o.Versions) + } + + out, err := builder.Build(o.Ctx) + if err != nil { + return err + } + + // output to Stdout or write the output to file given by option + if o.OutputFile == "" { + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + } else { + if err := os.WriteFile(o.OutputFile, out, 0644); err != nil { + return fmt.Errorf("failed to write file %s : %w", o.OutputFile, err) + } + } + return nil +} diff --git a/internal/cmd/template/generate_workspace.go b/internal/cmd/template/generate_workspace.go new file mode 100644 index 00000000..a7e2c0d0 --- /dev/null +++ b/internal/cmd/template/generate_workspace.go @@ -0,0 +1,209 @@ +package template + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/kubeutil" + "github.com/cosmo-workspace/cosmo/pkg/template" +) + +type generateWorkspaceOption struct { + *cli.RootOptions + Name string + OutputFile string + RequiredVars []string + Desc string + NoHeader bool + UserRoles []string + RequiredUserAddons []string + wsConfig cosmov1alpha1.Config +} + +func generateWorkspaceCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &generateWorkspaceOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + + cmd.Flags().StringVarP(&o.Name, "name", "n", "", "template name (use directory name if not specified)") + cmd.Flags().StringVarP(&o.OutputFile, "output", "o", "", "write output into file (default: Stdout)") + cmd.Flags().StringSliceVar(&o.RequiredVars, "var", []string{}, "indicate template custom vars. format --var=VAR1 --var=VAR2:default-value") + cmd.Flags().StringVar(&o.Desc, "desc", "", "template description") + cmd.Flags().BoolVar(&o.NoHeader, "no-header", false, "no output headers") + cmd.Flags().StringSliceVar(&o.UserRoles, "userroles", []string{}, "user roles only to show this template (e.g. 'teama-*', 'teamb-admin', etc.)") + cmd.Flags().StringSliceVar(&o.RequiredUserAddons, "required-useraddons", []string{}, "add dependency to use this useraddon") + + cmd.Flags().StringVar(&o.wsConfig.DeploymentName, "deployment", "", "Deployment name for Workspace (auto detected if not specified)") + cmd.Flags().StringVar(&o.wsConfig.ServiceName, "service", "", "Service name for Workspace (auto detected if not specified)") + cmd.Flags().StringVar(&o.wsConfig.ServiceMainPortName, "main-service-port", "", "ServicePort name for Workspace main container port (auto detected if not specified)") + + return cmd +} + +func (o *generateWorkspaceOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + return nil +} + +func (o *generateWorkspaceOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.CompleteWithoutClient(cmd, args); err != nil { + return err + } + if o.Name == "" { + dir, err := os.Getwd() + if err != nil { + return err + } + o.Name = filepath.Base(dir) + } + + if o.OutputFile != "" { + outFile, err := filepath.Abs(o.OutputFile) + if err != nil { + return err + } + o.OutputFile = outFile + } + return nil +} + +func (o *generateWorkspaceOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + input, err := cli.ReadFromPipedStdin() + if err != nil { + return err + } + o.Logr.Debug().Info(input) + + unsts, err := template.NewRawYAMLBuilder(input, nil).Build() + if err != nil { + return fmt.Errorf("failed to build template: %w", err) + } + if err := completeWorkspaceConfig(&o.wsConfig, unsts); err != nil { + return err + } + + builder := NewTemplateObjectBuilder(false). + Name(o.Name). + Description(o.Desc). + RequiredVars(o.RequiredVars). + SetRequiredAddons(o.RequiredUserAddons). + SetUserRoles(o.UserRoles). + TypeWorkspace(o.wsConfig). + Resources(input) + + if !o.NoHeader { + builder.SetHeader(o.Versions) + } + + out, err := builder.Build(o.Ctx) + if err != nil { + return err + } + + // output to Stdout or write the output to file given by option + if o.OutputFile == "" { + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + } else { + if err := os.WriteFile(o.OutputFile, out, 0644); err != nil { + return fmt.Errorf("failed to write file %s : %w", o.OutputFile, err) + } + } + return nil +} + +func completeWorkspaceConfig(wsConfig *cosmov1alpha1.Config, unst []unstructured.Unstructured) error { + if wsConfig == nil || len(unst) == 0 { + return errors.New("invalid args") + } + + dps := make([]unstructured.Unstructured, 0) + svcs := make([]unstructured.Unstructured, 0) + + for _, u := range unst { + if kubeutil.IsGVKEqual(u.GroupVersionKind(), kubeutil.DeploymentGVK) { + dps = append(dps, u) + } else if kubeutil.IsGVKEqual(u.GroupVersionKind(), kubeutil.ServiceGVK) { + svcs = append(svcs, u) + } + } + + // complete deployment name + if wsConfig.DeploymentName == "" { + if len(dps) != 1 { + return errors.New("no deployment") + } + wsConfig.DeploymentName = dps[0].GetName() + } + + // validate deployment + var validDep, validSvc bool + for _, v := range dps { + if wsConfig.DeploymentName == v.GetName() { + validDep = true + } + } + if !validDep { + return fmt.Errorf("deployment '%s' is not found", wsConfig.DeploymentName) + } + + // complete service name + if wsConfig.ServiceName == "" { + if len(svcs) != 1 { + return errors.New("no service") + } + wsConfig.ServiceName = svcs[0].GetName() + } + + // validate service + var svc corev1.Service + for _, v := range svcs { + if wsConfig.ServiceName == v.GetName() { + err := runtime.DefaultUnstructuredConverter.FromUnstructured(v.Object, &svc) + if err != nil { + return err + } + validSvc = true + } + } + if !validSvc { + return fmt.Errorf("service '%s' is not found", wsConfig.ServiceName) + } + + // complete service main port + if wsConfig.ServiceMainPortName == "" { + if len(svc.Spec.Ports) != 1 { + return errors.New("failed to specify the service port") + } + wsConfig.ServiceMainPortName = svc.Spec.Ports[0].Name + } + + // validate service main port + var mainServicePort int32 + for _, port := range svc.Spec.Ports { + if port.Name == wsConfig.ServiceMainPortName { + mainServicePort = port.Port + } + } + if mainServicePort == 0 { + return fmt.Errorf("service '%s' is not found", wsConfig.ServiceName) + } + + return nil +} diff --git a/internal/cmd/template/workspace_test.go b/internal/cmd/template/generate_workspace_test.go similarity index 98% rename from internal/cmd/template/workspace_test.go rename to internal/cmd/template/generate_workspace_test.go index 143e4d35..991eaa2d 100644 --- a/internal/cmd/template/workspace_test.go +++ b/internal/cmd/template/generate_workspace_test.go @@ -5,6 +5,7 @@ import ( "testing" cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/template" ) func Test_completeWorkspaceConfig(t *testing.T) { @@ -601,9 +602,9 @@ spec: } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - u, err := preTemplateBuild(tt.args.tmpl) + u, err := template.NewRawYAMLBuilder(tt.args.tmpl, nil).Build() if err != nil { - t.Errorf("preTemplateBuild() error = %v", err) + t.Errorf("dummyTemplateBuild() error = %v", err) } if err := completeWorkspaceConfig(tt.args.wsConfig, u); (err != nil) != tt.wantErr { t.Errorf("completeWorkspaceConfig() error = %v, wantErr %v", err, tt.wantErr) diff --git a/internal/cmd/template/get.go b/internal/cmd/template/get.go deleted file mode 100644 index cd05a4aa..00000000 --- a/internal/cmd/template/get.go +++ /dev/null @@ -1,178 +0,0 @@ -package template - -import ( - "context" - "fmt" - "strconv" - "strings" - "time" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/cli-runtime/pkg/printers" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" -) - -type GetOption struct { - *cmdutil.CliOptions - TemplateNames []string - TypeWorkspace bool - TypeUserAddon bool - - tmpltype string -} - -func GetCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &GetOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.PersistentFlags().BoolVar(&o.TypeWorkspace, "workspace", false, "show type workspace template") - cmd.PersistentFlags().BoolVar(&o.TypeUserAddon, "useraddon", false, "show type useraddon template") - return cmd -} - -func (o *GetOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *GetOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { - return err - } - if o.TypeWorkspace { - o.tmpltype = cosmov1alpha1.TemplateLabelEnumTypeWorkspace - } else if o.TypeUserAddon { - o.tmpltype = cosmov1alpha1.TemplateLabelEnumTypeUserAddon - } - return nil -} - -func (o *GetOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { - return err - } - if len(args) > 0 { - o.TemplateNames = args - } - return nil -} - -func (o *GetOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - - var tmpls []cosmov1alpha1.TemplateObject - - o.Logr.Debug().Info("options", "templateNames", o.TemplateNames) - - if o.tmpltype != "" { - ts, err := kubeutil.ListTemplateObjectsByType(ctx, o.Client, []string{cosmov1alpha1.TemplateLabelEnumTypeWorkspace}) - if err != nil { - return err - } - tmpls = ts - } else { - ts, err := kubeutil.ListTemplateObjects(ctx, o.Client) - if err != nil { - return err - } - tmpls = ts - } - o.Logr.DebugAll().Info("ListTemplates", "tmplList", tmpls) - - if len(o.TemplateNames) > 0 { - ts := make([]cosmov1alpha1.TemplateObject, 0, len(o.TemplateNames)) - for _, selected := range o.TemplateNames { - for _, t := range tmpls { - if selected == t.GetName() { - ts = append(ts, t) - } - } - } - tmpls = ts - } - - w := printers.GetNewTabWriter(o.Out) - defer w.Flush() - - switch o.tmpltype { - case cosmov1alpha1.TemplateLabelEnumTypeWorkspace: - - columnNames := []string{"NAME", "REQUIRED_VARS", "USERROLE", "REQUIRED_ADDONS"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) - - for _, v := range tmpls { - vars := make([]string, 0, len(v.GetSpec().RequiredVars)) - for _, t := range v.GetSpec().RequiredVars { - vars = append(vars, t.Var) - } - rawTmplVars := strings.Join(vars, ",") - - var forRoles, requiredAddons string - ann := v.GetAnnotations() - if ann != nil { - forRoles = ann[cosmov1alpha1.TemplateAnnKeyUserRoles] - requiredAddons = ann[cosmov1alpha1.TemplateAnnKeyRequiredAddons] - } - - rowdata := []string{v.GetName(), rawTmplVars, forRoles, requiredAddons} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } - - case cosmov1alpha1.TemplateLabelEnumTypeUserAddon: - columnNames := []string{"NAME", "REQUIRED_VARS", "CLUSTERSCOPE", "DEFAULT", "USERROLE"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) - - for _, v := range tmpls { - vars := make([]string, 0, len(v.GetSpec().RequiredVars)) - for _, t := range v.GetSpec().RequiredVars { - vars = append(vars, t.Var) - } - rawTmplVars := strings.Join(vars, ",") - - var isDefault, forRoles string - ann := v.GetAnnotations() - if ann != nil { - isDefault = ann[cosmov1alpha1.UserAddonTemplateAnnKeyDefaultUserAddon] - forRoles = ann[cosmov1alpha1.TemplateAnnKeyUserRoles] - } - rowdata := []string{v.GetName(), rawTmplVars, strconv.FormatBool(v.GetScope() == meta.RESTScopeRoot), isDefault, forRoles} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } - - default: - columnNames := []string{"TYPE", "NAME", "CLUSTERSCOPE", "REQUIRED_VARS", "DEFAULT", "USERROLE", "REQUIRED_ADDONS"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) - - for _, v := range tmpls { - vars := make([]string, 0, len(v.GetSpec().RequiredVars)) - for _, t := range v.GetSpec().RequiredVars { - vars = append(vars, t.Var) - } - rawTmplVars := strings.Join(vars, ",") - - var isDefault, forRoles, requiredAddons string - ann := v.GetAnnotations() - if ann != nil { - isDefault = ann[cosmov1alpha1.UserAddonTemplateAnnKeyDefaultUserAddon] - forRoles = ann[cosmov1alpha1.TemplateAnnKeyUserRoles] - requiredAddons = ann[cosmov1alpha1.TemplateAnnKeyRequiredAddons] - } - - tmplType := v.GetLabels()[cosmov1alpha1.TemplateLabelKeyType] - rowdata := []string{tmplType, v.GetName(), strconv.FormatBool(v.GetScope() == meta.RESTScopeRoot), rawTmplVars, isDefault, forRoles, requiredAddons} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } - } - - return nil -} diff --git a/internal/cmd/template/kustomize.go b/internal/cmd/template/kustomize.go deleted file mode 100644 index da3589f6..00000000 --- a/internal/cmd/template/kustomize.go +++ /dev/null @@ -1,48 +0,0 @@ -package template - -import ( - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/template" -) - -const ( - DefaultPackagedFile = "packaged.yaml" -) - -var ( - SecretFileDefaultMode = int32(420) -) - -func NewKustomize(disableNamePrefix bool) *types.Kustomization { - label := make(map[string]string) - label[cosmov1alpha1.LabelKeyInstanceName] = template.DefaultVarsInstance - label[cosmov1alpha1.LabelKeyTemplateName] = template.DefaultVarsTemplate - - kust := &types.Kustomization{ - Labels: []types.Label{{Pairs: label, IncludeSelectors: true}}, - Namespace: template.DefaultVarsNamespace, - Resources: []string{ - DefaultPackagedFile, - }, - } - if !disableNamePrefix { - kust.NamePrefix = template.DefaultVarsInstance + "-" - } - return kust -} - -func addPatchesStrategicMerges(kust *types.Kustomization, files ...types.PatchStrategicMerge) { - if kust.PatchesStrategicMerge == nil { - kust.PatchesStrategicMerge = files - } else { - kust.PatchesStrategicMerge = append(kust.PatchesStrategicMerge, files...) - } -} - -func StructToYaml(obj interface{}) []byte { - out, _ := yaml.Marshal(obj) - return out -} diff --git a/internal/cmd/template/template_builder.go b/internal/cmd/template/template_builder.go new file mode 100644 index 00000000..102989b5 --- /dev/null +++ b/internal/cmd/template/template_builder.go @@ -0,0 +1,247 @@ +package template + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kubeutil" + "github.com/cosmo-workspace/cosmo/pkg/template" + "github.com/cosmo-workspace/cosmo/pkg/workspace" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/yaml" +) + +type TemplateObjectBuilder struct { + tmpl cosmov1alpha1.TemplateObject + disableNamePrefix bool + header string + resourceData []string +} + +func NewTemplateObjectBuilder(isClusterScope bool) *TemplateObjectBuilder { + var b TemplateObjectBuilder + + if isClusterScope { + b.tmpl = &cosmov1alpha1.ClusterTemplate{} + } else { + b.tmpl = &cosmov1alpha1.Template{} + } + + scheme := runtime.NewScheme() + if err := cosmov1alpha1.AddToScheme(scheme); err != nil { + panic(err) + } + + gvk, err := apiutil.GVKForObject(b.tmpl, scheme) + if err != nil { + panic(err) + } + b.tmpl.SetGroupVersionKind(gvk) + return &b +} + +func (b *TemplateObjectBuilder) Name(name string) *TemplateObjectBuilder { + b.tmpl.SetName(name) + return b +} + +func (b *TemplateObjectBuilder) Description(desc string) *TemplateObjectBuilder { + b.tmpl.GetSpec().Description = desc + return b +} + +func (b *TemplateObjectBuilder) RequiredVars(vars []string) *TemplateObjectBuilder { + if len(vars) > 0 { + vv := make([]cosmov1alpha1.RequiredVarSpec, 0, len(vars)) + for _, v := range vars { + vcol := strings.Split(v, ":") + varSpec := cosmov1alpha1.RequiredVarSpec{Var: vcol[0]} + if len(vcol) > 1 { + varSpec.Default = vcol[1] + } + vv = append(vv, varSpec) + } + b.tmpl.GetSpec().RequiredVars = vv + } + return b +} + +func (b *TemplateObjectBuilder) SetUserRoles(roles []string) *TemplateObjectBuilder { + if len(roles) > 0 { + kubeutil.SetAnnotation(b.tmpl, cosmov1alpha1.TemplateAnnKeyUserRoles, strings.Join(roles, ",")) + } + return b +} + +func (b *TemplateObjectBuilder) SetRequiredAddons(addons []string) *TemplateObjectBuilder { + if len(addons) > 0 { + kubeutil.SetAnnotation(b.tmpl, cosmov1alpha1.TemplateAnnKeyRequiredAddons, strings.Join(addons, ",")) + } + return b +} + +func (b *TemplateObjectBuilder) Resources(rawYAML ...string) *TemplateObjectBuilder { + b.resourceData = append(b.resourceData, rawYAML...) + return b +} + +func (b *TemplateObjectBuilder) DisableNamePrefix() *TemplateObjectBuilder { + kubeutil.SetAnnotation(b.tmpl, cosmov1alpha1.TemplateAnnKeyDisableNamePrefix, strconv.FormatBool(true)) + b.disableNamePrefix = true + return b +} + +func (b *TemplateObjectBuilder) TypeUserAddon(setDefault bool) *TemplateObjectBuilder { + template.SetTemplateType(b.tmpl, cosmov1alpha1.TemplateLabelEnumTypeUserAddon) + if setDefault { + kubeutil.SetAnnotation(b.tmpl, cosmov1alpha1.UserAddonTemplateAnnKeyDefaultUserAddon, strconv.FormatBool(true)) + } + + b.DisableNamePrefix() + return b +} + +func (b *TemplateObjectBuilder) TypeWorkspace(wscfg cosmov1alpha1.Config) *TemplateObjectBuilder { + template.SetTemplateType(b.tmpl, cosmov1alpha1.TemplateLabelEnumTypeWorkspace) + workspace.SetConfigOnTemplateAnnotations(b.tmpl, wscfg) + return b +} + +func (b *TemplateObjectBuilder) SetHeader(v cli.VersionInfo) *TemplateObjectBuilder { + b.header = fmt.Sprintf("# Generated by cosmoctl - cosmo-workspace %s commit=%s build=%s\n", v.Version, v.Commit, v.Date) + return b +} + +func (b *TemplateObjectBuilder) Build(ctx context.Context) ([]byte, error) { + kust := NewTempKustomizeBuilder() + if b.disableNamePrefix { + kust.DisableNamePrefix() + } + for _, r := range b.resourceData { + kust.Resources(r) + } + + rawYAML, err := kust.Build(ctx) + if err != nil { + return nil, fmt.Errorf("failed to kustomize build: %w", err) + } + b.tmpl.GetSpec().RawYaml = string(rawYAML) + + output, err := yaml.Marshal(b.tmpl) + if err != nil { + return nil, fmt.Errorf("failed to marshal template object: %w", err) + } + + if b.header != "" { + output = append([]byte(b.header), output...) + } + return output, nil +} + +type TempKustomizeBuilder struct { + kust types.Kustomization + tmpDir string + resourceData []string +} + +func NewTempKustomizeBuilder() *TempKustomizeBuilder { + tmpDir, err := os.MkdirTemp(os.TempDir(), "cosmoctl-*") + if err != nil { + panic(err) + } + + b := TempKustomizeBuilder{ + tmpDir: tmpDir, + resourceData: []string{}, + } + b.kust = types.Kustomization{ + Labels: []types.Label{ + { + Pairs: map[string]string{ + cosmov1alpha1.LabelKeyInstanceName: template.DefaultVarsInstance, + cosmov1alpha1.LabelKeyTemplateName: template.DefaultVarsTemplate, + }, + IncludeSelectors: true, + }, + }, + Namespace: template.DefaultVarsNamespace, + Resources: []string{}, + NamePrefix: template.DefaultVarsInstance + "-", + } + return &b +} + +func (b *TempKustomizeBuilder) Resources(rawYAML ...string) *TempKustomizeBuilder { + b.resourceData = append(b.resourceData, rawYAML...) + return b +} + +func (b *TempKustomizeBuilder) DisableNamePrefix() *TempKustomizeBuilder { + b.kust.NamePrefix = "" + return b +} + +func (b *TempKustomizeBuilder) Build(ctx context.Context) ([]byte, error) { + log := clog.FromContext(ctx).WithCaller() + + cmd, err := kustomizeBuildCmd() + if err != nil { + return nil, err + } + + // save yaml in tmp and set resources + for _, data := range b.resourceData { + f, err := os.CreateTemp(b.tmpDir, "cosmoctl-*") + if err != nil { + return nil, fmt.Errorf("failed to create tmp file %s: %w", b.tmpDir, err) + } + if _, err := f.WriteString(data); err != nil { + return nil, fmt.Errorf("failed to write file %s: %w", f.Name(), err) + } + b.kust.Resources = append(b.kust.Resources, f.Name()) + } + + kustYaml, err := yaml.Marshal(b.kust) + if err != nil { + return nil, err + } + log.Debug().Info(string(kustYaml), "obj", "kustomization.yaml") + + // create kustomization.yaml + if err := os.WriteFile(filepath.Join(b.tmpDir, "kustomization.yaml"), kustYaml, 0644); err != nil { + return nil, err + } + + // run kustomize build + kustomizeCmd := append(cmd, b.tmpDir) + log.Debug().Info("kustomize cmd", "cmd", cmd) + + out, err := exec.CommandContext(ctx, kustomizeCmd[0], kustomizeCmd[1:]...).CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to exec kustomize : %w : %s", err, out) + } + return out, nil +} + +func kustomizeBuildCmd() ([]string, error) { + kust, kustErr := exec.LookPath("kustomize") + if kustErr != nil { + kctl, kctlErr := exec.LookPath("kubectl") + if kctlErr != nil { + return nil, fmt.Errorf("kubectl nor kustomize found: kustmizr=%v, kubectl=%v", kustErr, kctlErr) + } + return []string{kctl, "kustomize"}, nil + } + return []string{kust, "build"}, nil +} diff --git a/internal/cmd/template/validate.go b/internal/cmd/template/validate.go index d0c1d1cf..de81618d 100644 --- a/internal/cmd/template/validate.go +++ b/internal/cmd/template/validate.go @@ -24,13 +24,13 @@ import ( "sigs.k8s.io/yaml" cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - cmdutil "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/template" "github.com/cosmo-workspace/cosmo/pkg/transformer" ) type validateOption struct { - *cmdutil.CliOptions + *cli.RootOptions File string RawVars string @@ -41,10 +41,10 @@ type validateOption struct { vars map[string]string } -func validateCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &validateOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) +func validateCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &validateOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.File, "file", "f", "", "input COSMO Template file yaml path. when specified '-', input from Stdin") cmd.Flags().StringVar(&o.RawVars, "vars", "", "template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2)") cmd.Flags().BoolVar(&o.DryrunOnClientSide, "client", false, "dry-run on client-side. kubectl is required to be executable in PATH") @@ -52,18 +52,8 @@ func validateCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command return cmd } -func (o *validateOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *validateOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } if o.File == "" { @@ -73,7 +63,7 @@ func (o *validateOption) Validate(cmd *cobra.Command, args []string) error { } func (o *validateOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } @@ -85,7 +75,7 @@ func (o *validateOption) Complete(cmd *cobra.Command, args []string) error { return fmt.Errorf("no input via stdin") } // input data from stdin - input, err = io.ReadAll(o.In) + input, err = io.ReadAll(cmd.InOrStdin()) if err != nil { return fmt.Errorf("failed to read input: %w", err) } @@ -134,10 +124,19 @@ func (o *validateOption) Complete(cmd *cobra.Command, args []string) error { } o.vars = vars + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *validateOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) defer cancel() @@ -168,20 +167,20 @@ func (o *validateOption) RunE(cmd *cobra.Command, args []string) error { dummyInst.Spec.Vars = o.vars o.Logr.Info("smoke test: create dummy instance to apply each resources", "instance", dummyInst.GetName()) - o.Logr.Debug().DumpObject(o.Scheme, &dummyInst, "test instance") + o.Logr.Debug().DumpObject(o.KosmoClient.Scheme(), &dummyInst, "test instance") builts, err := template.BuildObjects(o.tmpl.Spec, &dummyInst) if err != nil { return fmt.Errorf("failed to build test instance: %w", err) } // only apply MetadataTransformer - ts := []transformer.Transformer{transformer.NewMetadataTransformer(&dummyInst, o.Scheme, template.IsDisableNamePrefix(&o.tmpl))} + ts := []transformer.Transformer{transformer.NewMetadataTransformer(&dummyInst, o.KosmoClient.Scheme(), template.IsDisableNamePrefix(&o.tmpl))} builts, err = transformer.ApplyTransformers(ctx, ts, builts) if err != nil { return fmt.Errorf("failed to transform objects: %w", err) } - w := printers.GetNewTabWriter(o.Out) + w := printers.GetNewTabWriter(cmd.OutOrStdout()) defer w.Flush() columnNames := []string{"APIVERSION", "KIND", "NAME", "RESULT", "MESSAGE"} fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) @@ -190,7 +189,7 @@ func (o *validateOption) RunE(cmd *cobra.Command, args []string) error { o.Logr.Info("smoke test: dryrun applying dummy resource", "apiVersion", built.GetAPIVersion(), "kind", built.GetKind()) - o.Logr.Debug().DumpObject(o.Scheme, &built, "validating object") + o.Logr.Debug().DumpObject(o.KosmoClient.Scheme(), &built, "validating object") if o.DryrunOnClientSide { err = o.kubectlDryrunApplyOnClient(ctx, &built) } else { @@ -210,7 +209,7 @@ func (o *validateOption) dryrunApplyOnServer(ctx context.Context, obj client.Obj DryRun: []string{metav1.DryRunAll}, } - if err := o.Client.Patch(ctx, obj, client.Apply, options); err != nil { + if err := o.KosmoClient.Patch(ctx, obj, client.Apply, options); err != nil { return fmt.Errorf("dryrun failed: %w", err) } return nil diff --git a/internal/cmd/template/workspace.go b/internal/cmd/template/workspace.go deleted file mode 100644 index 1cf239ee..00000000 --- a/internal/cmd/template/workspace.go +++ /dev/null @@ -1,93 +0,0 @@ -package template - -import ( - "errors" - "fmt" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" -) - -func completeWorkspaceConfig(wsConfig *cosmov1alpha1.Config, unst []unstructured.Unstructured) error { - if wsConfig == nil || len(unst) == 0 { - return errors.New("invalid args") - } - - dps := make([]unstructured.Unstructured, 0) - svcs := make([]unstructured.Unstructured, 0) - - for _, u := range unst { - if kubeutil.IsGVKEqual(u.GroupVersionKind(), kubeutil.DeploymentGVK) { - dps = append(dps, u) - } else if kubeutil.IsGVKEqual(u.GroupVersionKind(), kubeutil.ServiceGVK) { - svcs = append(svcs, u) - } - } - - // complete deployment name - if wsConfig.DeploymentName == "" { - if len(dps) != 1 { - return errors.New("no deployment") - } - wsConfig.DeploymentName = dps[0].GetName() - } - - // validate deployment - var validDep, validSvc bool - for _, v := range dps { - if wsConfig.DeploymentName == v.GetName() { - validDep = true - } - } - if !validDep { - return fmt.Errorf("deployment '%s' is not found", wsConfig.DeploymentName) - } - - // complete service name - if wsConfig.ServiceName == "" { - if len(svcs) != 1 { - return errors.New("no service") - } - wsConfig.ServiceName = svcs[0].GetName() - } - - // validate service - var svc corev1.Service - for _, v := range svcs { - if wsConfig.ServiceName == v.GetName() { - err := runtime.DefaultUnstructuredConverter.FromUnstructured(v.Object, &svc) - if err != nil { - return err - } - validSvc = true - } - } - if !validSvc { - return fmt.Errorf("service '%s' is not found", wsConfig.ServiceName) - } - - // complete service main port - if wsConfig.ServiceMainPortName == "" { - if len(svc.Spec.Ports) != 1 { - return errors.New("failed to specify the service port") - } - wsConfig.ServiceMainPortName = svc.Spec.Ports[0].Name - } - - // validate service main port - var mainServicePort int32 - for _, port := range svc.Spec.Ports { - if port.Name == wsConfig.ServiceMainPortName { - mainServicePort = port.Port - } - } - if mainServicePort == 0 { - return fmt.Errorf("service '%s' is not found", wsConfig.ServiceName) - } - - return nil -} diff --git a/internal/cmd/template_test.go b/internal/cmd/template_test.go deleted file mode 100644 index 43f8d61f..00000000 --- a/internal/cmd/template_test.go +++ /dev/null @@ -1,503 +0,0 @@ -package cmd - -import ( - "bytes" - "errors" - "io" - "os" - "path/filepath" - "regexp" - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "github.com/spf13/cobra" - - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" - . "github.com/cosmo-workspace/cosmo/pkg/snap" -) - -var _ = Describe("cosmoctl [template]", func() { - - var ( - clientMock kubeutil.ClientMock - rootCmd *cobra.Command - options *cmdutil.CliOptions - outBuf *bytes.Buffer - inBuf *bytes.Buffer - ) - consoleOut := func() string { - out, _ := io.ReadAll(outBuf) - return string(out) - } - - BeforeEach(func() { - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme - - baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) - Expect(err).NotTo(HaveOccurred()) - clientMock = kubeutil.NewClientMock(baseclient) - klient := kosmo.NewClient(&clientMock) - - options = cmdutil.NewCliOptions() - options.Client = &klient - inBuf = bytes.NewBufferString("") - outBuf = bytes.NewBufferString("") - options.In = inBuf - options.Out = outBuf - options.ErrOut = outBuf - options.Scheme = scheme - rootCmd = NewRootCmd(options) - }) - - AfterEach(func() { - clientMock.Clear() - testUtil.DeleteTemplateAll() - testUtil.DeleteClusterTemplateAll() - }) - - //================================================================================== - desc := func(args ...string) string { return strings.Join(args, " ") } - errSnap := func(err error) string { - if err == nil { - return "success" - } else { - return err.Error() - } - } - - //================================================================================== - Describe("[generate]", func() { - - var versionRegexp = regexp.MustCompile(`v[0-9]+.[0-9]+.[0-9]+.* cosmo-workspace`) - - templateOutputSnapshot := func(output string) string { - return versionRegexp.ReplaceAllString(output, "vX.X.X cosmo-workspace") - } - - run_test := func(args ...string) { - inBuf.WriteString(yamlData) - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(templateOutputSnapshot(consoleOut())).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - run_test, - Entry(desc, "template", "generate", "--workspace", "--workspace-main-service-port-name", "main", "--required-vars", "HOGE:HOGEHOGE,FUGA:FUGAFUGA"), - Entry(desc, "template", "generate", "--workspace", "--workspace-main-service-port-name", "main", "-o", "/tmp/test-cosmo-template"), - Entry(desc, "template", "generate", "--user-addon", "--set-default-user-addon", "--disable-nameprefix"), - Entry(desc, "template", "generate", "--user-addon", "--set-default-user-addon", "--cluster-scope", "--disable-nameprefix"), - Entry(desc, "template", "generate", "--workspace", "--userroles", "teama-*"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "template", "generate", "--workspace", "--user-addon", "--workspace-main-service-port-name", "main"), - ) - }) - - //================================================================================== - Describe("[get]", func() { - - run_test := func(args ...string) { - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeWorkspace, "template1") - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeWorkspace, "template2") - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeUserAddon, "template3") - testUtil.CreateClusterTemplate(cosmov1alpha1.TemplateLabelEnumTypeUserAddon, "cluster-template1") - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - run_test, - Entry(desc, "template", "get"), - Entry(desc, "template", "get", "--workspace"), - Entry(desc, "template", "get", "template2"), - Entry(desc, "template", "get", "template2", "--workspace"), - Entry(desc, "template", "get", "template2", "template3"), - Entry(desc, "template", "get", "template2", "cluster-template1", "notfound"), - Entry(desc, "template", "get", "notfound"), - ) - - DescribeTable("❌ fail with an unexpected error at list users:", - func(args ...string) { - clientMock.SetListError("\\.RunE$", errors.New("mock list error")) - run_test(args...) - }, - Entry(desc, "template", "get"), - Entry(desc, "template", "get", "--workspace"), - ) - }) - - //================================================================================== - Describe("[validate]", func() { - - createFile := func(data, fname string) string { - f, err := os.Create(filepath.Join(os.TempDir(), fname)) - defer func() { - Ω(f.Close()).ShouldNot(HaveOccurred()) - }() - Ω(err).ShouldNot(HaveOccurred()) - _, err = f.Write([]byte(data)) - Ω(err).ShouldNot(HaveOccurred()) - return f.Name() - } - - run_test := func(args ...string) { - inBuf.WriteString(tmplData) - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - o := consoleOut() - o = regexp.MustCompile(`cosmoctl-validate-[^-]+-`).ReplaceAllString(o, "cosmoctl-validate-XXXXXXXX-") - Expect(o).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - run_test, - Entry(desc, "template", "validate", "--file", createFile(tmplData, "test-template.yaml"), "--vars", "DOMAIN:example.com"), - Entry(desc, "template", "validate", "--file", "-"), - Entry(desc, "template", "validate", "--file", "-", "--client", "-v", "10"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "template", "validate"), - Entry(desc, "template", "validate", "--file"), - Entry(desc, "template", "validate", "--file", "/tmp/(xx*xx)"), - Entry(desc, "template", "validate", "--file", createFile("", "test-empty-template.yaml")), - Entry(desc, "template", "validate", "--file", createFile("hoge", "test-invalid-template.yaml")), - Entry(desc, "template", "validate", "--file", "-", "--vars", "HOGE"), - Entry(desc, "template", "validate", "--file", createFile(userAddonTmplData, "test-user-addon-template.yaml")), - ) - - }) - -}) - -const yamlData = `apiVersion: v1 -kind: Service -metadata: - name: 'workspace' -spec: - ports: - - name: main - port: 3000 - protocol: TCP - type: ClusterIP ---- -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: 'workspace' -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: 'workspace' -spec: - replicas: 1 - template: - spec: - containers: - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: 'workspace' -spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: 'workspace' - port: - name: main - path: /* - pathType: Exact -` - -const tmplData = `# Generated by cosmoctl template command -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Template -metadata: - annotations: - workspace.cosmo-workspace.github.io/deployment: workspace - workspace.cosmo-workspace.github.io/ingress: workspace - workspace.cosmo-workspace.github.io/service: workspace - workspace.cosmo-workspace.github.io/service-main-port: main - workspace.cosmo-workspace.github.io/urlbase: \"\" - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: workspace - name: cmd -spec: - rawYaml: | - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-cosmo-auth-proxy-role' - namespace: '{{NAMESPACE}}' - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: cosmo-auth-proxy-role - subjects: - - kind: ServiceAccount - name: hoge - namespace: '{{NAMESPACE}}' - --- - apiVersion: v1 - kind: Service - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - ports: - - name: main - port: 3000 - protocol: TCP - selector: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - type: ClusterIP - --- - apiVersion: v1 - kind: PersistentVolumeClaim - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 10Gi - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - replicas: 1 - selector: - matchLabels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - template: - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - spec: - containers: - - args: - - --insecure - env: - - name: COSMO_AUTH_PROXY_INSTANCE - value: '{{INSTANCE}}' - - name: COSMO_AUTH_PROXY_NAMESPACE - value: '{{NAMESPACE}}' - image: ghcr.io/cosmo-workspace/cosmo-auth-proxy:latest - name: cosmo-auth-proxy - - image: theiaide/theia - imagePullPolicy: IfNotPresent - name: theia - ports: - - containerPort: 3000 - name: http - protocol: TCP - volumeMounts: - - mountPath: /home/project - name: data - serviceAccountName: default - volumes: - - emptyDir: {} - name: data - --- - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-workspace' - namespace: '{{NAMESPACE}}' - spec: - rules: - - host: main-{{INSTANCE}}-{{NAMESPACE}}.{{DOMAIN}} - http: - paths: - - backend: - service: - name: '{{INSTANCE}}-workspace' - port: - name: main - path: /* - pathType: Exact - requiredVars: - - default: HOGEHOGE - var: HOGE - - default: FUGAFUGA - var: FUGA` - -const userAddonTmplData = ` -# Generated by cosmoctl template command -apiVersion: cosmo-workspace.github.io/v1alpha1 -kind: Template -metadata: - annotations: - useraddon.cosmo-workspace.github.io/default: "true" - creationTimestamp: null - labels: - cosmo-workspace.github.io/type: useraddon - name: cosmo-auth-proxy-role -spec: - description: Role and Rolebinding for COSMO Auth Proxy. By default, it is bound - to the service account named default in the user namespace. - rawYaml: | - apiVersion: rbac.authorization.k8s.io/v1 - kind: Role - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-role' - namespace: '{{NAMESPACE}}' - rules: - - apiGroups: - - cosmo-workspace.github.io - resources: - - workspaces - verbs: - - patch - - update - - get - - list - - watch - - apiGroups: - - cosmo-workspace.github.io - resources: - - workspaces/status - verbs: - - get - - list - - watch - - apiGroups: - - cosmo-workspace.github.io - resources: - - instances - verbs: - - patch - - update - - get - - list - - watch - - apiGroups: - - cosmo-workspace.github.io - resources: - - instances/status - verbs: - - get - - list - - watch - - apiGroups: - - "" - resources: - - events - verbs: - - create - - apiGroups: - - "" - resources: - - services - - secrets - verbs: - - get - - list - - watch - - apiGroups: - - networking.k8s.io - resources: - - ingresses - verbs: - - get - - list - - watch - --- - apiVersion: rbac.authorization.k8s.io/v1 - kind: RoleBinding - metadata: - labels: - cosmo-workspace.github.io/instance: '{{INSTANCE}}' - cosmo-workspace.github.io/template: '{{TEMPLATE}}' - name: '{{INSTANCE}}-rolebinding' - namespace: '{{NAMESPACE}}' - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: '{{INSTANCE}}-role' - subjects: - - kind: ServiceAccount - name: '{{SERVICE_ACCOUNT}}' - namespace: '{{NAMESPACE}}' - requiredVars: - - default: default - var: SERVICE_ACCOUNT - - var: REQUIRED_VAR -` diff --git a/internal/cmd/update/__snapshots__/cmd_test.snap b/internal/cmd/update/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..f07e2bef --- /dev/null +++ b/internal/cmd/update/__snapshots__/cmd_test.snap @@ -0,0 +1,17 @@ +['help should match snapshot 1'] +SnapShot = """ +Update cosmo resources + +Usage: + update [command] + +Available Commands: + network Upsert workspace network. Alias of 'cosmoctl workspace upsert-network' + user Update user. Alias of 'cosmoctl user update' + workspace Update workspace. Alias of 'cosmoctl workspace update' + +Flags: + -h, --help help for update + +Use \" update [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/update/cmd.go b/internal/cmd/update/cmd.go new file mode 100644 index 00000000..71c9a655 --- /dev/null +++ b/internal/cmd/update/cmd.go @@ -0,0 +1,35 @@ +package update + +import ( + "github.com/cosmo-workspace/cosmo/internal/cmd/user" + "github.com/cosmo-workspace/cosmo/internal/cmd/workspace" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/spf13/cobra" +) + +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { + updateCmd := &cobra.Command{ + Use: "update", + Short: "Update cosmo resources", + } + + updateCmd.AddCommand(user.UpdateCmd(&cobra.Command{ + Use: "user USER_NAME", + Short: "Update user. Alias of 'cosmoctl user update'", + Aliases: []string{"us"}, + }, o)) + + updateCmd.AddCommand(workspace.UpdateCmd(&cobra.Command{ + Use: "workspace WORKSPACE_NAME", + Short: "Update workspace. Alias of 'cosmoctl workspace update'", + Aliases: []string{"ws"}, + }, o)) + + updateCmd.AddCommand(workspace.UpsertNetworkCmd(&cobra.Command{ + Use: "network WORKSPACE_NAME --port 8080", + Short: "Upsert workspace network. Alias of 'cosmoctl workspace upsert-network'", + Aliases: []string{"net", "workspace-network", "workspace-networks", "ws-net", "wsnet"}, + }, o)) + + cmd.AddCommand(updateCmd) +} diff --git a/internal/cmd/update/cmd_test.go b/internal/cmd/update/cmd_test.go new file mode 100644 index 00000000..3a20790d --- /dev/null +++ b/internal/cmd/update/cmd_test.go @@ -0,0 +1,31 @@ +package update + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandUpdate(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl update suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"update", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/user/__snapshots__/cmd_test.snap b/internal/cmd/user/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..299613ba --- /dev/null +++ b/internal/cmd/user/__snapshots__/cmd_test.snap @@ -0,0 +1,27 @@ +['help should match snapshot 1'] +SnapShot = """ + +Manipulate COSMO User resource. + +\"User\" is a cluster-scoped Kubernetes CRD which represents a developer or user who use Workspace. + +Once you create User, Kubernetes Namespace is created and bound to the User. + +Usage: + user [command] + +Available Commands: + change-password Change password + create Create user + delete Delete users + get Get users + get-addons Get addons + get-events Get events for user + reset-password Reset password + update Update user + +Flags: + -h, --help help for user + +Use \" user [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/user/change_password.go b/internal/cmd/user/change_password.go new file mode 100644 index 00000000..93a447ba --- /dev/null +++ b/internal/cmd/user/change_password.go @@ -0,0 +1,177 @@ +package user + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type changePasswordOption struct { + *cli.RootOptions + + UserName string + PasswordStdin bool + + currentPassword string + newPassword string +} + +func changePasswordCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &changePasswordOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().BoolVar(&o.PasswordStdin, "password-stdin", false, "input new password from stdin pipe") + return cmd +} + +func (o *changePasswordOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if o.UseKubeAPI && len(args) < 1 { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *changePasswordOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.UserName = args[0] + } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + o.Logr.Info(fmt.Sprintf("Change login user password: %s", o.UserName)) + } + + if err := o.ValidateUser(o.Ctx); err != nil { + return err + } + + if o.PasswordStdin { + if !o.UseKubeAPI { + return errors.New("--password-stdin is only supported with -k") + } + input, err := cli.ReadFromPipedStdin() + if err != nil { + return fmt.Errorf("failed to read from stdin pipe: %w", err) + } + o.newPassword = input + + } else { + input, err := cli.AskInput("Current password: ", true) + if err != nil { + return err + } + o.currentPassword = input + + input, err = cli.AskInput("New password : ", true) + if err != nil { + return err + } + o.newPassword = input + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *changePasswordOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + if o.UseKubeAPI { + if err := o.changePasswordWithKubeClient(ctx); err != nil { + return err + } + } else { + if err := o.changePasswordWithDashClient(ctx); err != nil { + return err + } + } + + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully changed password: %s", o.UserName)) + + return nil +} + +func (o *changePasswordOption) ValidateUser(ctx context.Context) error { + var ( + user *dashv1alpha1.User + err error + ) + if o.UseKubeAPI { + user, err = o.getUserWithKubeClient(ctx, o.UserName) + } else { + user, err = o.getUserWithDashClient(ctx, o.UserName) + } + if err != nil { + return err + } + if cosmov1alpha1.UserAuthType(user.AuthType) != cosmov1alpha1.UserAuthTypePasswordSecert { + return fmt.Errorf("password cannot be changed if auth-type is '%s'", user.AuthType) + } + return nil +} + +func (o *changePasswordOption) getUserWithKubeClient(ctx context.Context, userName string) (*dashv1alpha1.User, error) { + c := o.KosmoClient + user, err := c.GetUser(ctx, userName) + if err != nil { + return nil, err + } + return apiconv.C2D_User(*user), nil +} + +func (o *changePasswordOption) getUserWithDashClient(ctx context.Context, userName string) (*dashv1alpha1.User, error) { + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(&dashv1alpha1.GetUserRequest{UserName: userName}, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + return res.Msg.User, nil +} + +func (o *changePasswordOption) changePasswordWithKubeClient(ctx context.Context) error { + c := o.KosmoClient + if err := c.RegisterPassword(ctx, o.UserName, []byte(o.newPassword)); err != nil { + return err + } + return nil +} + +func (o *changePasswordOption) changePasswordWithDashClient(ctx context.Context) error { + req := &dashv1alpha1.UpdateUserPasswordRequest{ + UserName: o.UserName, + CurrentPassword: o.currentPassword, + NewPassword: o.newPassword, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.UpdateUserPassword(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.UpdateUserPassword", "res", res) + return nil +} diff --git a/internal/cmd/user/cmd.go b/internal/cmd/user/cmd.go index a5dec6ca..ff17cf9c 100644 --- a/internal/cmd/user/cmd.go +++ b/internal/cmd/user/cmd.go @@ -3,40 +3,57 @@ package user import ( "github.com/spf13/cobra" - cmdutil "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" ) -func AddCommand(cmd *cobra.Command, o *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { userCmd := &cobra.Command{ Use: "user", Short: "Manipulate User resource", Long: ` -Manipulate Users like COSMO Dashboard UI. +Manipulate COSMO User resource. -User is actually a Kubernetes Namespace for running Workspaces. +"User" is a cluster-scoped Kubernetes CRD which represents a developer or user who use Workspace. + +Once you create User, Kubernetes Namespace is created and bound to the User. `, } userCmd.AddCommand(resetPasswordCmd(&cobra.Command{ Use: "reset-password USER_NAME", - Short: "Reset user password", + Short: "Reset password", + }, o)) + userCmd.AddCommand(changePasswordCmd(&cobra.Command{ + Use: "change-password [USER_NAME]", + Short: "Change password", }, o)) userCmd.AddCommand(CreateCmd(&cobra.Command{ - Use: "create USER_NAME --role cosmo-admin", + Use: "create USER_NAME", Short: "Create user", }, o)) userCmd.AddCommand(GetCmd(&cobra.Command{ - Use: "get", - Short: "Get users", - Long: ` -Get Users. This command is similar to "kubectl get namespace" -`, + Use: "get [USER_NAME...]", + Short: "Get users", + Aliases: []string{"list"}, + }, o)) + userCmd.AddCommand(GetAddonsCmd(&cobra.Command{ + Use: "get-addons [ADDON_NAME...]", + Short: "Get addons", + Aliases: []string{"get-addon", "get-addons", "addons", "addon"}, + }, o)) + userCmd.AddCommand(GetEventsCmd(&cobra.Command{ + Use: "get-events [USER_NAME]", + Short: "Get events for user", + Aliases: []string{"get-events", "get-event", "events", "event"}, }, o)) userCmd.AddCommand(DeleteCmd(&cobra.Command{ - Use: "delete USER_NAME", - Aliases: []string{"del"}, - Short: "Delete user", + Use: "delete USER_NAME...", + Aliases: []string{"rm"}, + Short: "Delete users", + }, o)) + userCmd.AddCommand(UpdateCmd(&cobra.Command{ + Use: "update", + Short: "Update user", }, o)) - cmd.AddCommand(userCmd) } diff --git a/internal/cmd/user/cmd_test.go b/internal/cmd/user/cmd_test.go new file mode 100644 index 00000000..5097b763 --- /dev/null +++ b/internal/cmd/user/cmd_test.go @@ -0,0 +1,31 @@ +package user + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandUser(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl user suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"user", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/user/create.go b/internal/cmd/user/create.go index 7cb8ea5e..0b842403 100644 --- a/internal/cmd/user/create.go +++ b/internal/cmd/user/create.go @@ -4,57 +4,47 @@ import ( "context" "errors" "fmt" - "regexp" "strings" "time" + "github.com/fatih/color" "github.com/spf13/cobra" cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type CreateOption struct { - *cmdutil.CliOptions + *cli.RootOptions - UserName string - DisplayName string - Roles []string - AuthType string - Admin bool - Addons []string - ClusterAddons []string + UserName string + DisplayName string + Roles []string + AuthType string + PrivilegedRole bool + Addons []string + Force bool - userAddons []cosmov1alpha1.UserAddon + userAddons []*dashv1alpha1.UserAddon } -func CreateCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &CreateOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.Flags().StringVar(&o.DisplayName, "name", "", "[DEPRICATED] use --display-name") +func CreateCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &CreateOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) cmd.Flags().StringVar(&o.DisplayName, "display-name", "", "user display name (default: same as USER_NAME)") cmd.Flags().StringSliceVar(&o.Roles, "role", nil, "user roles") cmd.Flags().StringVar(&o.AuthType, "auth-type", cosmov1alpha1.UserAuthTypePasswordSecert.String(), "user auth type 'password-secret'(default),'ldap'") - cmd.Flags().BoolVar(&o.Admin, "admin", false, "user admin role") - cmd.Flags().StringArrayVar(&o.Addons, "addon", nil, "user addons\nformat is '--addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --addon TEMPLATE_NAME2,KEY:VAL ...' ") - cmd.Flags().StringArrayVar(&o.ClusterAddons, "cluster-addon", nil, "user addons by ClusterTemplate\nformat is '--cluster-addon TEMPLATE_NAME1,KEY:VAL,KEY:VAL --cluster-addon TEMPLATE_NAME2,KEY:VAL ...' ") + cmd.Flags().BoolVar(&o.PrivilegedRole, "privileged", false, "add cosmo-admin role (privileged)") + cmd.Flags().StringArrayVar(&o.Addons, "addon", nil, "user addons\nformat is '--addon TEMPLATE_NAME1,KEY=VAL,KEY=VAL --addon TEMPLATE_NAME2,KEY=VAL ...' ") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") return cmd } -func (o *CreateOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *CreateOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } if !cosmov1alpha1.UserAuthType(o.AuthType).IsValid() { @@ -67,86 +57,114 @@ func (o *CreateOption) Validate(cmd *cobra.Command, args []string) error { } func (o *CreateOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } o.UserName = args[0] - if o.Admin { + if o.PrivilegedRole { o.Roles = []string{cosmov1alpha1.PrivilegedRoleName} } - o.userAddons = make([]cosmov1alpha1.UserAddon, 0, len(o.Addons)+len(o.ClusterAddons)) + o.userAddons = make([]*dashv1alpha1.UserAddon, 0, len(o.Addons)) if len(o.Addons) > 0 { - userAddons, err := parseUserAddonOptions(o.Addons, false) - if err != nil { - return err - } - o.userAddons = append(o.userAddons, userAddons...) - } - if len(o.ClusterAddons) > 0 { - userAddons, err := parseUserAddonOptions(o.ClusterAddons, true) + userAddons, err := apiconv.S2D_UserAddons(o.Addons) if err != nil { return err } o.userAddons = append(o.userAddons, userAddons...) } + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } -func parseUserAddonOptions(rawAddonOptionArray []string, isClusterScope bool) ([]cosmov1alpha1.UserAddon, error) { - // format - // TEMPLATE_NAME - // TEMPLATE_NAME,KEY1:XXX,KEY2:YYY ZZZ,KEY3: - r1 := regexp.MustCompile(`^[^: ,]+(,([^: ,]+):([^,]*))*$`) - r2 := regexp.MustCompile(`^([^: ,]+):([^,]*)$`) +func (o *CreateOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } - userAddons := make([]cosmov1alpha1.UserAddon, 0, len(rawAddonOptionArray)) + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) - for _, addonParm := range rawAddonOptionArray { - if !r1.MatchString(addonParm) { - return nil, fmt.Errorf("invalid addon vars format: %s", addonParm) + o.Logr.Info("creating user", "userName", o.UserName, "displayName", o.DisplayName, "roles", o.Roles, "authType", o.AuthType, "addons", o.Addons) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } } + } - addonSplits := strings.Split(addonParm, ",") - - userAddon := cosmov1alpha1.UserAddon{ - Template: cosmov1alpha1.UserAddonTemplateRef{ - Name: addonSplits[0], - ClusterScoped: isClusterScope, - }, - Vars: make(map[string]string, len(addonSplits)-1), - } + var ( + user *dashv1alpha1.User + err error + ) + if o.UseKubeAPI { + user, err = o.CreateUserWithKubeClient(ctx) + } else { + user, err = o.CreateUserWithDashClient(ctx) + } + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully created user %s", o.UserName)) + OutputTable(cmd.OutOrStdout(), []*dashv1alpha1.User{user}) - for _, k_v := range addonSplits[1:] { - kv := r2.FindStringSubmatch(k_v) - userAddon.Vars[kv[1]] = kv[2] - } - userAddons = append(userAddons, userAddon) + if o.AuthType == cosmov1alpha1.UserAuthTypePasswordSecert.String() { + fmt.Fprintln(cmd.OutOrStdout(), "Default password:", user.DefaultPassword) } - return userAddons, nil + return nil } -func (o *CreateOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - ctx = clog.IntoContext(ctx, o.Logr) +func (o *CreateOption) CreateUserWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.CreateUserRequest{ + UserName: o.UserName, + DisplayName: o.DisplayName, + Roles: o.Roles, + AuthType: o.AuthType, + Addons: o.userAddons, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.CreateUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.CreateUser", "res", res) - if _, err := o.Client.CreateUser(ctx, o.UserName, o.DisplayName, o.Roles, o.AuthType, o.userAddons); err != nil { - return err + return res.Msg.User, nil +} + +func (o *CreateOption) CreateUserWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + user, err := c.CreateUser(ctx, o.UserName, o.DisplayName, o.Roles, o.AuthType, apiconv.D2C_UserAddons(o.userAddons)) + if err != nil { + return nil, err } + d := apiconv.C2D_User(*user) if o.AuthType == cosmov1alpha1.UserAuthTypePasswordSecert.String() { - defaultPassword, err := o.Client.GetDefaultPasswordAwait(ctx, o.UserName) + defaultPassword, err := c.GetDefaultPasswordAwait(ctx, o.UserName) if err != nil { - return err + return nil, err } - cmdutil.PrintfColorInfo(o.Out, "Successfully created user %s\n", o.UserName) - fmt.Fprintln(o.Out, "Default password:", *defaultPassword) - } else { - cmdutil.PrintfColorInfo(o.Out, "Successfully created user %s\n", o.UserName) + d.DefaultPassword = *defaultPassword } - return nil + return d, nil } diff --git a/internal/cmd/user/delete.go b/internal/cmd/user/delete.go index a3c815b4..c33a1b2e 100644 --- a/internal/cmd/user/delete.go +++ b/internal/cmd/user/delete.go @@ -4,39 +4,33 @@ import ( "context" "errors" "fmt" + "strings" "time" + "github.com/fatih/color" "github.com/spf13/cobra" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type DeleteOption struct { - *cmdutil.CliOptions + *cli.RootOptions - UserName string + UserNames []string + Force bool } -func DeleteCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &DeleteOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) +func DeleteCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &DeleteOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") return cmd } -func (o *DeleteOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *DeleteOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } if len(args) < 1 { @@ -46,24 +40,81 @@ func (o *DeleteOption) Validate(cmd *cobra.Command, args []string) error { } func (o *DeleteOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } - o.UserName = args[0] + o.UserNames = args + + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *DeleteOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) defer cancel() ctx = clog.IntoContext(ctx, o.Logr) - c := o.Client + o.Logr.Info("deleting users", "users", o.UserNames) - if _, err := c.DeleteUser(ctx, o.UserName); err != nil { - return err + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } + } + } + + for _, v := range o.UserNames { + if o.UseKubeAPI { + if err := o.DeleteUserWithKubeClient(ctx, v); err != nil { + return err + } + } else { + if err := o.DeleteUserWithDashClient(ctx, v); err != nil { + return err + } + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully deleted user %s", v)) } - cmdutil.PrintfColorInfo(o.Out, "Successfully deleted user %s\n", o.UserName) + return nil +} + +func (o *DeleteOption) DeleteUserWithDashClient(ctx context.Context, userName string) error { + req := &dashv1alpha1.DeleteUserRequest{ + UserName: userName, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.DeleteUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.DeleteUser", "res", res) + + return nil +} + +func (o *DeleteOption) DeleteUserWithKubeClient(ctx context.Context, userName string) error { + c := o.KosmoClient + if _, err := c.DeleteUser(ctx, userName); err != nil { + return err + } return nil } diff --git a/internal/cmd/user/get.go b/internal/cmd/user/get.go index 99b724ff..31cdc46a 100644 --- a/internal/cmd/user/get.go +++ b/internal/cmd/user/get.go @@ -3,131 +3,197 @@ package user import ( "context" "fmt" - "path/filepath" + "io" "strings" "time" + connect_go "github.com/bufbuild/connect-go" + "github.com/fatih/color" "github.com/spf13/cobra" - - "k8s.io/cli-runtime/pkg/printers" + "k8s.io/utils/ptr" cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type GetOption struct { - *cmdutil.CliOptions + *cli.RootOptions - UserNames []string - Filter []string + UserNames []string + Filter []string + OutputFormat string - roleFilter []string - addonFilter []string + filters []cli.Filter } -func GetCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &GetOption{CliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.Flags().StringSliceVar(&o.Filter, "filter", nil, "filter option. 'role' and 'addon' are available for now. e.g. 'role=x', 'addon=y'") +func GetCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringSliceVar(&o.Filter, "filter", nil, "filter option. available columns are ['NAME', 'ROLE', 'ADDON', 'AUTHTYPE', 'PHASE']. available operators are ['==', '!=']. value format is filepath. e.g. '--filter ROLE==*-dev --filter ROLE!=team-a'") + cmd.Flags().StringVarP(&o.OutputFormat, "output", "o", "table", "output format. available values are ['table', 'yaml', 'wide']") return cmd } -func (o *GetOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *GetOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } + switch o.OutputFormat { + case "table", "yaml", "wide": + default: + return fmt.Errorf("invalid output format: %s", o.OutputFormat) + } return nil } func (o *GetOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } if len(args) > 0 { o.UserNames = args } if len(o.Filter) > 0 { - for _, f := range o.Filter { - s := strings.Split(f, "=") - if len(s) != 2 { - return fmt.Errorf("invalid filter expression: %s", f) - } - switch s[0] { - case "addon": - o.addonFilter = append(o.addonFilter, s[1]) - case "role": - o.roleFilter = append(o.roleFilter, s[1]) - default: - o.Logr.Info("invalid filter expression", "filter", f) - return fmt.Errorf("invalid filter expression: %s", f) - } + f, err := cli.ParseFilters(o.Filter) + if err != nil { + return err } + o.filters = f } + for _, f := range o.filters { + o.Logr.Debug().Info("filter", "key", f.Key, "value", f.Value, "op", f.Operator) + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *GetOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) defer cancel() ctx = clog.IntoContext(ctx, o.Logr) - c := o.Client + var users []*dashv1alpha1.User + var err error + if o.UseKubeAPI { + users, err = o.ListUsersByKubeClient(ctx) + if err != nil { + return err + } + } else { + users, err = o.ListUsersWithDashClient(ctx) + if err != nil { + if connect_go.CodeOf(err) == connect_go.CodePermissionDenied { - users, err := c.ListUsers(ctx) - if err != nil { - return err - } - o.Logr.DebugAll().Info("ListUsers", "users", users) - o.Logr.Debug().Info("filter", "role", o.roleFilter, "addon", o.addonFilter) - - if len(o.roleFilter) > 0 { - // And loop - for _, selected := range o.roleFilter { - ts := make([]cosmov1alpha1.User, 0) - for _, t := range users { - RoleFilterLoop: - for _, v := range t.Spec.Roles { - if matched, err := filepath.Match(selected, v.Name); err == nil && matched { - ts = append(ts, t) - break RoleFilterLoop + if len(o.UserNames) == 0 { + fmt.Fprintln(cmd.ErrOrStderr(), color.YellowString("WARNING: Without Admin roles, you can get only login user")) + } else { + for _, v := range o.UserNames { + if v != o.CliConfig.User { + return fmt.Errorf("permission denied: failed to get user: %s", v) + } } } + me, err := o.GetUserWithDashClient(ctx, o.CliConfig.User) + if err != nil { + return err + } + users = []*dashv1alpha1.User{me} + } else { + return err } - users = ts } } - if len(o.addonFilter) > 0 { - // And loop - for _, selected := range o.addonFilter { - ts := make([]cosmov1alpha1.User, 0, len(o.UserNames)) - for _, t := range users { - AddonsLoop: - for _, v := range t.Spec.Addons { - if matched, err := filepath.Match(selected, v.Template.Name); err == nil && matched { - ts = append(ts, t) - break AddonsLoop - } + o.Logr.Debug().Info("Users", "users", users) + + users = o.ApplyFilters(users) + + if o.OutputFormat == "yaml" { + o.OutputYAML(cmd.OutOrStdout(), users) + return nil + } else if o.OutputFormat == "wide" { + OutputWideTable(cmd.OutOrStdout(), users) + return nil + } else { + OutputTable(cmd.OutOrStdout(), users) + return nil + } +} + +func (o *GetOption) ListUsersWithDashClient(ctx context.Context) ([]*dashv1alpha1.User, error) { + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUsers(ctx, cli.NewRequestWithToken(&dashv1alpha1.GetUsersRequest{ + WithRaw: ptr.To(o.OutputFormat == "yaml"), + }, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUsers", "res", res) + return res.Msg.Items, nil +} + +func (o *GetOption) GetUserWithDashClient(ctx context.Context, userName string) (*dashv1alpha1.User, error) { + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(&dashv1alpha1.GetUserRequest{ + UserName: userName, + WithRaw: ptr.To(o.OutputFormat == "yaml"), + }, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + return res.Msg.User, nil +} + +func (o *GetOption) ApplyFilters(users []*dashv1alpha1.User) []*dashv1alpha1.User { + for _, f := range o.filters { + o.Logr.Debug().Info("applying filter", "key", f.Key, "value", f.Value, "op", f.Operator) + + switch strings.ToUpper(f.Key) { + case "NAME": + users = cli.DoFilter(users, func(u *dashv1alpha1.User) []string { + return []string{u.Name} + }, f) + case "ROLE", "ROLES": + users = cli.DoFilter(users, func(u *dashv1alpha1.User) []string { + arr := make([]string, 0, len(u.Roles)) + arr = append(arr, u.Roles...) + return arr + }, f) + case "ADDON", "ADDONS": + users = cli.DoFilter(users, func(u *dashv1alpha1.User) []string { + arr := make([]string, 0, len(u.Addons)) + for _, a := range u.Addons { + arr = append(arr, a.Template) } - } - users = ts + return arr + }, f) + case "AUTHTYPE": + users = cli.DoFilter(users, func(u *dashv1alpha1.User) []string { + return []string{u.AuthType} + }, f) + case "PHASE": + users = cli.DoFilter(users, func(u *dashv1alpha1.User) []string { + return []string{u.Status} + }, f) + default: + o.Logr.Info("WARNING: unknown filter key", "key", f.Key) } } if len(o.UserNames) > 0 { - ts := make([]cosmov1alpha1.User, 0, len(o.UserNames)) + ts := make([]*dashv1alpha1.User, 0, len(o.UserNames)) UserLoop: // Or loop for _, t := range users { @@ -140,24 +206,62 @@ func (o *GetOption) RunE(cmd *cobra.Command, args []string) error { } users = ts } + return users +} + +func (o *GetOption) OutputYAML(w io.Writer, objs []*dashv1alpha1.User) { + docs := make([]string, len(objs)) + for i, t := range objs { + docs[i] = *t.Raw + } + fmt.Fprintln(w, strings.Join(docs, "---\n")) +} + +func printAddons(addons []*dashv1alpha1.UserAddon) string { + arr := make([]string, len(addons)) + for i, v := range addons { + arr[i] = v.Template + } + return strings.Join(arr, ",") +} + +func printAddonWithVars(addons []*dashv1alpha1.UserAddon) string { + arr := make([]string, len(addons)) + for i, v := range apiconv.D2S_UserAddons(addons) { + arr[i] = v + } + return strings.Join(arr, " ") +} - w := printers.GetNewTabWriter(o.Out) - defer w.Flush() +func OutputTable(out io.Writer, users []*dashv1alpha1.User) { + data := [][]string{} - columnNames := []string{"NAME", "ROLES", "AUTHTYPE", "NAMESPACE", "PHASE", "ADDONS"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) for _, v := range users { - role := make([]string, 0, len(v.Spec.Roles)) - for _, v := range v.Spec.Roles { - role = append(role, v.Name) - } - addons := make([]string, 0, len(v.Spec.Addons)) - for _, v := range v.Spec.Addons { - addons = append(addons, v.Template.Name) - } - rowdata := []string{v.Name, strings.Join(role, ","), v.Spec.AuthType.String(), v.Status.Namespace.Name, string(v.Status.Phase), strings.Join(addons, ",")} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) + data = append(data, []string{v.Name, strings.Join(v.Roles, ","), v.AuthType, cosmov1alpha1.UserNamespace(v.Name), v.Status, printAddons(v.Addons)}) } - return nil + cli.OutputTable(out, + []string{"NAME", "ROLES", "AUTHTYPE", "NAMESPACE", "PHASE", "ADDONS"}, + data) +} + +func OutputWideTable(out io.Writer, users []*dashv1alpha1.User) { + data := [][]string{} + + for _, v := range users { + data = append(data, []string{v.Name, v.DisplayName, strings.Join(v.Roles, ","), v.AuthType, cosmov1alpha1.UserNamespace(v.Name), v.Status, printAddonWithVars(v.Addons)}) + } + + cli.OutputTable(out, + []string{"NAME", "DISPLAYNAME", "ROLES", "AUTHTYPE", "NAMESPACE", "PHASE", "ADDONS"}, + data) +} + +func (o *GetOption) ListUsersByKubeClient(ctx context.Context) ([]*dashv1alpha1.User, error) { + c := o.KosmoClient + users, err := c.ListUsers(ctx) + if err != nil { + return nil, err + } + return apiconv.C2D_Users(users, apiconv.WithUserRaw(ptr.To(o.OutputFormat == "yaml"))), nil } diff --git a/internal/cmd/user/get_addons.go b/internal/cmd/user/get_addons.go new file mode 100644 index 00000000..27191c1e --- /dev/null +++ b/internal/cmd/user/get_addons.go @@ -0,0 +1,213 @@ +package user + +import ( + "context" + "fmt" + "io" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type GetAddonsOption struct { + *cli.RootOptions + AddonNames []string + Filter []string + OutputFormat string + + filters []cli.Filter +} + +func GetAddonsCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetAddonsOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringSliceVar(&o.Filter, "filter", nil, "filter option. available columns are ['NAME', 'USERROLE', 'REQUIRED_USERADDON']. available operators are ['==', '!=']. value format is filepath. e.g. '--filter USERROLE==*-dev --filter USERROLE!=team-a'") + cmd.Flags().StringVarP(&o.OutputFormat, "output", "o", "table", "output format. available values are ['table', 'yaml']") + return cmd +} + +func (o *GetAddonsOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + switch o.OutputFormat { + case "table", "yaml": + default: + return fmt.Errorf("invalid output format: %s", o.OutputFormat) + } + return nil +} + +func (o *GetAddonsOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.AddonNames = args + } + + if len(o.Filter) > 0 { + f, err := cli.ParseFilters(o.Filter) + if err != nil { + return err + } + o.filters = f + } + for _, f := range o.filters { + o.Logr.Debug().Info("filter", "key", f.Key, "value", f.Value, "op", f.Operator) + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *GetAddonsOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + tmpls []*dashv1alpha1.Template + err error + ) + if o.UseKubeAPI { + tmpls, err = o.ListUserAddonsByKubeClient(ctx, o.OutputFormat == "yaml") + } else { + tmpls, err = o.ListUserAddonsWithDashClient(ctx, o.OutputFormat == "yaml") + } + if err != nil { + return err + } + o.Logr.Debug().Info("UserAddon templates", "templates", tmpls) + + tmpls = o.ApplyFilters(tmpls) + + if o.OutputFormat == "yaml" { + o.OutputYAML(cmd.OutOrStdout(), tmpls) + return nil + } else { + o.OutputTable(cmd.OutOrStdout(), tmpls) + return nil + } +} + +func (o *GetAddonsOption) ListUserAddonsWithDashClient(ctx context.Context, withRaw bool) ([]*dashv1alpha1.Template, error) { + req := &dashv1alpha1.GetUserAddonTemplatesRequest{ + UseRoleFilter: ptr.To(false), + WithRaw: ptr.To(withRaw), + } + c := o.CosmoDashClient + res, err := c.TemplateServiceClient.GetUserAddonTemplates(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("TemplateServiceClient.GetUserAddonTemplates", "res", res) + return res.Msg.Items, nil +} + +func (o *GetAddonsOption) ApplyFilters(tmpls []*dashv1alpha1.Template) []*dashv1alpha1.Template { + for _, f := range o.filters { + o.Logr.Debug().Info("applying filter", "key", f.Key, "value", f.Value, "op", f.Operator) + + switch strings.ToUpper(f.Key) { + case "NAME": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + return []string{u.Name} + }, f) + case "USERROLE", "USERROLES", "REQUIRED_USERROLES": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + arr := make([]string, 0, len(u.Userroles)) + arr = append(arr, u.Userroles...) + return arr + }, f) + case "REQUIRED_USERADDONS": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + arr := make([]string, 0, len(u.RequiredUseraddons)) + arr = append(arr, u.RequiredUseraddons...) + return arr + }, f) + default: + o.Logr.Info("WARNING: unknown filter key", "key", f.Key) + } + } + + if len(o.AddonNames) > 0 { + ts := make([]*dashv1alpha1.Template, 0, len(o.AddonNames)) + UserLoop: + // Or loop + for _, t := range tmpls { + for _, selected := range o.AddonNames { + if selected == t.GetName() { + ts = append(ts, t) + continue UserLoop + } + } + } + tmpls = ts + } + return tmpls +} + +func (o *GetAddonsOption) OutputYAML(w io.Writer, tmpls []*dashv1alpha1.Template) { + docs := make([]string, len(tmpls)) + for i, t := range tmpls { + docs[i] = *t.Raw + } + fmt.Fprintln(w, strings.Join(docs, "---\n")) +} + +func (o *GetAddonsOption) OutputTable(w io.Writer, tmpls []*dashv1alpha1.Template) { + data := [][]string{} + + for _, v := range tmpls { + rawRequiredAddons := strings.Join(v.RequiredUseraddons, ",") + rawUserroles := strings.Join(v.Userroles, ",") + + var isDefaultUserAddon bool + if v.IsDefaultUserAddon != nil { + isDefaultUserAddon = *v.IsDefaultUserAddon + } + data = append(data, []string{v.GetName(), strconv.FormatBool(isDefaultUserAddon), requiredVars(v.RequiredVars), rawUserroles, rawRequiredAddons}) + } + + cli.OutputTable(w, + []string{"NAME", "DEFAULT", "REQUIRED_VARS(default)", "USERROLE", "REQUIRED_USERADDON"}, + data) +} + +func requiredVars(vs []*dashv1alpha1.TemplateRequiredVars) string { + var s []string + for _, v := range vs { + data := v.VarName + if v.DefaultValue != "" { + data += fmt.Sprintf("(%s)", v.DefaultValue) + } + s = append(s, data) + } + return strings.Join(s, ",") +} + +func (o *GetAddonsOption) ListUserAddonsByKubeClient(ctx context.Context, withRaw bool) ([]*dashv1alpha1.Template, error) { + c := o.KosmoClient + tmpls, err := c.ListUserAddonTemplates(ctx) + if err != nil { + return nil, err + } + return apiconv.C2D_Templates(tmpls, apiconv.WithTemplateRaw(&withRaw)), nil +} diff --git a/internal/cmd/user/get_events.go b/internal/cmd/user/get_events.go new file mode 100644 index 00000000..9381bf1a --- /dev/null +++ b/internal/cmd/user/get_events.go @@ -0,0 +1,134 @@ +package user + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/spf13/cobra" + "google.golang.org/protobuf/types/known/timestamppb" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type GetEventsOption struct { + *cli.RootOptions + UserName string +} + +func GetEventsCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetEventsOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + return cmd +} + +func (o *GetEventsOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if o.UseKubeAPI && len(args) < 1 { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *GetEventsOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.UserName = args[0] + } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *GetEventsOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + events []*dashv1alpha1.Event + err error + ) + if o.UseKubeAPI { + events, err = o.GetEventsByKubeClient(ctx) + if err != nil { + return err + } + } else { + events, err = o.GetEventsWithDashClient(ctx) + if err != nil { + return err + } + } + o.Logr.Debug().Info("Events", "events", events) + + o.OutputTable(cmd.OutOrStdout(), events) + return nil +} + +func (o *GetEventsOption) GetEventsWithDashClient(ctx context.Context) ([]*dashv1alpha1.Event, error) { + req := &dashv1alpha1.GetUserRequest{ + UserName: o.UserName, + WithRaw: ptr.To(true), + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to get user: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + return res.Msg.User.Events, nil +} + +func (o *GetEventsOption) OutputTable(w io.Writer, events []*dashv1alpha1.Event) { + data := [][]string{} + + for _, v := range events { + data = append(data, []string{lastSeen(v.EventTime, v.Series), v.Type, v.Reason, regarding(v.Regarding), v.ReportingController, v.Note}) + } + cli.OutputTable(w, + []string{"LAST SEEN", "TYPE", "REASON", "OBJECT", "REPORTER", "MESSAGE"}, + data) +} + +func lastSeen(t *timestamppb.Timestamp, series *dashv1alpha1.EventSeries) string { + if series != nil { + return fmt.Sprintf("%s (%vx)", time.Since(t.AsTime()).Round(time.Second), series.Count) + } + return time.Since(t.AsTime()).Round(time.Second).String() +} + +func regarding(v *dashv1alpha1.ObjectReference) string { + return fmt.Sprintf("%s/%s", v.Kind, v.Name) +} + +func (o *GetEventsOption) GetEventsByKubeClient(ctx context.Context) ([]*dashv1alpha1.Event, error) { + c := o.KosmoClient + events, err := c.ListEvents(ctx, v1alpha1.UserNamespace(o.UserName)) + if err != nil { + return nil, err + } + o.Logr.Debug().Info("ListEvents", "events", events) + return apiconv.K2D_Events(events), nil +} diff --git a/internal/cmd/user/reset_password.go b/internal/cmd/user/reset_password.go index ebe160d5..f6f6a7ae 100644 --- a/internal/cmd/user/reset_password.go +++ b/internal/cmd/user/reset_password.go @@ -4,92 +4,114 @@ import ( "context" "errors" "fmt" + "strings" "time" + "github.com/fatih/color" "github.com/spf13/cobra" - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" ) type resetPasswordOption struct { - *cmdutil.CliOptions + *changePasswordOption - UserName string - Password string + Force bool + Silent bool } -func resetPasswordCmd(cmd *cobra.Command, cliOpt *cmdutil.CliOptions) *cobra.Command { - o := &resetPasswordOption{CliOptions: cliOpt} - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - cmd.Flags().StringVar(&o.Password, "password", "", "new password (default: random string)") +func resetPasswordCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &resetPasswordOption{changePasswordOption: &changePasswordOption{RootOptions: cliOpt}} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") + cmd.Flags().BoolVar(&o.Silent, "silent", false, "only output new password") return cmd } -func (o *resetPasswordOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *resetPasswordOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } if len(args) < 1 { return errors.New("invalid args") } + if !o.UseKubeAPI { + return errors.New("force reset is only available with -k") + } return nil } func (o *resetPasswordOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.CliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } o.UserName = args[0] + + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *resetPasswordOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) defer cancel() ctx = clog.IntoContext(ctx, o.Logr) - c := o.Client - - user, err := c.GetUser(ctx, o.UserName) - if err != nil { + if err := o.ValidateUser(ctx); err != nil { return err } - if user.Spec.AuthType != cosmov1alpha1.UserAuthTypePasswordSecert { - return fmt.Errorf("password cannot be changed if auth-type is '%s'", user.Spec.AuthType.String()) - } - if o.Password == "" { - if err := c.ResetPassword(ctx, o.UserName); err != nil { - return err - } - } else { - if err := c.RegisterPassword(ctx, o.UserName, []byte(o.Password)); err != nil { - return err + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } } } - cmdutil.PrintfColorInfo(o.Out, "Successfully reset password: user %s\n", o.UserName) + newPassword, err := o.resetPasswordWithKubeClient(ctx) + if err != nil { + return err + } - if o.Password == "" { - pass, err := c.GetDefaultPassword(ctx, o.UserName) - if err != nil { - return err - } - fmt.Fprintln(o.Out, "New password:", *pass) + if o.Silent { + fmt.Fprintln(cmd.OutOrStdout(), *newPassword) + } else { + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully reset password: user %s", o.UserName)) + fmt.Fprintln(cmd.OutOrStdout(), "New password:", *newPassword) } return nil } + +func (o *resetPasswordOption) resetPasswordWithKubeClient(ctx context.Context) (*string, error) { + c := o.KosmoClient + if err := c.ResetPassword(ctx, o.UserName); err != nil { + return nil, err + } + pass, err := c.GetDefaultPassword(ctx, o.UserName) + if err != nil { + return nil, err + } + if pass == nil { + return nil, errors.New("password is nil") + } + return pass, nil +} diff --git a/internal/cmd/user/update.go b/internal/cmd/user/update.go new file mode 100644 index 00000000..0b20c051 --- /dev/null +++ b/internal/cmd/user/update.go @@ -0,0 +1,25 @@ +package user + +import ( + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/cli" +) + +func UpdateCmd(cmd *cobra.Command, o *cli.RootOptions) *cobra.Command { + cmd.AddCommand(UpdateDisplayNameCmd(&cobra.Command{ + Use: "display-name USER_NAME", + Aliases: []string{"displayname", "name"}, + Short: "Update display name", + }, o)) + cmd.AddCommand(UpdateRoleCmd(&cobra.Command{ + Use: "role USER_NAME", + Short: "Update role", + }, o)) + cmd.AddCommand(UpdateAddonCmd(&cobra.Command{ + Use: "addon USER_NAME", + Aliases: []string{"addon", "useraddon", "user-addon"}, + Short: "Update addon", + }, o)) + return cmd +} diff --git a/internal/cmd/user/update_addon.go b/internal/cmd/user/update_addon.go new file mode 100644 index 00000000..6a37d196 --- /dev/null +++ b/internal/cmd/user/update_addon.go @@ -0,0 +1,183 @@ +package user + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type UpdateAddonOption struct { + *cli.RootOptions + + UserName string + Addons []string + Force bool + + userAddons []*dashv1alpha1.UserAddon +} + +func UpdateAddonCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &UpdateAddonOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringArrayVar(&o.Addons, "addon", nil, "user addons\nformat is '--addon TEMPLATE_NAME1,KEY=VAL,KEY=VAL --addon TEMPLATE_NAME2,KEY=VAL ...' ") + cmd.MarkFlagsOneRequired("addon") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") + return cmd +} + +func (o *UpdateAddonOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if len(args) < 1 { + return errors.New("invalid args") + } + return nil +} + +func (o *UpdateAddonOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + + o.UserName = args[0] + + o.userAddons = make([]*dashv1alpha1.UserAddon, 0, len(o.Addons)) + if len(o.Addons) > 0 { + userAddons, err := apiconv.S2D_UserAddons(o.Addons) + if err != nil { + return err + } + o.userAddons = append(o.userAddons, userAddons...) + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *UpdateAddonOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + currentUser *dashv1alpha1.User + err error + ) + if o.UseKubeAPI { + currentUser, err = o.GetUserWithKubeClient(ctx) + } else { + currentUser, err = o.GetUserWithDashClient(ctx) + } + if err != nil { + return err + } + + o.Logr.Info("updating user", "userName", o.UserName, "currentAddons", apiconv.D2S_UserAddons(currentUser.Addons), "newAddons", o.Addons) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } + } + } + + var user *dashv1alpha1.User + if o.UseKubeAPI { + user, err = o.UpdateUserWithKubeClient(ctx) + } else { + user, err = o.UpdateUserWithDashClient(ctx) + } + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully updated user %s", o.UserName)) + OutputWideTable(cmd.OutOrStdout(), []*dashv1alpha1.User{user}) + + return nil +} + +func (o *UpdateAddonOption) UpdateUserWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.CosmoDashClient + + req := &dashv1alpha1.UpdateUserAddonsRequest{ + UserName: o.UserName, + Addons: o.userAddons, + } + res, err := c.UserServiceClient.UpdateUserAddons(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.UpdateUserAddons", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateAddonOption) UpdateUserWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + opts := kosmo.UpdateUserOpts{ + UserAddons: apiconv.D2C_UserAddons(o.userAddons), + } + + user, err := c.UpdateUser(ctx, o.UserName, opts) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} + +func (o *UpdateAddonOption) GetUserWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.GetUserRequest{ + UserName: o.UserName, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateAddonOption) GetUserWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + user, err := c.GetUser(ctx, o.UserName) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} diff --git a/internal/cmd/user/update_display_name.go b/internal/cmd/user/update_display_name.go new file mode 100644 index 00000000..5363475d --- /dev/null +++ b/internal/cmd/user/update_display_name.go @@ -0,0 +1,173 @@ +package user + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type UpdateDisplayNameOption struct { + *cli.RootOptions + + UserName string + DisplayName string + Force bool +} + +func UpdateDisplayNameCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &UpdateDisplayNameOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVar(&o.DisplayName, "display-name", "", "user display name (Required)") + cmd.MarkFlagRequired("display-name") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") + return cmd +} + +func (o *UpdateDisplayNameOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if len(args) < 1 { + return errors.New("invalid args") + } + return nil +} + +func (o *UpdateDisplayNameOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + + o.UserName = args[0] + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *UpdateDisplayNameOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + currentUser *dashv1alpha1.User + err error + ) + if o.UseKubeAPI { + currentUser, err = o.GetUserWithKubeClient(ctx) + } else { + currentUser, err = o.GetUserWithDashClient(ctx) + } + if err != nil { + return err + } + + o.Logr.Info("updating user display name", "userName", o.UserName, "currentDisplayName", currentUser.DisplayName, "newDisplayName", o.DisplayName) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } + } + } + + var user *dashv1alpha1.User + if o.UseKubeAPI { + user, err = o.UpdateUserDisplayNameWithKubeClient(ctx) + } else { + user, err = o.UpdateUserDisplayNameWithDashClient(ctx) + } + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully updated user %s", o.UserName)) + OutputWideTable(cmd.OutOrStdout(), []*dashv1alpha1.User{user}) + + return nil +} + +func (o *UpdateDisplayNameOption) UpdateUserDisplayNameWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.UpdateUserDisplayNameRequest{ + UserName: o.UserName, + DisplayName: o.DisplayName, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.UpdateUserDisplayName(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.UpdateUserDisplayName", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateDisplayNameOption) UpdateUserDisplayNameWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + + opts := kosmo.UpdateUserOpts{ + DisplayName: ptr.To(o.UserName), + } + + user, err := c.UpdateUser(ctx, o.UserName, opts) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} + +func (o *UpdateDisplayNameOption) GetUserWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.GetUserRequest{ + UserName: o.UserName, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateDisplayNameOption) GetUserWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + user, err := c.GetUser(ctx, o.UserName) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} diff --git a/internal/cmd/user/update_role.go b/internal/cmd/user/update_role.go new file mode 100644 index 00000000..d3711784 --- /dev/null +++ b/internal/cmd/user/update_role.go @@ -0,0 +1,179 @@ +package user + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type UpdateRoleOption struct { + *cli.RootOptions + + UserName string + Roles []string + PrivilegedRole bool + Force bool + + userAddons []*dashv1alpha1.UserAddon +} + +func UpdateRoleCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &UpdateRoleOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringSliceVar(&o.Roles, "role", nil, "user roles") + cmd.MarkFlagsOneRequired("role") + cmd.Flags().BoolVar(&o.PrivilegedRole, "privileged", false, "add cosmo-admin role (privileged)") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") + return cmd +} + +func (o *UpdateRoleOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if len(args) < 1 { + return errors.New("invalid args") + } + return nil +} + +func (o *UpdateRoleOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + + o.UserName = args[0] + + if o.PrivilegedRole { + o.Roles = []string{cosmov1alpha1.PrivilegedRoleName} + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *UpdateRoleOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + currentUser *dashv1alpha1.User + err error + ) + if o.UseKubeAPI { + currentUser, err = o.GetUserWithKubeClient(ctx) + } else { + currentUser, err = o.GetUserWithDashClient(ctx) + } + if err != nil { + return err + } + + o.Logr.Info("updating user roles", "userName", o.UserName, "currentRole", currentUser.Roles, "newRole", o.Roles) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } + } + } + + var user *dashv1alpha1.User + if o.UseKubeAPI { + user, err = o.UpdateUserRoleWithKubeClient(ctx) + } else { + user, err = o.UpdateUserRoleWithDashClient(ctx) + } + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully updated user %s", o.UserName)) + OutputWideTable(cmd.OutOrStdout(), []*dashv1alpha1.User{user}) + + return nil +} + +func (o *UpdateRoleOption) UpdateUserRoleWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.UpdateUserRoleRequest{ + UserName: o.UserName, + Roles: o.Roles, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.UpdateUserRole(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.UpdateUserRole", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateRoleOption) UpdateUserRoleWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + opts := kosmo.UpdateUserOpts{ + UserRoles: apiconv.S2C_UserRoles(o.Roles), + } + user, err := c.UpdateUser(ctx, o.UserName, opts) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} + +func (o *UpdateRoleOption) GetUserWithDashClient(ctx context.Context) (*dashv1alpha1.User, error) { + req := &dashv1alpha1.GetUserRequest{ + UserName: o.UserName, + } + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUser(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUser", "res", res) + + return res.Msg.User, nil +} + +func (o *UpdateRoleOption) GetUserWithKubeClient(ctx context.Context) (*dashv1alpha1.User, error) { + c := o.KosmoClient + user, err := c.GetUser(ctx, o.UserName) + if err != nil { + return nil, err + } + d := apiconv.C2D_User(*user) + + return d, nil +} diff --git a/internal/cmd/user_test.go b/internal/cmd/user_test.go deleted file mode 100644 index 8d910049..00000000 --- a/internal/cmd/user_test.go +++ /dev/null @@ -1,307 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "errors" - "io" - "regexp" - "strings" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" - . "github.com/cosmo-workspace/cosmo/pkg/snap" -) - -var _ = Describe("cosmoctl [user]", func() { - - var ( - clientMock kubeutil.ClientMock - rootCmd *cobra.Command - options *cmdutil.CliOptions - outBuf *bytes.Buffer - ) - consoleOut := func() string { - out, _ := io.ReadAll(outBuf) - return string(out) - } - - userSnap := func(us *cosmov1alpha1.User) struct{ Name, Namespace, Spec, Status interface{} } { - return struct{ Name, Namespace, Spec, Status interface{} }{ - Name: us.Name, - Namespace: us.Namespace, - Spec: us.Spec, - Status: us.Status, - } - } - - BeforeEach(func() { - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme - - baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) - Expect(err).NotTo(HaveOccurred()) - clientMock = kubeutil.NewClientMock(baseclient) - klient := kosmo.NewClient(&clientMock) - - options = cmdutil.NewCliOptions() - options.Client = &klient - outBuf = bytes.NewBufferString("") - options.Out = outBuf - options.ErrOut = outBuf - options.Scheme = scheme - rootCmd = NewRootCmd(options) - }) - - AfterEach(func() { - clientMock.Clear() - testUtil.DeleteCosmoUserAll() - testUtil.DeleteTemplateAll() - testUtil.DeleteClusterTemplateAll() - }) - - //================================================================================== - desc := func(args ...string) string { return strings.Join(args, " ") } - errSnap := func(err error) string { - if err == nil { - return "success" - } else { - return err.Error() - } - } - //================================================================================== - Describe("[all]", func() { - - DescribeTable("❌ fail with invalid arg: kubeconfig", - func(args ...string) { - By("---------------test start----------------") - options.Client = nil - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).Should(HaveOccurred()) - Ω(consoleOut()).To(MatchSnapShot()) - By("---------------test end---------------") - }, - Entry(desc, "user", "create", "user1", "--kubeconfig", "XXXX"), - Entry(desc, "user", "get", "--kubeconfig", "XXXX"), - Entry(desc, "user", "delete", "user1", "--kubeconfig", "XXXX"), - Entry(desc, "user", "reset-password", "user1", "--password", "XXXXXXXX", "--kubeconfig", "XXXX"), - ) - }) - - //================================================================================== - Describe("[create]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - o := consoleOut() - o = regexp.MustCompile("Default password: .*").ReplaceAllString(o, "Default password: xxxxxxxx") - Ω(o).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - if err == nil { - wsv1User, err := k8sClient.GetUser(context.Background(), args[2]) - Expect(err).NotTo(HaveOccurred()) // created - Expect(userSnap(wsv1User)).To(MatchSnapShot()) - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateUserNameSpaceandDefaultPasswordIfAbsent("user-create") - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeUserAddon, "user-template1") - testUtil.CreateClusterTemplate(cosmov1alpha1.TemplateLabelEnumTypeUserAddon, "user-clustertemplate1") - run_test(args...) - }, - Entry(desc, "user", "create", "user-create", "--name", "create 1", "--role", "cosmo-admin", "--auth-type", "password-secret", "user-template1,HOGE:HOGEHOGE"), - Entry(desc, "user", "create", "user-create", "--name", "create 1", "--role", "cosmo-admin", "--auth-type", "ldap", "user-template1,HOGE:HOGEHOGE"), - Entry(desc, "user", "create", "user-create", "--name", "create 1", "--role", "cosmo-admin", "--addon", "user-template1,HOGE:HOGEHOGE"), - Entry(desc, "user", "create", "user-create", "--name", "create 1", "--admin", "--addon", "user-template1,HOGE:HOGEHOGE"), - Entry(desc, "user", "create", "user-create"), - Entry(desc, "user", "create", "user-create", "--addon", "user-template1"), - Entry(desc, "user", "create", "user-create", "--addon", "user-template1,HOGE: HOGE HOGE ,FUGA:FUGAF:UGA"), - Entry(desc, "user", "create", "user-create", "--addon", "user-template1", "--cluster-addon", "user-clustertemplate1"), - Entry(desc, "user", "create", "user-create", "--admin", "--role", "cosmo-admin"), - Entry(desc, "user", "create", "user-create", "--role", "xxx"), - ) - - DescribeTable("✅ success to create password immediately:", - func(args ...string) { - testUtil.CreateUserNameSpaceandDefaultPasswordIfAbsent("user-create") - run_test(args...) - }, - Entry(desc, "user", "create", "user-create"), - ) - - DescribeTable("✅ success to create password later:", - func(args ...string) { - timer := time.AfterFunc(100*time.Millisecond, func() { - testUtil.CreateUserNameSpaceandDefaultPasswordIfAbsent("user-create-later") - }) - defer timer.Stop() - run_test(args...) - }, - Entry(desc, "user", "create", "user-create-later"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "user", "create"), - Entry(desc, "user", "create", "--admin"), - Entry(desc, "user", "create", "TESTuser"), - Entry(desc, "user", "create", "user-create", "--addon", "XXXXXXXXX,HOGE:yyy"), - Entry(desc, "user", "create", "user-create", "--addon", "user-template1 ,HOGE:yyy"), - Entry(desc, "user", "create", "user-create", "--addon", "user-template1,HOGE :yyy"), - Entry(desc, "user", "create", "user-create", "--cluster-addon", "user-clustertemplate1,HOGE :"), - Entry(desc, "user", "create", "user-create", "--auth-type", "xxxx"), - ) - - DescribeTable("❌ fail to create password timeout", - func(args ...string) { - timer := time.AfterFunc(30*time.Second, func() { - testUtil.CreateUserNameSpaceandDefaultPasswordIfAbsent("user-create-timeout") - }) - defer timer.Stop() - run_test(args...) - }, - Entry(desc, "user", "create", "user-create-timeout"), - ) - }) - - //================================================================================== - Describe("[get]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateLoginUser("user1", "name1", nil, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateLoginUser("user2", "name2", []cosmov1alpha1.UserRole{cosmov1alpha1.PrivilegedRole}, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateLoginUser("user3", "name3", []cosmov1alpha1.UserRole{{Name: "myteam-admin"}}, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - run_test(args...) - }, - Entry(desc, "user", "get"), - Entry(desc, "user", "get", "--filter", "role=cosmo-admin"), - Entry(desc, "user", "get", "--filter", "role=*-admin"), - Entry(desc, "user", "get", "--filter", "role=*-admin", "--filter", "role=myteam-*"), - ) - - DescribeTable("✅ success with empty user:", - run_test, - Entry(desc, "user", "get"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "user", "get", "--filter", "x"), - Entry(desc, "user", "get", "--filter", "x=x"), - ) - - DescribeTable("❌ fail with an unexpected error at list:", - func(args ...string) { - clientMock.SetListError("\\.ListUsers$", errors.New("mock user list error")) - run_test(args...) - }, - Entry(desc, "user", "get"), - ) - }) - - //================================================================================== - Describe("[delete]", func() { - - run_test := func(args ...string) { - testUtil.CreateCosmoUser("user-delete1", "delete", nil, cosmov1alpha1.UserAuthTypePasswordSecert) - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - user, _ := k8sClient.GetUser(context.Background(), "user-delete1") - if err == nil { - Expect(user).Should(BeNil()) // deleted - } else { - Expect(user).ShouldNot(BeNil()) // undeleted - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - run_test, - Entry(desc, "user", "delete", "user-delete1"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "user", "delete"), - Entry(desc, "user", "delete", "XXXXX"), - ) - - DescribeTable("❌ fail with an unexpected error at delete:", - func(args ...string) { - clientMock.SetDeleteError("\\.RunE$", errors.New("mock delete user error")) - run_test(args...) - }, - - Entry(desc, "user", "delete", "user-delete1"), - ) - }) - - //================================================================================== - Describe("[reset-password]", func() { - - run_test := func(args ...string) { - testUtil.CreateLoginUser("user1", "name1", nil, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - o := consoleOut() - o = regexp.MustCompile("New password: .*").ReplaceAllString(o, "New password: xxxxxxxx") - Ω(o).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - run_test, - Entry(desc, "user", "reset-password", "user1"), - Entry(desc, "user", "reset-password", "user1", "--password", "XXXXXXXX"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "user", "reset-password", "XXXXXX"), - Entry(desc, "user", "reset-password"), - Entry(desc, "user", "reset-password", "user1", "--password", ""), - ) - - DescribeTable("❌ fail with an unexpected error at update:", - func(args ...string) { - clientMock.SetGetError("\\.GetDefaultPassword$", errors.New("mock get error")) - run_test(args...) - }, - Entry(desc, "user", "reset-password", "user1"), - ) - }) -}) diff --git a/internal/cmd/version/__snapshots__/cmd_test.snap b/internal/cmd/version/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..34d440a1 --- /dev/null +++ b/internal/cmd/version/__snapshots__/cmd_test.snap @@ -0,0 +1,15 @@ +['help should match snapshot 1'] +SnapShot = """ +Print the version number + +Usage: + version [flags] + +Flags: + -h, --help help for version +""" + +['version should match snapshot 1'] +SnapShot = """ +cosmoctl - cosmo-workspace v1.2.3 commit=commitid build=2022-01-01 +""" diff --git a/internal/cmd/version/cmd_test.go b/internal/cmd/version/cmd_test.go new file mode 100644 index 00000000..b21a1887 --- /dev/null +++ b/internal/cmd/version/cmd_test.go @@ -0,0 +1,50 @@ +package version + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandVersion(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl version suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"version", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) + +var _ = Describe("version", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + o := cli.NewRootOptions() + o.Versions = cli.VersionInfo{ + Version: "v1.2.3", + Commit: "commitid", + Date: "2022-01-01", + } + AddCommand(cmd, o) + cmd.SetArgs([]string{"version"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/version/version.go b/internal/cmd/version/version.go index 4be2f9e6..c4664235 100644 --- a/internal/cmd/version/version.go +++ b/internal/cmd/version/version.go @@ -3,18 +3,17 @@ package version import ( "fmt" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/spf13/cobra" ) -const Footprint = `cosmoctl - cosmo v1.0.0-rc5 cosmo-workspace 2023` - -func AddCommand(cmd *cobra.Command, o *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { versionCmd := &cobra.Command{ Use: "version", Short: "Print the version number", Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintln(o.Out, Footprint) + fmt.Fprintf(cmd.OutOrStdout(), "cosmoctl - cosmo-workspace %s commit=%s build=%s\n", + o.Versions.Version, o.Versions.Commit, o.Versions.Date) }, } cmd.AddCommand(versionCmd) diff --git a/internal/cmd/workspace/__snapshots__/cmd_test.snap b/internal/cmd/workspace/__snapshots__/cmd_test.snap new file mode 100644 index 00000000..ee7400ba --- /dev/null +++ b/internal/cmd/workspace/__snapshots__/cmd_test.snap @@ -0,0 +1,30 @@ +['help should match snapshot 1'] +SnapShot = """ + +Manipulate COSMO Workspace resource. + +\"Workspace\" is a namespaced Kubernetes CRD which represents a instance of workspace. + +Usage: + workspace [command] + +Aliases: + workspace, ws + +Available Commands: + create Create workspace + delete Delete workspaces + get Get workspaces + network Get workspace network + remove-network Remove workspace network + resume Resume stopped workspace pod + suspend Suspend workspace pod + templates Get workspace templates in cluster + update Update workspace + upsert-network Upsert workspace network + +Flags: + -h, --help help for workspace + +Use \" workspace [command] --help\" for more information about a command. +""" diff --git a/internal/cmd/workspace/cmd.go b/internal/cmd/workspace/cmd.go index 77ecc584..a64dd4a6 100644 --- a/internal/cmd/workspace/cmd.go +++ b/internal/cmd/workspace/cmd.go @@ -1,56 +1,70 @@ package workspace import ( - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/cli" ) -func AddCommand(cmd *cobra.Command, co *cmdutil.CliOptions) { +func AddCommand(cmd *cobra.Command, o *cli.RootOptions) { workspaceCmd := &cobra.Command{ - Use: "workspace", - Short: "Manipulate Workspace resource", + Use: "workspace", + Short: "Manipulate Workspace resource", + Aliases: []string{"ws"}, Long: ` -Workspace utility command. Manipulate Workspaces like COSMO Dashboard UI. +Manipulate COSMO Workspace resource. -For Workspace detailed status or trouble shooting, -use "kubectl describe workspace" or "kubectl describe instance" and see controller's events. +"Workspace" is a namespaced Kubernetes CRD which represents a instance of workspace. `, - Aliases: []string{"ws"}, } - o := cmdutil.NewUserNamespacedCliOptions(co) - + workspaceCmd.AddCommand(CreateCmd(&cobra.Command{ + Use: "create WORKSPACE_NAME --template TEMPLATE_NAME", + Short: "Create workspace", + }, o)) workspaceCmd.AddCommand(GetCmd(&cobra.Command{ - Use: "get [WORKSPACE_NAME]", - Short: "Get workspaces", - Long: ` -Get workspaces - -This command is like "kubectl get workspace" but show more information. - -But for Workspace detailed status or trouble shooting, -use "kubectl describe workspace" or "kubectl describe instance" and see controller's events. -`, + Use: "get [WORKSPACE_NAME...]", + Short: "Get workspaces", + Aliases: []string{"list"}, }, o)) - workspaceCmd.AddCommand(CreateCmd(&cobra.Command{ - Use: "create WORKSPACE_NAME --template TEMPLATE_NAME", - Short: "Create workspace", - Example: "create my-code-server --user example-user --template code-server --vars PVC_SIZE_Gi:10", + workspaceCmd.AddCommand(GetTemplatesCmd(&cobra.Command{ + Use: "templates [TEMPLATE_NAME...]", + Short: "Get workspace templates in cluster", + Aliases: []string{"template", "tmpls", "tmpl", "get-templates", "get-template", "get-tmpls", "get-tmpl"}, }, o)) workspaceCmd.AddCommand(DeleteCmd(&cobra.Command{ - Use: "delete WORKSPACE_NAME", - Aliases: []string{"del"}, - Short: "Delete workspace", + Use: "delete WORKSPACE_NAME...", + Short: "Delete workspaces", + Aliases: []string{"rm"}, }, o)) - workspaceCmd.AddCommand(RunInstanceCmd(&cobra.Command{ - Use: "run-instance WORKSPACE_NAME", - Aliases: []string{"run"}, - Short: "Run workspace instance", + workspaceCmd.AddCommand(ResumeCmd(&cobra.Command{ + Use: "resume WORKSPACE_NAME", + Short: "Resume stopped workspace pod", + Aliases: []string{"start", "run"}, }, o)) - workspaceCmd.AddCommand(StopInstanceCmd(&cobra.Command{ - Use: "stop-instance WORKSPACE_NAME", + workspaceCmd.AddCommand(SuspendCmd(&cobra.Command{ + Use: "suspend WORKSPACE_NAME", + Short: "Suspend workspace pod", Aliases: []string{"stop"}, - Short: "Stop workspace instance", + }, o)) + workspaceCmd.AddCommand(GetNetworkCmd(&cobra.Command{ + Use: "network WORKSPACE_NAME", + Short: "Get workspace network", + Aliases: []string{"net", "get-network", "get-networks", "get-net"}, + }, o)) + workspaceCmd.AddCommand(UpsertNetworkCmd(&cobra.Command{ + Use: "upsert-network WORKSPACE_NAME --port 8080", + Short: "Upsert workspace network", + Aliases: []string{"add-net"}, + }, o)) + workspaceCmd.AddCommand(RemoveNetworkCmd(&cobra.Command{ + Use: "remove-network WORKSPACE_NAME --port 8080", + Short: "Remove workspace network", + Aliases: []string{"rm-net", "remove-net", "delete-net", "delete-network"}, + }, o)) + workspaceCmd.AddCommand(UpdateCmd(&cobra.Command{ + Use: "update WORKSPACE_NAME", + Short: "Update workspace", }, o)) cmd.AddCommand(workspaceCmd) diff --git a/internal/cmd/workspace/cmd_test.go b/internal/cmd/workspace/cmd_test.go new file mode 100644 index 00000000..80aaab81 --- /dev/null +++ b/internal/cmd/workspace/cmd_test.go @@ -0,0 +1,31 @@ +package workspace + +import ( + "bytes" + "testing" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + . "github.com/cosmo-workspace/cosmo/pkg/snap" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" +) + +func TestCommandWorkspace(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "cosmoctl workspace suite") +} + +var _ = Describe("help", func() { + It("should match snapshot", func() { + cmd := &cobra.Command{} + out := bytes.Buffer{} + cmd.SetOut(&out) + AddCommand(cmd, cli.NewRootOptions()) + cmd.SetArgs([]string{"workspace", "--help"}) + err := cmd.Execute() + Expect(err).ShouldNot(HaveOccurred()) + Expect(out.String()).To(MatchSnapShot()) + }) +}) diff --git a/internal/cmd/workspace/create.go b/internal/cmd/workspace/create.go index 894c1b35..e121a2a8 100644 --- a/internal/cmd/workspace/create.go +++ b/internal/cmd/workspace/create.go @@ -7,121 +7,148 @@ import ( "strings" "time" + "github.com/fatih/color" "github.com/spf13/cobra" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "sigs.k8s.io/yaml" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type CreateOption struct { - *cmdutil.UserNamespacedCliOptions + *cli.RootOptions WorkspaceName string + UserName string Template string - RawVars string - DryRun bool + TemplateVars []string + Force bool vars map[string]string } -func CreateCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &CreateOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - - cmd.Flags().StringVarP(&o.Template, "template", "t", "", "template name") - cmd.Flags().StringVar(&o.RawVars, "vars", "", "template vars. the format is VarName:VarValue. also it can be set multiple vars by conma separated list. (example: VAR1:VAL1,VAR2:VAL2)") - cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "dry run") +func CreateCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &CreateOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().StringVarP(&o.Template, "template", "t", "", "template name (Required)") + cmd.MarkFlagRequired("template") + cmd.Flags().StringSliceVar(&o.TemplateVars, "set", []string{}, "template vars. the format is VarName=VarValue (example: --set VAR1=VAL1 --set VAR2=VAL2)") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") return cmd } -func (o *CreateOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *CreateOption) Validate(cmd *cobra.Command, args []string) error { - if o.AllNamespace { - return errors.New("--all-namespaces is not supported in this command") - } - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { - return err - } if len(args) < 1 { return errors.New("invalid args") } - if o.Template == "" { - return errors.New("--template is required") + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") } return nil } func (o *CreateOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } o.WorkspaceName = args[0] - if o.RawVars != "" { + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + if len(o.TemplateVars) > 0 { vars := make(map[string]string) - varAndVals := strings.Split(o.RawVars, ",") - for _, v := range varAndVals { - varAndVal := strings.Split(v, ":") + for _, v := range o.TemplateVars { + varAndVal := strings.Split(v, "=") if len(varAndVal) != 2 { - return fmt.Errorf("vars format error: vars %s must be 'VAR:VAL'", v) + return fmt.Errorf("vars format error: vars %s must be 'VAR=VAL'", v) } vars[varAndVal[0]] = varAndVal[1] } o.vars = vars } + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *CreateOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) defer cancel() ctx = clog.IntoContext(ctx, o.Logr) - c := o.Client + o.Logr.Info("creating workspace", "user", o.UserName, "name", o.WorkspaceName, "template", o.Template, "vars", o.TemplateVars) - if o.DryRun { - ws, err := c.CreateWorkspace(ctx, o.User, o.WorkspaceName, o.Template, o.vars, client.DryRunAll) - if err != nil { - return err + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } } + } - gvk, err := apiutil.GVKForObject(ws, o.Scheme) - if err != nil { - return err - } - ws.SetGroupVersionKind(gvk) - if out, err := yaml.Marshal(ws); err == nil { - fmt.Fprintln(o.Out, string(out)) - } + var ( + ws *dashv1alpha1.Workspace + err error + ) + if o.UseKubeAPI { + ws, err = o.CreateWorkspaceWithKubeClient(ctx) + } else { + ws, err = o.CreateWorkspaceWithDashClient(ctx) + } + if err != nil { + return err + } - cmdutil.PrintfColorInfo(o.ErrOut, "Successfully created workspace %s (dry-run)\n", o.WorkspaceName) + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully created workspace %s", o.WorkspaceName)) + OutputTable(cmd.OutOrStdout(), []*dashv1alpha1.Workspace{ws}) - } else { - if _, err := c.CreateWorkspace(ctx, o.User, o.WorkspaceName, o.Template, o.vars); err != nil { - return err - } + return nil +} - cmdutil.PrintfColorInfo(o.ErrOut, "Successfully created workspace %s\n", o.WorkspaceName) +func (o *CreateOption) CreateWorkspaceWithDashClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + req := &dashv1alpha1.CreateWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + Template: o.Template, + Vars: o.vars, } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.CreateWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.CreateWorkspace", "res", res) - return nil + return res.Msg.Workspace, nil +} + +func (o *CreateOption) CreateWorkspaceWithKubeClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + c := o.KosmoClient + ws, err := c.CreateWorkspace(ctx, o.UserName, o.WorkspaceName, o.Template, o.vars) + if err != nil { + return nil, err + } + return apiconv.C2D_Workspace(*ws), nil } diff --git a/internal/cmd/workspace/delete.go b/internal/cmd/workspace/delete.go index 2c7641b2..3cbd4166 100644 --- a/internal/cmd/workspace/delete.go +++ b/internal/cmd/workspace/delete.go @@ -4,81 +4,127 @@ import ( "context" "errors" "fmt" + "strings" "time" + "github.com/fatih/color" "github.com/spf13/cobra" - "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type DeleteOption struct { - *cmdutil.UserNamespacedCliOptions + *cli.RootOptions - WorkspaceName string - DryRun bool + WorkspaceNames []string + UserName string + Force bool } -func DeleteCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &DeleteOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - - cmd.Flags().BoolVar(&o.DryRun, "dry-run", false, "dry run") +func DeleteCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &DeleteOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") return cmd } -func (o *DeleteOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *DeleteOption) Validate(cmd *cobra.Command, args []string) error { - if o.AllNamespace { - return errors.New("--all-namespaces is not supported in this command") - } - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } if len(args) < 1 { return errors.New("invalid args") } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } return nil } func (o *DeleteOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } - o.WorkspaceName = args[0] + o.WorkspaceNames = args + + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *DeleteOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) defer cancel() - - if o.DryRun { - if _, err := o.Client.DeleteWorkspace(ctx, o.WorkspaceName, o.User, client.DryRunAll); err != nil { - return err + ctx = clog.IntoContext(ctx, o.Logr) + + o.Logr.Info("deleting workspaces", "workspaces", o.WorkspaceNames) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } } - cmdutil.PrintfColorInfo(o.ErrOut, "Successfully deleted workspace %s (dry-run)\n", o.WorkspaceName) + } - } else { - if _, err := o.Client.DeleteWorkspace(ctx, o.WorkspaceName, o.User); err != nil { - return err + for _, v := range o.WorkspaceNames { + if o.UseKubeAPI { + if err := o.DeleteWorkspaceWithKubeClient(ctx, v); err != nil { + return err + } + } else { + if err := o.DeleteWorkspaceWithDashClient(ctx, v); err != nil { + return err + } } - cmdutil.PrintfColorInfo(o.ErrOut, "Successfully deleted workspace %s\n", o.WorkspaceName) + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully deleted workspace %s", v)) + } + + return nil +} + +func (o *DeleteOption) DeleteWorkspaceWithDashClient(ctx context.Context, workspaceName string) error { + req := &dashv1alpha1.DeleteWorkspaceRequest{ + UserName: o.UserName, + WsName: workspaceName, } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.DeleteWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.DeleteWorkspace", "res", res) return nil } + +func (o *DeleteOption) DeleteWorkspaceWithKubeClient(ctx context.Context, workspaceName string) error { + c := o.KosmoClient + if _, err := c.DeleteWorkspace(ctx, workspaceName, o.UserName); err != nil { + return err + } + return nil +} diff --git a/internal/cmd/workspace/get.go b/internal/cmd/workspace/get.go index f05c451c..f06e6d66 100644 --- a/internal/cmd/workspace/get.go +++ b/internal/cmd/workspace/get.go @@ -3,161 +3,262 @@ package workspace import ( "context" "fmt" - "strconv" + "io" "strings" "time" "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/printers" + "k8s.io/utils/ptr" - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" ) type GetOption struct { - *cmdutil.UserNamespacedCliOptions + *cli.RootOptions - WorkspaceName string + WorkspaceNames []string + Filter []string + UserName string + AllUsers bool + OutputFormat string - showNetwork bool + filters []cli.Filter } -func GetCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &GetOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - cmd.Flags().BoolVarP(&o.AllNamespace, "all-namespaces", "A", false, "all namespaces") - - cmd.Flags().BoolVar(&o.showNetwork, "network", false, "show workspace network") +func GetCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().StringSliceVar(&o.Filter, "filter", nil, "filter option. available columns are ['NAME', 'TEMPLATE', 'PHASE']. available operators are ['==', '!=']. value format is filepath. e.g. '--filter TEMPLATE==dev-*'") + cmd.Flags().StringVarP(&o.OutputFormat, "output", "o", "table", "output format. available values are ['table', 'yaml', 'wide']") + cmd.Flags().BoolVarP(&o.AllUsers, "all-users", "A", false, "get all users workspace") return cmd } -func (o *GetOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - func (o *GetOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { + if err := o.RootOptions.Validate(cmd, args); err != nil { return err } + if !o.AllUsers && (o.UseKubeAPI && o.UserName == "") { + return fmt.Errorf("user name is required") + } + switch o.OutputFormat { + case "table", "yaml", "wide": + default: + return fmt.Errorf("invalid output format: %s", o.OutputFormat) + } return nil } func (o *GetOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { + if err := o.RootOptions.Complete(cmd, args); err != nil { return err } if len(args) > 0 { - o.WorkspaceName = args[0] + o.WorkspaceNames = args } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + if len(o.Filter) > 0 { + f, err := cli.ParseFilters(o.Filter) + if err != nil { + return err + } + o.filters = f + } + for _, f := range o.filters { + o.Logr.Debug().Info("filter", "key", f.Key, "value", f.Value, "op", f.Operator) + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true return nil } func (o *GetOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) defer cancel() ctx = clog.IntoContext(ctx, o.Logr) - c := o.Client - - var wss []cosmov1alpha1.Workspace - - o.Logr.Debug().Info("options", "namespace", o.Namespace, "all-namespaces", o.AllNamespace, "workspaceName", o.WorkspaceName) - - if o.AllNamespace { - users, err := c.ListUsers(ctx) + workspaces := []*dashv1alpha1.Workspace{} + users := []*dashv1alpha1.User{ + { + Name: o.UserName, + }, + } + if o.AllUsers { + u, err := o.ListUsers(ctx) if err != nil { return err } - o.Logr.DebugAll().Info("ListUsers", "users", users) - - for _, user := range users { - ws, err := c.ListWorkspacesByUserName(ctx, user.Name) - if err != nil { - return err - } - o.Logr.DebugAll().Info("ListWorkspacesByUserName", "user", o.User, "wsCount", len(ws), "wsList", ws) - wss = append(wss, ws...) - } - - } else if o.WorkspaceName != "" { - ws, err := c.GetWorkspaceByUserName(ctx, o.WorkspaceName, o.User) + users = u + } + for _, user := range users { + wss, err := o.ListWorkspaces(ctx, user.Name) if err != nil { return err } - wss = []cosmov1alpha1.Workspace{*ws} - o.Logr.DebugAll().Info("GetWorkspaceByUserName", "user", o.User, "ws", ws) + workspaces = append(workspaces, wss...) + } + o.Logr.Debug().Info("Workspaces", "workspaces", workspaces) + + workspaces = o.ApplyFilters(workspaces) + + if o.OutputFormat == "yaml" { + o.OutputYAML(cmd.OutOrStdout(), workspaces) + return nil + } else if o.OutputFormat == "wide" { + OutputWideTable(cmd.OutOrStdout(), workspaces) + return nil } else { - _, err := c.GetUser(ctx, o.User) - if err != nil { - return err - } + OutputTable(cmd.OutOrStdout(), workspaces) + return nil + } +} - wss, err = c.ListWorkspacesByUserName(ctx, o.User) - if err != nil { - return err - } - o.Logr.DebugAll().Info("ListWorkspacesByUserName", "user", o.User, "wsCount", len(wss), "wsList", wss) +func (o *GetOption) ListUsers(ctx context.Context) ([]*dashv1alpha1.User, error) { + if o.UseKubeAPI { + return o.listUsersByKubeClient(ctx) + } else { + return o.listUsersWithDashClient(ctx) } +} - w := printers.GetNewTabWriter(o.Out) - defer w.Flush() +func (o *GetOption) ListWorkspaces(ctx context.Context, userName string) ([]*dashv1alpha1.Workspace, error) { + if o.UseKubeAPI { + return o.listWorkspacesByKubeClient(ctx, userName) + } else { + return o.listWorkspacesWithDashClient(ctx, userName) + } +} - if o.showNetwork { - if o.AllNamespace { - columnNames := []string{"USER", "NAME", "PORT", "URL", "PUBLIC"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) +func (o *GetOption) listUsersWithDashClient(ctx context.Context) ([]*dashv1alpha1.User, error) { + c := o.CosmoDashClient + res, err := c.UserServiceClient.GetUsers(ctx, cli.NewRequestWithToken(&dashv1alpha1.GetUsersRequest{ + WithRaw: ptr.To(o.OutputFormat == "yaml"), + }, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("UserServiceClient.GetUsers", "res", res) + return res.Msg.Items, nil +} - for _, ws := range wss { - for _, v := range ws.Spec.Network { - url := ws.Status.URLs[v.UniqueKey()] - rowdata := []string{cosmov1alpha1.UserNameByNamespace(ws.Namespace), ws.Name, strconv.Itoa(int(v.PortNumber)), url, strconv.FormatBool(v.Public)} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } - } - } else { - columnNames := []string{"INDEX", "NAME", "PORT", "URL", "PUBLIC"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) - - for _, ws := range wss { - for i, v := range ws.Spec.Network { - url := ws.Status.URLs[v.UniqueKey()] - rowdata := []string{fmt.Sprint(i), ws.Name, strconv.Itoa(int(v.PortNumber)), url, strconv.FormatBool(v.Public)} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) +func (o *GetOption) listWorkspacesWithDashClient(ctx context.Context, userName string) ([]*dashv1alpha1.Workspace, error) { + req := &dashv1alpha1.GetWorkspacesRequest{ + UserName: userName, + WithRaw: ptr.To(o.OutputFormat == "yaml"), + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.GetWorkspaces(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.GetWorkspaces", "res", res) + return res.Msg.Items, nil +} + +func (o *GetOption) ApplyFilters(workspaces []*dashv1alpha1.Workspace) []*dashv1alpha1.Workspace { + for _, f := range o.filters { + o.Logr.Debug().Info("applying filter", "key", f.Key, "value", f.Value, "op", f.Operator) + + switch strings.ToUpper(f.Key) { + case "NAME": + workspaces = cli.DoFilter(workspaces, func(u *dashv1alpha1.Workspace) []string { + return []string{u.Name} + }, f) + case "TEMPLATE": + workspaces = cli.DoFilter(workspaces, func(u *dashv1alpha1.Workspace) []string { + return []string{u.Spec.Template} + }, f) + case "PHASE": + workspaces = cli.DoFilter(workspaces, func(u *dashv1alpha1.Workspace) []string { + return []string{u.Status.Phase} + }, f) + default: + o.Logr.Info("WARNING: unknown filter key", "key", f.Key) + } + } + + if len(o.WorkspaceNames) > 0 { + ts := make([]*dashv1alpha1.Workspace, 0, len(o.WorkspaceNames)) + WorkspaceLoop: + // Or loop + for _, t := range workspaces { + for _, selected := range o.WorkspaceNames { + if selected == t.GetName() { + ts = append(ts, t) + continue WorkspaceLoop } } } + workspaces = ts + } + return workspaces +} - } else { - if o.AllNamespace { - columnNames := []string{"USER", "NAME", "TEMPLATE", "PHASE"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) +func (o *GetOption) OutputYAML(w io.Writer, objs []*dashv1alpha1.Workspace) { + docs := make([]string, len(objs)) + for i, t := range objs { + docs[i] = *t.Raw + } + fmt.Fprintln(w, strings.Join(docs, "---\n")) +} - for _, ws := range wss { - rowdata := []string{cosmov1alpha1.UserNameByNamespace(ws.Namespace), ws.Name, ws.Spec.Template.Name, string(ws.Status.Phase)} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } - } else { - columnNames := []string{"NAME", "TEMPLATE", "PHASE"} - fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) +func OutputTable(out io.Writer, workspaces []*dashv1alpha1.Workspace) { + data := [][]string{} - for _, ws := range wss { - rowdata := []string{ws.Name, ws.Spec.Template.Name, string(ws.Status.Phase)} - fmt.Fprintf(w, "%s\n", strings.Join(rowdata, "\t")) - } + for _, v := range workspaces { + data = append(data, []string{v.OwnerName, v.Name, v.Spec.Template, v.Status.Phase, v.Status.MainUrl}) + } + + cli.OutputTable(out, + []string{"USER", "NAME", "TEMPLATE", "PHASE", "MAINURL"}, + data) +} + +func OutputWideTable(out io.Writer, workspaces []*dashv1alpha1.Workspace) { + data := [][]string{} + + for _, v := range workspaces { + vars := make([]string, 0, len(v.Spec.Vars)) + for k, vv := range v.Spec.Vars { + vars = append(vars, fmt.Sprintf("%s=%s", k, vv)) } + data = append(data, []string{v.OwnerName, v.Name, v.Spec.Template, strings.Join(vars, ","), v.Status.Phase, v.Status.MainUrl}) } - return nil + + cli.OutputTable(out, + []string{"USER", "NAME", "TEMPLATE", "VARS", "PHASE", "MAINURL"}, + data) +} + +func (o *GetOption) listWorkspacesByKubeClient(ctx context.Context, userName string) ([]*dashv1alpha1.Workspace, error) { + c := o.KosmoClient + workspaces, err := c.ListWorkspacesByUserName(ctx, userName) + if err != nil { + return nil, err + } + return apiconv.C2D_Workspaces(workspaces, apiconv.WithWorkspaceRaw(ptr.To(o.OutputFormat == "yaml"))), nil +} + +func (o *GetOption) listUsersByKubeClient(ctx context.Context) ([]*dashv1alpha1.User, error) { + c := o.KosmoClient + users, err := c.ListUsers(ctx) + if err != nil { + return nil, err + } + return apiconv.C2D_Users(users, apiconv.WithUserRaw(ptr.To(o.OutputFormat == "yaml"))), nil } diff --git a/internal/cmd/workspace/get_network.go b/internal/cmd/workspace/get_network.go new file mode 100644 index 00000000..92bfb695 --- /dev/null +++ b/internal/cmd/workspace/get_network.go @@ -0,0 +1,126 @@ +package workspace + +import ( + "context" + "fmt" + "io" + "strconv" + "time" + + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type GetNetworkOption struct { + *cli.RootOptions + + WorkspaceName string + UserName string +} + +func GetNetworkCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetNetworkOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + return cmd +} + +func (o *GetNetworkOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *GetNetworkOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.WorkspaceName = args[0] + } else if cli.UseServiceAccount(o.CliConfig) { + o.WorkspaceName = cli.GetCurrentWorkspaceName() + o.Logr.Info("Workspace name is auto detected from hostname", "name", o.WorkspaceName) + } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *GetNetworkOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + workspace *dashv1alpha1.Workspace + err error + ) + if o.UseKubeAPI { + workspace, err = o.GetWorkspaceByKubeClient(ctx) + } else { + workspace, err = o.GetWorkspaceWithDashClient(ctx) + } + if err != nil { + return err + } + o.Logr.Debug().Info("Workspace", "workspace", workspace) + + o.OutputTable(cmd.OutOrStdout(), workspace) + + return nil + +} + +func (o *GetNetworkOption) GetWorkspaceWithDashClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + req := &dashv1alpha1.GetWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.GetWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.GetWorkspace", "res", res) + return res.Msg.Workspace, nil +} + +func (o *GetNetworkOption) OutputTable(w io.Writer, workspace *dashv1alpha1.Workspace) { + data := [][]string{} + + for _, v := range workspace.Spec.Network { + data = append(data, []string{fmt.Sprintf("%d", v.PortNumber), v.CustomHostPrefix, v.HttpPath, strconv.FormatBool(v.Public), v.Url}) + } + + cli.OutputTable(w, + []string{"PORT", "CUSTOM_HOST_PREFIX", "HTTP_PATH", "PUBLIC", "URL"}, + data) +} + +func (o *GetNetworkOption) GetWorkspaceByKubeClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + c := o.KosmoClient + workspace, err := c.GetWorkspaceByUserName(ctx, o.WorkspaceName, o.UserName) + if err != nil { + return nil, err + } + return apiconv.C2D_Workspace(*workspace), nil +} diff --git a/internal/cmd/workspace/get_templates.go b/internal/cmd/workspace/get_templates.go new file mode 100644 index 00000000..a2b90fdf --- /dev/null +++ b/internal/cmd/workspace/get_templates.go @@ -0,0 +1,200 @@ +package workspace + +import ( + "context" + "fmt" + "io" + "strings" + "time" + + "github.com/spf13/cobra" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type GetTemplatesOption struct { + *cli.RootOptions + TemplateNames []string + Filter []string + OutputFormat string + + filters []cli.Filter +} + +func GetTemplatesCmd(cmd *cobra.Command, opt *cli.RootOptions) *cobra.Command { + o := &GetTemplatesOption{RootOptions: opt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringSliceVar(&o.Filter, "filter", nil, "filter option. available columns are ['NAME', 'USERROLE', 'REQUIRED_USERADDON']. available operators are ['==', '!=']. value format is filepath. e.g. '--filter USERROLE==*-dev --filter USERROLE!=team-a'") + cmd.Flags().StringVarP(&o.OutputFormat, "output", "o", "table", "output format. available values are ['table', 'yaml']") + return cmd +} + +func (o *GetTemplatesOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + return nil +} + +func (o *GetTemplatesOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.TemplateNames = args + } + + if len(o.Filter) > 0 { + f, err := cli.ParseFilters(o.Filter) + if err != nil { + return err + } + o.filters = f + } + for _, f := range o.filters { + o.Logr.Debug().Info("filter", "key", f.Key, "value", f.Value, "op", f.Operator) + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *GetTemplatesOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*30) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + tmpls []*dashv1alpha1.Template + err error + ) + if o.UseKubeAPI { + tmpls, err = o.ListWorkspaceTemplatesByKubeClient(ctx, o.OutputFormat == "yaml") + } else { + tmpls, err = o.ListWorkspaceTemplatesWithDashClient(ctx, o.OutputFormat == "yaml") + } + if err != nil { + return err + } + o.Logr.Debug().Info("WorkspaceTemplate templates", "templates", tmpls) + + tmpls = o.ApplyFilters(tmpls) + + if o.OutputFormat == "yaml" { + o.OutputYAML(cmd.OutOrStdout(), tmpls) + return nil + } else { + o.OutputTable(cmd.OutOrStdout(), tmpls) + return nil + } +} + +func (o *GetTemplatesOption) ListWorkspaceTemplatesWithDashClient(ctx context.Context, withRaw bool) ([]*dashv1alpha1.Template, error) { + req := &dashv1alpha1.GetWorkspaceTemplatesRequest{ + UseRoleFilter: ptr.To(false), + WithRaw: &withRaw, + } + c := o.CosmoDashClient + res, err := c.TemplateServiceClient.GetWorkspaceTemplates(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("TemplateServiceClient.GetWorkspaceTemplates", "res", res) + return res.Msg.Items, nil +} + +func (o *GetTemplatesOption) ApplyFilters(tmpls []*dashv1alpha1.Template) []*dashv1alpha1.Template { + for _, f := range o.filters { + o.Logr.Debug().Info("applying filter", "key", f.Key, "value", f.Value, "op", f.Operator) + + switch strings.ToUpper(f.Key) { + case "NAME": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + return []string{u.Name} + }, f) + case "USERROLE", "USERROLES", "REQUIRED_USERROLES": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + arr := make([]string, 0, len(u.Userroles)) + arr = append(arr, u.Userroles...) + return arr + }, f) + case "REQUIRED_USERADDONS": + tmpls = cli.DoFilter(tmpls, func(u *dashv1alpha1.Template) []string { + arr := make([]string, 0, len(u.RequiredUseraddons)) + arr = append(arr, u.RequiredUseraddons...) + return arr + }, f) + default: + o.Logr.Info("WARNING: unknown filter key", "key", f.Key) + } + } + + if len(o.TemplateNames) > 0 { + ts := make([]*dashv1alpha1.Template, 0, len(o.TemplateNames)) + WorkspaceLoop: + // Or loop + for _, t := range tmpls { + for _, selected := range o.TemplateNames { + if selected == t.GetName() { + ts = append(ts, t) + continue WorkspaceLoop + } + } + } + tmpls = ts + } + return tmpls +} + +func (o *GetTemplatesOption) OutputYAML(w io.Writer, tmpls []*dashv1alpha1.Template) { + docs := make([]string, len(tmpls)) + for i, t := range tmpls { + docs[i] = *t.Raw + } + fmt.Fprintln(w, strings.Join(docs, "---\n")) +} + +func (o *GetTemplatesOption) OutputTable(w io.Writer, tmpls []*dashv1alpha1.Template) { + data := [][]string{} + for _, v := range tmpls { + rawRequiredUseraddons := strings.Join(v.RequiredUseraddons, ",") + rawUserroles := strings.Join(v.Userroles, ",") + data = append(data, []string{v.GetName(), requiredVars(v.RequiredVars), rawUserroles, rawRequiredUseraddons}) + } + cli.OutputTable(w, + []string{"NAME", "REQUIRED_VARS(default)", "USERROLE", "REQUIRED_USERADDON"}, + data) +} + +func requiredVars(vs []*dashv1alpha1.TemplateRequiredVars) string { + var s []string + for _, v := range vs { + data := v.VarName + if v.DefaultValue != "" { + data += fmt.Sprintf("(%s)", v.DefaultValue) + } + s = append(s, data) + } + return strings.Join(s, ",") +} + +func (o *GetTemplatesOption) ListWorkspaceTemplatesByKubeClient(ctx context.Context, withRaw bool) ([]*dashv1alpha1.Template, error) { + c := o.KosmoClient + tmpls, err := c.ListWorkspaceTemplates(ctx) + if err != nil { + return nil, err + } + return apiconv.C2D_Templates(tmpls, apiconv.WithTemplateRaw(&withRaw)), nil +} diff --git a/internal/cmd/workspace/remove_network.go b/internal/cmd/workspace/remove_network.go new file mode 100644 index 00000000..191a52f6 --- /dev/null +++ b/internal/cmd/workspace/remove_network.go @@ -0,0 +1,158 @@ +package workspace + +import ( + "context" + "fmt" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type RemoveNetworkOption struct { + *cli.RootOptions + + WorkspaceName string + UserName string + CustomHostPrefix string + PortNumber int32 + HTTPPath string + Public bool + + rule cosmov1alpha1.NetworkRule +} + +func RemoveNetworkCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &RemoveNetworkOption{RootOptions: cliOpt} + + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().Int32Var(&o.PortNumber, "port", 0, "serivce port number (Required)") + cmd.MarkFlagRequired("port") + cmd.Flags().StringVar(&o.CustomHostPrefix, "custom-host-prefix", "", "custom host prefix") + cmd.Flags().StringVar(&o.HTTPPath, "path", "/", "path for Ingress path when using ingress") + + return cmd +} + +func (o *RemoveNetworkOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *RemoveNetworkOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.WorkspaceName = args[0] + } else if cli.UseServiceAccount(o.CliConfig) { + o.WorkspaceName = cli.GetCurrentWorkspaceName() + o.Logr.Info("Workspace name is auto detected from hostname", "name", o.WorkspaceName) + } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + o.rule = cosmov1alpha1.NetworkRule{ + CustomHostPrefix: o.CustomHostPrefix, + PortNumber: o.PortNumber, + HTTPPath: o.HTTPPath, + Public: o.Public, + } + o.rule.Default() + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *RemoveNetworkOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + if o.UseKubeAPI { + err := o.DeleteNetworkRuleByKubeClient(ctx) + if err != nil { + return err + } + } else { + err := o.DeleteNetworkRuleWithDashClient(ctx) + if err != nil { + return err + } + } + + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully removed network rule for workspace '%s'", o.WorkspaceName)) + return nil +} + +func (o *RemoveNetworkOption) DeleteNetworkRuleWithDashClient(ctx context.Context) error { + reqGet := &dashv1alpha1.GetWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + } + c := o.CosmoDashClient + resGet, err := c.WorkspaceServiceClient.GetWorkspace(ctx, cli.NewRequestWithToken(reqGet, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + + rules := apiconv.D2C_NetworkRules(resGet.Msg.Workspace.Spec.Network) + index := cosmov1alpha1.GetNetworkRuleIndex(rules, o.rule) + + if index < 0 || len(resGet.Msg.Workspace.Spec.Network) <= index { + return fmt.Errorf("network rule is not found: %#v", o.rule) + } + + req := &dashv1alpha1.DeleteNetworkRuleRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + Index: int32(index), + } + res, err := c.WorkspaceServiceClient.DeleteNetworkRule(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.DeleteNetworkRule", "res", res) + return nil +} + +func (o *RemoveNetworkOption) DeleteNetworkRuleByKubeClient(ctx context.Context) error { + c := o.KosmoClient + + ws, err := c.GetWorkspaceByUserName(ctx, o.WorkspaceName, o.UserName) + if err != nil { + return fmt.Errorf("failed to get workspace: %v", err) + } + index := cosmov1alpha1.GetNetworkRuleIndex(ws.Spec.Network, o.rule) + + if index < 0 || len(ws.Spec.Network) <= index { + return fmt.Errorf("network rule is not found: %#v", o.rule) + } + + if _, err := c.DeleteNetworkRule(ctx, o.WorkspaceName, o.UserName, index); err != nil { + return err + } + + return nil +} diff --git a/internal/cmd/workspace/resume.go b/internal/cmd/workspace/resume.go new file mode 100644 index 00000000..00b21dcc --- /dev/null +++ b/internal/cmd/workspace/resume.go @@ -0,0 +1,114 @@ +package workspace + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type ResumeOption struct { + *cli.RootOptions + + WorkspaceNames []string + UserName string +} + +func ResumeCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &ResumeOption{RootOptions: cliOpt} + + cmd.RunE = cli.ConnectErrorHandler(o) + + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + + return cmd +} + +func (o *ResumeOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if len(args) < 1 { + return errors.New("invalid args") + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *ResumeOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + o.WorkspaceNames = args + + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *ResumeOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + for _, v := range o.WorkspaceNames { + if o.UseKubeAPI { + if err := o.ResumeWorkspaceWithKubeClient(ctx, v); err != nil { + return err + } + } else { + if err := o.ResumeWorkspaceWithDashClient(ctx, v); err != nil { + return err + } + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully resumed workspace %s", v)) + } + + return nil +} + +func (o *ResumeOption) ResumeWorkspaceWithDashClient(ctx context.Context, workspaceName string) error { + req := &dashv1alpha1.UpdateWorkspaceRequest{ + UserName: o.UserName, + WsName: workspaceName, + Replicas: ptr.To(int64(1)), + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.UpdateWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.UpdateWorkspace", "res", res) + + return nil +} + +func (o *ResumeOption) ResumeWorkspaceWithKubeClient(ctx context.Context, workspaceName string) error { + c := o.KosmoClient + if _, err := c.UpdateWorkspace(ctx, workspaceName, o.UserName, kosmo.UpdateWorkspaceOpts{Replicas: ptr.To(int64(1))}); err != nil { + return err + } + return nil +} diff --git a/internal/cmd/workspace/run_instance.go b/internal/cmd/workspace/run_instance.go deleted file mode 100644 index 51ae90e7..00000000 --- a/internal/cmd/workspace/run_instance.go +++ /dev/null @@ -1,76 +0,0 @@ -package workspace - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/spf13/cobra" - "k8s.io/utils/ptr" - - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" -) - -type RunInstanceOption struct { - *cmdutil.UserNamespacedCliOptions - - InstanceName string -} - -func RunInstanceCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &RunInstanceOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - - return cmd -} - -func (o *RunInstanceOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *RunInstanceOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { - return err - } - if len(args) < 1 { - return errors.New("invalid args") - } - return nil -} - -func (o *RunInstanceOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { - return err - } - o.InstanceName = args[0] - return nil -} - -func (o *RunInstanceOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - ctx = clog.IntoContext(ctx, o.Logr) - - c := o.Client - - if _, err := c.UpdateWorkspace(ctx, o.InstanceName, o.User, kosmo.UpdateWorkspaceOpts{Replicas: ptr.To(int64(1))}); err != nil { - return err - } - - cmdutil.PrintfColorInfo(o.Out, "Successfully run workspace %s\n", o.InstanceName) - return nil -} diff --git a/internal/cmd/workspace/stop_instance.go b/internal/cmd/workspace/stop_instance.go deleted file mode 100644 index efa70da9..00000000 --- a/internal/cmd/workspace/stop_instance.go +++ /dev/null @@ -1,76 +0,0 @@ -package workspace - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/spf13/cobra" - "k8s.io/utils/ptr" - - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" -) - -type StopInstanceOption struct { - *cmdutil.UserNamespacedCliOptions - - InstanceName string -} - -func StopInstanceCmd(cmd *cobra.Command, cliOpt *cmdutil.UserNamespacedCliOptions) *cobra.Command { - o := &StopInstanceOption{UserNamespacedCliOptions: cliOpt} - - cmd.PersistentPreRunE = o.PreRunE - cmd.RunE = cmdutil.RunEHandler(o.RunE) - - cmd.Flags().StringVarP(&o.User, "user", "u", "", "user name") - cmd.Flags().StringVarP(&o.Namespace, "namespace", "n", "", "namespace") - - return cmd -} - -func (o *StopInstanceOption) PreRunE(cmd *cobra.Command, args []string) error { - if err := o.Validate(cmd, args); err != nil { - return fmt.Errorf("validation error: %w", err) - } - if err := o.Complete(cmd, args); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - return nil -} - -func (o *StopInstanceOption) Validate(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Validate(cmd, args); err != nil { - return err - } - if len(args) < 1 { - return errors.New("invalid args") - } - return nil -} - -func (o *StopInstanceOption) Complete(cmd *cobra.Command, args []string) error { - if err := o.UserNamespacedCliOptions.Complete(cmd, args); err != nil { - return err - } - o.InstanceName = args[0] - return nil -} - -func (o *StopInstanceOption) RunE(cmd *cobra.Command, args []string) error { - ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) - defer cancel() - ctx = clog.IntoContext(ctx, o.Logr) - - c := o.Client - - if _, err := c.UpdateWorkspace(ctx, o.InstanceName, o.User, kosmo.UpdateWorkspaceOpts{Replicas: ptr.To(int64(0))}); err != nil { - return err - } - - cmdutil.PrintfColorInfo(o.Out, "Successfully stopped workspace %s\n", o.InstanceName) - return nil -} diff --git a/internal/cmd/workspace/suspend.go b/internal/cmd/workspace/suspend.go new file mode 100644 index 00000000..d87195a8 --- /dev/null +++ b/internal/cmd/workspace/suspend.go @@ -0,0 +1,114 @@ +package workspace + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "k8s.io/utils/ptr" + + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type SuspendOption struct { + *cli.RootOptions + + WorkspaceNames []string + UserName string +} + +func SuspendCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &SuspendOption{RootOptions: cliOpt} + + cmd.RunE = cli.ConnectErrorHandler(o) + + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + + return cmd +} + +func (o *SuspendOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if len(args) < 1 { + return errors.New("invalid args") + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *SuspendOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + o.WorkspaceNames = args + + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *SuspendOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + for _, v := range o.WorkspaceNames { + if o.UseKubeAPI { + if err := o.SuspendWorkspaceWithKubeClient(ctx, v); err != nil { + return err + } + } else { + if err := o.SuspendWorkspaceWithDashClient(ctx, v); err != nil { + return err + } + } + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully suspended workspace %s", v)) + } + + return nil +} + +func (o *SuspendOption) SuspendWorkspaceWithDashClient(ctx context.Context, workspaceName string) error { + req := &dashv1alpha1.UpdateWorkspaceRequest{ + UserName: o.UserName, + WsName: workspaceName, + Replicas: ptr.To(int64(0)), + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.UpdateWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.UpdateWorkspace", "res", res) + + return nil +} + +func (o *SuspendOption) SuspendWorkspaceWithKubeClient(ctx context.Context, workspaceName string) error { + c := o.KosmoClient + if _, err := c.UpdateWorkspace(ctx, workspaceName, o.UserName, kosmo.UpdateWorkspaceOpts{Replicas: ptr.To(int64(0))}); err != nil { + return err + } + return nil +} diff --git a/internal/cmd/workspace/update.go b/internal/cmd/workspace/update.go new file mode 100644 index 00000000..99ad9564 --- /dev/null +++ b/internal/cmd/workspace/update.go @@ -0,0 +1,187 @@ +package workspace + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type UpdateOption struct { + *cli.RootOptions + + WorkspaceName string + UserName string + TemplateVars []string + Force bool + + vars map[string]string +} + +func UpdateCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &UpdateOption{RootOptions: cliOpt} + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().StringSliceVar(&o.TemplateVars, "set", []string{}, "template vars. the format is VarName=VarValue (example: --set VAR1=VAL1 --set VAR2=VAL2)") + cmd.Flags().BoolVar(&o.Force, "force", false, "not ask confirmation") + + return cmd +} + +func (o *UpdateOption) Validate(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("invalid args") + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *UpdateOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + o.WorkspaceName = args[0] + + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + if len(o.TemplateVars) > 0 { + vars := make(map[string]string) + for _, v := range o.TemplateVars { + varAndVal := strings.Split(v, "=") + if len(varAndVal) != 2 { + return fmt.Errorf("vars format error: vars %s must be 'VAR=VAL'", v) + } + vars[varAndVal[0]] = varAndVal[1] + } + o.vars = vars + } + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *UpdateOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + currentWs *dashv1alpha1.Workspace + err error + ) + if o.UseKubeAPI { + currentWs, err = o.GetWorkspaceWithKubeClient(ctx) + } else { + currentWs, err = o.GetWorkspaceWithDashClient(ctx) + } + if err != nil { + return err + } + + o.Logr.Info("updating workspace", "user", o.UserName, "name", o.WorkspaceName, "currentVars", currentWs.Spec.Vars, "newVars", o.vars) + + if !o.Force { + AskLoop: + for { + input, err := cli.AskInput("Confirm? [y/n] ", false) + if err != nil { + return err + } + switch strings.ToLower(input) { + case "y": + break AskLoop + case "n": + fmt.Println("canceled") + return nil + } + } + } + + var ws *dashv1alpha1.Workspace + if o.UseKubeAPI { + ws, err = o.UpdateWorkspaceWithKubeClient(ctx) + } else { + ws, err = o.UpdateWorkspaceWithDashClient(ctx) + } + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully updated workspace %s", o.WorkspaceName)) + OutputTable(cmd.OutOrStdout(), []*dashv1alpha1.Workspace{ws}) + + return nil +} + +func (o *UpdateOption) UpdateWorkspaceWithDashClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + req := &dashv1alpha1.UpdateWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + Vars: o.vars, + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.UpdateWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.UpdateWorkspace", "res", res) + + return res.Msg.Workspace, nil +} + +func (o *UpdateOption) UpdateWorkspaceWithKubeClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + c := o.KosmoClient + ws, err := c.UpdateWorkspace(ctx, o.UserName, o.WorkspaceName, kosmo.UpdateWorkspaceOpts{ + Vars: o.vars, + }) + if err != nil { + return nil, err + } + return apiconv.C2D_Workspace(*ws), nil +} + +func (o *UpdateOption) GetWorkspaceWithDashClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + req := &dashv1alpha1.GetWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + } + c := o.CosmoDashClient + res, err := c.WorkspaceServiceClient.GetWorkspace(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.GetWorkspace", "res", res) + + return res.Msg.Workspace, nil +} + +func (o *UpdateOption) GetWorkspaceWithKubeClient(ctx context.Context) (*dashv1alpha1.Workspace, error) { + c := o.KosmoClient + ws, err := c.GetWorkspace(ctx, o.UserName, o.WorkspaceName) + if err != nil { + return nil, err + } + return apiconv.C2D_Workspace(*ws), nil +} diff --git a/internal/cmd/workspace/upsert_network.go b/internal/cmd/workspace/upsert_network.go new file mode 100644 index 00000000..85336d32 --- /dev/null +++ b/internal/cmd/workspace/upsert_network.go @@ -0,0 +1,166 @@ +package workspace + +import ( + "context" + "fmt" + "io" + "strconv" + "time" + + "github.com/fatih/color" + "github.com/spf13/cobra" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/apiconv" + "github.com/cosmo-workspace/cosmo/pkg/cli" + "github.com/cosmo-workspace/cosmo/pkg/clog" + dashv1alpha1 "github.com/cosmo-workspace/cosmo/proto/gen/dashboard/v1alpha1" +) + +type UpsertNetworkOption struct { + *cli.RootOptions + + WorkspaceName string + UserName string + CustomHostPrefix string + PortNumber int32 + HTTPPath string + Public bool + + rule cosmov1alpha1.NetworkRule +} + +func UpsertNetworkCmd(cmd *cobra.Command, cliOpt *cli.RootOptions) *cobra.Command { + o := &UpsertNetworkOption{RootOptions: cliOpt} + + cmd.RunE = cli.ConnectErrorHandler(o) + cmd.Flags().StringVarP(&o.UserName, "user", "u", "", "user name (defualt: login user)") + cmd.Flags().Int32Var(&o.PortNumber, "port", 0, "serivce port number (Required)") + cmd.MarkFlagRequired("port") + cmd.Flags().StringVar(&o.CustomHostPrefix, "host-prefix", "", "custom host prefix") + cmd.Flags().StringVar(&o.HTTPPath, "path", "/", "path for Ingress path when using ingress") + cmd.Flags().BoolVar(&o.Public, "public", false, "disable authentication for this port") + + return cmd +} + +func (o *UpsertNetworkOption) Validate(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Validate(cmd, args); err != nil { + return err + } + if o.UseKubeAPI && o.UserName == "" { + return fmt.Errorf("user name is required") + } + return nil +} + +func (o *UpsertNetworkOption) Complete(cmd *cobra.Command, args []string) error { + if err := o.RootOptions.Complete(cmd, args); err != nil { + return err + } + if len(args) > 0 { + o.WorkspaceName = args[0] + } else if cli.UseServiceAccount(o.CliConfig) { + o.WorkspaceName = cli.GetCurrentWorkspaceName() + o.Logr.Info("Workspace name is auto detected from hostname", "name", o.WorkspaceName) + } + if !o.UseKubeAPI && o.UserName == "" { + o.UserName = o.CliConfig.User + } + + o.rule = cosmov1alpha1.NetworkRule{ + CustomHostPrefix: o.CustomHostPrefix, + PortNumber: o.PortNumber, + HTTPPath: o.HTTPPath, + Public: o.Public, + } + o.rule.Default() + + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return nil +} + +func (o *UpsertNetworkOption) RunE(cmd *cobra.Command, args []string) error { + if err := o.Validate(cmd, args); err != nil { + return fmt.Errorf("validation error: %w", err) + } + if err := o.Complete(cmd, args); err != nil { + return fmt.Errorf("invalid options: %w", err) + } + + ctx, cancel := context.WithTimeout(o.Ctx, time.Second*10) + defer cancel() + ctx = clog.IntoContext(ctx, o.Logr) + + var ( + rule *dashv1alpha1.NetworkRule + err error + ) + if o.UseKubeAPI { + rule, err = o.UpsertNetworkRuleByKubeClient(ctx) + } else { + rule, err = o.UpsertNetworkRuleWithDashClient(ctx) + } + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), color.GreenString("Successfully upsert network rule for workspace '%s'", o.WorkspaceName)) + + o.OutputTable(cmd.OutOrStdout(), rule) + return nil +} + +func (o *UpsertNetworkOption) UpsertNetworkRuleWithDashClient(ctx context.Context) (*dashv1alpha1.NetworkRule, error) { + reqGet := &dashv1alpha1.GetWorkspaceRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + } + c := o.CosmoDashClient + resGet, err := c.WorkspaceServiceClient.GetWorkspace(ctx, cli.NewRequestWithToken(reqGet, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + + rules := apiconv.D2C_NetworkRules(resGet.Msg.Workspace.Spec.Network) + index := cosmov1alpha1.GetNetworkRuleIndex(rules, o.rule) + + req := &dashv1alpha1.UpsertNetworkRuleRequest{ + WsName: o.WorkspaceName, + UserName: o.UserName, + NetworkRule: apiconv.C2D_NetworkRule(o.rule), + Index: int32(index), + } + res, err := c.WorkspaceServiceClient.UpsertNetworkRule(ctx, cli.NewRequestWithToken(req, o.CliConfig)) + if err != nil { + return nil, fmt.Errorf("failed to connect dashboard server: %w", err) + } + o.Logr.DebugAll().Info("WorkspaceServiceClient.UpsertNetworkRule", "res", res) + return res.Msg.NetworkRule, nil +} + +func (o *UpsertNetworkOption) UpsertNetworkRuleByKubeClient(ctx context.Context) (*dashv1alpha1.NetworkRule, error) { + c := o.KosmoClient + + ws, err := c.GetWorkspaceByUserName(ctx, o.WorkspaceName, o.UserName) + if err != nil { + return nil, fmt.Errorf("failed to get workspace: %v", err) + } + index := cosmov1alpha1.GetNetworkRuleIndex(ws.Spec.Network, o.rule) + + cr, err := c.AddNetworkRule(ctx, o.WorkspaceName, o.UserName, o.rule, index) + if err != nil { + return nil, err + } + return apiconv.C2D_NetworkRule(*cr), nil +} + +func (o *UpsertNetworkOption) OutputTable(w io.Writer, v *dashv1alpha1.NetworkRule) { + data := [][]string{ + {fmt.Sprintf("%d", v.PortNumber), v.CustomHostPrefix, v.HttpPath, strconv.FormatBool(v.Public), v.Url}, + } + cli.OutputTable(w, + []string{"PORT", "CUSTOM_HOST_PREFIX", "HTTP_PATH", "PUBLIC", "URL"}, + data) +} diff --git a/internal/cmd/workspace_test.go b/internal/cmd/workspace_test.go deleted file mode 100644 index 32a5b28b..00000000 --- a/internal/cmd/workspace_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package cmd - -import ( - "bytes" - "context" - "errors" - "io" - "regexp" - "strings" - - . "github.com/cosmo-workspace/cosmo/pkg/snap" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/cmdutil" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" - "github.com/cosmo-workspace/cosmo/pkg/kubeutil" -) - -var _ = Describe("cosmoctl [workspace]", func() { - - var ( - clientMock kubeutil.ClientMock - rootCmd *cobra.Command - options *cmdutil.CliOptions - outBuf *bytes.Buffer - ) - consoleOut := func() string { - out, _ := io.ReadAll(outBuf) - return string(out) - } - - BeforeEach(func() { - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme - - baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) - Expect(err).NotTo(HaveOccurred()) - clientMock = kubeutil.NewClientMock(baseclient) - klient := kosmo.NewClient(&clientMock) - - options = cmdutil.NewCliOptions() - options.Client = &klient - outBuf = bytes.NewBufferString("") - options.Out = outBuf - options.ErrOut = outBuf - options.Scheme = scheme - rootCmd = NewRootCmd(options) - - testUtil.CreateLoginUser("user2", "お名前", nil, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateLoginUser("user1", "アドミン", []cosmov1alpha1.UserRole{cosmov1alpha1.PrivilegedRole}, cosmov1alpha1.UserAuthTypePasswordSecert, "password") - testUtil.CreateTemplate(cosmov1alpha1.TemplateLabelEnumTypeWorkspace, "template1") - By("---------------BeforeEach end----------------") - }) - - AfterEach(func() { - By("---------------AfterEach start---------------") - clientMock.Clear() - testUtil.DeleteWorkspaceAll() - testUtil.DeleteCosmoUserAll() - testUtil.DeleteTemplateAll() - }) - - //================================================================================== - desc := func(args ...string) string { return strings.Join(args, " ") } - - errSnap := func(err error) string { - if err == nil { - return "success" - } else { - return err.Error() - } - } - - workspaceSnap := func(ws *cosmov1alpha1.Workspace) struct{ Name, Namespace, Spec, Status interface{} } { - return struct{ Name, Namespace, Spec, Status interface{} }{ - Name: ws.Name, - Namespace: ws.Namespace, - Spec: ws.Spec, - Status: ws.Status, - } - } - //================================================================================== - Describe("[create]", func() { - - DescribeTable("✅ success in normal context:", - func(args ...string) { - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).ShouldNot(HaveOccurred()) - Expect(consoleOut()).To(MatchSnapShot()) - - wsv1Workspace, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[2], args[4]) - Expect(err).NotTo(HaveOccurred()) // created - Ω(workspaceSnap(wsv1Workspace)).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template", "template1", "--vars", "HOGE:HOGEHOGE"), - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template", "template1"), - ) - - DescribeTable("✅ success with dry-run:", - func(args ...string) { - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).ShouldNot(HaveOccurred()) - o := consoleOut() - o = regexp.MustCompile(`creationTimestamp: .+`).ReplaceAllString(o, "creationTimestamp: xxxxxxxx") - o = regexp.MustCompile(`time: .+`).ReplaceAllString(o, "time: xxxxxxxx") - o = regexp.MustCompile(`uid: .+`).ReplaceAllString(o, "uid: xxxxxxxx") - Expect(o).To(MatchSnapShot()) - - _, err = k8sClient.GetWorkspaceByUserName(context.Background(), args[2], args[4]) - Expect(err).To(HaveOccurred()) // not created - }, - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template", "template1", "--vars", "HOGE:HOGEHOGE", "--dry-run"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).Should(HaveOccurred()) - Expect(consoleOut()).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "create"), - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template", "template1", "--all-namespaces"), - Entry(desc, "workspace", "create", "ws1", "--user", "xxxxx", "--template", "template1"), - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--namespace", "user1", "--template", "template1"), - Entry(desc, "workspace", "create", "ws1", "--namespace", "xxxx", "--template", "template1"), - Entry(desc, "workspace", "create", "--user", "user1", "--template", "template1"), - Entry(desc, "workspace", "create", "ws1", "--user", "--template", "template1"), - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template"), - Entry(desc, "workspace", "create", "ws1", "--user", "xxxxx", "--template", "template1", "--dry-run"), - Entry(desc, "workspace", "create", "ws1", "--user", "user1", "--template", "template1", "--vars", "HOGE"), - ) - }) - - //================================================================================== - Describe("[get]", func() { - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws2", "nw1", 1111, "/", false, -1) - testUtil.UpsertNetworkRule("user1", "ws2", "nw3", 2222, "/", false, -1) - - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).ShouldNot(HaveOccurred()) - o := consoleOut() - o = regexp.MustCompile(`creationTimestamp: .+`).ReplaceAllString(o, "creationTimestamp: xxxxxxxx") - o = regexp.MustCompile(`time: .+`).ReplaceAllString(o, "time: xxxxxxxx") - o = regexp.MustCompile(`uid: .+`).ReplaceAllString(o, "uid: xxxxxxxx") - o = regexp.MustCompile(`resourceVersion: .+`).ReplaceAllString(o, "resourceVersion: xxxxxxxx") - Expect(o).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "get", "--user", "user1"), - Entry(desc, "workspace", "get", "--user", "user1", "ws2"), - Entry(desc, "workspace", "get", "--namespace", "cosmo-user-user1"), - Entry(desc, "workspace", "get", "--namespace", "cosmo-user-user1", "ws2"), - Entry(desc, "workspace", "get", "-A"), - Entry(desc, "workspace", "get", "-A", "--network"), - ) - - DescribeTable("✅ success when workspace is empty:", - func(args ...string) { - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).ShouldNot(HaveOccurred()) - o := consoleOut() - o = regexp.MustCompile(`creationTimestamp: .+`).ReplaceAllString(o, "creationTimestamp: xxxxxxxx") - o = regexp.MustCompile(`time: .+`).ReplaceAllString(o, "time: xxxxxxxx") - o = regexp.MustCompile(`uid: .+`).ReplaceAllString(o, "uid: xxxxxxxx") - o = regexp.MustCompile(`resourceVersion: .+`).ReplaceAllString(o, "resourceVersion: xxxxxxxx") - Expect(o).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "get", "--user", "user1"), - Entry(desc, "workspace", "get", "--namespace", "cosmo-user-user1"), - Entry(desc, "workspace", "get", "--all-namespaces"), - Entry(desc, "workspace", "get", "-A", "--network"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).Should(HaveOccurred()) - Expect(consoleOut()).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "get", "--namespace", "cosmo-user-user1", "--user", "user1"), - Entry(desc, "workspace", "get", "--namespace", "xxx"), - Entry(desc, "workspace", "get", "-A", "--user", "user1"), - Entry(desc, "workspace", "get", "--user", "user1", "xxx"), - Entry(desc, "workspace", "get", "--user", "xxxx"), - ) - - DescribeTable("❌ fail with an unexpected error at list users:", - func(args ...string) { - clientMock.ListMock = func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (mocked bool, err error) { - if clientMock.IsCallingFrom("\\.ListUsers$") { - return true, errors.New("mock listUsers error") - } - return false, nil - } - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).Should(HaveOccurred()) - Expect(consoleOut()).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "get", "-A"), - ) - - DescribeTable("❌ fail with an unexpected error at list workspace:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - clientMock.ListMock = func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) (mocked bool, err error) { - if clientMock.IsCallingFrom("\\.ListWorkspacesByUserName$") { - return true, errors.New("mock listWorkspacesByUserName error") - } - return false, nil - } - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Ω(err).Should(HaveOccurred()) - Expect(consoleOut()).To(MatchSnapShot()) - }, - Entry(desc, "workspace", "get", "-A"), - Entry(desc, "workspace", "get", "--user", "user1"), - ) - }) - - //================================================================================== - Describe("[delete]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws2", "nw1", 1111, "/", false, -1) - testUtil.UpsertNetworkRule("user1", "ws2", "nw3", 2222, "/", false, -1) - - run_test(args...) - - _, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[2], "user1") - Expect(err).To(HaveOccurred()) // deleted - }, - Entry(desc, "workspace", "delete", "ws2", "--user", "user1"), - Entry(desc, "workspace", "delete", "ws2", "--namespace", "cosmo-user-user1"), - ) - - DescribeTable("✅ success with dry-run:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - testUtil.UpsertNetworkRule("user1", "ws2", "nw1", 1111, "/", false, -1) - testUtil.UpsertNetworkRule("user1", "ws2", "nw3", 2222, "/", false, -1) - - run_test(args...) - - _, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[2], "user1") - Expect(err).NotTo(HaveOccurred()) // undeleted - }, - Entry(desc, "workspace", "delete", "ws2", "--dry-run", "--user", "user1"), - Entry(desc, "workspace", "delete", "ws2", "--dry-run", "--namespace", "cosmo-user-user1"), - ) - - DescribeTable("❌ fail with invalid args:", - run_test, - Entry(desc, "workspace", "delete", "ws1", "--user", "user1", "-A"), - Entry(desc, "workspace", "delete", "ws1", "--namespace", "cosmo-user-user1", "--user", "user1"), - Entry(desc, "workspace", "delete", "ws1", "--namespace", "xxxx"), - Entry(desc, "workspace", "delete"), - Entry(desc, "workspace", "delete", "xxxx", "--user", "user1", "-A"), - Entry(desc, "workspace", "delete", "ws1", "--user", "user1", "xxx"), - ) - - DescribeTable("❌ fail with an unexpected error at delete:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - clientMock.DeleteMock = func(ctx context.Context, obj client.Object, opts ...client.DeleteOption) (mocked bool, err error) { - if clientMock.IsCallingFrom("\\.RunE$") { - return true, errors.New("mock delete error") - } - return false, nil - } - run_test(args...) - }, - Entry(desc, "workspace", "delete", "ws1", "--user", "user1"), - Entry(desc, "workspace", "delete", "ws1", "--dry-run", "--user", "user1"), - ) - }) - - Describe("[run-instance]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - if err == nil { - wsv1Workspace, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[2], "user1") - Expect(err).NotTo(HaveOccurred()) - Ω(workspaceSnap(wsv1Workspace)).To(MatchSnapShot()) - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.StopWorkspace("user1", "ws1") - run_test(args...) - }, - Entry(desc, "workspace", "run-instance", "ws1", "--user", "user1"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.StopWorkspace("user1", "ws1") - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - run_test(args...) - }, - Entry(desc, "workspace", "run-instance", "ws1", "--user", "user1", "-A"), - Entry(desc, "workspace", "run-instance", "ws1", "--user", "user1", "--namespace", "cosmo-user-user1"), - Entry(desc, "workspace", "run-instance", "ws1", "--namespace", "xxxxx"), - Entry(desc, "workspace", "run-instance"), - Entry(desc, "workspace", "run-instance", "ws1", "--user", "xxxxx"), - Entry(desc, "workspace", "run-instance", "xxx", "--user", "user1"), - Entry(desc, "workspace", "run-instance", "ws2", "--user", "user1"), - ) - - DescribeTable("❌ fail with an unexpected error at update:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.StopWorkspace("user1", "ws1") - clientMock.SetUpdateError("\\.RunE$", errors.New("mock update error")) - run_test(args...) - }, - Entry(desc, "workspace", "run-instance", "ws1", "--user", "user1"), - ) - }) - - //================================================================================== - Describe("[stop-instance]", func() { - - run_test := func(args ...string) { - By("---------------test start----------------") - rootCmd.SetArgs(args) - err := rootCmd.Execute() - Expect(consoleOut()).To(MatchSnapShot()) - Ω(errSnap(err)).To(MatchSnapShot()) - if err == nil { - wsv1Workspace, err := k8sClient.GetWorkspaceByUserName(context.Background(), args[2], "user1") - Expect(err).NotTo(HaveOccurred()) - Ω(workspaceSnap(wsv1Workspace)).To(MatchSnapShot()) - } - By("---------------test end---------------") - } - - DescribeTable("✅ success in normal context:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - run_test(args...) - }, - Entry(desc, "workspace", "stop-instance", "ws1", "--user", "user1"), - ) - - DescribeTable("❌ fail with invalid args:", - func(args ...string) { - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - testUtil.CreateWorkspace("user1", "ws2", "template1", nil) - testUtil.StopWorkspace("user1", "ws2") - run_test(args...) - }, - Entry(desc, "workspace", "stop-instance", "ws1", "--user", "user1", "-A"), - Entry(desc, "workspace", "stop-instance", "ws1", "--user", "user1", "--namespace", "cosmo-user-user1"), - Entry(desc, "workspace", "stop-instance", "ws1", "--namespace", "xxxxx"), - Entry(desc, "workspace", "stop-instance"), - Entry(desc, "workspace", "stop-instance", "ws1", "--user", "xxxxx"), - Entry(desc, "workspace", "stop-instance", "xxx", "--user", "user1"), - Entry(desc, "workspace", "stop-instance", "ws2", "--user", "user1"), - ) - - DescribeTable("❌ fail with an unexpected error at update:", - func(args ...string) { - clientMock.SetUpdateError("\\.RunE$", errors.New("mock update error")) - testUtil.CreateWorkspace("user1", "ws1", "template1", nil) - run_test(args...) - }, - Entry(desc, "workspace", "stop-instance", "ws1", "--user", "user1"), - ) - }) - -}) diff --git a/pkg/cli/filter.go b/pkg/cli/filter.go new file mode 100644 index 00000000..75f3dc48 --- /dev/null +++ b/pkg/cli/filter.go @@ -0,0 +1,93 @@ +package cli + +import ( + "fmt" + "path/filepath" + "strings" +) + +type Filter struct { + Key string + Value string + Operator string +} + +const ( + OperatorEqual = "==" + OperatorNotEqual = "!=" +) + +func opBool(op string) bool { + if op == OperatorEqual { + return true + } else if op == OperatorNotEqual { + return false + } + panic("unknown operator: " + op) +} + +func ParseFilters(filterExpressions []string) ([]Filter, error) { + filters := make([]Filter, 0, len(filterExpressions)) + for _, e := range filterExpressions { + var f *Filter + if strings.Contains(e, OperatorNotEqual) { + f = parseFilterExpression(e, OperatorNotEqual) + } else if strings.Contains(e, OperatorEqual) { + f = parseFilterExpression(e, OperatorEqual) + } else { + return nil, fmt.Errorf("invalid filter expression: %s", e) + } + if f != nil { + filters = append(filters, *f) + } + } + return filters, nil +} + +func parseFilterExpression(exp, op string) *Filter { + f := Filter{} + s := strings.Split(exp, op) + if len(s) != 2 { + return nil + } + f.Key = s[0] + f.Value = s[1] + f.Operator = op + return &f +} + +func DoFilter[T any](objects []T, objectFilterKeyFunc func(T) []string, f Filter) []T { + filtered := make([]T, 0, len(objects)) + for _, o := range objects { + values := objectFilterKeyFunc(o) + + matched := false + + KeysLoop: + for _, v := range values { + found, err := filepath.Match(f.Value, v) + if err != nil { + continue KeysLoop + } + switch f.Operator { + case OperatorEqual: + if found { + matched = true + break KeysLoop + } + case OperatorNotEqual: + if found { + matched = false + break KeysLoop + } else { + matched = true + } + } + } + + if matched { + filtered = append(filtered, o) + } + } + return filtered +} diff --git a/pkg/cli/filter_test.go b/pkg/cli/filter_test.go new file mode 100644 index 00000000..7169ecd7 --- /dev/null +++ b/pkg/cli/filter_test.go @@ -0,0 +1,270 @@ +package cli + +import ( + "reflect" + "testing" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_opBool(t *testing.T) { + type args struct { + op string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "eq", + args: args{op: OperatorEqual}, + want: true, + }, + { + name: "ne", + args: args{op: OperatorNotEqual}, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := opBool(tt.args.op); got != tt.want { + t.Errorf("opBool() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseFilters(t *testing.T) { + type args struct { + filterExpressions []string + } + tests := []struct { + name string + args args + want []Filter + wantErr bool + }{ + { + name: "eq", + args: args{ + filterExpressions: []string{"key1==value1", "key2==value2", "key3!=value3"}, + }, + want: []Filter{ + { + Key: "key1", + Value: "value1", + Operator: OperatorEqual, + }, + { + Key: "key2", + Value: "value2", + Operator: OperatorEqual, + }, + { + Key: "key3", + Value: "value3", + Operator: OperatorNotEqual, + }, + }, + }, + { + name: "error", + args: args{ + filterExpressions: []string{"key1==value1", "key2==value2", "key3=value3"}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseFilters(tt.args.filterExpressions) + if !reflect.DeepEqual(got, tt.want) || (err != nil) != tt.wantErr { + t.Errorf("ParseFilters() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseFilterExpression(t *testing.T) { + type args struct { + exp string + op string + } + tests := []struct { + name string + args args + want *Filter + }{ + { + name: "eq", + args: args{ + exp: "key==value", + op: OperatorEqual, + }, + want: &Filter{ + Key: "key", + Value: "value", + Operator: OperatorEqual, + }, + }, + { + name: "ne", + args: args{ + exp: "key!=value", + op: OperatorNotEqual, + }, + want: &Filter{ + Key: "key", + Value: "value", + Operator: OperatorNotEqual, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseFilterExpression(tt.args.exp, tt.args.op); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseFilterExpression() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDoFilter(t *testing.T) { + type args struct { + objects []cosmov1alpha1.User + objectFilterKeyFunc func(cosmov1alpha1.User) []string + f Filter + } + tests := []struct { + name string + args args + want []cosmov1alpha1.User + }{ + { + name: "eq", + args: args{ + objects: []cosmov1alpha1.User{ + { + ObjectMeta: metav1.ObjectMeta{Name: "user1"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "user2"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "user3"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + }, + objectFilterKeyFunc: func(u cosmov1alpha1.User) []string { + addons := make([]string, 0, len(u.Spec.Addons)) + for _, a := range u.Spec.Addons { + addons = append(addons, a.Template.Name) + } + return addons + }, + f: Filter{ + Value: "addon2", + Operator: OperatorEqual, + }, + }, + want: []cosmov1alpha1.User{ + { + ObjectMeta: metav1.ObjectMeta{Name: "user1"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "user3"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + }, + }, + { + name: "ne", + args: args{ + objects: []cosmov1alpha1.User{ + { + ObjectMeta: metav1.ObjectMeta{Name: "user1"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "user2"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "user3"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon2"}}, + }, + }, + }, + }, + objectFilterKeyFunc: func(u cosmov1alpha1.User) []string { + addons := make([]string, 0, len(u.Spec.Addons)) + for _, a := range u.Spec.Addons { + addons = append(addons, a.Template.Name) + } + return addons + }, + f: Filter{ + Value: "addon2", + Operator: OperatorNotEqual, + }, + }, + want: []cosmov1alpha1.User{ + { + ObjectMeta: metav1.ObjectMeta{Name: "user2"}, + Spec: cosmov1alpha1.UserSpec{ + Addons: []cosmov1alpha1.UserAddon{ + {Template: cosmov1alpha1.UserAddonTemplateRef{Name: "addon1"}}, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DoFilter(tt.args.objects, tt.args.objectFilterKeyFunc, tt.args.f); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DoFilter() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/cli/output.go b/pkg/cli/output.go new file mode 100644 index 00000000..e6f5513d --- /dev/null +++ b/pkg/cli/output.go @@ -0,0 +1,19 @@ +package cli + +import ( + "fmt" + "io" + "strings" + + "k8s.io/cli-runtime/pkg/printers" +) + +func OutputTable(output io.Writer, headers []string, data [][]string) { + w := printers.GetNewTabWriter(output) + defer w.Flush() + + fmt.Fprintf(w, "%s\n", strings.Join(headers, "\t")) + for _, v := range data { + fmt.Fprintf(w, "%s\n", strings.Join(v, "\t")) + } +} diff --git a/pkg/cli/root_options.go b/pkg/cli/root_options.go new file mode 100644 index 00000000..db76be62 --- /dev/null +++ b/pkg/cli/root_options.go @@ -0,0 +1,289 @@ +package cli + +import ( + "context" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-logr/logr" + "github.com/spf13/cobra" + "go.uber.org/zap/zapcore" + "google.golang.org/protobuf/types/known/emptypb" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" + "github.com/cosmo-workspace/cosmo/pkg/clog" + "github.com/cosmo-workspace/cosmo/pkg/kosmo" +) + +type VersionInfo struct { + Version string + Commit string + Date string +} + +type RootOptions struct { + UseKubeAPI bool + KubeConfigPath string + KubeContext string + DashboardURL string + ConfigPath string + LogLevel int + DisableUseServiceAccount bool + + Versions VersionInfo + Ctx context.Context + Logr *clog.Logger + KosmoClient *kosmo.Client + CosmoDashClient *CosmoDashClient + CliConfig *Config +} + +func NewRootOptions() *RootOptions { + ctx := context.TODO() + return &RootOptions{Ctx: ctx} +} + +const ( + ENV_CONFIG = "COSMOCTL_CONFIG" + ENV_DASHBOARD_URL = "COSMOCTL_DASHBOARD_URL" +) + +func (o *RootOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().BoolVarP(&o.UseKubeAPI, + "kube", "k", false, "use kubernetes API client instead of cosmo dashboard API client") + + cmd.PersistentFlags().StringVar(&o.KubeConfigPath, + "kubeconfig", "", "kubeconfig file path. env:KUBECONFIG (default: $HOME/.kube/config)") + + cmd.PersistentFlags().StringVar(&o.DashboardURL, + "dashboard-url", "", "COSMO Dashboard server endpoint URL. env:COSMOCTL_DASHBOARD_URL") + + cmd.PersistentFlags().StringVar(&o.ConfigPath, + "config", "", "cosmoctl config file path. env:COSMOCTL_CONFIG (default: $HOME/.config/cosmocfg)") + + cmd.PersistentFlags().StringVar(&o.KubeContext, + "context", "", "kube-context (default: current context)") + + cmd.PersistentFlags().IntVarP(&o.LogLevel, + "verbose", "v", 0, "log level. -1:DISABLED, 0:INFO, 1:DEBUG, 2:ALL") +} + +func (o *RootOptions) Validate(cmd *cobra.Command, args []string) error { + return nil +} + +func (o *RootOptions) CompleteWithoutClient(cmd *cobra.Command, args []string) error { + if err := o.buildLogger(); err != nil { + return fmt.Errorf("failed to build logger: %w", err) + } + return nil +} + +func (o *RootOptions) Complete(cmd *cobra.Command, args []string) error { + if err := o.buildLogger(); err != nil { + return fmt.Errorf("failed to build logger: %w", err) + } + if o.UseKubeAPI && o.KosmoClient == nil { + o.Logr.Debug().Info("use kube client") + if err := o.buildKosmoClient(); err != nil { + return fmt.Errorf("failed to kubernetes client: %w", err) + } + } else { + cfgPath, err := o.GetConfigFilePath() + if err != nil { + return fmt.Errorf("failed to get config file path: %w", err) + } + o.Logr.Debug().Info("config file path", "path", cfgPath, "dir", filepath.Dir(cfgPath)) + + cfg, err := NewOrLoadConfigFile(cfgPath) + if err != nil { + return fmt.Errorf("failed to load config file: %w", err) + } + o.CliConfig = cfg + o.Logr.DebugAll().Info("config", "endpoint", cfg.Endpoint, "token", cfg.Token, "user", cfg.User, "useServiceAccount", cfg.UseServiceAccount, "cacert", cfg.CACert) + + if !o.DisableUseServiceAccount && UseServiceAccount(o.CliConfig) { + o.Logr.Debug().Info("use in-cluster cosmo dashboard client") + if err := o.buildInClusterDashClientAndVerify(); err != nil { + return fmt.Errorf("failed to build in-cluster COSMO Dashboard API client: %w", err) + } + } else { + o.Logr.Debug().Info("use cosmo dashboard client") + if err := o.buildDashClient(); err != nil { + return fmt.Errorf("failed to build COSMO Dashboard API client: %w", err) + } + } + } + + return nil +} + +func (o *RootOptions) buildLogger() error { + if o.LogLevel >= 0 { + opt := zap.Options{ + Development: true, + Level: zapcore.Level(-o.LogLevel), + } + o.Logr = clog.NewLogger(zap.New(zap.UseFlagOptions(&opt))) + o.Ctx = clog.IntoContext(o.Ctx, o.Logr) + } else { + o.Logr = clog.NewLogger(logr.Discard()) + } + return nil +} + +func (o *RootOptions) GetConfigFilePath() (string, error) { + if o.ConfigPath != "" { + return o.ConfigPath, nil + } else if envCfg := os.Getenv(ENV_CONFIG); envCfg != "" { + return envCfg, nil + } else { + d, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(d, ".config", "cosmocfg"), nil + } +} + +func (o *RootOptions) GetDashboardURL() string { + if o.DashboardURL != "" { + return o.DashboardURL + } else if envURL := os.Getenv(ENV_DASHBOARD_URL); envURL != "" { + return envURL + } else if o.CliConfig.Endpoint != "" { + return o.CliConfig.Endpoint + } else if UseServiceAccount(o.CliConfig) { + return InClusterDashboardURL + } else { + return "" + } +} + +func (o *RootOptions) buildDashClient() error { + dashURL := o.GetDashboardURL() + if dashURL == "" { + return fmt.Errorf("failed to get dashboard URL. login first or run with --dashboard-url option") + } + o.Logr.Debug().Info("Dashboard URL", "url", dashURL) + + httpClient := http.DefaultClient + if o.CliConfig.CACert != "" { + c, err := InClusterHTTPClient(o.CliConfig.GetCACert()) + if err != nil { + return err + } + httpClient = c + } + + c, err := NewCosmoDashClient(httpClient, dashURL) + if err != nil { + return err + } + o.CosmoDashClient = c + + return nil +} + +func (o *RootOptions) buildInClusterDashClientAndVerify() error { + if o.CliConfig.CACert == "" { + // login first if config is not found + o.Logr.Debug().Info("login first") + if err := ServiceAccountLogin(o.Ctx, o.CliConfig); err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } + return o.buildInClusterDashClient() + + } else { + // verify and re-authenticate if expired + if err := o.buildInClusterDashClient(); err != nil { + return err + } + + o.Logr.Debug().Info("in-cluster pre verify") + _, err := o.CosmoDashClient.AuthServiceClient. + Verify(o.Ctx, NewRequestWithToken(&emptypb.Empty{}, o.CliConfig)) + + if err != nil { + o.Logr.Debug().Info("failed to verify session token. re-authenticate", "err", err) + + if err := ServiceAccountLogin(o.Ctx, o.CliConfig); err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } + o.Logr.Debug().Info("successfully re-authenticated") + } + } + return nil +} + +func (o *RootOptions) buildInClusterDashClient() error { + httpClient, err := InClusterHTTPClient(o.CliConfig.GetCACert()) + if err != nil { + return fmt.Errorf("serviceAccountLogin: failed to create http client: %w", err) + } + + c, err := NewCosmoDashClient(httpClient, InClusterDashboardURL) + if err != nil { + return fmt.Errorf("serviceAccountLogin: failed to parse dashboard url: %w", err) + } + o.CosmoDashClient = c + + return nil +} + +func (o *RootOptions) buildKosmoClient() error { + debug := o.Logr.WithCaller().DebugAll() + + cfgFlg := genericclioptions.NewConfigFlags(true) + debug.Info("kubeconfigs", "kubeConfigPath", o.KubeConfigPath, "kubeContext", o.KubeContext) + + if o.KubeConfigPath != "" { + cfgFlg.KubeConfig = &o.KubeConfigPath + } + if o.KubeContext != "" { + cfgFlg.Context = &o.KubeContext + } + + cfg, err := cfgFlg.ToRESTConfig() + if err != nil { + return err + } + debug.Info("RestConfig", "cfg", cfg) + + scheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme + + baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) + if err != nil { + return err + } + o.KosmoClient = &baseclient + + return nil +} + +func (o *RootOptions) Logger() *clog.Logger { + return o.Logr +} + +// GetCurrentWorkspaceName returns current workspace name. +// If running in Workspace pod, hostname is like `$INSTANCE-deploy-podsufix`(e.g.`ws1-workspace-575db4c9cd-h558m`) +// the first part is workspace name prefixed by cosmo. +func GetCurrentWorkspaceName() string { + hostname := os.Getenv("HOSTNAME") + h := strings.Split(hostname, "-") + if len(h) > 3 && h[0] != "" { + return h[0] + } + return "" +} diff --git a/pkg/cli/terminal.go b/pkg/cli/terminal.go new file mode 100644 index 00000000..f0fb6990 --- /dev/null +++ b/pkg/cli/terminal.go @@ -0,0 +1,49 @@ +package cli + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/mattn/go-isatty" + "golang.org/x/term" +) + +func ReadFromPipedStdin() (string, error) { + if isatty.IsTerminal(os.Stdin.Fd()) { + return "", fmt.Errorf("not terminal") + } + input, err := io.ReadAll(os.Stdin) + if err != nil { + return "", fmt.Errorf("failed to read input from stdin: %w", err) + } + return replaceLast(string(input), "\n", ""), nil +} + +func replaceLast(s, old, new string) string { + i := strings.LastIndex(s, old) + if i == -1 { + return s + } + return s[:i] + new + s[i+len(old):] +} + +func AskInput(prompt string, silent bool) (string, error) { + fmt.Print(prompt) + + var input []byte + var err error + if silent { + input, err = term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + } else { + r := bufio.NewReader(os.Stdin) + input, err = r.ReadBytes('\n') + } + if err != nil { + return "", fmt.Errorf("failed to read input : %w", err) + } + return strings.Trim(string(input), "\n"), nil +} diff --git a/pkg/cmdutil/cmdutil.go b/pkg/cmdutil/cmdutil.go deleted file mode 100644 index 296c7872..00000000 --- a/pkg/cmdutil/cmdutil.go +++ /dev/null @@ -1,129 +0,0 @@ -package cmdutil - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - - "github.com/cosmo-workspace/cosmo/pkg/clog" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/tools/clientcmd/api" - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" -) - -const ( - KustomizationFile = "kustomization.yaml" -) - -func GetKubeConfig(path string) (*api.Config, error) { - if path == "" { - rule := clientcmd.NewDefaultClientConfigLoadingRules() - return rule.Load() - } else { - return clientcmd.LoadFromFile(path) - } -} - -var inclusterNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" - -func GetDefaultNamespace(cfg *api.Config, kubecontext string) string { - if cfg == nil || len(cfg.Contexts) == 0 { - b, _ := ioutil.ReadFile(inclusterNamespaceFile) - if len(b) != 0 { - return string(b) - } - return "" - } - var ctxName string - if kubecontext == "" { - ctxName = cfg.CurrentContext - } else { - ctxName = kubecontext - } - ctx, ok := cfg.Contexts[ctxName] - if !ok { - return "" - } - return ctx.Namespace -} - -func KustomizeBuildCmd() ([]string, error) { - kust, kustErr := exec.LookPath("kustomize") - if kustErr != nil { - kctl, kctlErr := exec.LookPath("kubectl") - if kctlErr != nil { - return nil, fmt.Errorf("kubectl nor kustomize found: kustmizr=%v, kubectl=%v", kustErr, kctlErr) - } - return []string{kctl, "kustomize"}, nil - } - return []string{kust, "build"}, nil -} - -func ExecKustomize(ctx context.Context, dir string, kust *types.Kustomization) ([]byte, error) { - log := clog.FromContext(ctx).WithCaller() - - kustomizeBuildCmd, err := KustomizeBuildCmd() - if err != nil { - return nil, err - } - log.Debug().Info("kustomize cmd", "cmd", kustomizeBuildCmd) - - kustYaml, err := yaml.Marshal(kust) - if err != nil { - return nil, err - } - log.Debug().Info(string(kustYaml), "obj", "kustomization.yaml") - - // create kustomization.yaml - if err := CreateFile(dir, KustomizationFile, kustYaml); err != nil { - return nil, err - } - defer RemoveFile(dir, KustomizationFile) - - // run kustomize build - kustomizeCmd := append(kustomizeBuildCmd, dir) - - out, err := exec.CommandContext(ctx, kustomizeCmd[0], kustomizeCmd[1:]...).CombinedOutput() - if err != nil { - return nil, fmt.Errorf("failed to exec kustomize : %w : %s", err, out) - } - return out, nil -} - -func CreateFile(dir, fname string, data []byte) error { - fullPath, err := filepath.Abs(dir + "/" + fname) - if err != nil { - return fmt.Errorf("invaid file path : %w", err) - } - f, err := os.Create(fullPath) - if err != nil { - return fmt.Errorf("failed to create %s : %w", fname, err) - } - defer f.Close() - - if _, err = f.Write(data); err != nil { - return fmt.Errorf("failed to create %s : %w", fname, err) - } - return nil -} - -func RemoveFile(dir, fname string) error { - fullPath, err := filepath.Abs(dir + "/" + fname) - if err != nil { - return fmt.Errorf("invaid file path : %w", err) - } - return os.Remove(fullPath) -} - -func PrintfColorErr(out io.Writer, msg string, a ...interface{}) { - fmt.Fprintf(out, "\x1b[33m%s\x1b[0m", fmt.Sprintf(msg, a...)) -} - -func PrintfColorInfo(out io.Writer, msg string, a ...interface{}) { - fmt.Fprintf(out, "\x1b[32m%s\x1b[0m", fmt.Sprintf(msg, a...)) -} diff --git a/pkg/cmdutil/cmdutil_test.go b/pkg/cmdutil/cmdutil_test.go deleted file mode 100644 index e156f938..00000000 --- a/pkg/cmdutil/cmdutil_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package cmdutil - -import ( - "os" - "testing" - - "k8s.io/client-go/tools/clientcmd/api" -) - -func TestGetDefaultNamespace(t *testing.T) { - inclusterNamespaceFile = "incluster-namespace-test" - CreateFile(".", inclusterNamespaceFile, []byte("incluster-ns")) - defer RemoveFile(".", inclusterNamespaceFile) - - type args struct { - cfg *api.Config - kubecontext string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "incluster", - args: args{ - cfg: nil, - kubecontext: "default", - }, - want: "incluster-ns", - }, - { - name: "kubeconfig", - args: args{ - cfg: &api.Config{ - Contexts: map[string]*api.Context{ - "foo-cluster": { - Namespace: "cosmo-user-foo", - }, - "bar-cluster": { - Namespace: "bar", - }, - }, - CurrentContext: "bar-cluster", - }, - kubecontext: "foo-cluster", - }, - want: "cosmo-user-foo", - }, - { - name: "kubecontext not found in config", - args: args{ - cfg: &api.Config{ - Contexts: map[string]*api.Context{ - "foo-cluster": { - Namespace: "cosmo-user-foo", - }, - "bar-cluster": { - Namespace: "bar", - }, - }, - CurrentContext: "bar-cluster", - }, - kubecontext: "notfound", - }, - want: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := GetDefaultNamespace(tt.args.cfg, tt.args.kubecontext) - if got != tt.want { - t.Errorf("GetDefaultNamespace() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestPrepareKustomizeBuildCmd(t *testing.T) { - tests := []struct { - name string - want []string - wantErr bool - }{ - { - name: "kustomize", - want: []string{"/usr/local/bin/kustomize", "build"}, - wantErr: false, - }, - { - name: "kubectl", - want: []string{"/usr/bin/kubectl", "kustomize"}, - wantErr: false, - }, - } - t.Logf("KustomizeBuildCmd() PATH = %v", os.Getenv("PATH")) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := KustomizeBuildCmd() - if (err != nil) != tt.wantErr { - t.Logf("KustomizeBuildCmd() kustomize or kubectl is not found: %v", err) - return - } - t.Logf("KustomizeBuildCmd() got = %v", got) - // This test is dependent on the testing environment and goes OK at any time. - // When you test manually, do comment-in the below line and see the results. - // t.Fail() - }) - } -} diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go deleted file mode 100644 index 40d75432..00000000 --- a/pkg/cmdutil/errors.go +++ /dev/null @@ -1,12 +0,0 @@ -package cmdutil - -import ( - "github.com/spf13/cobra" -) - -func RunEHandler(runE func(*cobra.Command, []string) error) func(*cobra.Command, []string) error { - return func(cmd *cobra.Command, args []string) error { - err := runE(cmd, args) - return err - } -} diff --git a/pkg/cmdutil/options.go b/pkg/cmdutil/options.go deleted file mode 100644 index 191f17a4..00000000 --- a/pkg/cmdutil/options.go +++ /dev/null @@ -1,167 +0,0 @@ -package cmdutil - -import ( - "context" - "errors" - "fmt" - "io" - "os" - - "github.com/go-logr/logr" - "github.com/spf13/cobra" - "go.uber.org/zap/zapcore" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/cli-runtime/pkg/genericclioptions" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - cosmov1alpha1 "github.com/cosmo-workspace/cosmo/api/v1alpha1" - "github.com/cosmo-workspace/cosmo/pkg/clog" - "github.com/cosmo-workspace/cosmo/pkg/kosmo" -) - -var ( - scheme = runtime.NewScheme() -) - -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(cosmov1alpha1.AddToScheme(scheme)) - // +kubebuilder:scaffold:scheme -} - -type CliOptions struct { - KubeConfigPath string - KubeContext string - LogLevel int - In io.Reader - Out io.Writer - ErrOut io.Writer - - Ctx context.Context - Logr *clog.Logger - Client *kosmo.Client - Scheme *runtime.Scheme -} - -type NamespacedCliOptions struct { - *CliOptions - Namespace string - AllNamespace bool -} - -type UserNamespacedCliOptions struct { - *NamespacedCliOptions - User string -} - -func NewCliOptions() *CliOptions { - ctx := context.TODO() - return &CliOptions{Ctx: ctx} -} - -func NewNamespacedCliOptions(o *CliOptions) *NamespacedCliOptions { - return &NamespacedCliOptions{CliOptions: o} -} - -func NewUserNamespacedCliOptions(o *CliOptions) *UserNamespacedCliOptions { - return &UserNamespacedCliOptions{NamespacedCliOptions: NewNamespacedCliOptions(o)} -} - -func (o *CliOptions) Validate(cmd *cobra.Command, args []string) error { - return nil -} - -func (o *CliOptions) Complete(cmd *cobra.Command, args []string) error { - if o.LogLevel >= 0 { - opt := zap.Options{ - Development: true, - Level: zapcore.Level(-o.LogLevel), - } - o.Logr = clog.NewLogger(zap.New(zap.UseFlagOptions(&opt))) - o.Ctx = clog.IntoContext(o.Ctx, o.Logr) - } else { - o.Logr = clog.NewLogger(logr.Discard()) - } - debug := o.Logr.WithCaller().DebugAll() - - if o.Client == nil { - cfgFlg := genericclioptions.NewConfigFlags(true) - debug.Info("kubeconfigs", "kubeConfigPath", o.KubeConfigPath, "kubeContext", o.KubeContext) - - if o.KubeConfigPath != "" { - cfgFlg.KubeConfig = &o.KubeConfigPath - } - if o.KubeContext != "" { - cfgFlg.Context = &o.KubeContext - } - - cfg, err := cfgFlg.ToRESTConfig() - if err != nil { - return err - } - debug.Info("RestConfig", "cfg", cfg) - - baseclient, err := kosmo.NewClientByRestConfig(cfg, scheme) - if err != nil { - return err - } - o.Client = &baseclient - o.Scheme = scheme - } - - return nil -} - -func (o *NamespacedCliOptions) Validate(cmd *cobra.Command, args []string) error { - if o.AllNamespace && o.Namespace != "" { - return errors.New("--all-namespaces connot be used with --namespace") - } - return o.CliOptions.Validate(cmd, args) -} - -func (o *NamespacedCliOptions) Complete(cmd *cobra.Command, args []string) error { - if !o.AllNamespace && o.Namespace == "" { - cfg, err := GetKubeConfig(o.KubeConfigPath) - if err != nil && !os.IsNotExist(err) { - return err - } - o.Namespace = GetDefaultNamespace(cfg, o.KubeContext) - if o.Namespace == "" { - return errors.New("failed to get default namespace") - } - } - return o.CliOptions.Complete(cmd, args) -} - -func (o *UserNamespacedCliOptions) Validate(cmd *cobra.Command, args []string) error { - if o.User != "" && o.Namespace != "" { - return errors.New("--user and --namespace connot be used at the same time") - } - if o.AllNamespace && (o.Namespace != "" || o.User != "") { - return errors.New("--all-namespaces connot be used with --namespace or --user") - } - return o.NamespacedCliOptions.Validate(cmd, args) -} - -func (o *UserNamespacedCliOptions) Complete(cmd *cobra.Command, args []string) error { - if !o.AllNamespace { - if o.Namespace == "" && o.User != "" { - o.Namespace = cosmov1alpha1.UserNamespace(o.User) - } - } - if err := o.NamespacedCliOptions.Complete(cmd, args); err != nil { - return err - } - if !o.AllNamespace { - if o.Namespace != "" && o.User == "" { - userName := cosmov1alpha1.UserNameByNamespace(o.Namespace) - if userName == "" { - return fmt.Errorf("namespace %s is not cosmo user's namespace", o.Namespace) - } - o.User = userName - } - } - return nil -} diff --git a/pkg/cmdutil/options_test.go b/pkg/cmdutil/options_test.go deleted file mode 100644 index 4ac1903a..00000000 --- a/pkg/cmdutil/options_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package cmdutil - -import ( - "os" - "path" - "path/filepath" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - v1 "k8s.io/client-go/tools/clientcmd/api/v1" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/yaml" - - "github.com/cosmo-workspace/cosmo/pkg/clog" -) - -const kubeconfigFile = "kubeconfig-test" -const kubeconfigFile2 = "kubeconfig-test2" - -var testEnv *envtest.Environment - -func TestCmdutil(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Cmdutil Suite") -} - -var _ = BeforeSuite(func() { - z := zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(true)) - logf.SetLogger(z) - - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, - ErrorIfCRDPathMissing: true, - } - - envtestCfg, err := testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(envtestCfg).NotTo(BeNil()) - - // envtestClient, err := client.New(envtestCfg, client.Options{Scheme: k8scheme.Scheme}) - // Expect(err).NotTo(HaveOccurred()) - // Expect(envtestClient).NotTo(BeNil()) - - // var sa *corev1.ServiceAccount - // err = envtestClient.Get(context.TODO(), types.NamespacedName{Name: "default", Namespace: "default"}, sa) - // Expect(err).NotTo(HaveOccurred()) - // Expect(sa).NotTo(BeNil()) - - // var secret *corev1.Secret - // err = envtestClient.Get(context.TODO(), types.NamespacedName{Name: sa.Secrets[0].Name, Namespace: "default"}, secret) - // Expect(err).NotTo(HaveOccurred()) - // Expect(secret).NotTo(BeNil()) - - // envtestSAToken = secret.Data["token"] - - envtestClusterData := v1.Cluster{ - Server: envtestCfg.Host, - InsecureSkipTLSVerify: true, - } - - By("creating kubeconfig files") - - cfg := v1.Config{ - Clusters: []v1.NamedCluster{ - { - Name: "envtest", - Cluster: envtestClusterData, - }, - }, - Contexts: []v1.NamedContext{ - { - Name: "foo-cluster", - Context: v1.Context{ - Cluster: "envtest", - Namespace: "cosmo-user-foo", - }, - }, - { - Name: "bar-cluster", - Context: v1.Context{ - Cluster: "envtest", - Namespace: "bar", - }, - }, - }, - CurrentContext: "foo-cluster", - } - b, err := yaml.Marshal(cfg) - Expect(err).ShouldNot(HaveOccurred()) - CreateFile(".", kubeconfigFile, b) - - cfg2 := v1.Config{ - Clusters: []v1.NamedCluster{ - { - Name: "envtest", - Cluster: envtestClusterData, - }, - }, - Contexts: []v1.NamedContext{ - { - Name: "foo-cluster", - Context: v1.Context{ - Cluster: "envtest", - Namespace: "cosmo-user-default", - }, - }, - }, - CurrentContext: "foo-cluster", - } - b2, err := yaml.Marshal(cfg2) - Expect(err).ShouldNot(HaveOccurred()) - CreateFile(".", kubeconfigFile2, b2) - -}) - -var _ = AfterSuite(func() { - By("removing kubeconfig file") - var err error - err = RemoveFile(".", kubeconfigFile) - Expect(err).ShouldNot(HaveOccurred()) - err = RemoveFile(".", kubeconfigFile2) - Expect(err).ShouldNot(HaveOccurred()) - - By("tearing down the test environment") - err = testEnv.Stop() - Expect(err).NotTo(HaveOccurred()) -}) - -var _ = Describe("CliOptions", func() { - Context("when using default kubeconfig", func() { - It("should create client and logger", func() { - os.Setenv("KUBECONFIG", kubeconfigFile) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewCliOptions() - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - }) - }) - - Context("when kubeconfig is specified", func() { - It("should create client and logger with given kubeconfig", func() { - os.Setenv("KUBECONFIG", "notfound") - - logLevel := clog.LEVEL_DEBUG_ALL - kubeconfigFilePath := path.Join(".", kubeconfigFile) - o := NewCliOptions() - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - o.KubeConfigPath = kubeconfigFilePath - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - }) - }) -}) - -var _ = Describe("NamespacedCliOptions", func() { - Context("when namespace is specified", func() { - It("should use given namespace", func() { - os.Setenv("KUBECONFIG", kubeconfigFile) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - o.Namespace = "testtest" - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - - Expect(o.Namespace).Should(Equal("testtest")) - Expect(o.AllNamespace).Should(BeFalse()) - }) - }) - - Context("when all-namespaces is specified", func() { - It("should use all-namespaces", func() { - os.Setenv("KUBECONFIG", kubeconfigFile) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - o.AllNamespace = true - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - - Expect(o.Namespace).Should(BeEmpty()) - Expect(o.AllNamespace).Should(BeTrue()) - }) - }) - - Context("when all-namespaces nor name specified and kubeconfig current context found", func() { - It("should use kubeconfig current context namespace", func() { - os.Setenv("KUBECONFIG", kubeconfigFile2) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - - Expect(o.Namespace).Should(BeEquivalentTo("cosmo-user-default")) - Expect(o.AllNamespace).Should(BeFalse()) - }) - }) - - Context("when all-namespaces nor name specified and given context found in kubeconfig", func() { - It("should use kubeconfig given context namespace", func() { - os.Setenv("KUBECONFIG", kubeconfigFile) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - kubecontext := "bar-cluster" - o.KubeContext = kubecontext - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - - Expect(o.Namespace).Should(BeEquivalentTo("bar")) - Expect(o.AllNamespace).Should(BeFalse()) - }) - }) - - Context("when allnamespaces and namespace are specified", func() { - It("should return error", func() { - os.Setenv("KUBECONFIG", kubeconfigFile) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - o.Namespace = "testtest" - o.AllNamespace = true - - err := o.Validate(nil, []string{}) - Expect(err).Should(HaveOccurred()) - }) - }) -}) - -var _ = Describe("UserNamespacedCliOptions", func() { - Context("when user is specified", func() { - It("should use kubeconfig current context namespace", func() { - os.Setenv("KUBECONFIG", kubeconfigFile2) - - logLevel := clog.LEVEL_DEBUG_ALL - o := NewNamespacedCliOptions(NewCliOptions()) - o.LogLevel = logLevel - o.Out = GinkgoWriter - o.ErrOut = GinkgoWriter - - var err error - err = o.Validate(nil, []string{}) - Expect(err).ShouldNot(HaveOccurred()) - - err = o.Complete(nil, []string{}) - // Expect(err).ShouldNot(HaveOccurred()) // UnexpectedServerResponse - - Expect(o.Logr).ShouldNot(BeNil()) - // Expect(o.Client).ShouldNot(BeNil()) - - Expect(o.Namespace).Should(Equal("cosmo-user-default")) - Expect(o.AllNamespace).Should(BeFalse()) - }) - }) -})