Compare commits

..

43 Commits

Author SHA1 Message Date
github-actions[bot]
825f1aaa22 [Backport release-1.3] Fix: fix token invalid after the server restarted (#3662)
* Fix: fix token invalid after the server restarted

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 13c6f0c5a3)

* fix lint

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 96896d4956)

* Pending test temporary

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 33160fd199)

* Pending test temporary

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit c858b81d86)

Co-authored-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2022-04-14 22:26:57 +08:00
github-actions[bot]
82075427e6 Fix: vela status tree show cluster alias & raw format (#3661)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 4ff0f53c04)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-04-14 19:40:14 +08:00
github-actions[bot]
f89cf673c0 Fix: add label from inner system in CR can prevent sync (#3660)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
(cherry picked from commit dceb642ad6)

Co-authored-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-04-14 19:33:35 +08:00
github-actions[bot]
ce53f6922f [Backport release-1.3] Fix: duplicately list pods in velaQL (#3656)
* Fix: duplicately list pods in velaQL

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 5917141b12)

* Fix: the create time of synced app is empty

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit d404c4b507)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-14 17:46:08 +08:00
github-actions[bot]
b6f70d9a3c Fix: failed to deploy application when no there is no avaiable (#3654)
When there are configs, but not in the project where the appliation
is about to deploy, the sync application will hit an issue. It will
lead to block the deploy of an application.

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 6cac625d53)

Co-authored-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-14 17:26:31 +08:00
Somefive
64d063ccfe [Backport release-1.3] vela status tree & controller flags fix (#3649)
* Feat: vela status --tree (#3609)

* Feat: vela status --tree

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Feat: support show not-deployed clusters

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: add tests

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: add multicluster e2e coverage

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Chore: minor fix

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: cli default switch on feature flags (#3625)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Feat: support alias in cluster (#3630)

* Feat: support alias in cluster

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: add test for cluster alias

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Feat: rework vela up to support specified revision (#3634)

* Feat: rework vela up to support specified revision

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: add legacy compatibility

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Feat: fix test

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: enhance vela status tree print (#3639)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
2022-04-14 16:36:35 +08:00
github-actions[bot]
a98278fb7a [Backport release-1.3] Fix: refine the config sync logic (#3647)
* Fix: refine config management

- Refine the config sync logics

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 97cc021d7a)

* address comments

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 3e0f9c07a8)

Co-authored-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-14 13:05:01 +08:00
wyike
0553d603e6 Chore: cherry-pick 3641 to release 1.3 (#3646)
* Fix: try to fix CVE (#3641)


* use santize

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-14 11:25:02 +08:00
github-actions[bot]
a36e99308f [Backport release-1.3] Fix: clear info when addon version cannot meet require (#3645)
* first

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

version miss match erro for addon

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

add log

(cherry picked from commit 14fda35867)

* add test for this

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

small fix

(cherry picked from commit 1e218b5732)

Co-authored-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-14 10:03:42 +08:00
github-actions[bot]
947bac2d35 Fix: verify password valid (#3643)
Signed-off-by: Zhiyu Wang <zhiyuwang.newbis@gmail.com>
(cherry picked from commit b623976f1e)

Co-authored-by: Zhiyu Wang <zhiyuwang.newbis@gmail.com>
2022-04-13 19:40:05 +08:00
github-actions[bot]
7644cc59cb Feat: refine config creation and provide config list (#3640)
- Make the api of creation a config to be async
- In listing config page, show the status of a config

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 5314e5bf9e)

Co-authored-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-13 13:48:20 +08:00
github-actions[bot]
89a441b8ce [Backport release-1.3] Fix: fix dex login with existed email (#3633)
* Fix: fix dex login with existed email

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 15400df15e)

* add dex connector check

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit a7062e08e1)

* unset users' alias

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 1a818f4b8b)

* fix ut

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit 1b3768ca73)

* fix ut

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit e54fc776b0)

Co-authored-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2022-04-12 16:30:10 +08:00
github-actions[bot]
780572c68f Fix: flags for controller (#3632)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit e5a5916973)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-04-12 16:13:50 +08:00
github-actions[bot]
a13cab65b2 [Backport release-1.3] Feat: support basic auth private helm repo (#3631)
* support auth

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 54c05afb1a)

* add test

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix check diff

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix test

fix

add comments

fix test

(cherry picked from commit a8961ec8cc)

* add tests

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix

add more test

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 4f45a6af8e)

* add more test

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit dee791aa51)

* extract set auth info as a global func

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit f8fb0137e3)

* return bcode

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 057a67d8b9)

Co-authored-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-12 16:10:48 +08:00
Min Kim
e26104adcc bump cluster-gateway to 1.3.2 (#3620)
Signed-off-by: yue9944882 <291271447@qq.com>
2022-04-11 19:49:20 +08:00
github-actions[bot]
26ac584655 [Backport release-1.3] Feat: add api of listing configs for project when creating a target (#3626)
* Feat: add api of listing configs for project

In a project, list configs by its type

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 87aae26f3f)

* address comments

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 830cc79dcf)

* fix ci

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit bf10455f6b)

* add query parameter definition

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 73ff31382b)

* Update pkg/apiserver/rest/webservice/project.go

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit f0b346a1cb)

Co-authored-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-11 19:06:31 +08:00
github-actions[bot]
482976990d Fix: reuse chart values in vela install (#3617)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
(cherry picked from commit 5bf0dd045f)

Co-authored-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-04-11 09:52:03 +08:00
github-actions[bot]
bc4812a12e [Backport release-1.3] Fix: vela logs without specified resource name (#3608)
* Fix: vela logs without specified resource name

Signed-off-by: qiaozp <chivalry.pp@gmail.com>
(cherry picked from commit 43df60cb87)

* add unittest

Signed-off-by: qiaozp <chivalry.pp@gmail.com>
(cherry picked from commit daacb88601)

* reviewable

Signed-off-by: qiaozp <chivalry.pp@gmail.com>
(cherry picked from commit 195585b69f)

Co-authored-by: qiaozp <chivalry.pp@gmail.com>
2022-04-08 17:57:15 +08:00
github-actions[bot]
58c2208e2a add sorting for properties, outputs, writeSecretRefParameters in vela def doc-gen (#3604)
Signed-off-by: Nicola115 <2225992901@qq.com>
(cherry picked from commit f1f5fa563d)

Co-authored-by: Nicola115 <2225992901@qq.com>
2022-04-08 15:32:09 +08:00
github-actions[bot]
f83d88cfb0 [Backport release-1.3] Fix: add terraform aws provider without AWS_SESSION_TOKEN (#3594)
* Fix: add terraform aws provider without AWS_SESSION_TOKEN

Fix #3589 and refine prompts for cli

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
(cherry picked from commit 904a72857b)
2022-04-07 17:03:35 +08:00
Somefive
8af3dec0df Fix: add feature-gates to command-line args (#3591)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
2022-04-06 17:00:44 +08:00
Tianxin Dong
edebcc6c59 Fix: fix refresh code expired bcode (#3582)
Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2022-04-06 14:36:05 +08:00
Jianbo Sun
32382ba6be Chore: add new code flow for kubevela milestone (#3586)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-04-06 13:34:30 +08:00
Zheng Xi Zhou
46ef6f9df4 Fix: add response struct to config deletion api (#3579)
Added an empty struct in the API of config deletion

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-06 13:32:49 +08:00
Jianbo Sun
aea98ff5bf Chore: try fix lint (#3571)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-04-06 10:19:53 +08:00
wyike
c093676575 aglin config secret label (#3576)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix not update label

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix addon cannot update label bug

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

revert
2022-04-01 16:59:15 +08:00
wyike
ed05b4b035 aglin config secret label (#3574)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-01 16:13:12 +08:00
Zheng Xi Zhou
3aa4412a0f Fix: remove config image registry (#3572)
Temporarily removed image registry config

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-01 01:07:43 +08:00
wyike
ef4b9816e1 fix bug (#3569)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix bugs

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix

fix
2022-04-01 01:04:29 +08:00
Tianxin Dong
1c5aab1852 Fix: fix dex config field (#3568)
Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2022-04-01 00:51:17 +08:00
Zheng Xi Zhou
966dbc1c74 Feat: add config management apis (#3562)
* Feat: add config management apis

Added some APIs for config management

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* fix check-diff

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* fix ci issue

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* fix config sync

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* fix static check

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* fix sync

Signed-off-by: Zheng Xi Zhou <zzxwill@gmail.com>

* Fix: sync config bug

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-01 00:15:03 +08:00
qiaozp
4eafb46c87 Chore: bump test k8s to 1.20 (#3567)
Signed-off-by: qiaozp <chivalry.pp@gmail.com>
2022-03-31 22:12:55 +08:00
Tianxin Dong
a97a4d0ed7 Feat: add update dex config in apiserver (#3548)
* Feat: add update dex config in apiserver

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* use get connectors to get dex connectors

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* lint the code

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: seperate dex config from a component to an application

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* Fix: use dex config from secret

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* fix not found

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* fix restart dex

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* fix system info

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>

* fix restart

Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2022-03-31 18:42:18 +08:00
Jianbo Sun
77c02f9eec Chore: add video records in readme for chinese community call (#3565)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-03-31 18:05:22 +08:00
Jianbo Sun
3157efd421 Chore: refine chart readme and notes (#3563)
Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
2022-03-31 17:33:06 +08:00
wyike
8ff93b33e2 Feat: add helm repo list endpoint (#3564)
* add helm repo list

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

* fix commit

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

* fix comments

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

* build swagger

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-03-31 17:18:54 +08:00
qiaozp
c6b9abe4c4 Chore: bump k8s version contraint (#3560)
Signed-off-by: qiaozp <chivalry.pp@gmail.com>
2022-03-31 16:42:14 +08:00
Somefive
150ef6e99e Fix: livediff minor bug (#3558)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
2022-03-31 13:13:55 +08:00
wyike
0ada407fbe optimize the ux of addon (#3557)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix

fix
2022-03-31 10:24:20 +08:00
Somefive
c4af1ba643 Fix: topology use original resource namespace when not specify (#3554)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
2022-03-30 19:52:10 +08:00
qiaozp
de84421487 Feat: add name param in vela logs (#3556)
Signed-off-by: qiaozp <chivalry.pp@gmail.com>
2022-03-30 17:56:59 +08:00
yangs
38a8a7f88a Fix: fix the componentDefinition webservice parameter error (#3553)
Signed-off-by: yangsoon <songyang.song@alibaba-inc.com>

Co-authored-by: yangsoon <songyang.song@alibaba-inc.com>
2022-03-30 15:55:49 +08:00
Xiangbo Ma
b4ddf0e4c3 Feat: a new ComponentDefinition cron-task Signed-off-by: Xiangbo Ma <maxiangboo@cmbchina.com> (#3541)
Signed-off-by: fourierr <maxiangboo@qq.com>
2022-03-30 13:27:21 +08:00
120 changed files with 6676 additions and 733 deletions

View File

@@ -65,7 +65,7 @@ jobs:
- name: Setup Kind Cluster (Worker)
run: |
kind delete cluster --name worker
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
kubectl version
kubectl cluster-info
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
@@ -74,7 +74,7 @@ jobs:
- name: Setup Kind Cluster (Hub)
run: |
kind delete cluster
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
kubectl version
kubectl cluster-info

View File

@@ -60,7 +60,7 @@ jobs:
- name: Setup Kind Cluster (Worker)
run: |
kind delete cluster --name worker
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
kubectl version
kubectl cluster-info
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
@@ -69,7 +69,7 @@ jobs:
- name: Setup Kind Cluster (Hub)
run: |
kind delete cluster
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
kubectl version
kubectl cluster-info
@@ -96,7 +96,7 @@ jobs:
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: /tmp/e2e-profile.out
files: /tmp/e2e-profile.out,/tmp/e2e_multicluster_test.out
flags: e2e-multicluster-test
name: codecov-umbrella

View File

@@ -60,7 +60,7 @@ jobs:
- name: Setup Kind Cluster
run: |
kind delete cluster
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
kubectl version
kubectl cluster-info

View File

@@ -60,7 +60,7 @@ jobs:
- name: Setup Kind Cluster
run: |
kind delete cluster
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
kubectl version
kubectl cluster-info

View File

@@ -71,6 +71,11 @@ jobs:
if: needs.detect-noop.outputs.noop != 'true'
steps:
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- name: Checkout
uses: actions/checkout@v2
with:
@@ -88,7 +93,7 @@ jobs:
# version, but we prefer this action because it leaves 'annotations' (i.e.
# it comments on PRs to point out linter violations).
- name: Lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v3
with:
version: ${{ env.GOLANGCI_VERSION }}

View File

@@ -51,7 +51,8 @@ Full documentation is available on the [KubeVela website](https://kubevela.io/).
- Wechat Group (*Chinese*): Broker wechat to add you into the user group.
<img src="https://static.kubevela.net/images/barnett-wechat.jpg" width="200" />
- Bi-weekly Community Call: [Meeting Notes](https://docs.google.com/document/d/1nqdFEyULekyksFHtFvgvFAYE-0AMHKoS3RMnaKsarjs)
- Bi-weekly Community Call: [Meeting Notes](https://docs.google.com/document/d/1nqdFEyULekyksFHtFvgvFAYE-0AMHKoS3RMnaKsarjs).
- Bi-weekly Chinese Community Call: [Video Records](https://space.bilibili.com/180074935/channel/seriesdetail?sid=1842207).
## Talks and Conferences

View File

@@ -47,7 +47,7 @@ var (
// Workflow meta
var (
WorkflowKind = "Workflow"
WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(PolicyKind)
WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(WorkflowKind)
)
func init() {

View File

@@ -16,7 +16,10 @@ limitations under the License.
package types
import "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
import (
"github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
"github.com/oam-dev/cluster-gateway/pkg/config"
)
const (
// CredentialTypeInternal identifies the virtual cluster from internal kubevela system
@@ -29,3 +32,8 @@ const (
// ClustersArg indicates the argument for specific clusters to install addon
ClustersArg = "clusters"
)
var (
// AnnotationClusterAlias the annotation key for cluster alias
AnnotationClusterAlias = config.MetaApiGroupName + "/cluster-alias"
)

View File

@@ -61,6 +61,22 @@ const (
AnnoIngressControllerHTTPSPort = "ingress.controller/https-port"
// AnnoIngressControllerHTTPPort define ingress controller listen port for http
AnnoIngressControllerHTTPPort = "ingress.controller/http-port"
// LabelConfigType is the label for config type
LabelConfigType = "config.oam.dev/type"
// LabelConfigCatalog is the label for config catalog
LabelConfigCatalog = "config.oam.dev/catalog"
// LabelConfigSubType is the sub-type for a config type
LabelConfigSubType = "config.oam.dev/sub-type"
// LabelConfigProject is the label for config project
LabelConfigProject = "config.oam.dev/project"
// LabelConfigSyncToMultiCluster is the label to decide whether a config will be synchronized to multi-cluster
LabelConfigSyncToMultiCluster = "config.oam.dev/multi-cluster"
// LabelConfigIdentifier is the label for config identifier
LabelConfigIdentifier = "config.oam.dev/identifier"
// LabelConfigDescription is the label for config description
LabelConfigDescription = "config.oam.dev/description"
// AnnotationConfigAlias is the annotation for config alias
AnnotationConfigAlias = "config.oam.dev/alias"
)
const (
@@ -118,3 +134,17 @@ var DefaultFilterAnnots = []string{
oam.AnnotationFilterAnnotationKeys,
oam.AnnotationLastAppliedConfiguration,
}
// ConfigType is the type of config
type ConfigType string
const (
// TerraformProvider is the config type for terraform provider
TerraformProvider = "terraform-provider"
// DexConnector is the config type for dex connector
DexConnector = "config-dex-connector"
// ImageRegistry is the config type for image registry
ImageRegistry = "config-image-registry"
// HelmRepository is the config type for Helm chart repository
HelmRepository = "config-helm-repository"
)

View File

@@ -86,7 +86,7 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
| `multicluster.clusterGateway.replicaCount` | ClusterGateway replica count | `1` |
| `multicluster.clusterGateway.port` | ClusterGateway port | `9443` |
| `multicluster.clusterGateway.image.repository` | ClusterGateway image repository | `oamdev/cluster-gateway` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.0` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.2` |
| `multicluster.clusterGateway.image.pullPolicy` | ClusterGateway image pull policy | `IfNotPresent` |
| `multicluster.clusterGateway.resources.limits.cpu` | ClusterGateway cpu limit | `100m` |
| `multicluster.clusterGateway.resources.limits.memory` | ClusterGateway memory limit | `200Mi` |
@@ -125,18 +125,20 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
## Uninstalling the Chart
## Uninstallation
To uninstall/delete the KubeVela helm release
### Vela CLI
To uninstall KubeVela, you can just run the following command by vela CLI:
```shell
$ helm uninstall -n vela-system kubevela
vela uninstall --force
```
The command removes all the Kubernetes components associated with kubevela and deletes the release.
### Helm CLI
**Notice**: You must disable all the addons before uninstallation, this is a script for convenience.
**Notice**: If you enable fluxcd addon when install the chart by set `enableFluxcdAddon=true` .Uninstall wouldn't disable the fluxcd addon ,and it will be kept in the cluster.Please guarantee there is no application in cluster use this addon and disable it firstly before uninstall the helm chart.
You can use this script to disable all addons.
```shell
#! /bin/sh
addon=$(vela addon list|grep enabled|awk {'print $1'})
@@ -156,6 +158,15 @@ if [ $fluxcd ]; then
fi
```
To uninstall the KubeVela helm release:
```shell
$ helm uninstall -n vela-system kubevela
```
Finally, this command will remove all the Kubernetes resources associated with KubeVela and remove this chart release.

View File

@@ -27,9 +27,5 @@ Welcome to use the KubeVela! Enjoy your shipping application journey!
| . \| |_| || |_) || __/ \ V /| __/| || (_| |
|_|\_\\__,_||_.__/ \___| \_/ \___||_| \__,_|
** Please note before uninstalling **
If you enable fluxcd addon when install the chart by set `enableFluxcdAddon=true` .
Uninstall wouldn't disable the fluxcd addon ,and it will be kept in the cluster.
Please guarantee there is no application in cluster using this addon and disable it firstly before uninstall the helm chart.
And you can find the script of one-short disable all addons from the uninstalling section of https://github.com/oam-dev/kubevela/blob/master/charts/vela-core/README.md.
You can refer to https://kubevela.io for more details.

View File

@@ -1,70 +0,0 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/config-dex-connector.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
custom.definition.oam.dev/alias.config.oam.dev: Dex Connector
definition.oam.dev/description: Config information to authenticate Dex connectors
labels:
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
custom.definition.oam.dev/multi-cluster.config.oam.dev: "false"
custom.definition.oam.dev/type.config.oam.dev: dex-connector
name: config-dex-connector
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: parameter.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "dex-connector"
"config.oam.dev/multi-cluster": "false"
"config.oam.dev/identifier": parameter.name
"config.oam.dev/sub-type": parameter.type
}
}
type: "Opaque"
if parameter.type == "github" {
stringData: parameter.github
}
if parameter.type == "ldap" {
stringData: parameter.ldap
}
}
parameter: {
// +usage=Config type
type: "github" | "ldap"
github?: {
// +usage=GitHub client ID
clientID: string
// +usage=GitHub client secret
clientSecret: string
// +usage=GitHub call back URL
callbackURL: string
}
ldap?: {
host: string
insecureNoSSL: *true | bool
insecureSkipVerify: bool
startTLS: bool
usernamePrompt: string
userSearch: {
baseDN: string
username: string
idAttr: string
emailAttr: string
nameAttr: string
}
}
}
workload:
type: autodetects.core.oam.dev

View File

@@ -0,0 +1,308 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/cron-task.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
definition.oam.dev/description: Describes cron jobs that run code or a script to completion.
name: cron-task
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
output: {
apiVersion: "batch/v1beta1"
kind: "CronJob"
spec: {
schedule: parameter.schedule
concurrencyPolicy: parameter.concurrencyPolicy
suspend: parameter.suspend
successfulJobsHistoryLimit: parameter.successfulJobsHistoryLimit
failedJobsHistoryLimit: parameter.failedJobsHistoryLimit
if parameter.startingDeadlineSeconds != _|_ {
startingDeadlineSeconds: parameter.startingDeadlineSeconds
}
jobTemplate: {
if parameter.labels != _|_ {
metadata: labels: parameter.labels
}
if parameter.annotations != _|_ {
metadata: annotations: parameter.annotations
}
spec: {
parallelism: parameter.count
completions: parameter.count
if parameter.ttlSecondsAfterFinished != _|_ {
ttlSecondsAfterFinished: parameter.ttlSecondsAfterFinished
}
if parameter.activeDeadlineSeconds != _|_ {
activeDeadlineSeconds: parameter.activeDeadlineSeconds
}
backoffLimit: parameter.backoffLimit
template: {
if parameter.labels != _|_ {
metadata: labels: parameter.labels
}
if parameter.annotations != _|_ {
metadata: annotations: parameter.annotations
}
spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["imagePullPolicy"] != _|_ {
imagePullPolicy: parameter.imagePullPolicy
}
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if parameter["cpu"] != _|_ {
resources: {
limits: cpu: parameter.cpu
requests: cpu: parameter.cpu
}
}
if parameter["memory"] != _|_ {
resources: {
limits: memory: parameter.memory
requests: memory: parameter.memory
}
}
if parameter["volumes"] != _|_ {
volumeMounts: [ for v in parameter.volumes {
{
mountPath: v.mountPath
name: v.name
}}]
}
}]
if parameter["volumes"] != _|_ {
volumes: [ for v in parameter.volumes {
{
name: v.name
if v.type == "pvc" {
persistentVolumeClaim: claimName: v.claimName
}
if v.type == "configMap" {
configMap: {
defaultMode: v.defaultMode
name: v.cmName
if v.items != _|_ {
items: v.items
}
}
}
if v.type == "secret" {
secret: {
defaultMode: v.defaultMode
secretName: v.secretName
if v.items != _|_ {
items: v.items
}
}
}
if v.type == "emptyDir" {
emptyDir: medium: v.medium
}
}}]
}
if parameter["imagePullSecrets"] != _|_ {
imagePullSecrets: [ for v in parameter.imagePullSecrets {
name: v
},
]
}
if parameter.hostAliases != _|_ {
hostAliases: [ for v in parameter.hostAliases {
ip: v.ip
hostnames: v.hostnames
},
]
}
}
}
}
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
// +usage=Specify the annotations in the workload
annotations?: [string]: string
// +usage=Specify the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron
schedule: string
// +usage=Specify deadline in seconds for starting the job if it misses scheduled
startingDeadlineSeconds?: int
// +usage=suspend subsequent executions
suspend: *false | bool
// +usage=Specifies how to treat concurrent executions of a Job
concurrencyPolicy: *"Allow" | "Allow" | "Forbid" | "Replace"
// +usage=The number of successful finished jobs to retain
successfulJobsHistoryLimit: *3 | int
// +usage=The number of failed finished jobs to retain
failedJobsHistoryLimit: *1 | int
// +usage=Specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Specify image pull policy for your service
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
// +usage=Specify image pull secrets for your service
imagePullSecrets?: [...string]
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
// +usage=Selects a key of a config map in the pod's namespace
configMapKeyRef: {
// +usage=The name of the config map in the pod's namespace to select from
name: string
// +usage=The key of the config map to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
// +usage=Specifies the attributes of the memory resource required for the container.
memory?: string
// +usage=Declare volumes and volumeMounts
volumes?: [...{
name: string
mountPath: string
// +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir"
type: "pvc" | "configMap" | "secret" | "emptyDir"
if type == "pvc" {
claimName: string
}
if type == "configMap" {
defaultMode: *420 | int
cmName: string
items?: [...{
key: string
path: string
mode: *511 | int
}]
}
if type == "secret" {
defaultMode: *420 | int
secretName: string
items?: [...{
key: string
path: string
mode: *511 | int
}]
}
if type == "emptyDir" {
medium: *"" | "Memory"
}
}]
// +usage=An optional list of hosts and IPs that will be injected into the pod's hosts file
hostAliases?: [...{
ip: string
hostnames: [...string]
}]
// +usage=Limits the lifetime of a Job that has finished
ttlSecondsAfterFinished?: int
// +usage=The duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it
activeDeadlineSeconds?: int
// +usage=The number of retries before marking this job failed
backoffLimit: *6 | int
// +usage=Instructions for assessing whether the container is alive.
livenessProbe?: #HealthProbe
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
exec?: {
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
command: [...string]
}
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
httpGet?: {
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
path: string
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
port: int
httpHeaders?: [...{
name: string
value: string
}]
}
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
tcpSocket?: {
// +usage=The TCP socket within the container that should be probed to assess container health.
port: int
}
// +usage=Number of seconds after the container is started before the first probe is initiated.
initialDelaySeconds: *0 | int
// +usage=How often, in seconds, to execute the probe.
periodSeconds: *10 | int
// +usage=Number of seconds after which the probe times out.
timeoutSeconds: *1 | int
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
successThreshold: *1 | int
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
failureThreshold: *3 | int
}
workload:
definition:
apiVersion: batch/v1beta1
kind: CronJob
type: cronjobs.batch

View File

@@ -453,6 +453,12 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
// +usage=Specify the hostAliases to add
hostAliases: [...{
ip: string
hostnames: [...string]
}]
}
#HealthProbe: {
@@ -494,12 +500,6 @@ spec:
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
failureThreshold: *3 | int
// +usage=Specify the hostAliases to add
hostAliases: [...{
ip: string
hostnames: [...string]
}]
}
status:
customStatus: |-

View File

@@ -104,7 +104,7 @@ multicluster:
port: 9443
image:
repository: oamdev/cluster-gateway
tag: v1.3.0
tag: v1.3.2
pullPolicy: IfNotPresent
resources:
limits:

View File

@@ -105,7 +105,7 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-minimal --
| `multicluster.clusterGateway.replicaCount` | ClusterGateway replica count | `1` |
| `multicluster.clusterGateway.port` | ClusterGateway port | `9443` |
| `multicluster.clusterGateway.image.repository` | ClusterGateway image repository | `oamdev/cluster-gateway` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.0` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.2` |
| `multicluster.clusterGateway.image.pullPolicy` | ClusterGateway image pull policy | `IfNotPresent` |
| `multicluster.clusterGateway.resources.limits.cpu` | ClusterGateway cpu limit | `100m` |
| `multicluster.clusterGateway.resources.limits.memory` | ClusterGateway memory limit | `200Mi` |

View File

@@ -1,70 +0,0 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/config-dex-connector.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
custom.definition.oam.dev/alias.config.oam.dev: Dex Connector
definition.oam.dev/description: Config information to authenticate Dex connectors
labels:
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
custom.definition.oam.dev/multi-cluster.config.oam.dev: "false"
custom.definition.oam.dev/type.config.oam.dev: dex-connector
name: config-dex-connector
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: parameter.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "dex-connector"
"config.oam.dev/multi-cluster": "false"
"config.oam.dev/identifier": parameter.name
"config.oam.dev/sub-type": parameter.type
}
}
type: "Opaque"
if parameter.type == "github" {
stringData: parameter.github
}
if parameter.type == "ldap" {
stringData: parameter.ldap
}
}
parameter: {
// +usage=Config type
type: "github" | "ldap"
github?: {
// +usage=GitHub client ID
clientID: string
// +usage=GitHub client secret
clientSecret: string
// +usage=GitHub call back URL
callbackURL: string
}
ldap?: {
host: string
insecureNoSSL: *true | bool
insecureSkipVerify: bool
startTLS: bool
usernamePrompt: string
userSearch: {
baseDN: string
username: string
idAttr: string
emailAttr: string
nameAttr: string
}
}
}
workload:
type: autodetects.core.oam.dev

View File

@@ -0,0 +1,308 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/cron-task.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
definition.oam.dev/description: Describes cron jobs that run code or a script to completion.
name: cron-task
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
output: {
apiVersion: "batch/v1beta1"
kind: "CronJob"
spec: {
schedule: parameter.schedule
concurrencyPolicy: parameter.concurrencyPolicy
suspend: parameter.suspend
successfulJobsHistoryLimit: parameter.successfulJobsHistoryLimit
failedJobsHistoryLimit: parameter.failedJobsHistoryLimit
if parameter.startingDeadlineSeconds != _|_ {
startingDeadlineSeconds: parameter.startingDeadlineSeconds
}
jobTemplate: {
if parameter.labels != _|_ {
metadata: labels: parameter.labels
}
if parameter.annotations != _|_ {
metadata: annotations: parameter.annotations
}
spec: {
parallelism: parameter.count
completions: parameter.count
if parameter.ttlSecondsAfterFinished != _|_ {
ttlSecondsAfterFinished: parameter.ttlSecondsAfterFinished
}
if parameter.activeDeadlineSeconds != _|_ {
activeDeadlineSeconds: parameter.activeDeadlineSeconds
}
backoffLimit: parameter.backoffLimit
template: {
if parameter.labels != _|_ {
metadata: labels: parameter.labels
}
if parameter.annotations != _|_ {
metadata: annotations: parameter.annotations
}
spec: {
restartPolicy: parameter.restart
containers: [{
name: context.name
image: parameter.image
if parameter["imagePullPolicy"] != _|_ {
imagePullPolicy: parameter.imagePullPolicy
}
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
if parameter["env"] != _|_ {
env: parameter.env
}
if parameter["cpu"] != _|_ {
resources: {
limits: cpu: parameter.cpu
requests: cpu: parameter.cpu
}
}
if parameter["memory"] != _|_ {
resources: {
limits: memory: parameter.memory
requests: memory: parameter.memory
}
}
if parameter["volumes"] != _|_ {
volumeMounts: [ for v in parameter.volumes {
{
mountPath: v.mountPath
name: v.name
}}]
}
}]
if parameter["volumes"] != _|_ {
volumes: [ for v in parameter.volumes {
{
name: v.name
if v.type == "pvc" {
persistentVolumeClaim: claimName: v.claimName
}
if v.type == "configMap" {
configMap: {
defaultMode: v.defaultMode
name: v.cmName
if v.items != _|_ {
items: v.items
}
}
}
if v.type == "secret" {
secret: {
defaultMode: v.defaultMode
secretName: v.secretName
if v.items != _|_ {
items: v.items
}
}
}
if v.type == "emptyDir" {
emptyDir: medium: v.medium
}
}}]
}
if parameter["imagePullSecrets"] != _|_ {
imagePullSecrets: [ for v in parameter.imagePullSecrets {
name: v
},
]
}
if parameter.hostAliases != _|_ {
hostAliases: [ for v in parameter.hostAliases {
ip: v.ip
hostnames: v.hostnames
},
]
}
}
}
}
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
// +usage=Specify the annotations in the workload
annotations?: [string]: string
// +usage=Specify the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron
schedule: string
// +usage=Specify deadline in seconds for starting the job if it misses scheduled
startingDeadlineSeconds?: int
// +usage=suspend subsequent executions
suspend: *false | bool
// +usage=Specifies how to treat concurrent executions of a Job
concurrencyPolicy: *"Allow" | "Allow" | "Forbid" | "Replace"
// +usage=The number of successful finished jobs to retain
successfulJobsHistoryLimit: *3 | int
// +usage=The number of failed finished jobs to retain
failedJobsHistoryLimit: *1 | int
// +usage=Specify number of tasks to run in parallel
// +short=c
count: *1 | int
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Specify image pull policy for your service
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
// +usage=Specify image pull secrets for your service
imagePullSecrets?: [...string]
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
restart: *"Never" | string
// +usage=Commands to run in the container
cmd?: [...string]
// +usage=Define arguments by using environment variables
env?: [...{
// +usage=Environment variable name
name: string
// +usage=The value of the environment variable
value?: string
// +usage=Specifies a source the value of this var should come from
valueFrom?: {
// +usage=Selects a key of a secret in the pod's namespace
secretKeyRef: {
// +usage=The name of the secret in the pod's namespace to select from
name: string
// +usage=The key of the secret to select from. Must be a valid secret key
key: string
}
// +usage=Selects a key of a config map in the pod's namespace
configMapKeyRef: {
// +usage=The name of the config map in the pod's namespace to select from
name: string
// +usage=The key of the config map to select from. Must be a valid secret key
key: string
}
}
}]
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
cpu?: string
// +usage=Specifies the attributes of the memory resource required for the container.
memory?: string
// +usage=Declare volumes and volumeMounts
volumes?: [...{
name: string
mountPath: string
// +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir"
type: "pvc" | "configMap" | "secret" | "emptyDir"
if type == "pvc" {
claimName: string
}
if type == "configMap" {
defaultMode: *420 | int
cmName: string
items?: [...{
key: string
path: string
mode: *511 | int
}]
}
if type == "secret" {
defaultMode: *420 | int
secretName: string
items?: [...{
key: string
path: string
mode: *511 | int
}]
}
if type == "emptyDir" {
medium: *"" | "Memory"
}
}]
// +usage=An optional list of hosts and IPs that will be injected into the pod's hosts file
hostAliases?: [...{
ip: string
hostnames: [...string]
}]
// +usage=Limits the lifetime of a Job that has finished
ttlSecondsAfterFinished?: int
// +usage=The duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it
activeDeadlineSeconds?: int
// +usage=The number of retries before marking this job failed
backoffLimit: *6 | int
// +usage=Instructions for assessing whether the container is alive.
livenessProbe?: #HealthProbe
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
exec?: {
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
command: [...string]
}
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
httpGet?: {
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
path: string
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
port: int
httpHeaders?: [...{
name: string
value: string
}]
}
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
tcpSocket?: {
// +usage=The TCP socket within the container that should be probed to assess container health.
port: int
}
// +usage=Number of seconds after the container is started before the first probe is initiated.
initialDelaySeconds: *0 | int
// +usage=How often, in seconds, to execute the probe.
periodSeconds: *10 | int
// +usage=Number of seconds after which the probe times out.
timeoutSeconds: *1 | int
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
successThreshold: *1 | int
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
failureThreshold: *3 | int
}
workload:
definition:
apiVersion: batch/v1beta1
kind: CronJob
type: cronjobs.batch

View File

@@ -453,6 +453,12 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
// +usage=Specify the hostAliases to add
hostAliases: [...{
ip: string
hostnames: [...string]
}]
}
#HealthProbe: {
@@ -494,12 +500,6 @@ spec:
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
failureThreshold: *3 | int
// +usage=Specify the hostAliases to add
hostAliases: [...{
ip: string
hostnames: [...string]
}]
}
status:
customStatus: |-

View File

@@ -107,7 +107,7 @@ multicluster:
port: 9443
image:
repository: oamdev/cluster-gateway
tag: v1.3.0
tag: v1.3.2
pullPolicy: IfNotPresent
resources:
limits:

View File

@@ -19,7 +19,6 @@ package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"net/http"
@@ -30,6 +29,8 @@ import (
"strings"
"time"
flag "github.com/spf13/pflag"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
"k8s.io/klog/v2/klogr"
ctrl "sigs.k8s.io/controller-runtime"
@@ -147,6 +148,7 @@ func main() {
flag.IntVar(&workflow.MaxWorkflowWaitBackoffTime, "max-workflow-wait-backoff-time", 60, "Set the max workflow wait backoff time, default is 60")
flag.IntVar(&workflow.MaxWorkflowFailedBackoffTime, "max-workflow-failed-backoff-time", 300, "Set the max workflow wait backoff time, default is 300")
flag.IntVar(&custom.MaxWorkflowStepErrorRetryTimes, "max-workflow-step-error-retry-times", 10, "Set the max workflow step error retry times, default is 10")
utilfeature.DefaultMutableFeatureGate.AddFlag(flag.CommandLine)
flag.Parse()
// setup logging

View File

@@ -7,4 +7,5 @@ coverage:
default:
target: 70%
ignore:
- "**/zz_generated.deepcopy.go"
- "**/zz_generated.deepcopy.go"
- "references/"

View File

@@ -6,3 +6,4 @@ This directory contains guides for contributors to the KubeVela project.
* [Developer guide](./developer-guide.md)
* [Triage issues](./triage-issues.md)
* [Code conventions](./coding-conventions.md)
* [Develop Code Flow](./develop-code-flow.pdf)

Binary file not shown.

View File

@@ -5,7 +5,7 @@ This guide helps you get started developing KubeVela.
## Prerequisites
1. Golang version 1.17+
2. Kubernetes version v1.18+ with `~/.kube/config` configured.
2. Kubernetes version v1.20+ with `~/.kube/config` configured.
3. ginkgo 1.14.0+ (just for [E2E test](./developer-guide.md#e2e-test))
4. golangci-lint 1.38.0+, it will install automatically if you run `make`, you can [install it manually](https://golangci-lint.run/usage/install/#local-installation) if the installation is too slow.
5. kubebuilder v3.1.0+ and you need to manually install the dependency tools for unit test.
@@ -177,7 +177,7 @@ To execute the e2e test of the API module, the mongodb service needs to exist lo
# save your config
mv ~/.kube/config ~/.kube/config.save
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
kind get kubeconfig --name worker > /tmp/worker.client.kubeconfig

View File

@@ -3337,6 +3337,284 @@
}
}
},
"/api/v1/config_types": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "list all config types",
"operationId": "listConfigTypes",
"parameters": [
{
"type": "string",
"description": "Fuzzy search based on name and description.",
"name": "query",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/v1.ConfigType"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/config_types/{configType}": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "get a config type",
"operationId": "getConfigType",
"parameters": [
{
"type": "string",
"description": "identifier of the config type",
"name": "configType",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.ConfigType"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
},
"post": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "create or update a config",
"operationId": "createConfig",
"parameters": [
{
"type": "string",
"description": "identifier of the config type",
"name": "configType",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.CreateConfigRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.EmptyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/config_types/{configType}/configs": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "get configs from a config type",
"operationId": "getConfigs",
"parameters": [
{
"type": "string",
"description": "identifier of the config",
"name": "configType",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/*v1.Config"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/config_types/{configType}/configs/{name}": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "get a config from a config type",
"operationId": "getConfig",
"parameters": [
{
"type": "string",
"description": "identifier of the config type",
"name": "configType",
"in": "path",
"required": true
},
{
"type": "string",
"description": "identifier of the config",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/*v1.Config"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
},
"delete": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"config"
],
"summary": "delete a config",
"operationId": "deleteConfig",
"parameters": [
{
"type": "string",
"description": "identifier of the config type",
"name": "configType",
"in": "path",
"required": true
},
{
"type": "string",
"description": "identifier of the config",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/v1.EmptyResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/definitions": {
"get": {
"consumes": [
@@ -3862,6 +4140,55 @@
}
}
},
"/api/v1/projects/{projectName}/configs": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"project"
],
"summary": "get configs which are in a project",
"operationId": "getConfigs",
"parameters": [
{
"type": "string",
"description": "config type",
"name": "configType",
"in": "query"
},
{
"type": "string",
"description": "identifier of the project",
"name": "projectName",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/*v1.Config"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/projects/{projectName}/permissions": {
"get": {
"consumes": [
@@ -4304,6 +4631,49 @@
}
}
},
"/api/v1/repository/chart_repos": {
"get": {
"consumes": [
"application/xml",
"application/json"
],
"produces": [
"application/json",
"application/xml"
],
"tags": [
"repository",
"helm"
],
"summary": "list chart repo",
"operationId": "listRepo",
"parameters": [
{
"type": "string",
"description": "the config project",
"name": "project",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/bcode.Bcode"
}
}
}
}
},
"/api/v1/repository/charts": {
"get": {
"consumes": [
@@ -4326,6 +4696,12 @@
"description": "helm repository url",
"name": "repoUrl",
"in": "query"
},
{
"type": "string",
"description": "secret of the repo",
"name": "secretName",
"in": "query"
}
],
"responses": {
@@ -4369,6 +4745,12 @@
"description": "helm repository url",
"name": "repoUrl",
"in": "query"
},
{
"type": "string",
"description": "secret of the repo",
"name": "secretName",
"in": "query"
}
],
"responses": {
@@ -4409,6 +4791,12 @@
"description": "helm repository url",
"name": "repoUrl",
"in": "query"
},
{
"type": "string",
"description": "secret of the repo",
"name": "secretName",
"in": "query"
}
],
"responses": {
@@ -5241,6 +5629,7 @@
},
"definitions": {
"*v1.ApplicationTriggerBase": {},
"*v1.Config": {},
"*v1.EmptyResponse": {},
"addon.Dependency": {
"properties": {
@@ -5293,6 +5682,22 @@
}
}
},
"addon.GitlabAddonSource": {
"properties": {
"path": {
"type": "string"
},
"repo": {
"type": "string"
},
"token": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"addon.HelmSource": {
"properties": {
"url": {
@@ -6562,34 +6967,6 @@
}
}
},
"model.SystemInfo": {
"required": [
"createTime",
"updateTime",
"installID",
"enableCollection",
"loginType"
],
"properties": {
"createTime": {
"type": "string",
"format": "date-time"
},
"enableCollection": {
"type": "boolean"
},
"installID": {
"type": "string"
},
"loginType": {
"type": "string"
},
"updateTime": {
"type": "string",
"format": "date-time"
}
}
},
"model.WorkflowStepStatus": {
"required": [
"id",
@@ -6978,6 +7355,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},
@@ -7148,12 +7528,12 @@
},
"v1.ApplicationDeployResponse": {
"required": [
"note",
"envName",
"triggerType",
"createTime",
"version",
"note",
"status",
"envName",
"triggerType"
"status"
],
"properties": {
"codeInfo": {
@@ -7687,6 +8067,31 @@
}
}
},
"v1.ConfigType": {
"required": [
"definitions",
"alias",
"name",
"description"
],
"properties": {
"alias": {
"type": "string"
},
"definitions": {
"type": "array",
"items": {
"type": "string"
}
},
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"v1.ConnectCloudClusterRequest": {
"required": [
"accessKeyID",
@@ -7736,6 +8141,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},
@@ -8032,6 +8440,31 @@
}
}
},
"v1.CreateConfigRequest": {
"required": [
"name",
"alias",
"project",
"componentType"
],
"properties": {
"alias": {
"type": "string"
},
"componentType": {
"type": "string"
},
"name": {
"type": "string"
},
"project": {
"type": "string"
},
"properties": {
"type": "string"
}
}
},
"v1.CreateEnvRequest": {
"required": [
"name",
@@ -8243,11 +8676,11 @@
},
"v1.DetailAddonResponse": {
"required": [
"name",
"icon",
"invisible",
"version",
"description",
"invisible",
"name",
"icon",
"schema",
"uiSchema",
"definitions",
@@ -8327,13 +8760,13 @@
},
"v1.DetailApplicationResponse": {
"required": [
"alias",
"icon",
"project",
"description",
"createTime",
"name",
"project",
"alias",
"updateTime",
"icon",
"policies",
"envBindings",
"applicationType",
@@ -8394,19 +8827,19 @@
},
"v1.DetailClusterResponse": {
"required": [
"updateTime",
"kubeConfig",
"name",
"icon",
"reason",
"provider",
"apiServerURL",
"name",
"alias",
"icon",
"dashboardURL",
"createTime",
"alias",
"description",
"labels",
"status",
"updateTime",
"apiServerURL",
"kubeConfig",
"labels",
"kubeConfigSecret",
"resourceInfo"
],
@@ -8465,14 +8898,14 @@
},
"v1.DetailComponentResponse": {
"required": [
"name",
"updateTime",
"appPrimaryKey",
"createTime",
"creator",
"alias",
"name",
"main",
"createTime",
"type",
"main",
"appPrimaryKey",
"definition"
],
"properties": {
@@ -8574,13 +9007,13 @@
},
"v1.DetailPolicyResponse": {
"required": [
"name",
"type",
"description",
"creator",
"properties",
"createTime",
"updateTime",
"name",
"type"
"updateTime"
],
"properties": {
"createTime": {
@@ -8610,17 +9043,17 @@
},
"v1.DetailRevisionResponse": {
"required": [
"envName",
"reason",
"deployUser",
"updateTime",
"status",
"note",
"triggerType",
"workflowName",
"updateTime",
"version",
"reason",
"createTime",
"appPrimaryKey",
"version"
"status",
"deployUser",
"note",
"workflowName",
"envName",
"appPrimaryKey"
],
"properties": {
"appPrimaryKey": {
@@ -8674,10 +9107,10 @@
},
"v1.DetailTargetResponse": {
"required": [
"name",
"createTime",
"project",
"updateTime"
"createTime",
"updateTime",
"name"
],
"properties": {
"alias": {
@@ -8717,11 +9150,11 @@
},
"v1.DetailUserResponse": {
"required": [
"lastLoginTime",
"name",
"email",
"disabled",
"createTime",
"lastLoginTime",
"projects",
"roles"
],
@@ -8762,12 +9195,12 @@
},
"v1.DetailWorkflowRecordResponse": {
"required": [
"name",
"namespace",
"workflowName",
"workflowAlias",
"applicationRevision",
"status",
"name",
"namespace",
"deployTime",
"deployUser",
"note",
@@ -8819,14 +9252,14 @@
},
"v1.DetailWorkflowResponse": {
"required": [
"description",
"enable",
"createTime",
"updateTime",
"name",
"alias",
"enable",
"default",
"envName"
"envName",
"createTime",
"alias",
"description"
],
"properties": {
"alias": {
@@ -9021,8 +9454,8 @@
},
"v1.EnvBindingTarget": {
"required": [
"name",
"alias"
"alias",
"name"
],
"properties": {
"alias": {
@@ -9405,11 +9838,11 @@
},
"v1.LoginUserInfoResponse": {
"required": [
"createTime",
"lastLoginTime",
"name",
"email",
"disabled",
"createTime",
"lastLoginTime",
"projects",
"platformPermissions",
"projectPermissions"
@@ -9717,6 +10150,24 @@
}
}
},
"v1.SystemInfo": {
"required": [
"installID",
"enableCollection",
"loginType"
],
"properties": {
"enableCollection": {
"type": "boolean"
},
"installID": {
"type": "string"
},
"loginType": {
"type": "string"
}
}
},
"v1.SystemInfoRequest": {
"required": [
"enableCollection",
@@ -9728,23 +10179,20 @@
},
"loginType": {
"type": "string"
},
"velaAddress": {
"type": "string"
}
}
},
"v1.SystemInfoResponse": {
"required": [
"createTime",
"updateTime",
"installID",
"enableCollection",
"loginType",
"systemVersion"
],
"properties": {
"createTime": {
"type": "string",
"format": "date-time"
},
"enableCollection": {
"type": "boolean"
},
@@ -9756,10 +10204,6 @@
},
"systemVersion": {
"$ref": "#/definitions/v1.SystemVersion"
},
"updateTime": {
"type": "string",
"format": "date-time"
}
}
},
@@ -9828,6 +10272,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},

View File

@@ -3,6 +3,12 @@ kind: Application
metadata:
name: config-dex-connector-dev
namespace: vela-system
labels:
"app.oam.dev/source-of-truth": "from-inner-system"
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "config-dex-connector"
"config.oam.dev/sub-type": "github"
project: abc
spec:
components:
- name: dev
@@ -12,4 +18,4 @@ spec:
github:
clientID: "aa"
clientSecret: "bb"
callbackURL: "http://localhost:8080/callback"
redirectURI: "http://localhost:8080/callback"

View File

@@ -1,11 +1,16 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: config-image-registry-account-auth-dev
name: image-dev
namespace: vela-system
labels:
"app.oam.dev/source-of-truth": "from-inner-system"
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "config-image-registry"
project: abc
spec:
components:
- name: account-auth
- name: image-dev
type: config-image-registry
properties:
registry: "registry.cn-beijing.aliyuncs.com"

View File

@@ -12,12 +12,11 @@ spec:
type: ref-objects
properties:
objects:
- apiVersion: v1
kind: Secret
name: image-registry-dev
- name: reg-demo
resource: secret
policies:
- type: topology
name: dev
properties:
clusters: ["bj"]
# namespaces: ["ns1"]
namespace: default

View File

@@ -129,7 +129,7 @@ var _ = Describe("Test Kubectl Plugin", func() {
Expect(err).NotTo(HaveOccurred())
Expect(output).Should(ContainSubstring(showTdResult))
})
It("Test show componentDefinition use Helm Charts as Workload", func() {
PIt("Test show componentDefinition use Helm Charts as Workload", func() {
Eventually(func() string {
cdName := "test-webapp-chart"
output, _ := e2e.Exec(fmt.Sprintf("kubectl-vela show %s -n default", cdName))

7
go.mod
View File

@@ -65,7 +65,8 @@ require (
go.uber.org/zap v1.18.1
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/tools v0.1.6 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
@@ -145,6 +146,7 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/emicklei/proto v1.6.15 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
@@ -250,8 +252,7 @@ require (
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect

13
go.sum
View File

@@ -420,8 +420,9 @@ github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -2046,13 +2047,14 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2063,8 +2065,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@@ -1070,7 +1070,7 @@ func (h *Installer) enableAddon(addon *InstallPackage) error {
h.addon = addon
err = checkAddonVersionMeetRequired(h.ctx, addon.SystemRequirements, h.cli, h.dc)
if err != nil {
return ErrVersionMismatch
return VersionUnMatchError{addonName: addon.Name, err: err}
}
if err = h.installDependency(addon); err != nil {
@@ -1188,6 +1188,8 @@ func (h *Installer) createOrUpdate(app *v1beta1.Application) error {
return err
}
getapp.Spec = app.Spec
getapp.Labels = app.Labels
getapp.Annotations = app.Annotations
err = h.cli.Update(h.ctx, &getapp)
if err != nil {
klog.Errorf("fail to create application: %v", err)
@@ -1342,7 +1344,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
return err
}
if !res {
return fmt.Errorf("vela cli/ux version: %s cannot meet requirement", version2.VelaVersion)
return fmt.Errorf("vela cli/ux version: %s require: %s", version2.VelaVersion, require.VelaVersion)
}
}
@@ -1359,7 +1361,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
return err
}
if !res {
return fmt.Errorf("the vela core controller: %s cannot meet requirement ", imageVersion)
return fmt.Errorf("the vela core controller: %s require: %s", imageVersion, require.VelaVersion)
}
}
@@ -1380,7 +1382,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
}
if !res {
return fmt.Errorf("the kubernetes version %s cannot meet requirement", k8sVersion.GitVersion)
return fmt.Errorf("the kubernetes version %s require: %s", k8sVersion.GitVersion, require.KubernetesVersion)
}
}

View File

@@ -323,6 +323,45 @@ var _ = Describe("Test render addon with specified clusters", func() {
})
})
var _ = Describe("func addon update ", func() {
It("test update addon app label", func() {
app_test_update := v1beta1.Application{}
Expect(yaml.Unmarshal([]byte(addonUpdateAppYaml), &app_test_update)).Should(BeNil())
Expect(k8sClient.Create(ctx, &app_test_update)).Should(BeNil())
Eventually(func() error {
var err error
appCheck := v1beta1.Application{}
err = k8sClient.Get(ctx, types2.NamespacedName{Namespace: "vela-system", Name: "addon-test-update"}, &appCheck)
if err != nil {
return err
}
if appCheck.Labels["addons.oam.dev/version"] != "v1.2.0" {
return fmt.Errorf("label missmatch")
}
return nil
}, time.Millisecond*500, 30*time.Second).Should(BeNil())
pkg := &InstallPackage{Meta: Meta{Name: "test-update", Version: "1.3.0"}}
h := NewAddonInstaller(context.Background(), k8sClient, nil, nil, nil, &Registry{Name: "test"}, nil, nil)
h.addon = pkg
Expect(h.dispatchAddonResource(pkg)).Should(BeNil())
Eventually(func() error {
var err error
appCheck := v1beta1.Application{}
err = k8sClient.Get(context.Background(), types2.NamespacedName{Namespace: "vela-system", Name: "addon-test-update"}, &appCheck)
if err != nil {
return err
}
if appCheck.Labels["addons.oam.dev/version"] != "1.3.0" {
return fmt.Errorf("label missmatch")
}
return nil
}, time.Second*3, 300*time.Second).Should(BeNil())
})
})
const (
appYaml = `apiVersion: core.oam.dev/v1beta1
kind: Application
@@ -400,4 +439,21 @@ spec:
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30`
addonUpdateAppYaml = `
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: addon-test-update
namespace: vela-system
labels:
addons.oam.dev/version: v1.2.0
spec:
components:
- name: express-server
type: webservice
properties:
image: crccheck/hello-world
port: 8000
`
)

View File

@@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net/http"
"net/http/httptest"
@@ -35,7 +36,7 @@ import (
v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -303,7 +304,7 @@ func TestGetAddonStatus(t *testing.T) {
getFunc := test.MockGetFn(func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
switch key.Name {
case "addon-disabled", "disabled":
return errors.NewNotFound(schema.GroupResource{Group: "apiVersion: core.oam.dev/v1beta1", Resource: "app"}, key.Name)
return kerrors.NewNotFound(schema.GroupResource{Group: "apiVersion: core.oam.dev/v1beta1", Resource: "app"}, key.Name)
case "addon-suspend":
o := obj.(*v1beta1.Application)
app := &v1beta1.Application{}
@@ -897,3 +898,11 @@ func TestRenderCUETemplate(t *testing.T) {
assert.True(t, component.Type == "raw")
assert.True(t, config["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["version"] == "1.0.1")
}
func TestCheckEnableAddonErrorWhenMissMatch(t *testing.T) {
version2.VelaVersion = "v1.3.0"
i := InstallPackage{Meta: Meta{SystemRequirements: &SystemRequirements{VelaVersion: ">=1.4.0"}}}
installer := &Installer{}
err := installer.enableAddon(&i)
assert.Equal(t, errors.As(err, &VersionUnMatchError{}), true)
}

View File

@@ -22,6 +22,8 @@ import (
"sync"
"time"
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
)
@@ -106,7 +108,7 @@ func (u *Cache) GetUIData(r Registry, addonName, version string) (*UIData, error
versionedRegistry := BuildVersionedRegistry(r.Name, r.Helm.URL)
addon, err = versionedRegistry.GetAddonUIData(context.Background(), addonName, version)
if err != nil {
log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", r.Name, err)
log.Logger.Errorf("fail to get addons from registry %s for cache updating, %v", utils.Sanitize(r.Name), err)
return nil, err
}
}

View File

@@ -17,6 +17,8 @@ limitations under the License.
package addon
import (
"fmt"
"github.com/google/go-github/v32/github"
"github.com/pkg/errors"
)
@@ -35,9 +37,6 @@ var (
// ErrNotExist means addon not exists
ErrNotExist = NewAddonError("addon not exist")
// ErrVersionMismatch means addon version requirement mismatch
ErrVersionMismatch = NewAddonError("addon version requirements mismatch")
)
// WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly
@@ -48,3 +47,13 @@ func WrapErrRateLimit(err error) error {
}
return err
}
// VersionUnMatchError means addon system requirement cannot meet requirement
type VersionUnMatchError struct {
err error
addonName string
}
func (v VersionUnMatchError) Error() string {
return fmt.Sprintf("addon %s system requirement miss match: %v", v.addonName, v.err)
}

View File

@@ -52,7 +52,7 @@ type versionedRegistry struct {
}
func (i *versionedRegistry) ListAddon() ([]*UIData, error) {
chartIndex, err := i.h.GetIndexInfo(i.url, false)
chartIndex, err := i.h.GetIndexInfo(i.url, false, nil)
if err != nil {
return nil, err
}
@@ -107,7 +107,7 @@ func (i *versionedRegistry) resolveAddonListFromIndex(repoName string, index *re
}
func (i versionedRegistry) loadAddon(ctx context.Context, name, version string) (*WholeAddonPackage, error) {
versions, err := i.h.ListVersions(i.url, name, false)
versions, err := i.h.ListVersions(i.url, name, false, nil)
if err != nil {
return nil, err
}
@@ -131,7 +131,7 @@ func (i versionedRegistry) loadAddon(ctx context.Context, name, version string)
return nil, fmt.Errorf("specified version %s not exist", version)
}
for _, chartURL := range addonVersion.URLs {
archive, err := common.HTTPGet(ctx, chartURL)
archive, err := common.HTTPGetWithOption(ctx, chartURL, nil)
if err != nil {
continue
}

View File

@@ -30,9 +30,38 @@ const (
// SystemInfo systemInfo model
type SystemInfo struct {
BaseModel
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
DexConfig DexConfig `json:"dexConfig,omitempty"`
}
// DexConfig dex config
type DexConfig struct {
Issuer string `json:"issuer"`
Web DexWeb `json:"web"`
Storage DexStorage `json:"storage"`
StaticClients []DexStaticClient `json:"staticClients"`
Connectors []interface{} `json:"connectors,omitempty"`
EnablePasswordDB bool `json:"enablePasswordDB"`
}
// DexStorage dex storage
type DexStorage struct {
Type string `json:"type"`
}
// DexWeb dex web
type DexWeb struct {
HTTP string `json:"http"`
}
// DexStaticClient dex static client
type DexStaticClient struct {
ID string `json:"id"`
Name string `json:"name"`
Secret string `json:"secret"`
RedirectURIs []string `json:"redirectURIs"`
}
// TableName return custom table name

View File

@@ -184,6 +184,28 @@ type AddonArgsResponse struct {
Args map[string]string `json:"args"`
}
// ConfigType define the format for listing configuration types
type ConfigType struct {
Definitions []string `json:"definitions"`
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
}
// Config define the metadata of a config
type Config struct {
ConfigType string `json:"configType"`
ConfigTypeAlias string `json:"configTypeAlias"`
Name string `json:"name"`
Project string `json:"project"`
Identifier string `json:"identifier"`
Description string `json:"description"`
CreatedTime *time.Time `json:"createdTime"`
UpdatedTime *time.Time `json:"updatedTime"`
ApplicationStatus common.ApplicationPhase `json:"applicationStatus"`
Status string `json:"status"`
}
// AccessKeyRequest request parameters to access cloud provider
type AccessKeyRequest struct {
AccessKeyID string `json:"accessKeyID"`
@@ -382,6 +404,15 @@ type CreateApplicationRequest struct {
Component *CreateComponentRequest `json:"component"`
}
// CreateConfigRequest is the request body to creates a config
type CreateConfigRequest struct {
Name string `json:"name" validate:"checkname"`
Alias string `json:"alias"`
Project string `json:"project"`
ComponentType string `json:"componentType" validate:"checkname"`
Properties string `json:"properties,omitempty"`
}
// UpdateApplicationRequest update application base config
type UpdateApplicationRequest struct {
Alias string `json:"alias" validate:"checkalias" optional:"true"`
@@ -1081,14 +1112,22 @@ type DetailRevisionResponse struct {
// SystemInfoResponse get SystemInfo
type SystemInfoResponse struct {
model.SystemInfo
SystemInfo
SystemVersion SystemVersion `json:"systemVersion"`
}
// SystemInfo system info
type SystemInfo struct {
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
}
// SystemInfoRequest request by update SystemInfo
type SystemInfoRequest struct {
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
VelaAddress string `json:"velaAddress,omitempty"`
}
// SystemVersion contains KubeVela version
@@ -1277,3 +1316,14 @@ type LoginUserInfoResponse struct {
PlatformPermissions []PermissionBase `json:"platformPermissions"`
ProjectPermissions map[string][]PermissionBase `json:"projectPermissions"`
}
// ChartRepoResponse the response body of chart repo
type ChartRepoResponse struct {
URL string `json:"url"`
SecretName string `json:"secretName"`
}
// ChartRepoResponseList the response body of list chart repo
type ChartRepoResponseList struct {
ChartRepoResponse []*ChartRepoResponse `json:"repos"`
}

View File

@@ -398,7 +398,8 @@ func (u *defaultAddonHandler) EnableAddon(ctx context.Context, name string, args
}
// wrap this error with special bcode
if errors.Is(err, pkgaddon.ErrVersionMismatch) {
if errors.As(err, &pkgaddon.VersionUnMatchError{}) {
log.Logger.Error(err)
return bcode.ErrAddonSystemVersionMismatch
}
// except `addon not found`, other errors should return directly
@@ -464,7 +465,7 @@ func (u *defaultAddonHandler) UpdateAddon(ctx context.Context, name string, args
}
// wrap this error with special bcode
if errors.Is(err, pkgaddon.ErrVersionMismatch) {
if errors.As(err, &pkgaddon.VersionUnMatchError{}) {
return bcode.ErrAddonSystemVersionMismatch
}
// except `addon not found`, other errors should return directly

View File

@@ -33,18 +33,18 @@ import (
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
velatypes "github.com/oam-dev/kubevela/apis/types"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
@@ -686,7 +686,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap
}, nil
}
// Deploy deploy app to cluster
// Deploy deploys app to cluster
// means to render oam application config and apply to cluster.
// An event record is generated for each deploy.
func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) {
@@ -704,6 +704,11 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
return nil, err
}
// sync configs to clusters
if err := c.syncConfigs4Application(ctx, oamApp, app.Project, workflow.EnvName); err != nil {
return nil, err
}
// step2: check and create deploy event
if !req.Force {
var lastVersion = model.ApplicationRevision{
@@ -792,6 +797,44 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
}, nil
}
// sync configs to clusters
func (c *applicationUsecaseImpl) syncConfigs4Application(ctx context.Context, app *v1beta1.Application, projectName, envName string) error {
var areTerraformComponents = true
for _, m := range app.Spec.Components {
d := &v1beta1.ComponentDefinition{}
if err := c.kubeClient.Get(ctx, client.ObjectKey{Namespace: velatypes.DefaultKubeVelaNS, Name: m.Type}, d); err != nil {
klog.ErrorS(err, "failed to get config type", "ComponentDefinition", m.Type)
}
// check the type of the componentDefinition is Terraform
if d.Spec.Schematic != nil && d.Spec.Schematic.Terraform == nil {
areTerraformComponents = false
}
}
// skip configs sync
if areTerraformComponents {
return nil
}
env, err := c.envUsecase.GetEnv(ctx, envName)
if err != nil {
return err
}
var clusterTargets []*model.ClusterTarget
for _, t := range env.Targets {
target, err := c.targetUsecase.GetTarget(ctx, t)
if err != nil {
return err
}
if target.Cluster != nil {
clusterTargets = append(clusterTargets, target.Cluster)
}
}
if err := SyncConfigs(ctx, c.kubeClient, projectName, clusterTargets); err != nil {
return fmt.Errorf("sync config failure %w", err)
}
return nil
}
func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) {
// Priority 1 uses the requested workflow as release .
// Priority 2 uses the default workflow as release .

View File

@@ -18,6 +18,7 @@ package usecase
import (
"context"
"encoding/json"
"errors"
"net/http"
"time"
@@ -25,26 +26,30 @@ import (
"github.com/coreos/go-oidc"
"github.com/form3tech-oss/jwt-go"
"golang.org/x/oauth2"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
const (
secretDexConfig = "dex-config"
keyDex = "dex"
dexConfigName = "dex-config"
secretDexConfigKey = "config.yaml"
dexAddonName = "addon-dex"
jwtIssuer = "vela-issuer"
signedKey = "vela-singned"
// GrantTypeAccess is the grant type for access token
GrantTypeAccess = "access"
@@ -52,12 +57,15 @@ const (
GrantTypeRefresh = "refresh"
)
var signedKey = ""
// AuthenticationUsecase is the usecase of authentication
type AuthenticationUsecase interface {
Login(ctx context.Context, loginReq apisv1.LoginRequest) (*apisv1.LoginResponse, error)
RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error)
GetDexConfig(ctx context.Context) (*apisv1.DexConfigResponse, error)
GetLoginType(ctx context.Context) (*apisv1.GetLoginTypeResponse, error)
UpdateDexConfig(ctx context.Context) error
}
type authenticationUsecaseImpl struct {
@@ -86,7 +94,6 @@ type authHandler interface {
}
type dexHandlerImpl struct {
token *oauth2.Token
idToken *oidc.IDToken
ds datastore.DataStore
}
@@ -127,7 +134,6 @@ func (a *authenticationUsecaseImpl) newDexHandler(ctx context.Context, req apisv
return nil, err
}
return &dexHandlerImpl{
token: token,
idToken: idToken,
ds: a.ds,
}, nil
@@ -148,7 +154,7 @@ func (a *authenticationUsecaseImpl) newLocalHandler(req apisv1.LoginRequest) (*l
func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.LoginRequest) (*apisv1.LoginResponse, error) {
var handler authHandler
var err error
sysInfo, err := a.sysUsecase.GetSystemInfo(ctx)
sysInfo, err := a.sysUsecase.Get(ctx)
if err != nil {
return nil, err
}
@@ -202,13 +208,15 @@ func (a *authenticationUsecaseImpl) generateJWTToken(username, grantType string,
GrantType: grantType,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(signedKey))
}
func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error) {
claim, err := ParseToken(refreshToken)
if err != nil {
if errors.Is(err, bcode.ErrTokenExpired) {
return nil, bcode.ErrRefreshTokenExpired
}
return nil, err
}
if claim.GrantType == GrantTypeRefresh {
@@ -253,33 +261,10 @@ func ParseToken(tokenString string) (*model.CustomClaims, error) {
}
func (a *authenticationUsecaseImpl) GetDexConfig(ctx context.Context) (*apisv1.DexConfigResponse, error) {
secret := &v1.Secret{}
if err := a.kubeClient.Get(ctx, types.NamespacedName{
Name: secretDexConfig,
Namespace: velatypes.DefaultKubeVelaNS,
}, secret); err != nil {
if apierrors.IsNotFound(err) {
return nil, bcode.ErrDexConfigNotFound
}
log.Logger.Errorf("failed to get dex config: %s", err.Error())
config, err := getDexConfig(ctx, a.kubeClient)
if err != nil {
return nil, err
}
var config struct {
Issuer string `json:"issuer"`
StaticClients []struct {
ID string `json:"id"`
Secret string `json:"secret"`
RedirectURIs []string `json:"redirectURIs"`
} `json:"staticClients"`
}
if err := yaml.Unmarshal(secret.Data[secretDexConfigKey], &config); err != nil {
log.Logger.Errorf("failed to unmarshal dex config: %s", err.Error())
return nil, bcode.ErrInvalidDexConfig
}
if len(config.StaticClients) < 1 || len(config.StaticClients[0].RedirectURIs) < 1 {
return nil, bcode.ErrInvalidDexConfig
}
return &apisv1.DexConfigResponse{
Issuer: config.Issuer,
ClientID: config.StaticClients[0].ID,
@@ -288,8 +273,121 @@ func (a *authenticationUsecaseImpl) GetDexConfig(ctx context.Context) (*apisv1.D
}, nil
}
func (a *authenticationUsecaseImpl) UpdateDexConfig(ctx context.Context) error {
connectors, err := utils.GetDexConnectors(ctx, a.kubeClient)
if err != nil {
return err
}
dexConfig := &corev1.Secret{}
if err := a.kubeClient.Get(ctx, types.NamespacedName{
Name: dexConfigName,
Namespace: velatypes.DefaultKubeVelaNS,
}, dexConfig); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
}
config := &model.JSONStruct{}
if dexConfig == nil || dexConfig.Data == nil {
(*config)["connectors"] = connectors
c, err := yaml.Marshal(config)
if err != nil {
return err
}
if err := a.kubeClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: dexConfigName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
secretDexConfigKey: c,
},
}); err != nil {
return err
}
} else {
err = yaml.Unmarshal(dexConfig.Data[secretDexConfigKey], config)
if err != nil {
return err
}
(*config)["connectors"] = connectors
c, err := yaml.Marshal(config)
if err != nil {
return err
}
dexConfig.Data[secretDexConfigKey] = c
if err := a.kubeClient.Update(ctx, dexConfig); err != nil {
return err
}
}
return restartDex(ctx, a.kubeClient)
}
func restartDex(ctx context.Context, kubeClient client.Client) error {
dexApp := &v1beta1.Application{}
if err := kubeClient.Get(ctx, types.NamespacedName{
Name: dexAddonName,
Namespace: velatypes.DefaultKubeVelaNS,
}, dexApp); err != nil {
if kerrors.IsNotFound(err) {
return bcode.ErrDexNotFound
}
return err
}
for i, comp := range dexApp.Spec.Components {
if comp.Name == keyDex {
var v model.JSONStruct
err := json.Unmarshal(comp.Properties.Raw, &v)
if err != nil {
return err
}
// restart the dex server
if _, ok := v["values"]; ok {
v["values"].(map[string]interface{})["env"] = map[string]string{
"TIME_STAMP": time.Now().Format(time.RFC3339),
}
}
dexApp.Spec.Components[i].Properties = v.RawExtension()
if err := kubeClient.Update(ctx, dexApp); err != nil {
return err
}
break
}
}
return nil
}
func getDexConfig(ctx context.Context, kubeClient client.Client) (*model.DexConfig, error) {
dexConfigSecret := &corev1.Secret{}
if err := kubeClient.Get(ctx, types.NamespacedName{
Name: dexConfigName,
Namespace: velatypes.DefaultKubeVelaNS,
}, dexConfigSecret); err != nil {
if kerrors.IsNotFound(err) {
return nil, bcode.ErrDexConfigNotFound
}
return nil, err
}
if dexConfigSecret.Data == nil {
return nil, bcode.ErrInvalidDexConfig
}
config := &model.DexConfig{}
if err := yaml.Unmarshal(dexConfigSecret.Data[secretDexConfigKey], config); err != nil {
log.Logger.Errorf("failed to unmarshal dex config: %s", err.Error())
return nil, bcode.ErrInvalidDexConfig
}
if len(config.StaticClients) < 1 || len(config.StaticClients[0].RedirectURIs) < 1 {
return nil, bcode.ErrInvalidDexConfig
}
return config, nil
}
func (a *authenticationUsecaseImpl) GetLoginType(ctx context.Context) (*apisv1.GetLoginTypeResponse, error) {
sysInfo, err := a.sysUsecase.GetSystemInfo(ctx)
sysInfo, err := a.sysUsecase.Get(ctx)
if err != nil {
return nil, err
}
@@ -312,19 +410,18 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
}
user := &model.User{Email: claims.Email}
userBase := &apisv1.UserBase{Email: claims.Email, Name: claims.Name}
users, err := d.ds.List(ctx, user, &datastore.ListOptions{})
if err != nil {
return nil, err
}
if len(users) > 0 {
u := users[0].(*model.User)
if u.Name != claims.Name {
u.Name = claims.Name
}
u.LastLoginTime = time.Now()
if err := d.ds.Put(ctx, u); err != nil {
return nil, err
}
userBase.Name = u.Name
} else if err := d.ds.Add(ctx, &model.User{
Email: claims.Email,
Name: claims.Name,
@@ -333,10 +430,7 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
return nil, err
}
return &apisv1.UserBase{
Name: claims.Name,
Email: claims.Email,
}, nil
return userBase, nil
}
func (l *localHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {

View File

@@ -19,6 +19,7 @@ package usecase
import (
"context"
"encoding/json"
"io/ioutil"
"reflect"
"strconv"
"time"
@@ -27,13 +28,18 @@ import (
"github.com/coreos/go-oidc"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"golang.org/x/oauth2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -51,7 +57,7 @@ var _ = Describe("Test authentication usecase functions", func() {
Expect(ds).ToNot(BeNil())
Expect(err).Should(BeNil())
authUsecase = &authenticationUsecaseImpl{kubeClient: k8sClient, ds: ds}
sysUsecase = &systemInfoUsecaseImpl{ds: ds}
sysUsecase = &systemInfoUsecaseImpl{ds: ds, kubeClient: k8sClient}
userUsecase = &userUsecaseImpl{ds: ds, sysUsecase: sysUsecase}
})
It("Test Dex login", func() {
@@ -61,10 +67,6 @@ var _ = Describe("Test authentication usecase functions", func() {
})
defer patch.Reset()
dexHandler := dexHandlerImpl{
token: &oauth2.Token{
AccessToken: "access-token",
RefreshToken: "refresh-token",
},
idToken: testIDToken,
ds: ds,
}
@@ -79,6 +81,20 @@ var _ = Describe("Test authentication usecase functions", func() {
err = ds.Get(context.Background(), user)
Expect(err).Should(BeNil())
Expect(user.Email).Should(Equal("test@test.com"))
existUser := &model.User{
Name: "test",
}
err = ds.Delete(context.Background(), existUser)
Expect(err).Should(BeNil())
existUser.Name = "exist-user"
existUser.Email = "test@test.com"
err = ds.Add(context.Background(), existUser)
Expect(err).Should(BeNil())
resp, err = dexHandler.login(context.Background())
Expect(err).Should(BeNil())
Expect(resp.Email).Should(Equal("test@test.com"))
Expect(resp.Name).Should(Equal("exist-user"))
})
It("Test local login", func() {
@@ -99,37 +115,87 @@ var _ = Describe("Test authentication usecase functions", func() {
Expect(resp.Name).Should(Equal("test-login"))
})
It("Test get dex config", func() {
It("Test update dex config", func() {
err := k8sClient.Create(context.Background(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-system",
},
})
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
err = k8sClient.Create(context.Background(), &corev1.Secret{
webserver, err := ioutil.ReadFile("./testdata/dex-config-def.yaml")
Expect(err).Should(Succeed())
var cd v1beta1.ComponentDefinition
err = yaml.Unmarshal(webserver, &cd)
Expect(err).Should(Succeed())
err = k8sClient.Create(context.Background(), &cd)
Expect(err).Should(Succeed())
err = k8sClient.Create(context.Background(), &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: secretDexConfig,
Name: "addon-dex",
Namespace: "vela-system",
},
StringData: map[string]string{
secretDexConfigKey: `
issuer: https://dex.oam.dev
staticClients:
- id: client-id
secret: client-secret
redirectURIs:
- http://localhost:8080/auth/callback
`,
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "dex",
// only for test here
Type: "dex-config",
Properties: &runtime.RawExtension{Raw: []byte(`{"values":{"test":"test"}}`)},
Traits: []common.ApplicationTrait{},
Scopes: map[string]string{},
},
},
},
})
Expect(err).Should(BeNil())
config, err := authUsecase.GetDexConfig(context.Background())
err = k8sClient.Create(context.Background(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "a",
Namespace: "vela-system",
Labels: map[string]string{
"app.oam.dev/source-of-truth": "from-inner-system",
"config.oam.dev/catalog": "velacore-config",
"config.oam.dev/type": "config-dex-connector",
"config.oam.dev/sub-type": "ldap",
"project": "abc",
},
},
StringData: map[string]string{
"ldap": `{"clientID":"clientID","clientSecret":"clientSecret"}`,
},
Type: corev1.SecretTypeOpaque,
})
Expect(err).Should(BeNil())
By("try to update dex config without config secret")
err = authUsecase.UpdateDexConfig(context.Background())
Expect(err).Should(BeNil())
dexConfigSecret := &corev1.Secret{}
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "dex-config", Namespace: "vela-system"}, dexConfigSecret)
Expect(err).Should(BeNil())
config := &model.DexConfig{}
err = yaml.Unmarshal(dexConfigSecret.Data[secretDexConfigKey], config)
Expect(err).Should(BeNil())
Expect(len(config.Connectors)).Should(Equal(1))
By("try to update dex config with config secret")
err = authUsecase.UpdateDexConfig(context.Background())
Expect(err).Should(BeNil())
Expect(config.Issuer).Should(Equal("https://dex.oam.dev"))
Expect(config.ClientID).Should(Equal("client-id"))
Expect(config.ClientSecret).Should(Equal("client-secret"))
Expect(config.RedirectURL).Should(Equal("http://localhost:8080/auth/callback"))
})
It("Test get dex config", func() {
_, err := authUsecase.GetDexConfig(context.Background())
Expect(err).Should(Equal(bcode.ErrInvalidDexConfig))
err = ds.Add(context.Background(), &model.User{Name: "admin", Email: "test@test.com"})
Expect(err).Should(BeNil())
_, err = sysUsecase.UpdateSystemInfo(context.Background(), apisv1.SystemInfoRequest{
LoginType: model.LoginTypeDex,
VelaAddress: "http://velaux.com",
})
Expect(err).Should(BeNil())
config, err := authUsecase.GetDexConfig(context.Background())
Expect(err).Should(BeNil())
Expect(config.Issuer).Should(Equal("http://velaux.com/dex"))
Expect(config.ClientID).Should(Equal("velaux"))
Expect(config.ClientSecret).Should(Equal("velaux-secret"))
Expect(config.RedirectURL).Should(Equal("http://velaux.com/callback"))
})
})

View File

@@ -0,0 +1,478 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package usecase
import (
"context"
"encoding/json"
"fmt"
set "github.com/deckarep/golang-set"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/definition"
)
const (
definitionAlias = definition.UserPrefix + "alias.config.oam.dev"
definitionType = definition.UserPrefix + "type.config.oam.dev"
velaCoreConfig = "velacore-config"
configIsReady = "Ready"
configIsNotReady = "Not ready"
terraformProviderAlias = "Terraform Cloud Provider"
configSyncProjectPrefix = "config-sync"
)
// ConfigHandler handle CRUD of configs
type ConfigHandler interface {
ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error)
GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error)
CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error
GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error)
GetConfig(ctx context.Context, configType, name string) (*apis.Config, error)
DeleteConfig(ctx context.Context, configType, name string) error
}
// NewConfigUseCase returns a config use case
func NewConfigUseCase(authenticationUseCase AuthenticationUsecase) ConfigHandler {
k8sClient, err := clients.GetKubeClient()
if err != nil {
panic(err)
}
return &configUseCaseImpl{
authenticationUseCase: authenticationUseCase,
kubeClient: k8sClient,
}
}
type configUseCaseImpl struct {
kubeClient client.Client
authenticationUseCase AuthenticationUsecase
}
// ListConfigTypes returns all config types
func (u *configUseCaseImpl) ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error) {
defs := &v1beta1.ComponentDefinitionList{}
if err := u.kubeClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig}); err != nil {
return nil, err
}
var tfDefs []v1beta1.ComponentDefinition
var configTypes []*apis.ConfigType
for _, d := range defs.Items {
if d.Labels[definitionType] == types.TerraformProvider {
tfDefs = append(tfDefs, d)
continue
}
configTypes = append(configTypes, &apis.ConfigType{
Alias: d.Annotations[definitionAlias],
Name: d.Name,
Definitions: []string{d.Name},
Description: d.Annotations[types.AnnoDefinitionDescription],
})
}
if len(tfDefs) > 0 {
tfType := &apis.ConfigType{
Alias: terraformProviderAlias,
Name: types.TerraformProvider,
}
definitions := make([]string, len(tfDefs))
for i, tf := range tfDefs {
definitions[i] = tf.Name
}
tfType.Definitions = definitions
return append(configTypes, tfType), nil
}
return configTypes, nil
}
// GetConfigType returns a config type
func (u *configUseCaseImpl) GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error) {
d := &v1beta1.ComponentDefinition{}
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: configType}, d); err != nil {
return nil, errors.Wrap(err, "failed to get config type")
}
t := &apis.ConfigType{
Alias: d.Annotations[definitionAlias],
Name: configType,
Description: d.Annotations[types.AnnoDefinitionDescription],
}
return t, nil
}
func (u *configUseCaseImpl) CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error {
app := v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: req.Name,
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
types.AnnotationConfigAlias: req.Alias,
},
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: velaCoreConfig,
types.LabelConfigType: req.ComponentType,
types.LabelConfigProject: req.Project,
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: req.Name,
Type: req.ComponentType,
Properties: &runtime.RawExtension{Raw: []byte(req.Properties)},
},
},
},
}
return u.kubeClient.Create(ctx, &app)
}
func (u *configUseCaseImpl) GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error) {
switch configType {
case types.TerraformProvider:
defs := &v1beta1.ComponentDefinitionList{}
if err := u.kubeClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
definition.UserPrefix + "type.config.oam.dev": types.TerraformProvider,
}); err != nil {
return nil, err
}
var configs []*apis.Config
for _, d := range defs.Items {
subConfigs, err := u.getConfigsByConfigType(ctx, d.Name)
if err != nil {
return nil, err
}
configs = append(configs, subConfigs...)
}
return configs, nil
default:
return u.getConfigsByConfigType(ctx, configType)
}
}
func (u *configUseCaseImpl) getConfigsByConfigType(ctx context.Context, configType string) ([]*apis.Config, error) {
var apps = &v1beta1.ApplicationList{}
if err := u.kubeClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: velaCoreConfig,
types.LabelConfigType: configType,
}); err != nil {
return nil, err
}
configs := make([]*apis.Config, len(apps.Items))
for i, a := range apps.Items {
configs[i] = &apis.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: a.Labels[types.LabelConfigProject],
CreatedTime: &(a.CreationTimestamp.Time),
}
switch a.Status.Phase {
case common.ApplicationRunning:
configs[i].Status = configIsReady
default:
configs[i].Status = configIsNotReady
}
}
return configs, nil
}
func (u *configUseCaseImpl) GetConfig(ctx context.Context, configType, name string) (*apis.Config, error) {
var a = &v1beta1.Application{}
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
return nil, err
}
config := &apis.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: a.Labels[types.LabelConfigProject],
CreatedTime: &a.CreationTimestamp.Time,
}
return config, nil
}
func (u *configUseCaseImpl) DeleteConfig(ctx context.Context, configType, name string) error {
var a = &v1beta1.Application{}
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
return err
}
return u.kubeClient.Delete(ctx, a)
}
// ApplicationDeployTarget is the struct of application deploy target
type ApplicationDeployTarget struct {
Namespace string `json:"namespace"`
Clusters []string `json:"clusters"`
}
// SyncConfigs will sync configs to working clusters
func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, targets []*model.ClusterTarget) error {
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
// get all configs which can be synced to working clusters in the project
var secrets v1.SecretList
if err := k8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: velaCoreConfig,
types.LabelConfigSyncToMultiCluster: "true",
}); err != nil {
return err
}
if len(secrets.Items) == 0 {
return nil
}
var objects []map[string]string
for _, s := range secrets.Items {
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
objects = append(objects, map[string]string{
"name": s.Name,
"resource": "secret",
})
}
}
if len(objects) == 0 {
klog.InfoS("no configs need to sync to working clusters", "project", project)
return nil
}
objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects})
if err != nil {
return err
}
var app = &v1beta1.Application{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
// config sync application doesn't exist, create one
clusterTargets := convertClusterTargets(targets)
if len(clusterTargets) == 0 {
errMsg := "no policy (no targets found) to sync configs"
klog.InfoS(errMsg, "project", project)
return errors.New(errMsg)
}
policies := make([]v1beta1.AppPolicy, len(clusterTargets))
for i, t := range clusterTargets {
properties, err := json.Marshal(t)
if err != nil {
return err
}
policies[i] = v1beta1.AppPolicy{
Type: "topology",
Name: t.Namespace,
Properties: &runtime.RawExtension{
Raw: properties,
},
}
}
scratch := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: velaCoreConfig,
types.LabelConfigProject: project,
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: name,
Type: "ref-objects",
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
},
Policies: policies,
},
}
return k8sClient.Create(ctx, scratch)
}
// config sync application exists, update it
app.Spec.Components = []common.ApplicationComponent{
{
Name: name,
Type: "ref-objects",
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
}
currentTargets := make([]ApplicationDeployTarget, len(app.Spec.Policies))
for i, p := range app.Spec.Policies {
var t ApplicationDeployTarget
if err := json.Unmarshal(p.Properties.Raw, &t); err != nil {
return err
}
currentTargets[i] = t
}
mergedTarget := mergeTargets(currentTargets, targets)
if len(mergedTarget) == 0 {
errMsg := "no policy (no targets found) to sync configs"
klog.InfoS(errMsg, "project", project)
return errors.New(errMsg)
}
mergedPolicies := make([]v1beta1.AppPolicy, len(mergedTarget))
for i, t := range mergedTarget {
properties, err := json.Marshal(t)
if err != nil {
return err
}
mergedPolicies[i] = v1beta1.AppPolicy{
Type: "topology",
Name: t.Namespace,
Properties: &runtime.RawExtension{
Raw: properties,
},
}
}
app.Spec.Policies = mergedPolicies
return k8sClient.Update(ctx, app)
}
func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.ClusterTarget) []ApplicationDeployTarget {
var (
mergedTargets []ApplicationDeployTarget
// make sure the clusters of target with same namespace are merged
clusterTargets = convertClusterTargets(targets)
)
for _, c := range currentTargets {
var hasSameNamespace bool
for _, t := range clusterTargets {
if c.Namespace == t.Namespace {
hasSameNamespace = true
clusters := set.NewSetFromSlice(stringToInterfaceSlice(t.Clusters))
for _, cluster := range c.Clusters {
clusters.Add(cluster)
}
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace,
Clusters: interfaceToStringSlice(clusters.ToSlice())})
}
}
if !hasSameNamespace {
mergedTargets = append(mergedTargets, c)
}
}
for _, t := range clusterTargets {
var hasSameNamespace bool
for _, c := range currentTargets {
if c.Namespace == t.Namespace {
hasSameNamespace = true
}
}
if !hasSameNamespace {
mergedTargets = append(mergedTargets, t)
}
}
return mergedTargets
}
func convertClusterTargets(targets []*model.ClusterTarget) []ApplicationDeployTarget {
type Target struct {
Namespace string `json:"namespace"`
Clusters []interface{} `json:"clusters"`
}
var (
clusterTargets []Target
namespaceSet = set.NewSet()
)
for i := 0; i < len(targets); i++ {
clusters := set.NewSet(targets[i].ClusterName)
for j := i + 1; j < len(targets); j++ {
if targets[i].Namespace == targets[j].Namespace {
clusters.Add(targets[j].ClusterName)
}
}
if namespaceSet.Contains(targets[i].Namespace) {
continue
}
clusterTargets = append(clusterTargets, Target{
Namespace: targets[i].Namespace,
Clusters: clusters.ToSlice(),
})
namespaceSet.Add(targets[i].Namespace)
}
t := make([]ApplicationDeployTarget, len(clusterTargets))
for i, ct := range clusterTargets {
t[i] = ApplicationDeployTarget{
Namespace: ct.Namespace,
Clusters: interfaceToStringSlice(ct.Clusters),
}
}
return t
}
func interfaceToStringSlice(i []interface{}) []string {
var s []string
for _, v := range i {
s = append(s, v.(string))
}
return s
}
func stringToInterfaceSlice(i []string) []interface{} {
var s []interface{}
for _, v := range i {
s = append(s, v)
}
return s
}
// destroySyncConfigsApp will delete the application which is used to sync configs
func destroySyncConfigsApp(ctx context.Context, k8sClient client.Client, project string) error {
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
var app = &v1beta1.Application{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
}
return k8sClient.Delete(ctx, app)
}

View File

@@ -0,0 +1,589 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package usecase
import (
"context"
"encoding/json"
"sort"
"strings"
"testing"
. "github.com/agiledragon/gomonkey/v2"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/multicluster"
)
func TestListConfigTypes(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
def1 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
definitionType: types.TerraformProvider,
},
},
}
def2 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def2",
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
definitionAlias: "Def2",
},
Labels: map[string]string{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def1, def2).Build()
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
return k8sClient, nil, nil
})
defer patches.Reset()
h := NewConfigUseCase(nil)
type args struct {
h ConfigHandler
}
type want struct {
configTypes []*apis.ConfigType
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
h: h,
},
want: want{
configTypes: []*apis.ConfigType{
{
Name: "def2",
Alias: "Def2",
Definitions: []string{"def2"},
},
{
Alias: "Terraform Cloud Provider",
Name: types.TerraformProvider,
Definitions: []string{
"def1",
},
},
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.ListConfigTypes(ctx, "")
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configTypes)
})
}
}
func TestGetConfigType(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
def2 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def2",
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
definitionAlias: "Def2",
},
Labels: map[string]string{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def2).Build()
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
return k8sClient, nil, nil
})
defer patches.Reset()
h := NewConfigUseCase(nil)
type args struct {
h ConfigHandler
name string
}
type want struct {
configType *apis.ConfigType
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
h: h,
name: "def2",
},
want: want{
configType: &apis.ConfigType{
Alias: "Def2",
Name: "def2",
},
},
},
{
name: "error",
args: args{
h: h,
name: "def99",
},
want: want{
errMsg: "failed to get config type",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigType(ctx, tc.args.name)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configType)
})
}
}
func TestCreateConfig(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
h := &configUseCaseImpl{kubeClient: k8sClient}
type args struct {
h ConfigHandler
req apis.CreateConfigRequest
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "delete config when it's not ready",
args: args{
h: h,
req: apis.CreateConfigRequest{
Name: "a",
ComponentType: "b",
Project: "c",
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := tc.args.h.CreateConfig(ctx, tc.args.req)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
})
}
}
func TestGetConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
def1 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
definitionType: types.TerraformProvider,
},
},
}
def2 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def2",
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
definitionAlias: "Def2",
},
Labels: map[string]string{
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def1, def2).Build()
h := &configUseCaseImpl{kubeClient: k8sClient}
type args struct {
configType string
h ConfigHandler
}
type want struct {
configs []*apis.Config
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
configType: types.TerraformProvider,
h: h,
},
want: want{
configs: nil,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigs(ctx, tc.args.configType)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configs)
})
}
}
func TestMergeTargets(t *testing.T) {
currentTargets := []ApplicationDeployTarget{
{
Namespace: "n1",
Clusters: []string{"c1", "c2"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
},
}
targets := []*model.ClusterTarget{
{
Namespace: "n3",
ClusterName: "c4",
}, {
Namespace: "n1",
ClusterName: "c5",
},
{
Namespace: "n2",
ClusterName: "c3",
},
}
expected := []ApplicationDeployTarget{
{
Namespace: "n1",
Clusters: []string{"c1", "c2", "c5"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
}, {
Namespace: "n3",
Clusters: []string{"c4"},
},
}
got := mergeTargets(currentTargets, targets)
for i, g := range got {
clusters := g.Clusters
sort.SliceStable(clusters, func(i, j int) bool {
return clusters[i] < clusters[j]
})
got[i].Clusters = clusters
}
assert.DeepEqual(t, expected, got)
}
func TestConvert(t *testing.T) {
targets := []*model.ClusterTarget{
{
Namespace: "n3",
ClusterName: "c4",
}, {
Namespace: "n1",
ClusterName: "c5",
},
{
Namespace: "n2",
ClusterName: "c3",
},
{
Namespace: "n3",
ClusterName: "c5",
},
}
expected := []ApplicationDeployTarget{
{
Namespace: "n3",
Clusters: []string{"c4", "c5"},
},
{
Namespace: "n1",
Clusters: []string{"c5"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
},
}
got := convertClusterTargets(targets)
for i, g := range got {
clusters := g.Clusters
sort.SliceStable(clusters, func(i, j int) bool {
return clusters[i] < clusters[j]
})
got[i].Clusters = clusters
}
assert.DeepEqual(t, expected, got)
}
func TestDestroySyncConfigsApp(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-p1",
Namespace: types.DefaultKubeVelaNS,
},
}
k8sClient1 := fake.NewClientBuilder().WithScheme(s).WithObjects(app1).Build()
k8sClient2 := fake.NewClientBuilder().Build()
type args struct {
project string
k8sClient client.Client
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := map[string]struct {
args args
want want
}{
"found": {
args: args{
project: "p1",
k8sClient: k8sClient1,
},
},
"not found": {
args: args{
project: "p1",
k8sClient: k8sClient2,
},
want: want{
errMsg: "no kind is registered for the type v1beta1.Application",
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
err := destroySyncConfigsApp(ctx, tc.args.k8sClient, tc.args.project)
if err != nil || tc.want.errMsg != "" {
if !strings.Contains(err.Error(), tc.want.errMsg) {
assert.ErrorContains(t, err, tc.want.errMsg)
}
}
})
}
}
func TestSyncConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
secret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigCatalog: velaCoreConfig,
types.LabelConfigProject: "p1",
types.LabelConfigSyncToMultiCluster: "true",
},
},
}
policies := []ApplicationDeployTarget{{
Namespace: "n9",
Clusters: []string{"c19"},
}}
properties, _ := json.Marshal(policies)
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-p2",
Namespace: types.DefaultKubeVelaNS,
},
Spec: v1beta1.ApplicationSpec{
Policies: []v1beta1.AppPolicy{{
Name: "c19",
Type: "topology",
Properties: &runtime.RawExtension{Raw: properties},
}},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(secret1, app1).Build()
type args struct {
project string
targets []*model.ClusterTarget
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "create",
args: args{
project: "p1",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
{
name: "update",
args: args{
project: "p2",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
{
name: "skip config sync",
args: args{
project: "p3",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := SyncConfigs(ctx, k8sClient, tc.args.project, tc.args.targets)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
})
}
}

View File

@@ -20,57 +20,108 @@ import (
"context"
"strconv"
"helm.sh/helm/v3/pkg/repo"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/helm"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types2 "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"helm.sh/helm/v3/pkg/repo"
)
// NewHelmUsecase return a helmHandler
func NewHelmUsecase() HelmHandler {
c, err := clients.GetKubeClient()
if err != nil {
log.Logger.Fatalf("get kube client failure %s", err.Error())
}
return defaultHelmHandler{
helper: helm.NewHelperWithCache(),
helper: helm.NewHelperWithCache(),
k8sClient: c,
}
}
// HelmHandler responsible handle helm related interface
type HelmHandler interface {
ListChartNames(ctx context.Context, url string, skipCache bool) ([]string, error)
ListChartVersions(ctx context.Context, url string, chartName string, skipCache bool) (repo.ChartVersions, error)
GetChartValues(ctx context.Context, url string, chartName string, version string, skipCache bool) (map[string]interface{}, error)
ListChartNames(ctx context.Context, url string, secretName string, skipCache bool) ([]string, error)
ListChartVersions(ctx context.Context, url string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error)
GetChartValues(ctx context.Context, url string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error)
ListChartRepo(ctx context.Context, projectName string) (*v1.ChartRepoResponseList, error)
}
type defaultHelmHandler struct {
helper *helm.Helper
helper *helm.Helper
k8sClient client.Client
}
func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, skipCache bool) ([]string, error) {
charts, err := d.helper.ListChartsFromRepo(url, skipCache)
func (d defaultHelmHandler) ListChartNames(ctx context.Context, repoURL string, secretName string, skipCache bool) ([]string, error) {
if !utils.IsValidURL(repoURL) {
return nil, bcode.ErrRepoInvalidURL
}
var opts *common.HTTPOption
var err error
if len(secretName) != 0 {
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
if err != nil {
return nil, bcode.ErrRepoBasicAuth
}
}
charts, err := d.helper.ListChartsFromRepo(repoURL, skipCache, opts)
if err != nil {
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", url, err.Error())
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", utils.Sanitize(repoURL), err.Error())
return nil, bcode.ErrListHelmChart
}
return charts, nil
}
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, url string, chartName string, skipCache bool) (repo.ChartVersions, error) {
chartVersions, err := d.helper.ListVersions(url, chartName, skipCache)
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, repoURL string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error) {
if !utils.IsValidURL(repoURL) {
return nil, bcode.ErrRepoInvalidURL
}
var opts *common.HTTPOption
var err error
if len(secretName) != 0 {
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
if err != nil {
return nil, bcode.ErrRepoBasicAuth
}
}
chartVersions, err := d.helper.ListVersions(repoURL, chartName, skipCache, opts)
if err != nil {
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", url, chartName, err.Error())
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), err.Error())
return nil, bcode.ErrListHelmVersions
}
if len(chartVersions) == 0 {
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", url, chartName)
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName))
return nil, bcode.ErrChartNotExist
}
return chartVersions, nil
}
func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, chartName string, version string, skipCache bool) (map[string]interface{}, error) {
v, err := d.helper.GetValuesFromChart(url, chartName, version, skipCache)
func (d defaultHelmHandler) GetChartValues(ctx context.Context, repoURL string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error) {
if !utils.IsValidURL(repoURL) {
return nil, bcode.ErrRepoInvalidURL
}
var opts *common.HTTPOption
var err error
if len(secretName) != 0 {
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
if err != nil {
return nil, bcode.ErrRepoBasicAuth
}
}
v, err := d.helper.GetValuesFromChart(repoURL, chartName, version, skipCache, opts)
if err != nil {
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", url, chartName, version, err.Error())
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), utils.Sanitize(version), err.Error())
return nil, bcode.ErrGetChartValues
}
res := make(map[string]interface{}, len(v))
@@ -78,6 +129,50 @@ func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, char
return res, nil
}
func (d defaultHelmHandler) ListChartRepo(ctx context.Context, projectName string) (*v1.ChartRepoResponseList, error) {
var res []*v1.ChartRepoResponse
var err error
if len(projectName) != 0 {
projectSecrets := corev1.SecretList{}
opts := []client.ListOption{
client.MatchingLabels{oam.LabelConfigType: "config-helm-repository", types.LabelConfigProject: projectName},
client.InNamespace(types.DefaultKubeVelaNS),
}
err = d.k8sClient.List(ctx, &projectSecrets, opts...)
if err != nil {
return nil, err
}
for _, item := range projectSecrets.Items {
res = append(res, &v1.ChartRepoResponse{URL: string(item.Data["url"]), SecretName: item.Name})
}
}
globalSecrets := corev1.SecretList{}
selector := metav1.LabelSelector{
MatchLabels: map[string]string{oam.LabelConfigType: "config-helm-repository"},
MatchExpressions: []metav1.LabelSelectorRequirement{
{Key: types.LabelConfigProject, Operator: metav1.LabelSelectorOpDoesNotExist},
},
}
ls, _ := metav1.LabelSelectorAsSelector(&selector)
err = d.k8sClient.List(ctx, &globalSecrets, &client.ListOptions{
LabelSelector: ls,
Namespace: types.DefaultKubeVelaNS,
})
if err != nil {
return nil, err
}
for _, item := range globalSecrets.Items {
res = append(res, &v1.ChartRepoResponse{URL: string(item.Data["url"]), SecretName: item.Name})
}
return &v1.ChartRepoResponseList{ChartRepoResponse: res}, nil
}
// this func will flatten a nested map, the key will flatten with separator "." and the value's type will be keep
// src is the map you want to flatten the output will be set in dest map
// eg : src is {a:{b:{c:true}}} , the dest is {a.b.c:true}

View File

@@ -17,9 +17,23 @@ limitations under the License.
package usecase
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/oam/util"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"github.com/stretchr/testify/assert"
)
@@ -37,6 +51,147 @@ func TestFlattenKeyFunc(t *testing.T) {
assert.Equal(t, dstMap, res)
}
var _ = Describe("Test helm repo list", func() {
ctx := context.Background()
var pSec, gSec v1.Secret
BeforeEach(func() {
pSec = v1.Secret{}
gSec = v1.Secret{}
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(yaml.Unmarshal([]byte(projectSecret), &pSec)).Should(BeNil())
Expect(yaml.Unmarshal([]byte(globalSecret), &gSec)).Should(BeNil())
Expect(k8sClient.Create(ctx, &pSec)).Should(BeNil())
Expect(k8sClient.Create(ctx, &gSec)).Should(BeNil())
})
AfterEach(func() {
Expect(k8sClient.Delete(ctx, &gSec)).Should(BeNil())
Expect(k8sClient.Delete(ctx, &pSec)).Should(BeNil())
})
It("Test list with project ", func() {
u := NewHelmUsecase()
list, err := u.ListChartRepo(ctx, "my-project")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(2))
found := 0
for _, response := range list.ChartRepoResponse {
if response.SecretName == "project-helm-repo" {
Expect(response.URL).Should(BeEquivalentTo("https://kedacore.github.io/charts"))
found++
}
if response.SecretName == "global-helm-repo" {
Expect(response.URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
found++
}
}
Expect(found).Should(BeEquivalentTo(2))
})
It("Test list func with not exist project", func() {
u := NewHelmUsecase()
list, err := u.ListChartRepo(ctx, "not-exist-project")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
Expect(list.ChartRepoResponse[0].URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
Expect(list.ChartRepoResponse[0].SecretName).Should(BeEquivalentTo("global-helm-repo"))
})
It("Test list func without project", func() {
u := NewHelmUsecase()
list, err := u.ListChartRepo(ctx, "")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
Expect(list.ChartRepoResponse[0].URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
Expect(list.ChartRepoResponse[0].SecretName).Should(BeEquivalentTo("global-helm-repo"))
})
})
var _ = Describe("test helm usecasae", func() {
ctx := context.Background()
var repoSec v1.Secret
BeforeEach(func() {
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
repoSec = v1.Secret{}
Expect(yaml.Unmarshal([]byte(repoSecret), &repoSec)).Should(BeNil())
Expect(k8sClient.Create(ctx, &repoSec)).Should(BeNil())
})
AfterEach(func() {
Expect(k8sClient.Delete(ctx, &repoSec)).Should(BeNil())
})
It("helm associated usecase interface test", func() {
var mockServer *httptest.Server
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
u, p, ok := request.BasicAuth()
if !ok || u != "admin" || p != "admin" {
writer.WriteHeader(401)
return
}
switch {
case request.URL.Path == "/index.yaml":
index, err := ioutil.ReadFile("./testdata/helm/index.yaml")
indexFile := string(index)
indexFile = strings.ReplaceAll(indexFile, "server-url", mockServer.URL)
if err != nil {
writer.Write([]byte(err.Error()))
return
}
writer.Write([]byte(indexFile))
return
case strings.Contains(request.URL.Path, "mysql-8.8.23.tgz"):
pkg, err := ioutil.ReadFile("./testdata/helm/mysql-8.8.23.tgz")
if err != nil {
writer.Write([]byte(err.Error()))
return
}
writer.Write(pkg)
return
default:
writer.Write([]byte("404 page not found"))
}
})
mockServer = httptest.NewServer(handler)
defer mockServer.Close()
u := NewHelmUsecase()
charts, err := u.ListChartNames(ctx, mockServer.URL, "repo-secret", false)
Expect(err).Should(BeNil())
Expect(len(charts)).Should(BeEquivalentTo(1))
Expect(charts[0]).Should(BeEquivalentTo("mysql"))
versions, err := u.ListChartVersions(ctx, mockServer.URL, "mysql", "repo-secret", false)
Expect(err).Should(BeNil())
Expect(len(versions)).Should(BeEquivalentTo(1))
Expect(versions[0].Version).Should(BeEquivalentTo("8.8.23"))
values, err := u.GetChartValues(ctx, mockServer.URL, "mysql", "8.8.23", "repo-secret", false)
Expect(err).Should(BeNil())
Expect(values).ShouldNot(BeNil())
Expect(len(values)).ShouldNot(BeEquivalentTo(0))
})
It("coverage not secret notExist error", func() {
u := NewHelmUsecase()
_, err := u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false)
Expect(err).ShouldNot(BeNil())
_, err = u.ListChartVersions(ctx, "http://127.0.0.1:8080", "mysql", "repo-secret-notExist", false)
Expect(err).ShouldNot(BeNil())
_, err = u.GetChartValues(ctx, "http://127.0.0.1:8080", "mysql", "8.8.23", "repo-secret-notExist", false)
Expect(err).ShouldNot(BeNil())
})
})
var (
src = `{
"OAMSpecVer":"v0.2",
@@ -175,4 +330,43 @@ var (
"webhookService.port": 11443,
"webhookService.type": "ClusterIP"
}`
globalSecret = `
apiVersion: v1
stringData:
url: https://charts.bitnami.com/bitnami
kind: Secret
metadata:
labels:
config.oam.dev/type: config-helm-repository
name: global-helm-repo
namespace: vela-system
type: Opaque
`
projectSecret = `
apiVersion: v1
kind: Secret
metadata:
name: project-helm-repo
namespace: vela-system
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/project: my-project
stringData:
url: https://kedacore.github.io/charts
type: Opaque
`
repoSecret = `
apiVersion: v1
kind: Secret
metadata:
name: repo-secret
namespace: vela-system
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/project: my-project-2
stringData:
username: admin
password: admin
type: Opaque
`
)

View File

@@ -20,9 +20,16 @@ import (
"context"
"errors"
"fmt"
"strings"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
@@ -46,6 +53,7 @@ type ProjectUsecase interface {
DeleteProjectUser(ctx context.Context, projectName string, userName string) error
UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error)
Init(ctx context.Context) error
GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error)
}
type projectUsecaseImpl struct {
@@ -290,7 +298,11 @@ func (p *projectUsecaseImpl) DeleteProject(ctx context.Context, name string) err
return err
}
}
return p.ds.Delete(ctx, &model.Project{Name: name})
if err := p.ds.Delete(ctx, &model.Project{Name: name}); err != nil {
return err
}
// delete config-sync application
return destroySyncConfigsApp(ctx, p.k8sClient, name)
}
// CreateProject create project
@@ -466,6 +478,118 @@ func (p *projectUsecaseImpl) UpdateProjectUser(ctx context.Context, projectName
return ConvertProjectUserModel2Base(&projectUser), nil
}
func (p *projectUsecaseImpl) GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error) {
var (
configs []*apisv1.Config
legacyTerraformProviders []*apisv1.Config
apps = &v1beta1.ApplicationList{}
)
if err := p.k8sClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: velaCoreConfig,
}); err != nil {
return nil, err
}
if configType == types.TerraformProvider || configType == "" {
// legacy providers
var providers = &terraformapi.ProviderList{}
if err := p.k8sClient.List(ctx, providers, client.InNamespace(types.DefaultAppNamespace)); err != nil {
return nil, err
}
for _, p := range providers.Items {
if p.Labels[types.LabelConfigCatalog] == velaCoreConfig {
continue
}
t := p.CreationTimestamp.Time
var status = configIsNotReady
if p.Status.State == terraformtypes.ProviderIsReady {
status = configIsReady
}
legacyTerraformProviders = append(legacyTerraformProviders, &apisv1.Config{
Name: p.Name,
CreatedTime: &t,
Status: status,
})
}
}
switch configType {
case types.TerraformProvider:
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) ||
!strings.Contains(a.Labels[types.LabelConfigType], "terraform-") {
continue
}
configs = append(configs, &apisv1.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: appProject,
CreatedTime: &(a.CreationTimestamp.Time),
ApplicationStatus: a.Status.Phase,
})
}
configs = append(configs, legacyTerraformProviders...)
case "":
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if appProject != "" && appProject != projectName {
continue
}
configs = append(configs, &apisv1.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: appProject,
CreatedTime: &(a.CreationTimestamp.Time),
ApplicationStatus: a.Status.Phase,
})
}
configs = append(configs, legacyTerraformProviders...)
case types.DexConnector, types.HelmRepository, types.ImageRegistry:
t := strings.ReplaceAll(configType, "config-", "")
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) {
continue
}
if a.Labels[types.LabelConfigType] == t {
configs = append(configs, &apisv1.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: appProject,
CreatedTime: &(a.CreationTimestamp.Time),
ApplicationStatus: a.Status.Phase,
})
}
}
default:
return nil, errors.New("unsupported config type")
}
for i, c := range configs {
if c.ConfigType != "" {
d := &v1beta1.ComponentDefinition{}
err := p.k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: c.ConfigType}, d)
if err != nil {
klog.InfoS("failed to get component definition", "ComponentDefinition", configType, "err", err)
} else {
configs[i].ConfigTypeAlias = d.Annotations[definitionAlias]
}
}
if c.ApplicationStatus != "" {
if c.ApplicationStatus == common.ApplicationRunning {
configs[i].Status = configIsReady
} else {
configs[i].Status = configIsNotReady
}
}
}
return configs, nil
}
// ConvertProjectModel2Base convert project model to base struct
func ConvertProjectModel2Base(project *model.Project, owner *model.User) *apisv1.ProjectBase {
base := &apisv1.ProjectBase{

View File

@@ -18,14 +18,23 @@ package usecase
import (
"context"
"testing"
"time"
"github.com/google/go-cmp/cmp"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
@@ -155,6 +164,18 @@ var _ = Describe("Test project usecase functions", func() {
Name: "test-project",
Description: "this is a project description",
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-test-project",
Namespace: "vela-system",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Type: "aaa",
}},
},
}
Expect(k8sClient.Create(context.TODO(), app1)).Should(BeNil())
_, err := projectUsecase.CreateProject(context.TODO(), req)
Expect(err).Should(BeNil())
@@ -223,6 +244,19 @@ var _ = Describe("Test project usecase functions", func() {
Name: "test-project",
Description: "this is a project description",
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-test-project",
Namespace: "vela-system",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Type: "aaa",
}},
},
}
Expect(k8sClient.Create(context.TODO(), app1)).Should(BeNil())
_, err := projectUsecase.CreateProject(context.TODO(), req)
Expect(err).Should(BeNil())
@@ -244,3 +278,222 @@ var _ = Describe("Test project usecase functions", func() {
Expect(roles.Total).Should(BeEquivalentTo(0))
})
})
func TestProjectGetConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
createdTime, _ := time.Parse(time.UnixDate, "Wed Apr 7 11:06:39 PST 2022")
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a1",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velaCoreConfig,
velatypes.LabelConfigType: "terraform-provider",
"config.oam.dev/project": "p1",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
app2 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a2",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velaCoreConfig,
velatypes.LabelConfigType: "terraform-provider",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
app3 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a3",
Namespace: velatypes.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
velatypes.LabelConfigCatalog: velaCoreConfig,
velatypes.LabelConfigType: "dex-connector",
"config.oam.dev/project": "p3",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{Phase: common.ApplicationRunning},
}
provider1 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider1",
Namespace: "default",
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsReady,
},
}
provider2 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider2",
Namespace: "default",
Labels: map[string]string{
velatypes.LabelConfigCatalog: velaCoreConfig,
},
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsNotReady,
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(app1, app2, app3, provider1, provider2).Build()
h := &projectUsecaseImpl{k8sClient: k8sClient}
type args struct {
projectName string
configType string
h ProjectUsecase
}
type want struct {
configs []*apisv1.Config
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "project is matched",
args: args{
projectName: "p1",
configType: "terraform-provider",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a1",
Project: "p1",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "project is not matched",
args: args{
projectName: "p999",
configType: "terraform-provider",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "config type is empty",
args: args{
projectName: "p3",
configType: "",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "terraform-provider",
Name: "a2",
Project: "",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
ConfigType: "dex-connector",
Name: "a3",
Project: "p3",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}, {
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
}},
},
},
{
name: "config type is dex",
args: args{
projectName: "p3",
configType: "config-dex-connector",
h: h,
},
want: want{
configs: []*apisv1.Config{{
ConfigType: "dex-connector",
Name: "a3",
Project: "p3",
CreatedTime: &createdTime,
ApplicationStatus: "running",
Status: "Ready",
}},
},
},
{
name: "config type is invalid",
args: args{
configType: "xxx",
h: h,
},
want: want{
errMsg: "unsupported config type",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigs(ctx, tc.args.projectName, tc.args.configType)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configs)
})
}
}

View File

@@ -182,6 +182,7 @@ var ResourceMaps = map[string]resourceMetadata{
pathName: "userName",
},
"applicationTemplate": {},
"configs": {},
},
pathName: "projectName",
},
@@ -207,6 +208,14 @@ var ResourceMaps = map[string]resourceMetadata{
"permission": {},
"systemSetting": {},
"definition": {},
"configType": {
pathName: "configType",
subResources: map[string]resourceMetadata{
"config": {
pathName: "name",
},
},
},
}
var existResourcePaths = convert(ResourceMaps)

View File

@@ -18,31 +18,52 @@ package usecase
import (
"context"
"errors"
"fmt"
"reflect"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/rand"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
"github.com/oam-dev/kubevela/version"
)
// SystemInfoUsecase is usecase for systemInfoCollection
type SystemInfoUsecase interface {
Get(ctx context.Context) (*model.SystemInfo, error)
GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error)
UpdateSystemInfo(ctx context.Context, sysInfo v1.SystemInfoRequest) (*v1.SystemInfoResponse, error)
Init(ctx context.Context) error
}
type systemInfoUsecaseImpl struct {
ds datastore.DataStore
ds datastore.DataStore
kubeClient client.Client
}
// NewSystemInfoUsecase return a systemInfoCollectionUsecase
func NewSystemInfoUsecase(ds datastore.DataStore) SystemInfoUsecase {
return &systemInfoUsecaseImpl{ds: ds}
kubecli, err := clients.GetKubeClient()
if err != nil {
log.Logger.Fatalf("failed to get kube client: %s", err.Error())
}
return &systemInfoUsecaseImpl{ds: ds, kubeClient: kubecli}
}
func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error) {
func (u systemInfoUsecaseImpl) Get(ctx context.Context) (*model.SystemInfo, error) {
// first get request will init systemInfoCollection{installId: {random}, enableCollection: true}
info := &model.SystemInfo{}
entities, err := u.ds.List(ctx, info, &datastore.ListOptions{})
@@ -54,7 +75,7 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf
if info.LoginType == "" {
info.LoginType = model.LoginTypeLocal
}
return &v1.SystemInfoResponse{SystemInfo: *info, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
return info, nil
}
installID := rand.String(16)
info.InstallID = installID
@@ -64,11 +85,26 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf
if err != nil {
return nil, err
}
return &v1.SystemInfoResponse{SystemInfo: *info, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
return info, nil
}
func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error) {
// first get request will init systemInfoCollection{installId: {random}, enableCollection: true}
info, err := u.Get(ctx)
if err != nil {
return nil, err
}
return &v1.SystemInfoResponse{
SystemInfo: convertInfoToBase(info),
SystemVersion: v1.SystemVersion{
VelaVersion: version.VelaVersion,
GitVersion: version.GitRevision,
},
}, nil
}
func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.SystemInfoRequest) (*v1.SystemInfoResponse, error) {
info, err := u.GetSystemInfo(ctx)
info, err := u.Get(ctx)
if err != nil {
return nil, err
}
@@ -78,10 +114,131 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
LoginType: sysInfo.LoginType,
BaseModel: model.BaseModel{
CreateTime: info.CreateTime,
}}
},
}
if sysInfo.LoginType == model.LoginTypeDex {
admin := &model.User{Name: model.DefaultAdminUserName}
if err := u.ds.Get(ctx, admin); err != nil {
return nil, err
}
if admin.Email == "" {
return nil, bcode.ErrEmptyAdminEmail
}
if err := generateDexConfig(ctx, u.kubeClient, sysInfo.VelaAddress, &modifiedInfo); err != nil {
return nil, err
}
}
err = u.ds.Put(ctx, &modifiedInfo)
if err != nil {
return nil, err
}
return &v1.SystemInfoResponse{SystemInfo: modifiedInfo, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
return &v1.SystemInfoResponse{
SystemInfo: v1.SystemInfo{
InstallID: modifiedInfo.InstallID,
EnableCollection: modifiedInfo.EnableCollection,
LoginType: modifiedInfo.LoginType,
},
SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision},
}, nil
}
func (u systemInfoUsecaseImpl) Init(ctx context.Context) error {
info, err := u.Get(ctx)
if err != nil {
return err
}
signedKey = info.InstallID
_, err = initDexConfig(ctx, u.kubeClient, "http://velaux.com", &model.SystemInfo{})
return err
}
func convertInfoToBase(info *model.SystemInfo) v1.SystemInfo {
return v1.SystemInfo{
InstallID: info.InstallID,
EnableCollection: info.EnableCollection,
LoginType: info.LoginType,
}
}
func generateDexConfig(ctx context.Context, kubeClient client.Client, velaAddress string, info *model.SystemInfo) error {
secret, err := initDexConfig(ctx, kubeClient, velaAddress, info)
if err != nil {
return err
}
connectors, err := utils.GetDexConnectors(ctx, kubeClient)
if err != nil {
return err
}
if len(connectors) < 1 {
return bcode.ErrNoDexConnector
}
config, err := model.NewJSONStructByStruct(info.DexConfig)
if err != nil {
return err
}
(*config)["connectors"] = connectors
c, err := yaml.Marshal(config)
if err != nil {
return err
}
if !reflect.DeepEqual(secret.Data[secretDexConfigKey], c) {
secret.Data[secretDexConfigKey] = c
if err := kubeClient.Update(ctx, secret); err != nil {
return err
}
if err := restartDex(ctx, kubeClient); err != nil && !errors.Is(err, bcode.ErrDexNotFound) {
return err
}
}
return nil
}
func initDexConfig(ctx context.Context, kubeClient client.Client, velaAddress string, info *model.SystemInfo) (*corev1.Secret, error) {
dexConfig := model.DexConfig{
Issuer: fmt.Sprintf("%s/dex", velaAddress),
Web: model.DexWeb{
HTTP: "0.0.0.0:5556",
},
Storage: model.DexStorage{
Type: "memory",
},
StaticClients: []model.DexStaticClient{
{
ID: "velaux",
Name: "Vela UX",
Secret: "velaux-secret",
RedirectURIs: []string{fmt.Sprintf("%s/callback", velaAddress)},
},
},
EnablePasswordDB: true,
}
info.DexConfig = dexConfig
secret := &corev1.Secret{}
if err := kubeClient.Get(ctx, types.NamespacedName{
Name: dexConfigName,
Namespace: velatypes.DefaultKubeVelaNS,
}, secret); err != nil {
if !apierrors.IsNotFound(err) {
return nil, err
}
config, err := yaml.Marshal(info.DexConfig)
if err != nil {
return nil, err
}
if err := kubeClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: dexConfigName,
Namespace: velatypes.DefaultKubeVelaNS,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
secretDexConfigKey: config,
},
}); err != nil {
return nil, err
}
}
return secret, nil
}

View File

@@ -0,0 +1,26 @@
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
definition.oam.dev/description: Dex config allow users to specify dex config in properties
labels:
custom.definition.oam.dev/ui-hidden: "true"
name: dex-config
namespace: vela-system
spec:
schematic:
cue:
template: |
import "encoding/yaml"
output: {
apiVersion: "v1"
kind: "Secret"
metadata: name: context.name
namespace: context.namespace
type: "Opaque"
stringData: "config.yaml": yaml.Marshal(parameter)
}
parameter: {}
workload:
type: autodetects.core.oam.dev

View File

@@ -0,0 +1,36 @@
apiVersion: v1
entries:
mysql:
- annotations:
category: Database
apiVersion: v2
appVersion: 8.0.28
created: "2022-04-07T03:26:37.378966939Z"
dependencies:
- name: common
repository: https://charts.bitnami.com/bitnami
tags:
- bitnami-common
version: 1.x.x
description: Chart to create a Highly available MySQL cluster
digest: 96f79c6daba90fb40fc698979fab33f7a60987b1d23cd5080bc885129568a423
home: https://github.com/bitnami/charts/tree/master/bitnami/mysql
icon: https://bitnami.com/assets/stacks/mysql/img/mysql-stack-220x234.png
keywords:
- mysql
- database
- sql
- cluster
- high availability
maintainers:
- email: containers@bitnami.com
name: Bitnami
name: mysql
sources:
- https://github.com/bitnami/bitnami-docker-mysql
- https://mysql.com
urls:
- server-url/mysql-8.8.23.tgz
version: 8.8.23
generated: "2022-04-07T03:26:37Z"
serverInfo: {}

Binary file not shown.

View File

@@ -19,6 +19,8 @@ package usecase
import (
"context"
"errors"
"math/rand"
"strconv"
"golang.org/x/crypto/bcrypt"
"helm.sh/helm/v3/pkg/time"
@@ -82,7 +84,15 @@ func (u *userUsecaseImpl) Init(ctx context.Context) error {
Name: admin,
}); err != nil {
if errors.Is(err, datastore.ErrRecordNotExist) {
pwd := utils2.RandomString(8)
pwd := func() string {
p := utils2.RandomString(8)
p += strconv.Itoa(rand.Intn(9)) // #nosec
r := append([]rune(p), 'a'+rune(rand.Intn(26))) // #nosec
rand.Shuffle(len(r), func(i, j int) { r[i], r[j] = r[j], r[i] })
p = string(r)
return p
}()
encrypted, err := GeneratePasswordHash(pwd)
if err != nil {
return err
@@ -96,7 +106,7 @@ func (u *userUsecaseImpl) Init(ctx context.Context) error {
return err
}
// print default password of admin user in log
log.Logger.Infof("init admin user, password is %s", pwd)
log.Logger.Infof("initialized admin username and password: admin / %s\n", pwd)
secret := &corev1.Secret{}
if err := u.k8sClient.Get(ctx, k8stypes.NamespacedName{
Name: admin,
@@ -184,7 +194,7 @@ func (u *userUsecaseImpl) DeleteUser(ctx context.Context, username string) error
}
}
if err := u.ds.Delete(ctx, &model.User{Name: username}); err != nil {
log.Logger.Errorf("failed to delete user", username, err.Error())
log.Logger.Errorf("failed to delete user %s %v", utils2.Sanitize(username), err.Error())
return err
}
return nil
@@ -192,7 +202,7 @@ func (u *userUsecaseImpl) DeleteUser(ctx context.Context, username string) error
// CreateUser create user
func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserRequest) (*apisv1.UserBase, error) {
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
sysInfo, err := u.sysUsecase.Get(ctx)
if err != nil {
return nil, err
}
@@ -220,7 +230,7 @@ func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserR
// UpdateUser update user
func (u *userUsecaseImpl) UpdateUser(ctx context.Context, user *model.User, req apisv1.UpdateUserRequest) (*apisv1.UserBase, error) {
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
sysInfo, err := u.sysUsecase.Get(ctx)
if err != nil {
return nil, err
}

View File

@@ -25,11 +25,14 @@ import (
"github.com/emicklei/go-restful/v3"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
@@ -74,7 +77,12 @@ var _ = Describe("Test application usecase function", func() {
})
It("Test HandleApplicationWebhook function", func() {
_, err := projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"})
var ns = corev1.Namespace{}
ns.Name = types.DefaultKubeVelaNS
err := k8sClient.Create(context.TODO(), &ns)
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
_, err = projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"})
Expect(err).Should(BeNil())
_, err = targetUsecase.CreateTarget(context.TODO(), apisv1.CreateTargetRequest{Name: "dev-target-webhook", Project: "project-webhook"})

View File

@@ -35,4 +35,8 @@ var (
ErrInvalidLoginRequest = NewBcode(400, 12008, "the login request is invalid")
// ErrInvalidDexConfig is the error of invalid dex config
ErrInvalidDexConfig = NewBcode(400, 12009, "the dex config is invalid")
// ErrRefreshTokenExpired is the error of refresh token expired
ErrRefreshTokenExpired = NewBcode(400, 12010, "the refresh token is expired")
// ErrNoDexConnector is the error of no dex connector
ErrNoDexConnector = NewBcode(400, 12011, "there is no dex connector")
)

View File

@@ -30,3 +30,9 @@ var ErrChartNotExist = NewBcode(200, 13004, "this chart not exist in the reposit
// ErrSkipCacheParameter means the skip cache parameter miss config
var ErrSkipCacheParameter = NewBcode(400, 13005, "skip cache parameter miss config, the value only can be true or false")
// ErrRepoBasicAuth means extract repo auth info from secret error
var ErrRepoBasicAuth = NewBcode(400, 13006, "extract repo auth info from secret error")
// ErrRepoInvalidURL means user input url is invalid
var ErrRepoInvalidURL = NewBcode(400, 13007, "user input repository url is invalid")

View File

@@ -33,4 +33,8 @@ var (
ErrUserInconsistentPassword = NewBcode(401, 14007, "the password is inconsistent with the user")
// ErrUsernameNotExist is the error of username not exist
ErrUsernameNotExist = NewBcode(401, 14008, "the username is not exist")
// ErrDexNotFound is the error of dex not found
ErrDexNotFound = NewBcode(200, 14009, "the dex is not found")
// ErrEmptyAdminEmail is the error of empty admin email
ErrEmptyAdminEmail = NewBcode(400, 14010, "the admin email is empty, please set the admin email before using sso login")
)

View File

@@ -0,0 +1,52 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"context"
"encoding/json"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/types"
)
// GetDexConnectors returns the dex connectors for Dex connector controller
func GetDexConnectors(ctx context.Context, k8sClient client.Client) ([]map[string]interface{}, error) {
secrets := &v1.SecretList{}
if err := k8sClient.List(ctx, secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{types.LabelConfigType: types.DexConnector}); err != nil {
return nil, err
}
connectors := make([]map[string]interface{}, len(secrets.Items))
for i, s := range secrets.Items {
var data map[string]interface{}
key := s.Labels[types.LabelConfigSubType]
err := json.Unmarshal(s.Data[key], &data)
if err != nil {
return nil, err
}
connectors[i] = map[string]interface{}{
"type": s.Labels[types.LabelConfigSubType],
"id": s.Name,
"name": s.Name,
"config": data,
}
}
return connectors, nil
}

View File

@@ -0,0 +1,106 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"context"
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestGetDexConnectors(t *testing.T) {
ctx := context.Background()
type args struct {
k8sClient client.Client
}
type want struct {
connectors []map[string]interface{}
err error
}
ldap := map[string]interface{}{
"clientID": "clientID",
"clientSecret": "clientSecret",
"callbackURL": "redirectURL",
"xxx": map[string]interface{}{"aaa": "bbb", "ccc": "ddd"},
}
data, err := json.Marshal(ldap)
assert.NoError(t, err)
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "a",
Namespace: "vela-system",
Labels: map[string]string{
"app.oam.dev/source-of-truth": "from-inner-system",
"config.oam.dev/catalog": "velacore-config",
"config.oam.dev/type": "config-dex-connector",
"config.oam.dev/sub-type": "ldap",
"project": "abc",
},
},
Data: map[string][]byte{
"ldap": data,
},
Type: v1.SecretTypeOpaque,
}
k8sClient := fake.NewClientBuilder().WithObjects(secret).Build()
testcaes := map[string]struct {
args args
want want
}{
"test": {args: args{
k8sClient: k8sClient,
},
want: want{
connectors: []map[string]interface{}{{
"id": "a",
"name": "a",
"type": "ldap",
"config": map[string]interface{}{
"clientID": "clientID",
"clientSecret": "clientSecret",
"callbackURL": "redirectURL",
"xxx": map[string]interface{}{"aaa": "bbb", "ccc": "ddd"},
},
}},
},
},
}
for name, tc := range testcaes {
t.Run(name, func(t *testing.T) {
got, err := GetDexConnectors(ctx, tc.args.k8sClient)
if err != tc.want.err {
t.Errorf("%s: GetDexConnectors() error = %v, wantErr %v", name, err, tc.want.err)
return
}
if !reflect.DeepEqual(got, tc.want.connectors) {
t.Errorf("%s: GetDexConnectors() = %v, want %v", name, got, tc.want.connectors)
}
})
}
}

View File

@@ -0,0 +1,198 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webservice
import (
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
// ConfigWebService returns config web service
func ConfigWebService(u usecase.ConfigHandler, rbacUseCase usecase.RBACUsecase) WebService {
return &configWebService{
handler: u,
rbacUseCase: rbacUseCase,
}
}
type configWebService struct {
handler usecase.ConfigHandler
rbacUseCase usecase.RBACUsecase
}
func (s *configWebService) GetWebService() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/config_types").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for configuration management")
tags := []string{"config"}
ws.Route(ws.GET("/").To(s.listConfigTypes).
Doc("list all config types").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("configType", "list")).
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
Returns(200, "OK", []apis.ConfigType{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]apis.ConfigType{}))
ws.Route(ws.GET("/{configType}").To(s.getConfigType).
Doc("get a config type").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("configType", "get")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Returns(200, "OK", apis.ConfigType{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
ws.Route(ws.POST("/{configType}").To(s.createConfig).
Doc("create or update a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("configType", "create")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Reads(apis.CreateConfigRequest{}).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Returns(404, "Not Found", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.GET("/{configType}/configs").To(s.getConfigs).
Doc("get configs from a config type").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("config", "list")).
Param(ws.PathParameter("configType", "identifier of the config").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
ws.Route(ws.GET("/{configType}/configs/{name}").To(s.getConfig).
Doc("get a config from a config type").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("config", "get")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
ws.Route(ws.DELETE("/{configType}/configs/{name}").To(s.deleteConfig).
Doc("delete a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.rbacUseCase.CheckPerm("config", "delete")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Returns(404, "Not Found", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Filter(authCheckFilter)
return ws
}
func (s *configWebService) listConfigTypes(req *restful.Request, res *restful.Response) {
types, err := s.handler.ListConfigTypes(req.Request.Context(), req.QueryParameter("query"))
if len(types) == 0 && err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(types)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configWebService) getConfigType(req *restful.Request, res *restful.Response) {
t, err := s.handler.GetConfigType(req.Request.Context(), req.PathParameter("configType"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(t)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configWebService) createConfig(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var createReq apis.CreateConfigRequest
if err := req.ReadEntity(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
err := s.handler.CreateConfig(req.Request.Context(), createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configWebService) getConfigs(req *restful.Request, res *restful.Response) {
configs, err := s.handler.GetConfigs(req.Request.Context(), req.PathParameter("configType"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(configs)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configWebService) getConfig(req *restful.Request, res *restful.Response) {
t, err := s.handler.GetConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(t)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configWebService) deleteConfig(req *restful.Request, res *restful.Response) {
err := s.handler.DeleteConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}

View File

@@ -46,11 +46,21 @@ func (h helmWebService) GetWebService() *restful.WebService {
tags := []string{"repository", "helm"}
// List charts
ws.Route(ws.GET("/chart_repos").To(h.listRepo).
Doc("list chart repo").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("project", "the config project").DataType("string")).
Returns(200, "OK", []string{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
// List charts
ws.Route(ws.GET("/charts").To(h.listCharts).
Doc("list charts").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
Returns(200, "OK", []string{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
@@ -60,6 +70,7 @@ func (h helmWebService) GetWebService() *restful.WebService {
Doc("list versions").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
Returns(200, "OK", v1.ChartVersionListResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
@@ -69,6 +80,7 @@ func (h helmWebService) GetWebService() *restful.WebService {
Doc("get chart value").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
Returns(200, "OK", map[string]interface{}{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]string{}))
@@ -79,12 +91,13 @@ func (h helmWebService) GetWebService() *restful.WebService {
func (h helmWebService) listCharts(req *restful.Request, res *restful.Response) {
url := req.QueryParameter("repoUrl")
secName := req.QueryParameter("secretName")
skipCache, err := isSkipCache(req)
if err != nil {
bcode.ReturnError(req, res, bcode.ErrSkipCacheParameter)
return
}
charts, err := h.usecase.ListChartNames(context.Background(), url, skipCache)
charts, err := h.usecase.ListChartNames(context.Background(), url, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -99,13 +112,14 @@ func (h helmWebService) listCharts(req *restful.Request, res *restful.Response)
func (h helmWebService) listVersions(req *restful.Request, res *restful.Response) {
url := req.QueryParameter("repoUrl")
chartName := req.PathParameter("chart")
secName := req.QueryParameter("secretName")
skipCache, err := isSkipCache(req)
if err != nil {
bcode.ReturnError(req, res, bcode.ErrSkipCacheParameter)
return
}
versions, err := h.usecase.ListChartVersions(context.Background(), url, chartName, skipCache)
versions, err := h.usecase.ListChartVersions(context.Background(), url, chartName, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -119,6 +133,7 @@ func (h helmWebService) listVersions(req *restful.Request, res *restful.Response
func (h helmWebService) chartValues(req *restful.Request, res *restful.Response) {
url := req.QueryParameter("repoUrl")
secName := req.QueryParameter("secretName")
chartName := req.PathParameter("chart")
version := req.PathParameter("version")
skipCache, err := isSkipCache(req)
@@ -127,7 +142,7 @@ func (h helmWebService) chartValues(req *restful.Request, res *restful.Response)
return
}
versions, err := h.usecase.GetChartValues(context.Background(), url, chartName, version, skipCache)
versions, err := h.usecase.GetChartValues(context.Background(), url, chartName, version, secName, skipCache)
if err != nil {
bcode.ReturnError(req, res, err)
return
@@ -139,6 +154,20 @@ func (h helmWebService) chartValues(req *restful.Request, res *restful.Response)
}
}
func (h helmWebService) listRepo(req *restful.Request, res *restful.Response) {
project := req.QueryParameter("project")
repos, err := h.usecase.ListChartRepo(context.Background(), project)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(repos)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func isSkipCache(req *restful.Request) (bool, error) {
skipStr := req.QueryParameter("skipCache")
skipCache := false

View File

@@ -176,6 +176,16 @@ func (n *projectWebService) GetWebService() *restful.WebService {
Returns(200, "OK", []apis.PermissionBase{}).
Writes([]apis.PermissionBase{}))
ws.Route(ws.GET("/{projectName}/configs").To(n.getConfigs).
Doc("get configs which are in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.rbacUsecase.CheckPerm("project", "list")).
Param(ws.QueryParameter("configType", "config type").DataType("string")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]*apis.Config{}))
ws.Filter(authCheckFilter)
return ws
}
@@ -503,3 +513,23 @@ func (n *projectWebService) listProjectPermissions(req *restful.Request, res *re
return
}
}
func (n *projectWebService) getConfigs(req *restful.Request, res *restful.Response) {
configs, err := n.projectUsecase.GetConfigs(req.Request.Context(), req.PathParameter("projectName"), req.QueryParameter("configType"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if configs == nil {
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
return
}
err = res.WriteEntity(configs)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}

View File

@@ -73,15 +73,16 @@ func Init(ctx context.Context, ds datastore.DataStore, addonCacheTime time.Durat
definitionUsecase := usecase.NewDefinitionUsecase()
addonUsecase := usecase.NewAddonUsecase(addonCacheTime)
envBindingUsecase := usecase.NewEnvBindingUsecase(ds, workflowUsecase, definitionUsecase, envUsecase)
applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, envUsecase, targetUsecase, definitionUsecase, projectUsecase)
webhookUsecase := usecase.NewWebhookUsecase(ds, applicationUsecase)
systemInfoUsecase := usecase.NewSystemInfoUsecase(ds)
helmUsecase := usecase.NewHelmUsecase()
userUsecase := usecase.NewUserUsecase(ds, projectUsecase, systemInfoUsecase, rbacUsecase)
authenticationUsecase := usecase.NewAuthenticationUsecase(ds, systemInfoUsecase, userUsecase)
configUseCase := usecase.NewConfigUseCase(authenticationUsecase)
applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, envUsecase, targetUsecase, definitionUsecase, projectUsecase)
webhookUsecase := usecase.NewWebhookUsecase(ds, applicationUsecase)
// Modules that require default data initialization, Call it here in order
if initDatabase {
initData(ctx, userUsecase, rbacUsecase, projectUsecase, targetUsecase)
initData(ctx, userUsecase, rbacUsecase, projectUsecase, targetUsecase, systemInfoUsecase)
}
// Application
@@ -95,6 +96,9 @@ func Init(ctx context.Context, ds datastore.DataStore, addonCacheTime time.Durat
RegisterWebService(NewEnabledAddonWebService(addonUsecase, rbacUsecase))
RegisterWebService(NewAddonRegistryWebService(addonUsecase, rbacUsecase))
// Config management
RegisterWebService(ConfigWebService(configUseCase, rbacUsecase))
// Resources
RegisterWebService(NewClusterWebService(clusterUsecase, rbacUsecase))
RegisterWebService(NewOAMApplication(oamApplicationUsecase, rbacUsecase))

View File

@@ -66,6 +66,11 @@ func (c *CR2UX) initCache(ctx context.Context) error {
}
func (c *CR2UX) shouldSync(ctx context.Context, targetApp *v1beta1.Application, del bool) bool {
if targetApp != nil && targetApp.Labels != nil && targetApp.Labels[model.LabelSourceOfTruth] == model.FromInner {
return false
}
key := formatAppComposedName(targetApp.Name, targetApp.Namespace)
cachedData, ok := c.cache.Load(key)
if ok {
@@ -85,9 +90,11 @@ func (c *CR2UX) shouldSync(ctx context.Context, targetApp *v1beta1.Application,
// This is a double check to make sure the app not be converted and un-deployed
sot := c.CheckSoTFromAppMeta(ctx, targetApp.Name, targetApp.Namespace, CheckSoTFromCR(targetApp))
switch sot {
case model.FromUX, model.FromInner:
case model.FromUX:
// we don't sync if the application is not created from CR
return false
case model.FromInner:
// we don't sync if the application is not created from CR
return false
case model.FromCR:

View File

@@ -25,6 +25,7 @@ import (
corev1 "k8s.io/api/core/v1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
@@ -95,5 +96,28 @@ var _ = Describe("Test Cache", func() {
Expect(cr2ux.shouldSync(ctx, app1, false)).Should(BeEquivalentTo(true))
})
It("Test don't cache with from inner system label", func() {
dbNamespace := "cache-db-ns2-test"
ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: dbNamespace})
Expect(ds).ToNot(BeNil())
Expect(err).Should(BeNil())
var ns = corev1.Namespace{}
ns.Name = dbNamespace
err = k8sClient.Create(context.TODO(), &ns)
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
cr2ux := CR2UX{ds: ds, cli: k8sClient, cache: sync.Map{}}
ctx := context.Background()
app1 := &v1beta1.Application{}
app1.Name = "app1"
app1.Namespace = dbNamespace
app1.Generation = 1
app1.Spec.Components = []common.ApplicationComponent{}
app1.Labels = make(map[string]string)
app1.Labels[model.LabelSourceOfTruth] = model.FromInner
Expect(k8sClient.Create(ctx, app1)).Should(BeNil())
Expect(cr2ux.shouldSync(ctx, app1, false)).Should(BeEquivalentTo(false))
})
})

View File

@@ -20,6 +20,7 @@ import (
"context"
"strconv"
"strings"
"time"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
@@ -49,7 +50,8 @@ func (c *CR2UX) ConvertApp2DatastoreApp(ctx context.Context, targetApp *v1beta1.
model.LabelSourceOfTruth: model.FromCR,
},
}
appMeta.CreateTime = targetApp.CreationTimestamp.Time
appMeta.UpdateTime = time.Now()
// 1. convert app meta and env
dsApp := &model.DataStoreApp{
AppMeta: appMeta,

116
pkg/cmd/builder.go Normal file
View File

@@ -0,0 +1,116 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/term"
)
// Builder build command with factory
type Builder struct {
cmd *cobra.Command
f Factory
}
// NamespaceFlagConfig config for namespace flag in cmd
type NamespaceFlagConfig struct {
completion bool
usage string
loadEnv bool
}
// NamespaceFlagOption the option for configuring namespace flag in cmd
type NamespaceFlagOption interface {
ApplyToNamespaceFlagOptions(*NamespaceFlagConfig)
}
func newNamespaceFlagOptions(options ...NamespaceFlagOption) NamespaceFlagConfig {
cfg := NamespaceFlagConfig{
completion: true,
usage: usageNamespace,
loadEnv: true,
}
for _, option := range options {
option.ApplyToNamespaceFlagOptions(&cfg)
}
return cfg
}
// NamespaceFlagNoCompletionOption disable auto-completion for namespace flag
type NamespaceFlagNoCompletionOption struct{}
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagNoCompletionOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.completion = false
}
// NamespaceFlagUsageOption the usage description for namespace flag
type NamespaceFlagUsageOption string
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagUsageOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.usage = string(option)
}
// NamespaceFlagDisableEnvOption disable loading namespace from env
type NamespaceFlagDisableEnvOption struct{}
// ApplyToNamespaceFlagOptions .
func (option NamespaceFlagDisableEnvOption) ApplyToNamespaceFlagOptions(cfg *NamespaceFlagConfig) {
cfg.loadEnv = false
}
// WithNamespaceFlag add namespace flag to the command, by default, it will also add env flag to the command
func (builder *Builder) WithNamespaceFlag(options ...NamespaceFlagOption) *Builder {
cfg := newNamespaceFlagOptions(options...)
builder.cmd.Flags().StringP(flagNamespace, "n", "", cfg.usage)
if cfg.completion {
cmdutil.CheckErr(builder.cmd.RegisterFlagCompletionFunc(
flagNamespace,
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return GetNamespacesForCompletion(cmd.Context(), builder.f, toComplete)
}))
}
if cfg.loadEnv {
return builder.WithEnvFlag()
}
return builder
}
// WithEnvFlag add env flag to the command
func (builder *Builder) WithEnvFlag() *Builder {
builder.cmd.PersistentFlags().StringP(flagEnv, "e", "", usageEnv)
return builder
}
// WithResponsiveWriter format the command outputs
func (builder *Builder) WithResponsiveWriter() *Builder {
builder.cmd.SetOut(term.NewResponsiveWriter(builder.cmd.OutOrStdout()))
return builder
}
// Build construct the command
func (builder *Builder) Build() *cobra.Command {
return builder.cmd
}
// NewCommandBuilder builder for command
func NewCommandBuilder(f Factory, cmd *cobra.Command) *Builder {
return &Builder{cmd: cmd, f: f}
}

72
pkg/cmd/completion.go Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"strings"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
func listObjectNamesForCompletion(ctx context.Context, f Factory, gvk schema.GroupVersionKind, listOptions []client.ListOption, toComplete string) ([]string, cobra.ShellCompDirective) {
uns := &unstructured.UnstructuredList{}
uns.SetGroupVersionKind(gvk)
if err := f.Client().List(ctx, uns, listOptions...); err != nil {
return nil, cobra.ShellCompDirectiveError
}
var candidates []string
for _, obj := range uns.Items {
if name := obj.GetName(); strings.HasPrefix(name, toComplete) {
candidates = append(candidates, name)
}
}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
// GetNamespacesForCompletion auto-complete the namespace
func GetNamespacesForCompletion(ctx context.Context, f Factory, toComplete string) ([]string, cobra.ShellCompDirective) {
return listObjectNamesForCompletion(ctx, f, corev1.SchemeGroupVersion.WithKind("Namespace"), nil, toComplete)
}
// GetRevisionForCompletion auto-complete the revision according to the application
func GetRevisionForCompletion(ctx context.Context, f Factory, appName string, namespace string, toComplete string) ([]string, cobra.ShellCompDirective) {
var options []client.ListOption
if namespace != "" {
options = append(options, client.InNamespace(namespace))
}
if appName != "" {
options = append(options, client.MatchingLabels{oam.LabelAppName: appName})
}
return listObjectNamesForCompletion(ctx, f, v1beta1.SchemeGroupVersion.WithKind(v1beta1.ApplicationRevisionKind), options, toComplete)
}
// GetApplicationsForCompletion auto-complete application
func GetApplicationsForCompletion(ctx context.Context, f Factory, namespace string, toComplete string) ([]string, cobra.ShellCompDirective) {
var options []client.ListOption
if namespace != "" {
options = append(options, client.InNamespace(namespace))
}
return listObjectNamesForCompletion(ctx, f, v1beta1.SchemeGroupVersion.WithKind(v1beta1.ApplicationKind), options, toComplete)
}

46
pkg/cmd/factory.go Normal file
View File

@@ -0,0 +1,46 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/controller-runtime/pkg/client"
)
// Factory client factory for running command
type Factory interface {
Client() client.Client
}
// ClientGetter function for getting client
type ClientGetter func() (client.Client, error)
type defaultFactory struct {
ClientGetter
}
// Client return the client for command line use, interrupt if error encountered
func (f *defaultFactory) Client() client.Client {
cli, err := f.ClientGetter()
cmdutil.CheckErr(err)
return cli
}
// NewDefaultFactory create a factory based on client getter function
func NewDefaultFactory(clientGetter ClientGetter) Factory {
return &defaultFactory{ClientGetter: clientGetter}
}

27
pkg/cmd/types.go Normal file
View File

@@ -0,0 +1,27 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
const (
flagNamespace = "namespace"
flagEnv = "env"
)
const (
usageNamespace = "If present, the namespace scope for this CLI request"
usageEnv = "The environment name for the CLI request"
)

52
pkg/cmd/utils.go Normal file
View File

@@ -0,0 +1,52 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"github.com/spf13/cobra"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/env"
)
// GetNamespace get namespace from command flags and env
func GetNamespace(f Factory, cmd *cobra.Command) string {
namespace, err := cmd.Flags().GetString(flagNamespace)
cmdutil.CheckErr(err)
if namespace != "" {
return namespace
}
// find namespace from env
envName, err := cmd.Flags().GetString(flagEnv)
if err != nil {
// ignore env if the command does not use the flag
return ""
}
cmdutil.CheckErr(common.SetGlobalClient(f.Client()))
var envMeta *types.EnvMeta
if envName != "" {
envMeta, err = env.GetEnvByName(envName)
} else {
envMeta, err = env.GetCurrentEnv()
}
if err != nil {
return ""
}
return envMeta.Namespace
}

View File

@@ -17,7 +17,7 @@ limitations under the License.
package controller
import (
"flag"
flag "github.com/spf13/pflag"
ctrlClient "github.com/oam-dev/kubevela/pkg/client"
"github.com/oam-dev/kubevela/pkg/component"

View File

@@ -0,0 +1,51 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package utils
import (
"context"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
// FreezeApplication freeze application to disable the reconciling process for it
func FreezeApplication(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func()) (string, error) {
return oam.GetControllerRequirement(app), _updateApplicationWithControllerRequirement(ctx, cli, app, mutate, "Disabled")
}
// UnfreezeApplication unfreeze application to enable the reconciling process for it
func UnfreezeApplication(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func(), originalControllerRequirement string) error {
return _updateApplicationWithControllerRequirement(ctx, cli, app, mutate, originalControllerRequirement)
}
func _updateApplicationWithControllerRequirement(ctx context.Context, cli client.Client, app *v1beta1.Application, mutate func(), controllerRequirement string) error {
appKey := client.ObjectKeyFromObject(app)
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := cli.Get(ctx, appKey, app); err != nil {
return err
}
oam.SetControllerRequirement(app, controllerRequirement)
if mutate != nil {
mutate()
}
return cli.Update(ctx, app)
})
}

View File

@@ -449,6 +449,19 @@ func RenameCluster(ctx context.Context, k8sClient client.Client, oldClusterName
return nil
}
// AliasCluster alias cluster
func AliasCluster(ctx context.Context, cli client.Client, clusterName string, aliasName string) error {
if clusterName == ClusterLocalName {
return ErrReservedLocalClusterName
}
vc, err := GetVirtualCluster(ctx, cli, clusterName)
if err != nil {
return err
}
setClusterAlias(vc.Object, aliasName)
return cli.Update(ctx, vc.Object)
}
// ensureClusterNotExists will check the cluster is not existed in control plane
func ensureClusterNotExists(ctx context.Context, c client.Client, clusterName string) error {
_, err := GetVirtualCluster(ctx, c, clusterName)

View File

@@ -18,6 +18,7 @@ package multicluster
import (
"context"
"fmt"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
@@ -38,6 +39,7 @@ import (
// like cluster secret or ocm managed cluster
type VirtualCluster struct {
Name string
Alias string
Type v1alpha1.CredentialType
EndPoint string
Accepted bool
@@ -46,6 +48,30 @@ type VirtualCluster struct {
Object client.Object
}
// FullName the name with alias if available
func (vc *VirtualCluster) FullName() string {
if vc.Alias != "" {
return fmt.Sprintf("%s (%s)", vc.Name, vc.Alias)
}
return vc.Name
}
func getClusterAlias(o client.Object) string {
if annots := o.GetAnnotations(); annots != nil {
return annots[types.AnnotationClusterAlias]
}
return ""
}
func setClusterAlias(o client.Object, alias string) {
annots := o.GetAnnotations()
if annots == nil {
annots = map[string]string{}
}
annots[types.AnnotationClusterAlias] = alias
o.SetAnnotations(annots)
}
// NewVirtualClusterFromLocal return virtual cluster corresponding to local cluster
func NewVirtualClusterFromLocal() *VirtualCluster {
return &VirtualCluster{
@@ -74,6 +100,7 @@ func NewVirtualClusterFromSecret(secret *corev1.Secret) (*VirtualCluster, error)
}
return &VirtualCluster{
Name: secret.Name,
Alias: getClusterAlias(secret),
Type: v1alpha1.CredentialType(credType),
EndPoint: endpoint,
Accepted: true,
@@ -90,6 +117,7 @@ func NewVirtualClusterFromManagedCluster(managedCluster *clusterv1.ManagedCluste
}
return &VirtualCluster{
Name: managedCluster.Name,
Alias: getClusterAlias(managedCluster),
Type: types.CredentialTypeOCMManagedCluster,
EndPoint: types.ClusterBlankEndpoint,
Accepted: managedCluster.Spec.HubAcceptsClient,
@@ -205,3 +233,37 @@ func FindVirtualClustersByLabels(ctx context.Context, c client.Client, labels ma
}
return clusters, nil
}
// ClusterMapper mapper for clusters
type ClusterMapper interface {
GetCluster(string) *VirtualCluster
GetClusterFullName(string) string
}
type clusterMapper map[string]*VirtualCluster
// GetCluster .
func (cm clusterMapper) GetCluster(cluster string) *VirtualCluster {
return cm[cluster]
}
// GetClusterFullName .
func (cm clusterMapper) GetClusterFullName(cluster string) string {
if vc := cm.GetCluster(cluster); vc != nil {
return vc.FullName()
}
return ""
}
// NewClusterMapper load all clusters and return the mapper
func NewClusterMapper(ctx context.Context, c client.Client) (ClusterMapper, error) {
cm := clusterMapper(make(map[string]*VirtualCluster))
clusters, err := ListVirtualClusters(ctx, c)
if err != nil {
return nil, err
}
for i := range clusters {
cm[clusters[i].Name] = &clusters[i]
}
return cm, nil
}

View File

@@ -17,6 +17,8 @@ limitations under the License.
package oam
import (
"time"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@@ -49,3 +51,53 @@ func GetPublishVersion(o client.Object) string {
}
return ""
}
// GetDeployVersion get DeployVersion from object
func GetDeployVersion(o client.Object) string {
if annotations := o.GetAnnotations(); annotations != nil {
return annotations[AnnotationDeployVersion]
}
return ""
}
// GetLastAppliedTime .
func GetLastAppliedTime(o client.Object) time.Time {
if annotations := o.GetAnnotations(); annotations != nil {
s := annotations[AnnotationLastAppliedTime]
if t, err := time.Parse(time.RFC3339, s); err == nil {
return t
}
}
return o.GetCreationTimestamp().Time
}
// SetPublishVersion set PublishVersion for object
func SetPublishVersion(o client.Object, publishVersion string) {
annotations := o.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[AnnotationPublishVersion] = publishVersion
o.SetAnnotations(annotations)
}
// GetControllerRequirement get ControllerRequirement from object
func GetControllerRequirement(o client.Object) string {
if annotations := o.GetAnnotations(); annotations != nil {
return annotations[AnnotationControllerRequirement]
}
return ""
}
// SetControllerRequirement set ControllerRequirement for object
func SetControllerRequirement(o client.Object, controllerRequirement string) {
annotations := o.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[AnnotationControllerRequirement] = controllerRequirement
if controllerRequirement == "" {
delete(annotations, AnnotationControllerRequirement)
}
o.SetAnnotations(annotations)
}

View File

@@ -90,6 +90,12 @@ const (
// LabelRuntimeNamespaceUsage mark the usage of the namespace in runtime cluster.
// A control plane cluster can also be used as runtime cluster
LabelRuntimeNamespaceUsage = "usage.oam.dev/runtime"
// LabelConfigType means the config type
LabelConfigType = "config.oam.dev/type"
// LabelProject recorde the project the resource belong to
LabelProject = "core.oam.dev/project"
)
const (
@@ -114,6 +120,9 @@ const (
// resource for use in a three way diff during a patching apply
AnnotationLastAppliedConfig = "app.oam.dev/last-applied-configuration"
// AnnotationLastAppliedTime indicates the last applied time
AnnotationLastAppliedTime = "app.oam.dev/last-applied-time"
// AnnotationAppRollout indicates that the application is still rolling out
// the application controller should treat it differently
AnnotationAppRollout = "app.oam.dev/rollout-template"

View File

@@ -17,10 +17,17 @@ limitations under the License.
package policy
import (
"context"
"github.com/pkg/errors"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/utils"
)
// GetClusterLabelSelectorInTopology get cluster label selector in topology policy spec
@@ -33,3 +40,57 @@ func GetClusterLabelSelectorInTopology(topology *v1alpha1.TopologyPolicySpec) ma
}
return nil
}
// GetPlacementsFromTopologyPolicies get placements from topology policies with provided client
func GetPlacementsFromTopologyPolicies(ctx context.Context, cli client.Client, app *v1beta1.Application, policies []v1beta1.AppPolicy, allowCrossNamespace bool) ([]v1alpha1.PlacementDecision, error) {
var placements []v1alpha1.PlacementDecision
placementMap := map[string]struct{}{}
addCluster := func(cluster string, ns string, validateCluster bool) error {
if validateCluster {
if _, e := multicluster.GetVirtualCluster(ctx, cli, cluster); e != nil {
return errors.Wrapf(e, "failed to get cluster %s", cluster)
}
}
if !allowCrossNamespace && (ns != app.GetNamespace() && ns != "") {
return errors.Errorf("cannot cross namespace")
}
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
name := placement.String()
if _, found := placementMap[name]; !found {
placementMap[name] = struct{}{}
placements = append(placements, placement)
}
return nil
}
for _, policy := range policies {
if policy.Type == v1alpha1.TopologyPolicyType {
topologySpec := &v1alpha1.TopologyPolicySpec{}
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
return nil, errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
}
clusterLabelSelector := GetClusterLabelSelectorInTopology(topologySpec)
switch {
case topologySpec.Clusters != nil:
for _, cluster := range topologySpec.Clusters {
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
return nil, err
}
}
case clusterLabelSelector != nil:
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), cli, clusterLabelSelector)
if err != nil {
return nil, errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
}
if len(clusters) == 0 {
return nil, errors.New("failed to find any cluster matches given labels")
}
for _, cluster := range clusters {
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
return nil, err
}
}
}
}
}
return placements, nil
}

412
pkg/resourcetracker/tree.go Normal file
View File

@@ -0,0 +1,412 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcetracker
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"sort"
"strings"
"github.com/fatih/color"
"github.com/gosuri/uitable"
"github.com/gosuri/uitable/util/strutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
// ResourceDetailRetriever retriever to get details for resource
type ResourceDetailRetriever func(*resourceRow, string) error
// ResourceTreePrintOptions print options for resource tree
type ResourceTreePrintOptions struct {
DetailRetriever ResourceDetailRetriever
multicluster.ClusterMapper
// MaxWidth if set, the detail part will auto wrap
MaxWidth *int
// Format for details
Format string
}
const (
resourceRowStatusUpdated = "updated"
resourceRowStatusNotDeployed = "not-deployed"
resourceRowStatusOutdated = "outdated"
)
type resourceRow struct {
mr *v1beta1.ManagedResource
status string
cluster string
namespace string
resourceName string
connectClusterUp bool
connectClusterDown bool
connectNamespaceUp bool
connectNamespaceDown bool
applyTime string
details string
}
func (options *ResourceTreePrintOptions) loadResourceRows(currentRT *v1beta1.ResourceTracker, historyRT []*v1beta1.ResourceTracker) []*resourceRow {
var rows []*resourceRow
if currentRT != nil {
for _, mr := range currentRT.Spec.ManagedResources {
if mr.Deleted {
continue
}
rows = append(rows, &resourceRow{
mr: mr.DeepCopy(),
status: resourceRowStatusUpdated,
})
}
}
for _, rt := range historyRT {
for _, mr := range rt.Spec.ManagedResources {
var matchedRow *resourceRow
for _, row := range rows {
if row.mr.ResourceKey() == mr.ResourceKey() {
matchedRow = row
}
}
if matchedRow == nil {
rows = append(rows, &resourceRow{
mr: mr.DeepCopy(),
status: resourceRowStatusOutdated,
})
}
}
}
return rows
}
func (options *ResourceTreePrintOptions) sortRows(rows []*resourceRow) {
sort.Slice(rows, func(i, j int) bool {
if rows[i].mr.Cluster != rows[j].mr.Cluster {
return rows[i].mr.Cluster < rows[j].mr.Cluster
}
if rows[i].mr.Namespace != rows[j].mr.Namespace {
return rows[i].mr.Namespace < rows[j].mr.Namespace
}
return rows[i].mr.ResourceKey() < rows[j].mr.ResourceKey()
})
}
func (options *ResourceTreePrintOptions) fillResourceRows(rows []*resourceRow, colsWidth []int) {
for i := 0; i < 4; i++ {
colsWidth[i] = 10
}
connectLastRow := func(rowIdx int, cluster bool, namespace bool) {
rows[rowIdx].connectClusterUp = cluster
rows[rowIdx-1].connectClusterDown = cluster
rows[rowIdx].connectNamespaceUp = namespace
rows[rowIdx-1].connectNamespaceDown = namespace
}
for rowIdx, row := range rows {
if row.mr.Cluster == "" {
row.mr.Cluster = multicluster.ClusterLocalName
}
if row.mr.Namespace == "" {
row.mr.Namespace = "-"
}
row.cluster, row.namespace, row.resourceName = options.ClusterMapper.GetClusterFullName(row.mr.Cluster), row.mr.Namespace, fmt.Sprintf("%s/%s", row.mr.Kind, row.mr.Name)
if row.status == resourceRowStatusNotDeployed {
row.resourceName = "-"
}
if rowIdx > 0 && row.mr.Cluster == rows[rowIdx-1].mr.Cluster {
connectLastRow(rowIdx, true, false)
row.cluster = ""
if row.mr.Namespace == rows[rowIdx-1].mr.Namespace {
connectLastRow(rowIdx, true, true)
row.namespace = ""
}
}
for i, val := range []string{row.cluster, row.namespace, row.resourceName, row.status} {
if size := len(val) + 1; size > colsWidth[i] {
colsWidth[i] = size
}
}
}
for rowIdx := len(rows); rowIdx >= 1; rowIdx-- {
if rowIdx == len(rows) || rows[rowIdx].cluster != "" {
for j := rowIdx - 1; j >= 1; j-- {
if rows[j].cluster == "" && rows[j].namespace == "" {
connectLastRow(j, false, rows[j].connectNamespaceUp)
if j+1 < len(rows) {
connectLastRow(j+1, false, rows[j+1].connectNamespaceUp)
}
continue
}
break
}
}
}
// add extra spaces for tree connectors
colsWidth[0] += 4
colsWidth[1] += 4
}
const (
applyTimeWidth = 20
detailMinWidth = 20
)
func (options *ResourceTreePrintOptions) _getWidthForDetails(colsWidth []int) int {
detailWidth := 0
if options.MaxWidth == nil {
return math.MaxInt
}
detailWidth = *options.MaxWidth - applyTimeWidth
for _, width := range colsWidth {
detailWidth -= width
}
// if the space for details exceeds the max allowed width, give up wrapping lines
if detailWidth < detailMinWidth {
detailWidth = math.MaxInt
}
return detailWidth
}
func (options *ResourceTreePrintOptions) _wrapDetails(detail string, width int) (lines []string) {
for _, row := range strings.Split(detail, "\n") {
var sb strings.Builder
row = strings.ReplaceAll(row, "\t", " ")
sep := " "
if options.Format == "raw" {
sep = "\n"
}
for _, token := range strings.Split(row, sep) {
if sb.Len()+len(token)+2 <= width {
if sb.Len() > 0 {
sb.WriteString(sep)
}
sb.WriteString(token)
} else {
if sb.Len() > 0 {
lines = append(lines, sb.String())
sb.Reset()
}
offset := 0
for {
if offset+width > len(token) {
break
}
lines = append(lines, token[offset:offset+width])
offset += width
}
sb.WriteString(token[offset:])
}
}
if sb.Len() > 0 {
lines = append(lines, sb.String())
}
}
if len(lines) == 0 {
lines = []string{""}
}
return lines
}
func (options *ResourceTreePrintOptions) writeResourceTree(writer io.Writer, rows []*resourceRow, colsWidth []int) {
writePaddedString := func(sb *strings.Builder, head string, tail string, width int) {
sb.WriteString(head)
for c := strutil.StringWidth(head) + strutil.StringWidth(tail); c < width; c++ {
sb.WriteByte(' ')
}
sb.WriteString(tail)
}
var headerWriter strings.Builder
for colIdx, colName := range []string{"CLUSTER", "NAMESPACE", "RESOURCE", "STATUS"} {
writePaddedString(&headerWriter, colName, "", colsWidth[colIdx])
}
if options.DetailRetriever != nil {
writePaddedString(&headerWriter, "APPLY_TIME", "", applyTimeWidth)
_, _ = writer.Write([]byte(headerWriter.String() + "DETAIL" + "\n"))
} else {
_, _ = writer.Write([]byte(headerWriter.String() + "\n"))
}
connectorColorizer := color.WhiteString
outdatedColorizer := color.WhiteString
detailWidth := options._getWidthForDetails(colsWidth)
for _, row := range rows {
if options.DetailRetriever != nil && row.status != resourceRowStatusNotDeployed {
if err := options.DetailRetriever(row, options.Format); err != nil {
row.details = "Error: " + err.Error()
}
}
for lineIdx, line := range options._wrapDetails(row.details, detailWidth) {
var sb strings.Builder
rscName, rscStatus, applyTime := row.resourceName, row.status, row.applyTime
if row.status != resourceRowStatusUpdated {
rscName, rscStatus, applyTime, line = outdatedColorizer(row.resourceName), outdatedColorizer(row.status), outdatedColorizer(applyTime), outdatedColorizer(line)
}
if lineIdx == 0 {
writePaddedString(&sb, row.cluster, connectorColorizer(utils.GetBoxDrawingString(row.connectClusterUp, row.connectClusterDown, row.cluster != "", row.namespace != "", 1, 1))+" ", colsWidth[0])
writePaddedString(&sb, row.namespace, connectorColorizer(utils.GetBoxDrawingString(row.connectNamespaceUp, row.connectNamespaceDown, row.namespace != "", true, 1, 1))+" ", colsWidth[1])
writePaddedString(&sb, rscName, "", colsWidth[2])
writePaddedString(&sb, rscStatus, "", colsWidth[3])
} else {
writePaddedString(&sb, "", connectorColorizer(utils.GetBoxDrawingString(row.connectClusterDown, row.connectClusterDown, false, false, 1, 1))+" ", colsWidth[0])
writePaddedString(&sb, "", connectorColorizer(utils.GetBoxDrawingString(row.connectNamespaceDown, row.connectNamespaceDown, false, false, 1, 1))+" ", colsWidth[1])
writePaddedString(&sb, "", "", colsWidth[2])
writePaddedString(&sb, "", "", colsWidth[3])
}
if options.DetailRetriever != nil {
if lineIdx != 0 {
applyTime = ""
}
writePaddedString(&sb, applyTime, "", applyTimeWidth)
}
_, _ = writer.Write([]byte(sb.String() + line + "\n"))
}
}
}
func (options *ResourceTreePrintOptions) addNonExistingPlacementToRows(placements []v1alpha1.PlacementDecision, rows []*resourceRow) []*resourceRow {
existingClusters := map[string]struct{}{}
for _, row := range rows {
existingClusters[row.mr.Cluster] = struct{}{}
}
for _, p := range placements {
if _, found := existingClusters[p.Cluster]; !found {
rows = append(rows, &resourceRow{
mr: &v1beta1.ManagedResource{
ClusterObjectReference: apicommon.ClusterObjectReference{Cluster: p.Cluster},
},
status: resourceRowStatusNotDeployed,
})
}
}
return rows
}
// PrintResourceTree print resource tree to writer
func (options *ResourceTreePrintOptions) PrintResourceTree(writer io.Writer, currentPlacements []v1alpha1.PlacementDecision, currentRT *v1beta1.ResourceTracker, historyRT []*v1beta1.ResourceTracker) {
rows := options.loadResourceRows(currentRT, historyRT)
rows = options.addNonExistingPlacementToRows(currentPlacements, rows)
options.sortRows(rows)
colsWidth := make([]int, 4)
options.fillResourceRows(rows, colsWidth)
options.writeResourceTree(writer, rows, colsWidth)
}
type tableRoundTripper struct {
rt http.RoundTripper
}
// RoundTrip mutate the request header to let apiserver return table data
func (rt tableRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("Accept", strings.Join([]string{
fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
"application/json",
}, ","))
return rt.rt.RoundTrip(req)
}
// RetrieveKubeCtlGetMessageGenerator get details like kubectl get
func RetrieveKubeCtlGetMessageGenerator(cfg *rest.Config) (ResourceDetailRetriever, error) {
cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return tableRoundTripper{rt: rt}
})
cli, err := client.New(cfg, client.Options{Scheme: common.Scheme})
if err != nil {
return nil, err
}
return func(row *resourceRow, format string) error {
mr := row.mr
un := &unstructured.Unstructured{}
un.SetAPIVersion(mr.APIVersion)
un.SetKind(mr.Kind)
if err = cli.Get(multicluster.ContextWithClusterName(context.Background(), mr.Cluster), mr.NamespacedName(), un); err != nil {
return err
}
un.SetAPIVersion(metav1.SchemeGroupVersion.String())
un.SetKind("Table")
table := &metav1.Table{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, table); err != nil {
return err
}
obj := &unstructured.Unstructured{}
if err := json.Unmarshal(table.Rows[0].Object.Raw, obj); err == nil {
row.applyTime = oam.GetLastAppliedTime(obj).Format("2006-01-02 15:04:05")
}
switch format {
case "raw":
raw := table.Rows[0].Object.Raw
if annotations := obj.GetAnnotations(); annotations != nil && annotations[oam.AnnotationLastAppliedConfig] != "" {
raw = []byte(annotations[oam.AnnotationLastAppliedConfig])
}
bs, err := yaml.JSONToYAML(raw)
if err != nil {
return err
}
row.details = string(bs)
case "table":
tab := uitable.New()
var tabHeaders, tabValues []interface{}
for cid, column := range table.ColumnDefinitions {
if column.Name == "Name" || column.Name == "Created At" || column.Priority != 0 {
continue
}
tabHeaders = append(tabHeaders, column.Name)
tabValues = append(tabValues, table.Rows[0].Cells[cid])
}
tab.AddRow(tabHeaders...)
tab.AddRow(tabValues...)
row.details = tab.String()
default: // inline / wide / list
var entries []string
for cid, column := range table.ColumnDefinitions {
if column.Name == "Name" || column.Name == "Created At" || (format == "inline" && column.Priority != 0) {
continue
}
entries = append(entries, fmt.Sprintf("%s: %v", column.Name, table.Rows[0].Cells[cid]))
}
if format == "inline" || format == "wide" {
row.details = strings.Join(entries, " ")
} else {
row.details = strings.Join(entries, "\n")
}
}
return nil
}, nil
}

View File

@@ -0,0 +1,48 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resourcetracker
import (
"math"
"testing"
"github.com/stretchr/testify/require"
"k8s.io/utils/pointer"
)
func TestResourceTreePrintOption_getWidthForDetails(t *testing.T) {
r := require.New(t)
options := &ResourceTreePrintOptions{}
r.Equal(math.MaxInt, options._getWidthForDetails(nil))
options.MaxWidth = pointer.Int(50 + applyTimeWidth)
r.Equal(30, options._getWidthForDetails([]int{10, 10}))
r.Equal(math.MaxInt, options._getWidthForDetails([]int{20, 20}))
}
func TestResourceTreePrintOptions_wrapDetails(t *testing.T) {
r := require.New(t)
options := &ResourceTreePrintOptions{}
detail := "test-key: test-val\ttest-data: test-val\ntest-next-line: text-next-value test-long-key: test long long long long value test-append: test-append-val"
r.Equal(
[]string{
"test-key: test-val test-data: test-val",
"test-next-line: text-next-value",
"test-long-key: test long long long long ",
"value test-append: test-append-val",
},
options._wrapDetails(detail, 40))
}

View File

@@ -41,6 +41,10 @@
uid?: string
apiVersion?: string
resourceVersion?: string
publishVersion?: string
deployVersion?: string
revision?: string
latest?: bool
}]
...
}

View File

@@ -18,6 +18,7 @@ package apply
import (
"encoding/json"
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
@@ -147,6 +148,7 @@ func getModifiedConfiguration(obj runtime.Object, updateAnnotation bool) ([]byte
// restore original annotations back to the object
annots[oam.AnnotationLastAppliedConfig] = original
annots[oam.AnnotationLastAppliedTime] = time.Now().Format(time.RFC3339)
_ = metadataAccessor.SetAnnotations(obj, annots)
return modified, nil
}

View File

@@ -102,6 +102,12 @@ func init() {
// +kubebuilder:scaffold:scheme
}
// HTTPOption define the https options
type HTTPOption struct {
Username string
Password string
}
// InitBaseRestConfig will return reset config for create controller runtime client
func InitBaseRestConfig() (Args, error) {
args := Args{
@@ -134,13 +140,16 @@ func GetClient() (client.Client, error) {
return nil, errors.New("client not set, call SetGlobalClient first")
}
// HTTPGet will send GET http request with context
func HTTPGet(ctx context.Context, url string) ([]byte, error) {
// HTTPGetWithOption use HTTP option and default client to send get request
func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
// Change NewRequest to NewRequestWithContext and pass context it
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 {
req.SetBasicAuth(opts.Username, opts.Password)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
@@ -279,13 +288,13 @@ func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error {
// ClusterObject2Map convert ClusterObjectReference to a readable map
func ClusterObject2Map(refs []common.ClusterObjectReference) map[string]string {
clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Component: %s | Kind: %s"
clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Kind: %s | Name: %s"
objs := make(map[string]string, len(refs))
for _, r := range refs {
if r.Cluster == "" {
r.Cluster = "local"
}
objs[r.Cluster+"/"+r.Namespace+"/"+r.Name+"/"+r.Kind] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Name, r.Kind)
objs[r.Cluster+"/"+r.Namespace+"/"+r.Name+"/"+r.Kind] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Kind, r.Name)
}
return objs
}
@@ -312,6 +321,19 @@ func clusterObjectReferenceTypeFilterGenerator(allowedKinds ...string) clusterOb
var isWorkloadClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment", "StatefulSet", "CloneSet", "Job", "Configuration")
var isPortForwardEndpointClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment",
"StatefulSet", "CloneSet", "Job", "Service", "HelmRelease")
var resourceNameClusterObjectReferenceFilter = func(resourceName []string) clusterObjectReferenceFilter {
return func(reference common.ClusterObjectReference) bool {
if len(resourceName) == 0 {
return true
}
for _, r := range resourceName {
if r == reference.Name {
return true
}
}
return false
}
}
func filterResource(inputs []common.ClusterObjectReference, filters ...clusterObjectReferenceFilter) (outputs []common.ClusterObjectReference) {
for _, item := range inputs {
@@ -416,16 +438,32 @@ func filterClusterObjectRefFromAddonObservability(resources []common.ClusterObje
return resources
}
func removeEmptyString(items []string) []string {
r := []string{}
for _, i := range items {
if i != "" {
r = append(r, i)
}
}
return r
}
// AskToChooseOneEnvResource will ask users to select one applied resource of the application if more than one
// resource is a map for component to applied resources
// return the selected ClusterObjectReference
func AskToChooseOneEnvResource(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
return askToChooseOneResource(app, isWorkloadClusterObjectReferenceFilter)
func AskToChooseOneEnvResource(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
filters := []clusterObjectReferenceFilter{isWorkloadClusterObjectReferenceFilter}
_resourceName := removeEmptyString(resourceName)
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
return askToChooseOneResource(app, filters...)
}
// AskToChooseOnePortForwardEndpoint will ask user to select one applied resource as port forward endpoint
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
return askToChooseOneResource(app, isPortForwardEndpointClusterObjectReferenceFilter)
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
filters := []clusterObjectReferenceFilter{isPortForwardEndpointClusterObjectReferenceFilter}
_resourceName := removeEmptyString(resourceName)
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
return askToChooseOneResource(app, filters...)
}
func askToChooseOneInApplication(category string, options []string) (decision string, err error) {

View File

@@ -32,6 +32,7 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
@@ -74,7 +75,7 @@ func TestHTTPGet(t *testing.T) {
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := HTTPGet(ctx, tc.url)
got, err := HTTPGetWithOption(ctx, tc.url, nil)
if tc.want.errStr != "" {
if diff := cmp.Diff(tc.want.errStr, err.Error(), test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nHTTPGet(...): -want error, +got error:\n%s", tc.reason, diff)
@@ -89,6 +90,91 @@ func TestHTTPGet(t *testing.T) {
}
func TestHTTPGetWithOption(t *testing.T) {
type want struct {
data string
}
var ctx = context.Background()
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok {
w.Write([]byte("Error parsing basic auth"))
w.WriteHeader(401)
return
}
if u != "test-user" {
w.Write([]byte(fmt.Sprintf("Username provided is incorrect: %s", u)))
w.WriteHeader(401)
return
}
if p != "test-pass" {
w.Write([]byte(fmt.Sprintf("Password provided is incorrect: %s", p)))
w.WriteHeader(401)
return
}
w.Write([]byte("correct password"))
w.WriteHeader(200)
}))
defer testServer.Close()
cases := map[string]struct {
opts *HTTPOption
url string
want want
}{
"without auth case": {
opts: nil,
url: testServer.URL,
want: want{
data: "Error parsing basic auth",
},
},
"error user name case": {
opts: &HTTPOption{
Username: "no-user",
Password: "test-pass",
},
url: testServer.URL,
want: want{
data: "Username provided is incorrect: no-user",
},
},
"error password case": {
opts: &HTTPOption{
Username: "test-user",
Password: "error-pass",
},
url: testServer.URL,
want: want{
data: "Password provided is incorrect: error-pass",
},
},
"correct password case": {
opts: &HTTPOption{
Username: "test-user",
Password: "test-pass",
},
url: testServer.URL,
want: want{
data: "correct password",
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got, err := HTTPGetWithOption(ctx, tc.url, tc.opts)
assert.NoError(t, err)
if diff := cmp.Diff(tc.want.data, string(got)); diff != "" {
t.Errorf("\n%s\nHTTPGet(...): -want, +got:\n%s", tc.want.data, diff)
}
})
}
}
func TestGetCUEParameterValue(t *testing.T) {
type want struct {
err error
@@ -369,3 +455,56 @@ func TestFilterClusterObjectRefFromAddonObservability(t *testing.T) {
assert.Equal(t, "Service", res[0].Kind)
assert.Equal(t, "v1", res[0].APIVersion)
}
func TestResourceNameClusterObjectReferenceFilter(t *testing.T) {
fooRef := common.ClusterObjectReference{
ObjectReference: corev1.ObjectReference{
Name: "foo",
}}
barRef := common.ClusterObjectReference{
ObjectReference: corev1.ObjectReference{
Name: "bar",
}}
bazRef := common.ClusterObjectReference{
ObjectReference: corev1.ObjectReference{
Name: "baz",
}}
var refs = []common.ClusterObjectReference{
fooRef, barRef, bazRef,
}
testCases := []struct {
caseName string
filter clusterObjectReferenceFilter
filteredRefs []common.ClusterObjectReference
}{
{
caseName: "filter one resource",
filter: resourceNameClusterObjectReferenceFilter([]string{"foo"}),
filteredRefs: []common.ClusterObjectReference{fooRef},
},
{
caseName: "not filter resources",
filter: resourceNameClusterObjectReferenceFilter([]string{}),
filteredRefs: []common.ClusterObjectReference{fooRef, barRef, bazRef},
},
{
caseName: "filter multi resources",
filter: resourceNameClusterObjectReferenceFilter([]string{"foo", "bar"}),
filteredRefs: []common.ClusterObjectReference{fooRef, barRef},
},
}
for _, c := range testCases {
filteredResource := filterResource(refs, c.filter)
assert.Equal(t, c.filteredRefs, filteredResource, c.caseName)
}
}
func TestRemoveEmptyString(t *testing.T) {
withEmpty := []string{"foo", "bar", "", "baz", ""}
noEmpty := removeEmptyString(withEmpty)
assert.Equal(t, len(noEmpty), 3)
for _, s := range noEmpty {
assert.NotEmpty(t, s)
}
}

View File

@@ -17,12 +17,17 @@ limitations under the License.
package helm
import (
"context"
"fmt"
"io"
"log"
"os"
"strings"
v1 "k8s.io/api/core/v1"
types2 "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
@@ -268,3 +273,13 @@ func GetChart(client *action.Install, name string) (*chart.Chart, error) {
func InstallHelmChart(ioStreams cmdutil.IOStreams, c types.Chart) error {
return Install(ioStreams, c.Repo, c.URL, c.Name, c.Version, c.Namespace, c.Name, c.Values)
}
// SetBasicAuthInfo will read username and password from secret return a httpOption that contain these info.
func SetBasicAuthInfo(ctx context.Context, k8sClient client.Client, secretRef types2.NamespacedName) (*common.HTTPOption, error) {
sec := v1.Secret{}
err := k8sClient.Get(ctx, secretRef, &sec)
if err != nil {
return nil, err
}
return &common.HTTPOption{Username: string(sec.Data["username"]), Password: string(sec.Data["password"])}, nil
}

View File

@@ -32,8 +32,10 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
relutil "helm.sh/helm/v3/pkg/releaseutil"
"helm.sh/helm/v3/pkg/repo"
"helm.sh/helm/v3/pkg/storage"
"helm.sh/helm/v3/pkg/storage/driver"
@@ -73,11 +75,11 @@ func NewHelperWithCache() *Helper {
}
// LoadCharts load helm chart from local or remote
func (h *Helper) LoadCharts(chartRepoURL string) (*chart.Chart, error) {
func (h *Helper) LoadCharts(chartRepoURL string, opts *common.HTTPOption) (*chart.Chart, error) {
var err error
var chart *chart.Chart
if utils.IsValidURL(chartRepoURL) {
chartBytes, err := common.HTTPGet(context.Background(), chartRepoURL)
chartBytes, err := common.HTTPGetWithOption(context.Background(), chartRepoURL, opts)
if err != nil {
return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error())
}
@@ -118,7 +120,7 @@ func (h *Helper) UpgradeChart(ch *chart.Chart, releaseName, namespace string, va
var newRelease *release.Release
timeoutInMinutes := 18
releases, err := histClient.Run(releaseName)
if err != nil {
if err != nil || len(releases) == 0 {
if errors.Is(err, driver.ErrReleaseNotFound) {
// fresh install
install := action.NewInstall(cfg)
@@ -139,6 +141,20 @@ func (h *Helper) UpgradeChart(ch *chart.Chart, releaseName, namespace string, va
r.Info.Status == release.StatusPendingRollback {
return nil, fmt.Errorf("previous installation (e.g., using vela install or helm upgrade) is still in progress. Please try again in %d minutes", timeoutInMinutes)
}
}
// merge un-existing values into the values as user-input, because the helm chart upgrade didn't handle the new default values in the chart.
if config.ReuseValues {
// sort will sort the release by revision from old to new
relutil.SortByRevision(releases)
rel := releases[len(releases)-1]
// merge new chart values into old values, the values of old chart has the high priority
mergedWithNewValues := chartutil.CoalesceTables(rel.Chart.Values, ch.Values)
// merge the chart with the released chart config but follow the old config
mergeWithConfigs := chartutil.CoalesceTables(rel.Config, mergedWithNewValues)
// merge new values as the user input, follow the new user input for --set
values = chartutil.CoalesceTables(values, mergeWithConfigs)
}
// overwrite existing installation
@@ -178,8 +194,8 @@ func (h *Helper) UninstallRelease(releaseName, namespace string, config *rest.Co
}
// ListVersions list available versions from repo
func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool) (repo.ChartVersions, error) {
i, err := h.GetIndexInfo(repoURL, skipCache)
func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool, opts *common.HTTPOption) (repo.ChartVersions, error) {
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
if err != nil {
return nil, err
}
@@ -187,7 +203,7 @@ func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool)
}
// GetIndexInfo get index.yaml form given repo url
func (h *Helper) GetIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile, error) {
func (h *Helper) GetIndexInfo(repoURL string, skipCache bool, opts *common.HTTPOption) (*repo.IndexFile, error) {
if h.cache != nil && !skipCache {
if i := h.cache.Get(fmt.Sprintf(repoPatten, repoURL)); i != nil {
return i.(*repo.IndexFile), nil
@@ -202,7 +218,7 @@ func (h *Helper) GetIndexInfo(repoURL string, skipCache bool) (*repo.IndexFile,
parsedURL.RawPath = path.Join(parsedURL.RawPath, "index.yaml")
parsedURL.Path = path.Join(parsedURL.Path, "index.yaml")
indexURL := parsedURL.String()
body, err = common.HTTPGet(context.Background(), indexURL)
body, err = common.HTTPGetWithOption(context.Background(), indexURL, opts)
if err != nil {
return nil, fmt.Errorf("download index file from %s failure %w", repoURL, err)
}
@@ -284,8 +300,8 @@ func newActionConfig(config *rest.Config, namespace string, showDetail bool, log
}
// ListChartsFromRepo list available helm charts in a repo
func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool) ([]string, error) {
i, err := h.GetIndexInfo(repoURL, skipCache)
func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool, opts *common.HTTPOption) ([]string, error) {
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
if err != nil {
return nil, err
}
@@ -299,13 +315,13 @@ func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool) ([]string, e
}
// GetValuesFromChart will extract the parameter from a helm chart
func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool) (map[string]interface{}, error) {
func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool, opts *common.HTTPOption) (map[string]interface{}, error) {
if h.cache != nil && !skipCache {
if v := h.cache.Get(fmt.Sprintf(valuesPatten, repoURL, chartName, version)); v != nil {
return v.(map[string]interface{}), nil
}
}
i, err := h.GetIndexInfo(repoURL, skipCache)
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
if err != nil {
return nil, err
}
@@ -320,7 +336,7 @@ func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version st
}
}
for _, u := range urls {
c, err := h.LoadCharts(u)
c, err := h.LoadCharts(u, opts)
if err != nil {
continue
}

View File

@@ -17,12 +17,20 @@ limitations under the License.
package helm
import (
"context"
"os"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
types2 "github.com/oam-dev/kubevela/apis/types"
util2 "github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/util"
)
@@ -30,7 +38,7 @@ var _ = Describe("Test helm helper", func() {
It("Test LoadCharts ", func() {
helper := NewHelper()
chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz")
chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz", nil)
Expect(err).Should(BeNil())
Expect(chart).ShouldNot(BeNil())
Expect(chart.Metadata).ShouldNot(BeNil())
@@ -39,7 +47,7 @@ var _ = Describe("Test helm helper", func() {
It("Test UpgradeChart", func() {
helper := NewHelper()
chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz")
chart, err := helper.LoadCharts("./testdata/autoscalertrait-0.1.0.tgz", nil)
Expect(err).Should(BeNil())
release, err := helper.UpgradeChart(chart, "autoscalertrait", "default", nil, UpgradeChartOptions{
Config: cfg,
@@ -60,15 +68,57 @@ var _ = Describe("Test helm helper", func() {
It("Test ListVersions ", func() {
helper := NewHelper()
versions, err := helper.ListVersions("./testdata", "autoscalertrait", true)
versions, err := helper.ListVersions("./testdata", "autoscalertrait", true, nil)
Expect(err).Should(BeNil())
Expect(cmp.Diff(len(versions), 2)).Should(BeEmpty())
})
It("Test getValues from chart", func() {
helper := NewHelper()
values, err := helper.GetValuesFromChart("./testdata", "autoscalertrait", "0.2.0", true)
values, err := helper.GetValuesFromChart("./testdata", "autoscalertrait", "0.2.0", true, nil)
Expect(err).Should(BeNil())
Expect(values).ShouldNot(BeEmpty())
})
})
var _ = Describe("Test helm associated func", func() {
ctx := context.Background()
var aSec v1.Secret
BeforeEach(func() {
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: v12.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util2.AlreadyExistMatcher{}))
aSec = v1.Secret{}
Expect(yaml.Unmarshal([]byte(authSecret), &aSec)).Should(BeNil())
Expect(k8sClient.Create(ctx, &aSec)).Should(SatisfyAny(BeNil(), util2.AlreadyExistMatcher{}))
})
It("Test auth info secret func", func() {
opts, err := SetBasicAuthInfo(context.Background(), k8sClient, types.NamespacedName{Namespace: types2.DefaultKubeVelaNS, Name: "auth-secret"})
Expect(err).Should(BeNil())
Expect(opts.Username).Should(BeEquivalentTo("admin"))
Expect(opts.Password).Should(BeEquivalentTo("admin"))
})
It("Test auth info secret func", func() {
_, err := SetBasicAuthInfo(context.Background(), k8sClient, types.NamespacedName{Namespace: types2.DefaultKubeVelaNS, Name: "auth-secret-1"})
Expect(err).ShouldNot(BeNil())
})
})
var (
authSecret = `
apiVersion: v1
kind: Secret
metadata:
name: auth-secret
namespace: vela-system
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/project: my-project-1
stringData:
url: https://kedacore.github.io/charts
username: admin
password: admin
type: Opaque
`
)

View File

@@ -21,14 +21,18 @@ import (
"testing"
"time"
"github.com/oam-dev/kubevela/pkg/utils/common"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var _ = BeforeSuite(func(done Done) {
@@ -47,6 +51,9 @@ var _ = BeforeSuite(func(done Done) {
Expect(err).ShouldNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 240)

View File

@@ -19,6 +19,7 @@ package utils
import (
"reflect"
"sort"
"strings"
)
// StringsContain strings contain
@@ -85,3 +86,57 @@ func MapKey2Array(source map[string]string) []string {
}
return list
}
// GetBoxDrawingString get line drawing string, see https://en.wikipedia.org/wiki/Box-drawing_character
// nolint:gocyclo
func GetBoxDrawingString(up bool, down bool, left bool, right bool, padLeft int, padRight int) string {
var c rune
switch {
case up && down && left && right:
c = '┼'
case up && down && left && !right:
c = '┤'
case up && down && !left && right:
c = '├'
case up && down && !left && !right:
c = '│'
case up && !down && left && right:
c = '┴'
case up && !down && left && !right:
c = '┘'
case up && !down && !left && right:
c = '└'
case up && !down && !left && !right:
c = '╵'
case !up && down && left && right:
c = '┬'
case !up && down && left && !right:
c = '┐'
case !up && down && !left && right:
c = '┌'
case !up && down && !left && !right:
c = '╷'
case !up && !down && left && right:
c = '─'
case !up && !down && left && !right:
c = '╴'
case !up && !down && !left && right:
c = '╶'
case !up && !down && !left && !right:
c = ' '
}
sb := strings.Builder{}
writePadding := func(connect bool, width int) {
for i := 0; i < width; i++ {
if connect {
sb.WriteRune('─')
} else {
sb.WriteRune(' ')
}
}
}
writePadding(left, padLeft)
sb.WriteRune(c)
writePadding(right, padRight)
return sb.String()
}

View File

@@ -98,25 +98,32 @@ func (c *AppCollector) ListApplicationResources(app *v1beta1.Application) ([]typ
}
var managedResources []types.AppliedResource
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
for _, rt := range append(historyRTs, rootRT, currentRT) {
if rt != nil {
for i, managedResource := range rt.Spec.ManagedResources {
for _, managedResource := range rt.Spec.ManagedResources {
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) {
if _, ok := existResources[rt.Spec.ManagedResources[i].ClusterObjectReference]; !ok {
managedResources = append(managedResources, types.AppliedResource{
Cluster: managedResource.Cluster,
Kind: managedResource.Kind,
Component: managedResource.Component,
Trait: managedResource.Trait,
Name: managedResource.Name,
Namespace: managedResource.Namespace,
APIVersion: managedResource.APIVersion,
ResourceVersion: managedResource.ResourceVersion,
UID: managedResource.UID,
})
}
managedResources = append(managedResources, types.AppliedResource{
Cluster: managedResource.Cluster,
Kind: managedResource.Kind,
Component: managedResource.Component,
Trait: managedResource.Trait,
Name: managedResource.Name,
Namespace: managedResource.Namespace,
APIVersion: managedResource.APIVersion,
ResourceVersion: managedResource.ResourceVersion,
UID: managedResource.UID,
PublishVersion: oam.GetPublishVersion(rt),
DeployVersion: func() string {
obj, _ := managedResource.ToUnstructuredWithData()
if obj != nil {
return oam.GetDeployVersion(obj)
}
return ""
}(),
Revision: rt.GetLabels()[oam.LabelAppRevision],
Latest: currentRT != nil && rt.Name == currentRT.Name,
})
}
}
}
@@ -127,35 +134,35 @@ func (c *AppCollector) ListApplicationResources(app *v1beta1.Application) ([]typ
// FindResourceFromResourceTrackerSpec find resources from ResourceTracker spec
func (c *AppCollector) FindResourceFromResourceTrackerSpec(app *v1beta1.Application) ([]Resource, error) {
ctx := context.Background()
managedResources, err := c.ListApplicationResources(app)
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, c.k8sClient, app)
if err != nil {
return nil, err
}
resources := make([]Resource, 0, len(managedResources))
for _, objRef := range managedResources {
obj := new(unstructured.Unstructured)
obj.SetGroupVersionKind(objRef.GroupVersionKind())
obj.SetNamespace(objRef.Namespace)
obj.SetName(objRef.Name)
if err = c.k8sClient.Get(multicluster.ContextWithClusterName(ctx, objRef.Cluster),
client.ObjectKeyFromObject(obj), obj); err != nil {
if kerrors.IsNotFound(err) {
continue
var resources = []Resource{}
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
for _, rt := range append([]*v1beta1.ResourceTracker{rootRT, currentRT}, historyRTs...) {
if rt != nil {
for _, managedResource := range rt.Spec.ManagedResources {
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) {
if _, exist := existResources[managedResource.ClusterObjectReference]; exist {
continue
}
existResources[managedResource.ClusterObjectReference] = true
obj, err := managedResource.ToUnstructuredWithData()
if err != nil {
klog.Errorf("get obj from resource tracker failure %s", err.Error())
continue
}
resources = append(resources, Resource{
Cluster: managedResource.Cluster,
Revision: oam.GetPublishVersion(rt),
Component: managedResource.Component,
Object: obj,
})
}
}
return nil, err
}
if objRef.Cluster == "" {
objRef.Cluster = multicluster.ClusterLocalName
}
resources = append(resources, Resource{
Cluster: objRef.Cluster,
Revision: obj.GetLabels()[oam.LabelAppRevision],
Component: obj.GetLabels()[oam.LabelAppComponent],
Object: obj,
})
}
if len(resources) == 0 {
return nil, errors.Errorf("fail to find resources created by application: %v", c.opt.Name)
}
return resources, nil
}
@@ -163,11 +170,11 @@ func (c *AppCollector) FindResourceFromResourceTrackerSpec(app *v1beta1.Applicat
// FindResourceFromAppliedResourcesField find resources from AppliedResources field
func (c *AppCollector) FindResourceFromAppliedResourcesField(app *v1beta1.Application) ([]Resource, error) {
resources := make([]Resource, 0, len(app.Spec.Components))
for _, rsrcRef := range app.Status.AppliedResources {
if !isResourceInTargetCluster(c.opt.Filter, rsrcRef) {
for _, res := range app.Status.AppliedResources {
if !isResourceInTargetCluster(c.opt.Filter, res) {
continue
}
compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster)
compName, obj, err := getObjectCreatedByComponent(c.k8sClient, res.ObjectReference, res.Cluster)
if err != nil {
return nil, err
}
@@ -175,7 +182,7 @@ func (c *AppCollector) FindResourceFromAppliedResourcesField(app *v1beta1.Applic
resources = append(resources, Resource{
Component: compName,
Revision: obj.GetLabels()[oam.LabelAppRevision],
Cluster: rsrcRef.Cluster,
Cluster: res.Cluster,
Object: obj,
})
}

View File

@@ -96,6 +96,10 @@ type AppliedResource struct {
UID types.UID `json:"uid,omitempty"`
APIVersion string `json:"apiVersion,omitempty"`
ResourceVersion string `json:"resourceVersion,omitempty"`
DeployVersion string `json:"deployVersion,omitempty"`
PublishVersion string `json:"publishVersion,omitempty"`
Revision string `json:"revision,omitempty"`
Latest bool `json:"latest"`
}
// GroupVersionKind returns the stored group, version, and kind of an object

View File

@@ -168,61 +168,13 @@ func (p *provider) ExpandTopology(ctx wfContext.Context, v *value.Value, act wfT
if err != nil {
return err
}
policies := &[]*v1beta1.AppPolicy{}
policies := &[]v1beta1.AppPolicy{}
if err = policiesRaw.UnmarshalTo(policies); err != nil {
return errors.Wrapf(err, "failed to parse policies")
}
var placements []v1alpha1.PlacementDecision
placementMap := map[string]struct{}{}
addCluster := func(cluster string, ns string, validateCluster bool) error {
if validateCluster {
if _, e := multicluster.GetVirtualCluster(context.Background(), p, cluster); e != nil {
return errors.Wrapf(e, "failed to get cluster %s", cluster)
}
}
if ns == "" {
ns = p.app.GetNamespace()
}
if !resourcekeeper.AllowCrossNamespaceResource && ns != p.app.GetNamespace() {
return errors.Errorf("cannot cross namespace")
}
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
name := placement.String()
if _, found := placementMap[name]; !found {
placementMap[name] = struct{}{}
placements = append(placements, placement)
}
return nil
}
for _, policy := range *policies {
if policy.Type == v1alpha1.TopologyPolicyType {
topologySpec := &v1alpha1.TopologyPolicySpec{}
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
return errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
}
clusterLabelSelector := pkgpolicy.GetClusterLabelSelectorInTopology(topologySpec)
switch {
case topologySpec.Clusters != nil:
for _, cluster := range topologySpec.Clusters {
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
return err
}
}
case clusterLabelSelector != nil:
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), p, clusterLabelSelector)
if err != nil {
return errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
}
if len(clusters) == 0 {
return errors.Errorf("failed to find any cluster matches given labels")
}
for _, cluster := range clusters {
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
return err
}
}
}
}
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(context.Background(), p, p.app, *policies, resourcekeeper.AllowCrossNamespaceResource)
if err != nil {
return err
}
return v.FillObject(placements, "outputs", "decisions")
}

View File

@@ -576,7 +576,7 @@ func TestExpandTopology(t *testing.T) {
},
"topology-by-clusters": {
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusters:["cluster-a"]}}]}}`,
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}},
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}},
},
"topology-by-cluster-selector-404": {
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"bad-value"}}}]}}`,
@@ -584,11 +584,11 @@ func TestExpandTopology(t *testing.T) {
},
"topology-by-cluster-selector": {
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"value"}}}]}}`,
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}, {Cluster: "cluster-b", Namespace: "test"}},
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}, {Cluster: "cluster-b", Namespace: ""}},
},
"topology-by-cluster-label-selector": {
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterLabelSelector:{"key":"value"}}}]}}`,
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}, {Cluster: "cluster-b", Namespace: "test"}},
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}, {Cluster: "cluster-b", Namespace: ""}},
},
"topology-by-cluster-selector-and-namespace-invalid": {
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"value"},namespace:"override"}}]}}`,

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
@@ -68,6 +69,7 @@ const (
const (
statusEnabled = "enabled"
statusDisabled = "disabled"
statusSuspend = "suspend"
)
var forceDisable bool
@@ -204,7 +206,12 @@ func AdditionalEndpointPrinter(ctx context.Context, c common.Args, k8sClient cli
return
}
if name == "velaux" {
fmt.Println(`Please use command: "vela port-forward -n vela-system addon-velaux 9082:80" and Select "Cluster: local | Namespace: vela-system | Component: velaux | Kind: Service" to check the dashboard.`)
fmt.Println(`To check the initialized admin user name and password by:`)
fmt.Println(` vela logs -n vela-system --name apiserver addon-velaux | grep "initialized admin username"`)
fmt.Println(`To open the dashboard directly by port-forward:`)
fmt.Println(` vela port-forward -n vela-system addon-velaux 9082:80`)
fmt.Println(`Select "Cluster: local | Namespace: vela-system | Component: velaux | Kind: Service" from the prompt.`)
fmt.Println(`Please refer to https://kubevela.io/docs/reference/addons/velaux for more VelaUX addon installation and visiting method.`)
}
}
@@ -252,7 +259,7 @@ Upgrade addon for specific clusters, (local means control plane):
}
ioStream.Infof("enable addon by local dir: %s \n", addonOrDir)
// args[0] is a local path install with local dir
name := filepath.Base(addonOrDir)
name = filepath.Base(addonOrDir)
_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, name)
if err != nil {
return errors.Wrapf(err, "cannot fetch addon related addon %s", name)
@@ -265,7 +272,7 @@ Upgrade addon for specific clusters, (local means control plane):
if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") {
return fmt.Errorf("addon directory %s not found in local", addonOrDir)
}
name = addonOrDir
_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, addonOrDir)
if err != nil {
return errors.Wrapf(err, "cannot fetch addon related addon %s", addonOrDir)
@@ -399,7 +406,9 @@ func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c
if err != nil {
return err
}
fmt.Printf("addon %s status is %s \n", name, status.AddonPhase)
fmt.Print(generateAddonInfo(name, status))
if status.AddonPhase != statusEnabled && status.AddonPhase != statusDisabled {
fmt.Printf("diagnose addon info from application %s", pkgaddon.Convert2AppName(name))
err := printAppStatus(context.Background(), k8sClient, ioStreams, pkgaddon.Convert2AppName(name), types.DefaultKubeVelaNS, cmd, c)
@@ -410,6 +419,36 @@ func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c
return nil
}
func generateAddonInfo(name string, status pkgaddon.Status) string {
var res string
var phase string
switch status.AddonPhase {
case statusEnabled:
c := color.New(color.FgGreen)
phase = c.Sprintf("%s", status.AddonPhase)
case statusSuspend:
c := color.New(color.FgRed)
phase = c.Sprintf("%s", status.AddonPhase)
default:
phase = status.AddonPhase
}
res += fmt.Sprintf("addon %s status is %s \n", name, phase)
if len(status.InstalledVersion) != 0 {
res += fmt.Sprintf("installedVersion: %s \n", status.InstalledVersion)
}
if len(status.Clusters) != 0 {
var ic []string
for c := range status.Clusters {
ic = append(ic, c)
}
sort.Strings(ic)
res += fmt.Sprintf("installedClusters: %s \n", ic)
}
return res
}
func listAddons(ctx context.Context, clt client.Client, registry string) error {
var addons []*pkgaddon.UIData
var err error
@@ -504,9 +543,31 @@ func waitApplicationRunning(k8sClient client.Client, addonName string) error {
}
// generate the available version
// this func put the installed version as the first version and keep the origin order
// print ... if available version too much
func genAvailableVersionInfo(versions []string, status pkgaddon.Status) string {
var v []string
// put installed-version as the first version and keep the origin order
if len(status.InstalledVersion) != 0 {
for i, version := range versions {
if version == status.InstalledVersion {
v = append(v, version)
versions = append(versions[:i], versions[i+1:]...)
}
}
}
v = append(v, versions...)
res := "["
for _, version := range versions {
var count int
for _, version := range v {
if count == 3 {
// just show newest 3 versions
res += "..."
break
}
if version == status.InstalledVersion {
col := color.New(color.Bold, color.FgGreen)
res += col.Sprintf("%s", version)
@@ -514,6 +575,7 @@ func genAvailableVersionInfo(versions []string, status pkgaddon.Status) string {
res += version
}
res += ", "
count++
}
res = strings.TrimSuffix(res, ", ")
res += "]"

View File

@@ -18,8 +18,13 @@ package cli
import (
"fmt"
"strings"
"testing"
"github.com/fatih/color"
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/util"
@@ -151,3 +156,62 @@ func TestTransCluster(t *testing.T) {
assert.DeepEqual(t, transClusters(s.str), s.res)
}
}
func TestGenerateStatusIn(t *testing.T) {
testcases := []struct {
c pkgaddon.Status
res []string
}{
{
c: pkgaddon.Status{InstalledVersion: "1.2.1", Clusters: map[string]map[string]interface{}{"cluster1": nil, "cluster2": nil}, AddonPhase: statusEnabled},
res: []string{"installedVersion: 1.2.1", "installedClusters: [cluster1 cluster2]", fmt.Sprintf("status is %s", color.New(color.FgGreen).Sprintf(statusEnabled))},
},
{
c: pkgaddon.Status{InstalledVersion: "1.2.3", AddonPhase: statusSuspend},
res: []string{"installedVersion: 1.2.3", fmt.Sprintf("status is %s", color.New(color.FgRed).Sprintf(statusSuspend))},
},
}
for _, testcase := range testcases {
res := generateAddonInfo("test", testcase.c)
for _, re := range testcase.res {
assert.Equal(t, strings.Contains(res, re), true)
}
}
}
func TestGenerateAvailableVersions(t *testing.T) {
type testcase struct {
inVersion string
versions []string
}
testcases := []struct {
c testcase
res string
}{
{
c: testcase{
inVersion: "1.2.1",
versions: []string{"1.2.1"},
},
res: fmt.Sprintf("[%s]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
},
{
c: testcase{
inVersion: "1.2.1",
versions: []string{"1.2.3", "1.2.2", "1.2.1"},
},
res: fmt.Sprintf("[%s, 1.2.3, 1.2.2]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
},
{
c: testcase{
inVersion: "1.2.1",
versions: []string{"1.2.3", "1.2.2", "1.2.1", "1.2.0"},
},
res: fmt.Sprintf("[%s, 1.2.3, 1.2.2, ...]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
},
}
for _, s := range testcases {
re := genAvailableVersionInfo(s.c.versions, pkgaddon.Status{InstalledVersion: s.c.inVersion})
assert.Equal(t, re, s.res)
}
}

View File

@@ -28,6 +28,7 @@ import (
"k8s.io/klog"
"github.com/oam-dev/kubevela/apis/types"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/helm"
"github.com/oam-dev/kubevela/pkg/utils/system"
@@ -66,6 +67,7 @@ func NewCommand() *cobra.Command {
commandArgs := common.Args{
Schema: common.Scheme,
}
f := velacmd.NewDefaultFactory(commandArgs.GetClient)
if err := system.InitDirs(); err != nil {
fmt.Println("InitDir err", err)
@@ -76,7 +78,7 @@ func NewCommand() *cobra.Command {
// Getting Start
NewEnvCommand(commandArgs, "3", ioStream),
NewInitCommand(commandArgs, "2", ioStream),
NewUpCommand(commandArgs, "1", ioStream),
NewUpCommand(f, "1"),
NewCapabilityShowCommand(commandArgs, ioStream),
// Manage Apps
@@ -165,7 +167,7 @@ func NewVersionListCommand(ioStream util.IOStreams) *cobra.Command {
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
helmHelper := helm.NewHelper()
versions, err := helmHelper.ListVersions(kubevelaInstallerHelmRepoURL, kubeVelaChartName, true)
versions, err := helmHelper.ListVersions(kubevelaInstallerHelmRepoURL, kubeVelaChartName, true, nil)
if err != nil {
return err
}

View File

@@ -88,6 +88,7 @@ func ClusterCommandGroup(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Comm
NewClusterDetachCommand(&c),
NewClusterProbeCommand(&c),
NewClusterLabelCommandGroup(&c),
NewClusterAliasCommand(&c),
)
return cmd
}
@@ -101,7 +102,7 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
Long: "list worker clusters managed by KubeVela.",
Args: cobra.ExactValidArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
table := newUITable().AddRow("CLUSTER", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS")
table := newUITable().AddRow("CLUSTER", "ALIAS", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS")
client, err := c.GetClient()
if err != nil {
return err
@@ -123,9 +124,9 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
}
for i, l := range labels {
if i == 0 {
table.AddRow(cluster.Name, cluster.Type, cluster.EndPoint, fmt.Sprintf("%v", cluster.Accepted), l)
table.AddRow(cluster.Name, cluster.Alias, cluster.Type, cluster.EndPoint, fmt.Sprintf("%v", cluster.Accepted), l)
} else {
table.AddRow("", "", "", "", l)
table.AddRow("", "", "", "", "", l)
}
}
}
@@ -255,6 +256,29 @@ func NewClusterDetachCommand(c *common.Args) *cobra.Command {
return cmd
}
// NewClusterAliasCommand create an alias to the named cluster
func NewClusterAliasCommand(c *common.Args) *cobra.Command {
cmd := &cobra.Command{
Use: "alias CLUSTER_NAME ALIAS",
Short: "alias a named cluster.",
Long: "alias a named cluster.",
Args: cobra.ExactValidArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clusterName, aliasName := args[0], args[1]
k8sClient, err := c.GetClient()
if err != nil {
return err
}
if err = multicluster.AliasCluster(context.Background(), k8sClient, clusterName, aliasName); err != nil {
return err
}
cmd.Printf("Alias cluster %s as %s.\n", clusterName, aliasName)
return nil
},
}
return cmd
}
// NewClusterProbeCommand create command to help user try health probe for existing cluster
// TODO(somefive): move prob logic into cluster management
func NewClusterProbeCommand(c *common.Args) *cobra.Command {
@@ -299,12 +323,12 @@ func NewClusterLabelCommandGroup(c *common.Args) *cobra.Command {
func updateClusterLabelAndPrint(cmd *cobra.Command, cli client.Client, vc *multicluster.VirtualCluster, clusterName string) (err error) {
if err = cli.Update(context.Background(), vc.Object); err != nil {
return errors.Errorf("failed to update labels for cluster %s (type: %s)", vc.Name, vc.Type)
return errors.Errorf("failed to update labels for cluster %s, type: %s", vc.FullName(), vc.Type)
}
if vc, err = multicluster.GetVirtualCluster(context.Background(), cli, clusterName); err != nil {
return errors.Wrapf(err, "failed to get updated cluster %s", clusterName)
}
cmd.Printf("Successfully update labels for cluster %s (type: %s).\n", vc.Name, vc.Type)
cmd.Printf("Successfully update labels for cluster %s, type: %s.\n", vc.FullName(), vc.Type)
if len(vc.Labels) == 0 {
cmd.Println("No valid label exists.")
}

Some files were not shown because too many files have changed in this diff Show More