mirror of
https://github.com/kubevela/kubevela.git
synced 2026-03-01 01:00:38 +00:00
Compare commits
30 Commits
v1.3.3
...
v1.4.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62ecc70ade | ||
|
|
5857aa8790 | ||
|
|
49646ddc8e | ||
|
|
707905d877 | ||
|
|
7d3ef0595a | ||
|
|
af6dc4bda3 | ||
|
|
f44bd7c6dd | ||
|
|
eaec8348d9 | ||
|
|
2849dfc1fb | ||
|
|
d657ea4daf | ||
|
|
68500b3f17 | ||
|
|
c33eaa0609 | ||
|
|
7a0d2b552b | ||
|
|
385b2462e9 | ||
|
|
0c35753530 | ||
|
|
0e97aa2291 | ||
|
|
7fcb89906c | ||
|
|
86ef2d68e0 | ||
|
|
2e57be1022 | ||
|
|
ad01f3062a | ||
|
|
b6fac3f4d5 | ||
|
|
2eb7826070 | ||
|
|
5f7371815c | ||
|
|
d6b96fee5a | ||
|
|
85c673a574 | ||
|
|
7e6d9ccc73 | ||
|
|
e65dcf12db | ||
|
|
fd5faed71a | ||
|
|
b1823084af | ||
|
|
83fe4a160e |
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
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ Reviewers:
|
||||
- devholic
|
||||
- fourierr
|
||||
- JooKS-me
|
||||
- s4rd1nh4
|
||||
|
||||
Approvers:
|
||||
- Somefive (Multi-Cluster)
|
||||
|
||||
@@ -45,12 +45,13 @@ type GarbageCollectPolicyRule struct {
|
||||
}
|
||||
|
||||
// GarbageCollectPolicyRuleSelector select the targets of the rule
|
||||
// if both traitTypes and componentTypes are specified, combination logic is OR
|
||||
// if both traitTypes, oamTypes and componentTypes are specified, combination logic is OR
|
||||
// if one resource is specified with conflict strategies, strategy as component go first.
|
||||
type GarbageCollectPolicyRuleSelector struct {
|
||||
CompNames []string `json:"componentNames"`
|
||||
CompTypes []string `json:"componentTypes"`
|
||||
TraitTypes []string `json:"traitTypes"`
|
||||
CompNames []string `json:"componentNames"`
|
||||
CompTypes []string `json:"componentTypes"`
|
||||
OAMResourceTypes []string `json:"oamTypes"`
|
||||
TraitTypes []string `json:"traitTypes"`
|
||||
}
|
||||
|
||||
// GarbageCollectStrategy the strategy for target resource to recycle
|
||||
@@ -69,10 +70,11 @@ const (
|
||||
// FindStrategy find gc strategy for target resource
|
||||
func (in GarbageCollectPolicySpec) FindStrategy(manifest *unstructured.Unstructured) *GarbageCollectStrategy {
|
||||
for _, rule := range in.Rules {
|
||||
var compName, compType, traitType string
|
||||
var compName, compType, oamType, traitType string
|
||||
if labels := manifest.GetLabels(); labels != nil {
|
||||
compName = labels[oam.LabelAppComponent]
|
||||
compType = labels[oam.WorkloadTypeLabel]
|
||||
oamType = labels[oam.LabelOAMResourceType]
|
||||
traitType = labels[oam.TraitTypeLabel]
|
||||
}
|
||||
match := func(src []string, val string) (found bool) {
|
||||
@@ -83,6 +85,7 @@ func (in GarbageCollectPolicySpec) FindStrategy(manifest *unstructured.Unstructu
|
||||
}
|
||||
if match(rule.Selector.CompNames, compName) ||
|
||||
match(rule.Selector.CompTypes, compType) ||
|
||||
match(rule.Selector.OAMResourceTypes, oamType) ||
|
||||
match(rule.Selector.TraitTypes, traitType) {
|
||||
return &rule.Strategy
|
||||
}
|
||||
|
||||
@@ -109,6 +109,18 @@ func TestGarbageCollectPolicySpec_FindStrategy(t *testing.T) {
|
||||
}},
|
||||
expectStrategy: GarbageCollectStrategyNever,
|
||||
},
|
||||
"resource type rule match": {
|
||||
rules: []GarbageCollectPolicyRule{{
|
||||
Selector: GarbageCollectPolicyRuleSelector{OAMResourceTypes: []string{"TRAIT"}},
|
||||
Strategy: GarbageCollectStrategyNever,
|
||||
}},
|
||||
input: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"labels": map[string]interface{}{oam.LabelOAMResourceType: "TRAIT"},
|
||||
},
|
||||
}},
|
||||
expectStrategy: GarbageCollectStrategyNever,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
@@ -291,6 +291,11 @@ func (in *GarbageCollectPolicyRuleSelector) DeepCopyInto(out *GarbageCollectPoli
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.OAMResourceTypes != nil {
|
||||
in, out := &in.OAMResourceTypes, &out.OAMResourceTypes
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TraitTypes != nil {
|
||||
in, out := &in.TraitTypes, &out.TraitTypes
|
||||
*out = make([]string, len(*in))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,13 @@ spec:
|
||||
}
|
||||
}]
|
||||
}
|
||||
if parameter["livenessProbe"] != _|_ {
|
||||
livenessProbe: parameter.livenessProbe
|
||||
}
|
||||
|
||||
if parameter["readinessProbe"] != _|_ {
|
||||
readinessProbe: parameter.readinessProbe
|
||||
}
|
||||
}]
|
||||
}
|
||||
parameter: {
|
||||
@@ -55,5 +62,52 @@ spec:
|
||||
name: string
|
||||
path: string
|
||||
}]
|
||||
|
||||
// +usage=Instructions for assessing whether the container is alive.
|
||||
livenessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
|
||||
exec?: {
|
||||
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
|
||||
command: [...string]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
|
||||
httpGet?: {
|
||||
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
|
||||
path: string
|
||||
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
|
||||
port: int
|
||||
httpHeaders?: [...{
|
||||
name: string
|
||||
value: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
|
||||
tcpSocket?: {
|
||||
// +usage=The TCP socket within the container that should be probed to assess container health.
|
||||
port: int
|
||||
}
|
||||
|
||||
// +usage=Number of seconds after the container is started before the first probe is initiated.
|
||||
initialDelaySeconds: *0 | int
|
||||
|
||||
// +usage=How often, in seconds, to execute the probe.
|
||||
periodSeconds: *10 | int
|
||||
|
||||
// +usage=Number of seconds after which the probe times out.
|
||||
timeoutSeconds: *1 | int
|
||||
|
||||
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
|
||||
successThreshold: *1 | int
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
configMountToEnvsList: *[
|
||||
for v in parameter.configMap if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: configMapKeyRef: {
|
||||
name: v.name
|
||||
key: k.configMapKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretVolumeMountsList: *[
|
||||
for v in parameter.secret if v.mountPath != _|_ {
|
||||
{
|
||||
@@ -106,6 +117,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretMountToEnvsList: *[
|
||||
for v in parameter.secret if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: secretKeyRef: {
|
||||
name: v.name
|
||||
key: k.secretKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
emptyDirVolumeMountsList: *[
|
||||
for v in parameter.emptyDir {
|
||||
{
|
||||
@@ -128,7 +150,7 @@ spec:
|
||||
|
||||
containers: [{
|
||||
// +patchKey=name
|
||||
env: configMapEnvMountsList + secretEnvMountsList
|
||||
env: configMapEnvMountsList + secretEnvMountsList + configMountToEnvsList + secretMountToEnvsList
|
||||
// +patchKey=name
|
||||
volumeDevices: volumeDevicesList
|
||||
// +patchKey=name
|
||||
@@ -248,6 +270,10 @@ spec:
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
@@ -267,6 +293,10 @@ spec:
|
||||
envName: string
|
||||
secretKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
secretKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,13 @@ spec:
|
||||
}
|
||||
}]
|
||||
}
|
||||
if parameter["livenessProbe"] != _|_ {
|
||||
livenessProbe: parameter.livenessProbe
|
||||
}
|
||||
|
||||
if parameter["readinessProbe"] != _|_ {
|
||||
readinessProbe: parameter.readinessProbe
|
||||
}
|
||||
}]
|
||||
}
|
||||
parameter: {
|
||||
@@ -55,5 +62,52 @@ spec:
|
||||
name: string
|
||||
path: string
|
||||
}]
|
||||
|
||||
// +usage=Instructions for assessing whether the container is alive.
|
||||
livenessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
|
||||
exec?: {
|
||||
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
|
||||
command: [...string]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
|
||||
httpGet?: {
|
||||
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
|
||||
path: string
|
||||
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
|
||||
port: int
|
||||
httpHeaders?: [...{
|
||||
name: string
|
||||
value: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
|
||||
tcpSocket?: {
|
||||
// +usage=The TCP socket within the container that should be probed to assess container health.
|
||||
port: int
|
||||
}
|
||||
|
||||
// +usage=Number of seconds after the container is started before the first probe is initiated.
|
||||
initialDelaySeconds: *0 | int
|
||||
|
||||
// +usage=How often, in seconds, to execute the probe.
|
||||
periodSeconds: *10 | int
|
||||
|
||||
// +usage=Number of seconds after which the probe times out.
|
||||
timeoutSeconds: *1 | int
|
||||
|
||||
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
|
||||
successThreshold: *1 | int
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
configMountToEnvsList: *[
|
||||
for v in parameter.configMap if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: configMapKeyRef: {
|
||||
name: v.name
|
||||
key: k.configMapKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretVolumeMountsList: *[
|
||||
for v in parameter.secret if v.mountPath != _|_ {
|
||||
{
|
||||
@@ -106,6 +117,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretMountToEnvsList: *[
|
||||
for v in parameter.secret if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: secretKeyRef: {
|
||||
name: v.name
|
||||
key: k.secretKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
emptyDirVolumeMountsList: *[
|
||||
for v in parameter.emptyDir {
|
||||
{
|
||||
@@ -128,7 +150,7 @@ spec:
|
||||
|
||||
containers: [{
|
||||
// +patchKey=name
|
||||
env: configMapEnvMountsList + secretEnvMountsList
|
||||
env: configMapEnvMountsList + secretEnvMountsList + configMountToEnvsList + secretMountToEnvsList
|
||||
// +patchKey=name
|
||||
volumeDevices: volumeDevicesList
|
||||
// +patchKey=name
|
||||
@@ -248,6 +270,10 @@ spec:
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
@@ -267,6 +293,10 @@ spec:
|
||||
envName: string
|
||||
secretKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
secretKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
By leveraging the garbage-collect policy, users can persist some resources, which skip the normal garbage-collect process when application is updated.
|
||||
|
||||
### traitTypes
|
||||
|
||||
Take the following app as an example, in the garbage-collect policy, a rule is added which marks all the resources created by the `expose` trait to use the `onAppDelete` strategy. This will keep those services until application is deleted.
|
||||
```shell
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
@@ -78,6 +80,8 @@ hello-world ClusterIP 10.96.160.208 <none> 8000/TCP 5m56s
|
||||
hello-world-new ClusterIP 10.96.20.4 <none> 8000/TCP 13s
|
||||
```
|
||||
|
||||
### componentTypes
|
||||
|
||||
Users can also keep component if they are deploying job-like components. Resources dispatched by `job-like-component` type component will be kept after application is deleted.
|
||||
|
||||
```yaml
|
||||
@@ -100,6 +104,8 @@ spec:
|
||||
strategy: never
|
||||
```
|
||||
|
||||
### componentNames
|
||||
|
||||
A more straightforward way is to specify `compNames` to match specified components.
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
@@ -124,3 +130,74 @@ spec:
|
||||
- example-addon-namespace
|
||||
strategy: never
|
||||
```
|
||||
|
||||
### oamTypes
|
||||
|
||||
Users can also persist resources using `oamTypes`, where the values of `oamTypes` can be `TRAIT` and `WORKLOAD`.
|
||||
|
||||
```shell
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: garbage-collect-app
|
||||
spec:
|
||||
components:
|
||||
- name: hello-world
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
traits:
|
||||
- type: expose
|
||||
properties:
|
||||
port: [8000]
|
||||
policies:
|
||||
- name: garbage-collect
|
||||
type: garbage-collect
|
||||
properties:
|
||||
rules:
|
||||
- selector:
|
||||
oamTypes:
|
||||
- TRAIT
|
||||
strategy: onAppDelete
|
||||
EOF
|
||||
```
|
||||
|
||||
And then, let's modify the component name.
|
||||
|
||||
```shell
|
||||
$ cat <<EOF | kubectl apply -f -
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: garbage-collect-app
|
||||
spec:
|
||||
components:
|
||||
- name: hello-world-new
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
traits:
|
||||
- type: expose
|
||||
properties:
|
||||
port: [8000]
|
||||
policies:
|
||||
- name: garbage-collect
|
||||
type: garbage-collect
|
||||
properties:
|
||||
rules:
|
||||
- selector:
|
||||
oamTypes:
|
||||
- TRAIT
|
||||
strategy: onAppDelete
|
||||
EOF
|
||||
```
|
||||
|
||||
List the service in cluster, you will find:
|
||||
|
||||
```shell
|
||||
$ kubectl get service
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
hello-world ClusterIP 10.96.31.209 <none> 8000/TCP 31s
|
||||
hello-world-new ClusterIP 10.96.17.103 <none> 8000/TCP 5s
|
||||
```
|
||||
|
||||
@@ -32,6 +32,9 @@ spec:
|
||||
mountToEnv:
|
||||
envName: TEST_ENV
|
||||
configMapKey: key1
|
||||
mountToEnvs:
|
||||
- envName: TEST_CM_ENV
|
||||
configMapKey: key2
|
||||
data:
|
||||
key1: value1
|
||||
key2: value2
|
||||
@@ -49,9 +52,15 @@ spec:
|
||||
mountToEnv:
|
||||
envName: TEST_SECRET
|
||||
secretKey: key1
|
||||
mountToEnvs:
|
||||
- envName: TEST_SECRET_ENV_2
|
||||
secretKey: key2
|
||||
- envName: TEST_SECRET_ENV_3
|
||||
secretKey: key3
|
||||
data:
|
||||
key1: dmFsdWUx
|
||||
key2: dmFsdWUy
|
||||
key3: dmFsdWUz
|
||||
emptyDir:
|
||||
- name: test1
|
||||
mountPath: /test/mount/emptydir
|
||||
|
||||
@@ -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))
|
||||
|
||||
28
go.mod
28
go.mod
@@ -64,8 +64,8 @@ require (
|
||||
go.mongodb.org/mongo-driver v1.5.1
|
||||
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/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/tools v0.1.6 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
@@ -96,6 +96,18 @@ require (
|
||||
sigs.k8s.io/yaml v1.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/docker/distribution v2.8.0-beta.1+incompatible // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/xanzy/go-gitlab v0.60.0
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de // indirect
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.81.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
@@ -139,7 +151,6 @@ require (
|
||||
github.com/cyphar/filepath-securejoin v0.2.2 // indirect
|
||||
github.com/deislabs/oras v0.11.1 // indirect
|
||||
github.com/docker/cli v20.10.5+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.0-beta.1+incompatible // indirect
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.6.3 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
@@ -168,7 +179,7 @@ require (
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.1.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/googleapis/gnostic v0.5.5 // indirect
|
||||
@@ -183,7 +194,6 @@ require (
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/klauspost/compress v1.11.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/pty v1.1.8 // indirect
|
||||
@@ -234,7 +244,6 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.0.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.2 // indirect
|
||||
@@ -249,17 +258,14 @@ require (
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
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/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect
|
||||
google.golang.org/grpc v1.38.0 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/gorp.v1 v1.7.2 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
|
||||
37
go.sum
37
go.sum
@@ -781,8 +781,9 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoPhd27II=
|
||||
github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -895,8 +896,12 @@ github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPA
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v0.12.2 h1:F1fdYblUEsxKiailtkhCCG2g4bipEgaHiDc8vffNpD4=
|
||||
github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
@@ -906,6 +911,9 @@ github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
@@ -1618,6 +1626,8 @@ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgq
|
||||
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/wonderflow/cert-manager-api v1.0.3 h1:xQQMkJNQ12oYyy00jOQUlSKgdraApaURxv3PHFdVTfA=
|
||||
github.com/wonderflow/cert-manager-api v1.0.3/go.mod h1:1Se7MSg11/eNYlo4fWv6vOM55/jTBMOzg2DN1kVFiSc=
|
||||
github.com/xanzy/go-gitlab v0.60.0 h1:HaIlc14k4t9eJjAhY0Gmq2fBHgKd1MthBn3+vzDtsbA=
|
||||
github.com/xanzy/go-gitlab v0.60.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=
|
||||
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||
@@ -1908,9 +1918,12 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
|
||||
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc=
|
||||
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1922,8 +1935,9 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 h1:0Ja1LBD+yisY6RWM/BH7TJVXWsSjs2VwBSmvSX4HdBc=
|
||||
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -2047,13 +2061,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 +2079,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=
|
||||
@@ -2075,8 +2091,9 @@ golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -2246,6 +2263,7 @@ google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk
|
||||
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
@@ -2354,8 +2372,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
|
||||
@@ -48,6 +48,7 @@ ADDONSERVER = $(shell pgrep vela_addon_mock_server)
|
||||
e2e-apiserver-test:
|
||||
pkill vela_addon_mock_server || true
|
||||
go run ./e2e/addon/mock/vela_addon_mock_server.go &
|
||||
sleep 15
|
||||
go test -v -coverpkg=./... -coverprofile=/tmp/e2e_apiserver_test.out ./test/e2e-apiserver-test
|
||||
@$(OK) tests pass
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
"golang.org/x/oauth2"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -447,6 +448,15 @@ func createGiteeHelper(content *utils.Content, token string) *giteeHelper {
|
||||
}
|
||||
}
|
||||
|
||||
func createGitlabHelper(content *utils.Content, token string) (*gitlabHelper, error) {
|
||||
newClient, err := gitlab.NewClient(token, gitlab.WithBaseURL(content.GitlabContent.Host))
|
||||
|
||||
return &gitlabHelper{
|
||||
Client: newClient,
|
||||
Meta: content,
|
||||
}, err
|
||||
}
|
||||
|
||||
// readRepo will read relative path (relative to Meta.Path)
|
||||
func (h *gitHelper) readRepo(relativePath string) (*github.RepositoryContent, []*github.RepositoryContent, error) {
|
||||
file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.GithubContent.Owner, h.Meta.GithubContent.Repo, path.Join(h.Meta.GithubContent.Path, relativePath), nil)
|
||||
@@ -1070,7 +1080,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 +1354,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 +1371,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 +1392,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"
|
||||
@@ -146,7 +147,7 @@ func TestGetAddonData(t *testing.T) {
|
||||
server := httptest.NewServer(ossHandler)
|
||||
defer server.Close()
|
||||
|
||||
reader, err := NewAsyncReader(server.URL, "", "", "", ossType)
|
||||
reader, err := NewAsyncReader(server.URL, "", "", "", "", ossType)
|
||||
assert.NoError(t, err)
|
||||
testReaderFunc(t, reader)
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -616,9 +617,9 @@ func TestRenderApp4ObservabilityWithK8sData(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetPatternFromItem(t *testing.T) {
|
||||
ossR, err := NewAsyncReader("http://ep.beijing", "some-bucket", "some-sub-path", "", ossType)
|
||||
ossR, err := NewAsyncReader("http://ep.beijing", "some-bucket", "", "some-sub-path", "", ossType)
|
||||
assert.NoError(t, err)
|
||||
gitR, err := NewAsyncReader("https://github.com/oam-dev/catalog", "", "addons", "", gitType)
|
||||
gitR, err := NewAsyncReader("https://github.com/oam-dev/catalog", "", "", "addons", "", gitType)
|
||||
assert.NoError(t, err)
|
||||
gitItemName := "parameter.cue"
|
||||
gitItemType := FileType
|
||||
@@ -656,7 +657,7 @@ func TestGetPatternFromItem(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGitLabReaderNotPanic(t *testing.T) {
|
||||
_, err := NewAsyncReader("https://gitlab.com/test/catalog", "", "addons", "", gitType)
|
||||
_, err := NewAsyncReader("https://gitlab.com/test/catalog", "", "", "addons", "", gitType)
|
||||
assert.EqualError(t, err, "git type repository only support github for now")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
150
pkg/addon/reader_gitlab.go
Normal file
150
pkg/addon/reader_gitlab.go
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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 addon
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
var _ AsyncReader = &gitlabReader{}
|
||||
|
||||
// gitlabReader helps get addon's file by git
|
||||
type gitlabReader struct {
|
||||
h *gitlabHelper
|
||||
}
|
||||
|
||||
// gitlabHelper helps get addon's file by git
|
||||
type gitlabHelper struct {
|
||||
Client *gitlab.Client
|
||||
Meta *utils.Content
|
||||
}
|
||||
|
||||
// GitLabItem addon's sub item
|
||||
type GitLabItem struct {
|
||||
basePath string
|
||||
tp string
|
||||
path string
|
||||
name string
|
||||
}
|
||||
|
||||
// GetType get addon's sub item type
|
||||
func (g GitLabItem) GetType() string {
|
||||
return g.tp
|
||||
}
|
||||
|
||||
// GetPath get addon's sub item path
|
||||
func (g GitLabItem) GetPath() string {
|
||||
return g.path[len(g.basePath)+1:]
|
||||
}
|
||||
|
||||
// GetName get addon's sub item name
|
||||
func (g GitLabItem) GetName() string {
|
||||
return g.name
|
||||
}
|
||||
|
||||
// GetRef ref is empty , use default branch master
|
||||
func (g *gitlabReader) GetRef() string {
|
||||
var ref = "master"
|
||||
if g.h.Meta.GitlabContent.Ref != "" {
|
||||
return g.h.Meta.GitlabContent.Ref
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
// GetProjectID get gitlab project id
|
||||
func (g *gitlabReader) GetProjectID() int {
|
||||
return g.h.Meta.GitlabContent.PId
|
||||
}
|
||||
|
||||
// GetProjectPath get gitlab project path
|
||||
func (g *gitlabReader) GetProjectPath() string {
|
||||
return g.h.Meta.GitlabContent.Path
|
||||
}
|
||||
|
||||
// ListAddonMeta relative path to repoURL/basePath
|
||||
func (g *gitlabReader) ListAddonMeta() (addonCandidates map[string]SourceMeta, err error) {
|
||||
addonCandidates = make(map[string]SourceMeta)
|
||||
path := g.GetProjectPath()
|
||||
ref := g.GetRef()
|
||||
tree, _, err := g.h.Client.Repositories.ListTree(g.GetProjectID(), &gitlab.ListTreeOptions{Path: &path, Ref: &ref})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range tree {
|
||||
if node.Type == TreeType {
|
||||
items, err := g.listAddonItem(make([]Item, 0), node.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addonCandidates[node.Name] = SourceMeta{
|
||||
Name: node.Name,
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addonCandidates, nil
|
||||
}
|
||||
|
||||
func (g *gitlabReader) listAddonItem(item []Item, path string) ([]Item, error) {
|
||||
ref := g.GetRef()
|
||||
tree, _, err := g.h.Client.Repositories.ListTree(g.GetProjectID(), &gitlab.ListTreeOptions{Path: &path, Ref: &ref})
|
||||
if err != nil {
|
||||
return item, err
|
||||
}
|
||||
for _, node := range tree {
|
||||
switch node.Type {
|
||||
case TreeType:
|
||||
item, err = g.listAddonItem(item, node.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case BlobType:
|
||||
item = append(item, &GitLabItem{
|
||||
basePath: g.GetProjectPath(),
|
||||
tp: FileType,
|
||||
path: node.Path,
|
||||
name: node.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// ReadFile read file content from gitlab
|
||||
func (g *gitlabReader) ReadFile(path string) (content string, err error) {
|
||||
ref := g.GetRef()
|
||||
getFile, _, err := g.h.Client.RepositoryFiles.GetFile(g.GetProjectID(), g.GetProjectPath()+"/"+path, &gitlab.GetFileOptions{Ref: &ref})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
decodeString, err := base64.StdEncoding.DecodeString(getFile.Content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decodeString), nil
|
||||
}
|
||||
|
||||
func (g *gitlabReader) RelativePath(item Item) string {
|
||||
return item.GetPath()
|
||||
}
|
||||
113
pkg/addon/reader_gitlab_test.go
Normal file
113
pkg/addon/reader_gitlab_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
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 addon
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
var baseUrl = "/api/v4"
|
||||
|
||||
func gitlabSetup() (client *gitlab.Client, mux *http.ServeMux, teardown func()) {
|
||||
// mux is the HTTP request multiplexer used with the test server.
|
||||
mux = http.NewServeMux()
|
||||
|
||||
apiHandler := http.NewServeMux()
|
||||
apiHandler.Handle(baseUrl+"/", http.StripPrefix(baseUrl, mux))
|
||||
|
||||
// server is a test HTTP server used to provide mock API responses.
|
||||
server := httptest.NewServer(apiHandler)
|
||||
|
||||
// client is the Gitlab client being tested and is
|
||||
// configured to use test server.
|
||||
client, err := gitlab.NewClient("", gitlab.WithBaseURL(server.URL+baseUrl+"/"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return client, mux, server.Close
|
||||
}
|
||||
|
||||
func TestGitlabReader(t *testing.T) {
|
||||
client, mux, teardown := gitlabSetup()
|
||||
gitlabPattern := "/projects/9999/repository/files/"
|
||||
mux.HandleFunc(gitlabPattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
queryPath := strings.TrimPrefix(req.URL.Path, gitlabPattern)
|
||||
localPath := path.Join(testdataPrefix, queryPath)
|
||||
file, err := testdata.ReadFile(localPath)
|
||||
// test if it's a file
|
||||
if err == nil {
|
||||
content := &gitlab.File{
|
||||
FilePath: localPath,
|
||||
FileName: path.Base(queryPath),
|
||||
Size: *Int(len(file)),
|
||||
Encoding: "base64",
|
||||
Ref: "master",
|
||||
Content: base64.StdEncoding.EncodeToString(file),
|
||||
}
|
||||
res, _ := json.Marshal(content)
|
||||
rw.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, it could be directory
|
||||
dir, err := testdata.ReadDir(localPath)
|
||||
if err == nil {
|
||||
contents := make([]*gitlab.TreeNode, 0)
|
||||
for _, item := range dir {
|
||||
tp := "file"
|
||||
if item.IsDir() {
|
||||
tp = "dir"
|
||||
}
|
||||
contents = append(contents, &gitlab.TreeNode{
|
||||
ID: "",
|
||||
Name: item.Name(),
|
||||
Type: tp,
|
||||
Path: localPath + "/" + item.Name(),
|
||||
Mode: "",
|
||||
})
|
||||
}
|
||||
dRes, _ := json.Marshal(contents)
|
||||
rw.Write(dRes)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Write([]byte("invalid gitlab query"))
|
||||
})
|
||||
defer teardown()
|
||||
|
||||
gith := &gitlabHelper{
|
||||
Client: client,
|
||||
Meta: &utils.Content{GitlabContent: utils.GitlabContent{
|
||||
PId: 9999,
|
||||
}},
|
||||
}
|
||||
var r AsyncReader = &gitlabReader{gith}
|
||||
_, err := r.ReadFile("example/metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -37,10 +37,11 @@ const registriesKey = "registries"
|
||||
type Registry struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
Helm *HelmSource `json:"helm,omitempty"`
|
||||
Git *GitAddonSource `json:"git,omitempty"`
|
||||
OSS *OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *GiteeAddonSource `json:"gitee,omitempty"`
|
||||
Helm *HelmSource `json:"helm,omitempty"`
|
||||
Git *GitAddonSource `json:"git,omitempty"`
|
||||
OSS *OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *GiteeAddonSource `json:"gitee,omitempty"`
|
||||
Gitlab *GitlabAddonSource `json:"gitlab,omitempty"`
|
||||
}
|
||||
|
||||
// RegistryDataStore CRUD addon registry data in configmap
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
@@ -35,6 +36,10 @@ const (
|
||||
DirType = "dir"
|
||||
// FileType means a file
|
||||
FileType = "file"
|
||||
// BlobType means a blob
|
||||
BlobType = "blob"
|
||||
// TreeType means a tree
|
||||
TreeType = "tree"
|
||||
|
||||
bucketTmpl = "%s://%s.%s"
|
||||
singleOSSFileTmpl = "%s/%s"
|
||||
@@ -63,6 +68,14 @@ type GiteeAddonSource struct {
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// GitlabAddonSource defines the information about the Gitlab as addon source
|
||||
type GitlabAddonSource struct {
|
||||
URL string `json:"url,omitempty" validate:"required"`
|
||||
Repo string `json:"repo,omitempty" validate:"required"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
// HelmSource defines the information about the helm repo addon source
|
||||
type HelmSource struct {
|
||||
URL string `json:"url,omitempty" validate:"required"`
|
||||
@@ -122,15 +135,16 @@ func pathWithParent(subPath, parent string) string {
|
||||
type ReaderType string
|
||||
|
||||
const (
|
||||
gitType ReaderType = "git"
|
||||
ossType ReaderType = "oss"
|
||||
giteeType ReaderType = "gitee"
|
||||
gitType ReaderType = "git"
|
||||
ossType ReaderType = "oss"
|
||||
giteeType ReaderType = "gitee"
|
||||
gitlabType ReaderType = "gitlab"
|
||||
)
|
||||
|
||||
// NewAsyncReader create AsyncReader from
|
||||
// 1. GitHub url and directory
|
||||
// 2. OSS endpoint and bucket
|
||||
func NewAsyncReader(baseURL, bucket, subPath, token string, rdType ReaderType) (AsyncReader, error) {
|
||||
func NewAsyncReader(baseURL, bucket, repo, subPath, token string, rdType ReaderType) (AsyncReader, error) {
|
||||
|
||||
switch rdType {
|
||||
case gitType:
|
||||
@@ -182,23 +196,63 @@ func NewAsyncReader(baseURL, bucket, subPath, token string, rdType ReaderType) (
|
||||
return &giteeReader{
|
||||
h: gitee,
|
||||
}, nil
|
||||
case gitlabType:
|
||||
baseURL = strings.TrimSuffix(baseURL, ".git")
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, errors.New("addon registry invalid")
|
||||
}
|
||||
_, content, err := utils.ParseGitlab(u.String(), repo)
|
||||
content.GitlabContent.Path = subPath
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gitlabHelper, err := createGitlabHelper(content, token)
|
||||
if err != nil {
|
||||
return nil, errors.New("addon registry connect fail")
|
||||
}
|
||||
|
||||
err = gitlabHelper.getGitlabProject(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gitlabReader{
|
||||
h: gitlabHelper,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid addon registry type '%s'", rdType)
|
||||
}
|
||||
|
||||
// getGitlabProject get gitlab project , set project id
|
||||
func (h *gitlabHelper) getGitlabProject(content *utils.Content) error {
|
||||
projectURL := content.GitlabContent.Owner + "/" + content.GitlabContent.Repo
|
||||
projects, _, err := h.Client.Projects.GetProject(projectURL, &gitlab.GetProjectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content.GitlabContent.PId = projects.ID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildReader will build a AsyncReader from registry, AsyncReader are needed to read addon files
|
||||
func (r *Registry) BuildReader() (AsyncReader, error) {
|
||||
if r.OSS != nil {
|
||||
o := r.OSS
|
||||
return NewAsyncReader(o.Endpoint, o.Bucket, o.Path, "", ossType)
|
||||
return NewAsyncReader(o.Endpoint, o.Bucket, "", o.Path, "", ossType)
|
||||
}
|
||||
if r.Git != nil {
|
||||
g := r.Git
|
||||
return NewAsyncReader(g.URL, "", g.Path, g.Token, gitType)
|
||||
return NewAsyncReader(g.URL, "", "", g.Path, g.Token, gitType)
|
||||
}
|
||||
if r.Gitee != nil {
|
||||
g := r.Gitee
|
||||
return NewAsyncReader(g.URL, "", g.Path, g.Token, giteeType)
|
||||
return NewAsyncReader(g.URL, "", "", g.Path, g.Token, giteeType)
|
||||
}
|
||||
if r.Gitlab != nil {
|
||||
g := r.Gitlab
|
||||
return NewAsyncReader(g.URL, "", g.Repo, g.Path, g.Token, gitlabType)
|
||||
}
|
||||
return nil, errors.New("registry don't have enough info to build a reader")
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestPathWithParent(t *testing.T) {
|
||||
|
||||
func TestConvert2OssItem(t *testing.T) {
|
||||
subPath := "sub-addons"
|
||||
reader, err := NewAsyncReader("ep-beijing.com", "bucket", subPath, "", ossType)
|
||||
reader, err := NewAsyncReader("ep-beijing.com", "bucket", "", subPath, "", ossType)
|
||||
|
||||
assert.NoError(t, 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
|
||||
}
|
||||
|
||||
@@ -74,28 +74,31 @@ type NameAlias struct {
|
||||
|
||||
// CreateAddonRegistryRequest defines the format for addon registry create request
|
||||
type CreateAddonRegistryRequest struct {
|
||||
Name string `json:"name" validate:"checkname"`
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty" `
|
||||
Oss *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Name string `json:"name" validate:"checkname"`
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty" `
|
||||
Oss *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Gitlab *addon.GitlabAddonSource `json:"gitlab,omitempty" `
|
||||
}
|
||||
|
||||
// UpdateAddonRegistryRequest defines the format for addon registry update request
|
||||
type UpdateAddonRegistryRequest struct {
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty"`
|
||||
Oss *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty"`
|
||||
Oss *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Gitlab *addon.GitlabAddonSource `json:"gitlab,omitempty" `
|
||||
}
|
||||
|
||||
// AddonRegistry defines the format for a single addon registry
|
||||
type AddonRegistry struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty"`
|
||||
OSS *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Name string `json:"name" validate:"required"`
|
||||
Helm *addon.HelmSource `json:"helm,omitempty"`
|
||||
Git *addon.GitAddonSource `json:"git,omitempty"`
|
||||
OSS *addon.OSSAddonSource `json:"oss,omitempty"`
|
||||
Gitee *addon.GiteeAddonSource `json:"gitee,omitempty" `
|
||||
Gitlab *addon.GitlabAddonSource `json:"gitlab,omitempty" `
|
||||
}
|
||||
|
||||
// ListAddonRegistryResponse list addon registry
|
||||
@@ -194,13 +197,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
|
||||
|
||||
@@ -313,11 +313,12 @@ func (u *defaultAddonHandler) CreateAddonRegistry(ctx context.Context, req apis.
|
||||
|
||||
func convertAddonRegistry(r pkgaddon.Registry) *apis.AddonRegistry {
|
||||
return &apis.AddonRegistry{
|
||||
Name: r.Name,
|
||||
Git: r.Git,
|
||||
Gitee: r.Gitee,
|
||||
OSS: r.OSS,
|
||||
Helm: r.Helm,
|
||||
Name: r.Name,
|
||||
Git: r.Git,
|
||||
Gitee: r.Gitee,
|
||||
OSS: r.OSS,
|
||||
Helm: r.Helm,
|
||||
Gitlab: r.Gitlab,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +344,8 @@ func (u defaultAddonHandler) UpdateAddonRegistry(ctx context.Context, name strin
|
||||
r.OSS = req.Oss
|
||||
case req.Helm != nil:
|
||||
r.Helm = req.Helm
|
||||
case req.Gitlab != nil:
|
||||
r.Gitlab = req.Gitlab
|
||||
}
|
||||
|
||||
err = u.addonRegistryDS.UpdateRegistry(ctx, r)
|
||||
@@ -398,7 +401,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 +468,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
|
||||
@@ -475,11 +479,12 @@ func (u *defaultAddonHandler) UpdateAddon(ctx context.Context, name string, args
|
||||
|
||||
func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) pkgaddon.Registry {
|
||||
return pkgaddon.Registry{
|
||||
Name: req.Name,
|
||||
Git: req.Git,
|
||||
OSS: req.Oss,
|
||||
Gitee: req.Gitee,
|
||||
Helm: req.Helm,
|
||||
Name: req.Name,
|
||||
Git: req.Git,
|
||||
OSS: req.Oss,
|
||||
Gitee: req.Gitee,
|
||||
Helm: req.Helm,
|
||||
Gitlab: req.Gitlab,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -296,7 +296,7 @@ func (s *enabledAddonWebService) GetWebService() *restful.WebService {
|
||||
Filter(s.rbacUsecase.CheckPerm("addon", "list")).
|
||||
Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")).
|
||||
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
|
||||
Returns(200, "OK", apis.ListAddonResponse{}).
|
||||
Returns(200, "OK", apis.ListEnabledAddonResponse{}).
|
||||
Returns(400, "Bad Request", bcode.Bcode{}).
|
||||
Writes(apis.ListAddonResponse{}))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -292,6 +292,35 @@ var _ = Describe("Test Application Controller", func() {
|
||||
},
|
||||
}
|
||||
|
||||
appWithMountToEnvs := &v1beta1.Application{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Application",
|
||||
APIVersion: "core.oam.dev/v1beta1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-mount-to-envs",
|
||||
},
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "myweb",
|
||||
Type: "worker",
|
||||
Properties: &runtime.RawExtension{Raw: []byte("{\"cmd\":[\"sleep\",\"1000\"],\"image\":\"busybox\"}")},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
appWithMountToEnvs.Spec.Components[0].Traits = []common.ApplicationTrait{
|
||||
{
|
||||
Type: "storage",
|
||||
Properties: &runtime.RawExtension{Raw: []byte("{\"secret\": [{\"name\": \"myweb-secret\",\"mountToEnv\": {\"envName\": \"firstEnv\",\"secretKey\": \"firstKey\"},\"mountToEnvs\": [{\"envName\": \"secondEnv\",\"secretKey\": \"secondKey\"}],\"data\": {\"firstKey\": \"dmFsdWUwMQo=\",\"secondKey\": \"dmFsdWUwMgo=\"}}]}")},
|
||||
},
|
||||
{
|
||||
Type: "storage",
|
||||
Properties: &runtime.RawExtension{Raw: []byte("{\"configMap\": [{\"name\": \"myweb-cm\",\"mountToEnvs\": [{\"envName\":\"thirdEnv\",\"configMapKey\":\"thirdKey\"},{\"envName\":\"fourthEnv\",\"configMapKey\":\"fourthKey\"}],\"data\": {\"thirdKey\": \"Value03\",\"fourthKey\": \"Value04\"}}]}")},
|
||||
},
|
||||
}
|
||||
|
||||
cd := &v1beta1.ComponentDefinition{}
|
||||
cDDefJson, _ := yaml.YAMLToJSON([]byte(componentDefYaml))
|
||||
k8sObjectsCDJson, _ := yaml.YAMLToJSON([]byte(k8sObjectsComponentDefinitionYaml))
|
||||
@@ -2568,6 +2597,66 @@ var _ = Describe("Test Application Controller", func() {
|
||||
Expect(k8sClient.Delete(ctx, secret)).Should(BeNil())
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
})
|
||||
|
||||
It("test application with multi-mountToEnv will create application", func() {
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app-with-mount-to-envs",
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, ns)).Should(BeNil())
|
||||
|
||||
appWithMountToEnvs.SetNamespace(ns.Name)
|
||||
app := appWithMountToEnvs.DeepCopy()
|
||||
Expect(k8sClient.Create(ctx, app)).Should(BeNil())
|
||||
|
||||
appKey := client.ObjectKey{
|
||||
Name: app.Name,
|
||||
Namespace: app.Namespace,
|
||||
}
|
||||
testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey})
|
||||
|
||||
By("Check App running successfully")
|
||||
curApp := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(ctx, appKey, curApp)).Should(BeNil())
|
||||
Expect(curApp.Status.Phase).Should(Equal(common.ApplicationRunning))
|
||||
|
||||
appRevision := &v1beta1.ApplicationRevision{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: app.Namespace,
|
||||
Name: curApp.Status.LatestRevision.Name,
|
||||
}, appRevision)).Should(BeNil())
|
||||
By("Check affiliated resource tracker is created")
|
||||
expectRTName := fmt.Sprintf("%s-%s", appRevision.GetName(), appRevision.GetNamespace())
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Name: expectRTName}, &v1beta1.ResourceTracker{})
|
||||
}, 10*time.Second, 500*time.Millisecond).Should(Succeed())
|
||||
|
||||
By("Check AppRevision Created with the expected workload spec")
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
Eventually(func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Name: app.Name + "-v1", Namespace: app.GetNamespace()}, appRev)
|
||||
}, 10*time.Second, 500*time.Millisecond).Should(Succeed())
|
||||
|
||||
By("Check secret Created with the expected trait-storage spec")
|
||||
secret := &corev1.Secret{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: ns.Name,
|
||||
Name: app.Spec.Components[0].Name + "-secret",
|
||||
}, secret)).Should(BeNil())
|
||||
|
||||
By("Check configMap Created with the expected trait-storage spec")
|
||||
cm := &corev1.ConfigMap{}
|
||||
Expect(k8sClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: ns.Name,
|
||||
Name: app.Spec.Components[0].Name + "-cm",
|
||||
}, cm)).Should(BeNil())
|
||||
|
||||
Expect(k8sClient.Delete(ctx, cm)).Should(BeNil())
|
||||
Expect(k8sClient.Delete(ctx, secret)).Should(BeNil())
|
||||
Expect(k8sClient.Delete(ctx, app)).Should(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
const (
|
||||
@@ -3604,6 +3693,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
configMapMountToEnvsList: *[
|
||||
for v in parameter.configMap if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: configMapKeyRef: {
|
||||
name: v.name
|
||||
key: k.configMapKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretVolumeMountsList: *[
|
||||
for v in parameter.secret if v.mountPath != _|_ {
|
||||
{
|
||||
@@ -3623,6 +3723,17 @@ spec:
|
||||
}
|
||||
},
|
||||
] | []
|
||||
secretMountToEnvsList: *[
|
||||
for v in parameter.secret if v.mountToEnvs != _|_ for k in v.mountToEnvs {
|
||||
{
|
||||
name: k.envName
|
||||
valueFrom: secretKeyRef: {
|
||||
name: v.name
|
||||
key: k.secretKey
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
emptyDirVolumeMountsList: *[
|
||||
for v in parameter.emptyDir {
|
||||
{
|
||||
@@ -3645,7 +3756,7 @@ spec:
|
||||
|
||||
containers: [{
|
||||
// +patchKey=name
|
||||
env: configMapEnvMountsList + secretEnvMountsList
|
||||
env: configMapEnvMountsList + secretEnvMountsList + configMapMountToEnvsList + secretMountToEnvsList
|
||||
// +patchKey=name
|
||||
volumeDevices: volumeDevicesList
|
||||
// +patchKey=name
|
||||
@@ -3765,6 +3876,10 @@ spec:
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
configMapKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
@@ -3784,6 +3899,10 @@ spec:
|
||||
envName: string
|
||||
secretKey: string
|
||||
}
|
||||
mountToEnvs?: [...{
|
||||
envName: string
|
||||
secretKey: string
|
||||
}]
|
||||
mountPath?: string
|
||||
defaultMode: *420 | int
|
||||
readOnly: *false | bool
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ const TypeGithub = "github"
|
||||
// TypeGitee represents gitee
|
||||
const TypeGitee = "gitee"
|
||||
|
||||
// TypeGitlab represents gitlab
|
||||
const TypeGitlab = "gitlab"
|
||||
|
||||
// TypeUnknown represents parse failed
|
||||
const TypeUnknown = "unknown"
|
||||
|
||||
@@ -44,6 +47,7 @@ type Content struct {
|
||||
OssContent
|
||||
GithubContent
|
||||
GiteeContent
|
||||
GitlabContent
|
||||
LocalContent
|
||||
}
|
||||
|
||||
@@ -74,6 +78,16 @@ type GiteeContent struct {
|
||||
Ref string `json:"gitee_ref"`
|
||||
}
|
||||
|
||||
// GitlabContent for cap center
|
||||
type GitlabContent struct {
|
||||
Host string `json:"gitlab_host"`
|
||||
Owner string `json:"gitlab_owner"`
|
||||
Repo string `json:"gitlab_repo"`
|
||||
Path string `json:"gitlab_path"`
|
||||
Ref string `json:"gitlab_ref"`
|
||||
PId int `json:"gitlab_pid"`
|
||||
}
|
||||
|
||||
// Parse will parse config from address
|
||||
func Parse(addr string) (string, *Content, error) {
|
||||
URL, err := url.Parse(addr)
|
||||
@@ -198,3 +212,44 @@ func ByteCountIEC(b int64) string {
|
||||
return fmt.Sprintf("%.1f %ciB",
|
||||
float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// ParseGitlab will parse gitlab config from address
|
||||
func ParseGitlab(addr, repo string) (string, *Content, error) {
|
||||
if !strings.Contains(addr, repo) {
|
||||
return "", nil, errors.New("addon registry repo name invalid")
|
||||
}
|
||||
|
||||
// We support two valid format:
|
||||
// 1. https://example.gitlab.com/<owner>/<repo>
|
||||
// 2. https://example.gitlab.com/<owner>/<repo>/tree/<branch>
|
||||
URL, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
arr := strings.Split(addr, repo)
|
||||
owner := strings.Split(arr[0], URL.Host+"/")
|
||||
if !strings.Contains(arr[1], "/") {
|
||||
// https://example.gitlab.com/<owner>/<repo>
|
||||
return TypeGitlab, &Content{
|
||||
GitlabContent: GitlabContent{
|
||||
Host: URL.Scheme + "://" + URL.Host,
|
||||
Owner: owner[1][:len(owner[1])-1],
|
||||
Repo: repo,
|
||||
Ref: "", // use default branch
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// https://example.gitlab.com/<owner>/<repo>/tree/<branch>
|
||||
l := strings.Split(arr[1], "/")
|
||||
|
||||
return TypeGitlab, &Content{
|
||||
GitlabContent: GitlabContent{
|
||||
Host: URL.Scheme + "://" + URL.Host,
|
||||
Owner: owner[1][:len(owner[1])-1],
|
||||
Repo: repo,
|
||||
Ref: l[2],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -199,7 +199,11 @@ func listAddonRegistry(ctx context.Context, c common.Args) error {
|
||||
case registry.Helm != nil:
|
||||
repoType = "helm"
|
||||
repoURL = registry.Helm.URL
|
||||
case registry.Gitlab != nil:
|
||||
repoType = "gitlab"
|
||||
repoURL = registry.Gitlab.URL
|
||||
}
|
||||
|
||||
table.AddRow(registry.Name, repoType, repoURL)
|
||||
}
|
||||
fmt.Println(table.String())
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user