mirror of
https://github.com/kubevela/kubevela.git
synced 2026-03-01 01:00:38 +00:00
Compare commits
43 Commits
v1.3.0-bet
...
v1.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
825f1aaa22 | ||
|
|
82075427e6 | ||
|
|
f89cf673c0 | ||
|
|
ce53f6922f | ||
|
|
b6f70d9a3c | ||
|
|
64d063ccfe | ||
|
|
a98278fb7a | ||
|
|
0553d603e6 | ||
|
|
a36e99308f | ||
|
|
947bac2d35 | ||
|
|
7644cc59cb | ||
|
|
89a441b8ce | ||
|
|
780572c68f | ||
|
|
a13cab65b2 | ||
|
|
e26104adcc | ||
|
|
26ac584655 | ||
|
|
482976990d | ||
|
|
bc4812a12e | ||
|
|
58c2208e2a | ||
|
|
f83d88cfb0 | ||
|
|
8af3dec0df | ||
|
|
edebcc6c59 | ||
|
|
32382ba6be | ||
|
|
46ef6f9df4 | ||
|
|
aea98ff5bf | ||
|
|
c093676575 | ||
|
|
ed05b4b035 | ||
|
|
3aa4412a0f | ||
|
|
ef4b9816e1 | ||
|
|
1c5aab1852 | ||
|
|
966dbc1c74 | ||
|
|
4eafb46c87 | ||
|
|
a97a4d0ed7 | ||
|
|
77c02f9eec | ||
|
|
3157efd421 | ||
|
|
8ff93b33e2 | ||
|
|
c6b9abe4c4 | ||
|
|
150ef6e99e | ||
|
|
0ada407fbe | ||
|
|
c4af1ba643 | ||
|
|
de84421487 | ||
|
|
38a8a7f88a | ||
|
|
b4ddf0e4c3 |
4
.github/workflows/apiserver-test.yaml
vendored
4
.github/workflows/apiserver-test.yaml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
- name: Setup Kind Cluster (Worker)
|
||||
run: |
|
||||
kind delete cluster --name worker
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Setup Kind Cluster (Hub)
|
||||
run: |
|
||||
kind delete cluster
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
|
||||
|
||||
6
.github/workflows/e2e-multicluster-test.yml
vendored
6
.github/workflows/e2e-multicluster-test.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Setup Kind Cluster (Worker)
|
||||
run: |
|
||||
kind delete cluster --name worker
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Setup Kind Cluster (Hub)
|
||||
run: |
|
||||
kind delete cluster
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: /tmp/e2e-profile.out
|
||||
files: /tmp/e2e-profile.out,/tmp/e2e_multicluster_test.out
|
||||
flags: e2e-multicluster-test
|
||||
name: codecov-umbrella
|
||||
|
||||
|
||||
2
.github/workflows/e2e-rollout-test.yml
vendored
2
.github/workflows/e2e-rollout-test.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Setup Kind Cluster
|
||||
run: |
|
||||
kind delete cluster
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
|
||||
|
||||
2
.github/workflows/e2e-test.yml
vendored
2
.github/workflows/e2e-test.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Setup Kind Cluster
|
||||
run: |
|
||||
kind delete cluster
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
|
||||
|
||||
7
.github/workflows/go.yml
vendored
7
.github/workflows/go.yml
vendored
@@ -71,6 +71,11 @@ jobs:
|
||||
if: needs.detect-noop.outputs.noop != 'true'
|
||||
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -88,7 +93,7 @@ jobs:
|
||||
# version, but we prefer this action because it leaves 'annotations' (i.e.
|
||||
# it comments on PRs to point out linter violations).
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: ${{ env.GOLANGCI_VERSION }}
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ Full documentation is available on the [KubeVela website](https://kubevela.io/).
|
||||
- Wechat Group (*Chinese*): Broker wechat to add you into the user group.
|
||||
|
||||
<img src="https://static.kubevela.net/images/barnett-wechat.jpg" width="200" />
|
||||
- Bi-weekly Community Call: [Meeting Notes](https://docs.google.com/document/d/1nqdFEyULekyksFHtFvgvFAYE-0AMHKoS3RMnaKsarjs)
|
||||
- Bi-weekly Community Call: [Meeting Notes](https://docs.google.com/document/d/1nqdFEyULekyksFHtFvgvFAYE-0AMHKoS3RMnaKsarjs).
|
||||
- Bi-weekly Chinese Community Call: [Video Records](https://space.bilibili.com/180074935/channel/seriesdetail?sid=1842207).
|
||||
|
||||
## Talks and Conferences
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ var (
|
||||
// Workflow meta
|
||||
var (
|
||||
WorkflowKind = "Workflow"
|
||||
WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(PolicyKind)
|
||||
WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(WorkflowKind)
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -16,7 +16,10 @@ limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
|
||||
import (
|
||||
"github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
|
||||
"github.com/oam-dev/cluster-gateway/pkg/config"
|
||||
)
|
||||
|
||||
const (
|
||||
// CredentialTypeInternal identifies the virtual cluster from internal kubevela system
|
||||
@@ -29,3 +32,8 @@ const (
|
||||
// ClustersArg indicates the argument for specific clusters to install addon
|
||||
ClustersArg = "clusters"
|
||||
)
|
||||
|
||||
var (
|
||||
// AnnotationClusterAlias the annotation key for cluster alias
|
||||
AnnotationClusterAlias = config.MetaApiGroupName + "/cluster-alias"
|
||||
)
|
||||
|
||||
@@ -61,6 +61,22 @@ const (
|
||||
AnnoIngressControllerHTTPSPort = "ingress.controller/https-port"
|
||||
// AnnoIngressControllerHTTPPort define ingress controller listen port for http
|
||||
AnnoIngressControllerHTTPPort = "ingress.controller/http-port"
|
||||
// LabelConfigType is the label for config type
|
||||
LabelConfigType = "config.oam.dev/type"
|
||||
// LabelConfigCatalog is the label for config catalog
|
||||
LabelConfigCatalog = "config.oam.dev/catalog"
|
||||
// LabelConfigSubType is the sub-type for a config type
|
||||
LabelConfigSubType = "config.oam.dev/sub-type"
|
||||
// LabelConfigProject is the label for config project
|
||||
LabelConfigProject = "config.oam.dev/project"
|
||||
// LabelConfigSyncToMultiCluster is the label to decide whether a config will be synchronized to multi-cluster
|
||||
LabelConfigSyncToMultiCluster = "config.oam.dev/multi-cluster"
|
||||
// LabelConfigIdentifier is the label for config identifier
|
||||
LabelConfigIdentifier = "config.oam.dev/identifier"
|
||||
// LabelConfigDescription is the label for config description
|
||||
LabelConfigDescription = "config.oam.dev/description"
|
||||
// AnnotationConfigAlias is the annotation for config alias
|
||||
AnnotationConfigAlias = "config.oam.dev/alias"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -118,3 +134,17 @@ var DefaultFilterAnnots = []string{
|
||||
oam.AnnotationFilterAnnotationKeys,
|
||||
oam.AnnotationLastAppliedConfiguration,
|
||||
}
|
||||
|
||||
// ConfigType is the type of config
|
||||
type ConfigType string
|
||||
|
||||
const (
|
||||
// TerraformProvider is the config type for terraform provider
|
||||
TerraformProvider = "terraform-provider"
|
||||
// DexConnector is the config type for dex connector
|
||||
DexConnector = "config-dex-connector"
|
||||
// ImageRegistry is the config type for image registry
|
||||
ImageRegistry = "config-image-registry"
|
||||
// HelmRepository is the config type for Helm chart repository
|
||||
HelmRepository = "config-helm-repository"
|
||||
)
|
||||
|
||||
@@ -86,7 +86,7 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
|
||||
| `multicluster.clusterGateway.replicaCount` | ClusterGateway replica count | `1` |
|
||||
| `multicluster.clusterGateway.port` | ClusterGateway port | `9443` |
|
||||
| `multicluster.clusterGateway.image.repository` | ClusterGateway image repository | `oamdev/cluster-gateway` |
|
||||
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.0` |
|
||||
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.3.2` |
|
||||
| `multicluster.clusterGateway.image.pullPolicy` | ClusterGateway image pull policy | `IfNotPresent` |
|
||||
| `multicluster.clusterGateway.resources.limits.cpu` | ClusterGateway cpu limit | `100m` |
|
||||
| `multicluster.clusterGateway.resources.limits.memory` | ClusterGateway memory limit | `200Mi` |
|
||||
@@ -125,18 +125,20 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
|
||||
| `kubeClient.burst` | The burst for reconcile clients, default is 100 | `100` |
|
||||
|
||||
|
||||
## Uninstalling the Chart
|
||||
## Uninstallation
|
||||
|
||||
To uninstall/delete the KubeVela helm release
|
||||
### Vela CLI
|
||||
|
||||
To uninstall KubeVela, you can just run the following command by vela CLI:
|
||||
|
||||
```shell
|
||||
$ helm uninstall -n vela-system kubevela
|
||||
vela uninstall --force
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with kubevela and deletes the release.
|
||||
### Helm CLI
|
||||
|
||||
**Notice**: You must disable all the addons before uninstallation, this is a script for convenience.
|
||||
|
||||
**Notice**: If you enable fluxcd addon when install the chart by set `enableFluxcdAddon=true` .Uninstall wouldn't disable the fluxcd addon ,and it will be kept in the cluster.Please guarantee there is no application in cluster use this addon and disable it firstly before uninstall the helm chart.
|
||||
You can use this script to disable all addons.
|
||||
```shell
|
||||
#! /bin/sh
|
||||
addon=$(vela addon list|grep enabled|awk {'print $1'})
|
||||
@@ -156,6 +158,15 @@ if [ $fluxcd ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
To uninstall the KubeVela helm release:
|
||||
|
||||
```shell
|
||||
$ helm uninstall -n vela-system kubevela
|
||||
```
|
||||
|
||||
Finally, this command will remove all the Kubernetes resources associated with KubeVela and remove this chart release.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,9 +27,5 @@ Welcome to use the KubeVela! Enjoy your shipping application journey!
|
||||
| . \| |_| || |_) || __/ \ V /| __/| || (_| |
|
||||
|_|\_\\__,_||_.__/ \___| \_/ \___||_| \__,_|
|
||||
|
||||
** Please note before uninstalling **
|
||||
|
||||
If you enable fluxcd addon when install the chart by set `enableFluxcdAddon=true` .
|
||||
Uninstall wouldn't disable the fluxcd addon ,and it will be kept in the cluster.
|
||||
Please guarantee there is no application in cluster using this addon and disable it firstly before uninstall the helm chart.
|
||||
And you can find the script of one-short disable all addons from the uninstalling section of https://github.com/oam-dev/kubevela/blob/master/charts/vela-core/README.md.
|
||||
You can refer to https://kubevela.io for more details.
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/config-dex-connector.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
custom.definition.oam.dev/alias.config.oam.dev: Dex Connector
|
||||
definition.oam.dev/description: Config information to authenticate Dex connectors
|
||||
labels:
|
||||
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
|
||||
custom.definition.oam.dev/multi-cluster.config.oam.dev: "false"
|
||||
custom.definition.oam.dev/type.config.oam.dev: dex-connector
|
||||
name: config-dex-connector
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: {
|
||||
name: parameter.name
|
||||
namespace: context.namespace
|
||||
labels: {
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "dex-connector"
|
||||
"config.oam.dev/multi-cluster": "false"
|
||||
"config.oam.dev/identifier": parameter.name
|
||||
"config.oam.dev/sub-type": parameter.type
|
||||
}
|
||||
}
|
||||
type: "Opaque"
|
||||
|
||||
if parameter.type == "github" {
|
||||
stringData: parameter.github
|
||||
}
|
||||
if parameter.type == "ldap" {
|
||||
stringData: parameter.ldap
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Config type
|
||||
type: "github" | "ldap"
|
||||
github?: {
|
||||
// +usage=GitHub client ID
|
||||
clientID: string
|
||||
// +usage=GitHub client secret
|
||||
clientSecret: string
|
||||
// +usage=GitHub call back URL
|
||||
callbackURL: string
|
||||
}
|
||||
ldap?: {
|
||||
host: string
|
||||
insecureNoSSL: *true | bool
|
||||
insecureSkipVerify: bool
|
||||
startTLS: bool
|
||||
usernamePrompt: string
|
||||
userSearch: {
|
||||
baseDN: string
|
||||
username: string
|
||||
idAttr: string
|
||||
emailAttr: string
|
||||
nameAttr: string
|
||||
}
|
||||
}
|
||||
}
|
||||
workload:
|
||||
type: autodetects.core.oam.dev
|
||||
|
||||
308
charts/vela-core/templates/defwithtemplate/cron-task.yaml
Normal file
308
charts/vela-core/templates/defwithtemplate/cron-task.yaml
Normal file
@@ -0,0 +1,308 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/cron-task.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Describes cron jobs that run code or a script to completion.
|
||||
name: cron-task
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "batch/v1beta1"
|
||||
kind: "CronJob"
|
||||
spec: {
|
||||
schedule: parameter.schedule
|
||||
concurrencyPolicy: parameter.concurrencyPolicy
|
||||
suspend: parameter.suspend
|
||||
successfulJobsHistoryLimit: parameter.successfulJobsHistoryLimit
|
||||
failedJobsHistoryLimit: parameter.failedJobsHistoryLimit
|
||||
if parameter.startingDeadlineSeconds != _|_ {
|
||||
startingDeadlineSeconds: parameter.startingDeadlineSeconds
|
||||
}
|
||||
jobTemplate: {
|
||||
if parameter.labels != _|_ {
|
||||
metadata: labels: parameter.labels
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
metadata: annotations: parameter.annotations
|
||||
}
|
||||
spec: {
|
||||
parallelism: parameter.count
|
||||
completions: parameter.count
|
||||
if parameter.ttlSecondsAfterFinished != _|_ {
|
||||
ttlSecondsAfterFinished: parameter.ttlSecondsAfterFinished
|
||||
}
|
||||
if parameter.activeDeadlineSeconds != _|_ {
|
||||
activeDeadlineSeconds: parameter.activeDeadlineSeconds
|
||||
}
|
||||
backoffLimit: parameter.backoffLimit
|
||||
template: {
|
||||
if parameter.labels != _|_ {
|
||||
metadata: labels: parameter.labels
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
metadata: annotations: parameter.annotations
|
||||
}
|
||||
spec: {
|
||||
restartPolicy: parameter.restart
|
||||
containers: [{
|
||||
name: context.name
|
||||
image: parameter.image
|
||||
if parameter["imagePullPolicy"] != _|_ {
|
||||
imagePullPolicy: parameter.imagePullPolicy
|
||||
}
|
||||
if parameter["cmd"] != _|_ {
|
||||
command: parameter.cmd
|
||||
}
|
||||
if parameter["env"] != _|_ {
|
||||
env: parameter.env
|
||||
}
|
||||
if parameter["cpu"] != _|_ {
|
||||
resources: {
|
||||
limits: cpu: parameter.cpu
|
||||
requests: cpu: parameter.cpu
|
||||
}
|
||||
}
|
||||
if parameter["memory"] != _|_ {
|
||||
resources: {
|
||||
limits: memory: parameter.memory
|
||||
requests: memory: parameter.memory
|
||||
}
|
||||
}
|
||||
if parameter["volumes"] != _|_ {
|
||||
volumeMounts: [ for v in parameter.volumes {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
name: v.name
|
||||
}}]
|
||||
}
|
||||
}]
|
||||
if parameter["volumes"] != _|_ {
|
||||
volumes: [ for v in parameter.volumes {
|
||||
{
|
||||
name: v.name
|
||||
if v.type == "pvc" {
|
||||
persistentVolumeClaim: claimName: v.claimName
|
||||
}
|
||||
if v.type == "configMap" {
|
||||
configMap: {
|
||||
defaultMode: v.defaultMode
|
||||
name: v.cmName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "secret" {
|
||||
secret: {
|
||||
defaultMode: v.defaultMode
|
||||
secretName: v.secretName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "emptyDir" {
|
||||
emptyDir: medium: v.medium
|
||||
}
|
||||
}}]
|
||||
}
|
||||
if parameter["imagePullSecrets"] != _|_ {
|
||||
imagePullSecrets: [ for v in parameter.imagePullSecrets {
|
||||
name: v
|
||||
},
|
||||
]
|
||||
}
|
||||
if parameter.hostAliases != _|_ {
|
||||
hostAliases: [ for v in parameter.hostAliases {
|
||||
ip: v.ip
|
||||
hostnames: v.hostnames
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Specify the labels in the workload
|
||||
labels?: [string]: string
|
||||
|
||||
// +usage=Specify the annotations in the workload
|
||||
annotations?: [string]: string
|
||||
|
||||
// +usage=Specify the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron
|
||||
schedule: string
|
||||
|
||||
// +usage=Specify deadline in seconds for starting the job if it misses scheduled
|
||||
startingDeadlineSeconds?: int
|
||||
|
||||
// +usage=suspend subsequent executions
|
||||
suspend: *false | bool
|
||||
|
||||
// +usage=Specifies how to treat concurrent executions of a Job
|
||||
concurrencyPolicy: *"Allow" | "Allow" | "Forbid" | "Replace"
|
||||
|
||||
// +usage=The number of successful finished jobs to retain
|
||||
successfulJobsHistoryLimit: *3 | int
|
||||
|
||||
// +usage=The number of failed finished jobs to retain
|
||||
failedJobsHistoryLimit: *1 | int
|
||||
|
||||
// +usage=Specify number of tasks to run in parallel
|
||||
// +short=c
|
||||
count: *1 | int
|
||||
|
||||
// +usage=Which image would you like to use for your service
|
||||
// +short=i
|
||||
image: string
|
||||
|
||||
// +usage=Specify image pull policy for your service
|
||||
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
|
||||
|
||||
// +usage=Specify image pull secrets for your service
|
||||
imagePullSecrets?: [...string]
|
||||
|
||||
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
|
||||
restart: *"Never" | string
|
||||
|
||||
// +usage=Commands to run in the container
|
||||
cmd?: [...string]
|
||||
|
||||
// +usage=Define arguments by using environment variables
|
||||
env?: [...{
|
||||
// +usage=Environment variable name
|
||||
name: string
|
||||
// +usage=The value of the environment variable
|
||||
value?: string
|
||||
// +usage=Specifies a source the value of this var should come from
|
||||
valueFrom?: {
|
||||
// +usage=Selects a key of a secret in the pod's namespace
|
||||
secretKeyRef: {
|
||||
// +usage=The name of the secret in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the secret to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
// +usage=Selects a key of a config map in the pod's namespace
|
||||
configMapKeyRef: {
|
||||
// +usage=The name of the config map in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the config map to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
|
||||
cpu?: string
|
||||
|
||||
// +usage=Specifies the attributes of the memory resource required for the container.
|
||||
memory?: string
|
||||
|
||||
// +usage=Declare volumes and volumeMounts
|
||||
volumes?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
// +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir"
|
||||
type: "pvc" | "configMap" | "secret" | "emptyDir"
|
||||
if type == "pvc" {
|
||||
claimName: string
|
||||
}
|
||||
if type == "configMap" {
|
||||
defaultMode: *420 | int
|
||||
cmName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "secret" {
|
||||
defaultMode: *420 | int
|
||||
secretName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "emptyDir" {
|
||||
medium: *"" | "Memory"
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=An optional list of hosts and IPs that will be injected into the pod's hosts file
|
||||
hostAliases?: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
|
||||
// +usage=Limits the lifetime of a Job that has finished
|
||||
ttlSecondsAfterFinished?: int
|
||||
|
||||
// +usage=The duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it
|
||||
activeDeadlineSeconds?: int
|
||||
|
||||
// +usage=The number of retries before marking this job failed
|
||||
backoffLimit: *6 | int
|
||||
|
||||
// +usage=Instructions for assessing whether the container is alive.
|
||||
livenessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
|
||||
exec?: {
|
||||
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
|
||||
command: [...string]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
|
||||
httpGet?: {
|
||||
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
|
||||
path: string
|
||||
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
|
||||
port: int
|
||||
httpHeaders?: [...{
|
||||
name: string
|
||||
value: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
|
||||
tcpSocket?: {
|
||||
// +usage=The TCP socket within the container that should be probed to assess container health.
|
||||
port: int
|
||||
}
|
||||
|
||||
// +usage=Number of seconds after the container is started before the first probe is initiated.
|
||||
initialDelaySeconds: *0 | int
|
||||
|
||||
// +usage=How often, in seconds, to execute the probe.
|
||||
periodSeconds: *10 | int
|
||||
|
||||
// +usage=Number of seconds after which the probe times out.
|
||||
timeoutSeconds: *1 | int
|
||||
|
||||
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
|
||||
successThreshold: *1 | int
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
}
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
type: cronjobs.batch
|
||||
|
||||
@@ -453,6 +453,12 @@ spec:
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Specify the hostAliases to add
|
||||
hostAliases: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
@@ -494,12 +500,6 @@ spec:
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
|
||||
// +usage=Specify the hostAliases to add
|
||||
hostAliases: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
}
|
||||
status:
|
||||
customStatus: |-
|
||||
|
||||
@@ -104,7 +104,7 @@ multicluster:
|
||||
port: 9443
|
||||
image:
|
||||
repository: oamdev/cluster-gateway
|
||||
tag: v1.3.0
|
||||
tag: v1.3.2
|
||||
pullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/config-dex-connector.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
custom.definition.oam.dev/alias.config.oam.dev: Dex Connector
|
||||
definition.oam.dev/description: Config information to authenticate Dex connectors
|
||||
labels:
|
||||
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
|
||||
custom.definition.oam.dev/multi-cluster.config.oam.dev: "false"
|
||||
custom.definition.oam.dev/type.config.oam.dev: dex-connector
|
||||
name: config-dex-connector
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: {
|
||||
name: parameter.name
|
||||
namespace: context.namespace
|
||||
labels: {
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "dex-connector"
|
||||
"config.oam.dev/multi-cluster": "false"
|
||||
"config.oam.dev/identifier": parameter.name
|
||||
"config.oam.dev/sub-type": parameter.type
|
||||
}
|
||||
}
|
||||
type: "Opaque"
|
||||
|
||||
if parameter.type == "github" {
|
||||
stringData: parameter.github
|
||||
}
|
||||
if parameter.type == "ldap" {
|
||||
stringData: parameter.ldap
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Config type
|
||||
type: "github" | "ldap"
|
||||
github?: {
|
||||
// +usage=GitHub client ID
|
||||
clientID: string
|
||||
// +usage=GitHub client secret
|
||||
clientSecret: string
|
||||
// +usage=GitHub call back URL
|
||||
callbackURL: string
|
||||
}
|
||||
ldap?: {
|
||||
host: string
|
||||
insecureNoSSL: *true | bool
|
||||
insecureSkipVerify: bool
|
||||
startTLS: bool
|
||||
usernamePrompt: string
|
||||
userSearch: {
|
||||
baseDN: string
|
||||
username: string
|
||||
idAttr: string
|
||||
emailAttr: string
|
||||
nameAttr: string
|
||||
}
|
||||
}
|
||||
}
|
||||
workload:
|
||||
type: autodetects.core.oam.dev
|
||||
|
||||
308
charts/vela-minimal/templates/defwithtemplate/cron-task.yaml
Normal file
308
charts/vela-minimal/templates/defwithtemplate/cron-task.yaml
Normal file
@@ -0,0 +1,308 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/cron-task.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Describes cron jobs that run code or a script to completion.
|
||||
name: cron-task
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "batch/v1beta1"
|
||||
kind: "CronJob"
|
||||
spec: {
|
||||
schedule: parameter.schedule
|
||||
concurrencyPolicy: parameter.concurrencyPolicy
|
||||
suspend: parameter.suspend
|
||||
successfulJobsHistoryLimit: parameter.successfulJobsHistoryLimit
|
||||
failedJobsHistoryLimit: parameter.failedJobsHistoryLimit
|
||||
if parameter.startingDeadlineSeconds != _|_ {
|
||||
startingDeadlineSeconds: parameter.startingDeadlineSeconds
|
||||
}
|
||||
jobTemplate: {
|
||||
if parameter.labels != _|_ {
|
||||
metadata: labels: parameter.labels
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
metadata: annotations: parameter.annotations
|
||||
}
|
||||
spec: {
|
||||
parallelism: parameter.count
|
||||
completions: parameter.count
|
||||
if parameter.ttlSecondsAfterFinished != _|_ {
|
||||
ttlSecondsAfterFinished: parameter.ttlSecondsAfterFinished
|
||||
}
|
||||
if parameter.activeDeadlineSeconds != _|_ {
|
||||
activeDeadlineSeconds: parameter.activeDeadlineSeconds
|
||||
}
|
||||
backoffLimit: parameter.backoffLimit
|
||||
template: {
|
||||
if parameter.labels != _|_ {
|
||||
metadata: labels: parameter.labels
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
metadata: annotations: parameter.annotations
|
||||
}
|
||||
spec: {
|
||||
restartPolicy: parameter.restart
|
||||
containers: [{
|
||||
name: context.name
|
||||
image: parameter.image
|
||||
if parameter["imagePullPolicy"] != _|_ {
|
||||
imagePullPolicy: parameter.imagePullPolicy
|
||||
}
|
||||
if parameter["cmd"] != _|_ {
|
||||
command: parameter.cmd
|
||||
}
|
||||
if parameter["env"] != _|_ {
|
||||
env: parameter.env
|
||||
}
|
||||
if parameter["cpu"] != _|_ {
|
||||
resources: {
|
||||
limits: cpu: parameter.cpu
|
||||
requests: cpu: parameter.cpu
|
||||
}
|
||||
}
|
||||
if parameter["memory"] != _|_ {
|
||||
resources: {
|
||||
limits: memory: parameter.memory
|
||||
requests: memory: parameter.memory
|
||||
}
|
||||
}
|
||||
if parameter["volumes"] != _|_ {
|
||||
volumeMounts: [ for v in parameter.volumes {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
name: v.name
|
||||
}}]
|
||||
}
|
||||
}]
|
||||
if parameter["volumes"] != _|_ {
|
||||
volumes: [ for v in parameter.volumes {
|
||||
{
|
||||
name: v.name
|
||||
if v.type == "pvc" {
|
||||
persistentVolumeClaim: claimName: v.claimName
|
||||
}
|
||||
if v.type == "configMap" {
|
||||
configMap: {
|
||||
defaultMode: v.defaultMode
|
||||
name: v.cmName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "secret" {
|
||||
secret: {
|
||||
defaultMode: v.defaultMode
|
||||
secretName: v.secretName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "emptyDir" {
|
||||
emptyDir: medium: v.medium
|
||||
}
|
||||
}}]
|
||||
}
|
||||
if parameter["imagePullSecrets"] != _|_ {
|
||||
imagePullSecrets: [ for v in parameter.imagePullSecrets {
|
||||
name: v
|
||||
},
|
||||
]
|
||||
}
|
||||
if parameter.hostAliases != _|_ {
|
||||
hostAliases: [ for v in parameter.hostAliases {
|
||||
ip: v.ip
|
||||
hostnames: v.hostnames
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Specify the labels in the workload
|
||||
labels?: [string]: string
|
||||
|
||||
// +usage=Specify the annotations in the workload
|
||||
annotations?: [string]: string
|
||||
|
||||
// +usage=Specify the schedule in Cron format, see https://en.wikipedia.org/wiki/Cron
|
||||
schedule: string
|
||||
|
||||
// +usage=Specify deadline in seconds for starting the job if it misses scheduled
|
||||
startingDeadlineSeconds?: int
|
||||
|
||||
// +usage=suspend subsequent executions
|
||||
suspend: *false | bool
|
||||
|
||||
// +usage=Specifies how to treat concurrent executions of a Job
|
||||
concurrencyPolicy: *"Allow" | "Allow" | "Forbid" | "Replace"
|
||||
|
||||
// +usage=The number of successful finished jobs to retain
|
||||
successfulJobsHistoryLimit: *3 | int
|
||||
|
||||
// +usage=The number of failed finished jobs to retain
|
||||
failedJobsHistoryLimit: *1 | int
|
||||
|
||||
// +usage=Specify number of tasks to run in parallel
|
||||
// +short=c
|
||||
count: *1 | int
|
||||
|
||||
// +usage=Which image would you like to use for your service
|
||||
// +short=i
|
||||
image: string
|
||||
|
||||
// +usage=Specify image pull policy for your service
|
||||
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
|
||||
|
||||
// +usage=Specify image pull secrets for your service
|
||||
imagePullSecrets?: [...string]
|
||||
|
||||
// +usage=Define the job restart policy, the value can only be Never or OnFailure. By default, it's Never.
|
||||
restart: *"Never" | string
|
||||
|
||||
// +usage=Commands to run in the container
|
||||
cmd?: [...string]
|
||||
|
||||
// +usage=Define arguments by using environment variables
|
||||
env?: [...{
|
||||
// +usage=Environment variable name
|
||||
name: string
|
||||
// +usage=The value of the environment variable
|
||||
value?: string
|
||||
// +usage=Specifies a source the value of this var should come from
|
||||
valueFrom?: {
|
||||
// +usage=Selects a key of a secret in the pod's namespace
|
||||
secretKeyRef: {
|
||||
// +usage=The name of the secret in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the secret to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
// +usage=Selects a key of a config map in the pod's namespace
|
||||
configMapKeyRef: {
|
||||
// +usage=The name of the config map in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the config map to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
|
||||
cpu?: string
|
||||
|
||||
// +usage=Specifies the attributes of the memory resource required for the container.
|
||||
memory?: string
|
||||
|
||||
// +usage=Declare volumes and volumeMounts
|
||||
volumes?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
// +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir"
|
||||
type: "pvc" | "configMap" | "secret" | "emptyDir"
|
||||
if type == "pvc" {
|
||||
claimName: string
|
||||
}
|
||||
if type == "configMap" {
|
||||
defaultMode: *420 | int
|
||||
cmName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "secret" {
|
||||
defaultMode: *420 | int
|
||||
secretName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "emptyDir" {
|
||||
medium: *"" | "Memory"
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=An optional list of hosts and IPs that will be injected into the pod's hosts file
|
||||
hostAliases?: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
|
||||
// +usage=Limits the lifetime of a Job that has finished
|
||||
ttlSecondsAfterFinished?: int
|
||||
|
||||
// +usage=The duration in seconds relative to the startTime that the job may be continuously active before the system tries to terminate it
|
||||
activeDeadlineSeconds?: int
|
||||
|
||||
// +usage=The number of retries before marking this job failed
|
||||
backoffLimit: *6 | int
|
||||
|
||||
// +usage=Instructions for assessing whether the container is alive.
|
||||
livenessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
|
||||
exec?: {
|
||||
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
|
||||
command: [...string]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
|
||||
httpGet?: {
|
||||
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
|
||||
path: string
|
||||
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
|
||||
port: int
|
||||
httpHeaders?: [...{
|
||||
name: string
|
||||
value: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
|
||||
tcpSocket?: {
|
||||
// +usage=The TCP socket within the container that should be probed to assess container health.
|
||||
port: int
|
||||
}
|
||||
|
||||
// +usage=Number of seconds after the container is started before the first probe is initiated.
|
||||
initialDelaySeconds: *0 | int
|
||||
|
||||
// +usage=How often, in seconds, to execute the probe.
|
||||
periodSeconds: *10 | int
|
||||
|
||||
// +usage=Number of seconds after which the probe times out.
|
||||
timeoutSeconds: *1 | int
|
||||
|
||||
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
|
||||
successThreshold: *1 | int
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
}
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: batch/v1beta1
|
||||
kind: CronJob
|
||||
type: cronjobs.batch
|
||||
|
||||
@@ -453,6 +453,12 @@ spec:
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Specify the hostAliases to add
|
||||
hostAliases: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
@@ -494,12 +500,6 @@ spec:
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
|
||||
// +usage=Specify the hostAliases to add
|
||||
hostAliases: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
}
|
||||
status:
|
||||
customStatus: |-
|
||||
|
||||
@@ -107,7 +107,7 @@ multicluster:
|
||||
port: 9443
|
||||
image:
|
||||
repository: oamdev/cluster-gateway
|
||||
tag: v1.3.0
|
||||
tag: v1.3.2
|
||||
pullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -19,7 +19,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -30,6 +29,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/klog/v2/klogr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
@@ -147,6 +148,7 @@ func main() {
|
||||
flag.IntVar(&workflow.MaxWorkflowWaitBackoffTime, "max-workflow-wait-backoff-time", 60, "Set the max workflow wait backoff time, default is 60")
|
||||
flag.IntVar(&workflow.MaxWorkflowFailedBackoffTime, "max-workflow-failed-backoff-time", 300, "Set the max workflow wait backoff time, default is 300")
|
||||
flag.IntVar(&custom.MaxWorkflowStepErrorRetryTimes, "max-workflow-step-error-retry-times", 10, "Set the max workflow step error retry times, default is 10")
|
||||
utilfeature.DefaultMutableFeatureGate.AddFlag(flag.CommandLine)
|
||||
|
||||
flag.Parse()
|
||||
// setup logging
|
||||
|
||||
@@ -7,4 +7,5 @@ coverage:
|
||||
default:
|
||||
target: 70%
|
||||
ignore:
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "references/"
|
||||
|
||||
@@ -6,3 +6,4 @@ This directory contains guides for contributors to the KubeVela project.
|
||||
* [Developer guide](./developer-guide.md)
|
||||
* [Triage issues](./triage-issues.md)
|
||||
* [Code conventions](./coding-conventions.md)
|
||||
* [Develop Code Flow](./develop-code-flow.pdf)
|
||||
|
||||
BIN
contribute/develop-code-flow.pdf
Normal file
BIN
contribute/develop-code-flow.pdf
Normal file
Binary file not shown.
@@ -5,7 +5,7 @@ This guide helps you get started developing KubeVela.
|
||||
## Prerequisites
|
||||
|
||||
1. Golang version 1.17+
|
||||
2. Kubernetes version v1.18+ with `~/.kube/config` configured.
|
||||
2. Kubernetes version v1.20+ with `~/.kube/config` configured.
|
||||
3. ginkgo 1.14.0+ (just for [E2E test](./developer-guide.md#e2e-test))
|
||||
4. golangci-lint 1.38.0+, it will install automatically if you run `make`, you can [install it manually](https://golangci-lint.run/usage/install/#local-installation) if the installation is too slow.
|
||||
5. kubebuilder v3.1.0+ and you need to manually install the dependency tools for unit test.
|
||||
@@ -177,7 +177,7 @@ To execute the e2e test of the API module, the mongodb service needs to exist lo
|
||||
# save your config
|
||||
mv ~/.kube/config ~/.kube/config.save
|
||||
|
||||
kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca --name worker
|
||||
kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig
|
||||
kind get kubeconfig --name worker > /tmp/worker.client.kubeconfig
|
||||
|
||||
|
||||
@@ -3337,6 +3337,284 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "list all config types",
|
||||
"operationId": "listConfigTypes",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Fuzzy search based on name and description.",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1.ConfigType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get a config type",
|
||||
"operationId": "getConfigType",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ConfigType"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "create or update a config",
|
||||
"operationId": "createConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.CreateConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EmptyResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}/configs": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get configs from a config type",
|
||||
"operationId": "getConfigs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}/configs/{name}": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get a config from a config type",
|
||||
"operationId": "getConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "delete a config",
|
||||
"operationId": "deleteConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EmptyResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/definitions": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -3862,6 +4140,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/projects/{projectName}/configs": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"project"
|
||||
],
|
||||
"summary": "get configs which are in a project",
|
||||
"operationId": "getConfigs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "config type",
|
||||
"name": "configType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the project",
|
||||
"name": "projectName",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/projects/{projectName}/permissions": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -4304,6 +4631,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/repository/chart_repos": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"repository",
|
||||
"helm"
|
||||
],
|
||||
"summary": "list chart repo",
|
||||
"operationId": "listRepo",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the config project",
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/repository/charts": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -4326,6 +4696,12 @@
|
||||
"description": "helm repository url",
|
||||
"name": "repoUrl",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "secret of the repo",
|
||||
"name": "secretName",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4369,6 +4745,12 @@
|
||||
"description": "helm repository url",
|
||||
"name": "repoUrl",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "secret of the repo",
|
||||
"name": "secretName",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -4409,6 +4791,12 @@
|
||||
"description": "helm repository url",
|
||||
"name": "repoUrl",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "secret of the repo",
|
||||
"name": "secretName",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5241,6 +5629,7 @@
|
||||
},
|
||||
"definitions": {
|
||||
"*v1.ApplicationTriggerBase": {},
|
||||
"*v1.Config": {},
|
||||
"*v1.EmptyResponse": {},
|
||||
"addon.Dependency": {
|
||||
"properties": {
|
||||
@@ -5293,6 +5682,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"addon.GitlabAddonSource": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addon.HelmSource": {
|
||||
"properties": {
|
||||
"url": {
|
||||
@@ -6562,34 +6967,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.SystemInfo": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installID": {
|
||||
"type": "string"
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.WorkflowStepStatus": {
|
||||
"required": [
|
||||
"id",
|
||||
@@ -6978,6 +7355,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
@@ -7148,12 +7528,12 @@
|
||||
},
|
||||
"v1.ApplicationDeployResponse": {
|
||||
"required": [
|
||||
"note",
|
||||
"envName",
|
||||
"triggerType",
|
||||
"createTime",
|
||||
"version",
|
||||
"note",
|
||||
"status",
|
||||
"envName",
|
||||
"triggerType"
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"codeInfo": {
|
||||
@@ -7687,6 +8067,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ConfigType": {
|
||||
"required": [
|
||||
"definitions",
|
||||
"alias",
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"definitions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ConnectCloudClusterRequest": {
|
||||
"required": [
|
||||
"accessKeyID",
|
||||
@@ -7736,6 +8141,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
@@ -8032,6 +8440,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.CreateConfigRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
"alias",
|
||||
"project",
|
||||
"componentType"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"componentType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"project": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.CreateEnvRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
@@ -8243,11 +8676,11 @@
|
||||
},
|
||||
"v1.DetailAddonResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"icon",
|
||||
"invisible",
|
||||
"version",
|
||||
"description",
|
||||
"invisible",
|
||||
"name",
|
||||
"icon",
|
||||
"schema",
|
||||
"uiSchema",
|
||||
"definitions",
|
||||
@@ -8327,13 +8760,13 @@
|
||||
},
|
||||
"v1.DetailApplicationResponse": {
|
||||
"required": [
|
||||
"alias",
|
||||
"icon",
|
||||
"project",
|
||||
"description",
|
||||
"createTime",
|
||||
"name",
|
||||
"project",
|
||||
"alias",
|
||||
"updateTime",
|
||||
"icon",
|
||||
"policies",
|
||||
"envBindings",
|
||||
"applicationType",
|
||||
@@ -8394,19 +8827,19 @@
|
||||
},
|
||||
"v1.DetailClusterResponse": {
|
||||
"required": [
|
||||
"updateTime",
|
||||
"kubeConfig",
|
||||
"name",
|
||||
"icon",
|
||||
"reason",
|
||||
"provider",
|
||||
"apiServerURL",
|
||||
"name",
|
||||
"alias",
|
||||
"icon",
|
||||
"dashboardURL",
|
||||
"createTime",
|
||||
"alias",
|
||||
"description",
|
||||
"labels",
|
||||
"status",
|
||||
"updateTime",
|
||||
"apiServerURL",
|
||||
"kubeConfig",
|
||||
"labels",
|
||||
"kubeConfigSecret",
|
||||
"resourceInfo"
|
||||
],
|
||||
@@ -8465,14 +8898,14 @@
|
||||
},
|
||||
"v1.DetailComponentResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"updateTime",
|
||||
"appPrimaryKey",
|
||||
"createTime",
|
||||
"creator",
|
||||
"alias",
|
||||
"name",
|
||||
"main",
|
||||
"createTime",
|
||||
"type",
|
||||
"main",
|
||||
"appPrimaryKey",
|
||||
"definition"
|
||||
],
|
||||
"properties": {
|
||||
@@ -8574,13 +9007,13 @@
|
||||
},
|
||||
"v1.DetailPolicyResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"description",
|
||||
"creator",
|
||||
"properties",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"name",
|
||||
"type"
|
||||
"updateTime"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
@@ -8610,17 +9043,17 @@
|
||||
},
|
||||
"v1.DetailRevisionResponse": {
|
||||
"required": [
|
||||
"envName",
|
||||
"reason",
|
||||
"deployUser",
|
||||
"updateTime",
|
||||
"status",
|
||||
"note",
|
||||
"triggerType",
|
||||
"workflowName",
|
||||
"updateTime",
|
||||
"version",
|
||||
"reason",
|
||||
"createTime",
|
||||
"appPrimaryKey",
|
||||
"version"
|
||||
"status",
|
||||
"deployUser",
|
||||
"note",
|
||||
"workflowName",
|
||||
"envName",
|
||||
"appPrimaryKey"
|
||||
],
|
||||
"properties": {
|
||||
"appPrimaryKey": {
|
||||
@@ -8674,10 +9107,10 @@
|
||||
},
|
||||
"v1.DetailTargetResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"createTime",
|
||||
"project",
|
||||
"updateTime"
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
@@ -8717,11 +9150,11 @@
|
||||
},
|
||||
"v1.DetailUserResponse": {
|
||||
"required": [
|
||||
"lastLoginTime",
|
||||
"name",
|
||||
"email",
|
||||
"disabled",
|
||||
"createTime",
|
||||
"lastLoginTime",
|
||||
"projects",
|
||||
"roles"
|
||||
],
|
||||
@@ -8762,12 +9195,12 @@
|
||||
},
|
||||
"v1.DetailWorkflowRecordResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"namespace",
|
||||
"workflowName",
|
||||
"workflowAlias",
|
||||
"applicationRevision",
|
||||
"status",
|
||||
"name",
|
||||
"namespace",
|
||||
"deployTime",
|
||||
"deployUser",
|
||||
"note",
|
||||
@@ -8819,14 +9252,14 @@
|
||||
},
|
||||
"v1.DetailWorkflowResponse": {
|
||||
"required": [
|
||||
"description",
|
||||
"enable",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"name",
|
||||
"alias",
|
||||
"enable",
|
||||
"default",
|
||||
"envName"
|
||||
"envName",
|
||||
"createTime",
|
||||
"alias",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
@@ -9021,8 +9454,8 @@
|
||||
},
|
||||
"v1.EnvBindingTarget": {
|
||||
"required": [
|
||||
"name",
|
||||
"alias"
|
||||
"alias",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
@@ -9405,11 +9838,11 @@
|
||||
},
|
||||
"v1.LoginUserInfoResponse": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"lastLoginTime",
|
||||
"name",
|
||||
"email",
|
||||
"disabled",
|
||||
"createTime",
|
||||
"lastLoginTime",
|
||||
"projects",
|
||||
"platformPermissions",
|
||||
"projectPermissions"
|
||||
@@ -9717,6 +10150,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.SystemInfo": {
|
||||
"required": [
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType"
|
||||
],
|
||||
"properties": {
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installID": {
|
||||
"type": "string"
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.SystemInfoRequest": {
|
||||
"required": [
|
||||
"enableCollection",
|
||||
@@ -9728,23 +10179,20 @@
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
},
|
||||
"velaAddress": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.SystemInfoResponse": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType",
|
||||
"systemVersion"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -9756,10 +10204,6 @@
|
||||
},
|
||||
"systemVersion": {
|
||||
"$ref": "#/definitions/v1.SystemVersion"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9828,6 +10272,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,12 @@ kind: Application
|
||||
metadata:
|
||||
name: config-dex-connector-dev
|
||||
namespace: vela-system
|
||||
labels:
|
||||
"app.oam.dev/source-of-truth": "from-inner-system"
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "config-dex-connector"
|
||||
"config.oam.dev/sub-type": "github"
|
||||
project: abc
|
||||
spec:
|
||||
components:
|
||||
- name: dev
|
||||
@@ -12,4 +18,4 @@ spec:
|
||||
github:
|
||||
clientID: "aa"
|
||||
clientSecret: "bb"
|
||||
callbackURL: "http://localhost:8080/callback"
|
||||
redirectURI: "http://localhost:8080/callback"
|
||||
@@ -1,11 +1,16 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: config-image-registry-account-auth-dev
|
||||
name: image-dev
|
||||
namespace: vela-system
|
||||
labels:
|
||||
"app.oam.dev/source-of-truth": "from-inner-system"
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "config-image-registry"
|
||||
project: abc
|
||||
spec:
|
||||
components:
|
||||
- name: account-auth
|
||||
- name: image-dev
|
||||
type: config-image-registry
|
||||
properties:
|
||||
registry: "registry.cn-beijing.aliyuncs.com"
|
||||
|
||||
@@ -12,12 +12,11 @@ spec:
|
||||
type: ref-objects
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: Secret
|
||||
name: image-registry-dev
|
||||
- name: reg-demo
|
||||
resource: secret
|
||||
policies:
|
||||
- type: topology
|
||||
name: dev
|
||||
properties:
|
||||
clusters: ["bj"]
|
||||
# namespaces: ["ns1"]
|
||||
namespace: default
|
||||
|
||||
@@ -129,7 +129,7 @@ var _ = Describe("Test Kubectl Plugin", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(output).Should(ContainSubstring(showTdResult))
|
||||
})
|
||||
It("Test show componentDefinition use Helm Charts as Workload", func() {
|
||||
PIt("Test show componentDefinition use Helm Charts as Workload", func() {
|
||||
Eventually(func() string {
|
||||
cdName := "test-webapp-chart"
|
||||
output, _ := e2e.Exec(fmt.Sprintf("kubectl-vela show %s -n default", cdName))
|
||||
|
||||
7
go.mod
7
go.mod
@@ -65,7 +65,8 @@ require (
|
||||
go.uber.org/zap v1.18.1
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/tools v0.1.6 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
@@ -145,6 +146,7 @@ require (
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
|
||||
github.com/emicklei/proto v1.6.15 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
@@ -250,8 +252,7 @@ require (
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
|
||||
13
go.sum
13
go.sum
@@ -420,8 +420,9 @@ github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4=
|
||||
github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
@@ -2046,13 +2047,14 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -2063,8 +2065,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -1070,7 +1070,7 @@ func (h *Installer) enableAddon(addon *InstallPackage) error {
|
||||
h.addon = addon
|
||||
err = checkAddonVersionMeetRequired(h.ctx, addon.SystemRequirements, h.cli, h.dc)
|
||||
if err != nil {
|
||||
return ErrVersionMismatch
|
||||
return VersionUnMatchError{addonName: addon.Name, err: err}
|
||||
}
|
||||
|
||||
if err = h.installDependency(addon); err != nil {
|
||||
@@ -1188,6 +1188,8 @@ func (h *Installer) createOrUpdate(app *v1beta1.Application) error {
|
||||
return err
|
||||
}
|
||||
getapp.Spec = app.Spec
|
||||
getapp.Labels = app.Labels
|
||||
getapp.Annotations = app.Annotations
|
||||
err = h.cli.Update(h.ctx, &getapp)
|
||||
if err != nil {
|
||||
klog.Errorf("fail to create application: %v", err)
|
||||
@@ -1342,7 +1344,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
return err
|
||||
}
|
||||
if !res {
|
||||
return fmt.Errorf("vela cli/ux version: %s cannot meet requirement", version2.VelaVersion)
|
||||
return fmt.Errorf("vela cli/ux version: %s require: %s", version2.VelaVersion, require.VelaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,7 +1361,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
return err
|
||||
}
|
||||
if !res {
|
||||
return fmt.Errorf("the vela core controller: %s cannot meet requirement ", imageVersion)
|
||||
return fmt.Errorf("the vela core controller: %s require: %s", imageVersion, require.VelaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1380,7 +1382,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
}
|
||||
|
||||
if !res {
|
||||
return fmt.Errorf("the kubernetes version %s cannot meet requirement", k8sVersion.GitVersion)
|
||||
return fmt.Errorf("the kubernetes version %s require: %s", k8sVersion.GitVersion, require.KubernetesVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -323,6 +323,45 @@ var _ = Describe("Test render addon with specified clusters", func() {
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("func addon update ", func() {
|
||||
It("test update addon app label", func() {
|
||||
app_test_update := v1beta1.Application{}
|
||||
Expect(yaml.Unmarshal([]byte(addonUpdateAppYaml), &app_test_update)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, &app_test_update)).Should(BeNil())
|
||||
|
||||
Eventually(func() error {
|
||||
var err error
|
||||
appCheck := v1beta1.Application{}
|
||||
err = k8sClient.Get(ctx, types2.NamespacedName{Namespace: "vela-system", Name: "addon-test-update"}, &appCheck)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appCheck.Labels["addons.oam.dev/version"] != "v1.2.0" {
|
||||
return fmt.Errorf("label missmatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Millisecond*500, 30*time.Second).Should(BeNil())
|
||||
|
||||
pkg := &InstallPackage{Meta: Meta{Name: "test-update", Version: "1.3.0"}}
|
||||
h := NewAddonInstaller(context.Background(), k8sClient, nil, nil, nil, &Registry{Name: "test"}, nil, nil)
|
||||
h.addon = pkg
|
||||
Expect(h.dispatchAddonResource(pkg)).Should(BeNil())
|
||||
|
||||
Eventually(func() error {
|
||||
var err error
|
||||
appCheck := v1beta1.Application{}
|
||||
err = k8sClient.Get(context.Background(), types2.NamespacedName{Namespace: "vela-system", Name: "addon-test-update"}, &appCheck)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appCheck.Labels["addons.oam.dev/version"] != "1.3.0" {
|
||||
return fmt.Errorf("label missmatch")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*3, 300*time.Second).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
const (
|
||||
appYaml = `apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
@@ -400,4 +439,21 @@ spec:
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30`
|
||||
|
||||
addonUpdateAppYaml = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: addon-test-update
|
||||
namespace: vela-system
|
||||
labels:
|
||||
addons.oam.dev/version: v1.2.0
|
||||
spec:
|
||||
components:
|
||||
- name: express-server
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
port: 8000
|
||||
`
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -30,9 +30,38 @@ const (
|
||||
// SystemInfo systemInfo model
|
||||
type SystemInfo struct {
|
||||
BaseModel
|
||||
InstallID string `json:"installID"`
|
||||
EnableCollection bool `json:"enableCollection"`
|
||||
LoginType string `json:"loginType"`
|
||||
InstallID string `json:"installID"`
|
||||
EnableCollection bool `json:"enableCollection"`
|
||||
LoginType string `json:"loginType"`
|
||||
DexConfig DexConfig `json:"dexConfig,omitempty"`
|
||||
}
|
||||
|
||||
// DexConfig dex config
|
||||
type DexConfig struct {
|
||||
Issuer string `json:"issuer"`
|
||||
Web DexWeb `json:"web"`
|
||||
Storage DexStorage `json:"storage"`
|
||||
StaticClients []DexStaticClient `json:"staticClients"`
|
||||
Connectors []interface{} `json:"connectors,omitempty"`
|
||||
EnablePasswordDB bool `json:"enablePasswordDB"`
|
||||
}
|
||||
|
||||
// DexStorage dex storage
|
||||
type DexStorage struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// DexWeb dex web
|
||||
type DexWeb struct {
|
||||
HTTP string `json:"http"`
|
||||
}
|
||||
|
||||
// DexStaticClient dex static client
|
||||
type DexStaticClient struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Secret string `json:"secret"`
|
||||
RedirectURIs []string `json:"redirectURIs"`
|
||||
}
|
||||
|
||||
// TableName return custom table name
|
||||
|
||||
@@ -184,6 +184,28 @@ type AddonArgsResponse struct {
|
||||
Args map[string]string `json:"args"`
|
||||
}
|
||||
|
||||
// ConfigType define the format for listing configuration types
|
||||
type ConfigType struct {
|
||||
Definitions []string `json:"definitions"`
|
||||
Alias string `json:"alias"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Config define the metadata of a config
|
||||
type Config struct {
|
||||
ConfigType string `json:"configType"`
|
||||
ConfigTypeAlias string `json:"configTypeAlias"`
|
||||
Name string `json:"name"`
|
||||
Project string `json:"project"`
|
||||
Identifier string `json:"identifier"`
|
||||
Description string `json:"description"`
|
||||
CreatedTime *time.Time `json:"createdTime"`
|
||||
UpdatedTime *time.Time `json:"updatedTime"`
|
||||
ApplicationStatus common.ApplicationPhase `json:"applicationStatus"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// AccessKeyRequest request parameters to access cloud provider
|
||||
type AccessKeyRequest struct {
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
@@ -382,6 +404,15 @@ type CreateApplicationRequest struct {
|
||||
Component *CreateComponentRequest `json:"component"`
|
||||
}
|
||||
|
||||
// CreateConfigRequest is the request body to creates a config
|
||||
type CreateConfigRequest struct {
|
||||
Name string `json:"name" validate:"checkname"`
|
||||
Alias string `json:"alias"`
|
||||
Project string `json:"project"`
|
||||
ComponentType string `json:"componentType" validate:"checkname"`
|
||||
Properties string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateApplicationRequest update application base config
|
||||
type UpdateApplicationRequest struct {
|
||||
Alias string `json:"alias" validate:"checkalias" optional:"true"`
|
||||
@@ -1081,14 +1112,22 @@ type DetailRevisionResponse struct {
|
||||
|
||||
// SystemInfoResponse get SystemInfo
|
||||
type SystemInfoResponse struct {
|
||||
model.SystemInfo
|
||||
SystemInfo
|
||||
SystemVersion SystemVersion `json:"systemVersion"`
|
||||
}
|
||||
|
||||
// SystemInfo system info
|
||||
type SystemInfo struct {
|
||||
InstallID string `json:"installID"`
|
||||
EnableCollection bool `json:"enableCollection"`
|
||||
LoginType string `json:"loginType"`
|
||||
}
|
||||
|
||||
// SystemInfoRequest request by update SystemInfo
|
||||
type SystemInfoRequest struct {
|
||||
EnableCollection bool `json:"enableCollection"`
|
||||
LoginType string `json:"loginType"`
|
||||
VelaAddress string `json:"velaAddress,omitempty"`
|
||||
}
|
||||
|
||||
// SystemVersion contains KubeVela version
|
||||
@@ -1277,3 +1316,14 @@ type LoginUserInfoResponse struct {
|
||||
PlatformPermissions []PermissionBase `json:"platformPermissions"`
|
||||
ProjectPermissions map[string][]PermissionBase `json:"projectPermissions"`
|
||||
}
|
||||
|
||||
// ChartRepoResponse the response body of chart repo
|
||||
type ChartRepoResponse struct {
|
||||
URL string `json:"url"`
|
||||
SecretName string `json:"secretName"`
|
||||
}
|
||||
|
||||
// ChartRepoResponseList the response body of list chart repo
|
||||
type ChartRepoResponseList struct {
|
||||
ChartRepoResponse []*ChartRepoResponse `json:"repos"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,18 +33,18 @@ import (
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
@@ -686,7 +686,7 @@ func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Ap
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Deploy deploy app to cluster
|
||||
// Deploy deploys app to cluster
|
||||
// means to render oam application config and apply to cluster.
|
||||
// An event record is generated for each deploy.
|
||||
func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) {
|
||||
@@ -704,6 +704,11 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// sync configs to clusters
|
||||
if err := c.syncConfigs4Application(ctx, oamApp, app.Project, workflow.EnvName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// step2: check and create deploy event
|
||||
if !req.Force {
|
||||
var lastVersion = model.ApplicationRevision{
|
||||
@@ -792,6 +797,44 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sync configs to clusters
|
||||
func (c *applicationUsecaseImpl) syncConfigs4Application(ctx context.Context, app *v1beta1.Application, projectName, envName string) error {
|
||||
var areTerraformComponents = true
|
||||
for _, m := range app.Spec.Components {
|
||||
d := &v1beta1.ComponentDefinition{}
|
||||
if err := c.kubeClient.Get(ctx, client.ObjectKey{Namespace: velatypes.DefaultKubeVelaNS, Name: m.Type}, d); err != nil {
|
||||
klog.ErrorS(err, "failed to get config type", "ComponentDefinition", m.Type)
|
||||
}
|
||||
// check the type of the componentDefinition is Terraform
|
||||
if d.Spec.Schematic != nil && d.Spec.Schematic.Terraform == nil {
|
||||
areTerraformComponents = false
|
||||
}
|
||||
}
|
||||
// skip configs sync
|
||||
if areTerraformComponents {
|
||||
return nil
|
||||
}
|
||||
env, err := c.envUsecase.GetEnv(ctx, envName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var clusterTargets []*model.ClusterTarget
|
||||
for _, t := range env.Targets {
|
||||
target, err := c.targetUsecase.GetTarget(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Cluster != nil {
|
||||
clusterTargets = append(clusterTargets, target.Cluster)
|
||||
}
|
||||
}
|
||||
|
||||
if err := SyncConfigs(ctx, c.kubeClient, projectName, clusterTargets); err != nil {
|
||||
return fmt.Errorf("sync config failure %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) {
|
||||
// Priority 1 uses the requested workflow as release .
|
||||
// Priority 2 uses the default workflow as release .
|
||||
|
||||
@@ -18,6 +18,7 @@ package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -25,26 +26,30 @@ import (
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/form3tech-oss/jwt-go"
|
||||
"golang.org/x/oauth2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
)
|
||||
|
||||
const (
|
||||
secretDexConfig = "dex-config"
|
||||
keyDex = "dex"
|
||||
dexConfigName = "dex-config"
|
||||
secretDexConfigKey = "config.yaml"
|
||||
dexAddonName = "addon-dex"
|
||||
jwtIssuer = "vela-issuer"
|
||||
signedKey = "vela-singned"
|
||||
|
||||
// GrantTypeAccess is the grant type for access token
|
||||
GrantTypeAccess = "access"
|
||||
@@ -52,12 +57,15 @@ const (
|
||||
GrantTypeRefresh = "refresh"
|
||||
)
|
||||
|
||||
var signedKey = ""
|
||||
|
||||
// AuthenticationUsecase is the usecase of authentication
|
||||
type AuthenticationUsecase interface {
|
||||
Login(ctx context.Context, loginReq apisv1.LoginRequest) (*apisv1.LoginResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error)
|
||||
GetDexConfig(ctx context.Context) (*apisv1.DexConfigResponse, error)
|
||||
GetLoginType(ctx context.Context) (*apisv1.GetLoginTypeResponse, error)
|
||||
UpdateDexConfig(ctx context.Context) error
|
||||
}
|
||||
|
||||
type authenticationUsecaseImpl struct {
|
||||
@@ -86,7 +94,6 @@ type authHandler interface {
|
||||
}
|
||||
|
||||
type dexHandlerImpl struct {
|
||||
token *oauth2.Token
|
||||
idToken *oidc.IDToken
|
||||
ds datastore.DataStore
|
||||
}
|
||||
@@ -127,7 +134,6 @@ func (a *authenticationUsecaseImpl) newDexHandler(ctx context.Context, req apisv
|
||||
return nil, err
|
||||
}
|
||||
return &dexHandlerImpl{
|
||||
token: token,
|
||||
idToken: idToken,
|
||||
ds: a.ds,
|
||||
}, nil
|
||||
@@ -148,7 +154,7 @@ func (a *authenticationUsecaseImpl) newLocalHandler(req apisv1.LoginRequest) (*l
|
||||
func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.LoginRequest) (*apisv1.LoginResponse, error) {
|
||||
var handler authHandler
|
||||
var err error
|
||||
sysInfo, err := a.sysUsecase.GetSystemInfo(ctx)
|
||||
sysInfo, err := a.sysUsecase.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,13 +208,15 @@ func (a *authenticationUsecaseImpl) generateJWTToken(username, grantType string,
|
||||
GrantType: grantType,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
return token.SignedString([]byte(signedKey))
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error) {
|
||||
claim, err := ParseToken(refreshToken)
|
||||
if err != nil {
|
||||
if errors.Is(err, bcode.ErrTokenExpired) {
|
||||
return nil, bcode.ErrRefreshTokenExpired
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if claim.GrantType == GrantTypeRefresh {
|
||||
@@ -253,33 +261,10 @@ func ParseToken(tokenString string) (*model.CustomClaims, error) {
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) GetDexConfig(ctx context.Context) (*apisv1.DexConfigResponse, error) {
|
||||
secret := &v1.Secret{}
|
||||
if err := a.kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: secretDexConfig,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
}, secret); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, bcode.ErrDexConfigNotFound
|
||||
}
|
||||
log.Logger.Errorf("failed to get dex config: %s", err.Error())
|
||||
config, err := getDexConfig(ctx, a.kubeClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var config struct {
|
||||
Issuer string `json:"issuer"`
|
||||
StaticClients []struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
RedirectURIs []string `json:"redirectURIs"`
|
||||
} `json:"staticClients"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(secret.Data[secretDexConfigKey], &config); err != nil {
|
||||
log.Logger.Errorf("failed to unmarshal dex config: %s", err.Error())
|
||||
return nil, bcode.ErrInvalidDexConfig
|
||||
}
|
||||
if len(config.StaticClients) < 1 || len(config.StaticClients[0].RedirectURIs) < 1 {
|
||||
return nil, bcode.ErrInvalidDexConfig
|
||||
}
|
||||
return &apisv1.DexConfigResponse{
|
||||
Issuer: config.Issuer,
|
||||
ClientID: config.StaticClients[0].ID,
|
||||
@@ -288,8 +273,121 @@ func (a *authenticationUsecaseImpl) GetDexConfig(ctx context.Context) (*apisv1.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) UpdateDexConfig(ctx context.Context) error {
|
||||
connectors, err := utils.GetDexConnectors(ctx, a.kubeClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dexConfig := &corev1.Secret{}
|
||||
if err := a.kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: dexConfigName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
}, dexConfig); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
config := &model.JSONStruct{}
|
||||
if dexConfig == nil || dexConfig.Data == nil {
|
||||
(*config)["connectors"] = connectors
|
||||
c, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := a.kubeClient.Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: dexConfigName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
Data: map[string][]byte{
|
||||
secretDexConfigKey: c,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = yaml.Unmarshal(dexConfig.Data[secretDexConfigKey], config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
(*config)["connectors"] = connectors
|
||||
c, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dexConfig.Data[secretDexConfigKey] = c
|
||||
if err := a.kubeClient.Update(ctx, dexConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return restartDex(ctx, a.kubeClient)
|
||||
}
|
||||
|
||||
func restartDex(ctx context.Context, kubeClient client.Client) error {
|
||||
dexApp := &v1beta1.Application{}
|
||||
if err := kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: dexAddonName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
}, dexApp); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return bcode.ErrDexNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
for i, comp := range dexApp.Spec.Components {
|
||||
if comp.Name == keyDex {
|
||||
var v model.JSONStruct
|
||||
err := json.Unmarshal(comp.Properties.Raw, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// restart the dex server
|
||||
if _, ok := v["values"]; ok {
|
||||
v["values"].(map[string]interface{})["env"] = map[string]string{
|
||||
"TIME_STAMP": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
dexApp.Spec.Components[i].Properties = v.RawExtension()
|
||||
if err := kubeClient.Update(ctx, dexApp); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getDexConfig(ctx context.Context, kubeClient client.Client) (*model.DexConfig, error) {
|
||||
dexConfigSecret := &corev1.Secret{}
|
||||
if err := kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: dexConfigName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
}, dexConfigSecret); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil, bcode.ErrDexConfigNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if dexConfigSecret.Data == nil {
|
||||
return nil, bcode.ErrInvalidDexConfig
|
||||
}
|
||||
|
||||
config := &model.DexConfig{}
|
||||
if err := yaml.Unmarshal(dexConfigSecret.Data[secretDexConfigKey], config); err != nil {
|
||||
log.Logger.Errorf("failed to unmarshal dex config: %s", err.Error())
|
||||
return nil, bcode.ErrInvalidDexConfig
|
||||
}
|
||||
if len(config.StaticClients) < 1 || len(config.StaticClients[0].RedirectURIs) < 1 {
|
||||
return nil, bcode.ErrInvalidDexConfig
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) GetLoginType(ctx context.Context) (*apisv1.GetLoginTypeResponse, error) {
|
||||
sysInfo, err := a.sysUsecase.GetSystemInfo(ctx)
|
||||
sysInfo, err := a.sysUsecase.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -312,19 +410,18 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
}
|
||||
|
||||
user := &model.User{Email: claims.Email}
|
||||
userBase := &apisv1.UserBase{Email: claims.Email, Name: claims.Name}
|
||||
users, err := d.ds.List(ctx, user, &datastore.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) > 0 {
|
||||
u := users[0].(*model.User)
|
||||
if u.Name != claims.Name {
|
||||
u.Name = claims.Name
|
||||
}
|
||||
u.LastLoginTime = time.Now()
|
||||
if err := d.ds.Put(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userBase.Name = u.Name
|
||||
} else if err := d.ds.Add(ctx, &model.User{
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
@@ -333,10 +430,7 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apisv1.UserBase{
|
||||
Name: claims.Name,
|
||||
Email: claims.Email,
|
||||
}, nil
|
||||
return userBase, nil
|
||||
}
|
||||
|
||||
func (l *localHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -27,13 +28,18 @@ import (
|
||||
"github.com/coreos/go-oidc"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/oauth2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
@@ -51,7 +57,7 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
Expect(ds).ToNot(BeNil())
|
||||
Expect(err).Should(BeNil())
|
||||
authUsecase = &authenticationUsecaseImpl{kubeClient: k8sClient, ds: ds}
|
||||
sysUsecase = &systemInfoUsecaseImpl{ds: ds}
|
||||
sysUsecase = &systemInfoUsecaseImpl{ds: ds, kubeClient: k8sClient}
|
||||
userUsecase = &userUsecaseImpl{ds: ds, sysUsecase: sysUsecase}
|
||||
})
|
||||
It("Test Dex login", func() {
|
||||
@@ -61,10 +67,6 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
})
|
||||
defer patch.Reset()
|
||||
dexHandler := dexHandlerImpl{
|
||||
token: &oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
},
|
||||
idToken: testIDToken,
|
||||
ds: ds,
|
||||
}
|
||||
@@ -79,6 +81,20 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
err = ds.Get(context.Background(), user)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(user.Email).Should(Equal("test@test.com"))
|
||||
|
||||
existUser := &model.User{
|
||||
Name: "test",
|
||||
}
|
||||
err = ds.Delete(context.Background(), existUser)
|
||||
Expect(err).Should(BeNil())
|
||||
existUser.Name = "exist-user"
|
||||
existUser.Email = "test@test.com"
|
||||
err = ds.Add(context.Background(), existUser)
|
||||
Expect(err).Should(BeNil())
|
||||
resp, err = dexHandler.login(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(resp.Email).Should(Equal("test@test.com"))
|
||||
Expect(resp.Name).Should(Equal("exist-user"))
|
||||
})
|
||||
|
||||
It("Test local login", func() {
|
||||
@@ -99,37 +115,87 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
Expect(resp.Name).Should(Equal("test-login"))
|
||||
})
|
||||
|
||||
It("Test get dex config", func() {
|
||||
It("Test update dex config", func() {
|
||||
err := k8sClient.Create(context.Background(), &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "vela-system",
|
||||
},
|
||||
})
|
||||
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
err = k8sClient.Create(context.Background(), &corev1.Secret{
|
||||
webserver, err := ioutil.ReadFile("./testdata/dex-config-def.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
var cd v1beta1.ComponentDefinition
|
||||
err = yaml.Unmarshal(webserver, &cd)
|
||||
Expect(err).Should(Succeed())
|
||||
err = k8sClient.Create(context.Background(), &cd)
|
||||
Expect(err).Should(Succeed())
|
||||
err = k8sClient.Create(context.Background(), &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretDexConfig,
|
||||
Name: "addon-dex",
|
||||
Namespace: "vela-system",
|
||||
},
|
||||
StringData: map[string]string{
|
||||
secretDexConfigKey: `
|
||||
issuer: https://dex.oam.dev
|
||||
staticClients:
|
||||
- id: client-id
|
||||
secret: client-secret
|
||||
redirectURIs:
|
||||
- http://localhost:8080/auth/callback
|
||||
`,
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "dex",
|
||||
// only for test here
|
||||
Type: "dex-config",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"values":{"test":"test"}}`)},
|
||||
Traits: []common.ApplicationTrait{},
|
||||
Scopes: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
config, err := authUsecase.GetDexConfig(context.Background())
|
||||
err = k8sClient.Create(context.Background(), &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
Namespace: "vela-system",
|
||||
Labels: map[string]string{
|
||||
"app.oam.dev/source-of-truth": "from-inner-system",
|
||||
"config.oam.dev/catalog": "velacore-config",
|
||||
"config.oam.dev/type": "config-dex-connector",
|
||||
"config.oam.dev/sub-type": "ldap",
|
||||
"project": "abc",
|
||||
},
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"ldap": `{"clientID":"clientID","clientSecret":"clientSecret"}`,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
By("try to update dex config without config secret")
|
||||
err = authUsecase.UpdateDexConfig(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
dexConfigSecret := &corev1.Secret{}
|
||||
err = k8sClient.Get(context.Background(), types.NamespacedName{Name: "dex-config", Namespace: "vela-system"}, dexConfigSecret)
|
||||
Expect(err).Should(BeNil())
|
||||
config := &model.DexConfig{}
|
||||
err = yaml.Unmarshal(dexConfigSecret.Data[secretDexConfigKey], config)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(config.Connectors)).Should(Equal(1))
|
||||
By("try to update dex config with config secret")
|
||||
err = authUsecase.UpdateDexConfig(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(config.Issuer).Should(Equal("https://dex.oam.dev"))
|
||||
Expect(config.ClientID).Should(Equal("client-id"))
|
||||
Expect(config.ClientSecret).Should(Equal("client-secret"))
|
||||
Expect(config.RedirectURL).Should(Equal("http://localhost:8080/auth/callback"))
|
||||
})
|
||||
|
||||
It("Test get dex config", func() {
|
||||
_, err := authUsecase.GetDexConfig(context.Background())
|
||||
Expect(err).Should(Equal(bcode.ErrInvalidDexConfig))
|
||||
err = ds.Add(context.Background(), &model.User{Name: "admin", Email: "test@test.com"})
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = sysUsecase.UpdateSystemInfo(context.Background(), apisv1.SystemInfoRequest{
|
||||
LoginType: model.LoginTypeDex,
|
||||
VelaAddress: "http://velaux.com",
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
config, err := authUsecase.GetDexConfig(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(config.Issuer).Should(Equal("http://velaux.com/dex"))
|
||||
Expect(config.ClientID).Should(Equal("velaux"))
|
||||
Expect(config.ClientSecret).Should(Equal("velaux-secret"))
|
||||
Expect(config.RedirectURL).Should(Equal("http://velaux.com/callback"))
|
||||
})
|
||||
})
|
||||
|
||||
478
pkg/apiserver/rest/usecase/config.go
Normal file
478
pkg/apiserver/rest/usecase/config.go
Normal file
@@ -0,0 +1,478 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
set "github.com/deckarep/golang-set"
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/definition"
|
||||
)
|
||||
|
||||
const (
|
||||
definitionAlias = definition.UserPrefix + "alias.config.oam.dev"
|
||||
definitionType = definition.UserPrefix + "type.config.oam.dev"
|
||||
|
||||
velaCoreConfig = "velacore-config"
|
||||
configIsReady = "Ready"
|
||||
configIsNotReady = "Not ready"
|
||||
terraformProviderAlias = "Terraform Cloud Provider"
|
||||
configSyncProjectPrefix = "config-sync"
|
||||
)
|
||||
|
||||
// ConfigHandler handle CRUD of configs
|
||||
type ConfigHandler interface {
|
||||
ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error)
|
||||
GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error)
|
||||
CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error
|
||||
GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error)
|
||||
GetConfig(ctx context.Context, configType, name string) (*apis.Config, error)
|
||||
DeleteConfig(ctx context.Context, configType, name string) error
|
||||
}
|
||||
|
||||
// NewConfigUseCase returns a config use case
|
||||
func NewConfigUseCase(authenticationUseCase AuthenticationUsecase) ConfigHandler {
|
||||
k8sClient, err := clients.GetKubeClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &configUseCaseImpl{
|
||||
authenticationUseCase: authenticationUseCase,
|
||||
kubeClient: k8sClient,
|
||||
}
|
||||
}
|
||||
|
||||
type configUseCaseImpl struct {
|
||||
kubeClient client.Client
|
||||
authenticationUseCase AuthenticationUsecase
|
||||
}
|
||||
|
||||
// ListConfigTypes returns all config types
|
||||
func (u *configUseCaseImpl) ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error) {
|
||||
defs := &v1beta1.ComponentDefinitionList{}
|
||||
if err := u.kubeClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tfDefs []v1beta1.ComponentDefinition
|
||||
var configTypes []*apis.ConfigType
|
||||
|
||||
for _, d := range defs.Items {
|
||||
if d.Labels[definitionType] == types.TerraformProvider {
|
||||
tfDefs = append(tfDefs, d)
|
||||
continue
|
||||
}
|
||||
configTypes = append(configTypes, &apis.ConfigType{
|
||||
Alias: d.Annotations[definitionAlias],
|
||||
Name: d.Name,
|
||||
Definitions: []string{d.Name},
|
||||
Description: d.Annotations[types.AnnoDefinitionDescription],
|
||||
})
|
||||
}
|
||||
|
||||
if len(tfDefs) > 0 {
|
||||
tfType := &apis.ConfigType{
|
||||
Alias: terraformProviderAlias,
|
||||
Name: types.TerraformProvider,
|
||||
}
|
||||
definitions := make([]string, len(tfDefs))
|
||||
|
||||
for i, tf := range tfDefs {
|
||||
definitions[i] = tf.Name
|
||||
}
|
||||
tfType.Definitions = definitions
|
||||
|
||||
return append(configTypes, tfType), nil
|
||||
}
|
||||
return configTypes, nil
|
||||
}
|
||||
|
||||
// GetConfigType returns a config type
|
||||
func (u *configUseCaseImpl) GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error) {
|
||||
d := &v1beta1.ComponentDefinition{}
|
||||
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: configType}, d); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get config type")
|
||||
}
|
||||
|
||||
t := &apis.ConfigType{
|
||||
Alias: d.Annotations[definitionAlias],
|
||||
Name: configType,
|
||||
Description: d.Annotations[types.AnnoDefinitionDescription],
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error {
|
||||
app := v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Annotations: map[string]string{
|
||||
types.AnnotationConfigAlias: req.Alias,
|
||||
},
|
||||
Labels: map[string]string{
|
||||
model.LabelSourceOfTruth: model.FromInner,
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigType: req.ComponentType,
|
||||
types.LabelConfigProject: req.Project,
|
||||
},
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: req.Name,
|
||||
Type: req.ComponentType,
|
||||
Properties: &runtime.RawExtension{Raw: []byte(req.Properties)},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return u.kubeClient.Create(ctx, &app)
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error) {
|
||||
switch configType {
|
||||
case types.TerraformProvider:
|
||||
defs := &v1beta1.ComponentDefinitionList{}
|
||||
if err := u.kubeClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
definition.UserPrefix + "type.config.oam.dev": types.TerraformProvider,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var configs []*apis.Config
|
||||
for _, d := range defs.Items {
|
||||
subConfigs, err := u.getConfigsByConfigType(ctx, d.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
configs = append(configs, subConfigs...)
|
||||
}
|
||||
return configs, nil
|
||||
|
||||
default:
|
||||
return u.getConfigsByConfigType(ctx, configType)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) getConfigsByConfigType(ctx context.Context, configType string) ([]*apis.Config, error) {
|
||||
var apps = &v1beta1.ApplicationList{}
|
||||
if err := u.kubeClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{
|
||||
model.LabelSourceOfTruth: model.FromInner,
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigType: configType,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configs := make([]*apis.Config, len(apps.Items))
|
||||
for i, a := range apps.Items {
|
||||
configs[i] = &apis.Config{
|
||||
ConfigType: a.Labels[types.LabelConfigType],
|
||||
Name: a.Name,
|
||||
Project: a.Labels[types.LabelConfigProject],
|
||||
CreatedTime: &(a.CreationTimestamp.Time),
|
||||
}
|
||||
switch a.Status.Phase {
|
||||
case common.ApplicationRunning:
|
||||
configs[i].Status = configIsReady
|
||||
default:
|
||||
configs[i].Status = configIsNotReady
|
||||
}
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) GetConfig(ctx context.Context, configType, name string) (*apis.Config, error) {
|
||||
var a = &v1beta1.Application{}
|
||||
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &apis.Config{
|
||||
ConfigType: a.Labels[types.LabelConfigType],
|
||||
Name: a.Name,
|
||||
Project: a.Labels[types.LabelConfigProject],
|
||||
CreatedTime: &a.CreationTimestamp.Time,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) DeleteConfig(ctx context.Context, configType, name string) error {
|
||||
var a = &v1beta1.Application{}
|
||||
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
|
||||
return err
|
||||
}
|
||||
return u.kubeClient.Delete(ctx, a)
|
||||
}
|
||||
|
||||
// ApplicationDeployTarget is the struct of application deploy target
|
||||
type ApplicationDeployTarget struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Clusters []string `json:"clusters"`
|
||||
}
|
||||
|
||||
// SyncConfigs will sync configs to working clusters
|
||||
func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, targets []*model.ClusterTarget) error {
|
||||
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
|
||||
// get all configs which can be synced to working clusters in the project
|
||||
var secrets v1.SecretList
|
||||
if err := k8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigSyncToMultiCluster: "true",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secrets.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
var objects []map[string]string
|
||||
for _, s := range secrets.Items {
|
||||
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
|
||||
objects = append(objects, map[string]string{
|
||||
"name": s.Name,
|
||||
"resource": "secret",
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(objects) == 0 {
|
||||
klog.InfoS("no configs need to sync to working clusters", "project", project)
|
||||
return nil
|
||||
}
|
||||
objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var app = &v1beta1.Application{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
// config sync application doesn't exist, create one
|
||||
clusterTargets := convertClusterTargets(targets)
|
||||
if len(clusterTargets) == 0 {
|
||||
errMsg := "no policy (no targets found) to sync configs"
|
||||
klog.InfoS(errMsg, "project", project)
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
policies := make([]v1beta1.AppPolicy, len(clusterTargets))
|
||||
for i, t := range clusterTargets {
|
||||
properties, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies[i] = v1beta1.AppPolicy{
|
||||
Type: "topology",
|
||||
Name: t.Namespace,
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
scratch := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Labels: map[string]string{
|
||||
model.LabelSourceOfTruth: model.FromInner,
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigProject: project,
|
||||
},
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: name,
|
||||
Type: "ref-objects",
|
||||
Properties: &runtime.RawExtension{Raw: objectsBytes},
|
||||
},
|
||||
},
|
||||
Policies: policies,
|
||||
},
|
||||
}
|
||||
return k8sClient.Create(ctx, scratch)
|
||||
}
|
||||
// config sync application exists, update it
|
||||
app.Spec.Components = []common.ApplicationComponent{
|
||||
{
|
||||
Name: name,
|
||||
Type: "ref-objects",
|
||||
Properties: &runtime.RawExtension{Raw: objectsBytes},
|
||||
},
|
||||
}
|
||||
currentTargets := make([]ApplicationDeployTarget, len(app.Spec.Policies))
|
||||
for i, p := range app.Spec.Policies {
|
||||
var t ApplicationDeployTarget
|
||||
if err := json.Unmarshal(p.Properties.Raw, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
currentTargets[i] = t
|
||||
}
|
||||
|
||||
mergedTarget := mergeTargets(currentTargets, targets)
|
||||
if len(mergedTarget) == 0 {
|
||||
errMsg := "no policy (no targets found) to sync configs"
|
||||
klog.InfoS(errMsg, "project", project)
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
mergedPolicies := make([]v1beta1.AppPolicy, len(mergedTarget))
|
||||
for i, t := range mergedTarget {
|
||||
properties, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mergedPolicies[i] = v1beta1.AppPolicy{
|
||||
Type: "topology",
|
||||
Name: t.Namespace,
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
app.Spec.Policies = mergedPolicies
|
||||
return k8sClient.Update(ctx, app)
|
||||
}
|
||||
|
||||
func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.ClusterTarget) []ApplicationDeployTarget {
|
||||
var (
|
||||
mergedTargets []ApplicationDeployTarget
|
||||
// make sure the clusters of target with same namespace are merged
|
||||
clusterTargets = convertClusterTargets(targets)
|
||||
)
|
||||
|
||||
for _, c := range currentTargets {
|
||||
var hasSameNamespace bool
|
||||
for _, t := range clusterTargets {
|
||||
if c.Namespace == t.Namespace {
|
||||
hasSameNamespace = true
|
||||
clusters := set.NewSetFromSlice(stringToInterfaceSlice(t.Clusters))
|
||||
for _, cluster := range c.Clusters {
|
||||
clusters.Add(cluster)
|
||||
}
|
||||
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace,
|
||||
Clusters: interfaceToStringSlice(clusters.ToSlice())})
|
||||
}
|
||||
}
|
||||
if !hasSameNamespace {
|
||||
mergedTargets = append(mergedTargets, c)
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range clusterTargets {
|
||||
var hasSameNamespace bool
|
||||
for _, c := range currentTargets {
|
||||
if c.Namespace == t.Namespace {
|
||||
hasSameNamespace = true
|
||||
}
|
||||
}
|
||||
if !hasSameNamespace {
|
||||
mergedTargets = append(mergedTargets, t)
|
||||
}
|
||||
}
|
||||
|
||||
return mergedTargets
|
||||
}
|
||||
|
||||
func convertClusterTargets(targets []*model.ClusterTarget) []ApplicationDeployTarget {
|
||||
type Target struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Clusters []interface{} `json:"clusters"`
|
||||
}
|
||||
|
||||
var (
|
||||
clusterTargets []Target
|
||||
namespaceSet = set.NewSet()
|
||||
)
|
||||
|
||||
for i := 0; i < len(targets); i++ {
|
||||
clusters := set.NewSet(targets[i].ClusterName)
|
||||
for j := i + 1; j < len(targets); j++ {
|
||||
if targets[i].Namespace == targets[j].Namespace {
|
||||
clusters.Add(targets[j].ClusterName)
|
||||
}
|
||||
}
|
||||
if namespaceSet.Contains(targets[i].Namespace) {
|
||||
continue
|
||||
}
|
||||
clusterTargets = append(clusterTargets, Target{
|
||||
Namespace: targets[i].Namespace,
|
||||
Clusters: clusters.ToSlice(),
|
||||
})
|
||||
namespaceSet.Add(targets[i].Namespace)
|
||||
}
|
||||
|
||||
t := make([]ApplicationDeployTarget, len(clusterTargets))
|
||||
for i, ct := range clusterTargets {
|
||||
t[i] = ApplicationDeployTarget{
|
||||
Namespace: ct.Namespace,
|
||||
Clusters: interfaceToStringSlice(ct.Clusters),
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func interfaceToStringSlice(i []interface{}) []string {
|
||||
var s []string
|
||||
for _, v := range i {
|
||||
s = append(s, v.(string))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func stringToInterfaceSlice(i []string) []interface{} {
|
||||
var s []interface{}
|
||||
for _, v := range i {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// destroySyncConfigsApp will delete the application which is used to sync configs
|
||||
func destroySyncConfigsApp(ctx context.Context, k8sClient client.Client, project string) error {
|
||||
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
|
||||
var app = &v1beta1.Application{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return k8sClient.Delete(ctx, app)
|
||||
}
|
||||
589
pkg/apiserver/rest/usecase/config_test.go
Normal file
589
pkg/apiserver/rest/usecase/config_test.go
Normal file
@@ -0,0 +1,589 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
"gotest.tools/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/definition"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
)
|
||||
|
||||
func TestListConfigTypes(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
def1 := &v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ComponentDefinition",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Labels: map[string]string{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
definitionType: types.TerraformProvider,
|
||||
},
|
||||
},
|
||||
}
|
||||
def2 := &v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ComponentDefinition",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def2",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Annotations: map[string]string{
|
||||
definitionAlias: "Def2",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def1, def2).Build()
|
||||
|
||||
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
|
||||
return k8sClient, nil, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
h := NewConfigUseCase(nil)
|
||||
|
||||
type args struct {
|
||||
h ConfigHandler
|
||||
}
|
||||
|
||||
type want struct {
|
||||
configTypes []*apis.ConfigType
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
h: h,
|
||||
},
|
||||
want: want{
|
||||
configTypes: []*apis.ConfigType{
|
||||
{
|
||||
Name: "def2",
|
||||
Alias: "Def2",
|
||||
Definitions: []string{"def2"},
|
||||
},
|
||||
{
|
||||
Alias: "Terraform Cloud Provider",
|
||||
Name: types.TerraformProvider,
|
||||
Definitions: []string{
|
||||
"def1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.args.h.ListConfigTypes(ctx, "")
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
assert.DeepEqual(t, got, tc.want.configTypes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigType(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
def2 := &v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ComponentDefinition",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def2",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Annotations: map[string]string{
|
||||
definitionAlias: "Def2",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def2).Build()
|
||||
|
||||
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
|
||||
return k8sClient, nil, nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
h := NewConfigUseCase(nil)
|
||||
|
||||
type args struct {
|
||||
h ConfigHandler
|
||||
name string
|
||||
}
|
||||
|
||||
type want struct {
|
||||
configType *apis.ConfigType
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
h: h,
|
||||
name: "def2",
|
||||
},
|
||||
want: want{
|
||||
configType: &apis.ConfigType{
|
||||
Alias: "Def2",
|
||||
Name: "def2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error",
|
||||
args: args{
|
||||
h: h,
|
||||
name: "def99",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "failed to get config type",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.args.h.GetConfigType(ctx, tc.args.name)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
assert.DeepEqual(t, got, tc.want.configType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateConfig(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
|
||||
h := &configUseCaseImpl{kubeClient: k8sClient}
|
||||
|
||||
type args struct {
|
||||
h ConfigHandler
|
||||
req apis.CreateConfigRequest
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "delete config when it's not ready",
|
||||
args: args{
|
||||
h: h,
|
||||
req: apis.CreateConfigRequest{
|
||||
Name: "a",
|
||||
ComponentType: "b",
|
||||
Project: "c",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.args.h.CreateConfig(ctx, tc.args.req)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigs(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
def1 := &v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ComponentDefinition",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Labels: map[string]string{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
definitionType: types.TerraformProvider,
|
||||
},
|
||||
},
|
||||
}
|
||||
def2 := &v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "ComponentDefinition",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def2",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Annotations: map[string]string{
|
||||
definitionAlias: "Def2",
|
||||
},
|
||||
Labels: map[string]string{
|
||||
definition.UserPrefix + "catalog.config.oam.dev": velaCoreConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def1, def2).Build()
|
||||
|
||||
h := &configUseCaseImpl{kubeClient: k8sClient}
|
||||
|
||||
type args struct {
|
||||
configType string
|
||||
h ConfigHandler
|
||||
}
|
||||
|
||||
type want struct {
|
||||
configs []*apis.Config
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
configType: types.TerraformProvider,
|
||||
h: h,
|
||||
},
|
||||
want: want{
|
||||
configs: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.args.h.GetConfigs(ctx, tc.args.configType)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
assert.DeepEqual(t, got, tc.want.configs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTargets(t *testing.T) {
|
||||
currentTargets := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c1", "c2"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
},
|
||||
}
|
||||
targets := []*model.ClusterTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c4",
|
||||
}, {
|
||||
Namespace: "n1",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
{
|
||||
Namespace: "n2",
|
||||
ClusterName: "c3",
|
||||
},
|
||||
}
|
||||
|
||||
expected := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c1", "c2", "c5"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
}, {
|
||||
Namespace: "n3",
|
||||
Clusters: []string{"c4"},
|
||||
},
|
||||
}
|
||||
|
||||
got := mergeTargets(currentTargets, targets)
|
||||
|
||||
for i, g := range got {
|
||||
clusters := g.Clusters
|
||||
sort.SliceStable(clusters, func(i, j int) bool {
|
||||
return clusters[i] < clusters[j]
|
||||
})
|
||||
got[i].Clusters = clusters
|
||||
}
|
||||
assert.DeepEqual(t, expected, got)
|
||||
}
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
targets := []*model.ClusterTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c4",
|
||||
}, {
|
||||
Namespace: "n1",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
{
|
||||
Namespace: "n2",
|
||||
ClusterName: "c3",
|
||||
},
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
}
|
||||
|
||||
expected := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
Clusters: []string{"c4", "c5"},
|
||||
},
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c5"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
},
|
||||
}
|
||||
|
||||
got := convertClusterTargets(targets)
|
||||
|
||||
for i, g := range got {
|
||||
clusters := g.Clusters
|
||||
sort.SliceStable(clusters, func(i, j int) bool {
|
||||
return clusters[i] < clusters[j]
|
||||
})
|
||||
got[i].Clusters = clusters
|
||||
}
|
||||
assert.DeepEqual(t, expected, got)
|
||||
}
|
||||
|
||||
func TestDestroySyncConfigsApp(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
app1 := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config-sync-p1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
}
|
||||
k8sClient1 := fake.NewClientBuilder().WithScheme(s).WithObjects(app1).Build()
|
||||
|
||||
k8sClient2 := fake.NewClientBuilder().Build()
|
||||
|
||||
type args struct {
|
||||
project string
|
||||
k8sClient client.Client
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"found": {
|
||||
args: args{
|
||||
project: "p1",
|
||||
k8sClient: k8sClient1,
|
||||
},
|
||||
},
|
||||
"not found": {
|
||||
args: args{
|
||||
project: "p1",
|
||||
k8sClient: k8sClient2,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "no kind is registered for the type v1beta1.Application",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := destroySyncConfigsApp(ctx, tc.args.k8sClient, tc.args.project)
|
||||
if err != nil || tc.want.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncConfigs(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
secret1 := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Labels: map[string]string{
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigProject: "p1",
|
||||
types.LabelConfigSyncToMultiCluster: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policies := []ApplicationDeployTarget{{
|
||||
Namespace: "n9",
|
||||
Clusters: []string{"c19"},
|
||||
}}
|
||||
properties, _ := json.Marshal(policies)
|
||||
app1 := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config-sync-p2",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{{
|
||||
Name: "c19",
|
||||
Type: "topology",
|
||||
Properties: &runtime.RawExtension{Raw: properties},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(secret1, app1).Build()
|
||||
|
||||
type args struct {
|
||||
project string
|
||||
targets []*model.ClusterTarget
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
args: args{
|
||||
project: "p1",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
args: args{
|
||||
project: "p2",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip config sync",
|
||||
args: args{
|
||||
project: "p3",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := SyncConfigs(ctx, k8sClient, tc.args.project, tc.args.targets)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,57 +20,108 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/helm"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types2 "k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
)
|
||||
|
||||
// NewHelmUsecase return a helmHandler
|
||||
func NewHelmUsecase() HelmHandler {
|
||||
c, err := clients.GetKubeClient()
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("get kube client failure %s", err.Error())
|
||||
}
|
||||
return defaultHelmHandler{
|
||||
helper: helm.NewHelperWithCache(),
|
||||
helper: helm.NewHelperWithCache(),
|
||||
k8sClient: c,
|
||||
}
|
||||
}
|
||||
|
||||
// HelmHandler responsible handle helm related interface
|
||||
type HelmHandler interface {
|
||||
ListChartNames(ctx context.Context, url string, skipCache bool) ([]string, error)
|
||||
ListChartVersions(ctx context.Context, url string, chartName string, skipCache bool) (repo.ChartVersions, error)
|
||||
GetChartValues(ctx context.Context, url string, chartName string, version string, skipCache bool) (map[string]interface{}, error)
|
||||
ListChartNames(ctx context.Context, url string, secretName string, skipCache bool) ([]string, error)
|
||||
ListChartVersions(ctx context.Context, url string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error)
|
||||
GetChartValues(ctx context.Context, url string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error)
|
||||
ListChartRepo(ctx context.Context, projectName string) (*v1.ChartRepoResponseList, error)
|
||||
}
|
||||
|
||||
type defaultHelmHandler struct {
|
||||
helper *helm.Helper
|
||||
helper *helm.Helper
|
||||
k8sClient client.Client
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, skipCache bool) ([]string, error) {
|
||||
charts, err := d.helper.ListChartsFromRepo(url, skipCache)
|
||||
func (d defaultHelmHandler) ListChartNames(ctx context.Context, repoURL string, secretName string, skipCache bool) ([]string, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
charts, err := d.helper.ListChartsFromRepo(repoURL, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", url, err.Error())
|
||||
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", utils.Sanitize(repoURL), err.Error())
|
||||
return nil, bcode.ErrListHelmChart
|
||||
}
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, url string, chartName string, skipCache bool) (repo.ChartVersions, error) {
|
||||
chartVersions, err := d.helper.ListVersions(url, chartName, skipCache)
|
||||
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, repoURL string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
chartVersions, err := d.helper.ListVersions(repoURL, chartName, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", url, chartName, err.Error())
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), err.Error())
|
||||
return nil, bcode.ErrListHelmVersions
|
||||
}
|
||||
if len(chartVersions) == 0 {
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", url, chartName)
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName))
|
||||
return nil, bcode.ErrChartNotExist
|
||||
}
|
||||
return chartVersions, nil
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, chartName string, version string, skipCache bool) (map[string]interface{}, error) {
|
||||
v, err := d.helper.GetValuesFromChart(url, chartName, version, skipCache)
|
||||
func (d defaultHelmHandler) GetChartValues(ctx context.Context, repoURL string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
v, err := d.helper.GetValuesFromChart(repoURL, chartName, version, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", url, chartName, version, err.Error())
|
||||
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), utils.Sanitize(version), err.Error())
|
||||
return nil, bcode.ErrGetChartValues
|
||||
}
|
||||
res := make(map[string]interface{}, len(v))
|
||||
@@ -78,6 +129,50 @@ func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, char
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) ListChartRepo(ctx context.Context, projectName string) (*v1.ChartRepoResponseList, error) {
|
||||
var res []*v1.ChartRepoResponse
|
||||
var err error
|
||||
|
||||
if len(projectName) != 0 {
|
||||
projectSecrets := corev1.SecretList{}
|
||||
opts := []client.ListOption{
|
||||
client.MatchingLabels{oam.LabelConfigType: "config-helm-repository", types.LabelConfigProject: projectName},
|
||||
client.InNamespace(types.DefaultKubeVelaNS),
|
||||
}
|
||||
err = d.k8sClient.List(ctx, &projectSecrets, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range projectSecrets.Items {
|
||||
res = append(res, &v1.ChartRepoResponse{URL: string(item.Data["url"]), SecretName: item.Name})
|
||||
}
|
||||
}
|
||||
|
||||
globalSecrets := corev1.SecretList{}
|
||||
selector := metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{oam.LabelConfigType: "config-helm-repository"},
|
||||
MatchExpressions: []metav1.LabelSelectorRequirement{
|
||||
{Key: types.LabelConfigProject, Operator: metav1.LabelSelectorOpDoesNotExist},
|
||||
},
|
||||
}
|
||||
|
||||
ls, _ := metav1.LabelSelectorAsSelector(&selector)
|
||||
err = d.k8sClient.List(ctx, &globalSecrets, &client.ListOptions{
|
||||
LabelSelector: ls,
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range globalSecrets.Items {
|
||||
res = append(res, &v1.ChartRepoResponse{URL: string(item.Data["url"]), SecretName: item.Name})
|
||||
}
|
||||
|
||||
return &v1.ChartRepoResponseList{ChartRepoResponse: res}, nil
|
||||
}
|
||||
|
||||
// this func will flatten a nested map, the key will flatten with separator "." and the value's type will be keep
|
||||
// src is the map you want to flatten the output will be set in dest map
|
||||
// eg : src is {a:{b:{c:true}}} , the dest is {a.b.c:true}
|
||||
|
||||
@@ -17,9 +17,23 @@ limitations under the License.
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -37,6 +51,147 @@ func TestFlattenKeyFunc(t *testing.T) {
|
||||
assert.Equal(t, dstMap, res)
|
||||
}
|
||||
|
||||
var _ = Describe("Test helm repo list", func() {
|
||||
ctx := context.Background()
|
||||
var pSec, gSec v1.Secret
|
||||
|
||||
BeforeEach(func() {
|
||||
pSec = v1.Secret{}
|
||||
gSec = v1.Secret{}
|
||||
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
|
||||
Expect(yaml.Unmarshal([]byte(projectSecret), &pSec)).Should(BeNil())
|
||||
Expect(yaml.Unmarshal([]byte(globalSecret), &gSec)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, &pSec)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, &gSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(k8sClient.Delete(ctx, &gSec)).Should(BeNil())
|
||||
Expect(k8sClient.Delete(ctx, &pSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test list with project ", func() {
|
||||
u := NewHelmUsecase()
|
||||
list, err := u.ListChartRepo(ctx, "my-project")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(2))
|
||||
found := 0
|
||||
for _, response := range list.ChartRepoResponse {
|
||||
if response.SecretName == "project-helm-repo" {
|
||||
Expect(response.URL).Should(BeEquivalentTo("https://kedacore.github.io/charts"))
|
||||
found++
|
||||
}
|
||||
if response.SecretName == "global-helm-repo" {
|
||||
Expect(response.URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
|
||||
found++
|
||||
}
|
||||
}
|
||||
Expect(found).Should(BeEquivalentTo(2))
|
||||
})
|
||||
|
||||
It("Test list func with not exist project", func() {
|
||||
u := NewHelmUsecase()
|
||||
list, err := u.ListChartRepo(ctx, "not-exist-project")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
|
||||
Expect(list.ChartRepoResponse[0].URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
|
||||
Expect(list.ChartRepoResponse[0].SecretName).Should(BeEquivalentTo("global-helm-repo"))
|
||||
})
|
||||
|
||||
It("Test list func without project", func() {
|
||||
u := NewHelmUsecase()
|
||||
list, err := u.ListChartRepo(ctx, "")
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
|
||||
Expect(list.ChartRepoResponse[0].URL).Should(BeEquivalentTo("https://charts.bitnami.com/bitnami"))
|
||||
Expect(list.ChartRepoResponse[0].SecretName).Should(BeEquivalentTo("global-helm-repo"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("test helm usecasae", func() {
|
||||
ctx := context.Background()
|
||||
var repoSec v1.Secret
|
||||
|
||||
BeforeEach(func() {
|
||||
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
|
||||
|
||||
repoSec = v1.Secret{}
|
||||
Expect(yaml.Unmarshal([]byte(repoSecret), &repoSec)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, &repoSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(k8sClient.Delete(ctx, &repoSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("helm associated usecase interface test", func() {
|
||||
var mockServer *httptest.Server
|
||||
|
||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
u, p, ok := request.BasicAuth()
|
||||
if !ok || u != "admin" || p != "admin" {
|
||||
writer.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case request.URL.Path == "/index.yaml":
|
||||
index, err := ioutil.ReadFile("./testdata/helm/index.yaml")
|
||||
indexFile := string(index)
|
||||
indexFile = strings.ReplaceAll(indexFile, "server-url", mockServer.URL)
|
||||
if err != nil {
|
||||
writer.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write([]byte(indexFile))
|
||||
|
||||
return
|
||||
case strings.Contains(request.URL.Path, "mysql-8.8.23.tgz"):
|
||||
pkg, err := ioutil.ReadFile("./testdata/helm/mysql-8.8.23.tgz")
|
||||
if err != nil {
|
||||
writer.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write(pkg)
|
||||
return
|
||||
default:
|
||||
writer.Write([]byte("404 page not found"))
|
||||
}
|
||||
})
|
||||
|
||||
mockServer = httptest.NewServer(handler)
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
u := NewHelmUsecase()
|
||||
charts, err := u.ListChartNames(ctx, mockServer.URL, "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(charts)).Should(BeEquivalentTo(1))
|
||||
Expect(charts[0]).Should(BeEquivalentTo("mysql"))
|
||||
|
||||
versions, err := u.ListChartVersions(ctx, mockServer.URL, "mysql", "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(versions)).Should(BeEquivalentTo(1))
|
||||
Expect(versions[0].Version).Should(BeEquivalentTo("8.8.23"))
|
||||
|
||||
values, err := u.GetChartValues(ctx, mockServer.URL, "mysql", "8.8.23", "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(values).ShouldNot(BeNil())
|
||||
Expect(len(values)).ShouldNot(BeEquivalentTo(0))
|
||||
})
|
||||
|
||||
It("coverage not secret notExist error", func() {
|
||||
u := NewHelmUsecase()
|
||||
_, err := u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
|
||||
_, err = u.ListChartVersions(ctx, "http://127.0.0.1:8080", "mysql", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
|
||||
_, err = u.GetChartValues(ctx, "http://127.0.0.1:8080", "mysql", "8.8.23", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var (
|
||||
src = `{
|
||||
"OAMSpecVer":"v0.2",
|
||||
@@ -175,4 +330,43 @@ var (
|
||||
"webhookService.port": 11443,
|
||||
"webhookService.type": "ClusterIP"
|
||||
}`
|
||||
globalSecret = `
|
||||
apiVersion: v1
|
||||
stringData:
|
||||
url: https://charts.bitnami.com/bitnami
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
config.oam.dev/type: config-helm-repository
|
||||
name: global-helm-repo
|
||||
namespace: vela-system
|
||||
type: Opaque
|
||||
`
|
||||
projectSecret = `
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: project-helm-repo
|
||||
namespace: vela-system
|
||||
labels:
|
||||
config.oam.dev/type: config-helm-repository
|
||||
config.oam.dev/project: my-project
|
||||
stringData:
|
||||
url: https://kedacore.github.io/charts
|
||||
type: Opaque
|
||||
`
|
||||
repoSecret = `
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: repo-secret
|
||||
namespace: vela-system
|
||||
labels:
|
||||
config.oam.dev/type: config-helm-repository
|
||||
config.oam.dev/project: my-project-2
|
||||
stringData:
|
||||
username: admin
|
||||
password: admin
|
||||
type: Opaque
|
||||
`
|
||||
)
|
||||
|
||||
@@ -20,9 +20,16 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
|
||||
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
@@ -46,6 +53,7 @@ type ProjectUsecase interface {
|
||||
DeleteProjectUser(ctx context.Context, projectName string, userName string) error
|
||||
UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error)
|
||||
Init(ctx context.Context) error
|
||||
GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error)
|
||||
}
|
||||
|
||||
type projectUsecaseImpl struct {
|
||||
@@ -290,7 +298,11 @@ func (p *projectUsecaseImpl) DeleteProject(ctx context.Context, name string) err
|
||||
return err
|
||||
}
|
||||
}
|
||||
return p.ds.Delete(ctx, &model.Project{Name: name})
|
||||
if err := p.ds.Delete(ctx, &model.Project{Name: name}); err != nil {
|
||||
return err
|
||||
}
|
||||
// delete config-sync application
|
||||
return destroySyncConfigsApp(ctx, p.k8sClient, name)
|
||||
}
|
||||
|
||||
// CreateProject create project
|
||||
@@ -466,6 +478,118 @@ func (p *projectUsecaseImpl) UpdateProjectUser(ctx context.Context, projectName
|
||||
return ConvertProjectUserModel2Base(&projectUser), nil
|
||||
}
|
||||
|
||||
func (p *projectUsecaseImpl) GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error) {
|
||||
var (
|
||||
configs []*apisv1.Config
|
||||
legacyTerraformProviders []*apisv1.Config
|
||||
apps = &v1beta1.ApplicationList{}
|
||||
)
|
||||
if err := p.k8sClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{
|
||||
model.LabelSourceOfTruth: model.FromInner,
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if configType == types.TerraformProvider || configType == "" {
|
||||
// legacy providers
|
||||
var providers = &terraformapi.ProviderList{}
|
||||
if err := p.k8sClient.List(ctx, providers, client.InNamespace(types.DefaultAppNamespace)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range providers.Items {
|
||||
if p.Labels[types.LabelConfigCatalog] == velaCoreConfig {
|
||||
continue
|
||||
}
|
||||
t := p.CreationTimestamp.Time
|
||||
var status = configIsNotReady
|
||||
if p.Status.State == terraformtypes.ProviderIsReady {
|
||||
status = configIsReady
|
||||
}
|
||||
legacyTerraformProviders = append(legacyTerraformProviders, &apisv1.Config{
|
||||
Name: p.Name,
|
||||
CreatedTime: &t,
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
switch configType {
|
||||
case types.TerraformProvider:
|
||||
for _, a := range apps.Items {
|
||||
appProject := a.Labels[types.LabelConfigProject]
|
||||
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) ||
|
||||
!strings.Contains(a.Labels[types.LabelConfigType], "terraform-") {
|
||||
continue
|
||||
}
|
||||
configs = append(configs, &apisv1.Config{
|
||||
ConfigType: a.Labels[types.LabelConfigType],
|
||||
Name: a.Name,
|
||||
Project: appProject,
|
||||
CreatedTime: &(a.CreationTimestamp.Time),
|
||||
ApplicationStatus: a.Status.Phase,
|
||||
})
|
||||
}
|
||||
|
||||
configs = append(configs, legacyTerraformProviders...)
|
||||
case "":
|
||||
for _, a := range apps.Items {
|
||||
appProject := a.Labels[types.LabelConfigProject]
|
||||
if appProject != "" && appProject != projectName {
|
||||
continue
|
||||
}
|
||||
configs = append(configs, &apisv1.Config{
|
||||
ConfigType: a.Labels[types.LabelConfigType],
|
||||
Name: a.Name,
|
||||
Project: appProject,
|
||||
CreatedTime: &(a.CreationTimestamp.Time),
|
||||
ApplicationStatus: a.Status.Phase,
|
||||
})
|
||||
}
|
||||
configs = append(configs, legacyTerraformProviders...)
|
||||
case types.DexConnector, types.HelmRepository, types.ImageRegistry:
|
||||
t := strings.ReplaceAll(configType, "config-", "")
|
||||
for _, a := range apps.Items {
|
||||
appProject := a.Labels[types.LabelConfigProject]
|
||||
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) {
|
||||
continue
|
||||
}
|
||||
if a.Labels[types.LabelConfigType] == t {
|
||||
configs = append(configs, &apisv1.Config{
|
||||
ConfigType: a.Labels[types.LabelConfigType],
|
||||
Name: a.Name,
|
||||
Project: appProject,
|
||||
CreatedTime: &(a.CreationTimestamp.Time),
|
||||
ApplicationStatus: a.Status.Phase,
|
||||
})
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported config type")
|
||||
}
|
||||
|
||||
for i, c := range configs {
|
||||
if c.ConfigType != "" {
|
||||
d := &v1beta1.ComponentDefinition{}
|
||||
err := p.k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: c.ConfigType}, d)
|
||||
if err != nil {
|
||||
klog.InfoS("failed to get component definition", "ComponentDefinition", configType, "err", err)
|
||||
} else {
|
||||
configs[i].ConfigTypeAlias = d.Annotations[definitionAlias]
|
||||
}
|
||||
}
|
||||
if c.ApplicationStatus != "" {
|
||||
if c.ApplicationStatus == common.ApplicationRunning {
|
||||
configs[i].Status = configIsReady
|
||||
} else {
|
||||
configs[i].Status = configIsNotReady
|
||||
}
|
||||
}
|
||||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// ConvertProjectModel2Base convert project model to base struct
|
||||
func ConvertProjectModel2Base(project *model.Project, owner *model.User) *apisv1.ProjectBase {
|
||||
base := &apisv1.ProjectBase{
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ var ResourceMaps = map[string]resourceMetadata{
|
||||
pathName: "userName",
|
||||
},
|
||||
"applicationTemplate": {},
|
||||
"configs": {},
|
||||
},
|
||||
pathName: "projectName",
|
||||
},
|
||||
@@ -207,6 +208,14 @@ var ResourceMaps = map[string]resourceMetadata{
|
||||
"permission": {},
|
||||
"systemSetting": {},
|
||||
"definition": {},
|
||||
"configType": {
|
||||
pathName: "configType",
|
||||
subResources: map[string]resourceMetadata{
|
||||
"config": {
|
||||
pathName: "name",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var existResourcePaths = convert(ResourceMaps)
|
||||
|
||||
@@ -18,31 +18,52 @@ package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/rand"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
velatypes "github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/version"
|
||||
)
|
||||
|
||||
// SystemInfoUsecase is usecase for systemInfoCollection
|
||||
type SystemInfoUsecase interface {
|
||||
Get(ctx context.Context) (*model.SystemInfo, error)
|
||||
GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error)
|
||||
UpdateSystemInfo(ctx context.Context, sysInfo v1.SystemInfoRequest) (*v1.SystemInfoResponse, error)
|
||||
Init(ctx context.Context) error
|
||||
}
|
||||
|
||||
type systemInfoUsecaseImpl struct {
|
||||
ds datastore.DataStore
|
||||
ds datastore.DataStore
|
||||
kubeClient client.Client
|
||||
}
|
||||
|
||||
// NewSystemInfoUsecase return a systemInfoCollectionUsecase
|
||||
func NewSystemInfoUsecase(ds datastore.DataStore) SystemInfoUsecase {
|
||||
return &systemInfoUsecaseImpl{ds: ds}
|
||||
kubecli, err := clients.GetKubeClient()
|
||||
if err != nil {
|
||||
log.Logger.Fatalf("failed to get kube client: %s", err.Error())
|
||||
}
|
||||
return &systemInfoUsecaseImpl{ds: ds, kubeClient: kubecli}
|
||||
}
|
||||
|
||||
func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error) {
|
||||
func (u systemInfoUsecaseImpl) Get(ctx context.Context) (*model.SystemInfo, error) {
|
||||
// first get request will init systemInfoCollection{installId: {random}, enableCollection: true}
|
||||
info := &model.SystemInfo{}
|
||||
entities, err := u.ds.List(ctx, info, &datastore.ListOptions{})
|
||||
@@ -54,7 +75,7 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf
|
||||
if info.LoginType == "" {
|
||||
info.LoginType = model.LoginTypeLocal
|
||||
}
|
||||
return &v1.SystemInfoResponse{SystemInfo: *info, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
|
||||
return info, nil
|
||||
}
|
||||
installID := rand.String(16)
|
||||
info.InstallID = installID
|
||||
@@ -64,11 +85,26 @@ func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInf
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.SystemInfoResponse{SystemInfo: *info, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (u systemInfoUsecaseImpl) GetSystemInfo(ctx context.Context) (*v1.SystemInfoResponse, error) {
|
||||
// first get request will init systemInfoCollection{installId: {random}, enableCollection: true}
|
||||
info, err := u.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.SystemInfoResponse{
|
||||
SystemInfo: convertInfoToBase(info),
|
||||
SystemVersion: v1.SystemVersion{
|
||||
VelaVersion: version.VelaVersion,
|
||||
GitVersion: version.GitRevision,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.SystemInfoRequest) (*v1.SystemInfoResponse, error) {
|
||||
info, err := u.GetSystemInfo(ctx)
|
||||
info, err := u.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -78,10 +114,131 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
|
||||
LoginType: sysInfo.LoginType,
|
||||
BaseModel: model.BaseModel{
|
||||
CreateTime: info.CreateTime,
|
||||
}}
|
||||
},
|
||||
}
|
||||
|
||||
if sysInfo.LoginType == model.LoginTypeDex {
|
||||
admin := &model.User{Name: model.DefaultAdminUserName}
|
||||
if err := u.ds.Get(ctx, admin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if admin.Email == "" {
|
||||
return nil, bcode.ErrEmptyAdminEmail
|
||||
}
|
||||
if err := generateDexConfig(ctx, u.kubeClient, sysInfo.VelaAddress, &modifiedInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = u.ds.Put(ctx, &modifiedInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.SystemInfoResponse{SystemInfo: modifiedInfo, SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision}}, nil
|
||||
return &v1.SystemInfoResponse{
|
||||
SystemInfo: v1.SystemInfo{
|
||||
InstallID: modifiedInfo.InstallID,
|
||||
EnableCollection: modifiedInfo.EnableCollection,
|
||||
LoginType: modifiedInfo.LoginType,
|
||||
},
|
||||
SystemVersion: v1.SystemVersion{VelaVersion: version.VelaVersion, GitVersion: version.GitRevision},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u systemInfoUsecaseImpl) Init(ctx context.Context) error {
|
||||
info, err := u.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedKey = info.InstallID
|
||||
_, err = initDexConfig(ctx, u.kubeClient, "http://velaux.com", &model.SystemInfo{})
|
||||
return err
|
||||
}
|
||||
|
||||
func convertInfoToBase(info *model.SystemInfo) v1.SystemInfo {
|
||||
return v1.SystemInfo{
|
||||
InstallID: info.InstallID,
|
||||
EnableCollection: info.EnableCollection,
|
||||
LoginType: info.LoginType,
|
||||
}
|
||||
}
|
||||
|
||||
func generateDexConfig(ctx context.Context, kubeClient client.Client, velaAddress string, info *model.SystemInfo) error {
|
||||
secret, err := initDexConfig(ctx, kubeClient, velaAddress, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connectors, err := utils.GetDexConnectors(ctx, kubeClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(connectors) < 1 {
|
||||
return bcode.ErrNoDexConnector
|
||||
}
|
||||
config, err := model.NewJSONStructByStruct(info.DexConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
(*config)["connectors"] = connectors
|
||||
c, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(secret.Data[secretDexConfigKey], c) {
|
||||
secret.Data[secretDexConfigKey] = c
|
||||
if err := kubeClient.Update(ctx, secret); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restartDex(ctx, kubeClient); err != nil && !errors.Is(err, bcode.ErrDexNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func initDexConfig(ctx context.Context, kubeClient client.Client, velaAddress string, info *model.SystemInfo) (*corev1.Secret, error) {
|
||||
dexConfig := model.DexConfig{
|
||||
Issuer: fmt.Sprintf("%s/dex", velaAddress),
|
||||
Web: model.DexWeb{
|
||||
HTTP: "0.0.0.0:5556",
|
||||
},
|
||||
Storage: model.DexStorage{
|
||||
Type: "memory",
|
||||
},
|
||||
StaticClients: []model.DexStaticClient{
|
||||
{
|
||||
ID: "velaux",
|
||||
Name: "Vela UX",
|
||||
Secret: "velaux-secret",
|
||||
RedirectURIs: []string{fmt.Sprintf("%s/callback", velaAddress)},
|
||||
},
|
||||
},
|
||||
EnablePasswordDB: true,
|
||||
}
|
||||
info.DexConfig = dexConfig
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
if err := kubeClient.Get(ctx, types.NamespacedName{
|
||||
Name: dexConfigName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
}, secret); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
config, err := yaml.Marshal(info.DexConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := kubeClient.Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: dexConfigName,
|
||||
Namespace: velatypes.DefaultKubeVelaNS,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
Data: map[string][]byte{
|
||||
secretDexConfigKey: config,
|
||||
},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
26
pkg/apiserver/rest/usecase/testdata/dex-config-def.yaml
vendored
Normal file
26
pkg/apiserver/rest/usecase/testdata/dex-config-def.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Dex config allow users to specify dex config in properties
|
||||
labels:
|
||||
custom.definition.oam.dev/ui-hidden: "true"
|
||||
name: dex-config
|
||||
namespace: vela-system
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import "encoding/yaml"
|
||||
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: name: context.name
|
||||
namespace: context.namespace
|
||||
type: "Opaque"
|
||||
stringData: "config.yaml": yaml.Marshal(parameter)
|
||||
}
|
||||
parameter: {}
|
||||
workload:
|
||||
type: autodetects.core.oam.dev
|
||||
36
pkg/apiserver/rest/usecase/testdata/helm/index.yaml
vendored
Normal file
36
pkg/apiserver/rest/usecase/testdata/helm/index.yaml
vendored
Normal 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: {}
|
||||
BIN
pkg/apiserver/rest/usecase/testdata/helm/mysql-8.8.23.tgz
vendored
Normal file
BIN
pkg/apiserver/rest/usecase/testdata/helm/mysql-8.8.23.tgz
vendored
Normal file
Binary file not shown.
@@ -19,6 +19,8 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"helm.sh/helm/v3/pkg/time"
|
||||
@@ -82,7 +84,15 @@ func (u *userUsecaseImpl) Init(ctx context.Context) error {
|
||||
Name: admin,
|
||||
}); err != nil {
|
||||
if errors.Is(err, datastore.ErrRecordNotExist) {
|
||||
pwd := utils2.RandomString(8)
|
||||
pwd := func() string {
|
||||
p := utils2.RandomString(8)
|
||||
p += strconv.Itoa(rand.Intn(9)) // #nosec
|
||||
r := append([]rune(p), 'a'+rune(rand.Intn(26))) // #nosec
|
||||
rand.Shuffle(len(r), func(i, j int) { r[i], r[j] = r[j], r[i] })
|
||||
p = string(r)
|
||||
return p
|
||||
}()
|
||||
|
||||
encrypted, err := GeneratePasswordHash(pwd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -96,7 +106,7 @@ func (u *userUsecaseImpl) Init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
// print default password of admin user in log
|
||||
log.Logger.Infof("init admin user, password is %s", pwd)
|
||||
log.Logger.Infof("initialized admin username and password: admin / %s\n", pwd)
|
||||
secret := &corev1.Secret{}
|
||||
if err := u.k8sClient.Get(ctx, k8stypes.NamespacedName{
|
||||
Name: admin,
|
||||
@@ -184,7 +194,7 @@ func (u *userUsecaseImpl) DeleteUser(ctx context.Context, username string) error
|
||||
}
|
||||
}
|
||||
if err := u.ds.Delete(ctx, &model.User{Name: username}); err != nil {
|
||||
log.Logger.Errorf("failed to delete user", username, err.Error())
|
||||
log.Logger.Errorf("failed to delete user %s %v", utils2.Sanitize(username), err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -192,7 +202,7 @@ func (u *userUsecaseImpl) DeleteUser(ctx context.Context, username string) error
|
||||
|
||||
// CreateUser create user
|
||||
func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserRequest) (*apisv1.UserBase, error) {
|
||||
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
|
||||
sysInfo, err := u.sysUsecase.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -220,7 +230,7 @@ func (u *userUsecaseImpl) CreateUser(ctx context.Context, req apisv1.CreateUserR
|
||||
|
||||
// UpdateUser update user
|
||||
func (u *userUsecaseImpl) UpdateUser(ctx context.Context, user *model.User, req apisv1.UpdateUserRequest) (*apisv1.UserBase, error) {
|
||||
sysInfo, err := u.sysUsecase.GetSystemInfo(ctx)
|
||||
sysInfo, err := u.sysUsecase.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -25,11 +25,14 @@ import (
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
)
|
||||
|
||||
@@ -74,7 +77,12 @@ var _ = Describe("Test application usecase function", func() {
|
||||
})
|
||||
|
||||
It("Test HandleApplicationWebhook function", func() {
|
||||
_, err := projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"})
|
||||
var ns = corev1.Namespace{}
|
||||
ns.Name = types.DefaultKubeVelaNS
|
||||
err := k8sClient.Create(context.TODO(), &ns)
|
||||
Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
|
||||
|
||||
_, err = projectUsecase.CreateProject(context.TODO(), apisv1.CreateProjectRequest{Name: "project-webhook"})
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
_, err = targetUsecase.CreateTarget(context.TODO(), apisv1.CreateTargetRequest{Name: "dev-target-webhook", Project: "project-webhook"})
|
||||
|
||||
@@ -35,4 +35,8 @@ var (
|
||||
ErrInvalidLoginRequest = NewBcode(400, 12008, "the login request is invalid")
|
||||
// ErrInvalidDexConfig is the error of invalid dex config
|
||||
ErrInvalidDexConfig = NewBcode(400, 12009, "the dex config is invalid")
|
||||
// ErrRefreshTokenExpired is the error of refresh token expired
|
||||
ErrRefreshTokenExpired = NewBcode(400, 12010, "the refresh token is expired")
|
||||
// ErrNoDexConnector is the error of no dex connector
|
||||
ErrNoDexConnector = NewBcode(400, 12011, "there is no dex connector")
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -33,4 +33,8 @@ var (
|
||||
ErrUserInconsistentPassword = NewBcode(401, 14007, "the password is inconsistent with the user")
|
||||
// ErrUsernameNotExist is the error of username not exist
|
||||
ErrUsernameNotExist = NewBcode(401, 14008, "the username is not exist")
|
||||
// ErrDexNotFound is the error of dex not found
|
||||
ErrDexNotFound = NewBcode(200, 14009, "the dex is not found")
|
||||
// ErrEmptyAdminEmail is the error of empty admin email
|
||||
ErrEmptyAdminEmail = NewBcode(400, 14010, "the admin email is empty, please set the admin email before using sso login")
|
||||
)
|
||||
|
||||
52
pkg/apiserver/rest/utils/dex.go
Normal file
52
pkg/apiserver/rest/utils/dex.go
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
)
|
||||
|
||||
// GetDexConnectors returns the dex connectors for Dex connector controller
|
||||
func GetDexConnectors(ctx context.Context, k8sClient client.Client) ([]map[string]interface{}, error) {
|
||||
secrets := &v1.SecretList{}
|
||||
if err := k8sClient.List(ctx, secrets, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{types.LabelConfigType: types.DexConnector}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connectors := make([]map[string]interface{}, len(secrets.Items))
|
||||
for i, s := range secrets.Items {
|
||||
var data map[string]interface{}
|
||||
key := s.Labels[types.LabelConfigSubType]
|
||||
err := json.Unmarshal(s.Data[key], &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
connectors[i] = map[string]interface{}{
|
||||
"type": s.Labels[types.LabelConfigSubType],
|
||||
"id": s.Name,
|
||||
"name": s.Name,
|
||||
"config": data,
|
||||
}
|
||||
}
|
||||
|
||||
return connectors, nil
|
||||
}
|
||||
106
pkg/apiserver/rest/utils/dex_test.go
Normal file
106
pkg/apiserver/rest/utils/dex_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestGetDexConnectors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
k8sClient client.Client
|
||||
}
|
||||
type want struct {
|
||||
connectors []map[string]interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
ldap := map[string]interface{}{
|
||||
"clientID": "clientID",
|
||||
"clientSecret": "clientSecret",
|
||||
"callbackURL": "redirectURL",
|
||||
"xxx": map[string]interface{}{"aaa": "bbb", "ccc": "ddd"},
|
||||
}
|
||||
data, err := json.Marshal(ldap)
|
||||
assert.NoError(t, err)
|
||||
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "a",
|
||||
Namespace: "vela-system",
|
||||
Labels: map[string]string{
|
||||
"app.oam.dev/source-of-truth": "from-inner-system",
|
||||
"config.oam.dev/catalog": "velacore-config",
|
||||
"config.oam.dev/type": "config-dex-connector",
|
||||
"config.oam.dev/sub-type": "ldap",
|
||||
"project": "abc",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"ldap": data,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithObjects(secret).Build()
|
||||
|
||||
testcaes := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
|
||||
"test": {args: args{
|
||||
k8sClient: k8sClient,
|
||||
},
|
||||
want: want{
|
||||
connectors: []map[string]interface{}{{
|
||||
"id": "a",
|
||||
"name": "a",
|
||||
"type": "ldap",
|
||||
"config": map[string]interface{}{
|
||||
"clientID": "clientID",
|
||||
"clientSecret": "clientSecret",
|
||||
"callbackURL": "redirectURL",
|
||||
"xxx": map[string]interface{}{"aaa": "bbb", "ccc": "ddd"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testcaes {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := GetDexConnectors(ctx, tc.args.k8sClient)
|
||||
if err != tc.want.err {
|
||||
t.Errorf("%s: GetDexConnectors() error = %v, wantErr %v", name, err, tc.want.err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tc.want.connectors) {
|
||||
t.Errorf("%s: GetDexConnectors() = %v, want %v", name, got, tc.want.connectors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
198
pkg/apiserver/rest/webservice/config.go
Normal file
198
pkg/apiserver/rest/webservice/config.go
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package webservice
|
||||
|
||||
import (
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
)
|
||||
|
||||
// ConfigWebService returns config web service
|
||||
func ConfigWebService(u usecase.ConfigHandler, rbacUseCase usecase.RBACUsecase) WebService {
|
||||
return &configWebService{
|
||||
handler: u,
|
||||
rbacUseCase: rbacUseCase,
|
||||
}
|
||||
}
|
||||
|
||||
type configWebService struct {
|
||||
handler usecase.ConfigHandler
|
||||
rbacUseCase usecase.RBACUsecase
|
||||
}
|
||||
|
||||
func (s *configWebService) GetWebService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path(versionPrefix+"/config_types").
|
||||
Consumes(restful.MIME_XML, restful.MIME_JSON).
|
||||
Produces(restful.MIME_JSON, restful.MIME_XML).
|
||||
Doc("api for configuration management")
|
||||
|
||||
tags := []string{"config"}
|
||||
|
||||
ws.Route(ws.GET("/").To(s.listConfigTypes).
|
||||
Doc("list all config types").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("configType", "list")).
|
||||
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
|
||||
Returns(200, "OK", []apis.ConfigType{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes([]apis.ConfigType{}))
|
||||
|
||||
ws.Route(ws.GET("/{configType}").To(s.getConfigType).
|
||||
Doc("get a config type").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("configType", "get")).
|
||||
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
|
||||
Returns(200, "OK", apis.ConfigType{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.ConfigType{}))
|
||||
|
||||
ws.Route(ws.POST("/{configType}").To(s.createConfig).
|
||||
Doc("create or update a config").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("configType", "create")).
|
||||
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
|
||||
Reads(apis.CreateConfigRequest{}).
|
||||
Returns(200, "OK", apis.EmptyResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Returns(404, "Not Found", bcode.Bcode{}).
|
||||
Writes(apis.EmptyResponse{}))
|
||||
|
||||
ws.Route(ws.GET("/{configType}/configs").To(s.getConfigs).
|
||||
Doc("get configs from a config type").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("config", "list")).
|
||||
Param(ws.PathParameter("configType", "identifier of the config").DataType("string")).
|
||||
Returns(200, "OK", []*apis.Config{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.ConfigType{}))
|
||||
|
||||
ws.Route(ws.GET("/{configType}/configs/{name}").To(s.getConfig).
|
||||
Doc("get a config from a config type").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("config", "get")).
|
||||
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
|
||||
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
|
||||
Returns(200, "OK", []*apis.Config{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.ConfigType{}))
|
||||
|
||||
ws.Route(ws.DELETE("/{configType}/configs/{name}").To(s.deleteConfig).
|
||||
Doc("delete a config").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(s.rbacUseCase.CheckPerm("config", "delete")).
|
||||
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
|
||||
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
|
||||
Returns(200, "OK", apis.EmptyResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Returns(404, "Not Found", bcode.Bcode{}).
|
||||
Writes(apis.EmptyResponse{}))
|
||||
|
||||
ws.Filter(authCheckFilter)
|
||||
return ws
|
||||
}
|
||||
|
||||
func (s *configWebService) listConfigTypes(req *restful.Request, res *restful.Response) {
|
||||
types, err := s.handler.ListConfigTypes(req.Request.Context(), req.QueryParameter("query"))
|
||||
if len(types) == 0 && err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
err = res.WriteEntity(types)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *configWebService) getConfigType(req *restful.Request, res *restful.Response) {
|
||||
t, err := s.handler.GetConfigType(req.Request.Context(), req.PathParameter("configType"))
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
err = res.WriteEntity(t)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *configWebService) createConfig(req *restful.Request, res *restful.Response) {
|
||||
// Verify the validity of parameters
|
||||
var createReq apis.CreateConfigRequest
|
||||
if err := req.ReadEntity(&createReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&createReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
|
||||
err := s.handler.CreateConfig(req.Request.Context(), createReq)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *configWebService) getConfigs(req *restful.Request, res *restful.Response) {
|
||||
configs, err := s.handler.GetConfigs(req.Request.Context(), req.PathParameter("configType"))
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
err = res.WriteEntity(configs)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *configWebService) getConfig(req *restful.Request, res *restful.Response) {
|
||||
t, err := s.handler.GetConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
err = res.WriteEntity(t)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *configWebService) deleteConfig(req *restful.Request, res *restful.Response) {
|
||||
err := s.handler.DeleteConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -46,11 +46,21 @@ func (h helmWebService) GetWebService() *restful.WebService {
|
||||
|
||||
tags := []string{"repository", "helm"}
|
||||
|
||||
// List charts
|
||||
ws.Route(ws.GET("/chart_repos").To(h.listRepo).
|
||||
Doc("list chart repo").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.QueryParameter("project", "the config project").DataType("string")).
|
||||
Returns(200, "OK", []string{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes([]string{}))
|
||||
|
||||
// List charts
|
||||
ws.Route(ws.GET("/charts").To(h.listCharts).
|
||||
Doc("list charts").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
|
||||
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
|
||||
Returns(200, "OK", []string{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes([]string{}))
|
||||
@@ -60,6 +70,7 @@ func (h helmWebService) GetWebService() *restful.WebService {
|
||||
Doc("list versions").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
|
||||
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
|
||||
Returns(200, "OK", v1.ChartVersionListResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes([]string{}))
|
||||
@@ -69,6 +80,7 @@ func (h helmWebService) GetWebService() *restful.WebService {
|
||||
Doc("get chart value").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Param(ws.QueryParameter("repoUrl", "helm repository url").DataType("string")).
|
||||
Param(ws.QueryParameter("secretName", "secret of the repo").DataType("string")).
|
||||
Returns(200, "OK", map[string]interface{}{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes([]string{}))
|
||||
@@ -79,12 +91,13 @@ func (h helmWebService) GetWebService() *restful.WebService {
|
||||
|
||||
func (h helmWebService) listCharts(req *restful.Request, res *restful.Response) {
|
||||
url := req.QueryParameter("repoUrl")
|
||||
secName := req.QueryParameter("secretName")
|
||||
skipCache, err := isSkipCache(req)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, bcode.ErrSkipCacheParameter)
|
||||
return
|
||||
}
|
||||
charts, err := h.usecase.ListChartNames(context.Background(), url, skipCache)
|
||||
charts, err := h.usecase.ListChartNames(context.Background(), url, secName, skipCache)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
@@ -99,13 +112,14 @@ func (h helmWebService) listCharts(req *restful.Request, res *restful.Response)
|
||||
func (h helmWebService) listVersions(req *restful.Request, res *restful.Response) {
|
||||
url := req.QueryParameter("repoUrl")
|
||||
chartName := req.PathParameter("chart")
|
||||
secName := req.QueryParameter("secretName")
|
||||
skipCache, err := isSkipCache(req)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, bcode.ErrSkipCacheParameter)
|
||||
return
|
||||
}
|
||||
|
||||
versions, err := h.usecase.ListChartVersions(context.Background(), url, chartName, skipCache)
|
||||
versions, err := h.usecase.ListChartVersions(context.Background(), url, chartName, secName, skipCache)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
@@ -119,6 +133,7 @@ func (h helmWebService) listVersions(req *restful.Request, res *restful.Response
|
||||
|
||||
func (h helmWebService) chartValues(req *restful.Request, res *restful.Response) {
|
||||
url := req.QueryParameter("repoUrl")
|
||||
secName := req.QueryParameter("secretName")
|
||||
chartName := req.PathParameter("chart")
|
||||
version := req.PathParameter("version")
|
||||
skipCache, err := isSkipCache(req)
|
||||
@@ -127,7 +142,7 @@ func (h helmWebService) chartValues(req *restful.Request, res *restful.Response)
|
||||
return
|
||||
}
|
||||
|
||||
versions, err := h.usecase.GetChartValues(context.Background(), url, chartName, version, skipCache)
|
||||
versions, err := h.usecase.GetChartValues(context.Background(), url, chartName, version, secName, skipCache)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
@@ -139,6 +154,20 @@ func (h helmWebService) chartValues(req *restful.Request, res *restful.Response)
|
||||
}
|
||||
}
|
||||
|
||||
func (h helmWebService) listRepo(req *restful.Request, res *restful.Response) {
|
||||
project := req.QueryParameter("project")
|
||||
repos, err := h.usecase.ListChartRepo(context.Background(), project)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
err = res.WriteEntity(repos)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func isSkipCache(req *restful.Request) (bool, error) {
|
||||
skipStr := req.QueryParameter("skipCache")
|
||||
skipCache := false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,15 +73,16 @@ func Init(ctx context.Context, ds datastore.DataStore, addonCacheTime time.Durat
|
||||
definitionUsecase := usecase.NewDefinitionUsecase()
|
||||
addonUsecase := usecase.NewAddonUsecase(addonCacheTime)
|
||||
envBindingUsecase := usecase.NewEnvBindingUsecase(ds, workflowUsecase, definitionUsecase, envUsecase)
|
||||
applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, envUsecase, targetUsecase, definitionUsecase, projectUsecase)
|
||||
webhookUsecase := usecase.NewWebhookUsecase(ds, applicationUsecase)
|
||||
systemInfoUsecase := usecase.NewSystemInfoUsecase(ds)
|
||||
helmUsecase := usecase.NewHelmUsecase()
|
||||
userUsecase := usecase.NewUserUsecase(ds, projectUsecase, systemInfoUsecase, rbacUsecase)
|
||||
authenticationUsecase := usecase.NewAuthenticationUsecase(ds, systemInfoUsecase, userUsecase)
|
||||
configUseCase := usecase.NewConfigUseCase(authenticationUsecase)
|
||||
applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, envUsecase, targetUsecase, definitionUsecase, projectUsecase)
|
||||
webhookUsecase := usecase.NewWebhookUsecase(ds, applicationUsecase)
|
||||
// Modules that require default data initialization, Call it here in order
|
||||
if initDatabase {
|
||||
initData(ctx, userUsecase, rbacUsecase, projectUsecase, targetUsecase)
|
||||
initData(ctx, userUsecase, rbacUsecase, projectUsecase, targetUsecase, systemInfoUsecase)
|
||||
}
|
||||
|
||||
// Application
|
||||
@@ -95,6 +96,9 @@ func Init(ctx context.Context, ds datastore.DataStore, addonCacheTime time.Durat
|
||||
RegisterWebService(NewEnabledAddonWebService(addonUsecase, rbacUsecase))
|
||||
RegisterWebService(NewAddonRegistryWebService(addonUsecase, rbacUsecase))
|
||||
|
||||
// Config management
|
||||
RegisterWebService(ConfigWebService(configUseCase, rbacUsecase))
|
||||
|
||||
// Resources
|
||||
RegisterWebService(NewClusterWebService(clusterUsecase, rbacUsecase))
|
||||
RegisterWebService(NewOAMApplication(oamApplicationUsecase, rbacUsecase))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
116
pkg/cmd/builder.go
Normal 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
72
pkg/cmd/completion.go
Normal 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
46
pkg/cmd/factory.go
Normal 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
27
pkg/cmd/types.go
Normal 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
52
pkg/cmd/utils.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
51
pkg/controller/utils/actions.go
Normal file
51
pkg/controller/utils/actions.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,12 @@ const (
|
||||
// LabelRuntimeNamespaceUsage mark the usage of the namespace in runtime cluster.
|
||||
// A control plane cluster can also be used as runtime cluster
|
||||
LabelRuntimeNamespaceUsage = "usage.oam.dev/runtime"
|
||||
|
||||
// LabelConfigType means the config type
|
||||
LabelConfigType = "config.oam.dev/type"
|
||||
|
||||
// LabelProject recorde the project the resource belong to
|
||||
LabelProject = "core.oam.dev/project"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -114,6 +120,9 @@ const (
|
||||
// resource for use in a three way diff during a patching apply
|
||||
AnnotationLastAppliedConfig = "app.oam.dev/last-applied-configuration"
|
||||
|
||||
// AnnotationLastAppliedTime indicates the last applied time
|
||||
AnnotationLastAppliedTime = "app.oam.dev/last-applied-time"
|
||||
|
||||
// AnnotationAppRollout indicates that the application is still rolling out
|
||||
// the application controller should treat it differently
|
||||
AnnotationAppRollout = "app.oam.dev/rollout-template"
|
||||
|
||||
@@ -17,10 +17,17 @@ limitations under the License.
|
||||
package policy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// GetClusterLabelSelectorInTopology get cluster label selector in topology policy spec
|
||||
@@ -33,3 +40,57 @@ func GetClusterLabelSelectorInTopology(topology *v1alpha1.TopologyPolicySpec) ma
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlacementsFromTopologyPolicies get placements from topology policies with provided client
|
||||
func GetPlacementsFromTopologyPolicies(ctx context.Context, cli client.Client, app *v1beta1.Application, policies []v1beta1.AppPolicy, allowCrossNamespace bool) ([]v1alpha1.PlacementDecision, error) {
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
placementMap := map[string]struct{}{}
|
||||
addCluster := func(cluster string, ns string, validateCluster bool) error {
|
||||
if validateCluster {
|
||||
if _, e := multicluster.GetVirtualCluster(ctx, cli, cluster); e != nil {
|
||||
return errors.Wrapf(e, "failed to get cluster %s", cluster)
|
||||
}
|
||||
}
|
||||
if !allowCrossNamespace && (ns != app.GetNamespace() && ns != "") {
|
||||
return errors.Errorf("cannot cross namespace")
|
||||
}
|
||||
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
|
||||
name := placement.String()
|
||||
if _, found := placementMap[name]; !found {
|
||||
placementMap[name] = struct{}{}
|
||||
placements = append(placements, placement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, policy := range policies {
|
||||
if policy.Type == v1alpha1.TopologyPolicyType {
|
||||
topologySpec := &v1alpha1.TopologyPolicySpec{}
|
||||
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
|
||||
}
|
||||
clusterLabelSelector := GetClusterLabelSelectorInTopology(topologySpec)
|
||||
switch {
|
||||
case topologySpec.Clusters != nil:
|
||||
for _, cluster := range topologySpec.Clusters {
|
||||
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case clusterLabelSelector != nil:
|
||||
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), cli, clusterLabelSelector)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
|
||||
}
|
||||
if len(clusters) == 0 {
|
||||
return nil, errors.New("failed to find any cluster matches given labels")
|
||||
}
|
||||
for _, cluster := range clusters {
|
||||
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return placements, nil
|
||||
}
|
||||
|
||||
412
pkg/resourcetracker/tree.go
Normal file
412
pkg/resourcetracker/tree.go
Normal 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
|
||||
}
|
||||
48
pkg/resourcetracker/tree_test.go
Normal file
48
pkg/resourcetracker/tree_test.go
Normal 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))
|
||||
}
|
||||
@@ -41,6 +41,10 @@
|
||||
uid?: string
|
||||
apiVersion?: string
|
||||
resourceVersion?: string
|
||||
publishVersion?: string
|
||||
deployVersion?: string
|
||||
revision?: string
|
||||
latest?: bool
|
||||
}]
|
||||
...
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -102,6 +102,12 @@ func init() {
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
// HTTPOption define the https options
|
||||
type HTTPOption struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// InitBaseRestConfig will return reset config for create controller runtime client
|
||||
func InitBaseRestConfig() (Args, error) {
|
||||
args := Args{
|
||||
@@ -134,13 +140,16 @@ func GetClient() (client.Client, error) {
|
||||
return nil, errors.New("client not set, call SetGlobalClient first")
|
||||
}
|
||||
|
||||
// HTTPGet will send GET http request with context
|
||||
func HTTPGet(ctx context.Context, url string) ([]byte, error) {
|
||||
// HTTPGetWithOption use HTTP option and default client to send get request
|
||||
func HTTPGetWithOption(ctx context.Context, url string, opts *HTTPOption) ([]byte, error) {
|
||||
// Change NewRequest to NewRequestWithContext and pass context it
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if opts != nil && len(opts.Username) != 0 && len(opts.Password) != 0 {
|
||||
req.SetBasicAuth(opts.Username, opts.Password)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -279,13 +288,13 @@ func RealtimePrintCommandOutput(cmd *exec.Cmd, logFile string) error {
|
||||
|
||||
// ClusterObject2Map convert ClusterObjectReference to a readable map
|
||||
func ClusterObject2Map(refs []common.ClusterObjectReference) map[string]string {
|
||||
clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Component: %s | Kind: %s"
|
||||
clusterResourceRefTmpl := "Cluster: %s | Namespace: %s | Kind: %s | Name: %s"
|
||||
objs := make(map[string]string, len(refs))
|
||||
for _, r := range refs {
|
||||
if r.Cluster == "" {
|
||||
r.Cluster = "local"
|
||||
}
|
||||
objs[r.Cluster+"/"+r.Namespace+"/"+r.Name+"/"+r.Kind] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Name, r.Kind)
|
||||
objs[r.Cluster+"/"+r.Namespace+"/"+r.Name+"/"+r.Kind] = fmt.Sprintf(clusterResourceRefTmpl, r.Cluster, r.Namespace, r.Kind, r.Name)
|
||||
}
|
||||
return objs
|
||||
}
|
||||
@@ -312,6 +321,19 @@ func clusterObjectReferenceTypeFilterGenerator(allowedKinds ...string) clusterOb
|
||||
var isWorkloadClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment", "StatefulSet", "CloneSet", "Job", "Configuration")
|
||||
var isPortForwardEndpointClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment",
|
||||
"StatefulSet", "CloneSet", "Job", "Service", "HelmRelease")
|
||||
var resourceNameClusterObjectReferenceFilter = func(resourceName []string) clusterObjectReferenceFilter {
|
||||
return func(reference common.ClusterObjectReference) bool {
|
||||
if len(resourceName) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, r := range resourceName {
|
||||
if r == reference.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func filterResource(inputs []common.ClusterObjectReference, filters ...clusterObjectReferenceFilter) (outputs []common.ClusterObjectReference) {
|
||||
for _, item := range inputs {
|
||||
@@ -416,16 +438,32 @@ func filterClusterObjectRefFromAddonObservability(resources []common.ClusterObje
|
||||
return resources
|
||||
}
|
||||
|
||||
func removeEmptyString(items []string) []string {
|
||||
r := []string{}
|
||||
for _, i := range items {
|
||||
if i != "" {
|
||||
r = append(r, i)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// AskToChooseOneEnvResource will ask users to select one applied resource of the application if more than one
|
||||
// resource is a map for component to applied resources
|
||||
// return the selected ClusterObjectReference
|
||||
func AskToChooseOneEnvResource(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
|
||||
return askToChooseOneResource(app, isWorkloadClusterObjectReferenceFilter)
|
||||
func AskToChooseOneEnvResource(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
|
||||
filters := []clusterObjectReferenceFilter{isWorkloadClusterObjectReferenceFilter}
|
||||
_resourceName := removeEmptyString(resourceName)
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
|
||||
return askToChooseOneResource(app, filters...)
|
||||
}
|
||||
|
||||
// AskToChooseOnePortForwardEndpoint will ask user to select one applied resource as port forward endpoint
|
||||
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application) (*common.ClusterObjectReference, error) {
|
||||
return askToChooseOneResource(app, isPortForwardEndpointClusterObjectReferenceFilter)
|
||||
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
|
||||
filters := []clusterObjectReferenceFilter{isPortForwardEndpointClusterObjectReferenceFilter}
|
||||
_resourceName := removeEmptyString(resourceName)
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
|
||||
return askToChooseOneResource(app, filters...)
|
||||
}
|
||||
|
||||
func askToChooseOneInApplication(category string, options []string) (decision string, err error) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
`
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -98,25 +98,32 @@ func (c *AppCollector) ListApplicationResources(app *v1beta1.Application) ([]typ
|
||||
}
|
||||
|
||||
var managedResources []types.AppliedResource
|
||||
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
|
||||
for _, rt := range append(historyRTs, rootRT, currentRT) {
|
||||
if rt != nil {
|
||||
for i, managedResource := range rt.Spec.ManagedResources {
|
||||
for _, managedResource := range rt.Spec.ManagedResources {
|
||||
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
|
||||
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) {
|
||||
if _, ok := existResources[rt.Spec.ManagedResources[i].ClusterObjectReference]; !ok {
|
||||
managedResources = append(managedResources, types.AppliedResource{
|
||||
Cluster: managedResource.Cluster,
|
||||
Kind: managedResource.Kind,
|
||||
Component: managedResource.Component,
|
||||
Trait: managedResource.Trait,
|
||||
Name: managedResource.Name,
|
||||
Namespace: managedResource.Namespace,
|
||||
APIVersion: managedResource.APIVersion,
|
||||
ResourceVersion: managedResource.ResourceVersion,
|
||||
UID: managedResource.UID,
|
||||
})
|
||||
}
|
||||
managedResources = append(managedResources, types.AppliedResource{
|
||||
Cluster: managedResource.Cluster,
|
||||
Kind: managedResource.Kind,
|
||||
Component: managedResource.Component,
|
||||
Trait: managedResource.Trait,
|
||||
Name: managedResource.Name,
|
||||
Namespace: managedResource.Namespace,
|
||||
APIVersion: managedResource.APIVersion,
|
||||
ResourceVersion: managedResource.ResourceVersion,
|
||||
UID: managedResource.UID,
|
||||
PublishVersion: oam.GetPublishVersion(rt),
|
||||
DeployVersion: func() string {
|
||||
obj, _ := managedResource.ToUnstructuredWithData()
|
||||
if obj != nil {
|
||||
return oam.GetDeployVersion(obj)
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
Revision: rt.GetLabels()[oam.LabelAppRevision],
|
||||
Latest: currentRT != nil && rt.Name == currentRT.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,35 +134,35 @@ func (c *AppCollector) ListApplicationResources(app *v1beta1.Application) ([]typ
|
||||
// FindResourceFromResourceTrackerSpec find resources from ResourceTracker spec
|
||||
func (c *AppCollector) FindResourceFromResourceTrackerSpec(app *v1beta1.Application) ([]Resource, error) {
|
||||
ctx := context.Background()
|
||||
managedResources, err := c.ListApplicationResources(app)
|
||||
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, c.k8sClient, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resources := make([]Resource, 0, len(managedResources))
|
||||
for _, objRef := range managedResources {
|
||||
obj := new(unstructured.Unstructured)
|
||||
obj.SetGroupVersionKind(objRef.GroupVersionKind())
|
||||
obj.SetNamespace(objRef.Namespace)
|
||||
obj.SetName(objRef.Name)
|
||||
if err = c.k8sClient.Get(multicluster.ContextWithClusterName(ctx, objRef.Cluster),
|
||||
client.ObjectKeyFromObject(obj), obj); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
continue
|
||||
var resources = []Resource{}
|
||||
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
|
||||
for _, rt := range append([]*v1beta1.ResourceTracker{rootRT, currentRT}, historyRTs...) {
|
||||
if rt != nil {
|
||||
for _, managedResource := range rt.Spec.ManagedResources {
|
||||
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
|
||||
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) {
|
||||
if _, exist := existResources[managedResource.ClusterObjectReference]; exist {
|
||||
continue
|
||||
}
|
||||
existResources[managedResource.ClusterObjectReference] = true
|
||||
obj, err := managedResource.ToUnstructuredWithData()
|
||||
if err != nil {
|
||||
klog.Errorf("get obj from resource tracker failure %s", err.Error())
|
||||
continue
|
||||
}
|
||||
resources = append(resources, Resource{
|
||||
Cluster: managedResource.Cluster,
|
||||
Revision: oam.GetPublishVersion(rt),
|
||||
Component: managedResource.Component,
|
||||
Object: obj,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if objRef.Cluster == "" {
|
||||
objRef.Cluster = multicluster.ClusterLocalName
|
||||
}
|
||||
resources = append(resources, Resource{
|
||||
Cluster: objRef.Cluster,
|
||||
Revision: obj.GetLabels()[oam.LabelAppRevision],
|
||||
Component: obj.GetLabels()[oam.LabelAppComponent],
|
||||
Object: obj,
|
||||
})
|
||||
}
|
||||
if len(resources) == 0 {
|
||||
return nil, errors.Errorf("fail to find resources created by application: %v", c.opt.Name)
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
@@ -163,11 +170,11 @@ func (c *AppCollector) FindResourceFromResourceTrackerSpec(app *v1beta1.Applicat
|
||||
// FindResourceFromAppliedResourcesField find resources from AppliedResources field
|
||||
func (c *AppCollector) FindResourceFromAppliedResourcesField(app *v1beta1.Application) ([]Resource, error) {
|
||||
resources := make([]Resource, 0, len(app.Spec.Components))
|
||||
for _, rsrcRef := range app.Status.AppliedResources {
|
||||
if !isResourceInTargetCluster(c.opt.Filter, rsrcRef) {
|
||||
for _, res := range app.Status.AppliedResources {
|
||||
if !isResourceInTargetCluster(c.opt.Filter, res) {
|
||||
continue
|
||||
}
|
||||
compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster)
|
||||
compName, obj, err := getObjectCreatedByComponent(c.k8sClient, res.ObjectReference, res.Cluster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +182,7 @@ func (c *AppCollector) FindResourceFromAppliedResourcesField(app *v1beta1.Applic
|
||||
resources = append(resources, Resource{
|
||||
Component: compName,
|
||||
Revision: obj.GetLabels()[oam.LabelAppRevision],
|
||||
Cluster: rsrcRef.Cluster,
|
||||
Cluster: res.Cluster,
|
||||
Object: obj,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -168,61 +168,13 @@ func (p *provider) ExpandTopology(ctx wfContext.Context, v *value.Value, act wfT
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies := &[]*v1beta1.AppPolicy{}
|
||||
policies := &[]v1beta1.AppPolicy{}
|
||||
if err = policiesRaw.UnmarshalTo(policies); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse policies")
|
||||
}
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
placementMap := map[string]struct{}{}
|
||||
addCluster := func(cluster string, ns string, validateCluster bool) error {
|
||||
if validateCluster {
|
||||
if _, e := multicluster.GetVirtualCluster(context.Background(), p, cluster); e != nil {
|
||||
return errors.Wrapf(e, "failed to get cluster %s", cluster)
|
||||
}
|
||||
}
|
||||
if ns == "" {
|
||||
ns = p.app.GetNamespace()
|
||||
}
|
||||
if !resourcekeeper.AllowCrossNamespaceResource && ns != p.app.GetNamespace() {
|
||||
return errors.Errorf("cannot cross namespace")
|
||||
}
|
||||
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
|
||||
name := placement.String()
|
||||
if _, found := placementMap[name]; !found {
|
||||
placementMap[name] = struct{}{}
|
||||
placements = append(placements, placement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, policy := range *policies {
|
||||
if policy.Type == v1alpha1.TopologyPolicyType {
|
||||
topologySpec := &v1alpha1.TopologyPolicySpec{}
|
||||
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
|
||||
}
|
||||
clusterLabelSelector := pkgpolicy.GetClusterLabelSelectorInTopology(topologySpec)
|
||||
switch {
|
||||
case topologySpec.Clusters != nil:
|
||||
for _, cluster := range topologySpec.Clusters {
|
||||
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clusterLabelSelector != nil:
|
||||
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), p, clusterLabelSelector)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
|
||||
}
|
||||
if len(clusters) == 0 {
|
||||
return errors.Errorf("failed to find any cluster matches given labels")
|
||||
}
|
||||
for _, cluster := range clusters {
|
||||
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(context.Background(), p, p.app, *policies, resourcekeeper.AllowCrossNamespaceResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.FillObject(placements, "outputs", "decisions")
|
||||
}
|
||||
|
||||
@@ -576,7 +576,7 @@ func TestExpandTopology(t *testing.T) {
|
||||
},
|
||||
"topology-by-clusters": {
|
||||
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusters:["cluster-a"]}}]}}`,
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}},
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}},
|
||||
},
|
||||
"topology-by-cluster-selector-404": {
|
||||
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"bad-value"}}}]}}`,
|
||||
@@ -584,11 +584,11 @@ func TestExpandTopology(t *testing.T) {
|
||||
},
|
||||
"topology-by-cluster-selector": {
|
||||
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"value"}}}]}}`,
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}, {Cluster: "cluster-b", Namespace: "test"}},
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}, {Cluster: "cluster-b", Namespace: ""}},
|
||||
},
|
||||
"topology-by-cluster-label-selector": {
|
||||
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterLabelSelector:{"key":"value"}}}]}}`,
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: "test"}, {Cluster: "cluster-b", Namespace: "test"}},
|
||||
Outputs: []v1alpha1.PlacementDecision{{Cluster: "cluster-a", Namespace: ""}, {Cluster: "cluster-b", Namespace: ""}},
|
||||
},
|
||||
"topology-by-cluster-selector-and-namespace-invalid": {
|
||||
Input: `{inputs:{policies:[{name:"topology-policy",type:"topology",properties:{clusterSelector:{"key":"value"},namespace:"override"}}]}}`,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -68,6 +69,7 @@ const (
|
||||
const (
|
||||
statusEnabled = "enabled"
|
||||
statusDisabled = "disabled"
|
||||
statusSuspend = "suspend"
|
||||
)
|
||||
|
||||
var forceDisable bool
|
||||
@@ -204,7 +206,12 @@ func AdditionalEndpointPrinter(ctx context.Context, c common.Args, k8sClient cli
|
||||
return
|
||||
}
|
||||
if name == "velaux" {
|
||||
fmt.Println(`Please use command: "vela port-forward -n vela-system addon-velaux 9082:80" and Select "Cluster: local | Namespace: vela-system | Component: velaux | Kind: Service" to check the dashboard.`)
|
||||
fmt.Println(`To check the initialized admin user name and password by:`)
|
||||
fmt.Println(` vela logs -n vela-system --name apiserver addon-velaux | grep "initialized admin username"`)
|
||||
fmt.Println(`To open the dashboard directly by port-forward:`)
|
||||
fmt.Println(` vela port-forward -n vela-system addon-velaux 9082:80`)
|
||||
fmt.Println(`Select "Cluster: local | Namespace: vela-system | Component: velaux | Kind: Service" from the prompt.`)
|
||||
fmt.Println(`Please refer to https://kubevela.io/docs/reference/addons/velaux for more VelaUX addon installation and visiting method.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +259,7 @@ Upgrade addon for specific clusters, (local means control plane):
|
||||
}
|
||||
ioStream.Infof("enable addon by local dir: %s \n", addonOrDir)
|
||||
// args[0] is a local path install with local dir
|
||||
name := filepath.Base(addonOrDir)
|
||||
name = filepath.Base(addonOrDir)
|
||||
_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot fetch addon related addon %s", name)
|
||||
@@ -265,7 +272,7 @@ Upgrade addon for specific clusters, (local means control plane):
|
||||
if filepath.IsAbs(addonOrDir) || strings.HasPrefix(addonOrDir, ".") || strings.HasSuffix(addonOrDir, "/") {
|
||||
return fmt.Errorf("addon directory %s not found in local", addonOrDir)
|
||||
}
|
||||
|
||||
name = addonOrDir
|
||||
_, err = pkgaddon.FetchAddonRelatedApp(context.Background(), k8sClient, addonOrDir)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot fetch addon related addon %s", addonOrDir)
|
||||
@@ -399,7 +406,9 @@ func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("addon %s status is %s \n", name, status.AddonPhase)
|
||||
|
||||
fmt.Print(generateAddonInfo(name, status))
|
||||
|
||||
if status.AddonPhase != statusEnabled && status.AddonPhase != statusDisabled {
|
||||
fmt.Printf("diagnose addon info from application %s", pkgaddon.Convert2AppName(name))
|
||||
err := printAppStatus(context.Background(), k8sClient, ioStreams, pkgaddon.Convert2AppName(name), types.DefaultKubeVelaNS, cmd, c)
|
||||
@@ -410,6 +419,36 @@ func statusAddon(name string, ioStreams cmdutil.IOStreams, cmd *cobra.Command, c
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateAddonInfo(name string, status pkgaddon.Status) string {
|
||||
var res string
|
||||
var phase string
|
||||
|
||||
switch status.AddonPhase {
|
||||
case statusEnabled:
|
||||
c := color.New(color.FgGreen)
|
||||
phase = c.Sprintf("%s", status.AddonPhase)
|
||||
case statusSuspend:
|
||||
c := color.New(color.FgRed)
|
||||
phase = c.Sprintf("%s", status.AddonPhase)
|
||||
default:
|
||||
phase = status.AddonPhase
|
||||
}
|
||||
res += fmt.Sprintf("addon %s status is %s \n", name, phase)
|
||||
if len(status.InstalledVersion) != 0 {
|
||||
res += fmt.Sprintf("installedVersion: %s \n", status.InstalledVersion)
|
||||
}
|
||||
|
||||
if len(status.Clusters) != 0 {
|
||||
var ic []string
|
||||
for c := range status.Clusters {
|
||||
ic = append(ic, c)
|
||||
}
|
||||
sort.Strings(ic)
|
||||
res += fmt.Sprintf("installedClusters: %s \n", ic)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func listAddons(ctx context.Context, clt client.Client, registry string) error {
|
||||
var addons []*pkgaddon.UIData
|
||||
var err error
|
||||
@@ -504,9 +543,31 @@ func waitApplicationRunning(k8sClient client.Client, addonName string) error {
|
||||
|
||||
}
|
||||
|
||||
// generate the available version
|
||||
// this func put the installed version as the first version and keep the origin order
|
||||
// print ... if available version too much
|
||||
func genAvailableVersionInfo(versions []string, status pkgaddon.Status) string {
|
||||
var v []string
|
||||
|
||||
// put installed-version as the first version and keep the origin order
|
||||
if len(status.InstalledVersion) != 0 {
|
||||
for i, version := range versions {
|
||||
if version == status.InstalledVersion {
|
||||
v = append(v, version)
|
||||
versions = append(versions[:i], versions[i+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
v = append(v, versions...)
|
||||
|
||||
res := "["
|
||||
for _, version := range versions {
|
||||
var count int
|
||||
for _, version := range v {
|
||||
if count == 3 {
|
||||
// just show newest 3 versions
|
||||
res += "..."
|
||||
break
|
||||
}
|
||||
if version == status.InstalledVersion {
|
||||
col := color.New(color.Bold, color.FgGreen)
|
||||
res += col.Sprintf("%s", version)
|
||||
@@ -514,6 +575,7 @@ func genAvailableVersionInfo(versions []string, status pkgaddon.Status) string {
|
||||
res += version
|
||||
}
|
||||
res += ", "
|
||||
count++
|
||||
}
|
||||
res = strings.TrimSuffix(res, ", ")
|
||||
res += "]"
|
||||
|
||||
@@ -18,8 +18,13 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
pkgaddon "github.com/oam-dev/kubevela/pkg/addon"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
|
||||
@@ -151,3 +156,62 @@ func TestTransCluster(t *testing.T) {
|
||||
assert.DeepEqual(t, transClusters(s.str), s.res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateStatusIn(t *testing.T) {
|
||||
testcases := []struct {
|
||||
c pkgaddon.Status
|
||||
res []string
|
||||
}{
|
||||
{
|
||||
c: pkgaddon.Status{InstalledVersion: "1.2.1", Clusters: map[string]map[string]interface{}{"cluster1": nil, "cluster2": nil}, AddonPhase: statusEnabled},
|
||||
res: []string{"installedVersion: 1.2.1", "installedClusters: [cluster1 cluster2]", fmt.Sprintf("status is %s", color.New(color.FgGreen).Sprintf(statusEnabled))},
|
||||
},
|
||||
{
|
||||
c: pkgaddon.Status{InstalledVersion: "1.2.3", AddonPhase: statusSuspend},
|
||||
res: []string{"installedVersion: 1.2.3", fmt.Sprintf("status is %s", color.New(color.FgRed).Sprintf(statusSuspend))},
|
||||
},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
res := generateAddonInfo("test", testcase.c)
|
||||
for _, re := range testcase.res {
|
||||
assert.Equal(t, strings.Contains(res, re), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateAvailableVersions(t *testing.T) {
|
||||
type testcase struct {
|
||||
inVersion string
|
||||
versions []string
|
||||
}
|
||||
testcases := []struct {
|
||||
c testcase
|
||||
res string
|
||||
}{
|
||||
{
|
||||
c: testcase{
|
||||
inVersion: "1.2.1",
|
||||
versions: []string{"1.2.1"},
|
||||
},
|
||||
res: fmt.Sprintf("[%s]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
|
||||
},
|
||||
{
|
||||
c: testcase{
|
||||
inVersion: "1.2.1",
|
||||
versions: []string{"1.2.3", "1.2.2", "1.2.1"},
|
||||
},
|
||||
res: fmt.Sprintf("[%s, 1.2.3, 1.2.2]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
|
||||
},
|
||||
{
|
||||
c: testcase{
|
||||
inVersion: "1.2.1",
|
||||
versions: []string{"1.2.3", "1.2.2", "1.2.1", "1.2.0"},
|
||||
},
|
||||
res: fmt.Sprintf("[%s, 1.2.3, 1.2.2, ...]", color.New(color.Bold, color.FgGreen).Sprintf("1.2.1")),
|
||||
},
|
||||
}
|
||||
for _, s := range testcases {
|
||||
re := genAvailableVersionInfo(s.c.versions, pkgaddon.Status{InstalledVersion: s.c.inVersion})
|
||||
assert.Equal(t, re, s.res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ func ClusterCommandGroup(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Comm
|
||||
NewClusterDetachCommand(&c),
|
||||
NewClusterProbeCommand(&c),
|
||||
NewClusterLabelCommandGroup(&c),
|
||||
NewClusterAliasCommand(&c),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
@@ -101,7 +102,7 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
|
||||
Long: "list worker clusters managed by KubeVela.",
|
||||
Args: cobra.ExactValidArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
table := newUITable().AddRow("CLUSTER", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS")
|
||||
table := newUITable().AddRow("CLUSTER", "ALIAS", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS")
|
||||
client, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -123,9 +124,9 @@ func NewClusterListCommand(c *common.Args) *cobra.Command {
|
||||
}
|
||||
for i, l := range labels {
|
||||
if i == 0 {
|
||||
table.AddRow(cluster.Name, cluster.Type, cluster.EndPoint, fmt.Sprintf("%v", cluster.Accepted), l)
|
||||
table.AddRow(cluster.Name, cluster.Alias, cluster.Type, cluster.EndPoint, fmt.Sprintf("%v", cluster.Accepted), l)
|
||||
} else {
|
||||
table.AddRow("", "", "", "", l)
|
||||
table.AddRow("", "", "", "", "", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,6 +256,29 @@ func NewClusterDetachCommand(c *common.Args) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewClusterAliasCommand create an alias to the named cluster
|
||||
func NewClusterAliasCommand(c *common.Args) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alias CLUSTER_NAME ALIAS",
|
||||
Short: "alias a named cluster.",
|
||||
Long: "alias a named cluster.",
|
||||
Args: cobra.ExactValidArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clusterName, aliasName := args[0], args[1]
|
||||
k8sClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = multicluster.AliasCluster(context.Background(), k8sClient, clusterName, aliasName); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Printf("Alias cluster %s as %s.\n", clusterName, aliasName)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewClusterProbeCommand create command to help user try health probe for existing cluster
|
||||
// TODO(somefive): move prob logic into cluster management
|
||||
func NewClusterProbeCommand(c *common.Args) *cobra.Command {
|
||||
@@ -299,12 +323,12 @@ func NewClusterLabelCommandGroup(c *common.Args) *cobra.Command {
|
||||
|
||||
func updateClusterLabelAndPrint(cmd *cobra.Command, cli client.Client, vc *multicluster.VirtualCluster, clusterName string) (err error) {
|
||||
if err = cli.Update(context.Background(), vc.Object); err != nil {
|
||||
return errors.Errorf("failed to update labels for cluster %s (type: %s)", vc.Name, vc.Type)
|
||||
return errors.Errorf("failed to update labels for cluster %s, type: %s", vc.FullName(), vc.Type)
|
||||
}
|
||||
if vc, err = multicluster.GetVirtualCluster(context.Background(), cli, clusterName); err != nil {
|
||||
return errors.Wrapf(err, "failed to get updated cluster %s", clusterName)
|
||||
}
|
||||
cmd.Printf("Successfully update labels for cluster %s (type: %s).\n", vc.Name, vc.Type)
|
||||
cmd.Printf("Successfully update labels for cluster %s, type: %s.\n", vc.FullName(), vc.Type)
|
||||
if len(vc.Labels) == 0 {
|
||||
cmd.Println("No valid label exists.")
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user