Compare commits

...

31 Commits

Author SHA1 Message Date
wyike
d08aa7d12c fix several issues (#3729) (#3735)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-22 17:29:01 +08:00
wyike
f9755a405f Fix: change systemInfo some fields (cp #3715) (#3723)
* Fix: change systemInfo some  fields (#3715)

* add some field an calculate workflow step

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

* fix the calculate job cannot start issue

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

* fix comments

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

fix test

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

* add suit test framework

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

* modify the go mod file

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

fix worry file name

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-22 16:42:54 +08:00
github-actions[bot]
d751d95bac Feat: change the webservice and config-image-registry definitions (#3733)
Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
(cherry picked from commit 300f0c5ace)

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-22 16:34:28 +08:00
github-actions[bot]
e86eec07e0 specify staticcheck version (#3728)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

fix the workflow

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

fix

try to fix

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

fix make file

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

fix makefile

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

Co-authored-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-22 14:26:37 +08:00
github-actions[bot]
4abb5c6ced [Backport release-1.3] Fix: embed.FS filepath that follow the unix style file path when running on windows (#3720)
* fix: "builtin-apply-component.cue: file does not exist"

Signed-off-by: lei.chu <1062186165@qq.com>
(cherry picked from commit fba60a1af1)

* fix: "builtin-apply-component.cue: file does not exist"

Signed-off-by: lei.chu <1062186165@qq.com>
(cherry picked from commit 9e74023951)

Co-authored-by: lei.chu <1062186165@qq.com>
2022-04-21 14:32:30 +08:00
github-actions[bot]
32d9a9ec94 Fix: vela-core does not report error, when component depends on invalid component (#3712)
Signed-off-by: StevenLeiZhang <zhangleiic@163.com>
(cherry picked from commit 01781bdc02)

Co-authored-by: StevenLeiZhang <zhangleiic@163.com>
2022-04-20 13:39:53 +08:00
github-actions[bot]
f6f9ef4ded Feat: support disable legacy gc upgrade operation (#3697)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 31ab3d859c)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-04-19 09:53:10 +08:00
github-actions[bot]
166c93d548 Fix: set provider name as the config name (#3695)
- For VelaUX, hidden a provider name (users don't need to manual set it). Used
the application/component name (config name) to be the provider name.
- Store description of a config to the annotation of the config application

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

Co-authored-by: Zheng Xi Zhou <zzxwill@gmail.com>
2022-04-18 16:48:37 +08:00
github-actions[bot]
8f767068bf Fix: rt resource key compare mismatch local cluster (#3685)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit fa12bc1950)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2022-04-15 16:37:00 +08:00
github-actions[bot]
8d9e2a71e7 [Backport release-1.3] Fix: can not query the instance list for the app with apply once policy (#3684)
* Fix: can not query the instance list for the app with apply once policy

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

* Fix: change the test case about ListResourcesInApp

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

Co-authored-by: barnettZQG <barnett.zqg@gmail.com>
2022-04-15 15:04:04 +08:00
wyike
58b3bca537 cherrypick 3665 and 3605 to release 1.3 (#3668)
Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2022-04-15 12:11:05 +08:00
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
113 changed files with 5122 additions and 605 deletions

View File

@@ -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

@@ -57,7 +57,7 @@ jobs:
restore-keys: ${{ runner.os }}-pkg-
- name: Install StaticCheck
run: GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck
run: GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck@v0.3.0
- name: Static Check
run: staticcheck ./...

View File

@@ -31,6 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/interfaces"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/errors"
)
@@ -121,7 +122,11 @@ func (in ManagedResource) NamespacedName() types.NamespacedName {
// ResourceKey computes the key for managed resource, resources with the same key points to the same resource
func (in ManagedResource) ResourceKey() string {
gv, kind := in.GroupVersionKind().ToAPIVersionAndKind()
return strings.Join([]string{gv, kind, in.Cluster, in.Namespace, in.Name}, "/")
cluster := in.Cluster
if cluster == "" {
cluster = velatypes.ClusterLocalName
}
return strings.Join([]string{gv, kind, cluster, in.Namespace, in.Name}, "/")
}
// ComponentKey computes the key for the component which managed resource belongs to

View File

@@ -16,9 +16,15 @@ 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 (
// ClusterLocalName the name for the hub cluster
ClusterLocalName = "local"
// CredentialTypeInternal identifies the virtual cluster from internal kubevela system
CredentialTypeInternal v1alpha1.CredentialType = "Internal"
// CredentialTypeOCMManagedCluster identifies the virtual cluster from ocm
@@ -29,3 +35,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

@@ -71,6 +71,10 @@ const (
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"
// AnnotationConfigDescription is the annotation for config description
AnnotationConfigDescription = "config.oam.dev/description"
// AnnotationConfigAlias is the annotation for config alias
AnnotationConfigAlias = "config.oam.dev/alias"
)
@@ -139,4 +143,13 @@ const (
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"
)
const (
// TerrfaormComponentPrefix is the prefix of component type of terraform-xxx
TerrfaormComponentPrefix = "terraform-"
)

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` |

View File

@@ -0,0 +1,70 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/config-image-registry.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
custom.definition.oam.dev/alias.config.oam.dev: Image Registry
definition.oam.dev/description: Config information to authenticate image registry
labels:
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
custom.definition.oam.dev/multi-cluster.config.oam.dev: "true"
custom.definition.oam.dev/type.config.oam.dev: image-registry
custom.definition.oam.dev/ui-hidden: "true"
name: config-image-registry
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"encoding/base64"
"encoding/json"
)
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: context.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "image-registry"
"config.oam.dev/multi-cluster": "true"
"config.oam.dev/identifier": parameter.registry
"config.oam.dev/sub-type": "auth"
}
}
type: "kubernetes.io/dockerconfigjson"
stringData: {
if parameter.auth != _|_ {
".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
})
}
}
}
parameter: {
// +usage=Image registry FQDN
registry: string
// +usage=Authenticate the image registry
auth?: {
// +usage=Private Image registry username
username: string
// +usage=Private Image registry password
password: string
// +usage=Private Image registry email
email?: string
}
}
workload:
type: autodetects.core.oam.dev

View File

@@ -455,7 +455,7 @@ spec:
readinessProbe?: #HealthProbe
// +usage=Specify the hostAliases to add
hostAliases: [...{
hostAliases?: [...{
ip: string
hostnames: [...string]
}]

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

@@ -0,0 +1,70 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/config-image-registry.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
custom.definition.oam.dev/alias.config.oam.dev: Image Registry
definition.oam.dev/description: Config information to authenticate image registry
labels:
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
custom.definition.oam.dev/multi-cluster.config.oam.dev: "true"
custom.definition.oam.dev/type.config.oam.dev: image-registry
custom.definition.oam.dev/ui-hidden: "true"
name: config-image-registry
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"encoding/base64"
"encoding/json"
)
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: context.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "image-registry"
"config.oam.dev/multi-cluster": "true"
"config.oam.dev/identifier": parameter.registry
"config.oam.dev/sub-type": "auth"
}
}
type: "kubernetes.io/dockerconfigjson"
stringData: {
if parameter.auth != _|_ {
".dockerconfigjson": json.Marshal({
auths: "\(parameter.registry)": {
username: parameter.auth.username
password: parameter.auth.password
if parameter.auth.email != _|_ {
email: parameter.auth.email
}
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
})
}
}
}
parameter: {
// +usage=Image registry FQDN
registry: string
// +usage=Authenticate the image registry
auth?: {
// +usage=Private Image registry username
username: string
// +usage=Private Image registry password
password: string
// +usage=Private Image registry email
email?: string
}
}
workload:
type: autodetects.core.oam.dev

View File

@@ -455,7 +455,7 @@ spec:
readinessProbe?: #HealthProbe
// +usage=Specify the hostAliases to add
hostAliases: [...{
hostAliases?: [...{
ip: string
hostnames: [...string]
}]

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

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

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": [
@@ -5302,6 +5629,7 @@
},
"definitions": {
"*v1.ApplicationTriggerBase": {},
"*v1.Config": {},
"*v1.EmptyResponse": {},
"addon.Dependency": {
"properties": {
@@ -5354,6 +5682,22 @@
}
}
},
"addon.GitlabAddonSource": {
"properties": {
"path": {
"type": "string"
},
"repo": {
"type": "string"
},
"token": {
"type": "string"
},
"url": {
"type": "string"
}
}
},
"addon.HelmSource": {
"properties": {
"url": {
@@ -6623,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",
@@ -7039,6 +7355,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},
@@ -7209,12 +7528,12 @@
},
"v1.ApplicationDeployResponse": {
"required": [
"version",
"status",
"note",
"createTime",
"envName",
"triggerType"
"triggerType",
"createTime",
"version",
"status"
],
"properties": {
"codeInfo": {
@@ -7748,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",
@@ -7797,6 +8141,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},
@@ -8093,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",
@@ -8304,11 +8676,11 @@
},
"v1.DetailAddonResponse": {
"required": [
"name",
"description",
"icon",
"version",
"description",
"invisible",
"name",
"icon",
"schema",
"uiSchema",
"definitions",
@@ -8388,12 +8760,12 @@
},
"v1.DetailApplicationResponse": {
"required": [
"alias",
"icon",
"project",
"description",
"icon",
"name",
"createTime",
"name",
"alias",
"updateTime",
"policies",
"envBindings",
@@ -8455,20 +8827,20 @@
},
"v1.DetailClusterResponse": {
"required": [
"status",
"apiServerURL",
"dashboardURL",
"kubeConfigSecret",
"icon",
"labels",
"kubeConfig",
"updateTime",
"reason",
"provider",
"name",
"alias",
"description",
"reason",
"icon",
"dashboardURL",
"createTime",
"provider",
"description",
"status",
"updateTime",
"apiServerURL",
"kubeConfig",
"labels",
"kubeConfigSecret",
"resourceInfo"
],
"properties": {
@@ -8526,14 +8898,14 @@
},
"v1.DetailComponentResponse": {
"required": [
"createTime",
"updateTime",
"appPrimaryKey",
"type",
"main",
"name",
"updateTime",
"createTime",
"creator",
"alias",
"type",
"main",
"appPrimaryKey",
"definition"
],
"properties": {
@@ -8635,13 +9007,13 @@
},
"v1.DetailPolicyResponse": {
"required": [
"name",
"type",
"description",
"creator",
"properties",
"createTime",
"updateTime",
"name"
"updateTime"
],
"properties": {
"createTime": {
@@ -8671,17 +9043,17 @@
},
"v1.DetailRevisionResponse": {
"required": [
"updateTime",
"triggerType",
"updateTime",
"version",
"deployUser",
"reason",
"createTime",
"status",
"deployUser",
"note",
"workflowName",
"envName",
"appPrimaryKey",
"reason",
"note"
"appPrimaryKey"
],
"properties": {
"appPrimaryKey": {
@@ -8737,8 +9109,8 @@
"required": [
"project",
"createTime",
"name",
"updateTime"
"updateTime",
"name"
],
"properties": {
"alias": {
@@ -8778,11 +9150,11 @@
},
"v1.DetailUserResponse": {
"required": [
"disabled",
"createTime",
"lastLoginTime",
"name",
"email",
"disabled",
"createTime",
"projects",
"roles"
],
@@ -8823,12 +9195,12 @@
},
"v1.DetailWorkflowRecordResponse": {
"required": [
"status",
"name",
"namespace",
"workflowName",
"workflowAlias",
"applicationRevision",
"status",
"deployTime",
"deployUser",
"note",
@@ -8880,14 +9252,14 @@
},
"v1.DetailWorkflowResponse": {
"required": [
"envName",
"createTime",
"updateTime",
"name",
"enable",
"default",
"description",
"name",
"alias"
"envName",
"createTime",
"alias",
"description"
],
"properties": {
"alias": {
@@ -9466,11 +9838,11 @@
},
"v1.LoginUserInfoResponse": {
"required": [
"disabled",
"createTime",
"lastLoginTime",
"name",
"email",
"disabled",
"projects",
"platformPermissions",
"projectPermissions"
@@ -9778,6 +10150,24 @@
}
}
},
"v1.SystemInfo": {
"required": [
"installID",
"enableCollection",
"loginType"
],
"properties": {
"enableCollection": {
"type": "boolean"
},
"installID": {
"type": "string"
},
"loginType": {
"type": "string"
}
}
},
"v1.SystemInfoRequest": {
"required": [
"enableCollection",
@@ -9789,6 +10179,9 @@
},
"loginType": {
"type": "string"
},
"velaAddress": {
"type": "string"
}
}
},
@@ -9797,15 +10190,9 @@
"installID",
"enableCollection",
"loginType",
"createTime",
"updateTime",
"systemVersion"
],
"properties": {
"createTime": {
"type": "string",
"format": "date-time"
},
"enableCollection": {
"type": "boolean"
},
@@ -9817,10 +10204,6 @@
},
"systemVersion": {
"$ref": "#/definitions/v1.SystemVersion"
},
"updateTime": {
"type": "string",
"format": "date-time"
}
}
},
@@ -9889,6 +10272,9 @@
"gitee": {
"$ref": "#/definitions/addon.GiteeAddonSource"
},
"gitlab": {
"$ref": "#/definitions/addon.GitlabAddonSource"
},
"helm": {
"$ref": "#/definitions/addon.HelmSource"
},

View File

@@ -0,0 +1,23 @@
name: mock-addon
version: 1.0.0
description: Extended workload to do continuous and progressive delivery
icon: https://raw.githubusercontent.com/fluxcd/flux/master/docs/_files/weave-flux.png
url: https://fluxcd.io
tags:
- extended_workload
- gitops
- only_example
deployTo:
control_plane: true
runtime_cluster: false
dependencies: []
#- name: addon_name
# set invisible means this won't be list and will be enabled when depended on
# for example, terraform-alibaba depends on terraform which is invisible,
# when terraform-alibaba is enabled, terraform will be enabled automatically
# default: false
invisible: false

View File

@@ -0,0 +1,14 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: mock-addon
namespace: vela-system
spec:
components:
- name: ns-example-system
type: raw
properties:
apiVersion: v1
kind: Namespace
metadata:
name: mock-system

8
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
@@ -96,6 +97,8 @@ require (
sigs.k8s.io/yaml v1.2.0
)
require github.com/robfig/cron/v3 v3.0.1
require (
cloud.google.com/go v0.81.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
@@ -251,8 +254,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

12
go.sum
View File

@@ -1437,6 +1437,8 @@ github.com/rancher/wrangler v0.4.0/go.mod h1:1cR91WLhZgkZ+U4fV9nVuXqKurWbgXcIReU
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -2047,13 +2049,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=
@@ -2064,8 +2067,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

@@ -25,7 +25,7 @@ ifeq (, $(shell which staticcheck))
@{ \
set -e ;\
echo 'installing honnef.co/go/tools/cmd/staticcheck ' ;\
GO111MODULE=off go get honnef.co/go/tools/cmd/staticcheck ;\
GO111MODULE=on go get honnef.co/go/tools/cmd/staticcheck@v0.3.0 ;\
}
STATICCHECK=$(GOBIN)/staticcheck
else

View File

@@ -49,6 +49,7 @@ e2e-apiserver-test:
pkill vela_addon_mock_server || true
go run ./e2e/addon/mock/vela_addon_mock_server.go &
go test -v -coverpkg=./... -coverprofile=/tmp/e2e_apiserver_test.out ./test/e2e-apiserver-test
sleep 15
@$(OK) tests pass
.PHONY: e2e-test

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 {
@@ -1344,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)
}
}
@@ -1361,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)
}
}
@@ -1382,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

@@ -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

@@ -0,0 +1,100 @@
/*
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 collect
import (
"context"
"fmt"
"math/rand"
"testing"
"time"
. "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"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
func TestCalculateJob(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Caclculate systemInfo cronJob")
}
var _ = BeforeSuite(func(done Done) {
rand.Seed(time.Now().UnixNano())
By("bootstrapping test environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
CRDDirectoryPaths: []string{"../../../charts/vela-core/crds"},
}
By("start kube test env")
var err error
cfg, err = testEnv.Start()
Expect(err).Should(BeNil())
Expect(cfg).ToNot(BeNil())
By("new kube client")
cfg.Timeout = time.Minute * 2
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
By("new kube client success")
clients.SetKubeClient(k8sClient)
Expect(err).Should(BeNil())
close(done)
}, 240)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func NewDatastore(cfg datastore.Config) (ds datastore.DataStore, err error) {
switch cfg.Type {
case "mongodb":
ds, err = mongodb.New(context.Background(), cfg)
if err != nil {
return nil, fmt.Errorf("create mongodb datastore instance failure %w", err)
}
case "kubeapi":
ds, err = kubeapi.New(context.Background(), cfg)
if err != nil {
return nil, fmt.Errorf("create mongodb datastore instance failure %w", err)
}
default:
return nil, fmt.Errorf("not support datastore type %s", cfg.Type)
}
return ds, nil
}

View File

@@ -0,0 +1,331 @@
/*
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 collect
import (
"context"
"sort"
"time"
client2 "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/oam"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
"github.com/robfig/cron/v3"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
)
// TopKFrequent top frequency component or trait definition
var TopKFrequent = 5
// CrontabSpec the cron spec of job running
var CrontabSpec = "0 0 * * *"
// maximum tires is 5, initial duration is 1 minute
var waitBackOff = wait.Backoff{
Steps: 5,
Duration: 1 * time.Minute,
Factor: 5.0,
Jitter: 0.1,
}
// InfoCalculateCronJob is the cronJob to calculate the system info store in db
type InfoCalculateCronJob struct {
ds datastore.DataStore
}
// StartCalculatingInfoCronJob will start the system info calculating job.
func StartCalculatingInfoCronJob(ds datastore.DataStore) {
i := InfoCalculateCronJob{
ds: ds,
}
// run calculate job in 0:00 of every day
i.start(CrontabSpec)
}
func (i InfoCalculateCronJob) start(cronSpec string) {
c := cron.New(cron.WithChain(
// don't let job panic crash whole api-server process
cron.Recover(cron.DefaultLogger),
))
// ignore the entityId and error, the cron spec is defined by hard code, mustn't generate error
_, _ = c.AddFunc(cronSpec, func() {
// ExponentialBackoff retry this job
err := retry.OnError(waitBackOff, func(err error) bool {
// always retry
return true
}, func() error {
if err := i.run(); err != nil {
log.Logger.Errorf("Failed to calculate systemInfo, will try again after several minute error %v", err)
return err
}
log.Logger.Info("Successfully to calculate systemInfo")
return nil
})
if err != nil {
log.Logger.Errorf("After 5 tries the calculating cronJob failed: %v", err)
}
})
c.Start()
}
func (i InfoCalculateCronJob) run() error {
ctx := context.Background()
systemInfo := model.SystemInfo{}
e, err := i.ds.List(ctx, &systemInfo, &datastore.ListOptions{})
if err != nil {
return err
}
// if no systemInfo means velaux have not have not send get requestso skip calculate job
if len(e) == 0 {
return nil
}
info, ok := e[0].(*model.SystemInfo)
if !ok {
return nil
}
// if disable collection skip calculate job
if !info.EnableCollection {
return nil
}
if err := i.calculateAndUpdate(ctx, *info); err != nil {
return err
}
return nil
}
func (i InfoCalculateCronJob) calculateAndUpdate(ctx context.Context, systemInfo model.SystemInfo) error {
appCount, topKComp, topKTrait, topWorkflowStep, topKPolicy, err := i.calculateAppInfo(ctx)
if err != nil {
return err
}
enabledAddon, err := i.calculateAddonInfo(ctx)
if err != nil {
return err
}
clusterCount, err := i.calculateClusterInfo(ctx)
if err != nil {
return err
}
statisticInfo := model.StatisticInfo{
AppCount: genCountInfo(appCount),
TopKCompDef: topKComp,
TopKTraitDef: topKTrait,
TopKWorkflowStepDef: topWorkflowStep,
TopKPolicyDef: topKPolicy,
ClusterCount: genClusterCountInfo(clusterCount),
EnabledAddon: enabledAddon,
UpdateTime: time.Now(),
}
systemInfo.StatisticInfo = statisticInfo
if err := i.ds.Put(ctx, &systemInfo); err != nil {
return err
}
return nil
}
func (i InfoCalculateCronJob) calculateAppInfo(ctx context.Context) (int, []string, []string, []string, []string, error) {
var err error
var appCount int
compDef := map[string]int{}
traitDef := map[string]int{}
workflowDef := map[string]int{}
policyDef := map[string]int{}
var app = model.Application{}
entities, err := i.ds.List(ctx, &app, &datastore.ListOptions{})
if err != nil {
return 0, nil, nil, nil, nil, err
}
for _, entity := range entities {
appModel, ok := entity.(*model.Application)
if !ok {
continue
}
appCount++
comp := model.ApplicationComponent{
AppPrimaryKey: appModel.Name,
}
comps, err := i.ds.List(ctx, &comp, &datastore.ListOptions{})
if err != nil {
return 0, nil, nil, nil, nil, err
}
for _, e := range comps {
c, ok := e.(*model.ApplicationComponent)
if !ok {
continue
}
compDef[c.Type]++
for _, t := range c.Traits {
traitDef[t.Type]++
}
}
workflow := model.Workflow{
AppPrimaryKey: app.PrimaryKey(),
}
workflows, err := i.ds.List(ctx, &workflow, &datastore.ListOptions{})
if err != nil {
return 0, nil, nil, nil, nil, err
}
for _, e := range workflows {
w, ok := e.(*model.Workflow)
if !ok {
continue
}
for _, step := range w.Steps {
workflowDef[step.Type]++
}
}
policy := model.ApplicationPolicy{
AppPrimaryKey: app.PrimaryKey(),
}
policies, err := i.ds.List(ctx, &policy, &datastore.ListOptions{})
if err != nil {
return 0, nil, nil, nil, nil, err
}
for _, e := range policies {
p, ok := e.(*model.ApplicationPolicy)
if !ok {
continue
}
policyDef[p.Type]++
}
}
return appCount, topKFrequent(compDef, TopKFrequent), topKFrequent(traitDef, TopKFrequent), topKFrequent(workflowDef, TopKFrequent), topKFrequent(policyDef, TopKFrequent), nil
}
func (i InfoCalculateCronJob) calculateAddonInfo(ctx context.Context) (map[string]string, error) {
client, err := clients.GetKubeClient()
if err != nil {
return nil, err
}
apps := &v1beta1.ApplicationList{}
if err := client.List(ctx, apps, client2.InNamespace(types.DefaultKubeVelaNS), client2.HasLabels{oam.LabelAddonName}); err != nil {
return nil, err
}
res := map[string]string{}
for _, application := range apps.Items {
if addonName := application.Labels[oam.LabelAddonName]; addonName != "" {
var status string
switch application.Status.Phase {
case common.ApplicationRunning:
status = "enabled"
case common.ApplicationDeleting:
status = "disabling"
default:
status = "enabling"
}
res[addonName] = status
}
}
return res, nil
}
func (i InfoCalculateCronJob) calculateClusterInfo(ctx context.Context) (int, error) {
client, err := clients.GetKubeClient()
if err != nil {
return 0, err
}
cs, err := multicluster.ListVirtualClusters(ctx, client)
if err != nil {
return 0, err
}
return len(cs), nil
}
type defPair struct {
name string
count int
}
func topKFrequent(defs map[string]int, k int) []string {
var pairs []defPair
var res []string
for name, num := range defs {
pairs = append(pairs, defPair{name: name, count: num})
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].count >= pairs[j].count
})
i := 0
for _, pair := range pairs {
res = append(res, pair.name)
i++
if i == k {
break
}
}
return res
}
func genCountInfo(num int) string {
switch {
case num < 10:
return "<10"
case num < 50:
return "<50"
case num < 100:
return "<100"
case num < 500:
return "<500"
case num < 2000:
return "<2000"
case num < 5000:
return "<5000"
case num < 10000:
return "<10000"
default:
return ">=10000"
}
}
func genClusterCountInfo(num int) string {
switch {
case num < 3:
return "<3"
case num < 10:
return "<10"
case num < 50:
return "<50"
default:
return ">=50"
}
}

View File

@@ -0,0 +1,273 @@
/*
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 collect
import (
"context"
"errors"
"testing"
"time"
"github.com/onsi/gomega/format"
"gotest.tools/assert"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
var _ = Describe("Test calculate cronJob", func() {
var (
ds datastore.DataStore
testProject string
i InfoCalculateCronJob
ctx = context.Background()
)
mockDataInDs := func() {
app1 := model.Application{BaseModel: model.BaseModel{CreateTime: time.Now()}, Name: "app1", Project: testProject}
app2 := model.Application{BaseModel: model.BaseModel{CreateTime: time.Now()}, Name: "app2", Project: testProject}
trait1 := model.ApplicationTrait{Type: "rollout"}
trait2 := model.ApplicationTrait{Type: "expose"}
trait3 := model.ApplicationTrait{Type: "rollout"}
trait4 := model.ApplicationTrait{Type: "patch"}
trait5 := model.ApplicationTrait{Type: "patch"}
trait6 := model.ApplicationTrait{Type: "rollout"}
appComp1 := model.ApplicationComponent{AppPrimaryKey: app1.PrimaryKey(), Name: "comp1", Type: "helm", Traits: []model.ApplicationTrait{trait1, trait4}}
appComp2 := model.ApplicationComponent{AppPrimaryKey: app2.PrimaryKey(), Name: "comp2", Type: "webservice", Traits: []model.ApplicationTrait{trait3}}
appComp3 := model.ApplicationComponent{AppPrimaryKey: app2.PrimaryKey(), Name: "comp3", Type: "webservice", Traits: []model.ApplicationTrait{trait2, trait5, trait6}}
Expect(ds.Add(ctx, &app1)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
Expect(ds.Add(ctx, &app2)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
Expect(ds.Add(ctx, &appComp1)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
Expect(ds.Add(ctx, &appComp2)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
Expect(ds.Add(ctx, &appComp3)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "addon-fluxcd", Labels: map[string]string{oam.LabelAddonName: "fluxcd"}}, Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{},
}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: "vela-system", Name: "addon-rollout", Labels: map[string]string{oam.LabelAddonName: "rollout"}}, Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{},
}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
}
BeforeEach(func() {
var err error
ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "target-test-kubevela"})
Expect(ds).ShouldNot(BeNil())
Expect(err).Should(BeNil())
testProject = "test-cronjob-project"
mockDataInDs()
i = InfoCalculateCronJob{
ds: ds,
}
systemInfo := model.SystemInfo{InstallID: "test-id", EnableCollection: true}
Expect(ds.Add(ctx, &systemInfo)).Should(SatisfyAny(BeNil(), DataExistMatcher{}))
})
It("Test calculate app Info", func() {
appNum, topKCom, topKTrait, _, _, err := i.calculateAppInfo(ctx)
Expect(err).Should(BeNil())
Expect(appNum).Should(BeEquivalentTo(2))
Expect(topKCom).Should(BeEquivalentTo([]string{"webservice", "helm"}))
Expect(topKTrait).Should(BeEquivalentTo([]string{"rollout", "patch", "expose"}))
})
It("Test calculate addon Info", func() {
enabledAddon, err := i.calculateAddonInfo(ctx)
Expect(err).Should(BeNil())
Expect(enabledAddon).Should(BeEquivalentTo(map[string]string{
"fluxcd": "enabling",
"rollout": "enabling",
}))
})
It("Test calculate cluster Info", func() {
clusterNum, err := i.calculateClusterInfo(ctx)
Expect(err).Should(BeNil())
Expect(clusterNum).Should(BeEquivalentTo(1))
})
It("Test calculateAndUpdate func", func() {
systemInfo := model.SystemInfo{}
es, err := ds.List(ctx, &systemInfo, &datastore.ListOptions{})
Expect(err).Should(BeNil())
Expect(len(es)).Should(BeEquivalentTo(1))
info, ok := es[0].(*model.SystemInfo)
Expect(ok).Should(BeTrue())
Expect(info.InstallID).Should(BeEquivalentTo("test-id"))
Expect(i.calculateAndUpdate(ctx, *info)).Should(BeNil())
systemInfo = model.SystemInfo{}
es, err = ds.List(ctx, &systemInfo, &datastore.ListOptions{})
Expect(err).Should(BeNil())
Expect(len(es)).Should(BeEquivalentTo(1))
info, ok = es[0].(*model.SystemInfo)
Expect(ok).Should(BeTrue())
Expect(info.InstallID).Should(BeEquivalentTo("test-id"))
Expect(info.StatisticInfo.AppCount).Should(BeEquivalentTo("<10"))
Expect(info.StatisticInfo.ClusterCount).Should(BeEquivalentTo("<3"))
Expect(info.StatisticInfo.TopKCompDef).Should(BeEquivalentTo([]string{"webservice", "helm"}))
Expect(info.StatisticInfo.TopKTraitDef).Should(BeEquivalentTo([]string{"rollout", "patch", "expose"}))
Expect(info.StatisticInfo.EnabledAddon).Should(BeEquivalentTo(map[string]string{
"fluxcd": "enabling",
"rollout": "enabling",
}))
})
It("Test run func", func() {
app3 := model.Application{BaseModel: model.BaseModel{CreateTime: time.Now()}, Name: "app3", Project: testProject}
Expect(ds.Add(ctx, &app3)).Should(BeNil())
systemInfo := model.SystemInfo{InstallID: "test-id", EnableCollection: false}
Expect(ds.Put(ctx, &systemInfo)).Should(BeNil())
Expect(i.run()).Should(BeNil())
})
})
func TestGenCountInfo(t *testing.T) {
testcases := []struct {
count int
res string
}{
{
count: 3,
res: "<10",
},
{
count: 14,
res: "<50",
},
{
count: 80,
res: "<100",
},
{
count: 350,
res: "<500",
},
{
count: 1800,
res: "<2000",
},
{
count: 4000,
res: "<5000",
},
{
count: 9000,
res: "<10000",
},
{
count: 30000,
res: ">=10000",
},
}
for _, testcase := range testcases {
assert.Equal(t, genCountInfo(testcase.count), testcase.res)
}
}
func TestGenClusterCountInfo(t *testing.T) {
testcases := []struct {
count int
res string
}{
{
count: 2,
res: "<3",
},
{
count: 7,
res: "<10",
},
{
count: 34,
res: "<50",
},
{
count: 100,
res: ">=50",
},
}
for _, testcase := range testcases {
assert.Equal(t, genClusterCountInfo(testcase.count), testcase.res)
}
}
func TestTopKFrequent(t *testing.T) {
testCases := []struct {
def map[string]int
k int
res []string
}{
{
def: map[string]int{
"rollout": 4,
"patch": 3,
"expose": 6,
},
k: 3,
res: []string{"expose", "rollout", "patch"},
},
{
// just return top2
def: map[string]int{
"rollout": 4,
"patch": 3,
"expose": 6,
},
k: 2,
res: []string{"expose", "rollout"},
},
}
for _, testCase := range testCases {
assert.DeepEqual(t, topKFrequent(testCase.def, testCase.k), testCase.res)
}
}
type DataExistMatcher struct{}
// Match matches error.
func (matcher DataExistMatcher) Match(actual interface{}) (success bool, err error) {
if actual == nil {
return false, nil
}
actualError := actual.(error)
return errors.Is(actualError, datastore.ErrRecordExist), nil
}
// FailureMessage builds an error message.
func (matcher DataExistMatcher) FailureMessage(actual interface{}) (message string) {
return format.Message(actual, "to be already exist")
}
// NegatedFailureMessage builds an error message.
func (matcher DataExistMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return format.Message(actual, "not to be already exist")
}

View File

@@ -16,6 +16,8 @@ limitations under the License.
package model
import "time"
func init() {
RegisterModel(&SystemInfo{})
}
@@ -30,10 +32,11 @@ const (
// SystemInfo systemInfo model
type SystemInfo struct {
BaseModel
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
DexConfig DexConfig `json:"dexConfig,omitempty"`
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
DexConfig DexConfig `json:"dexConfig,omitempty"`
StatisticInfo StatisticInfo `json:"statisticInfo,omitempty"`
}
// DexConfig dex config
@@ -46,6 +49,18 @@ type DexConfig struct {
EnablePasswordDB bool `json:"enablePasswordDB"`
}
// StatisticInfo the system statistic info
type StatisticInfo struct {
ClusterCount string `json:"clusterCount,omitempty"`
AppCount string `json:"appCount,omitempty"`
EnabledAddon map[string]string `json:"enabledAddon,omitempty"`
TopKCompDef []string `json:"topKCompDef,omitempty"`
TopKTraitDef []string `json:"topKTraitDef,omitempty"`
TopKWorkflowStepDef []string `json:"topKWorkflowStepDef,omitempty"`
TopKPolicyDef []string `json:"topKPolicyDef,omitempty"`
UpdateTime time.Time `json:"updateTime,omitempty"`
}
// DexStorage dex storage
type DexStorage struct {
Type string `json:"type"`

View File

@@ -194,13 +194,17 @@ type ConfigType struct {
// Config define the metadata of a config
type Config struct {
ConfigType string `json:"configType"`
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"`
ConfigType string `json:"configType"`
ConfigTypeAlias string `json:"configTypeAlias"`
Name string `json:"name"`
Project string `json:"project"`
Identifier string `json:"identifier"`
Alias string `json:"alias"`
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
@@ -405,6 +409,7 @@ type CreateApplicationRequest struct {
type CreateConfigRequest struct {
Name string `json:"name" validate:"checkname"`
Alias string `json:"alias"`
Description string `json:"description"`
Project string `json:"project"`
ComponentType string `json:"componentType" validate:"checkname"`
Properties string `json:"properties,omitempty"`
@@ -1111,13 +1116,27 @@ type DetailRevisionResponse struct {
type SystemInfoResponse struct {
SystemInfo
SystemVersion SystemVersion `json:"systemVersion"`
StatisticInfo StatisticInfo `json:"statisticInfo,omitempty"`
}
// SystemInfo system info
type SystemInfo struct {
InstallID string `json:"installID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
PlatformID string `json:"platformID"`
EnableCollection bool `json:"enableCollection"`
LoginType string `json:"loginType"`
InstallTime time.Time `json:"installTime,omitempty"`
}
// StatisticInfo generated by cronJob running in backend
type StatisticInfo struct {
ClusterCount string `json:"clusterCount,omitempty"`
AppCount string `json:"appCount,omitempty"`
EnableAddonList map[string]string `json:"enableAddonList,omitempty"`
ComponentDefinitionTopList []string `json:"componentDefinitionTopList,omitempty"`
TraitDefinitionTopList []string `json:"traitDefinitionTopList,omitempty"`
WorkflowDefinitionTopList []string `json:"workflowDefinitionTopList,omitempty"`
PolicyDefinitionTopList []string `json:"policyDefinitionTopList,omitempty"`
UpdateTime time.Time `json:"updateTime,omitempty"`
}
// SystemInfoRequest request by update SystemInfo

View File

@@ -23,6 +23,8 @@ import (
"os"
"time"
"github.com/oam-dev/kubevela/pkg/apiserver/collect"
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
"github.com/go-openapi/spec"
@@ -60,6 +62,9 @@ type Config struct {
// AddonCacheTime is how long between two cache operations
AddonCacheTime time.Duration
// DisableStatisticCronJob close the calculate system info cronJob
DisableStatisticCronJob bool
}
type leaderConfig struct {
@@ -141,6 +146,10 @@ func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
go velasync.Start(ctx, s.dataStore, restCfg, s.usecases)
if !s.cfg.DisableStatisticCronJob {
collect.StartCalculatingInfoCronJob(s.dataStore)
}
// this process would block the whole process, any other handler should start before this func
s.runWorkflowRecordSync(ctx, s.cfg.LeaderConfig.Duration)
},
OnStoppedLeading: func() {

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"
@@ -705,21 +705,9 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
}
// sync configs to clusters
// TODO(zzxwill) need to check the type of the componentDefinition, if it is `Cloud`, skip the sync
targets, err := listTarget(ctx, c.ds, app.Project, nil)
if err != nil {
if err := c.syncConfigs4Application(ctx, oamApp, app.Project, workflow.EnvName); err != nil {
return nil, err
}
var clusterTargets []*model.ClusterTarget
for i, t := range targets {
if t.Cluster != nil {
clusterTargets = append(clusterTargets, targets[i].Cluster)
}
}
if err := SyncConfigs(ctx, c.kubeClient, app.Project, clusterTargets); err != nil {
return nil, fmt.Errorf("sync config failure %w", err)
}
// step2: check and create deploy event
if !req.Force {
@@ -809,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

@@ -94,7 +94,6 @@ type authHandler interface {
}
type dexHandlerImpl struct {
token *oauth2.Token
idToken *oidc.IDToken
ds datastore.DataStore
}
@@ -135,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
@@ -183,11 +181,11 @@ func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.L
if userBase.Disabled {
return nil, bcode.ErrUserAlreadyDisabled
}
accessToken, err := a.generateJWTToken(ctx, userBase.Name, GrantTypeAccess, time.Hour)
accessToken, err := a.generateJWTToken(userBase.Name, GrantTypeAccess, time.Hour)
if err != nil {
return nil, err
}
refreshToken, err := a.generateJWTToken(ctx, userBase.Name, GrantTypeRefresh, time.Hour*24)
refreshToken, err := a.generateJWTToken(userBase.Name, GrantTypeRefresh, time.Hour*24)
if err != nil {
return nil, err
}
@@ -198,7 +196,7 @@ func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.L
}, nil
}
func (a *authenticationUsecaseImpl) generateJWTToken(ctx context.Context, username, grantType string, expireDuration time.Duration) (string, error) {
func (a *authenticationUsecaseImpl) generateJWTToken(username, grantType string, expireDuration time.Duration) (string, error) {
expire := time.Now().Add(expireDuration)
claims := model.CustomClaims{
StandardClaims: jwt.StandardClaims{
@@ -210,24 +208,7 @@ func (a *authenticationUsecaseImpl) generateJWTToken(ctx context.Context, userna
GrantType: grantType,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := a.getSignedKey(ctx)
if err != nil {
return "", err
}
return token.SignedString([]byte(signed))
}
func (a *authenticationUsecaseImpl) getSignedKey(ctx context.Context) (string, error) {
if signedKey != "" {
return signedKey, nil
}
info, err := a.sysUsecase.Get(ctx)
if err != nil {
return "", err
}
signedKey = info.InstallID
return signedKey, nil
return token.SignedString([]byte(signedKey))
}
func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error) {
@@ -239,7 +220,7 @@ func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshTok
return nil, err
}
if claim.GrantType == GrantTypeRefresh {
accessToken, err := a.generateJWTToken(ctx, claim.Username, GrantTypeAccess, time.Hour)
accessToken, err := a.generateJWTToken(claim.Username, GrantTypeAccess, time.Hour)
if err != nil {
return nil, err
}
@@ -429,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,
@@ -450,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

@@ -28,7 +28,6 @@ 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"
@@ -68,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,
}
@@ -86,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() {
@@ -175,6 +184,8 @@ var _ = Describe("Test authentication usecase functions", func() {
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",

View File

@@ -20,15 +20,15 @@ import (
"context"
"encoding/json"
"fmt"
"time"
"strings"
set "github.com/deckarep/golang-set"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/yaml"
"github.com/pkg/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"
@@ -44,7 +44,11 @@ const (
definitionAlias = definition.UserPrefix + "alias.config.oam.dev"
definitionType = definition.UserPrefix + "type.config.oam.dev"
velaCoreConfig = "velacore-config"
velaCoreConfig = "velacore-config"
configIsReady = "Ready"
configIsNotReady = "Not ready"
terraformProviderAlias = "Terraform Cloud Provider"
configSyncProjectPrefix = "config-sync"
)
// ConfigHandler handle CRUD of configs
@@ -98,18 +102,21 @@ func (u *configUseCaseImpl) ListConfigTypes(ctx context.Context, query string) (
})
}
tfType := &apis.ConfigType{
Alias: "Terraform Cloud Provider",
Name: types.TerraformProvider,
}
definitions := make([]string, len(tfDefs))
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
for i, tf := range tfDefs {
definitions[i] = tf.Name
}
tfType.Definitions = definitions
return append(configTypes, tfType), nil
return append(configTypes, tfType), nil
}
return configTypes, nil
}
// GetConfigType returns a config type
@@ -128,12 +135,27 @@ func (u *configUseCaseImpl) GetConfigType(ctx context.Context, configType string
}
func (u *configUseCaseImpl) CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error {
p := req.Properties
// If the component is Terraform type, set the provider name same as the application name and the component name
if strings.HasPrefix(req.ComponentType, types.TerrfaormComponentPrefix) {
var properties map[string]interface{}
if err := json.Unmarshal([]byte(p), &properties); err != nil {
return errors.Wrapf(err, "unable to process the properties of %s", req.ComponentType)
}
properties["name"] = req.Name
tmp, err := json.Marshal(properties)
if err != nil {
return errors.Wrapf(err, "unable to process the properties of %s", req.ComponentType)
}
p = string(tmp)
}
app := v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: req.Name,
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
types.AnnotationConfigAlias: req.Alias,
types.AnnotationConfigAlias: req.Alias,
types.AnnotationConfigDescription: req.Description,
},
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
@@ -147,40 +169,12 @@ func (u *configUseCaseImpl) CreateConfig(ctx context.Context, req apis.CreateCon
{
Name: req.Name,
Type: req.ComponentType,
Properties: &runtime.RawExtension{Raw: []byte(req.Properties)},
Properties: &runtime.RawExtension{Raw: []byte(p)},
},
},
},
}
if err := u.kubeClient.Create(ctx, &app); err != nil {
return err
}
// try to check whether the underlying config secrets is successfully created
var succeeded bool
var configApp v1beta1.Application
for i := 0; i < 100; i++ {
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: req.Name}, &configApp); err == nil {
if configApp.Status.Phase == common.ApplicationRunning {
succeeded = true
break
}
}
time.Sleep(time.Second)
}
// clean up failed application
if !succeeded {
if err := u.kubeClient.Delete(ctx, &app); err != nil {
return err
}
return errors.New("failed to create config")
}
if succeeded && req.ComponentType == types.DexConnector {
return u.authenticationUseCase.UpdateDexConfig(ctx)
}
return nil
return u.kubeClient.Create(ctx, &app)
}
func (u *configUseCaseImpl) GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error) {
@@ -223,11 +217,12 @@ func (u *configUseCaseImpl) getConfigsByConfigType(ctx context.Context, configTy
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),
configs[i] = retrieveConfigFromApplication(a, a.Labels[types.LabelConfigProject])
switch a.Status.Phase {
case common.ApplicationRunning:
configs[i].Status = configIsReady
default:
configs[i].Status = configIsNotReady
}
}
return configs, nil
@@ -265,13 +260,12 @@ type ApplicationDeployTarget struct {
// SyncConfigs will sync configs to working clusters
func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, targets []*model.ClusterTarget) error {
name := fmt.Sprintf("config-sync-%s", project)
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.LabelConfigProject: project,
types.LabelConfigSyncToMultiCluster: "true",
}); err != nil {
return err
@@ -279,13 +273,19 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
if len(secrets.Items) == 0 {
return nil
}
objects := make([]map[string]string, len(secrets.Items))
for i, s := range secrets.Items {
objects[i] = map[string]string{
"name": s.Name,
"resource": "secret",
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
@@ -297,6 +297,26 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
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{
@@ -316,11 +336,10 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
},
Policies: policies,
},
}
if err := k8sClient.Create(ctx, scratch); err != nil {
return err
}
return k8sClient.Create(ctx, scratch)
}
// config sync application exists, update it
app.Spec.Components = []common.ApplicationComponent{
@@ -340,6 +359,11 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, 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)
@@ -355,21 +379,27 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
}
}
app.Spec.Policies = mergedPolicies
out, _ := yaml.Marshal(app)
fmt.Println(string(out))
return k8sClient.Update(ctx, app)
}
func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.ClusterTarget) []ApplicationDeployTarget {
var mergedTargets []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 targets {
for _, t := range clusterTargets {
if c.Namespace == t.Namespace {
hasSameNamespace = true
clusters := append(c.Clusters, t.ClusterName)
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace, Clusters: clusters})
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 {
@@ -377,7 +407,7 @@ func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.Clu
}
}
for _, t := range targets {
for _, t := range clusterTargets {
var hasSameNamespace bool
for _, c := range currentTargets {
if c.Namespace == t.Namespace {
@@ -385,9 +415,75 @@ func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.Clu
}
}
if !hasSameNamespace {
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: t.Namespace, Clusters: []string{t.ClusterName}})
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

@@ -18,12 +18,14 @@ package usecase
import (
"context"
"encoding/json"
"sort"
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
. "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"
@@ -32,6 +34,7 @@ import (
"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"
@@ -233,6 +236,11 @@ func TestCreateConfig(t *testing.T) {
ctx := context.Background()
properties, err := json.Marshal(map[string]interface{}{
"name": "default",
})
assert.NilError(t, err)
testcases := []struct {
name string
args args
@@ -249,6 +257,18 @@ func TestCreateConfig(t *testing.T) {
},
},
},
{
name: "create terraform-alibaba config",
args: args{
h: h,
req: apis.CreateConfigRequest{
Name: "n1",
ComponentType: "terraform-alibaba",
Project: "p1",
Properties: string(properties),
},
},
},
}
for _, tc := range testcases {
@@ -338,3 +358,249 @@ func TestGetConfigs(t *testing.T) {
})
}
}
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

@@ -26,10 +26,13 @@ import (
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"
@@ -60,33 +63,65 @@ type defaultHelmHandler struct {
k8sClient client.Client
}
func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, secretName string, skipCache bool) ([]string, error) {
// TODO(wangyikewxgm): support authority helm repo
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, secretName 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, secretName 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))

View File

@@ -19,6 +19,10 @@ package usecase
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
. "github.com/onsi/ginkgo"
@@ -104,6 +108,90 @@ var _ = Describe("Test helm repo list", func() {
})
})
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",
@@ -266,5 +354,19 @@ metadata:
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,100 @@ 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], types.TerrfaormComponentPrefix) {
continue
}
configs = append(configs, retrieveConfigFromApplication(a, appProject))
}
configs = append(configs, legacyTerraformProviders...)
case "":
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if appProject != "" && appProject != projectName {
continue
}
configs = append(configs, retrieveConfigFromApplication(a, appProject))
}
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, retrieveConfigFromApplication(a, appProject))
}
}
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{
@@ -492,3 +598,15 @@ func ConvertProjectUserModel2Base(user *model.ProjectUser) *apisv1.ProjectUserBa
}
return base
}
func retrieveConfigFromApplication(a v1beta1.Application, project string) *apisv1.Config {
return &apisv1.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: project,
CreatedTime: &(a.CreationTimestamp.Time),
ApplicationStatus: a.Status.Phase,
Alias: a.Annotations[types.AnnotationConfigAlias],
Description: a.Annotations[types.AnnotationConfigDescription],
}
}

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",
},

View File

@@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"reflect"
"time"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -81,6 +82,7 @@ func (u systemInfoUsecaseImpl) Get(ctx context.Context) (*model.SystemInfo, erro
info.InstallID = installID
info.EnableCollection = true
info.LoginType = model.LoginTypeLocal
info.BaseModel = model.BaseModel{CreateTime: time.Now()}
err = u.ds.Add(ctx, info)
if err != nil {
return nil, err
@@ -100,6 +102,16 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf
VelaVersion: version.VelaVersion,
GitVersion: version.GitRevision,
},
StatisticInfo: v1.StatisticInfo{
AppCount: info.StatisticInfo.AppCount,
ClusterCount: info.StatisticInfo.ClusterCount,
EnableAddonList: info.StatisticInfo.EnabledAddon,
ComponentDefinitionTopList: info.StatisticInfo.TopKCompDef,
TraitDefinitionTopList: info.StatisticInfo.TopKTraitDef,
WorkflowDefinitionTopList: info.StatisticInfo.TopKWorkflowStepDef,
PolicyDefinitionTopList: info.StatisticInfo.TopKPolicyDef,
UpdateTime: info.StatisticInfo.UpdateTime,
},
}, nil
}
@@ -114,10 +126,19 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
LoginType: sysInfo.LoginType,
BaseModel: model.BaseModel{
CreateTime: info.CreateTime,
UpdateTime: time.Now(),
},
StatisticInfo: info.StatisticInfo,
}
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
}
@@ -128,24 +149,32 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
}
return &v1.SystemInfoResponse{
SystemInfo: v1.SystemInfo{
InstallID: modifiedInfo.InstallID,
PlatformID: modifiedInfo.InstallID,
EnableCollection: modifiedInfo.EnableCollection,
LoginType: modifiedInfo.LoginType,
// always use the initial createTime as system's installTime
InstallTime: info.CreateTime,
},
SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision},
}, nil
}
func (u systemInfoUsecaseImpl) Init(ctx context.Context) error {
_, err := initDexConfig(ctx, u.kubeClient, "http://velaux.com", &model.SystemInfo{})
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,
PlatformID: info.InstallID,
EnableCollection: info.EnableCollection,
LoginType: info.LoginType,
InstallTime: info.CreateTime,
}
}
@@ -158,6 +187,9 @@ func generateDexConfig(ctx context.Context, kubeClient client.Client, velaAddres
if err != nil {
return err
}
if len(connectors) < 1 {
return bcode.ErrNoDexConnector
}
config, err := model.NewJSONStructByStruct(info.DexConfig)
if err != nil {
return err

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
@@ -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

View File

@@ -37,4 +37,6 @@ var (
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

@@ -35,4 +35,6 @@ var (
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

@@ -296,7 +296,7 @@ func (s *enabledAddonWebService) GetWebService() *restful.WebService {
Filter(s.rbacUsecase.CheckPerm("addon", "list")).
Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")).
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
Returns(200, "OK", apis.ListAddonResponse{}).
Returns(200, "OK", apis.ListEnabledAddonResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListAddonResponse{}))

View File

@@ -20,7 +20,6 @@ import (
restfulspec "github.com/emicklei/go-restful-openapi/v2"
"github.com/emicklei/go-restful/v3"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
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"
@@ -151,7 +150,10 @@ func (s *configWebService) createConfig(req *restful.Request, res *restful.Respo
err := s.handler.CreateConfig(req.Request.Context(), createReq)
if err != nil {
log.Logger.Errorf("failed to create config: %s", err.Error())
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}

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

@@ -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

@@ -117,8 +117,16 @@ func convertStepProperties(step *v1beta1.WorkflowStep, app *v1beta1.Application)
return err
}
var componentNames []string
for _, c := range app.Spec.Components {
componentNames = append(componentNames, c.Name)
}
for _, c := range app.Spec.Components {
if c.Name == o.Component {
if dcName, ok := checkDependsOnValidComponent(c.DependsOn, componentNames); !ok {
return errors.Errorf("component %s not found, which is depended by %s", dcName, c.Name)
}
step.Inputs = append(step.Inputs, c.Inputs...)
for index := range step.Inputs {
parameterKey := strings.TrimSpace(step.Inputs[index].ParameterKey)
@@ -135,11 +143,23 @@ func convertStepProperties(step *v1beta1.WorkflowStep, app *v1beta1.Application)
step.Properties = util.Object2RawExtension(c)
return nil
}
}
return errors.Errorf("component %s not found", o.Component)
}
func checkDependsOnValidComponent(dependsOnComponentNames, allComponentNames []string) (string, bool) {
// does not depends on other components
if dependsOnComponentNames == nil {
return "", true
}
for _, dc := range dependsOnComponentNames {
if !utils.StringsContain(allComponentNames, dc) {
return dc, false
}
}
return "", true
}
func (h *AppHandler) renderComponentFunc(appParser *appfile.Parser, appRev *v1beta1.ApplicationRevision, af *appfile.Appfile) oamProvider.ComponentRender {
return func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string, env string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) {
ctx := multicluster.ContextWithClusterName(context.Background(), clusterName)

View File

@@ -240,4 +240,118 @@ var _ = Describe("Test Application workflow generator", func() {
_, _, err = renderFunc(comp, nil, "", "", "")
Expect(err).Should(BeNil())
})
It("Test generate application workflow with dependsOn", func() {
app := &oamcore.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app-with-input-output",
Namespace: namespaceName,
},
Spec: oamcore.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "myweb1",
Type: "worker-with-health",
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
},
{
Name: "myweb2",
Type: "worker-with-health",
DependsOn: []string{"myweb1"},
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox","lives": "i am lives","enemies": "empty"}`)},
},
},
},
}
af, err := appParser.GenerateAppFile(ctx, app)
Expect(err).Should(BeNil())
appRev := &oamcore.ApplicationRevision{}
handler, err := NewAppHandler(ctx, reconciler, app, appParser)
Expect(err).Should(Succeed())
taskRunner, err := handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev)
Expect(err).To(BeNil())
Expect(len(taskRunner)).Should(BeEquivalentTo(2))
Expect(taskRunner[0].Name()).Should(BeEquivalentTo("myweb1"))
Expect(taskRunner[1].Name()).Should(BeEquivalentTo("myweb2"))
})
It("Test generate application workflow with invalid dependsOn", func() {
app := &oamcore.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app-with-input-output",
Namespace: namespaceName,
},
Spec: oamcore.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "myweb1",
Type: "worker-with-health",
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
},
{
Name: "myweb2",
Type: "worker-with-health",
DependsOn: []string{"myweb0"},
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox","lives": "i am lives","enemies": "empty"}`)},
},
},
},
}
af, err := appParser.GenerateAppFile(ctx, app)
Expect(err).Should(BeNil())
appRev := &oamcore.ApplicationRevision{}
handler, err := NewAppHandler(ctx, reconciler, app, appParser)
Expect(err).Should(Succeed())
_, err = handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev)
Expect(err).NotTo(BeNil())
})
It("Test generate application workflow with multiple invalid dependsOn", func() {
app := &oamcore.Application{
TypeMeta: metav1.TypeMeta{
Kind: "Application",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "app-with-input-output",
Namespace: namespaceName,
},
Spec: oamcore.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "myweb1",
Type: "worker-with-health",
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox"}`)},
},
{
Name: "myweb2",
Type: "worker-with-health",
DependsOn: []string{"myweb1", "myweb0", "myweb3"},
Properties: &runtime.RawExtension{Raw: []byte(`{"cmd":["sleep","1000"],"image":"busybox","lives": "i am lives","enemies": "empty"}`)},
},
},
},
}
af, err := appParser.GenerateAppFile(ctx, app)
Expect(err).Should(BeNil())
appRev := &oamcore.ApplicationRevision{}
handler, err := NewAppHandler(ctx, reconciler, app, appParser)
Expect(err).Should(Succeed())
_, err = handler.GenerateApplicationSteps(ctx, app, appParser, af, appRev)
Expect(err).NotTo(BeNil())
})
})

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

@@ -29,12 +29,15 @@ const (
LegacyObjectTypeIdentifier featuregate.Feature = "LegacyObjectTypeIdentifier"
// DeprecatedObjectLabelSelector enable the use of deprecated object label selector for selecting ref-object
DeprecatedObjectLabelSelector featuregate.Feature = "DeprecatedObjectLabelSelector"
// LegacyResourceTrackerGC enable the gc of legacy resource tracker in managed clusters
LegacyResourceTrackerGC featuregate.Feature = "LegacyResourceTrackerGC"
)
var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
DeprecatedPolicySpec: {Default: false, PreRelease: featuregate.Alpha},
LegacyObjectTypeIdentifier: {Default: false, PreRelease: featuregate.Alpha},
DeprecatedObjectLabelSelector: {Default: false, PreRelease: featuregate.Alpha},
LegacyResourceTrackerGC: {Default: true, PreRelease: featuregate.Alpha},
}
func init() {

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

@@ -35,6 +35,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/common"
errors3 "github.com/oam-dev/kubevela/pkg/utils/errors"
@@ -46,7 +47,7 @@ const (
// ClusterContextKey is the name of cluster using in client http context
ClusterContextKey = contextKey("ClusterName")
// ClusterLocalName specifies the local cluster
ClusterLocalName = "local"
ClusterLocalName = velatypes.ClusterLocalName
)
var (

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

@@ -120,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
}

View File

@@ -30,10 +30,12 @@ import (
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
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/monitor/metrics"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
@@ -319,6 +321,10 @@ func (h *gcHandler) GarbageCollectComponentRevisionResourceTracker(ctx context.C
const velaVersionNumberToUpgradeResourceTracker = "v1.2.0"
func (h *gcHandler) GarbageCollectLegacyResourceTrackers(ctx context.Context) error {
// skip legacy gc if controller not enable this feature
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.LegacyResourceTrackerGC) {
return nil
}
// skip legacy gc if application is not handled by new version rt
if h.app.GetDeletionTimestamp() == nil && h.resourceKeeper._currentRT == nil {
return 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
@@ -312,9 +321,17 @@ 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 {
var resourceNameClusterObjectReferenceFilter = func(resourceName []string) clusterObjectReferenceFilter {
return func(reference common.ClusterObjectReference) bool {
return resourceName == reference.Name
if len(resourceName) == 0 {
return true
}
for _, r := range resourceName {
if r == reference.Name {
return true
}
}
return false
}
}
@@ -421,24 +438,31 @@ 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, resourceName ...string) (*common.ClusterObjectReference, error) {
filters := []clusterObjectReferenceFilter{isWorkloadClusterObjectReferenceFilter}
for _, n := range resourceName {
filters = append(filters, resourceNameClusterObjectReferenceFilter(n))
}
_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, resourceName ...string) (*common.ClusterObjectReference, error) {
filters := []clusterObjectReferenceFilter{isPortForwardEndpointClusterObjectReferenceFilter}
for _, n := range resourceName {
filters = append(filters, resourceNameClusterObjectReferenceFilter(n))
}
_resourceName := removeEmptyString(resourceName)
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
return askToChooseOneResource(app, filters...)
}

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,44 @@ 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 {
klog.Errorf("query the resourcetrackers failure %s", err.Error())
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 {
// For the application with apply once policy, there is no data in RT.
_, obj, err = getObjectCreatedByComponent(c.k8sClient, managedResource.ObjectReference, managedResource.Cluster)
if err != nil {
klog.Errorf("get obj from the cluster failure %s", err.Error())
continue
}
}
clusterName := managedResource.Cluster
if clusterName == "" {
clusterName = multicluster.ClusterLocalName
}
resources = append(resources, Resource{
Cluster: clusterName,
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 +179,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 +191,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

@@ -31,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
@@ -83,7 +84,8 @@ var _ = Describe("Test Query Provider", func() {
Name: "test",
Namespace: "test",
Annotations: map[string]string{
"oam.dev/kubevela-version": "v1.2.0-beta.2",
oam.AnnotationKubeVelaVersion: "v1.3.1",
oam.AnnotationPublishVersion: "v1",
},
},
Spec: v1beta1.ApplicationSpec{
@@ -154,6 +156,53 @@ var _ = Describe("Test Query Provider", func() {
})
Expect(k8sClient.Create(ctx, appService)).Should(BeNil())
rt := &v1beta1.ResourceTracker{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-v1-%s", oldApp.Name, oldApp.Namespace),
Labels: map[string]string{
oam.LabelAppName: oldApp.Name,
oam.LabelAppNamespace: oldApp.Namespace,
},
Annotations: map[string]string{
oam.AnnotationPublishVersion: "v1",
},
},
Spec: v1beta1.ResourceTrackerSpec{
ManagedResources: []v1beta1.ManagedResource{
{
ClusterObjectReference: common.ClusterObjectReference{
Cluster: "",
ObjectReference: corev1.ObjectReference{
APIVersion: "v1",
Kind: "Service",
Namespace: namespace,
Name: "web",
},
},
OAMObjectReference: common.OAMObjectReference{
Component: "web",
},
},
{
ClusterObjectReference: common.ClusterObjectReference{
Cluster: "",
ObjectReference: corev1.ObjectReference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: namespace,
Name: "web",
},
},
OAMObjectReference: common.OAMObjectReference{
Component: "web",
},
},
},
Type: v1beta1.ResourceTrackerTypeVersioned,
},
}
Expect(k8sClient.Create(ctx, rt)).Should(BeNil())
prd := provider{cli: k8sClient}
opt := `app: {
name: "test"
@@ -170,6 +219,9 @@ var _ = Describe("Test Query Provider", func() {
appResList := new(AppResourcesList)
Expect(v.UnmarshalTo(appResList)).Should(BeNil())
if appResList.Err != "" {
klog.Error(appResList.Err)
}
Expect(len(appResList.List)).Should(Equal(2))
@@ -180,7 +232,7 @@ var _ = Describe("Test Query Provider", func() {
Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&app), updateApp)).Should(BeNil())
updateApp.ObjectMeta.Annotations = map[string]string{
"oam.dev/kubevela-version": "master",
oam.AnnotationKubeVelaVersion: "v1.1.0",
}
Expect(k8sClient.Update(ctx, updateApp)).Should(BeNil())
newValue, err := value.NewValue(opt, nil, "")

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,58 +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 !resourcekeeper.AllowCrossNamespaceResource && (ns != p.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 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

@@ -19,7 +19,7 @@ package template
import (
"context"
"embed"
"path/filepath"
"fmt"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
@@ -60,7 +60,8 @@ func (loader *WorkflowStepLoader) LoadTaskTemplate(ctx context.Context, name str
staticFilename := name + ".cue"
for _, file := range files {
if staticFilename == file.Name() {
content, err := templateFS.ReadFile(filepath.Join(templateDir, file.Name()))
fileName := fmt.Sprintf("%s/%s", templateDir, file.Name())
content, err := templateFS.ReadFile(fileName)
return string(content), err
}
}

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
@@ -442,6 +443,7 @@ func generateAddonInfo(name string, status pkgaddon.Status) string {
for c := range status.Clusters {
ic = append(ic, c)
}
sort.Strings(ic)
res += fmt.Sprintf("installedClusters: %s \n", ic)
}
return 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.")
}

View File

@@ -108,7 +108,7 @@ func getPrompt(cmd *cobra.Command, reader *bufio.Reader, description string, pro
func loadYAMLBytesFromFileOrHTTP(pathOrURL string) ([]byte, error) {
if strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://") {
return common.HTTPGet(context.Background(), pathOrURL)
return common.HTTPGetWithOption(context.Background(), pathOrURL, nil)
}
return os.ReadFile(path.Clean(pathOrURL))
}
@@ -295,7 +295,7 @@ func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, p
}
switch provider {
case "aws", "azure", "alibaba", "tencent", "gcp", "baidu", "elastic":
case "aws", "azure", "alibaba", "tencent", "gcp", "baidu", "elastic", "ucloud":
var terraform *commontype.Terraform
git, err := cmd.Flags().GetString(FlagGit)
@@ -371,7 +371,7 @@ func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, p
}
return out.String(), nil
default:
return "", errors.Errorf("Provider `%s` is not supported. Only `alibaba`, `aws`, `azure` are supported.", provider)
return "", errors.Errorf("Provider `%s` is not supported. Only `alibaba`, `aws`, `azure`, `gcp`, `baidu`, `tencent`, `elastic`, `ucloud` are supported.", provider)
}
}

View File

@@ -106,7 +106,7 @@ func NewInstallCommand(c common.Args, order string, ioStreams util.IOStreams) *c
if installArgs.ChartFilePath == "" {
installArgs.ChartFilePath = getKubeVelaHelmChartRepoURL(installArgs.Version)
}
chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath)
chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath, nil)
if err != nil {
return fmt.Errorf("loadding the helm chart of kubeVela control plane failure, %w", err)
}

View File

@@ -47,8 +47,6 @@ const (
// ProviderNamespace is the namespace of Terraform Cloud Provider
ProviderNamespace = "default"
labelVal = "terraform-provider"
providerNameParam = "name"
)
@@ -63,24 +61,24 @@ func NewProviderCommand(c common.Args, order string, ioStreams cmdutil.IOStreams
types.TagCommandType: types.TypeExtension,
},
}
add, err := prepareProviderAddCommand(c)
add, err := prepareProviderAddCommand(c, ioStreams)
if err == nil {
cmd.AddCommand(add)
}
delete, err := prepareProviderDeleteCommand(c)
delete, err := prepareProviderDeleteCommand(c, ioStreams)
if err == nil {
cmd.AddCommand(delete)
}
cmd.AddCommand(
NewProviderListCommand(c),
NewProviderListCommand(c, ioStreams),
)
return cmd
}
// NewProviderListCommand create addon list command
func NewProviderListCommand(c common.Args) *cobra.Command {
func NewProviderListCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
return &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
@@ -91,7 +89,7 @@ func NewProviderListCommand(c common.Args) *cobra.Command {
if err != nil {
return err
}
err = listProviders(context.Background(), k8sClient)
err = listProviders(context.Background(), k8sClient, ioStreams)
if err != nil {
return err
}
@@ -100,7 +98,7 @@ func NewProviderListCommand(c common.Args) *cobra.Command {
}
}
func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
func prepareProviderAddCommand(c common.Args, ioStreams cmdutil.IOStreams) (*cobra.Command, error) {
ctx := context.Background()
k8sClient, err := c.GetClient()
if err != nil {
@@ -114,7 +112,7 @@ func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
Example: "vela provider add <provider-type>",
}
addSubCommands, err := prepareProviderAddSubCommand(c)
addSubCommands, err := prepareProviderAddSubCommand(c, ioStreams)
if err != nil {
return nil, err
}
@@ -153,7 +151,7 @@ func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
return cmd, nil
}
func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
func prepareProviderAddSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
ctx := context.Background()
k8sClient, err := c.GetClient()
if err != nil {
@@ -188,14 +186,14 @@ func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
if err != nil {
return err
}
if value == "" {
if value == "" && p.Required {
return fmt.Errorf("must specify a value for %s", p.Name)
}
properties[p.Name] = value
}
data, err := json.Marshal(properties)
if err != nil {
return fmt.Errorf("failed to authentiate Terraform cloud provier %s", providerType)
return fmt.Errorf("failed to authenticate Terraform cloud provider %s", providerType)
}
providerAppName := fmt.Sprintf("config-terraform-provider-%s", name)
a := &v1beta1.Application{}
@@ -219,9 +217,12 @@ func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
},
}
if err := k8sClient.Create(ctx, a); err != nil {
return err
return fmt.Errorf("failed to authenticate Terraform cloud provider %s", providerType)
}
ioStreams.Infof("Successfully authenticate provider %s for %s\n", name, providerType)
return nil
}
return fmt.Errorf("failed to authenticate Terraform cloud provider %s", providerType)
}
return fmt.Errorf("terraform provider %s for %s already exists", name, providerType)
}
@@ -252,7 +253,7 @@ type ProviderMeta struct {
Age string
}
func listProviders(ctx context.Context, k8sClient client.Client) error {
func listProviders(ctx context.Context, k8sClient client.Client, ioStreams cmdutil.IOStreams) error {
var (
providers []ProviderMeta
currentProviders []tcv1beta1.Provider
@@ -265,7 +266,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
}
for _, p := range tcProviders.Items {
if p.Labels["config.oam.dev/type"] == labelVal {
if p.Labels["config.oam.dev/type"] == types.TerraformProvider {
currentProviders = append(currentProviders, p)
} else {
// if not labeled, the provider is manually created or created by `vela addon enable`.
@@ -276,7 +277,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
defs, err := getTerraformProviderTypes(ctx, k8sClient)
if err != nil {
if kerrors.IsNotFound(err) {
return errors.New("no Terraform Cloud Provider found, please run `vela addon enable` first")
ioStreams.Info("no Terraform Cloud Provider found, please run `vela addon enable` first")
}
return errors.Wrap(err, "failed to retrieve providers")
}
@@ -318,7 +319,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
for _, p := range providers {
table.AddRow(p.Type, p.Name, p.Age)
}
fmt.Println(table.String())
ioStreams.Info(table.String())
return nil
}
@@ -327,7 +328,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
func getTerraformProviderTypes(ctx context.Context, k8sClient client.Client) ([]v1beta1.ComponentDefinition, error) {
defs := &v1beta1.ComponentDefinitionList{}
if err := k8sClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{definition.UserPrefix + "type.config.oam.dev": labelVal}); err != nil {
client.MatchingLabels{definition.UserPrefix + "type.config.oam.dev": types.TerraformProvider}); err != nil {
return nil, err
}
return defs.Items, nil
@@ -343,7 +344,7 @@ func getTerraformProviderType(ctx context.Context, k8sClient client.Client, name
return def, nil
}
func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
func prepareProviderDeleteCommand(c common.Args, ioStreams cmdutil.IOStreams) (*cobra.Command, error) {
ctx := context.Background()
k8sClient, err := c.GetClient()
if err != nil {
@@ -358,7 +359,7 @@ func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
Example: "vela provider delete <provider-type> -name <provider-name>",
}
deleteSubCommands, err := prepareProviderDeleteSubCommand(c)
deleteSubCommands, err := prepareProviderDeleteSubCommand(c, ioStreams)
if err != nil {
return nil, err
}
@@ -397,7 +398,7 @@ func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
return cmd, nil
}
func prepareProviderDeleteSubCommand(c common.Args) ([]*cobra.Command, error) {
func prepareProviderDeleteSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
ctx := context.Background()
k8sClient, err := c.GetClient()
if err != nil {
@@ -438,7 +439,7 @@ func prepareProviderDeleteSubCommand(c common.Args) ([]*cobra.Command, error) {
if err := k8sClient.Delete(ctx, a); err != nil {
return err
}
fmt.Printf("Successfully delete provider %s for %s\n", name, providerType)
ioStreams.Infof("Successfully delete provider %s for %s\n", name, providerType)
return nil
}
cmds[i] = cmd

View File

@@ -0,0 +1,95 @@
/*
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 cli
import (
"context"
"os"
"testing"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"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/utils/util"
)
func TestLlistProviders(t *testing.T) {
ctx := context.Background()
type args struct {
k8sClient client.Client
}
type want struct {
errMsg string
}
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
p1 := &terraformapi.Provider{
TypeMeta: metav1.TypeMeta{
Kind: "Provider",
APIVersion: "terraform.core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "p1",
Namespace: "default",
Labels: map[string]string{
"config.oam.dev/type": types.TerraformProvider,
},
},
}
p2 := &terraformapi.Provider{
TypeMeta: metav1.TypeMeta{
Kind: "Provider",
APIVersion: "terraform.core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "p2",
Namespace: "default",
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(p1, p2).Build()
ioStream := util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
testcases := map[string]struct {
args args
want want
}{
"success": {
args: args{
k8sClient: k8sClient,
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
err := listProviders(ctx, tc.args.k8sClient, ioStream)
if err != nil || tc.want.errMsg != "" {
assert.Contains(t, err.Error(), tc.want.errMsg)
}
})
}
}

View File

@@ -84,8 +84,10 @@ func NewRevisionListCommand(c common.Args) *cobra.Command {
status = "Succeeded"
case rev.Status.Workflow.Terminated || rev.Status.Workflow.Suspend || rev.Status.Workflow.Finished:
status = "Failed"
default:
case app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == rev.Name:
status = "Executing"
default:
status = "Failed"
}
}
if labels := rev.GetLabels(); labels != nil {

View File

@@ -27,12 +27,20 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"golang.org/x/term"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
commontypes "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/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
pkgappfile "github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/policy"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
"github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/appfile"
@@ -103,6 +111,9 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
if err != nil {
return err
}
if printTree, err := cmd.Flags().GetBool("tree"); err == nil && printTree {
return printApplicationTree(c, cmd, appName, namespace)
}
newClient, err := c.GetClient()
if err != nil {
return err
@@ -125,8 +136,10 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
cmd.Flags().StringP("svc", "s", "", "service name")
cmd.Flags().BoolP("endpoint", "p", false, "show all service endpoints of the application")
cmd.Flags().StringP("component", "c", "", "filter service endpoints by component name")
cmd.Flags().BoolP("tree", "t", false, "display the application resources into tree structure")
cmd.Flags().BoolP("detail", "d", false, "display the realtime details of application resources")
cmd.Flags().StringP("detail-format", "", "inline", "the format for displaying details. Can be one of inline (default), wide, list, table, raw.")
addNamespaceAndEnvArg(cmd)
cmd.SetOut(ioStreams.Out)
return cmd
}
@@ -344,3 +357,65 @@ func getAppPhaseColor(appPhase commontypes.ApplicationPhase) *color.Color {
}
return yellow
}
func printApplicationTree(c common.Args, cmd *cobra.Command, appName string, appNs string) error {
config, err := c.GetConfig()
if err != nil {
return err
}
config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper)
cli, err := c.GetClient()
if err != nil {
return err
}
pd, err := c.GetPackageDiscover()
if err != nil {
return err
}
dm, err := discoverymapper.New(config)
if err != nil {
return err
}
app, err := loadRemoteApplication(cli, appNs, appName)
if err != nil {
return err
}
ctx := context.Background()
_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app)
if err != nil {
return err
}
svc, err := multicluster.GetClusterGatewayService(context.Background(), cli)
if err != nil {
return errors.Wrapf(err, "failed to get cluster secret namespace, please ensure cluster gateway is correctly deployed")
}
multicluster.ClusterGatewaySecretNamespace = svc.Namespace
clusterMapper, err := multicluster.NewClusterMapper(ctx, cli)
if err != nil {
return errors.Wrapf(err, "failed to get cluster mapper")
}
var placements []v1alpha1.PlacementDecision
af, err := pkgappfile.NewApplicationParser(cli, dm, pd).GenerateAppFile(context.Background(), app)
if err == nil {
placements, _ = policy.GetPlacementsFromTopologyPolicies(context.Background(), cli, app, af.Policies, true)
}
format, _ := cmd.Flags().GetString("detail-format")
var maxWidth *int
if w, _, err := term.GetSize(0); err == nil && w > 0 {
maxWidth = pointer.Int(w)
}
options := resourcetracker.ResourceTreePrintOptions{MaxWidth: maxWidth, Format: format, ClusterMapper: clusterMapper}
printDetails, _ := cmd.Flags().GetBool("detail")
if printDetails {
msgRetriever, err := resourcetracker.RetrieveKubeCtlGetMessageGenerator(config)
if err != nil {
return err
}
options.DetailRetriever = msgRetriever
}
options.PrintResourceTree(cmd.OutOrStdout(), placements, currentRT, historyRTs)
return nil
}

View File

@@ -19,71 +19,272 @@ package cli
import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
apitypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
apicommon "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"
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/component"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/oam"
utilcommon "github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/common"
)
// UpCommandOptions command args for vela up
type UpCommandOptions struct {
AppName string
Namespace string
File string
PublishVersion string
RevisionName string
}
// Complete fill the args for vela up
func (opt *UpCommandOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) {
if len(args) > 0 {
opt.AppName = args[0]
}
opt.Namespace = velacmd.GetNamespace(f, cmd)
}
// Validate if vela up args is valid, interrupt the command
func (opt *UpCommandOptions) Validate() error {
if opt.AppName != "" && opt.File != "" {
return errors.Errorf("cannot use app name and file at the same time")
}
if opt.AppName == "" && opt.File == "" {
return errors.Errorf("either app name or file should be set")
}
if opt.AppName != "" && opt.PublishVersion == "" {
return errors.Errorf("publish-version must be set if you want to force existing application to re-run")
}
if opt.AppName == "" && opt.RevisionName != "" {
return errors.Errorf("revision name must be used with application name")
}
return nil
}
// Run execute the vela up command
func (opt *UpCommandOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
if opt.File != "" {
return opt.deployApplicationFromFile(f, cmd)
}
if opt.RevisionName == "" {
return opt.deployExistingApp(f, cmd)
}
return opt.deployExistingAppUsingRevision(f, cmd)
}
func (opt *UpCommandOptions) deployExistingAppUsingRevision(f velacmd.Factory, cmd *cobra.Command) error {
ctx, cli := cmd.Context(), f.Client()
app := &v1beta1.Application{}
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
return err
}
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
return errors.Errorf("current PublishVersion is %s", publishVersion)
}
// check revision
revs, err := application.GetSortedAppRevisions(ctx, cli, opt.AppName, opt.Namespace)
if err != nil {
return err
}
var matchedRev *v1beta1.ApplicationRevision
for _, rev := range revs {
if rev.Name == opt.RevisionName {
matchedRev = rev.DeepCopy()
}
}
if matchedRev == nil {
return errors.Errorf("failed to find revision %s matching application %s", opt.RevisionName, opt.AppName)
}
if app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == opt.RevisionName {
return nil
}
// freeze the application
appKey := client.ObjectKeyFromObject(app)
controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() {
app.Spec = matchedRev.Spec.Application.Spec
oam.SetPublishVersion(app, opt.PublishVersion)
})
if err != nil {
return errors.Wrapf(err, "failed to freeze application %s before update", appKey)
}
// create new revision based on the matched revision
revName, revisionNum := utils.GetAppNextRevision(app)
matchedRev.Name = revName
oam.SetPublishVersion(matchedRev, opt.PublishVersion)
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(matchedRev)
if err != nil {
return err
}
un := &unstructured.Unstructured{Object: obj}
component.ClearRefObjectForDispatch(un)
if err = cli.Create(ctx, un); err != nil {
return errors.Wrapf(err, "failed to update application %s to create new revision %s", appKey, revName)
}
// update application status to point to the new revision
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err = cli.Get(ctx, appKey, app); err != nil {
return err
}
app.Status = apicommon.AppStatus{
LatestRevision: &apicommon.Revision{Name: revName, Revision: revisionNum, RevisionHash: matchedRev.GetLabels()[oam.LabelAppRevisionHash]},
}
return cli.Status().Update(ctx, app)
}); err != nil {
return errors.Wrapf(err, "failed to update application %s to use new revision %s", appKey, revName)
}
// unfreeze application
if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil {
return errors.Wrapf(err, "failed to unfreeze application %s after update", appKey)
}
cmd.Printf("Application updated with new PublishVersion %s using revision %s\n", opt.PublishVersion, opt.RevisionName)
return nil
}
func (opt *UpCommandOptions) deployExistingApp(f velacmd.Factory, cmd *cobra.Command) error {
ctx, cli := cmd.Context(), f.Client()
app := &v1beta1.Application{}
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
return err
}
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
return errors.Errorf("current PublishVersion is %s", publishVersion)
}
oam.SetPublishVersion(app, opt.PublishVersion)
return cli.Update(ctx, app)
}); err != nil {
return err
}
cmd.Printf("Application updated with new PublishVersion %s\n", opt.PublishVersion)
return nil
}
func (opt *UpCommandOptions) deployApplicationFromFile(f velacmd.Factory, cmd *cobra.Command) error {
cli := f.Client()
body, err := common.ReadRemoteOrLocalPath(opt.File)
if err != nil {
return err
}
ioStream := util.IOStreams{
In: cmd.InOrStdin(),
Out: cmd.OutOrStdout(),
ErrOut: cmd.ErrOrStderr(),
}
if common.IsAppfile(body) { // legacy compatibility
o := &common.AppfileOptions{Kubecli: cli, IO: ioStream, Namespace: opt.Namespace}
return o.Run(opt.File, o.Namespace, utilcommon.Args{Schema: utilcommon.Scheme})
}
var app v1beta1.Application
err = yaml.Unmarshal(body, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
if opt.Namespace != "" && opt.Namespace != types.DefaultAppNamespace {
app.SetNamespace(opt.Namespace)
}
if opt.PublishVersion != "" {
oam.SetPublishVersion(&app, opt.PublishVersion)
}
err = common.ApplyApplication(app, ioStream, cli)
if err != nil {
return err
}
cmd.Printf("Application %s/%s applied.\n", app.Namespace, app.Name)
return nil
}
var (
upLong = templates.LongDesc(i18n.T(`
Deploy one application
Deploy one application based on local files or re-deploy an existing application.
With the -n/--namespace flag, you can choose the location of the target application.
To apply application from file, use the -f/--file flag to specify the application
file location.
To give a particular version to this deploy, use the -v/--publish-version flag. When
you are deploying an existing application, the version name must be different from
the current name. You can also use a history revision for the deploy and override the
current application by using the -r/--revision flag.`))
upExample = templates.Examples(i18n.T(`
# Deploy an application from file
vela up -f ./app.yaml
# Deploy an application with a version name
vela up example-app -n example-ns --publish-version beta
# Deploy an application using existing revision
vela up example-app -n example-ns --publish-version beta --revision example-app-v2`))
)
// NewUpCommand will create command for applying an AppFile
func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cobra.Command {
appFilePath := new(string)
func NewUpCommand(f velacmd.Factory, order string) *cobra.Command {
o := &UpCommandOptions{}
cmd := &cobra.Command{
Use: "up",
DisableFlagsInUseLine: true,
Short: "Apply an appfile or application from file",
Long: "Create or update vela application from file or URL, both appfile or application object format are supported.",
Short: i18n.T("Deploy one application"),
Long: upLong,
Example: upExample,
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeStart,
},
RunE: func(cmd *cobra.Command, args []string) error {
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
if err != nil {
return err
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
o.Complete(f, cmd, args)
if o.File == "" {
return velacmd.GetApplicationsForCompletion(cmd.Context(), f, o.Namespace, toComplete)
}
kubecli, err := c.GetClient()
if err != nil {
return err
}
body, err := common.ReadRemoteOrLocalPath(*appFilePath)
if err != nil {
return err
}
if common.IsAppfile(body) {
o := &common.AppfileOptions{
Kubecli: kubecli,
IO: ioStream,
Namespace: namespace,
}
return o.Run(*appFilePath, o.Namespace, c)
}
var app corev1beta1.Application
err = yaml.Unmarshal(body, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
if namespace != "" && namespace != types.DefaultAppNamespace {
app.SetNamespace(namespace)
}
err = common.ApplyApplication(app, ioStream, kubecli)
if err != nil {
return err
}
return nil
return nil, cobra.ShellCompDirectiveDefault
},
Run: func(cmd *cobra.Command, args []string) {
o.Complete(f, cmd, args)
cmdutil.CheckErr(o.Validate())
cmdutil.CheckErr(o.Run(f, cmd))
},
}
cmd.SetOut(ioStream.Out)
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application, it could be a remote url.")
cmd.Flags().StringVarP(&o.File, "file", "f", o.File, "The file path for appfile or application. It could be a remote url.")
cmd.Flags().StringVarP(&o.PublishVersion, "publish-version", "v", o.PublishVersion, "The publish version for deploying application.")
cmd.Flags().StringVarP(&o.RevisionName, "revision", "r", o.RevisionName, "The revision to use for deploying the application, if empty, the current application configuration will be used.")
cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
"revision",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var appName string
if len(args) > 0 {
appName = args[0]
}
namespace := velacmd.GetNamespace(f, cmd)
return velacmd.GetRevisionForCompletion(cmd.Context(), f, appName, namespace, toComplete)
}))
addNamespaceAndEnvArg(cmd)
return cmd
return velacmd.NewCommandBuilder(f, cmd).
WithNamespaceFlag().
WithResponsiveWriter().
Build()
}

View File

@@ -31,7 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/util"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/references/common"
)
@@ -128,7 +128,10 @@ spec:
}))
var buf bytes.Buffer
cmd := NewUpCommand(args, "", util.IOStreams{In: os.Stdin, Out: &buf, ErrOut: &buf})
cmd := NewUpCommand(velacmd.NewDefaultFactory(args.GetClient), "")
cmd.SetArgs([]string{})
cmd.SetOut(&buf)
cmd.SetErr(&buf)
if c.namespace != "" {
require.NoError(t, cmd.Flags().Set(FlagNamespace, c.namespace))
}

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