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())
- })
- })
-})