mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-23 22:33:58 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
825f1aaa22 | ||
|
|
82075427e6 | ||
|
|
f89cf673c0 | ||
|
|
ce53f6922f | ||
|
|
b6f70d9a3c | ||
|
|
64d063ccfe | ||
|
|
a98278fb7a | ||
|
|
0553d603e6 | ||
|
|
a36e99308f | ||
|
|
947bac2d35 | ||
|
|
7644cc59cb | ||
|
|
89a441b8ce | ||
|
|
780572c68f | ||
|
|
a13cab65b2 | ||
|
|
e26104adcc | ||
|
|
26ac584655 | ||
|
|
482976990d | ||
|
|
bc4812a12e | ||
|
|
58c2208e2a | ||
|
|
f83d88cfb0 |
2
.github/workflows/e2e-multicluster-test.yml
vendored
2
.github/workflows/e2e-multicluster-test.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -71,6 +71,10 @@ const (
|
||||
LabelConfigProject = "config.oam.dev/project"
|
||||
// LabelConfigSyncToMultiCluster is the label to decide whether a config will be synchronized to multi-cluster
|
||||
LabelConfigSyncToMultiCluster = "config.oam.dev/multi-cluster"
|
||||
// LabelConfigIdentifier is the label for config identifier
|
||||
LabelConfigIdentifier = "config.oam.dev/identifier"
|
||||
// LabelConfigDescription is the label for config description
|
||||
LabelConfigDescription = "config.oam.dev/description"
|
||||
// AnnotationConfigAlias is the annotation for config alias
|
||||
AnnotationConfigAlias = "config.oam.dev/alias"
|
||||
)
|
||||
@@ -139,4 +143,8 @@ const (
|
||||
TerraformProvider = "terraform-provider"
|
||||
// DexConnector is the config type for dex connector
|
||||
DexConnector = "config-dex-connector"
|
||||
// ImageRegistry is the config type for image registry
|
||||
ImageRegistry = "config-image-registry"
|
||||
// HelmRepository is the config type for Helm chart repository
|
||||
HelmRepository = "config-helm-repository"
|
||||
)
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/config-image-registry.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
custom.definition.oam.dev/alias.config.oam.dev: Image Registry
|
||||
definition.oam.dev/description: Config information to authenticate image registry
|
||||
labels:
|
||||
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
|
||||
custom.definition.oam.dev/multi-cluster.config.oam.dev: "true"
|
||||
custom.definition.oam.dev/type.config.oam.dev: image-registry
|
||||
name: config-image-registry
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: {
|
||||
name: context.name
|
||||
namespace: context.namespace
|
||||
labels: {
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "image-registry"
|
||||
"config.oam.dev/multi-cluster": "true"
|
||||
"config.oam.dev/identifier": parameter.registry
|
||||
"config.oam.dev/sub-type": "auth"
|
||||
}
|
||||
}
|
||||
type: "kubernetes.io/dockerconfigjson"
|
||||
stringData: {
|
||||
if parameter.auth != _|_ {
|
||||
".dockerconfigjson": json.Marshal({
|
||||
auths: "\(parameter.registry)": {
|
||||
username: parameter.auth.username
|
||||
password: parameter.auth.password
|
||||
if parameter.auth.email != _|_ {
|
||||
email: parameter.auth.email
|
||||
}
|
||||
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Image registry FQDN
|
||||
registry: string
|
||||
// +usage=Authenticate the image registry
|
||||
auth?: {
|
||||
// +usage=Private Image registry username
|
||||
username: string
|
||||
// +usage=Private Image registry password
|
||||
password: string
|
||||
// +usage=Private Image registry email
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
workload:
|
||||
type: autodetects.core.oam.dev
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/config-image-registry.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
custom.definition.oam.dev/alias.config.oam.dev: Image Registry
|
||||
definition.oam.dev/description: Config information to authenticate image registry
|
||||
labels:
|
||||
custom.definition.oam.dev/catalog.config.oam.dev: velacore-config
|
||||
custom.definition.oam.dev/multi-cluster.config.oam.dev: "true"
|
||||
custom.definition.oam.dev/type.config.oam.dev: image-registry
|
||||
name: config-image-registry
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: {
|
||||
name: context.name
|
||||
namespace: context.namespace
|
||||
labels: {
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "image-registry"
|
||||
"config.oam.dev/multi-cluster": "true"
|
||||
"config.oam.dev/identifier": parameter.registry
|
||||
"config.oam.dev/sub-type": "auth"
|
||||
}
|
||||
}
|
||||
type: "kubernetes.io/dockerconfigjson"
|
||||
stringData: {
|
||||
if parameter.auth != _|_ {
|
||||
".dockerconfigjson": json.Marshal({
|
||||
auths: "\(parameter.registry)": {
|
||||
username: parameter.auth.username
|
||||
password: parameter.auth.password
|
||||
if parameter.auth.email != _|_ {
|
||||
email: parameter.auth.email
|
||||
}
|
||||
auth: base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Image registry FQDN
|
||||
registry: string
|
||||
// +usage=Authenticate the image registry
|
||||
auth?: {
|
||||
// +usage=Private Image registry username
|
||||
username: string
|
||||
// +usage=Private Image registry password
|
||||
password: string
|
||||
// +usage=Private Image registry email
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
workload:
|
||||
type: autodetects.core.oam.dev
|
||||
|
||||
@@ -107,7 +107,7 @@ multicluster:
|
||||
port: 9443
|
||||
image:
|
||||
repository: oamdev/cluster-gateway
|
||||
tag: v1.3.0
|
||||
tag: v1.3.2
|
||||
pullPolicy: IfNotPresent
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -7,4 +7,5 @@ coverage:
|
||||
default:
|
||||
target: 70%
|
||||
ignore:
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "**/zz_generated.deepcopy.go"
|
||||
- "references/"
|
||||
|
||||
@@ -3337,6 +3337,284 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "list all config types",
|
||||
"operationId": "listConfigTypes",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Fuzzy search based on name and description.",
|
||||
"name": "query",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1.ConfigType"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get a config type",
|
||||
"operationId": "getConfigType",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.ConfigType"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "create or update a config",
|
||||
"operationId": "createConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.CreateConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EmptyResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}/configs": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get configs from a config type",
|
||||
"operationId": "getConfigs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/config_types/{configType}/configs/{name}": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "get a config from a config type",
|
||||
"operationId": "getConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "delete a config",
|
||||
"operationId": "deleteConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config type",
|
||||
"name": "configType",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the config",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1.EmptyResponse"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/definitions": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -3862,6 +4140,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/projects/{projectName}/configs": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/xml",
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/xml"
|
||||
],
|
||||
"tags": [
|
||||
"project"
|
||||
],
|
||||
"summary": "get configs which are in a project",
|
||||
"operationId": "getConfigs",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "config type",
|
||||
"name": "configType",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "identifier of the project",
|
||||
"name": "projectName",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/*v1.Config"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/bcode.Bcode"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/projects/{projectName}/permissions": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -5302,6 +5629,7 @@
|
||||
},
|
||||
"definitions": {
|
||||
"*v1.ApplicationTriggerBase": {},
|
||||
"*v1.Config": {},
|
||||
"*v1.EmptyResponse": {},
|
||||
"addon.Dependency": {
|
||||
"properties": {
|
||||
@@ -5354,6 +5682,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"addon.GitlabAddonSource": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addon.HelmSource": {
|
||||
"properties": {
|
||||
"url": {
|
||||
@@ -6623,34 +6967,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.SystemInfo": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installID": {
|
||||
"type": "string"
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model.WorkflowStepStatus": {
|
||||
"required": [
|
||||
"id",
|
||||
@@ -7039,6 +7355,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
@@ -7209,12 +7528,12 @@
|
||||
},
|
||||
"v1.ApplicationDeployResponse": {
|
||||
"required": [
|
||||
"version",
|
||||
"status",
|
||||
"note",
|
||||
"createTime",
|
||||
"envName",
|
||||
"triggerType"
|
||||
"triggerType",
|
||||
"createTime",
|
||||
"version",
|
||||
"status"
|
||||
],
|
||||
"properties": {
|
||||
"codeInfo": {
|
||||
@@ -7748,6 +8067,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ConfigType": {
|
||||
"required": [
|
||||
"definitions",
|
||||
"alias",
|
||||
"name",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"definitions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.ConnectCloudClusterRequest": {
|
||||
"required": [
|
||||
"accessKeyID",
|
||||
@@ -7797,6 +8141,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
@@ -8093,6 +8440,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.CreateConfigRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
"alias",
|
||||
"project",
|
||||
"componentType"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"componentType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"project": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.CreateEnvRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
@@ -8304,11 +8676,11 @@
|
||||
},
|
||||
"v1.DetailAddonResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"description",
|
||||
"icon",
|
||||
"version",
|
||||
"description",
|
||||
"invisible",
|
||||
"name",
|
||||
"icon",
|
||||
"schema",
|
||||
"uiSchema",
|
||||
"definitions",
|
||||
@@ -8388,12 +8760,12 @@
|
||||
},
|
||||
"v1.DetailApplicationResponse": {
|
||||
"required": [
|
||||
"alias",
|
||||
"icon",
|
||||
"project",
|
||||
"description",
|
||||
"icon",
|
||||
"name",
|
||||
"createTime",
|
||||
"name",
|
||||
"alias",
|
||||
"updateTime",
|
||||
"policies",
|
||||
"envBindings",
|
||||
@@ -8455,20 +8827,20 @@
|
||||
},
|
||||
"v1.DetailClusterResponse": {
|
||||
"required": [
|
||||
"status",
|
||||
"apiServerURL",
|
||||
"dashboardURL",
|
||||
"kubeConfigSecret",
|
||||
"icon",
|
||||
"labels",
|
||||
"kubeConfig",
|
||||
"updateTime",
|
||||
"reason",
|
||||
"provider",
|
||||
"name",
|
||||
"alias",
|
||||
"description",
|
||||
"reason",
|
||||
"icon",
|
||||
"dashboardURL",
|
||||
"createTime",
|
||||
"provider",
|
||||
"description",
|
||||
"status",
|
||||
"updateTime",
|
||||
"apiServerURL",
|
||||
"kubeConfig",
|
||||
"labels",
|
||||
"kubeConfigSecret",
|
||||
"resourceInfo"
|
||||
],
|
||||
"properties": {
|
||||
@@ -8526,14 +8898,14 @@
|
||||
},
|
||||
"v1.DetailComponentResponse": {
|
||||
"required": [
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"appPrimaryKey",
|
||||
"type",
|
||||
"main",
|
||||
"name",
|
||||
"updateTime",
|
||||
"createTime",
|
||||
"creator",
|
||||
"alias",
|
||||
"type",
|
||||
"main",
|
||||
"appPrimaryKey",
|
||||
"definition"
|
||||
],
|
||||
"properties": {
|
||||
@@ -8635,13 +9007,13 @@
|
||||
},
|
||||
"v1.DetailPolicyResponse": {
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"description",
|
||||
"creator",
|
||||
"properties",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"name"
|
||||
"updateTime"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
@@ -8671,17 +9043,17 @@
|
||||
},
|
||||
"v1.DetailRevisionResponse": {
|
||||
"required": [
|
||||
"updateTime",
|
||||
"triggerType",
|
||||
"updateTime",
|
||||
"version",
|
||||
"deployUser",
|
||||
"reason",
|
||||
"createTime",
|
||||
"status",
|
||||
"deployUser",
|
||||
"note",
|
||||
"workflowName",
|
||||
"envName",
|
||||
"appPrimaryKey",
|
||||
"reason",
|
||||
"note"
|
||||
"appPrimaryKey"
|
||||
],
|
||||
"properties": {
|
||||
"appPrimaryKey": {
|
||||
@@ -8737,8 +9109,8 @@
|
||||
"required": [
|
||||
"project",
|
||||
"createTime",
|
||||
"name",
|
||||
"updateTime"
|
||||
"updateTime",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
@@ -8778,11 +9150,11 @@
|
||||
},
|
||||
"v1.DetailUserResponse": {
|
||||
"required": [
|
||||
"disabled",
|
||||
"createTime",
|
||||
"lastLoginTime",
|
||||
"name",
|
||||
"email",
|
||||
"disabled",
|
||||
"createTime",
|
||||
"projects",
|
||||
"roles"
|
||||
],
|
||||
@@ -8823,12 +9195,12 @@
|
||||
},
|
||||
"v1.DetailWorkflowRecordResponse": {
|
||||
"required": [
|
||||
"status",
|
||||
"name",
|
||||
"namespace",
|
||||
"workflowName",
|
||||
"workflowAlias",
|
||||
"applicationRevision",
|
||||
"status",
|
||||
"deployTime",
|
||||
"deployUser",
|
||||
"note",
|
||||
@@ -8880,14 +9252,14 @@
|
||||
},
|
||||
"v1.DetailWorkflowResponse": {
|
||||
"required": [
|
||||
"envName",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"name",
|
||||
"enable",
|
||||
"default",
|
||||
"description",
|
||||
"name",
|
||||
"alias"
|
||||
"envName",
|
||||
"createTime",
|
||||
"alias",
|
||||
"description"
|
||||
],
|
||||
"properties": {
|
||||
"alias": {
|
||||
@@ -9466,11 +9838,11 @@
|
||||
},
|
||||
"v1.LoginUserInfoResponse": {
|
||||
"required": [
|
||||
"disabled",
|
||||
"createTime",
|
||||
"lastLoginTime",
|
||||
"name",
|
||||
"email",
|
||||
"disabled",
|
||||
"projects",
|
||||
"platformPermissions",
|
||||
"projectPermissions"
|
||||
@@ -9778,6 +10150,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.SystemInfo": {
|
||||
"required": [
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType"
|
||||
],
|
||||
"properties": {
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"installID": {
|
||||
"type": "string"
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1.SystemInfoRequest": {
|
||||
"required": [
|
||||
"enableCollection",
|
||||
@@ -9789,6 +10179,9 @@
|
||||
},
|
||||
"loginType": {
|
||||
"type": "string"
|
||||
},
|
||||
"velaAddress": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9797,15 +10190,9 @@
|
||||
"installID",
|
||||
"enableCollection",
|
||||
"loginType",
|
||||
"createTime",
|
||||
"updateTime",
|
||||
"systemVersion"
|
||||
],
|
||||
"properties": {
|
||||
"createTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"enableCollection": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -9817,10 +10204,6 @@
|
||||
},
|
||||
"systemVersion": {
|
||||
"$ref": "#/definitions/v1.SystemVersion"
|
||||
},
|
||||
"updateTime": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9889,6 +10272,9 @@
|
||||
"gitee": {
|
||||
"$ref": "#/definitions/addon.GiteeAddonSource"
|
||||
},
|
||||
"gitlab": {
|
||||
"$ref": "#/definitions/addon.GitlabAddonSource"
|
||||
},
|
||||
"helm": {
|
||||
"$ref": "#/definitions/addon.HelmSource"
|
||||
},
|
||||
|
||||
@@ -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))
|
||||
|
||||
6
go.mod
6
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
|
||||
@@ -251,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
|
||||
|
||||
10
go.sum
10
go.sum
@@ -2047,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=
|
||||
@@ -2064,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 {
|
||||
@@ -1344,7 +1344,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
return err
|
||||
}
|
||||
if !res {
|
||||
return fmt.Errorf("vela cli/ux version: %s cannot meet requirement", version2.VelaVersion)
|
||||
return fmt.Errorf("vela cli/ux version: %s require: %s", version2.VelaVersion, require.VelaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1361,7 +1361,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
return err
|
||||
}
|
||||
if !res {
|
||||
return fmt.Errorf("the vela core controller: %s cannot meet requirement ", imageVersion)
|
||||
return fmt.Errorf("the vela core controller: %s require: %s", imageVersion, require.VelaVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1382,7 +1382,7 @@ func checkAddonVersionMeetRequired(ctx context.Context, require *SystemRequireme
|
||||
}
|
||||
|
||||
if !res {
|
||||
return fmt.Errorf("the kubernetes version %s cannot meet requirement", k8sVersion.GitVersion)
|
||||
return fmt.Errorf("the kubernetes version %s require: %s", k8sVersion.GitVersion, require.KubernetesVersion)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -194,13 +194,16 @@ type ConfigType struct {
|
||||
|
||||
// Config define the metadata of a config
|
||||
type Config struct {
|
||||
ConfigType string `json:"configType"`
|
||||
Name string `json:"name"`
|
||||
Project string `json:"project"`
|
||||
Identifier string `json:"identifier"`
|
||||
Description string `json:"description"`
|
||||
CreatedTime *time.Time `json:"createdTime"`
|
||||
UpdatedTime *time.Time `json:"updatedTime"`
|
||||
ConfigType string `json:"configType"`
|
||||
ConfigTypeAlias string `json:"configTypeAlias"`
|
||||
Name string `json:"name"`
|
||||
Project string `json:"project"`
|
||||
Identifier string `json:"identifier"`
|
||||
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
|
||||
|
||||
@@ -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"
|
||||
@@ -705,21 +705,9 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
|
||||
}
|
||||
|
||||
// sync configs to clusters
|
||||
// TODO(zzxwill) need to check the type of the componentDefinition, if it is `Cloud`, skip the sync
|
||||
targets, err := listTarget(ctx, c.ds, app.Project, nil)
|
||||
if err != nil {
|
||||
if err := c.syncConfigs4Application(ctx, oamApp, app.Project, workflow.EnvName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var clusterTargets []*model.ClusterTarget
|
||||
for i, t := range targets {
|
||||
if t.Cluster != nil {
|
||||
clusterTargets = append(clusterTargets, targets[i].Cluster)
|
||||
}
|
||||
}
|
||||
|
||||
if err := SyncConfigs(ctx, c.kubeClient, app.Project, clusterTargets); err != nil {
|
||||
return nil, fmt.Errorf("sync config failure %w", err)
|
||||
}
|
||||
|
||||
// step2: check and create deploy event
|
||||
if !req.Force {
|
||||
@@ -809,6 +797,44 @@ func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Applicat
|
||||
}, nil
|
||||
}
|
||||
|
||||
// sync configs to clusters
|
||||
func (c *applicationUsecaseImpl) syncConfigs4Application(ctx context.Context, app *v1beta1.Application, projectName, envName string) error {
|
||||
var areTerraformComponents = true
|
||||
for _, m := range app.Spec.Components {
|
||||
d := &v1beta1.ComponentDefinition{}
|
||||
if err := c.kubeClient.Get(ctx, client.ObjectKey{Namespace: velatypes.DefaultKubeVelaNS, Name: m.Type}, d); err != nil {
|
||||
klog.ErrorS(err, "failed to get config type", "ComponentDefinition", m.Type)
|
||||
}
|
||||
// check the type of the componentDefinition is Terraform
|
||||
if d.Spec.Schematic != nil && d.Spec.Schematic.Terraform == nil {
|
||||
areTerraformComponents = false
|
||||
}
|
||||
}
|
||||
// skip configs sync
|
||||
if areTerraformComponents {
|
||||
return nil
|
||||
}
|
||||
env, err := c.envUsecase.GetEnv(ctx, envName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var clusterTargets []*model.ClusterTarget
|
||||
for _, t := range env.Targets {
|
||||
target, err := c.targetUsecase.GetTarget(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Cluster != nil {
|
||||
clusterTargets = append(clusterTargets, target.Cluster)
|
||||
}
|
||||
}
|
||||
|
||||
if err := SyncConfigs(ctx, c.kubeClient, projectName, clusterTargets); err != nil {
|
||||
return fmt.Errorf("sync config failure %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *applicationUsecaseImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, version string) (*v1beta1.Application, error) {
|
||||
// Priority 1 uses the requested workflow as release .
|
||||
// Priority 2 uses the default workflow as release .
|
||||
|
||||
@@ -94,7 +94,6 @@ type authHandler interface {
|
||||
}
|
||||
|
||||
type dexHandlerImpl struct {
|
||||
token *oauth2.Token
|
||||
idToken *oidc.IDToken
|
||||
ds datastore.DataStore
|
||||
}
|
||||
@@ -135,7 +134,6 @@ func (a *authenticationUsecaseImpl) newDexHandler(ctx context.Context, req apisv
|
||||
return nil, err
|
||||
}
|
||||
return &dexHandlerImpl{
|
||||
token: token,
|
||||
idToken: idToken,
|
||||
ds: a.ds,
|
||||
}, nil
|
||||
@@ -183,11 +181,11 @@ func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.L
|
||||
if userBase.Disabled {
|
||||
return nil, bcode.ErrUserAlreadyDisabled
|
||||
}
|
||||
accessToken, err := a.generateJWTToken(ctx, userBase.Name, GrantTypeAccess, time.Hour)
|
||||
accessToken, err := a.generateJWTToken(userBase.Name, GrantTypeAccess, time.Hour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refreshToken, err := a.generateJWTToken(ctx, userBase.Name, GrantTypeRefresh, time.Hour*24)
|
||||
refreshToken, err := a.generateJWTToken(userBase.Name, GrantTypeRefresh, time.Hour*24)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -198,7 +196,7 @@ func (a *authenticationUsecaseImpl) Login(ctx context.Context, loginReq apisv1.L
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) generateJWTToken(ctx context.Context, username, grantType string, expireDuration time.Duration) (string, error) {
|
||||
func (a *authenticationUsecaseImpl) generateJWTToken(username, grantType string, expireDuration time.Duration) (string, error) {
|
||||
expire := time.Now().Add(expireDuration)
|
||||
claims := model.CustomClaims{
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
@@ -210,24 +208,7 @@ func (a *authenticationUsecaseImpl) generateJWTToken(ctx context.Context, userna
|
||||
GrantType: grantType,
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
signed, err := a.getSignedKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.SignedString([]byte(signed))
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) getSignedKey(ctx context.Context) (string, error) {
|
||||
if signedKey != "" {
|
||||
return signedKey, nil
|
||||
}
|
||||
info, err := a.sysUsecase.Get(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signedKey = info.InstallID
|
||||
|
||||
return signedKey, nil
|
||||
return token.SignedString([]byte(signedKey))
|
||||
}
|
||||
|
||||
func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshToken string) (*apisv1.RefreshTokenResponse, error) {
|
||||
@@ -239,7 +220,7 @@ func (a *authenticationUsecaseImpl) RefreshToken(ctx context.Context, refreshTok
|
||||
return nil, err
|
||||
}
|
||||
if claim.GrantType == GrantTypeRefresh {
|
||||
accessToken, err := a.generateJWTToken(ctx, claim.Username, GrantTypeAccess, time.Hour)
|
||||
accessToken, err := a.generateJWTToken(claim.Username, GrantTypeAccess, time.Hour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -429,19 +410,18 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
}
|
||||
|
||||
user := &model.User{Email: claims.Email}
|
||||
userBase := &apisv1.UserBase{Email: claims.Email, Name: claims.Name}
|
||||
users, err := d.ds.List(ctx, user, &datastore.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(users) > 0 {
|
||||
u := users[0].(*model.User)
|
||||
if u.Name != claims.Name {
|
||||
u.Name = claims.Name
|
||||
}
|
||||
u.LastLoginTime = time.Now()
|
||||
if err := d.ds.Put(ctx, u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userBase.Name = u.Name
|
||||
} else if err := d.ds.Add(ctx, &model.User{
|
||||
Email: claims.Email,
|
||||
Name: claims.Name,
|
||||
@@ -450,10 +430,7 @@ func (d *dexHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &apisv1.UserBase{
|
||||
Name: claims.Name,
|
||||
Email: claims.Email,
|
||||
}, nil
|
||||
return userBase, nil
|
||||
}
|
||||
|
||||
func (l *localHandlerImpl) login(ctx context.Context) (*apisv1.UserBase, error) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"github.com/coreos/go-oidc"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"golang.org/x/oauth2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@@ -68,10 +67,6 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
})
|
||||
defer patch.Reset()
|
||||
dexHandler := dexHandlerImpl{
|
||||
token: &oauth2.Token{
|
||||
AccessToken: "access-token",
|
||||
RefreshToken: "refresh-token",
|
||||
},
|
||||
idToken: testIDToken,
|
||||
ds: ds,
|
||||
}
|
||||
@@ -86,6 +81,20 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
err = ds.Get(context.Background(), user)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(user.Email).Should(Equal("test@test.com"))
|
||||
|
||||
existUser := &model.User{
|
||||
Name: "test",
|
||||
}
|
||||
err = ds.Delete(context.Background(), existUser)
|
||||
Expect(err).Should(BeNil())
|
||||
existUser.Name = "exist-user"
|
||||
existUser.Email = "test@test.com"
|
||||
err = ds.Add(context.Background(), existUser)
|
||||
Expect(err).Should(BeNil())
|
||||
resp, err = dexHandler.login(context.Background())
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(resp.Email).Should(Equal("test@test.com"))
|
||||
Expect(resp.Name).Should(Equal("exist-user"))
|
||||
})
|
||||
|
||||
It("Test local login", func() {
|
||||
@@ -175,6 +184,8 @@ var _ = Describe("Test authentication usecase functions", func() {
|
||||
It("Test get dex config", func() {
|
||||
_, err := authUsecase.GetDexConfig(context.Background())
|
||||
Expect(err).Should(Equal(bcode.ErrInvalidDexConfig))
|
||||
err = ds.Add(context.Background(), &model.User{Name: "admin", Email: "test@test.com"})
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = sysUsecase.UpdateSystemInfo(context.Background(), apisv1.SystemInfoRequest{
|
||||
LoginType: model.LoginTypeDex,
|
||||
VelaAddress: "http://velaux.com",
|
||||
|
||||
@@ -20,15 +20,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
set "github.com/deckarep/golang-set"
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
@@ -44,7 +43,11 @@ const (
|
||||
definitionAlias = definition.UserPrefix + "alias.config.oam.dev"
|
||||
definitionType = definition.UserPrefix + "type.config.oam.dev"
|
||||
|
||||
velaCoreConfig = "velacore-config"
|
||||
velaCoreConfig = "velacore-config"
|
||||
configIsReady = "Ready"
|
||||
configIsNotReady = "Not ready"
|
||||
terraformProviderAlias = "Terraform Cloud Provider"
|
||||
configSyncProjectPrefix = "config-sync"
|
||||
)
|
||||
|
||||
// ConfigHandler handle CRUD of configs
|
||||
@@ -98,18 +101,21 @@ func (u *configUseCaseImpl) ListConfigTypes(ctx context.Context, query string) (
|
||||
})
|
||||
}
|
||||
|
||||
tfType := &apis.ConfigType{
|
||||
Alias: "Terraform Cloud Provider",
|
||||
Name: types.TerraformProvider,
|
||||
}
|
||||
definitions := make([]string, len(tfDefs))
|
||||
if len(tfDefs) > 0 {
|
||||
tfType := &apis.ConfigType{
|
||||
Alias: terraformProviderAlias,
|
||||
Name: types.TerraformProvider,
|
||||
}
|
||||
definitions := make([]string, len(tfDefs))
|
||||
|
||||
for i, tf := range tfDefs {
|
||||
definitions[i] = tf.Name
|
||||
}
|
||||
tfType.Definitions = definitions
|
||||
for i, tf := range tfDefs {
|
||||
definitions[i] = tf.Name
|
||||
}
|
||||
tfType.Definitions = definitions
|
||||
|
||||
return append(configTypes, tfType), nil
|
||||
return append(configTypes, tfType), nil
|
||||
}
|
||||
return configTypes, nil
|
||||
}
|
||||
|
||||
// GetConfigType returns a config type
|
||||
@@ -152,35 +158,7 @@ func (u *configUseCaseImpl) CreateConfig(ctx context.Context, req apis.CreateCon
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := u.kubeClient.Create(ctx, &app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// try to check whether the underlying config secrets is successfully created
|
||||
var succeeded bool
|
||||
var configApp v1beta1.Application
|
||||
for i := 0; i < 100; i++ {
|
||||
if err := u.kubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: req.Name}, &configApp); err == nil {
|
||||
if configApp.Status.Phase == common.ApplicationRunning {
|
||||
succeeded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
// clean up failed application
|
||||
if !succeeded {
|
||||
if err := u.kubeClient.Delete(ctx, &app); err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("failed to create config")
|
||||
}
|
||||
|
||||
if succeeded && req.ComponentType == types.DexConnector {
|
||||
return u.authenticationUseCase.UpdateDexConfig(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
return u.kubeClient.Create(ctx, &app)
|
||||
}
|
||||
|
||||
func (u *configUseCaseImpl) GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error) {
|
||||
@@ -229,6 +207,12 @@ func (u *configUseCaseImpl) getConfigsByConfigType(ctx context.Context, configTy
|
||||
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
|
||||
}
|
||||
@@ -265,13 +249,12 @@ type ApplicationDeployTarget struct {
|
||||
|
||||
// SyncConfigs will sync configs to working clusters
|
||||
func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, targets []*model.ClusterTarget) error {
|
||||
name := fmt.Sprintf("config-sync-%s", project)
|
||||
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
|
||||
// get all configs which can be synced to working clusters in the project
|
||||
var secrets v1.SecretList
|
||||
if err := k8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigProject: project,
|
||||
types.LabelConfigSyncToMultiCluster: "true",
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -279,13 +262,19 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
|
||||
if len(secrets.Items) == 0 {
|
||||
return nil
|
||||
}
|
||||
objects := make([]map[string]string, len(secrets.Items))
|
||||
for i, s := range secrets.Items {
|
||||
objects[i] = map[string]string{
|
||||
"name": s.Name,
|
||||
"resource": "secret",
|
||||
var objects []map[string]string
|
||||
for _, s := range secrets.Items {
|
||||
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
|
||||
objects = append(objects, map[string]string{
|
||||
"name": s.Name,
|
||||
"resource": "secret",
|
||||
})
|
||||
}
|
||||
}
|
||||
if len(objects) == 0 {
|
||||
klog.InfoS("no configs need to sync to working clusters", "project", project)
|
||||
return nil
|
||||
}
|
||||
objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -297,6 +286,26 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
|
||||
return err
|
||||
}
|
||||
// config sync application doesn't exist, create one
|
||||
clusterTargets := convertClusterTargets(targets)
|
||||
if len(clusterTargets) == 0 {
|
||||
errMsg := "no policy (no targets found) to sync configs"
|
||||
klog.InfoS(errMsg, "project", project)
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
policies := make([]v1beta1.AppPolicy, len(clusterTargets))
|
||||
for i, t := range clusterTargets {
|
||||
properties, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies[i] = v1beta1.AppPolicy{
|
||||
Type: "topology",
|
||||
Name: t.Namespace,
|
||||
Properties: &runtime.RawExtension{
|
||||
Raw: properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
scratch := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -316,11 +325,10 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
|
||||
Properties: &runtime.RawExtension{Raw: objectsBytes},
|
||||
},
|
||||
},
|
||||
Policies: policies,
|
||||
},
|
||||
}
|
||||
if err := k8sClient.Create(ctx, scratch); err != nil {
|
||||
return err
|
||||
}
|
||||
return k8sClient.Create(ctx, scratch)
|
||||
}
|
||||
// config sync application exists, update it
|
||||
app.Spec.Components = []common.ApplicationComponent{
|
||||
@@ -340,6 +348,11 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
|
||||
}
|
||||
|
||||
mergedTarget := mergeTargets(currentTargets, targets)
|
||||
if len(mergedTarget) == 0 {
|
||||
errMsg := "no policy (no targets found) to sync configs"
|
||||
klog.InfoS(errMsg, "project", project)
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
mergedPolicies := make([]v1beta1.AppPolicy, len(mergedTarget))
|
||||
for i, t := range mergedTarget {
|
||||
properties, err := json.Marshal(t)
|
||||
@@ -355,21 +368,27 @@ func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, t
|
||||
}
|
||||
}
|
||||
app.Spec.Policies = mergedPolicies
|
||||
out, _ := yaml.Marshal(app)
|
||||
fmt.Println(string(out))
|
||||
|
||||
return k8sClient.Update(ctx, app)
|
||||
}
|
||||
|
||||
func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.ClusterTarget) []ApplicationDeployTarget {
|
||||
var mergedTargets []ApplicationDeployTarget
|
||||
var (
|
||||
mergedTargets []ApplicationDeployTarget
|
||||
// make sure the clusters of target with same namespace are merged
|
||||
clusterTargets = convertClusterTargets(targets)
|
||||
)
|
||||
|
||||
for _, c := range currentTargets {
|
||||
var hasSameNamespace bool
|
||||
for _, t := range targets {
|
||||
for _, t := range clusterTargets {
|
||||
if c.Namespace == t.Namespace {
|
||||
hasSameNamespace = true
|
||||
clusters := append(c.Clusters, t.ClusterName)
|
||||
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace, Clusters: clusters})
|
||||
clusters := set.NewSetFromSlice(stringToInterfaceSlice(t.Clusters))
|
||||
for _, cluster := range c.Clusters {
|
||||
clusters.Add(cluster)
|
||||
}
|
||||
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace,
|
||||
Clusters: interfaceToStringSlice(clusters.ToSlice())})
|
||||
}
|
||||
}
|
||||
if !hasSameNamespace {
|
||||
@@ -377,7 +396,7 @@ func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.Clu
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range targets {
|
||||
for _, t := range clusterTargets {
|
||||
var hasSameNamespace bool
|
||||
for _, c := range currentTargets {
|
||||
if c.Namespace == t.Namespace {
|
||||
@@ -385,9 +404,75 @@ func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.Clu
|
||||
}
|
||||
}
|
||||
if !hasSameNamespace {
|
||||
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: t.Namespace, Clusters: []string{t.ClusterName}})
|
||||
mergedTargets = append(mergedTargets, t)
|
||||
}
|
||||
}
|
||||
|
||||
return mergedTargets
|
||||
}
|
||||
|
||||
func convertClusterTargets(targets []*model.ClusterTarget) []ApplicationDeployTarget {
|
||||
type Target struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Clusters []interface{} `json:"clusters"`
|
||||
}
|
||||
|
||||
var (
|
||||
clusterTargets []Target
|
||||
namespaceSet = set.NewSet()
|
||||
)
|
||||
|
||||
for i := 0; i < len(targets); i++ {
|
||||
clusters := set.NewSet(targets[i].ClusterName)
|
||||
for j := i + 1; j < len(targets); j++ {
|
||||
if targets[i].Namespace == targets[j].Namespace {
|
||||
clusters.Add(targets[j].ClusterName)
|
||||
}
|
||||
}
|
||||
if namespaceSet.Contains(targets[i].Namespace) {
|
||||
continue
|
||||
}
|
||||
clusterTargets = append(clusterTargets, Target{
|
||||
Namespace: targets[i].Namespace,
|
||||
Clusters: clusters.ToSlice(),
|
||||
})
|
||||
namespaceSet.Add(targets[i].Namespace)
|
||||
}
|
||||
|
||||
t := make([]ApplicationDeployTarget, len(clusterTargets))
|
||||
for i, ct := range clusterTargets {
|
||||
t[i] = ApplicationDeployTarget{
|
||||
Namespace: ct.Namespace,
|
||||
Clusters: interfaceToStringSlice(ct.Clusters),
|
||||
}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func interfaceToStringSlice(i []interface{}) []string {
|
||||
var s []string
|
||||
for _, v := range i {
|
||||
s = append(s, v.(string))
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func stringToInterfaceSlice(i []string) []interface{} {
|
||||
var s []interface{}
|
||||
for _, v := range i {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// destroySyncConfigsApp will delete the application which is used to sync configs
|
||||
func destroySyncConfigsApp(ctx context.Context, k8sClient client.Client, project string) error {
|
||||
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
|
||||
var app = &v1beta1.Application{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return k8sClient.Delete(ctx, app)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
"gotest.tools/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/rest"
|
||||
@@ -32,6 +34,7 @@ import (
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/definition"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
@@ -338,3 +341,249 @@ func TestGetConfigs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeTargets(t *testing.T) {
|
||||
currentTargets := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c1", "c2"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
},
|
||||
}
|
||||
targets := []*model.ClusterTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c4",
|
||||
}, {
|
||||
Namespace: "n1",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
{
|
||||
Namespace: "n2",
|
||||
ClusterName: "c3",
|
||||
},
|
||||
}
|
||||
|
||||
expected := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c1", "c2", "c5"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
}, {
|
||||
Namespace: "n3",
|
||||
Clusters: []string{"c4"},
|
||||
},
|
||||
}
|
||||
|
||||
got := mergeTargets(currentTargets, targets)
|
||||
|
||||
for i, g := range got {
|
||||
clusters := g.Clusters
|
||||
sort.SliceStable(clusters, func(i, j int) bool {
|
||||
return clusters[i] < clusters[j]
|
||||
})
|
||||
got[i].Clusters = clusters
|
||||
}
|
||||
assert.DeepEqual(t, expected, got)
|
||||
}
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
targets := []*model.ClusterTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c4",
|
||||
}, {
|
||||
Namespace: "n1",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
{
|
||||
Namespace: "n2",
|
||||
ClusterName: "c3",
|
||||
},
|
||||
{
|
||||
Namespace: "n3",
|
||||
ClusterName: "c5",
|
||||
},
|
||||
}
|
||||
|
||||
expected := []ApplicationDeployTarget{
|
||||
{
|
||||
Namespace: "n3",
|
||||
Clusters: []string{"c4", "c5"},
|
||||
},
|
||||
{
|
||||
Namespace: "n1",
|
||||
Clusters: []string{"c5"},
|
||||
}, {
|
||||
Namespace: "n2",
|
||||
Clusters: []string{"c3"},
|
||||
},
|
||||
}
|
||||
|
||||
got := convertClusterTargets(targets)
|
||||
|
||||
for i, g := range got {
|
||||
clusters := g.Clusters
|
||||
sort.SliceStable(clusters, func(i, j int) bool {
|
||||
return clusters[i] < clusters[j]
|
||||
})
|
||||
got[i].Clusters = clusters
|
||||
}
|
||||
assert.DeepEqual(t, expected, got)
|
||||
}
|
||||
|
||||
func TestDestroySyncConfigsApp(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
app1 := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config-sync-p1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
}
|
||||
k8sClient1 := fake.NewClientBuilder().WithScheme(s).WithObjects(app1).Build()
|
||||
|
||||
k8sClient2 := fake.NewClientBuilder().Build()
|
||||
|
||||
type args struct {
|
||||
project string
|
||||
k8sClient client.Client
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"found": {
|
||||
args: args{
|
||||
project: "p1",
|
||||
k8sClient: k8sClient1,
|
||||
},
|
||||
},
|
||||
"not found": {
|
||||
args: args{
|
||||
project: "p1",
|
||||
k8sClient: k8sClient2,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "no kind is registered for the type v1beta1.Application",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := destroySyncConfigsApp(ctx, tc.args.k8sClient, tc.args.project)
|
||||
if err != nil || tc.want.errMsg != "" {
|
||||
if !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncConfigs(t *testing.T) {
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
secret1 := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "s1",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Labels: map[string]string{
|
||||
types.LabelConfigCatalog: velaCoreConfig,
|
||||
types.LabelConfigProject: "p1",
|
||||
types.LabelConfigSyncToMultiCluster: "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
policies := []ApplicationDeployTarget{{
|
||||
Namespace: "n9",
|
||||
Clusters: []string{"c19"},
|
||||
}}
|
||||
properties, _ := json.Marshal(policies)
|
||||
app1 := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config-sync-p2",
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{{
|
||||
Name: "c19",
|
||||
Type: "topology",
|
||||
Properties: &runtime.RawExtension{Raw: properties},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(secret1, app1).Build()
|
||||
|
||||
type args struct {
|
||||
project string
|
||||
targets []*model.ClusterTarget
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
args: args{
|
||||
project: "p1",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
args: args{
|
||||
project: "p2",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip config sync",
|
||||
args: args{
|
||||
project: "p3",
|
||||
targets: []*model.ClusterTarget{{
|
||||
ClusterName: "c1",
|
||||
Namespace: "n1",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := SyncConfigs(ctx, k8sClient, tc.args.project, tc.args.targets)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.ErrorContains(t, err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,13 @@ import (
|
||||
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/helm"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
types2 "k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
@@ -60,33 +63,65 @@ type defaultHelmHandler struct {
|
||||
k8sClient client.Client
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) ListChartNames(ctx context.Context, url string, secretName string, skipCache bool) ([]string, error) {
|
||||
// TODO(wangyikewxgm): support authority helm repo
|
||||
charts, err := d.helper.ListChartsFromRepo(url, skipCache)
|
||||
func (d defaultHelmHandler) ListChartNames(ctx context.Context, repoURL string, secretName string, skipCache bool) ([]string, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
charts, err := d.helper.ListChartsFromRepo(repoURL, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", url, err.Error())
|
||||
log.Logger.Errorf("cannot fetch charts repo: %s, error: %s", utils.Sanitize(repoURL), err.Error())
|
||||
return nil, bcode.ErrListHelmChart
|
||||
}
|
||||
return charts, nil
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, url string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error) {
|
||||
chartVersions, err := d.helper.ListVersions(url, chartName, skipCache)
|
||||
func (d defaultHelmHandler) ListChartVersions(ctx context.Context, repoURL string, chartName string, secretName string, skipCache bool) (repo.ChartVersions, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
chartVersions, err := d.helper.ListVersions(repoURL, chartName, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", url, chartName, err.Error())
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), err.Error())
|
||||
return nil, bcode.ErrListHelmVersions
|
||||
}
|
||||
if len(chartVersions) == 0 {
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", url, chartName)
|
||||
log.Logger.Errorf("cannot fetch chart versions repo: %s, chart: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName))
|
||||
return nil, bcode.ErrChartNotExist
|
||||
}
|
||||
return chartVersions, nil
|
||||
}
|
||||
|
||||
func (d defaultHelmHandler) GetChartValues(ctx context.Context, url string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error) {
|
||||
v, err := d.helper.GetValuesFromChart(url, chartName, version, skipCache)
|
||||
func (d defaultHelmHandler) GetChartValues(ctx context.Context, repoURL string, chartName string, version string, secretName string, skipCache bool) (map[string]interface{}, error) {
|
||||
if !utils.IsValidURL(repoURL) {
|
||||
return nil, bcode.ErrRepoInvalidURL
|
||||
}
|
||||
var opts *common.HTTPOption
|
||||
var err error
|
||||
if len(secretName) != 0 {
|
||||
opts, err = helm.SetBasicAuthInfo(ctx, d.k8sClient, types2.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: secretName})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrRepoBasicAuth
|
||||
}
|
||||
}
|
||||
v, err := d.helper.GetValuesFromChart(repoURL, chartName, version, skipCache, opts)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", url, chartName, version, err.Error())
|
||||
log.Logger.Errorf("cannot fetch chart values repo: %s, chart: %s, version: %s, error: %s", utils.Sanitize(repoURL), utils.Sanitize(chartName), utils.Sanitize(version), err.Error())
|
||||
return nil, bcode.ErrGetChartValues
|
||||
}
|
||||
res := make(map[string]interface{}, len(v))
|
||||
|
||||
@@ -19,6 +19,10 @@ package usecase
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
@@ -104,6 +108,90 @@ var _ = Describe("Test helm repo list", func() {
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("test helm usecasae", func() {
|
||||
ctx := context.Background()
|
||||
var repoSec v1.Secret
|
||||
|
||||
BeforeEach(func() {
|
||||
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
|
||||
|
||||
repoSec = v1.Secret{}
|
||||
Expect(yaml.Unmarshal([]byte(repoSecret), &repoSec)).Should(BeNil())
|
||||
Expect(k8sClient.Create(ctx, &repoSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(k8sClient.Delete(ctx, &repoSec)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("helm associated usecase interface test", func() {
|
||||
var mockServer *httptest.Server
|
||||
|
||||
handler := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
u, p, ok := request.BasicAuth()
|
||||
if !ok || u != "admin" || p != "admin" {
|
||||
writer.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case request.URL.Path == "/index.yaml":
|
||||
index, err := ioutil.ReadFile("./testdata/helm/index.yaml")
|
||||
indexFile := string(index)
|
||||
indexFile = strings.ReplaceAll(indexFile, "server-url", mockServer.URL)
|
||||
if err != nil {
|
||||
writer.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write([]byte(indexFile))
|
||||
|
||||
return
|
||||
case strings.Contains(request.URL.Path, "mysql-8.8.23.tgz"):
|
||||
pkg, err := ioutil.ReadFile("./testdata/helm/mysql-8.8.23.tgz")
|
||||
if err != nil {
|
||||
writer.Write([]byte(err.Error()))
|
||||
return
|
||||
}
|
||||
writer.Write(pkg)
|
||||
return
|
||||
default:
|
||||
writer.Write([]byte("404 page not found"))
|
||||
}
|
||||
})
|
||||
|
||||
mockServer = httptest.NewServer(handler)
|
||||
|
||||
defer mockServer.Close()
|
||||
|
||||
u := NewHelmUsecase()
|
||||
charts, err := u.ListChartNames(ctx, mockServer.URL, "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(charts)).Should(BeEquivalentTo(1))
|
||||
Expect(charts[0]).Should(BeEquivalentTo("mysql"))
|
||||
|
||||
versions, err := u.ListChartVersions(ctx, mockServer.URL, "mysql", "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(len(versions)).Should(BeEquivalentTo(1))
|
||||
Expect(versions[0].Version).Should(BeEquivalentTo("8.8.23"))
|
||||
|
||||
values, err := u.GetChartValues(ctx, mockServer.URL, "mysql", "8.8.23", "repo-secret", false)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(values).ShouldNot(BeNil())
|
||||
Expect(len(values)).ShouldNot(BeEquivalentTo(0))
|
||||
})
|
||||
|
||||
It("coverage not secret notExist error", func() {
|
||||
u := NewHelmUsecase()
|
||||
_, err := u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
|
||||
_, err = u.ListChartVersions(ctx, "http://127.0.0.1:8080", "mysql", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
|
||||
_, err = u.GetChartValues(ctx, "http://127.0.0.1:8080", "mysql", "8.8.23", "repo-secret-notExist", false)
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var (
|
||||
src = `{
|
||||
"OAMSpecVer":"v0.2",
|
||||
@@ -266,5 +354,19 @@ metadata:
|
||||
stringData:
|
||||
url: https://kedacore.github.io/charts
|
||||
type: Opaque
|
||||
`
|
||||
repoSecret = `
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: repo-secret
|
||||
namespace: vela-system
|
||||
labels:
|
||||
config.oam.dev/type: config-helm-repository
|
||||
config.oam.dev/project: my-project-2
|
||||
stringData:
|
||||
username: admin
|
||||
password: admin
|
||||
type: Opaque
|
||||
`
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -118,6 +118,13 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -137,7 +144,12 @@ func (u systemInfoUsecaseImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
|
||||
}
|
||||
|
||||
func (u systemInfoUsecaseImpl) Init(ctx context.Context) error {
|
||||
_, err := initDexConfig(ctx, u.kubeClient, "http://velaux.com", &model.SystemInfo{})
|
||||
info, err := u.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedKey = info.InstallID
|
||||
_, err = initDexConfig(ctx, u.kubeClient, "http://velaux.com", &model.SystemInfo{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -158,6 +170,9 @@ func generateDexConfig(ctx context.Context, kubeClient client.Client, velaAddres
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(connectors) < 1 {
|
||||
return bcode.ErrNoDexConnector
|
||||
}
|
||||
config, err := model.NewJSONStructByStruct(info.DexConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
@@ -37,4 +37,6 @@ var (
|
||||
ErrInvalidDexConfig = NewBcode(400, 12009, "the dex config is invalid")
|
||||
// ErrRefreshTokenExpired is the error of refresh token expired
|
||||
ErrRefreshTokenExpired = NewBcode(400, 12010, "the refresh token is expired")
|
||||
// ErrNoDexConnector is the error of no dex connector
|
||||
ErrNoDexConnector = NewBcode(400, 12011, "there is no dex connector")
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -35,4 +35,6 @@ var (
|
||||
ErrUsernameNotExist = NewBcode(401, 14008, "the username is not exist")
|
||||
// ErrDexNotFound is the error of dex not found
|
||||
ErrDexNotFound = NewBcode(200, 14009, "the dex is not found")
|
||||
// ErrEmptyAdminEmail is the error of empty admin email
|
||||
ErrEmptyAdminEmail = NewBcode(400, 14010, "the admin email is empty, please set the admin email before using sso login")
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
@@ -151,7 +150,10 @@ func (s *configWebService) createConfig(req *restful.Request, res *restful.Respo
|
||||
|
||||
err := s.handler.CreateConfig(req.Request.Context(), createReq)
|
||||
if err != nil {
|
||||
log.Logger.Errorf("failed to create config: %s", err.Error())
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ const (
|
||||
// resource for use in a three way diff during a patching apply
|
||||
AnnotationLastAppliedConfig = "app.oam.dev/last-applied-configuration"
|
||||
|
||||
// AnnotationLastAppliedTime indicates the last applied time
|
||||
AnnotationLastAppliedTime = "app.oam.dev/last-applied-time"
|
||||
|
||||
// AnnotationAppRollout indicates that the application is still rolling out
|
||||
// the application controller should treat it differently
|
||||
AnnotationAppRollout = "app.oam.dev/rollout-template"
|
||||
|
||||
@@ -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
|
||||
@@ -312,9 +321,17 @@ func clusterObjectReferenceTypeFilterGenerator(allowedKinds ...string) clusterOb
|
||||
var isWorkloadClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment", "StatefulSet", "CloneSet", "Job", "Configuration")
|
||||
var isPortForwardEndpointClusterObjectReferenceFilter = clusterObjectReferenceTypeFilterGenerator("Deployment",
|
||||
"StatefulSet", "CloneSet", "Job", "Service", "HelmRelease")
|
||||
var resourceNameClusterObjectReferenceFilter = func(resourceName string) clusterObjectReferenceFilter {
|
||||
var resourceNameClusterObjectReferenceFilter = func(resourceName []string) clusterObjectReferenceFilter {
|
||||
return func(reference common.ClusterObjectReference) bool {
|
||||
return resourceName == reference.Name
|
||||
if len(resourceName) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, r := range resourceName {
|
||||
if r == reference.Name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,24 +438,31 @@ func filterClusterObjectRefFromAddonObservability(resources []common.ClusterObje
|
||||
return resources
|
||||
}
|
||||
|
||||
func removeEmptyString(items []string) []string {
|
||||
r := []string{}
|
||||
for _, i := range items {
|
||||
if i != "" {
|
||||
r = append(r, i)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// AskToChooseOneEnvResource will ask users to select one applied resource of the application if more than one
|
||||
// resource is a map for component to applied resources
|
||||
// return the selected ClusterObjectReference
|
||||
func AskToChooseOneEnvResource(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
|
||||
filters := []clusterObjectReferenceFilter{isWorkloadClusterObjectReferenceFilter}
|
||||
for _, n := range resourceName {
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(n))
|
||||
}
|
||||
_resourceName := removeEmptyString(resourceName)
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
|
||||
return askToChooseOneResource(app, filters...)
|
||||
}
|
||||
|
||||
// AskToChooseOnePortForwardEndpoint will ask user to select one applied resource as port forward endpoint
|
||||
func AskToChooseOnePortForwardEndpoint(app *v1beta1.Application, resourceName ...string) (*common.ClusterObjectReference, error) {
|
||||
filters := []clusterObjectReferenceFilter{isPortForwardEndpointClusterObjectReferenceFilter}
|
||||
for _, n := range resourceName {
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(n))
|
||||
}
|
||||
|
||||
_resourceName := removeEmptyString(resourceName)
|
||||
filters = append(filters, resourceNameClusterObjectReferenceFilter(_resourceName))
|
||||
return askToChooseOneResource(app, filters...)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,58 +168,13 @@ func (p *provider) ExpandTopology(ctx wfContext.Context, v *value.Value, act wfT
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
policies := &[]*v1beta1.AppPolicy{}
|
||||
policies := &[]v1beta1.AppPolicy{}
|
||||
if err = policiesRaw.UnmarshalTo(policies); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse policies")
|
||||
}
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
placementMap := map[string]struct{}{}
|
||||
addCluster := func(cluster string, ns string, validateCluster bool) error {
|
||||
if validateCluster {
|
||||
if _, e := multicluster.GetVirtualCluster(context.Background(), p, cluster); e != nil {
|
||||
return errors.Wrapf(e, "failed to get cluster %s", cluster)
|
||||
}
|
||||
}
|
||||
if !resourcekeeper.AllowCrossNamespaceResource && (ns != p.app.GetNamespace() && ns != "") {
|
||||
return errors.Errorf("cannot cross namespace")
|
||||
}
|
||||
placement := v1alpha1.PlacementDecision{Cluster: cluster, Namespace: ns}
|
||||
name := placement.String()
|
||||
if _, found := placementMap[name]; !found {
|
||||
placementMap[name] = struct{}{}
|
||||
placements = append(placements, placement)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, policy := range *policies {
|
||||
if policy.Type == v1alpha1.TopologyPolicyType {
|
||||
topologySpec := &v1alpha1.TopologyPolicySpec{}
|
||||
if err := utils.StrictUnmarshal(policy.Properties.Raw, topologySpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse topology policy %s", policy.Name)
|
||||
}
|
||||
clusterLabelSelector := pkgpolicy.GetClusterLabelSelectorInTopology(topologySpec)
|
||||
switch {
|
||||
case topologySpec.Clusters != nil:
|
||||
for _, cluster := range topologySpec.Clusters {
|
||||
if err := addCluster(cluster, topologySpec.Namespace, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clusterLabelSelector != nil:
|
||||
clusters, err := multicluster.FindVirtualClustersByLabels(context.Background(), p, clusterLabelSelector)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to find clusters in topology %s", policy.Name)
|
||||
}
|
||||
if len(clusters) == 0 {
|
||||
return errors.Errorf("failed to find any cluster matches given labels")
|
||||
}
|
||||
for _, cluster := range clusters {
|
||||
if err = addCluster(cluster.Name, topologySpec.Namespace, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(context.Background(), p, p.app, *policies, resourcekeeper.AllowCrossNamespaceResource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.FillObject(placements, "outputs", "decisions")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -442,6 +443,7 @@ func generateAddonInfo(name string, status pkgaddon.Status) string {
|
||||
for c := range status.Clusters {
|
||||
ic = append(ic, c)
|
||||
}
|
||||
sort.Strings(ic)
|
||||
res += fmt.Sprintf("installedClusters: %s \n", ic)
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ func getPrompt(cmd *cobra.Command, reader *bufio.Reader, description string, pro
|
||||
|
||||
func loadYAMLBytesFromFileOrHTTP(pathOrURL string) ([]byte, error) {
|
||||
if strings.HasPrefix(pathOrURL, "http://") || strings.HasPrefix(pathOrURL, "https://") {
|
||||
return common.HTTPGet(context.Background(), pathOrURL)
|
||||
return common.HTTPGetWithOption(context.Background(), pathOrURL, nil)
|
||||
}
|
||||
return os.ReadFile(path.Clean(pathOrURL))
|
||||
}
|
||||
@@ -295,7 +295,7 @@ func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, p
|
||||
}
|
||||
|
||||
switch provider {
|
||||
case "aws", "azure", "alibaba", "tencent", "gcp", "baidu", "elastic":
|
||||
case "aws", "azure", "alibaba", "tencent", "gcp", "baidu", "elastic", "ucloud":
|
||||
var terraform *commontype.Terraform
|
||||
|
||||
git, err := cmd.Flags().GetString(FlagGit)
|
||||
@@ -371,7 +371,7 @@ func generateTerraformTypedComponentDefinition(cmd *cobra.Command, name, kind, p
|
||||
}
|
||||
return out.String(), nil
|
||||
default:
|
||||
return "", errors.Errorf("Provider `%s` is not supported. Only `alibaba`, `aws`, `azure` are supported.", provider)
|
||||
return "", errors.Errorf("Provider `%s` is not supported. Only `alibaba`, `aws`, `azure`, `gcp`, `baidu`, `tencent`, `elastic`, `ucloud` are supported.", provider)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ func NewInstallCommand(c common.Args, order string, ioStreams util.IOStreams) *c
|
||||
if installArgs.ChartFilePath == "" {
|
||||
installArgs.ChartFilePath = getKubeVelaHelmChartRepoURL(installArgs.Version)
|
||||
}
|
||||
chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath)
|
||||
chart, err := installArgs.helmHelper.LoadCharts(installArgs.ChartFilePath, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loadding the helm chart of kubeVela control plane failure, %w", err)
|
||||
}
|
||||
|
||||
@@ -47,8 +47,6 @@ const (
|
||||
// ProviderNamespace is the namespace of Terraform Cloud Provider
|
||||
ProviderNamespace = "default"
|
||||
|
||||
labelVal = "terraform-provider"
|
||||
|
||||
providerNameParam = "name"
|
||||
)
|
||||
|
||||
@@ -63,24 +61,24 @@ func NewProviderCommand(c common.Args, order string, ioStreams cmdutil.IOStreams
|
||||
types.TagCommandType: types.TypeExtension,
|
||||
},
|
||||
}
|
||||
add, err := prepareProviderAddCommand(c)
|
||||
add, err := prepareProviderAddCommand(c, ioStreams)
|
||||
if err == nil {
|
||||
cmd.AddCommand(add)
|
||||
}
|
||||
|
||||
delete, err := prepareProviderDeleteCommand(c)
|
||||
delete, err := prepareProviderDeleteCommand(c, ioStreams)
|
||||
if err == nil {
|
||||
cmd.AddCommand(delete)
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
NewProviderListCommand(c),
|
||||
NewProviderListCommand(c, ioStreams),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewProviderListCommand create addon list command
|
||||
func NewProviderListCommand(c common.Args) *cobra.Command {
|
||||
func NewProviderListCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
@@ -91,7 +89,7 @@ func NewProviderListCommand(c common.Args) *cobra.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = listProviders(context.Background(), k8sClient)
|
||||
err = listProviders(context.Background(), k8sClient, ioStreams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -100,7 +98,7 @@ func NewProviderListCommand(c common.Args) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
|
||||
func prepareProviderAddCommand(c common.Args, ioStreams cmdutil.IOStreams) (*cobra.Command, error) {
|
||||
ctx := context.Background()
|
||||
k8sClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
@@ -114,7 +112,7 @@ func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
|
||||
Example: "vela provider add <provider-type>",
|
||||
}
|
||||
|
||||
addSubCommands, err := prepareProviderAddSubCommand(c)
|
||||
addSubCommands, err := prepareProviderAddSubCommand(c, ioStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -153,7 +151,7 @@ func prepareProviderAddCommand(c common.Args) (*cobra.Command, error) {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
|
||||
func prepareProviderAddSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
|
||||
ctx := context.Background()
|
||||
k8sClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
@@ -188,7 +186,7 @@ func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if value == "" {
|
||||
if value == "" && p.Required {
|
||||
return fmt.Errorf("must specify a value for %s", p.Name)
|
||||
}
|
||||
properties[p.Name] = value
|
||||
@@ -219,9 +217,12 @@ func prepareProviderAddSubCommand(c common.Args) ([]*cobra.Command, error) {
|
||||
},
|
||||
}
|
||||
if err := k8sClient.Create(ctx, a); err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to authentiate Terraform cloud provier %s", providerType)
|
||||
}
|
||||
ioStreams.Infof("Successfully authentiate provider %s for %s\n", name, providerType)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to authentiate Terraform cloud provier %s", providerType)
|
||||
}
|
||||
return fmt.Errorf("terraform provider %s for %s already exists", name, providerType)
|
||||
}
|
||||
@@ -252,7 +253,7 @@ type ProviderMeta struct {
|
||||
Age string
|
||||
}
|
||||
|
||||
func listProviders(ctx context.Context, k8sClient client.Client) error {
|
||||
func listProviders(ctx context.Context, k8sClient client.Client, ioStreams cmdutil.IOStreams) error {
|
||||
var (
|
||||
providers []ProviderMeta
|
||||
currentProviders []tcv1beta1.Provider
|
||||
@@ -265,7 +266,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
|
||||
}
|
||||
|
||||
for _, p := range tcProviders.Items {
|
||||
if p.Labels["config.oam.dev/type"] == labelVal {
|
||||
if p.Labels["config.oam.dev/type"] == types.TerraformProvider {
|
||||
currentProviders = append(currentProviders, p)
|
||||
} else {
|
||||
// if not labeled, the provider is manually created or created by `vela addon enable`.
|
||||
@@ -276,7 +277,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
|
||||
defs, err := getTerraformProviderTypes(ctx, k8sClient)
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return errors.New("no Terraform Cloud Provider found, please run `vela addon enable` first")
|
||||
ioStreams.Info("no Terraform Cloud Provider found, please run `vela addon enable` first")
|
||||
}
|
||||
return errors.Wrap(err, "failed to retrieve providers")
|
||||
}
|
||||
@@ -318,7 +319,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
|
||||
for _, p := range providers {
|
||||
table.AddRow(p.Type, p.Name, p.Age)
|
||||
}
|
||||
fmt.Println(table.String())
|
||||
ioStreams.Info(table.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -327,7 +328,7 @@ func listProviders(ctx context.Context, k8sClient client.Client) error {
|
||||
func getTerraformProviderTypes(ctx context.Context, k8sClient client.Client) ([]v1beta1.ComponentDefinition, error) {
|
||||
defs := &v1beta1.ComponentDefinitionList{}
|
||||
if err := k8sClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
|
||||
client.MatchingLabels{definition.UserPrefix + "type.config.oam.dev": labelVal}); err != nil {
|
||||
client.MatchingLabels{definition.UserPrefix + "type.config.oam.dev": types.TerraformProvider}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return defs.Items, nil
|
||||
@@ -343,7 +344,7 @@ func getTerraformProviderType(ctx context.Context, k8sClient client.Client, name
|
||||
return def, nil
|
||||
}
|
||||
|
||||
func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
|
||||
func prepareProviderDeleteCommand(c common.Args, ioStreams cmdutil.IOStreams) (*cobra.Command, error) {
|
||||
ctx := context.Background()
|
||||
k8sClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
@@ -358,7 +359,7 @@ func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
|
||||
Example: "vela provider delete <provider-type> -name <provider-name>",
|
||||
}
|
||||
|
||||
deleteSubCommands, err := prepareProviderDeleteSubCommand(c)
|
||||
deleteSubCommands, err := prepareProviderDeleteSubCommand(c, ioStreams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -397,7 +398,7 @@ func prepareProviderDeleteCommand(c common.Args) (*cobra.Command, error) {
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
func prepareProviderDeleteSubCommand(c common.Args) ([]*cobra.Command, error) {
|
||||
func prepareProviderDeleteSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
|
||||
ctx := context.Background()
|
||||
k8sClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
@@ -438,7 +439,7 @@ func prepareProviderDeleteSubCommand(c common.Args) ([]*cobra.Command, error) {
|
||||
if err := k8sClient.Delete(ctx, a); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Successfully delete provider %s for %s\n", name, providerType)
|
||||
ioStreams.Infof("Successfully delete provider %s for %s\n", name, providerType)
|
||||
return nil
|
||||
}
|
||||
cmds[i] = cmd
|
||||
|
||||
95
references/cli/provider_test.go
Normal file
95
references/cli/provider_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
)
|
||||
|
||||
func TestLlistProviders(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
k8sClient client.Client
|
||||
}
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
corev1.AddToScheme(s)
|
||||
terraformapi.AddToScheme(s)
|
||||
p1 := &terraformapi.Provider{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Provider",
|
||||
APIVersion: "terraform.core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"config.oam.dev/type": types.TerraformProvider,
|
||||
},
|
||||
},
|
||||
}
|
||||
p2 := &terraformapi.Provider{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Provider",
|
||||
APIVersion: "terraform.core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p2",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(p1, p2).Build()
|
||||
|
||||
ioStream := util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
|
||||
|
||||
testcases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"success": {
|
||||
args: args{
|
||||
k8sClient: k8sClient,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
err := listProviders(ctx, tc.args.k8sClient, ioStream)
|
||||
if err != nil || tc.want.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,10 @@ func NewRevisionListCommand(c common.Args) *cobra.Command {
|
||||
status = "Succeeded"
|
||||
case rev.Status.Workflow.Terminated || rev.Status.Workflow.Suspend || rev.Status.Workflow.Finished:
|
||||
status = "Failed"
|
||||
default:
|
||||
case app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == rev.Name:
|
||||
status = "Executing"
|
||||
default:
|
||||
status = "Failed"
|
||||
}
|
||||
}
|
||||
if labels := rev.GetLabels(); labels != nil {
|
||||
|
||||
@@ -27,12 +27,20 @@ import (
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
"k8s.io/utils/pointer"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
pkgappfile "github.com/oam-dev/kubevela/pkg/appfile"
|
||||
"github.com/oam-dev/kubevela/pkg/multicluster"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
"github.com/oam-dev/kubevela/pkg/policy"
|
||||
"github.com/oam-dev/kubevela/pkg/resourcetracker"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile"
|
||||
@@ -103,6 +111,9 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if printTree, err := cmd.Flags().GetBool("tree"); err == nil && printTree {
|
||||
return printApplicationTree(c, cmd, appName, namespace)
|
||||
}
|
||||
newClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -125,8 +136,10 @@ func NewAppStatusCommand(c common.Args, order string, ioStreams cmdutil.IOStream
|
||||
cmd.Flags().StringP("svc", "s", "", "service name")
|
||||
cmd.Flags().BoolP("endpoint", "p", false, "show all service endpoints of the application")
|
||||
cmd.Flags().StringP("component", "c", "", "filter service endpoints by component name")
|
||||
cmd.Flags().BoolP("tree", "t", false, "display the application resources into tree structure")
|
||||
cmd.Flags().BoolP("detail", "d", false, "display the realtime details of application resources")
|
||||
cmd.Flags().StringP("detail-format", "", "inline", "the format for displaying details. Can be one of inline (default), wide, list, table, raw.")
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
cmd.SetOut(ioStreams.Out)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -344,3 +357,65 @@ func getAppPhaseColor(appPhase commontypes.ApplicationPhase) *color.Color {
|
||||
}
|
||||
return yellow
|
||||
}
|
||||
|
||||
func printApplicationTree(c common.Args, cmd *cobra.Command, appName string, appNs string) error {
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Wrap(multicluster.NewSecretModeMultiClusterRoundTripper)
|
||||
cli, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pd, err := c.GetPackageDiscover()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dm, err := discoverymapper.New(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app, err := loadRemoteApplication(cli, appNs, appName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc, err := multicluster.GetClusterGatewayService(context.Background(), cli)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get cluster secret namespace, please ensure cluster gateway is correctly deployed")
|
||||
}
|
||||
multicluster.ClusterGatewaySecretNamespace = svc.Namespace
|
||||
clusterMapper, err := multicluster.NewClusterMapper(ctx, cli)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get cluster mapper")
|
||||
}
|
||||
|
||||
var placements []v1alpha1.PlacementDecision
|
||||
af, err := pkgappfile.NewApplicationParser(cli, dm, pd).GenerateAppFile(context.Background(), app)
|
||||
if err == nil {
|
||||
placements, _ = policy.GetPlacementsFromTopologyPolicies(context.Background(), cli, app, af.Policies, true)
|
||||
}
|
||||
format, _ := cmd.Flags().GetString("detail-format")
|
||||
var maxWidth *int
|
||||
if w, _, err := term.GetSize(0); err == nil && w > 0 {
|
||||
maxWidth = pointer.Int(w)
|
||||
}
|
||||
options := resourcetracker.ResourceTreePrintOptions{MaxWidth: maxWidth, Format: format, ClusterMapper: clusterMapper}
|
||||
printDetails, _ := cmd.Flags().GetBool("detail")
|
||||
if printDetails {
|
||||
msgRetriever, err := resourcetracker.RetrieveKubeCtlGetMessageGenerator(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
options.DetailRetriever = msgRetriever
|
||||
}
|
||||
options.PrintResourceTree(cmd.OutOrStdout(), placements, currentRT, historyRTs)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -19,71 +19,272 @@ package cli
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
||||
"github.com/oam-dev/kubevela/pkg/component"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
utilcommon "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/common"
|
||||
)
|
||||
|
||||
// UpCommandOptions command args for vela up
|
||||
type UpCommandOptions struct {
|
||||
AppName string
|
||||
Namespace string
|
||||
File string
|
||||
PublishVersion string
|
||||
RevisionName string
|
||||
}
|
||||
|
||||
// Complete fill the args for vela up
|
||||
func (opt *UpCommandOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) {
|
||||
if len(args) > 0 {
|
||||
opt.AppName = args[0]
|
||||
}
|
||||
opt.Namespace = velacmd.GetNamespace(f, cmd)
|
||||
}
|
||||
|
||||
// Validate if vela up args is valid, interrupt the command
|
||||
func (opt *UpCommandOptions) Validate() error {
|
||||
if opt.AppName != "" && opt.File != "" {
|
||||
return errors.Errorf("cannot use app name and file at the same time")
|
||||
}
|
||||
if opt.AppName == "" && opt.File == "" {
|
||||
return errors.Errorf("either app name or file should be set")
|
||||
}
|
||||
if opt.AppName != "" && opt.PublishVersion == "" {
|
||||
return errors.Errorf("publish-version must be set if you want to force existing application to re-run")
|
||||
}
|
||||
if opt.AppName == "" && opt.RevisionName != "" {
|
||||
return errors.Errorf("revision name must be used with application name")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run execute the vela up command
|
||||
func (opt *UpCommandOptions) Run(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
if opt.File != "" {
|
||||
return opt.deployApplicationFromFile(f, cmd)
|
||||
}
|
||||
if opt.RevisionName == "" {
|
||||
return opt.deployExistingApp(f, cmd)
|
||||
}
|
||||
return opt.deployExistingAppUsingRevision(f, cmd)
|
||||
}
|
||||
|
||||
func (opt *UpCommandOptions) deployExistingAppUsingRevision(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
ctx, cli := cmd.Context(), f.Client()
|
||||
app := &v1beta1.Application{}
|
||||
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
|
||||
return err
|
||||
}
|
||||
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
|
||||
return errors.Errorf("current PublishVersion is %s", publishVersion)
|
||||
}
|
||||
// check revision
|
||||
revs, err := application.GetSortedAppRevisions(ctx, cli, opt.AppName, opt.Namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var matchedRev *v1beta1.ApplicationRevision
|
||||
for _, rev := range revs {
|
||||
if rev.Name == opt.RevisionName {
|
||||
matchedRev = rev.DeepCopy()
|
||||
}
|
||||
}
|
||||
if matchedRev == nil {
|
||||
return errors.Errorf("failed to find revision %s matching application %s", opt.RevisionName, opt.AppName)
|
||||
}
|
||||
if app.Status.LatestRevision != nil && app.Status.LatestRevision.Name == opt.RevisionName {
|
||||
return nil
|
||||
}
|
||||
|
||||
// freeze the application
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() {
|
||||
app.Spec = matchedRev.Spec.Application.Spec
|
||||
oam.SetPublishVersion(app, opt.PublishVersion)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to freeze application %s before update", appKey)
|
||||
}
|
||||
|
||||
// create new revision based on the matched revision
|
||||
revName, revisionNum := utils.GetAppNextRevision(app)
|
||||
matchedRev.Name = revName
|
||||
oam.SetPublishVersion(matchedRev, opt.PublishVersion)
|
||||
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(matchedRev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
un := &unstructured.Unstructured{Object: obj}
|
||||
component.ClearRefObjectForDispatch(un)
|
||||
if err = cli.Create(ctx, un); err != nil {
|
||||
return errors.Wrapf(err, "failed to update application %s to create new revision %s", appKey, revName)
|
||||
}
|
||||
|
||||
// update application status to point to the new revision
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
app.Status = apicommon.AppStatus{
|
||||
LatestRevision: &apicommon.Revision{Name: revName, Revision: revisionNum, RevisionHash: matchedRev.GetLabels()[oam.LabelAppRevisionHash]},
|
||||
}
|
||||
return cli.Status().Update(ctx, app)
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to update application %s to use new revision %s", appKey, revName)
|
||||
}
|
||||
|
||||
// unfreeze application
|
||||
if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil {
|
||||
return errors.Wrapf(err, "failed to unfreeze application %s after update", appKey)
|
||||
}
|
||||
|
||||
cmd.Printf("Application updated with new PublishVersion %s using revision %s\n", opt.PublishVersion, opt.RevisionName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt *UpCommandOptions) deployExistingApp(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
ctx, cli := cmd.Context(), f.Client()
|
||||
app := &v1beta1.Application{}
|
||||
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err := cli.Get(ctx, apitypes.NamespacedName{Name: opt.AppName, Namespace: opt.Namespace}, app); err != nil {
|
||||
return err
|
||||
}
|
||||
if publishVersion := oam.GetPublishVersion(app); publishVersion == opt.PublishVersion {
|
||||
return errors.Errorf("current PublishVersion is %s", publishVersion)
|
||||
}
|
||||
oam.SetPublishVersion(app, opt.PublishVersion)
|
||||
return cli.Update(ctx, app)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Printf("Application updated with new PublishVersion %s\n", opt.PublishVersion)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opt *UpCommandOptions) deployApplicationFromFile(f velacmd.Factory, cmd *cobra.Command) error {
|
||||
cli := f.Client()
|
||||
body, err := common.ReadRemoteOrLocalPath(opt.File)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ioStream := util.IOStreams{
|
||||
In: cmd.InOrStdin(),
|
||||
Out: cmd.OutOrStdout(),
|
||||
ErrOut: cmd.ErrOrStderr(),
|
||||
}
|
||||
if common.IsAppfile(body) { // legacy compatibility
|
||||
o := &common.AppfileOptions{Kubecli: cli, IO: ioStream, Namespace: opt.Namespace}
|
||||
return o.Run(opt.File, o.Namespace, utilcommon.Args{Schema: utilcommon.Scheme})
|
||||
}
|
||||
var app v1beta1.Application
|
||||
err = yaml.Unmarshal(body, &app)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
|
||||
}
|
||||
|
||||
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
|
||||
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
|
||||
if opt.Namespace != "" && opt.Namespace != types.DefaultAppNamespace {
|
||||
app.SetNamespace(opt.Namespace)
|
||||
}
|
||||
if opt.PublishVersion != "" {
|
||||
oam.SetPublishVersion(&app, opt.PublishVersion)
|
||||
}
|
||||
err = common.ApplyApplication(app, ioStream, cli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Printf("Application %s/%s applied.\n", app.Namespace, app.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
upLong = templates.LongDesc(i18n.T(`
|
||||
Deploy one application
|
||||
|
||||
Deploy one application based on local files or re-deploy an existing application.
|
||||
With the -n/--namespace flag, you can choose the location of the target application.
|
||||
|
||||
To apply application from file, use the -f/--file flag to specify the application
|
||||
file location.
|
||||
|
||||
To give a particular version to this deploy, use the -v/--publish-version flag. When
|
||||
you are deploying an existing application, the version name must be different from
|
||||
the current name. You can also use a history revision for the deploy and override the
|
||||
current application by using the -r/--revision flag.`))
|
||||
|
||||
upExample = templates.Examples(i18n.T(`
|
||||
# Deploy an application from file
|
||||
vela up -f ./app.yaml
|
||||
|
||||
# Deploy an application with a version name
|
||||
vela up example-app -n example-ns --publish-version beta
|
||||
|
||||
# Deploy an application using existing revision
|
||||
vela up example-app -n example-ns --publish-version beta --revision example-app-v2`))
|
||||
)
|
||||
|
||||
// NewUpCommand will create command for applying an AppFile
|
||||
func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cobra.Command {
|
||||
appFilePath := new(string)
|
||||
func NewUpCommand(f velacmd.Factory, order string) *cobra.Command {
|
||||
o := &UpCommandOptions{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "up",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Apply an appfile or application from file",
|
||||
Long: "Create or update vela application from file or URL, both appfile or application object format are supported.",
|
||||
Short: i18n.T("Deploy one application"),
|
||||
Long: upLong,
|
||||
Example: upExample,
|
||||
Annotations: map[string]string{
|
||||
types.TagCommandOrder: order,
|
||||
types.TagCommandType: types.TypeStart,
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
|
||||
if err != nil {
|
||||
return err
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
o.Complete(f, cmd, args)
|
||||
if o.File == "" {
|
||||
return velacmd.GetApplicationsForCompletion(cmd.Context(), f, o.Namespace, toComplete)
|
||||
}
|
||||
kubecli, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := common.ReadRemoteOrLocalPath(*appFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if common.IsAppfile(body) {
|
||||
o := &common.AppfileOptions{
|
||||
Kubecli: kubecli,
|
||||
IO: ioStream,
|
||||
Namespace: namespace,
|
||||
}
|
||||
return o.Run(*appFilePath, o.Namespace, c)
|
||||
}
|
||||
var app corev1beta1.Application
|
||||
err = yaml.Unmarshal(body, &app)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
|
||||
}
|
||||
|
||||
// Override namespace if namespace flag is set. We should check if namespace is `default` or not
|
||||
// since GetFlagNamespaceOrEnv returns default namespace when failed to get current env.
|
||||
if namespace != "" && namespace != types.DefaultAppNamespace {
|
||||
app.SetNamespace(namespace)
|
||||
}
|
||||
err = common.ApplyApplication(app, ioStream, kubecli)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o.Complete(f, cmd, args)
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run(f, cmd))
|
||||
},
|
||||
}
|
||||
cmd.SetOut(ioStream.Out)
|
||||
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application, it could be a remote url.")
|
||||
cmd.Flags().StringVarP(&o.File, "file", "f", o.File, "The file path for appfile or application. It could be a remote url.")
|
||||
cmd.Flags().StringVarP(&o.PublishVersion, "publish-version", "v", o.PublishVersion, "The publish version for deploying application.")
|
||||
cmd.Flags().StringVarP(&o.RevisionName, "revision", "r", o.RevisionName, "The revision to use for deploying the application, if empty, the current application configuration will be used.")
|
||||
cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
|
||||
"revision",
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var appName string
|
||||
if len(args) > 0 {
|
||||
appName = args[0]
|
||||
}
|
||||
namespace := velacmd.GetNamespace(f, cmd)
|
||||
return velacmd.GetRevisionForCompletion(cmd.Context(), f, appName, namespace, toComplete)
|
||||
}))
|
||||
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
return cmd
|
||||
return velacmd.NewCommandBuilder(f, cmd).
|
||||
WithNamespaceFlag().
|
||||
WithResponsiveWriter().
|
||||
Build()
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
|
||||
"github.com/oam-dev/kubevela/references/common"
|
||||
)
|
||||
|
||||
@@ -128,7 +128,10 @@ spec:
|
||||
}))
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd := NewUpCommand(args, "", util.IOStreams{In: os.Stdin, Out: &buf, ErrOut: &buf})
|
||||
cmd := NewUpCommand(velacmd.NewDefaultFactory(args.GetClient), "")
|
||||
cmd.SetArgs([]string{})
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
if c.namespace != "" {
|
||||
require.NoError(t, cmd.Flags().Set(FlagNamespace, c.namespace))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -384,18 +383,12 @@ func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client
|
||||
cmd.Printf("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n", rev.Name, publishVersion)
|
||||
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
var controllerRequirement string
|
||||
// rollback application spec and freeze
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationPublishVersion, publishVersion)
|
||||
controllerRequirement = app.GetAnnotations()[oam.AnnotationControllerRequirement]
|
||||
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationControllerRequirement, "Not Available")
|
||||
controllerRequirement, err := utils.FreezeApplication(ctx, cli, app, func() {
|
||||
app.Spec = rev.Spec.Application.Spec
|
||||
return cli.Update(ctx, app)
|
||||
}); err != nil {
|
||||
oam.SetPublishVersion(app, publishVersion)
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
|
||||
}
|
||||
cmd.Printf("Application spec rollback successfully.\n")
|
||||
@@ -435,19 +428,7 @@ func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client
|
||||
}
|
||||
|
||||
// unfreeze application
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
annotations := app.GetAnnotations()
|
||||
if controllerRequirement != "" {
|
||||
annotations[oam.AnnotationControllerRequirement] = controllerRequirement
|
||||
} else {
|
||||
delete(annotations, oam.AnnotationControllerRequirement)
|
||||
}
|
||||
app.SetAnnotations(annotations)
|
||||
return cli.Update(ctx, app)
|
||||
}); err != nil {
|
||||
if err = utils.UnfreezeApplication(ctx, cli, app, nil, controllerRequirement); err != nil {
|
||||
return errors.Wrapf(err, "failed to resume application to restart")
|
||||
}
|
||||
cmd.Printf("Application rollback completed.\n")
|
||||
|
||||
@@ -21,8 +21,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/references/a/preimport"
|
||||
"github.com/oam-dev/kubevela/references/cli"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
func main() {
|
||||
preimport.ResumeLogging()
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
_ = utilfeature.DefaultMutableFeatureGate.Set("AllAlpha=true")
|
||||
|
||||
if ns := os.Getenv("DEFAULT_VELA_NS"); len(ns) != 0 {
|
||||
types.DefaultKubeVelaNS = ns
|
||||
|
||||
@@ -257,7 +257,7 @@ func ReadRemoteOrLocalPath(pathOrURL string) ([]byte, error) {
|
||||
var body []byte
|
||||
var err error
|
||||
if utils.IsValidURL(pathOrURL) {
|
||||
body, err = common.HTTPGet(context.Background(), pathOrURL)
|
||||
body, err = common.HTTPGetWithOption(context.Background(), pathOrURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ func HandleTemplate(in *runtime.RawExtension, schematic *commontypes.Schematic,
|
||||
}
|
||||
}
|
||||
if tmp.CueTemplateURI != "" {
|
||||
b, err := common.HTTPGet(context.Background(), tmp.CueTemplateURI)
|
||||
b, err := common.HTTPGetWithOption(context.Background(), tmp.CueTemplateURI, nil)
|
||||
if err != nil {
|
||||
return types.Capability{}, err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -1114,6 +1115,9 @@ func (ref *ParseReference) parseTerraformCapabilityParameters(capability types.C
|
||||
refParameterList = append(refParameterList, refParam)
|
||||
}
|
||||
refParameterList = append(refParameterList, writeConnectionSecretToRefReferenceParameter)
|
||||
sort.SliceStable(refParameterList, func(i, j int) bool {
|
||||
return refParameterList[i].Name < refParameterList[j].Name
|
||||
})
|
||||
|
||||
propertiesTableName := fmt.Sprintf("%s %s", strings.Repeat("#", 3), propertiesTitle)
|
||||
tables = append(tables, ReferenceParameterTable{
|
||||
@@ -1140,6 +1144,9 @@ func (ref *ParseReference) parseTerraformCapabilityParameters(capability types.C
|
||||
writeSecretRefParameterList := []ReferenceParameter{writeSecretRefNameParam, writeSecretRefNameSpaceParam}
|
||||
writeSecretTableName := fmt.Sprintf("%s %s", strings.Repeat("#", 4), terraform.TerraformWriteConnectionSecretToRefName)
|
||||
|
||||
sort.SliceStable(writeSecretRefParameterList, func(i, j int) bool {
|
||||
return writeSecretRefParameterList[i].Name < writeSecretRefParameterList[j].Name
|
||||
})
|
||||
tables = append(tables, ReferenceParameterTable{
|
||||
Name: writeSecretTableName,
|
||||
Parameters: writeSecretRefParameterList,
|
||||
@@ -1153,11 +1160,13 @@ func (ref *ParseReference) parseTerraformCapabilityParameters(capability types.C
|
||||
outputsList = append(outputsList, refParam)
|
||||
}
|
||||
|
||||
sort.SliceStable(outputsList, func(i, j int) bool {
|
||||
return outputsList[i].Name < outputsList[j].Name
|
||||
})
|
||||
outputsTables = append(outputsTables, ReferenceParameterTable{
|
||||
Name: outputsTableName,
|
||||
Parameters: outputsList,
|
||||
})
|
||||
|
||||
return tables, outputsTables, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -203,15 +203,17 @@ var _ = Describe("Test velaQL rest api", func() {
|
||||
}, 2*time.Minute, 3*time.Microsecond).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test collect pod from helmRelease", func() {
|
||||
PIt("Test collect pod from helmRelease", func() {
|
||||
appWithHelm := new(v1beta1.Application)
|
||||
Expect(yaml.Unmarshal([]byte(podInfoApp), appWithHelm)).Should(BeNil())
|
||||
req := apiv1.ApplicationRequest{
|
||||
Components: appWithHelm.Spec.Components,
|
||||
}
|
||||
res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), req)
|
||||
Expect(res).ShouldNot(BeNil())
|
||||
Expect(res.StatusCode).Should(Equal(200))
|
||||
Eventually(func(g Gomega) {
|
||||
res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), req)
|
||||
g.Expect(res).ShouldNot(BeNil())
|
||||
g.Expect(res.StatusCode).Should(Equal(200))
|
||||
}, 1*time.Minute).Should(Succeed())
|
||||
|
||||
newApp := new(v1beta1.Application)
|
||||
Eventually(func() error {
|
||||
|
||||
@@ -101,6 +101,20 @@ var _ = Describe("Test multicluster CLI commands", func() {
|
||||
Expect(string(bs)).Should(ContainSubstring("Hello World"))
|
||||
})
|
||||
|
||||
It("Test vela status --tree", func() {
|
||||
_, err := execCommand("cluster", "alias", WorkerClusterName, "alias-worker-tree")
|
||||
Expect(err).Should(Succeed())
|
||||
for _, format := range []string{"inline", "wide", "table", "list"} {
|
||||
outputs, err := execCommand("status", app.Name, "-n", namespace, "--tree", "--detail", "--detail-format", format)
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(string(outputs)).Should(SatisfyAll(
|
||||
ContainSubstring("alias-worker-tree"),
|
||||
ContainSubstring("Deployment/exec-podinfo"),
|
||||
ContainSubstring("updated"),
|
||||
ContainSubstring("1/1"),
|
||||
))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
@@ -111,6 +112,14 @@ var _ = Describe("Test multicluster scenario", func() {
|
||||
Expect(out).ShouldNot(ContainSubstring("purpose"))
|
||||
})
|
||||
|
||||
It("Test alias for cluster", func() {
|
||||
_, err := execCommand("cluster", "alias", WorkerClusterName, "alias-worker")
|
||||
Expect(err).Should(Succeed())
|
||||
out, err := execCommand("cluster", "list")
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(out).Should(ContainSubstring("alias-worker"))
|
||||
})
|
||||
|
||||
It("Test detach cluster with application use", func() {
|
||||
const testClusterName = "test-cluster"
|
||||
_, err := execCommand("cluster", "join", "/tmp/worker.kubeconfig", "--name", testClusterName)
|
||||
@@ -439,5 +448,52 @@ var _ = Describe("Test multicluster scenario", func() {
|
||||
g.Expect(k8sClient.Get(workerCtx, types.NamespacedName{Name: "test-busybox", Namespace: prodNamespace}, &appsv1.Deployment{})).Should(Succeed())
|
||||
}, time.Minute).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test re-deploy application with old revisions", func() {
|
||||
By("apply application")
|
||||
app := &v1beta1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "test-app-target"},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{{
|
||||
Name: "test-busybox",
|
||||
Type: "webservice",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{"image":"busybox","cmd":["sleep","86400"]}`)},
|
||||
}},
|
||||
Policies: []v1beta1.AppPolicy{{
|
||||
Name: "topology-local",
|
||||
Type: "topology",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{"clusters":["local"],"namespace":"%s"}`, testNamespace))},
|
||||
},
|
||||
}}}
|
||||
oam.SetPublishVersion(app, "alpha")
|
||||
Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
|
||||
}, time.Minute).Should(Succeed())
|
||||
|
||||
By("update application to new version")
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
app.Spec.Components[0].Name = "test-busybox-v2"
|
||||
oam.SetPublishVersion(app, "beta")
|
||||
g.Expect(k8sClient.Update(hubCtx, app)).Should(Succeed())
|
||||
}, 15*time.Second).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox-v2", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
|
||||
err := k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})
|
||||
g.Expect(kerrors.IsNotFound(err)).Should(BeTrue())
|
||||
}, time.Minute).Should(Succeed())
|
||||
|
||||
By("Re-publish application to v1")
|
||||
_, err := execCommand("up", appKey.Name, "-n", appKey.Namespace, "--revision", appKey.Name+"-v1", "--publish-version", "v1.0")
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox", Namespace: testNamespace}, &appsv1.Deployment{})).Should(Succeed())
|
||||
err := k8sClient.Get(hubCtx, types.NamespacedName{Name: "test-busybox-v2", Namespace: testNamespace}, &appsv1.Deployment{})
|
||||
g.Expect(kerrors.IsNotFound(err)).Should(BeTrue())
|
||||
}, 2*time.Minute).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
"config-image-registry": {
|
||||
type: "component"
|
||||
annotations: {
|
||||
"alias.config.oam.dev": "Image Registry"
|
||||
}
|
||||
labels: {
|
||||
"catalog.config.oam.dev": "velacore-config"
|
||||
"type.config.oam.dev": "image-registry"
|
||||
"multi-cluster.config.oam.dev": "true"
|
||||
}
|
||||
description: "Config information to authenticate image registry"
|
||||
attributes: workload: type: "autodetects.core.oam.dev"
|
||||
}
|
||||
|
||||
template: {
|
||||
output: {
|
||||
apiVersion: "v1"
|
||||
kind: "Secret"
|
||||
metadata: {
|
||||
name: context.name
|
||||
namespace: context.namespace
|
||||
labels: {
|
||||
"config.oam.dev/catalog": "velacore-config"
|
||||
"config.oam.dev/type": "image-registry"
|
||||
"config.oam.dev/multi-cluster": "true"
|
||||
"config.oam.dev/identifier": parameter.registry
|
||||
"config.oam.dev/sub-type": "auth"
|
||||
}
|
||||
}
|
||||
type: "kubernetes.io/dockerconfigjson"
|
||||
stringData: {
|
||||
if parameter.auth != _|_ {
|
||||
".dockerconfigjson": json.Marshal({
|
||||
"auths": "\(parameter.registry)": {
|
||||
"username": parameter.auth.username
|
||||
"password": parameter.auth.password
|
||||
if parameter.auth.email != _|_ {
|
||||
"email": parameter.auth.email
|
||||
}
|
||||
"auth": base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Image registry FQDN
|
||||
registry: string
|
||||
// +usage=Authenticate the image registry
|
||||
auth?: {
|
||||
// +usage=Private Image registry username
|
||||
username: string
|
||||
// +usage=Private Image registry password
|
||||
password: string
|
||||
// +usage=Private Image registry email
|
||||
email?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user