diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml index 2200be360..8773d206e 100644 --- a/.github/workflows/apiserver-test.yaml +++ b/.github/workflows/apiserver-test.yaml @@ -5,11 +5,13 @@ on: branches: - master - release-* + - apiserver workflow_dispatch: {} pull_request: branches: - master - release-* + - apiserver env: # Common versions @@ -60,6 +62,15 @@ jobs: version: ${{ env.KIND_VERSION }} skipClusterCreation: true + - name: Setup Kind Cluster (Worker) + run: | + kind delete cluster --name worker + kind create cluster --image kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4 --name worker + kubectl version + kubectl cluster-info + kind get kubeconfig --name worker --internal > /tmp/worker.kubeconfig + kind get kubeconfig --name worker > /tmp/worker.client.kubeconfig + - name: Setup Kind Cluster (Hub) run: | kind delete cluster @@ -67,7 +78,7 @@ jobs: kubectl version kubectl cluster-info - - name: Load Image to kind cluster (Hub) + - name: Load Image to kind cluster run: make kind-load - name: Cleanup for e2e tests @@ -75,11 +86,22 @@ jobs: make e2e-cleanup make e2e-setup-core + make vela-cli + bin/vela addon enable fluxcd + timeout 600s bash -c -- 'while true; do kubectl get ns flux-system; if [ $? -eq 0 ] ; then break; else sleep 5; fi;done' + kubectl wait --for=condition=Ready pod -l app.kubernetes.io/name=vela-core,app.kubernetes.io/instance=kubevela -n vela-system --timeout=600s + kubectl wait --for=condition=Ready pod -l app=source-controller -n flux-system --timeout=600s + kubectl wait --for=condition=Ready pod -l app=helm-controller -n flux-system --timeout=600s + - name: Run apiserver unit test run: make unit-test-apiserver - name: Run apiserver e2e test - run: make e2e-apiserver-test + run: | + export ALIYUN_ACCESS_KEY_ID=${{ secrets.ALIYUN_ACCESS_KEY_ID }} + export ALIYUN_ACCESS_KEY_SECRET=${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} + export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + make e2e-apiserver-test - name: Stop kubevela, get profile run: make end-e2e-core diff --git a/Dockerfile b/Dockerfile index 3e888ea3b..57d77e277 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +ARG BASE_IMAGE="alpine:latest" # Build the manager binary FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.16-alpine as builder @@ -28,15 +29,10 @@ RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ -o manager-${TARGETARCH} main.go -RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ - go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ - -o apiserver-${TARGETARCH} cmd/apiserver/main.go - # Use alpine as base image due to the discussion in issue #1448 # You can replace distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details # Overwrite `BASE_IMAGE` by passing `--build-arg=BASE_IMAGE=gcr.io/distroless/static:nonroot` -ARG BASE_IMAGE FROM ${BASE_IMAGE:-alpine:latest} # This is required by daemon connnecting with cri RUN apk add --no-cache ca-certificates bash @@ -45,7 +41,6 @@ WORKDIR / ARG TARGETARCH COPY --from=builder /workspace/manager-${TARGETARCH} /usr/local/bin/manager -COPY --from=builder /workspace/apiserver-${TARGETARCH} /usr/local/bin/apiserver COPY entrypoint.sh /usr/local/bin/ diff --git a/Dockerfile.apiserver b/Dockerfile.apiserver new file mode 100644 index 000000000..4b39b7130 --- /dev/null +++ b/Dockerfile.apiserver @@ -0,0 +1,48 @@ +ARG BASE_IMAGE="alpine:latest" +# Build the manager binary +FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.16-alpine as builder +ARG GOPROXY +ENV GOPROXY=${GOPROXY:-https://goproxy.cn} +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Copy the go source +COPY cmd/core/main.go main.go +COPY cmd/apiserver/main.go cmd/apiserver/main.go +COPY apis/ apis/ +COPY pkg/ pkg/ +COPY version/ version/ + +# Build +ARG TARGETARCH +ARG VERSION +ARG GITVERSION + +RUN GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \ + go build -a -ldflags "-s -w -X github.com/oam-dev/kubevela/version.VelaVersion=${VERSION:-undefined} -X github.com/oam-dev/kubevela/version.GitRevision=${GITVERSION:-undefined}" \ + -o apiserver-${TARGETARCH} cmd/apiserver/main.go + +# Use alpine as base image due to the discussion in issue #1448 +# You can replace distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +# Overwrite `BASE_IMAGE` by passing `--build-arg=BASE_IMAGE=gcr.io/distroless/static:nonroot` + +FROM ${BASE_IMAGE:-alpine:latest} +# This is required by daemon connnecting with cri +RUN apk add --no-cache ca-certificates bash + +WORKDIR / + +ARG TARGETARCH +COPY --from=builder /workspace/apiserver-${TARGETARCH} /usr/local/bin/apiserver + +COPY entrypoint.sh /usr/local/bin/ + +ENTRYPOINT ["entrypoint.sh"] + +CMD ["apiserver"] diff --git a/Makefile b/Makefile index e5451a06a..8ddeb5194 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ endif # Image URL to use all building/pushing image targets VELA_CORE_IMAGE ?= vela-core:latest VELA_CORE_TEST_IMAGE ?= vela-core-test:$(GIT_COMMIT) +VELA_APISERVER_IMAGE ?= apiserver:latest VELA_RUNTIME_ROLLOUT_IMAGE ?= vela-runtime-rollout:latest VELA_RUNTIME_ROLLOUT_TEST_IMAGE ?= vela-runtime-rollout-test:$(GIT_COMMIT) RUNTIME_CLUSTER_CONFIG ?= /tmp/worker.kubeconfig @@ -55,7 +56,7 @@ unit-test-core: go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep -v apiserver) go test $(shell go list ./references/... | grep -v apiserver) unit-test-apiserver: - go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep apiserver) + go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep -E 'apiserver|velaql') # Build vela cli binary build: fmt vet lint staticcheck vela-cli kubectl-vela @@ -135,8 +136,12 @@ check-diff: reviewable @$(OK) branch is clean # Build the docker image -docker-build: +docker-build: docker-build-core docker-build-apiserver + @$(OK) +docker-build-core: docker build --build-arg=VERSION=$(VELA_VERSION) --build-arg=GITVERSION=$(GIT_COMMIT) -t $(VELA_CORE_IMAGE) . +docker-build-apiserver: + docker build --build-arg=VERSION=$(VELA_VERSION) --build-arg=GITVERSION=$(GIT_COMMIT) -t $(VELA_APISERVER_IMAGE) -f Dockerfile.apiserver . # Build the runtime docker image docker-build-runtime-rollout: @@ -169,12 +174,14 @@ e2e-setup: kubectl wait --for=condition=Ready pod -l app=source-controller -n flux-system --timeout=600s kubectl wait --for=condition=Ready pod -l app=helm-controller -n flux-system --timeout=600s +build-swagger: + go run ./cmd/apiserver/main.go build-swagger ./docs/apidoc/swagger.json e2e-api-test: # Run e2e test ginkgo -v -skipPackage capability,setup,application -r e2e ginkgo -v -r e2e/application -e2e-apiserver-test: +e2e-apiserver-test: build-swagger go test -v -coverpkg=./... -coverprofile=/tmp/e2e_apiserver_test.out ./test/e2e-apiserver-test @$(OK) tests pass diff --git a/apis/types/capability.go b/apis/types/capability.go index 375b0d72e..cecb4d633 100644 --- a/apis/types/capability.go +++ b/apis/types/capability.go @@ -19,12 +19,14 @@ package types import ( "encoding/json" + "cuelang.org/go/cue" + "github.com/getkin/kin-openapi/openapi3" + "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - - "cuelang.org/go/cue" - "github.com/spf13/pflag" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" ) // Source record the source of Capability @@ -79,6 +81,8 @@ const CapabilityConfigMapNamePrefix = "schema-" const ( // OpenapiV3JSONSchema is the key to store OpenAPI v3 JSON schema in ConfigMap OpenapiV3JSONSchema string = "openapi-v3-json-schema" + // UISchema is the key to store ui custom schema + UISchema string = "ui-schema" ) // CapabilityCategory defines the category of a capability @@ -183,3 +187,50 @@ type Capability struct { KubeTemplate runtime.RawExtension `json:"kubetemplate,omitempty"` KubeParameter []common.KubeParameter `json:"kubeparameter,omitempty"` } + +// Addon contains all information represent an addon +type Addon struct { + AddonMeta + + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` + + // More details about the addon, e.g. README + Detail string `json:"detail,omitempty"` + Definitions []AddonElementFile `json:"definitions"` + Parameters string `json:"parameters"` + CUETemplates []AddonElementFile `json:"cue_templates"` + YAMLTemplates []AddonElementFile `json:"yaml_templates,omitempty"` + AppTemplate *v1beta1.Application `json:"app_template"` +} + +// AddonMeta defines the format for a single addon +type AddonMeta struct { + Name string `json:"name" validate:"required"` + Version string `json:"version"` + Description string `json:"description"` + Icon string `json:"icon"` + URL string `json:"url,omitempty"` + Tags []string `json:"tags,omitempty"` + DeployTo *AddonDeployTo `json:"deployTo,omitempty"` + Dependencies []*AddonDependency `json:"dependencies,omitempty"` + NeedNamespace []string `json:"needNamespace,omitempty"` +} + +// AddonDeployTo defines where the addon to deploy to +type AddonDeployTo struct { + ControlPlane bool `json:"control_plane"` + RuntimeCluster bool `json:"runtime_cluster"` +} + +// AddonDependency defines the other addons it depends on +type AddonDependency struct { + Name string `json:"name,omitempty"` +} + +// AddonElementFile can be addon's definition or addon's component +type AddonElementFile struct { + Data string + Name string + Path []string +} diff --git a/charts/vela-core/templates/addons/fluxcd.yaml b/charts/vela-core/templates/addons/fluxcd.yaml index ed1737aa8..35021e140 100644 --- a/charts/vela-core/templates/addons/fluxcd.yaml +++ b/charts/vela-core/templates/addons/fluxcd.yaml @@ -6020,6 +6020,12 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: "# fluxcd\n\nThis addon is built based [FluxCD](https://fluxcd.io/) \n\n## + install\n\n```shell\nvela addon enable fluxcd\n```\n\n## X-Definitions\n\nEnable + fluxcd addon to use these X-definitions\n\n- [helm](https://kubevela.io/docs/end-user/components/helm) + helps to deploy a helm chart from everywhere:\ngit repo / helm repo / S3 compatible + bucket.\n\n- [kustomize](https://kubevela.io/docs/end-user/components/kustomize) + helps to deploy a kustomize style artifact.\n" kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/istio.yaml b/charts/vela-core/templates/addons/istio.yaml index cc36828bf..8e4be31e2 100644 --- a/charts/vela-core/templates/addons/istio.yaml +++ b/charts/vela-core/templates/addons/istio.yaml @@ -245,6 +245,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # istio + + This addon provides istio support for vela rollout. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/kruise.yaml b/charts/vela-core/templates/addons/kruise.yaml index 45fed21c4..2a57c3a77 100644 --- a/charts/vela-core/templates/addons/kruise.yaml +++ b/charts/vela-core/templates/addons/kruise.yaml @@ -170,6 +170,10 @@ data: - name: apply-resources type: apply-application status: {} + detail: |- + # kruise + + This addon provides [open-kruise](https://github.com/openkruise/kruise) workload. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/observability.yaml b/charts/vela-core/templates/addons/observability.yaml index afccc88d3..72c5491ed 100644 --- a/charts/vela-core/templates/addons/observability.yaml +++ b/charts/vela-core/templates/addons/observability.yaml @@ -122,6 +122,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # observability + + This addon expose system and application level metrics for KubeVela. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/ocm-cluster-manager.yaml b/charts/vela-core/templates/addons/ocm-cluster-manager.yaml index 0c6f4f05f..9b6d35e24 100644 --- a/charts/vela-core/templates/addons/ocm-cluster-manager.yaml +++ b/charts/vela-core/templates/addons/ocm-cluster-manager.yaml @@ -503,6 +503,10 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: |- + # ocm-cluster-manager + + This addon aims to support multi-cluster application deployment. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml index 91e11a5d7..33520c91b 100644 --- a/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-alibaba.yaml @@ -43,6 +43,10 @@ data: region: '[[ index .Args "ALICLOUD_REGION" ]]' type: raw status: {} + detail: |- + # terraform/provider-alibaba + + This addon contains terraform provider for Alibaba Cloud. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-aws.yaml b/charts/vela-core/templates/addons/terraform-provider-aws.yaml index 111c4384e..4ae77de1e 100644 --- a/charts/vela-core/templates/addons/terraform-provider-aws.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-aws.yaml @@ -43,6 +43,10 @@ data: region: '[[ index .Args "AWS_DEFAULT_REGION" ]]' type: raw status: {} + detail: |- + # terraform/provider-aws + + This addon contains terraform provider for AWS kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform-provider-azure.yaml b/charts/vela-core/templates/addons/terraform-provider-azure.yaml index 6d52af3b9..1dc89d886 100644 --- a/charts/vela-core/templates/addons/terraform-provider-azure.yaml +++ b/charts/vela-core/templates/addons/terraform-provider-azure.yaml @@ -43,6 +43,10 @@ data: provider: azure type: raw status: {} + detail: |- + # terraform/provider-azure + + This addon contains terraform provider for Azure. kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/addons/terraform.yaml b/charts/vela-core/templates/addons/terraform.yaml index cfa828c11..fad176325 100644 --- a/charts/vela-core/templates/addons/terraform.yaml +++ b/charts/vela-core/templates/addons/terraform.yaml @@ -584,6 +584,8 @@ data: - name: apply-resources type: apply-remaining status: {} + detail: "# Terraform\n\nThis addon contains terraform operation kit, which allows + you to arrange, \ngenerate and use cloud service from different cloud vendor." kind: ConfigMap metadata: annotations: diff --git a/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml index 5b007b43d..b0bbabae2 100644 --- a/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml +++ b/charts/vela-core/templates/defwithtemplate/deploy2runtime.yaml @@ -32,10 +32,10 @@ spec: "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used diff --git a/charts/vela-core/templates/velaql-views/component-pod-view.yaml b/charts/vela-core/templates/velaql-views/component-pod-view.yaml new file mode 100644 index 000000000..e16ec99b8 --- /dev/null +++ b/charts/vela-core/templates/velaql-views/component-pod-view.yaml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: component-pod-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + "vela/op" + ) + + parameter: { + appName: string + appNs: string + name: string + cluster?: string + clusterNs?: string + } + + appList: ql.#ListResourcesInApp & { + app: { + name: parameter.appName + namespace: parameter.appNs + components: [parameter.name] + filter: { + if parameter.cluster != _|_ { + cluster: parameter.cluster + } + if parameter.clusterNs != _|_ { + clusterNamespace: parameter.clusterNs + } + } + } + } + + if appList.err == _|_ { + appRev: appList.list[0].revision + appPublishVersion: appList.list[0].publishVersion + appDeployVersion: appList.list[0].deployVersion + resources: appList.list[0].components[0].resources + collectedPods: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: [ for pods in collectedPods for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }] + + status: { + podList: [ for pod in podsWithCluster { + clusterName: pod.cluster + revision: appRev + publishVersion: appPublishVersion + deployVersion: appDeployVersion + podName: pod.obj.metadata.name + podNs: pod.obj.metadata.namespace + status: pod.obj.status.phase + // refer to https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + if status != "Pending" && status != "Unknown" { + podIP: pod.obj.status.podIP + hostIP: pod.obj.status.hostIP + nodeName: pod.obj.spec.nodeName + } + }] + } + } + + if appList.err != _|_ { + status: { + error: appList.err + } + } diff --git a/charts/vela-core/templates/velaql-views/pod-view.yaml b/charts/vela-core/templates/velaql-views/pod-view.yaml new file mode 100644 index 000000000..9ba8258d0 --- /dev/null +++ b/charts/vela-core/templates/velaql-views/pod-view.yaml @@ -0,0 +1,80 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pod-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + ) + + parameter: { + name: string + namespace: string + cluster: *"" | string + } + + pod: ql.#Read & { + value: { + apiVersion: "v1" + kind: "Pod" + metadata: { + name: parameter.name + namespace: parameter.namespace + } + } + cluster: parameter.cluster + } + + eventList: ql.#SearchEvents & { + value: { + apiVersion: "v1" + kind: "Pod" + metadata: pod.value.metadata + } + cluster: parameter.cluster + } + + usageMetrics: ql.#Read & { + cluster: parameter.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: parameter.name + namespace: parameter.namespace + } + } + } + + status: { + if pod.err == _|_ { + containers: [ for container in pod.value.spec.containers { + name: container.name + image: container.image + status: {for containerStatus in pod.value.status.containerStatuses { + if containerStatus.name == container.name { + state: containerStatus.state + restartCount: containerStatus.restartCount + } + }} + resource: container.resources + if usageMetrics.err == _|_ { + usageResource: {for containerUsage in usageMetrics.value.containers { + if containerUsage.name == container.name { + cpu: containerUsage.usage.cpu + memory: containerUsage.usage.memory + } + }} + } + }] + if eventList.err == _|_ { + events: eventList.list + } + } + if pod.err != _|_ { + error: pod.err + } + } + diff --git a/charts/vela-core/templates/velaql-views/resource-view.yaml b/charts/vela-core/templates/velaql-views/resource-view.yaml new file mode 100644 index 000000000..a6e33e727 --- /dev/null +++ b/charts/vela-core/templates/velaql-views/resource-view.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: resource-view + namespace: {{.Values.systemDefinitionNamespace}} +data: + template: | + import ( + "vela/ql" + ) + + parameter: { + type: string + namespace: *"" | string + cluster: *"" | string + } + + schema: { + "secret": { + apiVersion: "v1" + kind: "Secret" + } + "configMap": { + apiVersion: "v1" + kind: "ConfigMap" + } + "pvc": { + apiVersion: "v1" + kind: "PersistentVolumeClaim" + } + "storageClass": { + apiVersion: "storage.k8s.io/v1" + kind: "StorageClass" + } + "ns": { + apiVersion: "v1" + kind: "Namespace" + } + } + + List: ql.#List & { + resource: schema[parameter.type] + filter: { + namespace: parameter.namespace + } + cluster: parameter.cluster + } + + status: { + if List.err == _|_ { + if len(List.list.items) == 0 { + error: "failed to list \(parameter.type) in namespace \(parameter.namespace)" + } + if len(List.list.items) != 0 { + list: List.list.items + } + } + + if List.err != _|_ { + error: List.err + } + } diff --git a/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml index 5b007b43d..b0bbabae2 100644 --- a/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml +++ b/charts/vela-minimal/templates/defwithtemplate/deploy2runtime.yaml @@ -32,10 +32,10 @@ spec: "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { // +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index d702c0c2c..d211b6186 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -18,11 +18,17 @@ package main import ( "context" + "encoding/json" "flag" "fmt" "os" "os/signal" "syscall" + "time" + + restfulspec "github.com/emicklei/go-restful-openapi/v2" + "github.com/go-openapi/spec" + "github.com/google/uuid" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/rest" @@ -36,22 +42,57 @@ func main() { flag.StringVar(&s.restCfg.Datastore.Type, "datastore-type", "kubeapi", "Metadata storage driver type, support kubeapi and mongodb") flag.StringVar(&s.restCfg.Datastore.Database, "datastore-database", "kubevela", "Metadata storage database name, takes effect when the storage driver is mongodb.") flag.StringVar(&s.restCfg.Datastore.URL, "datastore-url", "", "Metadata storage database url,takes effect when the storage driver is mongodb.") + flag.StringVar(&s.restCfg.LeaderConfig.ID, "id", uuid.New().String(), "the holder identity name") + flag.StringVar(&s.restCfg.LeaderConfig.LockName, "lock-name", "apiserver-lock", "the lease lock resource name") + flag.DurationVar(&s.restCfg.LeaderConfig.Duration, "duration", time.Second*5, "the lease lock resource name") flag.Parse() + if len(os.Args) > 2 && os.Args[1] == "build-swagger" { + func() { + swagger, err := s.buildSwagger() + if err != nil { + log.Logger.Fatal(err.Error()) + } + outData, err := json.MarshalIndent(swagger, "", "\t") + if err != nil { + log.Logger.Fatal(err.Error()) + } + swaggerFile, err := os.OpenFile(os.Args[2], os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) + if err != nil { + log.Logger.Fatal(err.Error()) + } + defer func() { + if err := swaggerFile.Close(); err != nil { + log.Logger.Errorf("close swagger file failure %s", err.Error()) + } + }() + _, err = swaggerFile.Write(outData) + if err != nil { + log.Logger.Fatal(err.Error()) + } + fmt.Println("build swagger config file success") + }() + return + } + srvc := make(chan struct{}) + ctx, cancel := context.WithCancel(context.Background()) go func() { - if err := s.run(); err != nil { + if err := s.run(ctx); err != nil { log.Logger.Errorf("failed to run apiserver: %v", err) } close(srvc) }() var term = make(chan os.Signal, 1) signal.Notify(term, os.Interrupt, syscall.SIGTERM) + select { case <-term: log.Logger.Infof("Received SIGTERM, exiting gracefully...") + cancel() case <-srvc: + cancel() os.Exit(1) } log.Logger.Infof("See you next time!") @@ -62,14 +103,21 @@ type Server struct { restCfg rest.Config } -func (s *Server) run() error { +func (s *Server) run(ctx context.Context) error { log.Logger.Infof("KubeVela information: version: %v, gitRevision: %v", version.VelaVersion, version.GitRevision) - ctx := context.Background() - server, err := rest.New(s.restCfg) if err != nil { return fmt.Errorf("create apiserver failed : %w ", err) } + return server.Run(ctx) } + +func (s *Server) buildSwagger() (*spec.Swagger, error) { + server, err := rest.New(s.restCfg) + if err != nil { + return nil, fmt.Errorf("create apiserver failed : %w ", err) + } + return restfulspec.BuildSwagger(server.RegisterServices()), nil +} diff --git a/cmd/core/main.go b/cmd/core/main.go index deb898698..3f29912ce 100644 --- a/cmd/core/main.go +++ b/cmd/core/main.go @@ -191,7 +191,7 @@ func main() { // wrapper the round tripper by multi cluster rewriter if enableClusterGateway { - if err := multicluster.Initialize(restConfig); err != nil { + if _, err := multicluster.Initialize(restConfig, true); err != nil { klog.ErrorS(err, "failed to enable multicluster") os.Exit(1) } diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index 38dbcaf31..e12eeacc9 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -1,2695 +1,6831 @@ { - "swagger": "2.0", - "info": { - "description": "Kubevela api doc", - "title": "Kubevela api doc", - "contact": { - "name": "kubevela", - "url": "https://kubevela.io/", - "email": "feedback@mail.kubevela.io" - }, - "license": { - "name": "Apache License 2.0", - "url": "https://github.com/oam-dev/kubevela/blob/master/LICENSE" - }, - "version": "v1beta1" - }, - "paths": { - "/api/v1/applications": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "list all applications", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "Fuzzy search based on name or description", - "name": "query", - "in": "query" - }, - { - "type": "string", - "description": "Namespace-based search", - "name": "namespace", - "in": "query" - }, - { - "type": "string", - "description": "Cluster-based search", - "name": "cluster", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create one application", - "operationId": "noop", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateApplicationRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail one application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "delete one application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}/components": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "gets the component topology of the application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "list components that deployed in define cluster", - "name": "cluster", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create component for application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateComponentRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}/deploy": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "deploy or update the application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}/policies": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create policy for application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreatePolicyRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}/policies/{policyName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail policy for application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the application policy", - "name": "policyName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "detail policy for application", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the application policy", - "name": "policyName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/applications/{name}/template": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "application" - ], - "summary": "create one application template", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the application", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateApplicationTemplateRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/clusters": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list all clusters", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "Fuzzy search based on name or description", - "name": "query", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "create cluster", - "operationId": "createKubeCluster", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/*v1.CreateClusterRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/clusters/{clusterName}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "detail cluster info", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the cluster", - "name": "clusterName", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/componentdefinitions": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "componentdefinition" - ], - "summary": "list all componentdefinition", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "if specified, query the componentdefinition supported by the cluster where the application resides.", - "name": "appName", - "in": "query" - }, - { - "type": "string", - "description": "if specified, query the componentdefinition supported by the cluster.", - "name": "clusterName", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/namespaces": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "list all namespaces", - "operationId": "noop", - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "create namespace", - "operationId": "noop", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.CreateNamespaceRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/namespaces/{namespace}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "get one namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/namespaces/{namespace}/applications/:appname": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "get the specified oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.ApplicationRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "namespace" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/policydefinitions": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "policydefinition" - ], - "summary": "list all policydefinition", - "operationId": "noop", - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/api/v1/workflows/{name}": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "detail application workflow", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow, Currently, the application name is used.", - "name": "name", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - }, - "put": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "create or update application workflow config", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.UpdateWorkflowRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/api/v1/workflows/{name}/records": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "query application workflow execution record", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the workflow", - "name": "name", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Query the page number.", - "name": "page", - "in": "path", - "required": true - }, - { - "type": "integer", - "description": "Query the page size number.", - "name": "pageSize", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/v1/catalogs": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "cluster" - ], - "summary": "list all clusters", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "Fuzzy search based on name or description", - "name": "query", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/map[string]string" - } - }, - "500": { - "description": "Bummer, something went wrong" - } - } - } - }, - "/v1/{namespace}/applications/:appname": { - "get": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "oam" - ], - "summary": "get the specified oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - }, - "delete": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "oam" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - }, - "/v1/{namespace}/applications/{appname}": { - "post": { - "consumes": [ - "application/xml", - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "tags": [ - "oam" - ], - "summary": "create or update oam application in the specified namespace", - "operationId": "noop", - "parameters": [ - { - "type": "string", - "description": "identifier of the namespace", - "name": "namespace", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "identifier of the oam application", - "name": "appname", - "in": "path", - "required": true - }, - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/v1.ApplicationRequest" - } - } - ], - "responses": { - "200": { - "description": "OK" - } - } - } - } - }, - "definitions": { - "common.AppRolloutStatus": { - "required": [ - "rollingState", - "batchRollingState", - "currentBatch", - "upgradedReplicas", - "upgradedReadyReplicas", - "lastTargetAppRevision" - ], - "properties": { - "LastSourceAppRevision": { - "type": "string" - }, - "batchRollingState": { - "type": "string" - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/definitions/condition.Condition" - } - }, - "currentBatch": { - "type": "integer", - "format": "int32" - }, - "lastAppliedPodTemplateIdentifier": { - "type": "string" - }, - "lastTargetAppRevision": { - "type": "string" - }, - "rollingState": { - "type": "string" - }, - "rolloutOriginalSize": { - "type": "integer", - "format": "int32" - }, - "rolloutTargetSize": { - "type": "integer", - "format": "int32" - }, - "targetGeneration": { - "type": "string" - }, - "upgradedReadyReplicas": { - "type": "integer", - "format": "int32" - }, - "upgradedReplicas": { - "type": "integer", - "format": "int32" - } - } - }, - "common.AppStatus": { - "properties": { - "appliedResources": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ClusterObjectReference" - } - }, - "components": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ObjectReference" - } - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/definitions/condition.Condition" - } - }, - "latestRevision": { - "$ref": "#/definitions/common.Revision" - }, - "observedGeneration": { - "type": "integer", - "format": "int64" - }, - "resourceTracker": { - "$ref": "#/definitions/v1.ObjectReference" - }, - "rollout": { - "$ref": "#/definitions/common.AppRolloutStatus" - }, - "services": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ApplicationComponentStatus" - } - }, - "status": { - "type": "string" - }, - "workflow": { - "$ref": "#/definitions/common.WorkflowStatus" - } - } - }, - "common.ApplicationComponent": { - "required": [ - "name", - "type" - ], - "properties": { - "externalRevision": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "type": "string" - }, - "scopes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "traits": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ApplicationTrait" - } - }, - "type": { - "type": "string" - } - } - }, - "common.ApplicationComponentStatus": { - "required": [ - "name", - "healthy" - ], - "properties": { - "env": { - "type": "string" - }, - "healthy": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ObjectReference" - } - }, - "traits": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ApplicationTraitStatus" - } - }, - "workloadDefinition": { - "$ref": "#/definitions/common.WorkloadGVK" - } - } - }, - "common.ApplicationTrait": { - "required": [ - "type" - ], - "properties": { - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "common.ApplicationTraitStatus": { - "required": [ - "type", - "healthy" - ], - "properties": { - "healthy": { - "type": "boolean" - }, - "message": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "common.ClusterObjectReference": { - "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", - "properties": { - "apiVersion": { - "description": "API version of the referent.", - "type": "string" - }, - "cluster": { - "type": "string" - }, - "creator": { - "type": "string" - }, - "fieldPath": { - "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", - "type": "string" - }, - "kind": { - "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "name": { - "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", - "type": "string" - }, - "namespace": { - "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", - "type": "string" - }, - "resourceVersion": { - "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", - "type": "string" - }, - "uid": { - "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", - "type": "string" - } - } - }, - "common.Revision": { - "required": [ - "name", - "revision" - ], - "properties": { - "name": { - "type": "string" - }, - "revision": { - "type": "integer", - "format": "int64" - }, - "revisionHash": { - "type": "string" - } - } - }, - "common.SubStepsStatus": { - "properties": { - "mode": { - "type": "string" - }, - "stepIndex": { - "type": "integer", - "format": "int32" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowSubStepStatus" - } - } - } - }, - "common.WorkflowStatus": { - "required": [ - "mode", - "suspend", - "terminated" - ], - "properties": { - "appRevision": { - "type": "string" - }, - "contextBackend": { - "$ref": "#/definitions/v1.ObjectReference" - }, - "mode": { - "type": "string" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowStepStatus" - } - }, - "suspend": { - "type": "boolean" - }, - "terminated": { - "type": "boolean" - } - } - }, - "common.WorkflowStepStatus": { - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phase": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "subSteps": { - "$ref": "#/definitions/common.SubStepsStatus" - }, - "type": { - "type": "string" - } - } - }, - "common.WorkflowSubStepStatus": { - "required": [ - "id" - ], - "properties": { - "id": { - "type": "string" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phase": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "common.WorkloadGVK": { - "required": [ - "apiVersion", - "kind" - ], - "properties": { - "apiVersion": { - "type": "string" - }, - "kind": { - "type": "string" - } - } - }, - "common.inputItem": { - "required": [ - "parameterKey", - "from" - ], - "properties": { - "from": { - "type": "string" - }, - "parameterKey": { - "type": "string" - } - } - }, - "common.outputItem": { - "required": [ - "valueFrom", - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "valueFrom": { - "type": "string" - } - } - }, - "condition.Condition": { - "required": [ - "type", - "status", - "lastTransitionTime", - "reason" - ], - "properties": { - "lastTransitionTime": { - "type": "string" - }, - "message": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "status": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "condition.ConditionedStatus": { - "properties": { - "conditions": { - "type": "array", - "items": { - "$ref": "#/definitions/condition.Condition" - } - } - } - }, - "map[string]string": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "types.Parameter": { - "required": [ - "name" - ], - "properties": { - "alias": { - "type": "string" - }, - "default": { - "$ref": "#/definitions/types.Parameter.default" - }, - "ignore": { - "type": "boolean" - }, - "jsonType": { - "type": "string" - }, - "name": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "short": { - "type": "string" - }, - "type": { - "type": "integer", - "format": "int32" - }, - "usage": { - "type": "string" - } - } - }, - "types.Parameter.default": {}, - "v1.ApplicationBase": { - "required": [ - "name", - "namespace", - "description", - "createTime", - "updateTime", - "icon", - "status", - "gatewayRule" - ], - "properties": { - "clusterList": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "gatewayRule": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.GatewayRule" - } - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "status": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - } - } - }, - "v1.ApplicationRequest": { - "required": [ - "components" - ], - "properties": { - "components": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ApplicationComponent" - } - }, - "policies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1beta1.AppPolicy" - } - }, - "workflow": { - "$ref": "#/definitions/v1beta1.Workflow" - } - } - }, - "v1.ApplicationResourceInfo": { - "required": [ - "componentNum" - ], - "properties": { - "componentNum": { - "type": "integer", - "format": "int32" - } - } - }, - "v1.ApplicationResponse": { - "required": [ - "apiVersion", - "kind", - "spec", - "status" - ], - "properties": { - "apiVersion": { - "type": "string" - }, - "kind": { - "type": "string" - }, - "spec": { - "$ref": "#/definitions/v1beta1.ApplicationSpec" - }, - "status": { - "$ref": "#/definitions/common.AppStatus" - } - } - }, - "v1.ApplicationTemplateBase": { - "required": [ - "templateName" - ], - "properties": { - "templateName": { - "type": "string" - }, - "versions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ApplicationTemplateVersion" - } - } - } - }, - "v1.ApplicationTemplateVersion": { - "required": [ - "version", - "description", - "createUser", - "createTime", - "updateTime" - ], - "properties": { - "createTime": { - "type": "string", - "format": "date-time" - }, - "createUser": { - "type": "string" - }, - "description": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - }, - "version": { - "type": "string" - } - } - }, - "v1.ClusterBase": { - "required": [ - "name", - "description", - "icon", - "labels", - "status", - "reason" - ], - "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "v1.ClusterResourceInfo": { - "required": [ - "workerNumber", - "masterNumber", - "memoryCapacity", - "cpuCapacity" - ], - "properties": { - "cpuCapacity": { - "type": "integer", - "format": "int64" - }, - "gpuCapacity": { - "type": "integer", - "format": "int64" - }, - "masterNumber": { - "type": "integer", - "format": "int32" - }, - "memoryCapacity": { - "type": "integer", - "format": "int64" - }, - "storageClassList": { - "type": "array", - "items": { - "type": "string" - } - }, - "workerNumber": { - "type": "integer", - "format": "int32" - } - } - }, - "v1.ComponentBase": { - "required": [ - "name", - "description", - "componentType", - "bindClusters", - "dependsOn", - "deployVersion" - ], - "properties": { - "bindClusters": { - "type": "array", - "items": { - "type": "string" - } - }, - "componentType": { - "type": "string" - }, - "creator": { - "type": "string" - }, - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "deployVersion": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "v1.ComponentDefinitionBase": { - "required": [ - "name", - "description", - "icon", - "requiredParams" - ], - "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "name": { - "type": "string" - }, - "requiredParams": { - "type": "array", - "items": { - "$ref": "#/definitions/types.Parameter" - } - } - } - }, - "v1.ComponentListResponse": { - "required": [ - "components" - ], - "properties": { - "components": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ComponentBase" - } - } - } - }, - "v1.CreateApplicationRequest": { - "required": [ - "name", - "namespace", - "description", - "icon" - ], - "properties": { - "clusterList": { - "type": "array", - "items": { - "type": "string" - } - }, - "deploy": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "yamlConfig": { - "type": "string" - } - } - }, - "v1.CreateApplicationTemplateRequest": { - "required": [ - "templateName", - "version", - "description" - ], - "properties": { - "description": { - "type": "string" - }, - "templateName": { - "type": "string" - }, - "version": { - "type": "string" - } - } - }, - "v1.CreateClusterRequest": { - "required": [ - "name", - "icon", - "kubeConfig" - ], - "properties": { - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "kubeConfig": { - "type": "string" - }, - "kubeConfigSecret": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - } - } - }, - "v1.CreateComponentRequest": { - "required": [ - "appName", - "name", - "description", - "componentType", - "bindClusters" - ], - "properties": { - "appName": { - "type": "string" - }, - "bindClusters": { - "type": "array", - "items": { - "type": "string" - } - }, - "componentType": { - "type": "string" - }, - "description": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "properties": { - "type": "string" - } - } - }, - "v1.CreateNamespaceRequest": { - "required": [ - "name", - "description" - ], - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.CreatePolicyRequest": { - "required": [ - "name", - "type", - "properties" - ], - "properties": { - "name": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.DetailApplicationResponse": { - "required": [ - "updateTime", - "icon", - "status", - "gatewayRule", - "name", - "namespace", - "description", - "createTime", - "policies", - "status", - "resourceInfo", - "workflowStatus" - ], - "properties": { - "clusterList": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - }, - "createTime": { - "type": "string", - "format": "date-time" - }, - "description": { - "type": "string" - }, - "gatewayRule": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.GatewayRule" - } - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "namespace": { - "type": "string" - }, - "policies": { - "type": "array", - "items": { - "type": "string" - } - }, - "resourceInfo": { - "$ref": "#/definitions/v1.ApplicationResourceInfo" - }, - "status": { - "type": "string" - }, - "updateTime": { - "type": "string", - "format": "date-time" - }, - "workflowStatus": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStepStatus" - } - } - } - }, - "v1.DetailClusterResponse": { - "required": [ - "name", - "description", - "icon", - "labels", - "status", - "reason", - "resourceInfo" - ], - "properties": { - "dashboardURL": { - "type": "string" - }, - "description": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "labels": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "name": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "remoteManageURL": { - "type": "string" - }, - "resourceInfo": { - "$ref": "#/definitions/v1.ClusterResourceInfo" - }, - "status": { - "type": "string" - } - } - }, - "v1.DetailPolicyResponse": { - "required": [ - "name", - "type", - "properties" - ], - "properties": { - "name": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.DetailWorkflowResponse": { - "required": [ - "enable", - "workflowRecord" - ], - "properties": { - "enable": { - "type": "boolean" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - }, - "workflowRecord": { - "$ref": "#/definitions/v1.WorkflowRecord" - } - } - }, - "v1.GatewayRule": { - "required": [ - "ruleType", - "address", - "protocol", - "componentName", - "componentPort" - ], - "properties": { - "address": { - "type": "string" - }, - "componentName": { - "type": "string" - }, - "componentPort": { - "type": "integer", - "format": "int32" - }, - "protocol": { - "type": "string" - }, - "ruleType": { - "type": "string" - } - } - }, - "v1.ListApplicationResponse": { - "required": [ - "applications" - ], - "properties": { - "applications": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ApplicationBase" - } - } - } - }, - "v1.ListClusterResponse": { - "required": [ - "clusters" - ], - "properties": { - "clusters": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ClusterBase" - } - } - } - }, - "v1.ListComponentDefinitionResponse": { - "required": [ - "componentDefinitions" - ], - "properties": { - "componentDefinitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ComponentDefinitionBase" - } - } - } - }, - "v1.ListNamespaceResponse": { - "required": [ - "namesapces" - ], - "properties": { - "namesapces": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.NamesapceBase" - } - } - } - }, - "v1.ListPolicyDefinitionResponse": { - "required": [ - "policyDefinitions" - ], - "properties": { - "policyDefinitions": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.PolicyDefinition" - } - } - } - }, - "v1.ListWorkflowRecordsResponse": { - "required": [ - "records", - "total" - ], - "properties": { - "records": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowRecord" - } - }, - "total": { - "type": "integer", - "format": "int64" - } - } - }, - "v1.NamesapceBase": { - "required": [ - "name", - "description" - ], - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.NamesapceDetailResponse": { - "required": [ - "name", - "description", - "clusterBind" - ], - "properties": { - "clusterBind": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } - }, - "v1.ObjectReference": { - "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", - "properties": { - "apiVersion": { - "description": "API version of the referent.", - "type": "string" - }, - "fieldPath": { - "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", - "type": "string" - }, - "kind": { - "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "name": { - "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", - "type": "string" - }, - "namespace": { - "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", - "type": "string" - }, - "resourceVersion": { - "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", - "type": "string" - }, - "uid": { - "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", - "type": "string" - } - } - }, - "v1.PolicyDefinition": { - "required": [ - "name", - "description", - "parameters" - ], - "properties": { - "description": { - "type": "string" - }, - "name": { - "type": "string" - }, - "parameters": { - "type": "array", - "items": { - "$ref": "#/definitions/types.Parameter" - } - } - } - }, - "v1.UpdateWorkflowRequest": { - "required": [ - "enable" - ], - "properties": { - "enable": { - "type": "boolean" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.WorkflowStep" - } - } - } - }, - "v1.WorkflowRecord": {}, - "v1.WorkflowStep": { - "required": [ - "name", - "type" - ], - "properties": { - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1.WorkflowStepStatus": { - "required": [ - "name", - "status", - "takeTime" - ], - "properties": { - "name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "takeTime": { - "type": "integer", - "format": "integer" - } - } - }, - "v1alpha1.CanaryMetric": { - "required": [ - "name" - ], - "properties": { - "interval": { - "type": "string" - }, - "metricsRange": { - "$ref": "#/definitions/v1alpha1.MetricsExpectedRange" - }, - "name": { - "type": "string" - }, - "templateRef": { - "$ref": "#/definitions/v1.ObjectReference" - } - } - }, - "v1alpha1.MetricsExpectedRange": { - "properties": { - "max": { - "type": "string" - }, - "min": { - "type": "string" - } - } - }, - "v1alpha1.RolloutBatch": { - "properties": { - "batchRolloutWebhooks": { - "type": "array", - "items": { - "$ref": "#/definitions/v1alpha1.RolloutWebhook" - } - }, - "canaryMetric": { - "type": "array", - "items": { - "$ref": "#/definitions/v1alpha1.CanaryMetric" - } - }, - "instanceInterval": { - "type": "integer", - "format": "int32" - }, - "maxUnavailable": { - "type": "string" - }, - "podList": { - "type": "array", - "items": { - "type": "string" - } - }, - "replicas": { - "type": "string" - } - } - }, - "v1alpha1.RolloutPlan": { - "properties": { - "batchPartition": { - "type": "integer", - "format": "int32" - }, - "canaryMetric": { - "type": "array", - "items": { - "$ref": "#/definitions/v1alpha1.CanaryMetric" - } - }, - "numBatches": { - "type": "integer", - "format": "int32" - }, - "paused": { - "type": "boolean" - }, - "rolloutBatches": { - "type": "array", - "items": { - "$ref": "#/definitions/v1alpha1.RolloutBatch" - } - }, - "rolloutStrategy": { - "type": "string" - }, - "rolloutWebhooks": { - "type": "array", - "items": { - "$ref": "#/definitions/v1alpha1.RolloutWebhook" - } - }, - "targetSize": { - "type": "integer", - "format": "int32" - } - } - }, - "v1alpha1.RolloutStatus": { - "required": [ - "rollingState", - "batchRollingState", - "currentBatch", - "upgradedReplicas", - "upgradedReadyReplicas" - ], - "properties": { - "batchRollingState": { - "type": "string" - }, - "conditions": { - "type": "array", - "items": { - "$ref": "#/definitions/condition.Condition" - } - }, - "currentBatch": { - "type": "integer", - "format": "int32" - }, - "lastAppliedPodTemplateIdentifier": { - "type": "string" - }, - "rollingState": { - "type": "string" - }, - "rolloutOriginalSize": { - "type": "integer", - "format": "int32" - }, - "rolloutTargetSize": { - "type": "integer", - "format": "int32" - }, - "targetGeneration": { - "type": "string" - }, - "upgradedReadyReplicas": { - "type": "integer", - "format": "int32" - }, - "upgradedReplicas": { - "type": "integer", - "format": "int32" - } - } - }, - "v1alpha1.RolloutWebhook": { - "required": [ - "type", - "name", - "url" - ], - "properties": { - "expectedStatus": { - "type": "array", - "items": { - "type": "integer" - } - }, - "metadata": { - "$ref": "#/definitions/v1alpha1.RolloutWebhook.metadata" - }, - "method": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "v1alpha1.RolloutWebhook.metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "v1beta1.AppPolicy": { - "required": [ - "name", - "type" - ], - "properties": { - "name": { - "type": "string" - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "v1beta1.ApplicationSpec": { - "required": [ - "components" - ], - "properties": { - "components": { - "type": "array", - "items": { - "$ref": "#/definitions/common.ApplicationComponent" - } - }, - "policies": { - "type": "array", - "items": { - "$ref": "#/definitions/v1beta1.AppPolicy" - } - }, - "rolloutPlan": { - "$ref": "#/definitions/v1alpha1.RolloutPlan" - }, - "workflow": { - "$ref": "#/definitions/v1beta1.Workflow" - } - } - }, - "v1beta1.Workflow": { - "properties": { - "steps": { - "type": "array", - "items": { - "$ref": "#/definitions/v1beta1.WorkflowStep" - } - } - } - }, - "v1beta1.WorkflowStep": { - "required": [ - "name", - "type" - ], - "properties": { - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - } + "swagger": "2.0", + "info": { + "description": "Kubevela api doc", + "title": "Kubevela api doc", + "contact": { + "name": "kubevela", + "url": "https://kubevela.io/", + "email": "feedback@mail.kubevela.io" + }, + "license": { + "name": "Apache License 2.0", + "url": "https://github.com/oam-dev/kubevela/blob/master/LICENSE" + }, + "version": "v1beta1" + }, + "paths": { + "/api/v1/addon_registries": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "list all addon registry", + "operationId": "listAddonRegistry", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonRegistryResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "create an addon registry", + "operationId": "createAddonRegistry", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateAddonRegistryRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addon_registries/{name}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "update an addon registry", + "operationId": "updateAddonRegistry", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateAddonRegistryRequest" + } + }, + { + "type": "string", + "description": "identifier of the addon registry", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon_registry" + ], + "summary": "delete an addon registry", + "operationId": "deleteAddonRegistry", + "parameters": [ + { + "type": "string", + "description": "identifier of the addon registry", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "list all addons", + "operationId": "listAddons", + "parameters": [ + { + "type": "string", + "description": "filter addons from given registry", + "name": "registry", + "in": "query" + }, + { + "type": "string", + "description": "Fuzzy search based on name and description.", + "name": "query", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show details of an addon", + "operationId": "detailAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query detail", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "filter addons from given registry", + "name": "registry", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailAddonResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/disable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "disable an addon", + "operationId": "disableAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/enable": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "enable an addon", + "operationId": "enableAddon", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.EnableAddonRequest" + } + }, + { + "type": "string", + "description": "addon name to enable", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/addons/{name}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "addon" + ], + "summary": "show status of an addon", + "operationId": "statusAddon", + "parameters": [ + { + "type": "string", + "description": "addon name to query status", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.AddonStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list all applications", + "operationId": "listApplications", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "The namespace of the managed cluster", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Name of the application delivery target", + "name": "target", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create one application ", + "operationId": "createApplication", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail one application ", + "operationId": "detailApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailApplicationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update one application ", + "operationId": "updateApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete one application", + "operationId": "deleteApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "gets the list of application components", + "operationId": "listApplicationComponents", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "list components that deployed in define env", + "name": "envName", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentListResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create component for application ", + "operationId": "createComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateComponentRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components/{compName}/traits": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "add trait for a component", + "operationId": "addApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationTraitRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components/{compName}/traits/{traitType}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update trait from a component", + "operationId": "updateApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the type of trait", + "name": "traitType", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationTraitRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTrait" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete trait from a component", + "operationId": "deleteApplicationTrait", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the component", + "name": "compName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the type of trait", + "name": "traitType", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTrait" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/components/{componentName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail component for application ", + "operationId": "detailComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailComponentResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update component config", + "operationId": "updateComponent", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateApplicationComponentRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ComponentBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/deploy": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "deploy or upgrade the application", + "operationId": "deployApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationDeployRequest" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/envs": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list policy for application", + "operationId": "listApplicationEnvs", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationEnvBinding" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "creating an application environment ", + "operationId": "createApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationEnvRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBinding" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/envs/{envName}": { + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "set application differences in the specified environment", + "operationId": "updateApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the envBinding ", + "name": "envName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.PutApplicationEnvRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EnvBinding" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete an application environment ", + "operationId": "deleteApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the envBinding ", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "404": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/envs/{envName}/recycle": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "get application status", + "operationId": "recycleApplicationEnv", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application envbinding", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/envs/{envName}/status": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "get application status", + "operationId": "getApplicationStatus", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application envbinding", + "name": "envName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationStatusResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/policies": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list policy for application", + "operationId": "listApplicationPolicies", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListApplicationPolicy" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create policy for application", + "operationId": "createApplicationPolicy", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreatePolicyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.PolicyBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/policies/{policyName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail policy for application", + "operationId": "detailApplicationPolicy", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application policy", + "name": "policyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update policy for application", + "operationId": "updateApplicationPolicy", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application policy", + "name": "policyName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdatePolicyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailPolicyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail policy for application", + "operationId": "deleteApplicationPolicy", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application policy", + "name": "policyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/revisions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list revisions for application", + "operationId": "listApplicationRevisions", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "query identifier of the env", + "name": "envName", + "in": "query" + }, + { + "type": "string", + "description": "query identifier of the status", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "query the page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "query the page size number", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListRevisionsResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/revisions/{revision}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail revision for application", + "operationId": "detailApplicationRevision", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application revision", + "name": "revision", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailRevisionResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/statistics": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail one application ", + "operationId": "applicationStatistics", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationStatisticsResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/template": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create one application template", + "operationId": "publishApplicationTemplate", + "parameters": [ + { + "type": "string", + "description": "identifier of the application ", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationTemplateRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationTemplateBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/workflows": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list application workflow", + "operationId": "listApplicationWorkflows", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create application workflow", + "operationId": "createApplicationWorkflow", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateWorkflowRequest" + } + }, + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail application workflow", + "operationId": "detailWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workfloc.", + "name": "workflowName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "update application workflow config", + "operationId": "updateWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateWorkflowRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "deletet workflow", + "operationId": "deleteWorkflow", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "query application workflow execution record", + "operationId": "listWorkflowRecords", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "query the page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "query the page size number", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "query application workflow execution record detail", + "operationId": "detailWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/resume": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "resume suspend workflow record", + "operationId": "resumeWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/rollback": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "rollback suspend application record", + "operationId": "rollbackWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the rollback revision", + "name": "rollbackVersion", + "in": "query" + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/applications/{name}/workflows/{workflowName}/records/{record}/terminate": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "terminate suspend workflow record", + "operationId": "terminateWorkflowRecord", + "parameters": [ + { + "type": "string", + "description": "identifier of the application.", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow", + "name": "workflowName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the workflow record", + "name": "record", + "in": "path", + "required": true + } + ], + "responses": { + "200": {}, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list all clusters", + "operationId": "listKubeClusters", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + }, + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cluster", + "operationId": "createKubeCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.CreateClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud clusters", + "operationId": "listCloudClusters", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "int", + "default": 0, + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "int", + "default": 20, + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.AccessKeyRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/connect": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cluster from cloud cluster", + "operationId": "connectCloudCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.ConnectCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/create": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cloud cluster", + "operationId": "createCloudCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list cloud cluster creation", + "operationId": "listCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListCloudClusterCreationResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/cloud-clusters/{provider}/creation/{cloudClusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "check cloud cluster create status", + "operationId": "getCloudClusterCreationStatus", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cloud cluster creation", + "operationId": "deleteCloudClusterCreation", + "parameters": [ + { + "type": "string", + "description": "identifier of the cloud provider", + "name": "provider", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier for cloud cluster which is creating", + "name": "cloudClusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/{clusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "detail cluster info", + "operationId": "getKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.DetailClusterResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "modify cluster", + "operationId": "modifyKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateClusterRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "delete cluster", + "operationId": "deleteKubeCluster", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/clusters/{clusterName}/namespaces": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create namespace in cluster", + "operationId": "createNamespace", + "parameters": [ + { + "type": "string", + "description": "name of the target cluster", + "name": "clusterName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateClusterNamespaceRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.CreateClusterNamespaceResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/definitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "list all definitions", + "operationId": "listDefinitions", + "parameters": [ + { + "enum": [ + "workflowstep", + "component", + "trait" + ], + "type": "string", + "description": "query the definition type", + "name": "type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "if specified, query the definition supported by the env.", + "name": "envName", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/definitions/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "detail definition", + "operationId": "detailDefinition", + "parameters": [ + { + "type": "string", + "description": "identifier of the definition", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "query the definition type", + "name": "type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/deliveryTargets": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "list deliveryTarget", + "operationId": "listDeliveryTargets", + "parameters": [ + { + "type": "string", + "description": "Query the delivery target belonging to a namespace", + "name": "namesapce", + "in": "query" + }, + { + "type": "integer", + "description": "Page for paging", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "PageSize for paging", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "create deliveryTarget", + "operationId": "createDeliveryTarget", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateDeliveryTargetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "400": { + "description": "create failure", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/deliveryTargets/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "detail deliveryTarget", + "operationId": "detailDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget.", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "update application DeliveryTarget config", + "operationId": "updateDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateDeliveryTargetRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "deliveryTarget" + ], + "summary": "deletet DeliveryTarget", + "operationId": "deleteDeliveryTarget", + "parameters": [ + { + "type": "string", + "description": "identifier of the deliveryTarget", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/namespaces": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "list all namespaces", + "operationId": "listNamespaces", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListNamespaceResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "create namespace", + "operationId": "createNamespace", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateNamespaceRequest" + } + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.NamespaceDetailResponse" + } + } + } + } + }, + "/api/v1/policydefinitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "definition" + ], + "summary": "list all policydefinition", + "operationId": "noop", + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ListPolicyDefinitionResponse" + } + } + } + } + }, + "/api/v1/query": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "velaQL" + ], + "summary": "use velaQL to query resource status", + "operationId": "queryView", + "parameters": [ + { + "type": "string", + "description": "velaql query statement", + "name": "velaql", + "in": "query" + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.VelaQLViewResponse" + } + }, + "400": { + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/v1/namespaces/{namespace}/applications/{appname}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam-application" + ], + "summary": "get the specified oam application in the specified namespace", + "operationId": "getApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "schema": { + "$ref": "#/definitions/v1.ApplicationResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam-application" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "createOrUpdateApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.ApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam-application" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "deleteApplication", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "addon.GitAddonSource": { + "properties": { + "path": { + "type": "string" + }, + "token": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "bcode.Bcode": { + "required": [ + "BusinessCode", + "Message" + ], + "properties": { + "BusinessCode": { + "type": "integer", + "format": "int32" + }, + "Message": { + "type": "string" + } + } + }, + "cloudprovider.CloudCluster": { + "required": [ + "provider", + "id", + "name", + "type", + "zone", + "zoneID", + "regionID", + "vpcID", + "labels", + "status", + "apiServerURL", + "dashboardURL" + ], + "properties": { + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "id": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "regionID": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + }, + "vpcID": { + "type": "string" + }, + "zone": { + "type": "string" + }, + "zoneID": { + "type": "string" + } + } + }, + "common.AppRolloutStatus": { + "required": [ + "rollingState", + "currentBatch", + "upgradedReadyReplicas", + "batchRollingState", + "upgradedReplicas", + "lastTargetAppRevision" + ], + "properties": { + "LastSourceAppRevision": { + "type": "string" + }, + "batchRollingState": { + "type": "string" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "currentBatch": { + "type": "integer", + "format": "int32" + }, + "lastAppliedPodTemplateIdentifier": { + "type": "string" + }, + "lastTargetAppRevision": { + "type": "string" + }, + "rollingState": { + "type": "string" + }, + "rolloutOriginalSize": { + "type": "integer", + "format": "int32" + }, + "rolloutTargetSize": { + "type": "integer", + "format": "int32" + }, + "targetGeneration": { + "type": "string" + }, + "upgradedReadyReplicas": { + "type": "integer", + "format": "int32" + }, + "upgradedReplicas": { + "type": "integer", + "format": "int32" + } + } + }, + "common.AppStatus": { + "properties": { + "appliedResources": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ClusterObjectReference" + } + }, + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ObjectReference" + } + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "latestRevision": { + "$ref": "#/definitions/common.Revision" + }, + "observedGeneration": { + "type": "integer", + "format": "int64" + }, + "policy": { + "type": "array", + "items": { + "$ref": "#/definitions/common.PolicyStatus" + } + }, + "resourceTracker": { + "$ref": "#/definitions/v1.ObjectReference" + }, + "rollout": { + "$ref": "#/definitions/common.AppRolloutStatus" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponentStatus" + } + }, + "status": { + "type": "string" + }, + "workflow": { + "$ref": "#/definitions/common.WorkflowStatus" + } + } + }, + "common.ApplicationComponent": { + "required": [ + "name", + "type" + ], + "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "externalRevision": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationTrait" + } + }, + "type": { + "type": "string" + } + } + }, + "common.ApplicationComponentStatus": { + "required": [ + "name", + "healthy" + ], + "properties": { + "env": { + "type": "string" + }, + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ObjectReference" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationTraitStatus" + } + }, + "workloadDefinition": { + "$ref": "#/definitions/common.WorkloadGVK" + } + } + }, + "common.ApplicationTrait": { + "required": [ + "type" + ], + "properties": { + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.ApplicationTraitStatus": { + "required": [ + "type", + "healthy" + ], + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.ClusterObjectReference": { + "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "cluster": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "fieldPath": { + "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", + "type": "string" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "type": "string" + }, + "resourceVersion": { + "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", + "type": "string" + } + } + }, + "common.PolicyStatus": { + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.Revision": { + "required": [ + "name", + "revision" + ], + "properties": { + "name": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "revisionHash": { + "type": "string" + } + } + }, + "common.SubStepsStatus": { + "properties": { + "mode": { + "type": "string" + }, + "stepIndex": { + "type": "integer", + "format": "int32" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowSubStepStatus" + } + } + } + }, + "common.WorkflowStatus": { + "required": [ + "mode", + "suspend", + "terminated", + "finished" + ], + "properties": { + "appRevision": { + "type": "string" + }, + "contextBackend": { + "$ref": "#/definitions/v1.ObjectReference" + }, + "finished": { + "type": "boolean" + }, + "mode": { + "type": "string" + }, + "startTime": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, + "common.WorkflowStepStatus": { + "required": [ + "id" + ], + "properties": { + "firstExecuteTime": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastExecuteTime": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "subSteps": { + "$ref": "#/definitions/common.SubStepsStatus" + }, + "type": { + "type": "string" + } + } + }, + "common.WorkflowSubStepStatus": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.WorkloadGVK": { + "required": [ + "apiVersion", + "kind" + ], + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + } + } + }, + "common.inputItem": { + "required": [ + "parameterKey", + "from" + ], + "properties": { + "from": { + "type": "string" + }, + "parameterKey": { + "type": "string" + } + } + }, + "common.outputItem": { + "required": [ + "valueFrom", + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "valueFrom": { + "type": "string" + } + } + }, + "condition.Condition": { + "required": [ + "type", + "status", + "lastTransitionTime", + "reason" + ], + "properties": { + "lastTransitionTime": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "condition.ConditionedStatus": { + "properties": { + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + } + } + }, + "map[string]string": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "model.ApplicationComponent": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "creator", + "name", + "alias", + "type" + ], + "properties": { + "alias": { + "type": "string" + }, + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTrait" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.ApplicationRevision": { + "required": [ + "createTime", + "updateTime", + "appPrimaryKey", + "version", + "status", + "reason", + "deployUser", + "note", + "triggerType", + "workflowName", + "envName" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "applyAppConfig": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, + "model.ApplicationTrait": { + "required": [ + "alias", + "description", + "type", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.Cluster": { + "required": [ + "model", + "name", + "alias", + "description", + "icon", + "labels", + "status", + "reason", + "provider", + "apiServerURL", + "dashboardURL", + "kubeConfig", + "kubeConfigSecret" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "model": { + "$ref": "#/definitions/model.Model" + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "model.JSONStruct": { + "type": "object" + }, + "model.Model": { + "required": [ + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "model.ProviderInfo": { + "required": [ + "provider", + "clusterID", + "labels" + ], + "properties": { + "clusterID": { + "type": "string" + }, + "clusterName": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "provider": { + "type": "string" + }, + "regionID": { + "type": "string" + }, + "vpcID": { + "type": "string" + }, + "zone": { + "type": "string" + }, + "zoneID": { + "type": "string" + } + } + }, + "types.AddonDependency": { + "properties": { + "name": { + "type": "string" + } + } + }, + "types.AddonDeployTo": { + "required": [ + "control_plane", + "runtime_cluster" + ], + "properties": { + "control_plane": { + "type": "boolean" + }, + "runtime_cluster": { + "type": "boolean" + } + } + }, + "types.AddonMeta": { + "required": [ + "name", + "version", + "description", + "icon" + ], + "properties": { + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AddonDependency" + } + }, + "deployTo": { + "$ref": "#/definitions/types.AddonDeployTo" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "types.Parameter": { + "required": [ + "name" + ], + "properties": { + "alias": { + "type": "string" + }, + "default": { + "$ref": "#/definitions/types.Parameter.default" + }, + "ignore": { + "type": "boolean" + }, + "jsonType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "short": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "int32" + }, + "usage": { + "type": "string" + } + } + }, + "types.Parameter.default": {}, + "utils.GroupOption": { + "required": [ + "label", + "keys" + ], + "properties": { + "keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "label": { + "type": "string" + } + } + }, + "utils.Option": { + "required": [ + "label", + "value" + ], + "properties": { + "label": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/utils.Option.value" + } + } + }, + "utils.Option.value": {}, + "utils.UIParameter": { + "required": [ + "sort", + "label", + "description", + "jsonKey", + "uiType" + ], + "properties": { + "description": { + "type": "string" + }, + "disable": { + "type": "boolean" + }, + "jsonKey": { + "type": "string" + }, + "label": { + "type": "string" + }, + "sort": { + "type": "integer", + "format": "integer" + }, + "subParameterGroupOption": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.GroupOption" + } + }, + "subParameters": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + }, + "uiType": { + "type": "string" + }, + "validate": { + "$ref": "#/definitions/utils.Validate" + } + } + }, + "utils.Validate": { + "properties": { + "defaultValue": { + "$ref": "#/definitions/utils.Validate.defaultValue" + }, + "max": { + "type": "number", + "format": "double" + }, + "maxLength": { + "type": "integer", + "format": "integer" + }, + "min": { + "type": "number", + "format": "double" + }, + "minLength": { + "type": "integer", + "format": "integer" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.Option" + } + }, + "pattern": { + "type": "string" + }, + "required": { + "type": "boolean" + } + } + }, + "utils.Validate.defaultValue": {}, + "v1.AccessKeyRequest": { + "required": [ + "accessKeyID", + "accessKeySecret" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + } + } + }, + "v1.AddonDefinition": { + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.AddonRegistryMeta": { + "required": [ + "name" + ], + "properties": { + "git": { + "$ref": "#/definitions/addon.GitAddonSource" + }, + "name": { + "type": "string" + } + } + }, + "v1.AddonStatusResponse": { + "required": [ + "phase", + "args" + ], + "properties": { + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "enabling_progress": { + "$ref": "#/definitions/v1.EnablingProgress" + }, + "phase": { + "type": "string" + } + } + }, + "v1.ApplicationBase": { + "required": [ + "name", + "alias", + "namespace", + "description", + "createTime", + "updateTime", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ApplicationDeployRequest": { + "required": [ + "workflowName", + "note", + "triggerType", + "force" + ], + "properties": { + "force": { + "type": "boolean" + }, + "note": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, + "v1.ApplicationDeployResponse": { + "required": [ + "note", + "envName", + "triggerType", + "createTime", + "version", + "status", + "reason", + "deployUser" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.ApplicationRequest": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponent" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.AppPolicy" + } + }, + "workflow": { + "$ref": "#/definitions/v1beta1.Workflow" + } + } + }, + "v1.ApplicationResourceInfo": { + "required": [ + "componentNum" + ], + "properties": { + "componentNum": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ApplicationResponse": { + "required": [ + "apiVersion", + "kind", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/definitions/v1beta1.ApplicationSpec" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + } + } + }, + "v1.ApplicationRevisionBase": { + "required": [ + "createTime", + "version", + "status", + "reason", + "deployUser", + "note", + "envName", + "triggerType" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.ApplicationStatisticsResponse": { + "required": [ + "envCount", + "deliveryTargetCount", + "revisonCount", + "workflowCount" + ], + "properties": { + "deliveryTargetCount": { + "type": "integer", + "format": "int64" + }, + "envCount": { + "type": "integer", + "format": "int64" + }, + "revisonCount": { + "type": "integer", + "format": "int64" + }, + "workflowCount": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ApplicationStatusResponse": { + "required": [ + "envName", + "status" + ], + "properties": { + "envName": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + } + } + }, + "v1.ApplicationTemplateBase": { + "required": [ + "templateName", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "templateName": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationTemplateVersion" + } + } + } + }, + "v1.ApplicationTemplateVersion": { + "required": [ + "version", + "description", + "createUser", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "createUser": { + "type": "string" + }, + "description": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "v1.ApplicationTrait": { + "required": [ + "name", + "type", + "properties", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ClusterBase": { + "required": [ + "name", + "providerInfo", + "apiServerURL", + "dashboardURL", + "status", + "reason" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "providerInfo": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "v1.ClusterResourceInfo": { + "required": [ + "workerNumber", + "masterNumber", + "memoryCapacity", + "cpuCapacity", + "podCapacity", + "memoryUsed", + "cpuUsed", + "podUsed" + ], + "properties": { + "cpuCapacity": { + "type": "integer", + "format": "int64" + }, + "cpuUsed": { + "type": "integer", + "format": "int64" + }, + "gpuCapacity": { + "type": "integer", + "format": "int64" + }, + "gpuUsed": { + "type": "integer", + "format": "int64" + }, + "masterNumber": { + "type": "integer", + "format": "int32" + }, + "memoryCapacity": { + "type": "integer", + "format": "int64" + }, + "memoryUsed": { + "type": "integer", + "format": "int64" + }, + "podCapacity": { + "type": "integer", + "format": "int64" + }, + "podUsed": { + "type": "integer", + "format": "int64" + }, + "storageClassList": { + "type": "array", + "items": { + "type": "string" + } + }, + "workerNumber": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ClusterTarget": { + "required": [ + "clusterName" + ], + "properties": { + "clusterName": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "v1.ComponentBase": { + "required": [ + "name", + "alias", + "description", + "componentType", + "envNames", + "dependsOn", + "deployVersion", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentType": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "deployVersion": { + "type": "string" + }, + "description": { + "type": "string" + }, + "envNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ComponentListResponse": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ComponentBase" + } + } + } + }, + "v1.ComponentSelector": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.ConnectCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "clusterID", + "name", + "icon" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "clusterID": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateAddonRegistryRequest": { + "required": [ + "name" + ], + "properties": { + "git": { + "$ref": "#/definitions/addon.GitAddonSource" + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateApplicationEnvRequest": { + "required": [ + "name", + "targetNames" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.CreateApplicationRequest": { + "required": [ + "name", + "namespace", + "icon", + "component" + ], + "properties": { + "alias": { + "type": "string" + }, + "component": { + "$ref": "#/definitions/v1.CreateComponentRequest" + }, + "description": { + "type": "string" + }, + "envBinding": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBinding" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "yamlConfig": { + "type": "string" + } + } + }, + "v1.CreateApplicationTemplateRequest": { + "required": [ + "templateName", + "version", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.CreateApplicationTraitRequest": { + "required": [ + "type", + "properties" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.CreateCloudClusterRequest": { + "required": [ + "accessKeyID", + "accessKeySecret", + "name", + "zone", + "workerNumber", + "cpuCoresPerWorker", + "memoryPerWorker" + ], + "properties": { + "accessKeyID": { + "type": "string" + }, + "accessKeySecret": { + "type": "string" + }, + "cpuCoresPerWorker": { + "type": "integer", + "format": "int64" + }, + "memoryPerWorker": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "workerNumber": { + "type": "integer", + "format": "int32" + }, + "zone": { + "type": "string" + } + } + }, + "v1.CreateCloudClusterResponse": { + "required": [ + "clusterName", + "clusterID", + "status" + ], + "properties": { + "clusterID": { + "type": "string" + }, + "clusterName": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "v1.CreateClusterNamespaceRequest": { + "required": [ + "namespace" + ], + "properties": { + "namespace": { + "type": "string" + } + } + }, + "v1.CreateClusterNamespaceResponse": { + "required": [ + "exists" + ], + "properties": { + "exists": { + "type": "boolean" + } + } + }, + "v1.CreateClusterRequest": { + "required": [ + "name", + "icon" + ], + "properties": { + "alias": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateComponentRequest": { + "required": [ + "name", + "componentType" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentType": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.CreateApplicationTraitRequest" + } + } + } + }, + "v1.CreateDeliveryTargetRequest": { + "required": [ + "name", + "namespace" + ], + "properties": { + "alias": { + "type": "string" + }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "variable": { + "type": "object" + } + } + }, + "v1.CreateNamespaceRequest": { + "required": [ + "name", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.CreatePolicyRequest": { + "required": [ + "name", + "description", + "type", + "properties" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.CreateWorkflowRequest": { + "required": [ + "appName", + "name", + "default", + "envName" + ], + "properties": { + "alias": { + "type": "string" + }, + "appName": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, + "v1.DefinitionBase": { + "required": [ + "name", + "description", + "icon" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.DeliveryTargetBase": { + "required": [ + "name", + "namespace", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "appNum": { + "type": "integer", + "format": "int64" + }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "variable": { + "type": "object" + } + } + }, + "v1.DetailAddonResponse": { + "required": [ + "icon", + "name", + "version", + "description", + "schema", + "uiSchema", + "definitions" + ], + "properties": { + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonDefinition" + } + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AddonDependency" + } + }, + "deployTo": { + "$ref": "#/definitions/types.AddonDeployTo" + }, + "description": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "schema": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "uiSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.DetailApplicationResponse": { + "required": [ + "updateTime", + "icon", + "name", + "alias", + "namespace", + "description", + "createTime", + "policies", + "envBindings", + "status", + "resourceInfo" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "envBindings": { + "type": "array", + "items": { + "type": "string" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "policies": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceInfo": { + "$ref": "#/definitions/v1.ApplicationResourceInfo" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailClusterResponse": { + "required": [ + "status", + "reason", + "dashboardURL", + "name", + "description", + "icon", + "labels", + "kubeConfig", + "kubeConfigSecret", + "model", + "alias", + "provider", + "apiServerURL", + "resourceInfo" + ], + "properties": { + "alias": { + "type": "string" + }, + "apiServerURL": { + "type": "string" + }, + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "model": { + "$ref": "#/definitions/model.Model" + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/model.ProviderInfo" + }, + "reason": { + "type": "string" + }, + "resourceInfo": { + "$ref": "#/definitions/v1.ClusterResourceInfo" + }, + "status": { + "type": "string" + } + } + }, + "v1.DetailComponentResponse": { + "required": [ + "alias", + "creator", + "updateTime", + "appPrimaryKey", + "createTime", + "type", + "name" + ], + "properties": { + "alias": { + "type": "string" + }, + "appPrimaryKey": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "externalRevision": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ApplicationTrait" + } + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailDefinitionResponse": { + "required": [ + "schema", + "uiSchema" + ], + "properties": { + "schema": { + "type": "string" + }, + "uiSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } + } + } + }, + "v1.DetailDeliveryTargetResponse": { + "required": [ + "namespace", + "createTime", + "updateTime", + "name" + ], + "properties": { + "alias": { + "type": "string" + }, + "appNum": { + "type": "integer", + "format": "int64" + }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "variable": { + "type": "object" + } + } + }, + "v1.DetailPolicyResponse": { + "required": [ + "properties", + "createTime", + "updateTime", + "name", + "type", + "description", + "creator" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.DetailRevisionResponse": { + "required": [ + "triggerType", + "workflowName", + "version", + "appPrimaryKey", + "status", + "reason", + "deployUser", + "note", + "envName", + "createTime", + "updateTime" + ], + "properties": { + "appPrimaryKey": { + "type": "string" + }, + "applyAppConfig": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "envName": { + "type": "string" + }, + "note": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "triggerType": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + }, + "workflowName": { + "type": "string" + } + } + }, + "v1.DetailWorkflowRecordResponse": { + "required": [ + "namespace", + "status", + "name", + "deployTime", + "deployUser", + "note", + "triggerType" + ], + "properties": { + "deployTime": { + "type": "string", + "format": "date-time" + }, + "deployUser": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "note": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "triggerType": { + "type": "string" + } + } + }, + "v1.DetailWorkflowResponse": { + "required": [ + "alias", + "description", + "envName", + "createTime", + "updateTime", + "name", + "enable", + "default" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "envName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.EmptyResponse": {}, + "v1.EnableAddonRequest": { + "properties": { + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.EnablingProgress": { + "required": [ + "enabled_components", + "total_components" + ], + "properties": { + "enabled_components": { + "type": "integer", + "format": "int32" + }, + "total_components": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.EnvBinding": { + "required": [ + "name", + "targetNames" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.EnvBindingBase": { + "required": [ + "name", + "targetNames", + "createTime", + "updateTime", + "appDeployName" + ], + "properties": { + "alias": { + "type": "string" + }, + "appDeployName": { + "type": "string" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "deliveryTargets": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DeliveryTargetBase" + } + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ListAddonRegistryResponse": { + "required": [ + "registrys" + ], + "properties": { + "registrys": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.AddonRegistryMeta" + } + } + } + }, + "v1.ListAddonResponse": { + "required": [ + "addons" + ], + "properties": { + "addons": { + "type": "array", + "items": { + "$ref": "#/definitions/types.AddonMeta" + } + } + } + }, + "v1.ListApplicationEnvBinding": { + "required": [ + "envBindings" + ], + "properties": { + "envBindings": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.EnvBindingBase" + } + } + } + }, + "v1.ListApplicationPolicy": { + "required": [ + "policies" + ], + "properties": { + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyBase" + } + } + } + }, + "v1.ListApplicationResponse": { + "required": [ + "applications" + ], + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationBase" + } + } + } + }, + "v1.ListCloudClusterCreationResponse": { + "required": [ + "creations" + ], + "properties": { + "creations": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.CreateCloudClusterResponse" + } + } + } + }, + "v1.ListCloudClusterResponse": { + "required": [ + "clusters", + "total" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/cloudprovider.CloudCluster" + } + }, + "total": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ListClusterResponse": { + "required": [ + "clusters", + "total" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ListDefinitionResponse": { + "required": [ + "definitions" + ], + "properties": { + "definitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DefinitionBase" + } + } + } + }, + "v1.ListDeliveryTargetResponse": { + "required": [ + "deliveryTargets", + "total" + ], + "properties": { + "deliveryTargets": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.DeliveryTargetBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ListNamespaceResponse": { + "required": [ + "namespaces" + ], + "properties": { + "namespaces": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NamespaceBase" + } + } + } + }, + "v1.ListPolicyDefinitionResponse": { + "required": [ + "policyDefinitions" + ], + "properties": { + "policyDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyDefinition" + } + } + } + }, + "v1.ListRevisionsResponse": { + "required": [ + "revisions", + "total" + ], + "properties": { + "revisions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationRevisionBase" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ListWorkflowRecordsResponse": { + "required": [ + "records", + "total" + ], + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.ListWorkflowResponse": { + "required": [ + "workflows" + ], + "properties": { + "workflows": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowBase" + } + } + } + }, + "v1.NamespaceBase": { + "required": [ + "name", + "description", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.NamespaceDetailResponse": { + "required": [ + "description", + "createTime", + "updateTime", + "name" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ObjectReference": { + "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "fieldPath": { + "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", + "type": "string" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "type": "string" + }, + "resourceVersion": { + "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", + "type": "string" + } + } + }, + "v1.PolicyBase": { + "required": [ + "name", + "type", + "description", + "creator", + "properties", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "properties": { + "$ref": "#/definitions/model.JSONStruct" + }, + "type": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.PolicyDefinition": { + "required": [ + "name", + "description", + "parameters" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Parameter" + } + } + } + }, + "v1.PutApplicationEnvRequest": { + "required": [ + "targetNames" + ], + "properties": { + "alias": { + "type": "string" + }, + "componentSelector": { + "$ref": "#/definitions/v1.ComponentSelector" + }, + "description": { + "type": "string" + }, + "targetNames": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "v1.UpdateAddonRegistryRequest": { + "properties": { + "git": { + "$ref": "#/definitions/addon.GitAddonSource" + } + } + }, + "v1.UpdateApplicationComponentRequest": { + "properties": { + "alias": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "$ref": "#/definitions/v1.UpdateApplicationComponentRequest.labels" + }, + "properties": { + "type": "string" + } + } + }, + "v1.UpdateApplicationComponentRequest.labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "v1.UpdateApplicationRequest": { + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "v1.UpdateApplicationTraitRequest": { + "required": [ + "properties" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "properties": { + "type": "string" + } + } + }, + "v1.UpdateDeliveryTargetRequest": { + "properties": { + "alias": { + "type": "string" + }, + "cluster": { + "$ref": "#/definitions/v1.ClusterTarget" + }, + "description": { + "type": "string" + }, + "variable": { + "type": "object" + } + } + }, + "v1.UpdatePolicyRequest": { + "required": [ + "description", + "type", + "properties" + ], + "properties": { + "description": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.UpdateWorkflowRequest": { + "required": [ + "enable", + "default", + "envName" + ], + "properties": { + "alias": { + "type": "string" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "envName": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, + "v1.VelaQLViewResponse": { + "type": "object" + }, + "v1.WorkflowBase": { + "required": [ + "name", + "alias", + "description", + "enable", + "default", + "envName", + "createTime", + "updateTime" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "default": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "envName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.WorkflowRecord": { + "required": [ + "name", + "namespace", + "status" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + } + } + }, + "v1.WorkflowStep": { + "required": [ + "name", + "type" + ], + "properties": { + "alias": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1alpha1.CanaryMetric": { + "required": [ + "name" + ], + "properties": { + "interval": { + "type": "string" + }, + "metricsRange": { + "$ref": "#/definitions/v1alpha1.MetricsExpectedRange" + }, + "name": { + "type": "string" + }, + "templateRef": { + "$ref": "#/definitions/v1.ObjectReference" + } + } + }, + "v1alpha1.MetricsExpectedRange": { + "properties": { + "max": { + "type": "string" + }, + "min": { + "type": "string" + } + } + }, + "v1alpha1.RolloutBatch": { + "properties": { + "batchRolloutWebhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook" + } + }, + "canaryMetric": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.CanaryMetric" + } + }, + "instanceInterval": { + "type": "integer", + "format": "int32" + }, + "maxUnavailable": { + "type": "string" + }, + "podList": { + "type": "array", + "items": { + "type": "string" + } + }, + "replicas": { + "type": "string" + } + } + }, + "v1alpha1.RolloutPlan": { + "properties": { + "batchPartition": { + "type": "integer", + "format": "int32" + }, + "canaryMetric": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.CanaryMetric" + } + }, + "numBatches": { + "type": "integer", + "format": "int32" + }, + "paused": { + "type": "boolean" + }, + "rolloutBatches": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutBatch" + } + }, + "rolloutStrategy": { + "type": "string" + }, + "rolloutWebhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook" + } + }, + "targetSize": { + "type": "integer", + "format": "int32" + } + } + }, + "v1alpha1.RolloutStatus": { + "required": [ + "rollingState", + "batchRollingState", + "currentBatch", + "upgradedReplicas", + "upgradedReadyReplicas" + ], + "properties": { + "batchRollingState": { + "type": "string" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "currentBatch": { + "type": "integer", + "format": "int32" + }, + "lastAppliedPodTemplateIdentifier": { + "type": "string" + }, + "rollingState": { + "type": "string" + }, + "rolloutOriginalSize": { + "type": "integer", + "format": "int32" + }, + "rolloutTargetSize": { + "type": "integer", + "format": "int32" + }, + "targetGeneration": { + "type": "string" + }, + "upgradedReadyReplicas": { + "type": "integer", + "format": "int32" + }, + "upgradedReplicas": { + "type": "integer", + "format": "int32" + } + } + }, + "v1alpha1.RolloutWebhook": { + "required": [ + "type", + "name", + "url" + ], + "properties": { + "expectedStatus": { + "type": "array", + "items": { + "type": "integer" + } + }, + "metadata": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook.metadata" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "v1alpha1.RolloutWebhook.metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "v1beta1.AppPolicy": { + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1beta1.ApplicationSpec": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponent" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.AppPolicy" + } + }, + "rolloutPlan": { + "$ref": "#/definitions/v1alpha1.RolloutPlan" + }, + "workflow": { + "$ref": "#/definitions/v1beta1.Workflow" + } + } + }, + "v1beta1.Workflow": { + "properties": { + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.WorkflowStep" + } + } + } + }, + "v1beta1.WorkflowStep": { + "required": [ + "name", + "type" + ], + "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } } \ No newline at end of file diff --git a/docs/examples/velaql-views/usage.md b/docs/examples/velaql-views/usage.md new file mode 100644 index 000000000..c39b69559 --- /dev/null +++ b/docs/examples/velaql-views/usage.md @@ -0,0 +1,171 @@ + + +### Usage + +You can use velaQL with a syntax similar to promeQL. + +The syntax format of velaQL is as follows: + +```sql +view{parameter1=value1}.statusKey +``` + +1. `view` represents different query views, we have built a few views: `component-pod-view`,`pod-view`,`resource-view` +2. `parameter1=value1` represents query configuration items +3. `statusKey` represents the aggregate result of the query, default is `status` + +### component-pod-view + +#### describe + +List the pods created by specified component + +#### parameter + +``` +parameter: { + appName: string // application name + appNs: string // application namespace + name: string // component name + cluster?: string // cluster name(Optional) + clusterNs?: string // cluster namespace(Optional) +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + podList: [{ + clusterName: string + revision: string + publishVersion: string + podName: string + podNs: string + status: string + podIP: string + hostIP: string + nodeName: string + }] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +```sql +component-pod-view{appName=demo,appNs=default,cluster=prod,clusterNs=default,name=web}.status +``` + +### pod-view + +#### describe + +Query the pods detail infomation + +#### parameter + +``` +parameter: { + name: string // pod name + namespace: string // pod namespace + cluster?: string // cluster name(Optional) +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + containers: [ { + name: string + image: string + status: { + state: string + restartCount: string + } + resource: { + limits: { + cpu: string + memory: string + } + requests: { + cpu: string + memory: string + } + } + usageResource: { + cpu: string + memory: string + } + }] + events: [...corev1.Event] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +``` +pod-view{name=demo,namespace=default,cluster=prod}.status +``` + +### resource-view + +#### describe + +List resources + +#### parameter + +``` +parameter: { + type: "ns" | "secret" | "configMap" | "pvc" | "storageClass" + namespace: *"" | string // Optional + cluster: *"" | string // Optional +} +``` + +#### statusKey + +`status` + +#### query result + +``` +// query successful +status: { + list: [...k8sObject] +} + +// query failed +status: { + error: string +} +``` + +#### demo + +``` +resource-view{type=ns,cluster=prod}.status +``` + + diff --git a/go.mod b/go.mod index 573e1bf4c..af0941a88 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,9 @@ require ( github.com/Masterminds/sprig v2.22.0+incompatible github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 github.com/agiledragon/gomonkey/v2 v2.3.0 + github.com/alibabacloud-go/cs-20151215/v2 v2.4.5 + github.com/alibabacloud-go/darabonba-openapi v0.1.4 + github.com/alibabacloud-go/tea v1.1.15 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 github.com/briandowns/spinner v1.11.1 @@ -20,6 +23,7 @@ require ( github.com/emicklei/go-restful/v3 v3.0.0-rc2 github.com/evanphx/json-patch v4.11.0+incompatible github.com/fatih/color v1.12.0 + github.com/fluxcd/helm-controller/api v0.12.1 github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gertd/go-pluralize v0.1.7 github.com/getkin/kin-openapi v0.34.0 @@ -28,6 +32,7 @@ require ( github.com/go-playground/validator/v10 v10.9.0 github.com/google/go-cmp v0.5.6 github.com/google/go-github/v32 v32.1.0 + github.com/google/uuid v1.1.2 github.com/gosuri/uitable v0.0.4 github.com/hashicorp/hcl/v2 v2.9.1 github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 @@ -65,7 +70,7 @@ require ( istio.io/api v0.0.0-20210128181506-0c4b8e54850f istio.io/client-go v0.0.0-20210128182905-ee2edd059e02 k8s.io/api v0.22.1 - k8s.io/apiextensions-apiserver v0.21.3 + k8s.io/apiextensions-apiserver v0.22.1 k8s.io/apimachinery v0.22.1 k8s.io/cli-runtime v0.21.0 k8s.io/client-go v0.22.1 diff --git a/go.sum b/go.sum index 76813e77e..2cafafc28 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,30 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/alessio/shellescape v1.2.2 h1:8LnL+ncxhWT2TR00dfJRT25JWWrhkMZXneHVWnetDZg= github.com/alessio/shellescape v1.2.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alibabacloud-go/cs-20151215/v2 v2.4.5 h1:v7SWYM+3nCfw7L5DjstXgwSo6PLGpMYZHdNdDi6yajU= +github.com/alibabacloud-go/cs-20151215/v2 v2.4.5/go.mod h1:pIg8PCfRO6qSylVbW9BiG6q0zaYCP/aIKCCEwsuvbPg= +github.com/alibabacloud-go/darabonba-openapi v0.1.4 h1:eV4mB+45/QxWFQqghSUVO5H5Ct4c+tCaCp4c57TCTVY= +github.com/alibabacloud-go/darabonba-openapi v0.1.4/go.mod h1:j03z4XUkIC9aBj/w5Bt7H0cygmPNt5sug8NXle68+Og= +github.com/alibabacloud-go/darabonba-string v1.0.0/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 h1:NqugFkGxx1TXSh/pBcU00Y6bljgDPaFdh5MUSeJ7e50= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.0.7 h1:Kt/9kicJxvq1It739psKFBi1IB9imhqGWA9g4chIbjI= +github.com/alibabacloud-go/openapi-util v0.0.7/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.15 h1:IaBC1Mm5Ss+l7cWnOXSxCmnWoWrEdeHEtDgQzoCCgjY= +github.com/alibabacloud-go/tea v1.1.15/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils v1.3.9 h1:TtbzxS+BXrisA7wzbAMRtlU8A2eWLg0ufm7m/Tl6fc4= +github.com/alibabacloud-go/tea-utils v1.3.9/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1318/go.mod h1:9CMdKNL3ynIGPpfTcdwTvIm8SGuAZYYC4jFVSSvE1YQ= github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/credentials-go v1.1.2 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -458,6 +480,14 @@ github.com/fatih/structtag v1.1.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/helm-controller/api v0.12.1 h1:rDyhMPvbhCxslqiNNG4nlfDCeYgrk6D+1ZKLsBS/Irs= +github.com/fluxcd/helm-controller/api v0.12.1/go.mod h1:zWmzV0s2SU4rEIGLPTt+dsaMs40OsNQgSgOATgJmxB0= +github.com/fluxcd/pkg/apis/kustomize v0.1.0 h1:sauL+KHmZ0zV2ZgpsLMyDzCQudBTtaFzSys+rXn9g9w= +github.com/fluxcd/pkg/apis/kustomize v0.1.0/go.mod h1:gEl+W5cVykCC3RfrCaqe+Pz+j4lKl2aeR4dxsom/zII= +github.com/fluxcd/pkg/apis/meta v0.10.0 h1:N7wVGHC1cyPdT87hrDC7UwCwRwnZdQM46PBSLjG2rlE= +github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE= +github.com/fluxcd/pkg/runtime v0.12.0 h1:BPZZ8bBkimpqGAPXqOf3LTaw+tcw6HgbWyCuzbbsJGs= +github.com/fluxcd/pkg/runtime v0.12.0/go.mod h1:EyaTR2TOYcjL5U//C4yH3bt2tvTgIOSXpVRbWxUn/C4= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -810,6 +840,8 @@ github.com/gophercloud/gophercloud v0.10.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU8 github.com/gophercloud/gophercloud v0.11.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20191106031601-ce3c9ade29de/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254/go.mod h1:M9mZEtGIsR1oDaZagNPNG9iq9n2HrhZ17dsXk73V3Lw= github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= @@ -866,6 +898,7 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv 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-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/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -876,6 +909,7 @@ 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-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= @@ -980,6 +1014,7 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -1460,7 +1495,10 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -1535,6 +1573,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tjfoc/gmsm v1.3.2 h1:7JVkAn5bvUJ7HtU08iW6UiD+UTmJTIToHCfeFzkcCxM= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tklauser/go-sysconf v0.3.4/go.mod h1:Cl2c8ZRWfHD5IrfHo9VN+FX9kCFjIOyVklgXycLB6ek= github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1gBkr4QyP8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1604,6 +1644,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -1737,10 +1778,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= @@ -1959,6 +2002,7 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2109,6 +2153,7 @@ golang.org/x/tools v0.0.0-20200422205258-72e4a01eba43/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200426102838-f3a5411a4c3b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200603131246-cc40288be839/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -2332,6 +2377,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= @@ -2407,9 +2454,11 @@ k8s.io/apiextensions-apiserver v0.17.0/go.mod h1:XiIFUakZywkUl54fVXa7QTEHcqQz9HG k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKizADfvUM/SS/M= k8s.io/apiextensions-apiserver v0.21.0/go.mod h1:gsQGNtGkc/YoDG9loKI0V+oLZM4ljRPjc/sql5tmvzc= +k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= k8s.io/apiextensions-apiserver v0.21.2/go.mod h1:+Axoz5/l3AYpGLlhJDfcVQzCerVYq3K3CvDMvw6X1RA= -k8s.io/apiextensions-apiserver v0.21.3 h1:+B6biyUWpqt41kz5x6peIsljlsuwvNAp/oFax/j2/aY= k8s.io/apiextensions-apiserver v0.21.3/go.mod h1:kl6dap3Gd45+21Jnh6utCx8Z2xxLm8LGDkprcd+KbsE= +k8s.io/apiextensions-apiserver v0.22.1 h1:YSJYzlFNFSfUle+yeEXX0lSQyLEoxoPJySRupepb0gE= +k8s.io/apiextensions-apiserver v0.22.1/go.mod h1:HeGmorjtRmRLE+Q8dJu6AYRoZccvCMsghwS8XTUYb2c= k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8= k8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4= @@ -2436,6 +2485,7 @@ k8s.io/apiserver v0.17.0/go.mod h1:ABM+9x/prjINN6iiffRVNCBR2Wk7uY4z+EtEGZD48cg= k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= +k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= k8s.io/apiserver v0.21.2/go.mod h1:lN4yBoGyiNT7SC1dmNk0ue6a5Wi6O3SWOIw91TsucQw= k8s.io/apiserver v0.21.3/go.mod h1:eDPWlZG6/cCCMj/JBcEpDoK+I+6i3r9GsChYBHSbAzU= k8s.io/apiserver v0.22.1 h1:Ul9Iv8OMB2s45h2tl5XWPpAZo1VPIJ/6N+MESeed7L8= @@ -2467,6 +2517,7 @@ k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRV k8s.io/code-generator v0.18.6/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= k8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg= k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= k8s.io/code-generator v0.21.2/go.mod h1:8mXJDCB7HcRo1xiEQstcguZkbxZaqeUOrO9SsicWs3U= k8s.io/code-generator v0.21.3/go.mod h1:K3y0Bv9Cz2cOW2vXUrNZlFbflhuPvuadW6JdnN6gGKo= k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= @@ -2477,6 +2528,7 @@ k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmD k8s.io/component-base v0.18.6/go.mod h1:knSVsibPR5K6EW2XOjEHik6sdU5nCvKMrzMt2D4In14= k8s.io/component-base v0.20.10/go.mod h1:ZKOEin1xu68aJzxgzl5DZSp5J1IrjAOPlPN90/t6OI8= k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= +k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= k8s.io/component-base v0.21.2/go.mod h1:9lvmIThzdlrJj5Hp8Z/TOgIkdfsNARQ1pT+3PByuiuc= k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ= k8s.io/component-base v0.22.1 h1:SFqIXsEN3v3Kkr1bS6rstrs1wd45StJqbtgbQ4nRQdo= @@ -2558,6 +2610,7 @@ sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2 h1:c6RYHA1wUg sigs.k8s.io/apiserver-runtime v1.0.3-0.20210913073608-0663f60bfee2/go.mod h1:gvPfh5FX3Wi3kIRpkh7qvY0i/DQl3SDpRtvqMGZE3Vo= sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= sigs.k8s.io/controller-runtime v0.6.2/go.mod h1:vhcq/rlnENJ09SIRp3EveTaZ0yqH526hjf9iJdbUJ/E= +sigs.k8s.io/controller-runtime v0.9.0/go.mod h1:TgkfvrhhEw3PlI0BRL/5xM+89y3/yc0ZDfdbTl84si8= sigs.k8s.io/controller-runtime v0.9.2/go.mod h1:TxzMCHyEUpaeuOiZx/bIdc2T81vfs/aKdvJt9wuu0zk= sigs.k8s.io/controller-runtime v0.9.5 h1:WThcFE6cqctTn2jCZprLICO6BaKZfhsT37uAapTNfxc= sigs.k8s.io/controller-runtime v0.9.5/go.mod h1:q6PpkM5vqQubEKUKOM6qr06oXGzOBcCby1DA9FbyZeA= diff --git a/pkg/addon/addon.go b/pkg/addon/addon.go new file mode 100644 index 000000000..c21abe4d3 --- /dev/null +++ b/pkg/addon/addon.go @@ -0,0 +1,637 @@ +package addon + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "path" + "path/filepath" + "strings" + "sync" + "time" + + v1 "k8s.io/api/core/v1" + + "sigs.k8s.io/yaml" + + "cuelang.org/go/cue" + cueyaml "cuelang.org/go/encoding/yaml" + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" + "golang.org/x/oauth2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + + common2 "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" + utils2 "github.com/oam-dev/kubevela/pkg/controller/utils" + cuemodel "github.com/oam-dev/kubevela/pkg/cue/model" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +const ( + // ReadmeFileName is the addon readme file name + ReadmeFileName string = "readme.md" + + // MetadataFileName is the addon meatadata.yaml file name + MetadataFileName string = "metadata.yaml" + + // TemplateFileName is the addon template.yaml dir name + TemplateFileName string = "template.yaml" + + // ResourcesDirName is the addon resources/ dir name + ResourcesDirName string = "resources" + + // DefinitionsDirName is the addon definitions/ dir name + DefinitionsDirName string = "definitions" +) + +// ListOptions contains flags mark what files should be read in an addon directory +type ListOptions struct { + GetDetail bool + GetDefinition bool + GetResource bool + GetParameter bool + GetTemplate bool +} + +var ( + // GetLevelOptions used when get or list addons + GetLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true} + + // EnableLevelOptions used when enable addon + EnableLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetResource: true, GetTemplate: true, GetParameter: true} +) + +// aError is internal error type of addon +type aError error + +var ( + // ErrNotExist means addon not exists + ErrNotExist aError = errors.New("addon not exist") +) + +// gitHelper helps get addon's file by git +type gitHelper struct { + Client *github.Client + Meta *utils.Content +} + +// GitAddonSource defines the information about the Git as addon source +type GitAddonSource struct { + URL string `json:"url,omitempty" validate:"required"` + Path string `json:"path,omitempty"` + Token string `json:"token,omitempty"` +} + +// asyncReader helps async read files of addon +type asyncReader struct { + addon *types.Addon + h *gitHelper + item *github.RepositoryContent + errChan chan error +} + +// SetReadContent set which file to read +func (r *asyncReader) SetReadContent(content *github.RepositoryContent) { + r.item = content +} + +// GetAddon get a addon info from GitAddonSource, can be used for get or enable +func GetAddon(name string, git *GitAddonSource, opt ListOptions) (*types.Addon, error) { + addon, err := getSingleAddonFromGit(git.URL, git.Path, name, git.Token, opt) + if err != nil { + return nil, err + } + return addon, nil +} + +// ListAddons list addons' info from GitAddonSource +func ListAddons(git *GitAddonSource, opt ListOptions) ([]*types.Addon, error) { + gitAddons, err := getAddonsFromGit(git.URL, git.Path, git.Token, opt) + if err != nil { + return nil, err + } + return gitAddons, nil +} + +func getAddonsFromGit(baseURL, dir, token string, opt ListOptions) ([]*types.Addon, error) { + var addons []*types.Addon + var err error + var wg sync.WaitGroup + errChan := make(chan error, 1) + + gith, err := createGitHelper(baseURL, dir, token) + if err != nil { + return nil, err + } + _, items, err := gith.readRepo(gith.Meta.Path) + if err != nil { + return nil, err + } + + for _, subItems := range items { + if subItems.GetType() != "dir" { + continue + } + wg.Add(1) + go func(item *github.RepositoryContent) { + defer wg.Done() + addonRes, err := getSingleAddonFromGit(baseURL, dir, item.GetName(), token, opt) + if err != nil { + errChan <- err + return + } + addons = append(addons, addonRes) + }(subItems) + } + wg.Wait() + if len(errChan) != 0 { + return nil, <-errChan + } + return addons, nil +} + +func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOptions) (*types.Addon, error) { + var wg sync.WaitGroup + + gith, err := createGitHelper(baseURL, path.Join(dir, addonName), token) + if err != nil { + return nil, err + } + _, items, err := gith.readRepo(gith.Meta.Path) + if err != nil { + return nil, err + } + + reader := asyncReader{ + addon: &types.Addon{}, + h: gith, + errChan: make(chan error, 1), + } + for _, item := range items { + switch strings.ToLower(item.GetName()) { + case ReadmeFileName: + if !opt.GetDetail { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readReadme(&wg, reader) + case MetadataFileName: + reader.SetReadContent(item) + wg.Add(1) + go readMetadata(&wg, reader) + case DefinitionsDirName: + if !opt.GetDefinition { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readDefinitions(&wg, reader) + case ResourcesDirName: + if !opt.GetResource && !opt.GetParameter { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readResources(&wg, reader) + case TemplateFileName: + if !opt.GetTemplate { + break + } + reader.SetReadContent(item) + wg.Add(1) + go readTemplate(&wg, reader) + } + } + wg.Wait() + + if opt.GetParameter && reader.addon.Parameters != "" { + err = genAddonAPISchema(reader.addon) + if err != nil { + return nil, err + } + } + return reader.addon, nil + +} + +func readTemplate(wg *sync.WaitGroup, reader asyncReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + data, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + reader.addon.AppTemplate = &v1beta1.Application{} + _, _, err = dec.Decode([]byte(data), nil, reader.addon.AppTemplate) + if err != nil { + reader.errChan <- err + return + } +} + +func readResources(wg *sync.WaitGroup, reader asyncReader) { + defer wg.Done() + dirPath := strings.Split(reader.item.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, ResourcesDirName) + if err != nil { + reader.errChan <- err + } + + _, items, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + for _, item := range items { + switch item.GetType() { + case "file": + reader.SetReadContent(item) + wg.Add(1) + go readResFile(wg, reader, dirPath) + case "dir": + reader.SetReadContent(item) + wg.Add(1) + go readResources(wg, reader) + + } + } +} + +// readResFile read single resource file +func readResFile(wg *sync.WaitGroup, reader asyncReader, dirPath []string) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + b, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + + if reader.item.GetName() == "parameter.cue" { + reader.addon.Parameters = b + return + } + switch filepath.Ext(reader.item.GetName()) { + case ".cue": + reader.addon.CUETemplates = append(reader.addon.CUETemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) + default: + reader.addon.YAMLTemplates = append(reader.addon.YAMLTemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) + } +} + +func readDefinitions(wg *sync.WaitGroup, reader asyncReader) { + defer wg.Done() + dirPath := strings.Split(reader.item.GetPath(), "/") + dirPath, err := cutPathUntil(dirPath, DefinitionsDirName) + if err != nil { + reader.errChan <- err + return + } + _, items, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + for _, item := range items { + switch item.GetType() { + case "file": + reader.SetReadContent(item) + wg.Add(1) + go readDefFile(wg, reader, dirPath) + case "dir": + reader.SetReadContent(item) + wg.Add(1) + go readDefinitions(wg, reader) + } + } +} + +// readDefFile read single definition file +func readDefFile(wg *sync.WaitGroup, reader asyncReader, dirPath []string) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + b, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + reader.addon.Definitions = append(reader.addon.Definitions, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath}) +} + +func readMetadata(wg *sync.WaitGroup, reader asyncReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + b, err := content.GetContent() + if err != nil { + reader.errChan <- err + return + } + err = yaml.Unmarshal([]byte(b), &reader.addon.AddonMeta) + if err != nil { + reader.errChan <- err + return + } +} + +func readReadme(wg *sync.WaitGroup, reader asyncReader) { + defer wg.Done() + content, _, err := reader.h.readRepo(*reader.item.Path) + if err != nil { + reader.errChan <- err + return + } + reader.addon.Detail, err = content.GetContent() + if err != nil { + reader.errChan <- err + return + } +} + +func createGitHelper(baseURL, dir, token string) (*gitHelper, error) { + var ts oauth2.TokenSource + if token != "" { + ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + } + tc := oauth2.NewClient(context.Background(), ts) + tc.Timeout = time.Second * 10 + cli := github.NewClient(tc) + + baseURL = strings.TrimSuffix(baseURL, ".git") + u, err := url.Parse(baseURL) + if err != nil { + return nil, errors.New("addon registry invalid") + } + u.Path = path.Join(u.Path, dir) + _, gitmeta, err := utils.Parse(u.String()) + if err != nil { + return nil, errors.New("addon registry invalid") + } + + return &gitHelper{ + Client: cli, + Meta: gitmeta, + }, nil +} + +func (h *gitHelper) readRepo(path string) (*github.RepositoryContent, []*github.RepositoryContent, error) { + file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, path, nil) + if err != nil { + return nil, nil, WrapErrRateLimit(err) + } + return file, items, nil +} + +func genAddonAPISchema(addonRes *types.Addon) error { + param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters) + if err != nil { + return err + } + var r cue.Runtime + cueInst, err := r.Compile("-", param) + if err != nil { + return err + } + data, err := common.GenOpenAPI(cueInst) + if err != nil { + return err + } + schema, err := utils2.ConvertOpenAPISchema2SwaggerObject(data) + if err != nil { + return err + } + utils2.FixOpenAPISchema("", schema) + addonRes.APISchema = schema + return nil +} + +func cutPathUntil(path []string, end string) ([]string, error) { + for i, d := range path { + if d == end { + return path[i:], nil + } + } + return nil, errors.New("cut path fail, target directory name not found") +} + +// RenderApplication render a K8s application +func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.Application, []*unstructured.Unstructured, error) { + if args == nil { + args = map[string]string{} + } + app := addon.AppTemplate + if app == nil { + app = &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: Convert2AppName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + Labels: map[string]string{ + oam.LabelAddonName: addon.Name, + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common2.ApplicationComponent{}, + }, + } + } + app.Name = Convert2AppName(addon.Name) + app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name}) + if app.Spec.Workflow == nil { + app.Spec.Workflow = &v1beta1.Workflow{} + } + for _, namespace := range addon.NeedNamespace { + comp := common2.ApplicationComponent{ + Type: "raw", + Name: fmt.Sprintf("%s-namespace", namespace), + Properties: util.Object2RawExtension(renderNamespace(namespace)), + } + app.Spec.Components = append(app.Spec.Components, comp) + } + + for _, tmpl := range addon.YAMLTemplates { + comp, err := renderRawComponent(tmpl) + if err != nil { + return nil, nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + for _, tmpl := range addon.CUETemplates { + comp, err := renderCUETemplate(tmpl, addon.Parameters, args) + if err != nil { + return nil, nil, ErrRenderCueTmpl + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + + var defObjs []*unstructured.Unstructured + + if isDeployToRuntimeOnly(addon) { + // Runtime cluster mode needs to deploy definitions to control plane k8s. + for _, def := range addon.Definitions { + obj, err := renderObject(def) + if err != nil { + return nil, nil, err + } + defObjs = append(defObjs, obj) + } + if app.Spec.Workflow == nil { + app.Spec.Workflow = &v1beta1.Workflow{Steps: make([]v1beta1.WorkflowStep, 0)} + } + app.Spec.Workflow.Steps = append(app.Spec.Workflow.Steps, + v1beta1.WorkflowStep{ + Name: "deploy-control-plane", + Type: "apply-application", + }, + v1beta1.WorkflowStep{ + Name: "deploy-runtime", + Type: "deploy2runtime", + }) + } else { + for _, def := range addon.Definitions { + comp, err := renderRawComponent(def) + if err != nil { + return nil, nil, err + } + app.Spec.Components = append(app.Spec.Components, *comp) + } + } + + return app, defObjs, nil +} + +func isDeployToRuntimeOnly(addon *types.Addon) bool { + if addon.DeployTo == nil { + return false + } + return addon.DeployTo.RuntimeCluster +} + +func renderObject(elem types.AddonElementFile) (*unstructured.Unstructured, error) { + obj := &unstructured.Unstructured{} + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(elem.Data), nil, obj) + if err != nil { + return nil, err + } + return obj, nil +} + +func renderNamespace(namespace string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetAPIVersion("v1") + u.SetKind("Namespace") + u.SetName(namespace) + return u +} + +// renderRawComponent will return a component in raw type from string +func renderRawComponent(elem types.AddonElementFile) (*common2.ApplicationComponent, error) { + baseRawComponent := common2.ApplicationComponent{ + Type: "raw", + Name: strings.Join(append(elem.Path, elem.Name), "-"), + } + obj, err := renderObject(elem) + if err != nil { + return nil, err + } + baseRawComponent.Properties = util.Object2RawExtension(obj) + return &baseRawComponent, nil +} + +// renderCUETemplate will return a component from cue template +func renderCUETemplate(elem types.AddonElementFile, parameters string, args map[string]string) (*common2.ApplicationComponent, error) { + bt, err := json.Marshal(args) + if err != nil { + return nil, err + } + var paramFile = cuemodel.ParameterFieldName + ": {}" + if string(bt) != "null" { + paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt)) + } + param := fmt.Sprintf("%s\n%s", paramFile, parameters) + v, err := value.NewValue(param, nil, "") + if err != nil { + return nil, err + } + out, err := v.LookupByScript(fmt.Sprintf("{%s}", elem.Data)) + if err != nil { + return nil, err + } + compContent, err := out.LookupValue("output") + if err != nil { + return nil, err + } + b, err := cueyaml.Encode(compContent.CueValue()) + if err != nil { + return nil, err + } + comp := common2.ApplicationComponent{ + Name: strings.Join(append(elem.Path, elem.Name), "-"), + } + err = yaml.Unmarshal(b, &comp) + if err != nil { + return nil, err + } + + return &comp, err +} + +const addonAppPrefix = "addon-" +const addonSecPrefix = "addon-secret-" + +// Convert2AppName - +func Convert2AppName(name string) string { + return addonAppPrefix + name +} + +// Convert2AddonName - +func Convert2AddonName(name string) string { + return strings.TrimPrefix(name, addonAppPrefix) +} + +// RenderArgsSecret TODO add desc +func RenderArgsSecret(addon *types.Addon, args map[string]string) *v1.Secret { + sec := v1.Secret{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, + ObjectMeta: metav1.ObjectMeta{ + Name: Convert2SecName(addon.Name), + Namespace: types.DefaultKubeVelaNS, + }, + StringData: args, + Type: v1.SecretTypeOpaque, + } + return &sec +} + +// Convert2SecName TODO add desc +func Convert2SecName(name string) string { + return addonSecPrefix + name +} diff --git a/pkg/addon/error.go b/pkg/addon/error.go new file mode 100644 index 000000000..1a0c6f98c --- /dev/null +++ b/pkg/addon/error.go @@ -0,0 +1,28 @@ +package addon + +import ( + "github.com/google/go-github/v32/github" + "github.com/pkg/errors" +) + +// NewAddonError will return an +func NewAddonError(msg string) error { + return errors.New(msg) +} + +var ( + // ErrRenderCueTmpl is error when render addon's cue file + ErrRenderCueTmpl = NewAddonError("fail to render cue tmpl") + + // ErrRateLimit means exceed github access rate limit + ErrRateLimit = NewAddonError("exceed github access rate limit") +) + +// WrapErrRateLimit return ErrRateLimit if is the situation, or return error directly +func WrapErrRateLimit(err error) error { + errRate := &github.RateLimitError{} + if errors.As(err, &errRate) { + return ErrRateLimit + } + return err +} diff --git a/pkg/apiserver/clients/kubeclient.go b/pkg/apiserver/clients/kubeclient.go index 8e3733022..0f880b7b3 100644 --- a/pkg/apiserver/clients/kubeclient.go +++ b/pkg/apiserver/clients/kubeclient.go @@ -17,13 +17,17 @@ limitations under the License. package clients import ( + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/config" - "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" ) var kubeClient client.Client +var kubeConfig *rest.Config // SetKubeClient for test func SetKubeClient(c client.Client) { @@ -35,13 +39,48 @@ func GetKubeClient() (client.Client, error) { if kubeClient != nil { return kubeClient, nil } - conf, err := config.GetConfig() + var err error + kubeClient, kubeConfig, err = multicluster.GetMulticlusterKubernetesClient() if err != nil { return nil, err } - k8sClient, err := client.New(conf, client.Options{Scheme: common.Scheme}) - if err != nil { - return nil, err - } - return k8sClient, nil + return kubeClient, nil +} + +// GetKubeConfig create/get kube runtime config +func GetKubeConfig() (*rest.Config, error) { + var err error + if kubeConfig == nil { + kubeConfig, err = config.GetConfig() + return kubeConfig, err + } + return kubeConfig, nil +} + +// GetDiscoverMapper get discover mapper +func GetDiscoverMapper() (discoverymapper.DiscoveryMapper, error) { + conf, err := GetKubeConfig() + if err != nil { + return nil, err + } + dm, err := discoverymapper.New(conf) + if err != nil { + return nil, err + } + return dm, nil +} + +// GetPackageDiscover get package discover +func GetPackageDiscover() (*packages.PackageDiscover, error) { + conf, err := GetKubeConfig() + if err != nil { + return nil, err + } + pd, err := packages.NewPackageDiscover(conf) + if err != nil { + if !packages.IsCUEParseErr(err) { + return nil, err + } + } + return pd, nil } diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 5e08fbd51..6a431de7d 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "reflect" + "time" ) var ( @@ -65,6 +66,8 @@ type Config struct { // Entity database data model type Entity interface { + SetCreateTime(time time.Time) + SetUpdateTime(time time.Time) PrimaryKey() string TableName() string Index() map[string]string @@ -83,10 +86,39 @@ func NewEntity(in Entity) (Entity, error) { return new.Interface().(Entity), nil } +// SortOrder is the order of sort +type SortOrder int + +const ( + // SortOrderAscending defines the order of ascending for sorting + SortOrderAscending = SortOrder(1) + // SortOrderDescending defines the order of descending for sorting + SortOrderDescending = SortOrder(-1) +) + +// SortOption describes the sorting parameters for list +type SortOption struct { + Key string + Order SortOrder +} + +// FuzzyQueryOption defines the fuzzy query search filter option +type FuzzyQueryOption struct { + Key string + Query string +} + +// FilterOptions filter query returned items +type FilterOptions struct { + Queries []FuzzyQueryOption +} + // ListOptions list api options type ListOptions struct { + FilterOptions Page int PageSize int + SortBy []SortOption } // DataStore datastore interface @@ -106,9 +138,12 @@ type DataStore interface { // Get entity from database, Name() and TableName() can't return zero value. Get(ctx context.Context, entity Entity) error - // TableName() can't return zero value. + // List entities from database, TableName() can't return zero value. List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error) + // Count entities from database, TableName() can't return zero value. + Count(ctx context.Context, entity Entity, options *FilterOptions) (int64, error) + // IsExist Name() and TableName() can't return zero value. IsExist(ctx context.Context, entity Entity) (bool, error) } diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index fc100f5de..b696b5752 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -21,8 +21,11 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" + "time" + "github.com/tidwall/gjson" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -58,7 +61,7 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) Name: cfg.Database, Annotations: map[string]string{"description": "For kubevela apiserver metadata storage."}, }}); err != nil { - return nil, fmt.Errorf("create namesapce failure %w", err) + return nil, fmt.Errorf("create namespace failure %w", err) } } return &kubeapi{ @@ -74,17 +77,17 @@ func generateName(entity datastore.Entity) string { func (m *kubeapi) generateConfigMap(entity datastore.Entity) *corev1.ConfigMap { data, _ := json.Marshal(entity) - lables := entity.Index() - if lables == nil { - lables = make(map[string]string) + labels := entity.Index() + if labels == nil { + labels = make(map[string]string) } - lables["table"] = entity.TableName() - lables["primaryKey"] = entity.PrimaryKey() + labels["table"] = entity.TableName() + labels["primaryKey"] = entity.PrimaryKey() var configMap = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: generateName(entity), Namespace: m.namespace, - Labels: lables, + Labels: labels, }, BinaryData: map[string][]byte{ "data": data, @@ -101,6 +104,8 @@ func (m *kubeapi) Add(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetCreateTime(time.Now()) + entity.SetUpdateTime(time.Now()) configMap := m.generateConfigMap(entity) if err := m.kubeclient.Create(ctx, configMap); err != nil { if apierrors.IsAlreadyExists(err) { @@ -163,6 +168,7 @@ func (m *kubeapi) Put(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetUpdateTime(time.Now()) var configMap corev1.ConfigMap if err := m.kubeclient.Get(ctx, types.NamespacedName{Namespace: m.namespace, Name: generateName(entity)}, &configMap); err != nil { if apierrors.IsNotFound(err) { @@ -216,6 +222,95 @@ func (m *kubeapi) Delete(ctx context.Context, entity datastore.Entity) error { return nil } +type bySortOptionConfigMap struct { + items []corev1.ConfigMap + objects []map[string]interface{} + sortBy []datastore.SortOption +} + +func newBySortOptionConfigMap(items []corev1.ConfigMap, sortBy []datastore.SortOption) bySortOptionConfigMap { + s := bySortOptionConfigMap{ + items: items, + objects: make([]map[string]interface{}, len(items)), + sortBy: sortBy, + } + for i, item := range items { + m := map[string]interface{}{} + data := item.BinaryData["data"] + for _, op := range sortBy { + res := gjson.Get(string(data), op.Key) + if res.Type == gjson.Number { + m[op.Key] = res.Num + } else { + m[op.Key] = res.Raw + } + } + s.objects[i] = m + } + return s +} + +func (b bySortOptionConfigMap) Len() int { + return len(b.items) +} + +func (b bySortOptionConfigMap) Swap(i, j int) { + b.items[i], b.items[j] = b.items[j], b.items[i] + b.objects[i], b.objects[j] = b.objects[j], b.objects[i] +} + +func (b bySortOptionConfigMap) Less(i, j int) bool { + for _, op := range b.sortBy { + x := b.objects[i][op.Key] + y := b.objects[j][op.Key] + _x, xok := x.(float64) + _y, yok := y.(float64) + var lt, gt bool + if xok && yok { + lt, gt = _x < _y, _x > _y + } + if !xok && !yok { + lt, gt = x.(string) < y.(string), x.(string) > y.(string) + } + if xok != yok { + lt, gt = false, false + } + if !lt && !gt { + continue + } + if op.Order == datastore.SortOrderAscending { + return lt + } + return gt + } + return true +} + +func _sortConfigMapBySortOptions(items []corev1.ConfigMap, sortOptions []datastore.SortOption) []corev1.ConfigMap { + so := newBySortOptionConfigMap(items, sortOptions) + sort.Sort(so) + return so.items +} + +func _filterConfigMapByFuzzyQueryOptions(items []corev1.ConfigMap, queries []datastore.FuzzyQueryOption) []corev1.ConfigMap { + var _items []corev1.ConfigMap + for _, item := range items { + data := string(item.BinaryData["data"]) + valid := true + for _, query := range queries { + res := gjson.Get(data, query.Key) + if res.Type != gjson.String || !strings.Contains(res.Str, query.Query) { + valid = false + break + } + } + if valid { + _items = append(_items, item) + } + } + return _items +} + // TableName() can't return zero value. func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) { if entity.TableName() == "" { @@ -235,18 +330,15 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } options := &client.ListOptions{ LabelSelector: selector, + Namespace: m.namespace, } - var skip, limit int64 + var skip, limit int if op != nil && op.PageSize > 0 && op.Page > 0 { - skip = int64(op.PageSize * (op.Page - 1)) - limit = int64(op.PageSize * op.Page) + skip = op.PageSize * (op.Page - 1) + limit = op.PageSize if skip < 0 { skip = 0 } - if limit < 0 { - limit = skip - } - options.Limit = limit } var configMaps corev1.ConfigMapList if err := m.kubeclient.List(ctx, &configMaps, options); err != nil { @@ -256,14 +348,25 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto return nil, datastore.NewDBError(err) } items := configMaps.Items + if op != nil && len(op.Queries) > 0 { + items = _filterConfigMapByFuzzyQueryOptions(items, op.Queries) + } + if op != nil && len(op.SortBy) > 0 { + items = _sortConfigMapBySortOptions(items, op.SortBy) + } if op != nil && op.PageSize > 0 && op.Page > 0 { - if len(configMaps.Items) > int(limit) { - items = configMaps.Items[skip:limit] + if skip >= len(items) { + items = []corev1.ConfigMap{} } else { - items = configMaps.Items[skip:] + items = items[skip:] } + if limit >= len(items) { + limit = len(items) + } + items = items[:limit] } var list []datastore.Entity + log.Logger.Debugf("query %s result count %d", selector, len(items)) for _, item := range items { ent, err := datastore.NewEntity(entity) if err != nil { @@ -276,3 +379,39 @@ func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datasto } return list, nil } + +// Count counts entities +func (m *kubeapi) Count(ctx context.Context, entity datastore.Entity, filterOptions *datastore.FilterOptions) (int64, error) { + if entity.TableName() == "" { + return 0, datastore.ErrTableNameEmpty + } + + selector, err := labels.Parse(fmt.Sprintf("table=%s", entity.TableName())) + if err != nil { + return 0, datastore.NewDBError(err) + } + for k, v := range entity.Index() { + rq, err := labels.NewRequirement(k, selection.Equals, []string{v}) + if err != nil { + return 0, datastore.ErrIndexInvalid + } + selector = selector.Add(*rq) + } + options := &client.ListOptions{ + LabelSelector: selector, + Namespace: m.namespace, + } + + var configMaps corev1.ConfigMapList + if err := m.kubeclient.List(ctx, &configMaps, options); err != nil { + if apierrors.IsNotFound(err) { + return 0, nil + } + return 0, datastore.NewDBError(err) + } + items := configMaps.Items + if filterOptions != nil && len(filterOptions.Queries) > 0 { + items = _filterConfigMapByFuzzyQueryOptions(configMaps.Items, filterOptions.Queries) + } + return int64(len(items)), nil +} diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go index 60778d86e..70f8172a0 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -87,22 +87,22 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(err).Should(BeNil()) Expect(kubeStore).ToNot(BeNil()) - It("Test add funtion", func() { + It("Test add function", func() { err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test batch add funtion", func() { + It("Test batch add function", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := kubeStore.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = kubeStore.BatchAdd(context.TODO(), datas2) @@ -110,7 +110,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(equal).To(BeEmpty()) }) - It("Test get funtion", func() { + It("Test get function", func() { app := &model.Application{Name: "kubevela-app"} err := kubeStore.Get(context.TODO(), app) Expect(err).Should(BeNil()) @@ -118,7 +118,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test put funtion", func() { + It("Test put function", func() { err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) @@ -136,7 +136,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { } Expect(cmp.Diff(selector.String(), "namespace=test,table=vela_application")).Should(BeEmpty()) }) - It("Test list funtion", func() { + It("Test list function", func() { var app model.Application list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) @@ -158,14 +158,71 @@ var _ = Describe("Test kubeapi datastore driver", func() { diff = cmp.Diff(len(list), 4) Expect(diff).Should(BeEmpty()) - app.Namespace = "test-namesapce" + app.Namespace = "test-namespace" list, err = kubeStore.List(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 1) Expect(diff).Should(BeEmpty()) }) - It("Test isExist funtion", func() { + It("Test list clusters with sort and fuzzy query", func() { + clusters, err := kubeStore.List(context.TODO(), &model.Cluster{}, nil) + Expect(err).Should(Succeed()) + for _, cluster := range clusters { + Expect(kubeStore.Delete(context.TODO(), cluster)).Should(Succeed()) + } + for _, name := range []string{"first", "second", "third"} { + Expect(kubeStore.Add(context.TODO(), &model.Cluster{Name: name})).Should(Succeed()) + time.Sleep(time.Millisecond * 100) + } + entities, err := kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderAscending}}}) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(3)) + for i, name := range []string{"first", "second", "third"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + Page: 2, + PageSize: 2, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(1)) + for i, name := range []string{"first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = kubeStore.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(2)) + for i, name := range []string{"third", "first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + }) + + It("Test count function", func() { + var app model.Application + count, err := kubeStore.Count(context.TODO(), &app, nil) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(4))) + + app.Namespace = "test-namespace" + count, err = kubeStore.Count(context.TODO(), &app, nil) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(1))) + + count, err = kubeStore.Count(context.TODO(), &model.Cluster{}, &datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }) + Expect(err).Should(Succeed()) + Expect(count).Should(Equal(int64(2))) + }) + + It("Test isExist function", func() { var app model.Application app.Name = "kubevela-app-3" exist, err := kubeStore.IsExist(context.TODO(), &app) @@ -180,7 +237,7 @@ var _ = Describe("Test kubeapi datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test delete funtion", func() { + It("Test delete function", func() { var app model.Application app.Name = "kubevela-app" err := kubeStore.Delete(context.TODO(), &app) diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index 270b0c7f4..d16dcb237 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -20,11 +20,13 @@ import ( "context" "errors" "fmt" + "time" "cuelang.org/go/pkg/strings" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/x/bsonx" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" @@ -61,6 +63,7 @@ func (m *mongodb) Add(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetCreateTime(time.Now()) if err := m.Get(ctx, entity); err == nil { return datastore.ErrRecordExist } @@ -121,6 +124,7 @@ func (m *mongodb) Put(ctx context.Context, entity datastore.Entity) error { if entity.TableName() == "" { return datastore.ErrTableNameEmpty } + entity.SetUpdateTime(time.Now()) collection := m.client.Database(m.database).Collection(entity.TableName()) _, err := collection.UpdateOne(ctx, makeNameFilter(entity.PrimaryKey()), makeEntityUpdate(entity)) if err != nil { @@ -179,6 +183,15 @@ func (m *mongodb) Delete(ctx context.Context, entity datastore.Entity) error { return nil } +func _applyFilterOptions(filter bson.D, filterOptions datastore.FilterOptions) bson.D { + if len(filterOptions.Queries) > 0 { + for _, queryOp := range filterOptions.Queries { + filter = append(filter, bson.E{Key: strings.ToLower(queryOp.Key), Value: bsonx.Regex(".*"+queryOp.Query+".*", "s")}) + } + } + return filter +} + // List list entity function func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) { if entity.TableName() == "" { @@ -195,11 +208,21 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto }) } } + if op != nil && len(op.Queries) > 0 { + filter = _applyFilterOptions(filter, op.FilterOptions) + } var findOptions options.FindOptions if op != nil && op.PageSize > 0 && op.Page > 0 { findOptions.SetSkip(int64(op.PageSize * (op.Page - 1))) findOptions.SetLimit(int64(op.PageSize)) } + if op != nil && len(op.SortBy) > 0 { + _d := bson.D{} + for _, sortOp := range op.SortBy { + _d = append(_d, bson.E{Key: strings.ToLower(sortOp.Key), Value: int(sortOp.Order)}) + } + findOptions.SetSort(_d) + } cur, err := collection.Find(ctx, filter, &findOptions) if err != nil { return nil, datastore.NewDBError(err) @@ -226,6 +249,31 @@ func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datasto return list, nil } +// Count counts entities +func (m *mongodb) Count(ctx context.Context, entity datastore.Entity, filterOptions *datastore.FilterOptions) (int64, error) { + if entity.TableName() == "" { + return 0, datastore.ErrTableNameEmpty + } + collection := m.client.Database(m.database).Collection(entity.TableName()) + filter := bson.D{} + if entity.Index() != nil { + for k, v := range entity.Index() { + filter = append(filter, bson.E{ + Key: k, + Value: v, + }) + } + } + if filterOptions != nil && len(filterOptions.Queries) > 0 { + filter = _applyFilterOptions(filter, *filterOptions) + } + count, err := collection.CountDocuments(ctx, filter) + if err != nil { + return 0, datastore.NewDBError(err) + } + return count, nil +} + func makeNameFilter(name string) bson.D { return bson.D{{Key: "name", Value: name}} } diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go index 19a53c9d9..8d49ab63c 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb_test.go +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -55,22 +55,22 @@ var _ = BeforeSuite(func(done Done) { var _ = Describe("Test mongodb datastore driver", func() { - It("Test add funtion", func() { + It("Test add function", func() { err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test batch add funtion", func() { + It("Test batch add function", func() { var datas = []datastore.Entity{ &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, - &model.Application{Namespace: "test-namesapce", Name: "kubevela-app-3", Description: "this is demo 3"}, - &model.Application{Namespace: "test-namesapce2", Name: "kubevela-app-4", Description: "this is demo 4"}, + &model.Application{Namespace: "test-namespace", Name: "kubevela-app-3", Description: "this is demo 3"}, + &model.Application{Namespace: "test-namespace2", Name: "kubevela-app-4", Description: "this is demo 4"}, } err := mongodbDriver.BatchAdd(context.TODO(), datas) Expect(err).ToNot(HaveOccurred()) var datas2 = []datastore.Entity{ - &model.Application{Namespace: "test-namesapce", Name: "can-delete", Description: "this is demo can-delete"}, + &model.Application{Namespace: "test-namespace", Name: "can-delete", Description: "this is demo can-delete"}, &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}, } err = mongodbDriver.BatchAdd(context.TODO(), datas2) @@ -78,7 +78,7 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(equal).To(BeEmpty()) }) - It("Test get funtion", func() { + It("Test get function", func() { app := &model.Application{Name: "kubevela-app"} err := mongodbDriver.Get(context.TODO(), app) Expect(err).Should(BeNil()) @@ -86,11 +86,11 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test put funtion", func() { + It("Test put function", func() { err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) Expect(err).ToNot(HaveOccurred()) }) - It("Test list funtion", func() { + It("Test list function", func() { var app model.Application list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) Expect(err).ShouldNot(HaveOccurred()) @@ -112,14 +112,71 @@ var _ = Describe("Test mongodb datastore driver", func() { diff = cmp.Diff(len(list), 4) Expect(diff).Should(BeEmpty()) - app.Namespace = "test-namesapce" + app.Namespace = "test-namespace" list, err = mongodbDriver.List(context.TODO(), &app, nil) Expect(err).ShouldNot(HaveOccurred()) diff = cmp.Diff(len(list), 1) Expect(diff).Should(BeEmpty()) }) - It("Test isExist funtion", func() { + It("Test list clusters with sort and fuzzy query", func() { + clusters, err := mongodbDriver.List(context.TODO(), &model.Cluster{}, nil) + Expect(err).Should(Succeed()) + for _, cluster := range clusters { + Expect(mongodbDriver.Delete(context.TODO(), cluster)).Should(Succeed()) + } + for _, name := range []string{"first", "second", "third"} { + Expect(mongodbDriver.Add(context.TODO(), &model.Cluster{Name: name})).Should(Succeed()) + time.Sleep(time.Millisecond * 100) + } + entities, err := mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderAscending}}}) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(3)) + for i, name := range []string{"first", "second", "third"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + Page: 2, + PageSize: 2, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(1)) + for i, name := range []string{"first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + entities, err = mongodbDriver.List(context.TODO(), &model.Cluster{}, &datastore.ListOptions{ + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }, + }) + Expect(err).Should(Succeed()) + Expect(len(entities)).Should(Equal(2)) + for i, name := range []string{"third", "first"} { + Expect(entities[i].(*model.Cluster).Name).Should(Equal(name)) + } + }) + + It("Test count function", func() { + var app model.Application + count, err := mongodbDriver.Count(context.TODO(), &app, nil) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(4))) + + app.Namespace = "test-namespace" + count, err = mongodbDriver.Count(context.TODO(), &app, nil) + Expect(err).ShouldNot(HaveOccurred()) + Expect(count).Should(Equal(int64(1))) + + count, err = mongodbDriver.Count(context.TODO(), &model.Cluster{}, &datastore.FilterOptions{ + Queries: []datastore.FuzzyQueryOption{{Key: "name", Query: "ir"}}, + }) + Expect(err).Should(Succeed()) + Expect(count).Should(Equal(int64(2))) + }) + + It("Test isExist function", func() { var app model.Application app.Name = "kubevela-app-3" exist, err := mongodbDriver.IsExist(context.TODO(), &app) @@ -134,7 +191,7 @@ var _ = Describe("Test mongodb datastore driver", func() { Expect(diff).Should(BeEmpty()) }) - It("Test delete funtion", func() { + It("Test delete function", func() { var app model.Application app.Name = "kubevela-app" err := mongodbDriver.Delete(context.TODO(), &app) diff --git a/pkg/apiserver/model/addon.go b/pkg/apiserver/model/addon.go new file mode 100644 index 000000000..d9f8e86ab --- /dev/null +++ b/pkg/apiserver/model/addon.go @@ -0,0 +1,46 @@ +/* +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 model + +import "github.com/oam-dev/kubevela/pkg/addon" + +// AddonRegistry defines the data model of a AddonRegistry +type AddonRegistry struct { + Model + Name string `json:"name"` + + Git *addon.GitAddonSource `json:"git,omitempty"` +} + +// TableName return custom table name +func (a *AddonRegistry) TableName() string { + return tableNamePrefix + "addon_registry" +} + +// PrimaryKey return custom primary key +func (a *AddonRegistry) PrimaryKey() string { + return a.Name +} + +// Index return custom index +func (a *AddonRegistry) Index() map[string]string { + index := make(map[string]string) + if a.Name != "" { + index["name"] = a.Name + } + return index +} diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go index 9e0e69cbc..7538634ec 100644 --- a/pkg/apiserver/model/application.go +++ b/pkg/apiserver/model/application.go @@ -18,18 +18,24 @@ package model import ( "fmt" + "time" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) -// Application database model +func init() { + RegistModel(&ApplicationComponent{}, &ApplicationPolicy{}, &Application{}, &ApplicationRevision{}) +} + +// Application application delivery model type Application struct { + Model Name string `json:"name"` + Alias string `json:"alias"` Namespace string `json:"namespace"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - ClusterList []string `json:"clusterList,omitempty"` } // TableName return custom table name @@ -54,14 +60,28 @@ func (a *Application) Index() map[string]string { return index } +// ClusterSelector cluster selector +type ClusterSelector struct { + Name string `json:"name"` + // Adapt to a scenario where only one Namespace is available or a user-defined Namespace is available. + Namespace string `json:"namespace,omitempty"` +} + +// ComponentSelector component selector +type ComponentSelector struct { + Components []string `json:"components"` +} + // ApplicationComponent component database model type ApplicationComponent struct { + Model AppPrimaryKey string `json:"appPrimaryKey"` Description string `json:"description,omitempty"` - Labels map[string]string `json:"lables,omitempty"` + Labels map[string]string `json:"labels,omitempty"` Icon string `json:"icon,omitempty"` Creator string `json:"creator"` Name string `json:"name"` + Alias string `json:"alias"` Type string `json:"type"` // ExternalRevision specified the component revisionName @@ -104,9 +124,12 @@ func (a *ApplicationComponent) Index() map[string]string { // ApplicationPolicy app policy type ApplicationPolicy struct { + Model AppPrimaryKey string `json:"appPrimaryKey"` Name string `json:"name"` + Description string `json:"description"` Type string `json:"type"` + Creator string `json:"creator"` Properties *JSONStruct `json:"properties,omitempty"` } @@ -137,6 +160,88 @@ func (a *ApplicationPolicy) Index() map[string]string { // ApplicationTrait application trait type ApplicationTrait struct { - Type string `json:"type"` - Properties *JSONStruct `json:"properties,omitempty"` + Alias string `json:"alias"` + Description string `json:"description"` + Type string `json:"type"` + Properties *JSONStruct `json:"properties,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// RevisionStatusInit event status init +var RevisionStatusInit = "init" + +// RevisionStatusRunning event status running +var RevisionStatusRunning = "running" + +// RevisionStatusComplete event status complete +var RevisionStatusComplete = "complete" + +// RevisionStatusFail event status failure +var RevisionStatusFail = "failure" + +// RevisionStatusTerminated event status terminated +var RevisionStatusTerminated = "terminated" + +// ApplicationRevision be created when an application initiates deployment and describes the phased version of the application. +type ApplicationRevision struct { + Model + AppPrimaryKey string `json:"appPrimaryKey"` + Version string `json:"version"` + // ApplyAppConfig Stores the application configuration during the current deploy. + ApplyAppConfig string `json:"applyAppConfig,omitempty"` + + // Deploy event status + Status string `json:"status"` + Reason string `json:"reason"` + + // The user that triggers the deploy. + DeployUser string `json:"deployUser"` + + // Information that users can note. + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType"` + + // WorkflowName deploy controller by workflow + WorkflowName string `json:"workflowName"` + // EnvName is the env name of this application revision + EnvName string `json:"envName"` +} + +// TableName return custom table name +func (a *ApplicationRevision) TableName() string { + return tableNamePrefix + "application_revision" +} + +// PrimaryKey return custom primary key +func (a *ApplicationRevision) PrimaryKey() string { + return fmt.Sprintf("%s-%s", a.AppPrimaryKey, a.Version) +} + +// Index return custom index +func (a *ApplicationRevision) Index() map[string]string { + index := make(map[string]string) + if a.Version != "" { + index["version"] = a.Version + } + if a.AppPrimaryKey != "" { + index["appPrimaryKey"] = a.AppPrimaryKey + } + if a.WorkflowName != "" { + index["workflowName"] = a.WorkflowName + } + if a.DeployUser != "" { + index["deployUser"] = a.DeployUser + } + if a.Status != "" { + index["status"] = a.Status + } + if a.TriggerType != "" { + index["triggerType"] = a.TriggerType + } + if a.EnvName != "" { + index["envName"] = a.EnvName + } + return index } diff --git a/pkg/apiserver/model/catalog.go b/pkg/apiserver/model/catalog.go deleted file mode 100644 index 2e334d1ab..000000000 --- a/pkg/apiserver/model/catalog.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -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 model - -// Catalog defines the data model of a Catalog -type Catalog struct { - Name string `json:"name,omitempty"` - Desc string `json:"desc,omitempty"` - // UpdatedAt is the unix time of the last time when the catalog is updated. - UpdatedAt int64 `json:"updated_at,omitempty"` - // Type of the Catalog, such as "github" for a github repo. - Type string `json:"type,omitempty"` - // URL of the Catalog. - URL string `json:"url,omitempty"` - // Auth token used to sync Catalog. - Token string `json:"token,omitempty"` -} diff --git a/pkg/apiserver/model/cluster.go b/pkg/apiserver/model/cluster.go new file mode 100644 index 000000000..3b5f858d4 --- /dev/null +++ b/pkg/apiserver/model/cluster.go @@ -0,0 +1,102 @@ +/* +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 model + +import ( + "time" + + "github.com/oam-dev/kubevela/pkg/multicluster" +) + +func init() { + RegistModel(&Cluster{}) +} + +// ProviderInfo describes the information from provider API +type ProviderInfo struct { + Provider string `json:"provider"` + ClusterID string `json:"clusterID"` + ClusterName string `json:"clusterName,omitempty"` + Zone string `json:"zone,omitempty"` + ZoneID string `json:"zoneID,omitempty"` + RegionID string `json:"regionID,omitempty"` + VpcID string `json:"vpcID,omitempty"` + Labels map[string]string `json:"labels"` +} + +const ( + // ClusterStatusHealthy healthy cluster + ClusterStatusHealthy = "Healthy" + // ClusterStatusUnhealthy unhealthy cluster + ClusterStatusUnhealthy = "Unhealthy" +) + +var ( + // LocalClusterCreatedTime create time for local cluster, set to late date in order to ensure it is sorted to first + LocalClusterCreatedTime = time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC) +) + +// Cluster describes the model of cluster in apiserver +type Cluster struct { + Model `json:"model"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` + Reason string `json:"reason"` + Provider ProviderInfo `json:"provider"` + APIServerURL string `json:"apiServerURL"` + DashboardURL string `json:"dashboardURL"` + KubeConfig string `json:"kubeConfig"` + KubeConfigSecret string `json:"kubeConfigSecret"` +} + +// SetCreateTime for local cluster, create time is set to a large date which ensures the order of list +func (c *Cluster) SetCreateTime(t time.Time) { + if c.Name == multicluster.ClusterLocalName { + c.CreateTime = LocalClusterCreatedTime + c.SetUpdateTime(t) + } else { + c.CreateTime = t + } +} + +// TableName table name for datastore +func (c *Cluster) TableName() string { + return tableNamePrefix + "cluster" +} + +// PrimaryKey primary key for datastore +func (c *Cluster) PrimaryKey() string { + return c.Name +} + +// Index set to nil for list +func (c *Cluster) Index() map[string]string { + index := make(map[string]string) + if c.Name != "" { + index["name"] = c.Name + } + return index +} + +// DeepCopy create a copy of cluster +func (c *Cluster) DeepCopy() *Cluster { + return deepCopy(c).(*Cluster) +} diff --git a/pkg/apiserver/model/deliverytarget.go b/pkg/apiserver/model/deliverytarget.go new file mode 100644 index 000000000..78d98acef --- /dev/null +++ b/pkg/apiserver/model/deliverytarget.go @@ -0,0 +1,61 @@ +/* +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 model + +func init() { + RegistModel(&DeliveryTarget{}) +} + +// DeliveryTarget defines the delivery target information for the application +// It includes kubernetes clusters or cloud service providers +type DeliveryTarget struct { + Model + Name string `json:"name"` + Namespace string `json:"namespace"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + Cluster *ClusterTarget `json:"cluster,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` +} + +// TableName return custom table name +func (d *DeliveryTarget) TableName() string { + return tableNamePrefix + "delivery_target" +} + +// PrimaryKey return custom primary key +func (d *DeliveryTarget) PrimaryKey() string { + return d.Name +} + +// Index return custom index +func (d *DeliveryTarget) Index() map[string]string { + index := make(map[string]string) + if d.Name != "" { + index["name"] = d.Name + } + if d.Namespace != "" { + index["namespace"] = d.Namespace + } + return index +} + +// ClusterTarget kubernetes delivery target +type ClusterTarget struct { + ClusterName string `json:"clusterName" validate:"checkname"` + Namespace string `json:"namespace" optional:"true"` +} diff --git a/pkg/apiserver/model/envbinding.go b/pkg/apiserver/model/envbinding.go new file mode 100644 index 000000000..c4821521e --- /dev/null +++ b/pkg/apiserver/model/envbinding.go @@ -0,0 +1,57 @@ +/* +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 model + +import "fmt" + +func init() { + RegistModel(&EnvBinding{}) +} + +// EnvBinding application env binding +type EnvBinding struct { + Model + AppPrimaryKey string `json:"appPrimaryKey"` + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description,omitempty"` + TargetNames []string `json:"targetNames"` + ComponentSelector *ComponentSelector `json:"componentSelector"` + //TODO: componentPatchs +} + +// TableName return custom table name +func (e *EnvBinding) TableName() string { + return tableNamePrefix + "envbinding" +} + +// PrimaryKey return custom primary key +func (e *EnvBinding) PrimaryKey() string { + return fmt.Sprintf("%s-%s", e.AppPrimaryKey, e.Name) +} + +// Index return custom index +func (e *EnvBinding) Index() map[string]string { + index := make(map[string]string) + if e.Name != "" { + index["name"] = e.Name + } + if e.AppPrimaryKey != "" { + index["appPrimaryKey"] = e.AppPrimaryKey + } + return index +} diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go index 54955f316..dd6bf5702 100644 --- a/pkg/apiserver/model/model.go +++ b/pkg/apiserver/model/model.go @@ -19,17 +19,39 @@ package model import ( "encoding/json" "fmt" + "reflect" + "time" "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" ) var tableNamePrefix = "vela_" +var registedModels = map[string]Interface{} + +// Interface model interface +type Interface interface { + TableName() string +} + +// RegistModel regist model +func RegistModel(models ...Interface) { + for _, model := range models { + if _, exist := registedModels[model.TableName()]; exist { + panic(fmt.Errorf("model table name %s conflict", model.TableName())) + } + registedModels[model.TableName()] = model + } +} + // JSONStruct json struct, same with runtime.RawExtension type JSONStruct map[string]interface{} // NewJSONStruct new jsonstruct from runtime.RawExtension -func NewJSONStruct(raw runtime.RawExtension) (*JSONStruct, error) { +func NewJSONStruct(raw *runtime.RawExtension) (*JSONStruct, error) { var data JSONStruct err := json.Unmarshal(raw.Raw, &data) if err != nil { @@ -37,3 +59,85 @@ func NewJSONStruct(raw runtime.RawExtension) (*JSONStruct, error) { } return &data, nil } + +// NewJSONStructByString new jsonstruct from string +func NewJSONStructByString(source string) (*JSONStruct, error) { + if source == "" { + return nil, nil + } + var data JSONStruct + err := json.Unmarshal([]byte(source), &data) + if err != nil { + return nil, fmt.Errorf("parse raw data failure %w", err) + } + return &data, nil +} + +// NewJSONStructByStruct new jsonstruct from strcut object +func NewJSONStructByStruct(object interface{}) (*JSONStruct, error) { + if object == nil { + return nil, nil + } + var data JSONStruct + out, err := yaml.Marshal(object) + if err != nil { + return nil, fmt.Errorf("marshal object data failure %w", err) + } + if err := yaml.Unmarshal(out, &data); err != nil { + return nil, fmt.Errorf("unmarshal object data failure %w", err) + } + return &data, nil +} + +// JSON Encoded as a JSON string +func (j *JSONStruct) JSON() string { + b, err := json.Marshal(j) + if err != nil { + log.Logger.Errorf("json marshal failure %s", err.Error()) + } + return string(b) +} + +// RawExtension Encoded as a RawExtension +func (j *JSONStruct) RawExtension() *runtime.RawExtension { + yamlByte, err := yaml.Marshal(j) + if err != nil { + log.Logger.Errorf("yaml marshal failure %s", err.Error()) + return nil + } + b, err := yaml.YAMLToJSON(yamlByte) + if err != nil { + log.Logger.Errorf("yaml to json failure %s", err.Error()) + return nil + } + return &runtime.RawExtension{Raw: b} +} + +// Model common model +type Model struct { + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// SetCreateTime set create time +func (m *Model) SetCreateTime(time time.Time) { + m.CreateTime = time +} + +// SetUpdateTime set update time +func (m *Model) SetUpdateTime(time time.Time) { + m.UpdateTime = time +} + +func deepCopy(src interface{}) interface{} { + dst := reflect.New(reflect.TypeOf(src).Elem()) + + val := reflect.ValueOf(src).Elem() + nVal := dst.Elem() + for i := 0; i < val.NumField(); i++ { + nvField := nVal.Field(i) + nvField.Set(val.Field(i)) + } + + return dst.Interface() +} diff --git a/pkg/apiserver/model/workflow.go b/pkg/apiserver/model/workflow.go index afa38608d..ecdc8ef2a 100644 --- a/pkg/apiserver/model/workflow.go +++ b/pkg/apiserver/model/workflow.go @@ -18,40 +18,96 @@ package model import ( "fmt" + "strconv" + "time" "github.com/oam-dev/kubevela/apis/core.oam.dev/common" ) -// Workflow workflow database model +func init() { + RegistModel(&Workflow{}) + RegistModel(&WorkflowRecord{}) +} + +// Workflow application delivery database model type Workflow struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Steps []WorkflowStep `json:"steps,omitempty"` + Model + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description"` + // Workflow used by the default + Default bool `json:"default"` + AppPrimaryKey string `json:"appPrimaryKey"` + EnvName string `json:"envName"` + Steps []WorkflowStep `json:"steps,omitempty"` } // WorkflowStep defines how to execute a workflow step. type WorkflowStep struct { // Name is the unique name of the workflow step. - Name string `json:"name"` - Type string `json:"type"` - Properties JSONStruct `json:"properties,omitempty"` - DependsOn []string `json:"dependsOn,omitempty"` - Inputs common.StepInputs `json:"inputs,omitempty"` - Outputs common.StepOutputs `json:"outputs,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Group string `json:"group"` + Description string `json:"description"` + OrderIndex int `json:"orderIndex"` + Inputs common.StepInputs `json:"inputs,omitempty"` + Outputs common.StepOutputs `json:"outputs,omitempty"` + DependsOn []string `json:"dependsOn"` + Properties *JSONStruct `json:"properties,omitempty"` } // TableName return custom table name func (w *Workflow) TableName() string { - return tableNamePrefix + "application_component" + return tableNamePrefix + "workflow" } // PrimaryKey return custom primary key func (w *Workflow) PrimaryKey() string { - return fmt.Sprintf("%s-%s", w.Namespace, w.Name) + return fmt.Sprintf("%s-%s", w.AppPrimaryKey, w.Name) } // Index return custom primary key func (w *Workflow) Index() map[string]string { + index := make(map[string]string) + if w.Name != "" { + index["name"] = w.Name + } + if w.AppPrimaryKey != "" { + index["appPrimaryKey"] = w.AppPrimaryKey + } + if w.EnvName != "" { + index["envName"] = w.EnvName + } + index["default"] = strconv.FormatBool(w.Default) + return index +} + +// WorkflowRecord is the workflow record database model +type WorkflowRecord struct { + Model + WorkflowName string `json:"workflowName"` + AppPrimaryKey string `json:"appPrimaryKey"` + RevisionPrimaryKey string `json:"revisionPrimaryKey"` + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Finished string `json:"finished"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` + Status string `json:"status"` +} + +// TableName return custom table name +func (w *WorkflowRecord) TableName() string { + return tableNamePrefix + "workflow_record" +} + +// PrimaryKey return custom primary key +func (w *WorkflowRecord) PrimaryKey() string { + return w.Name +} + +// Index return custom primary key +func (w *WorkflowRecord) Index() map[string]string { index := make(map[string]string) if w.Name != "" { index["name"] = w.Name @@ -59,5 +115,20 @@ func (w *Workflow) Index() map[string]string { if w.Namespace != "" { index["namespace"] = w.Namespace } + if w.WorkflowName != "" { + index["workflowPrimaryKey"] = w.WorkflowName + } + if w.AppPrimaryKey != "" { + index["appPrimaryKey"] = w.AppPrimaryKey + } + if w.RevisionPrimaryKey != "" { + index["revisionPrimaryKey"] = w.RevisionPrimaryKey + } + if w.Finished != "" { + index["finished"] = w.Finished + } + if w.Status != "" { + index["status"] = w.Status + } return index } diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 4cc798fb6..1b2cee373 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -19,8 +19,28 @@ package v1 import ( "time" + "github.com/oam-dev/kubevela/pkg/addon" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/cloudprovider" +) + +var ( + // CtxKeyApplication request context key of application + CtxKeyApplication = "application" + // CtxKeyWorkflow request context key of workflow + CtxKeyWorkflow = "workflow" + // CtxKeyDeliveryTarget request context key of workflow + CtxKeyDeliveryTarget = "delivery-target" + // CtxKeyApplicationEnvBinding request context key of env binding + CtxKeyApplicationEnvBinding = "envbinding-policy" + // CtxKeyApplicationComponent request context key of component + CtxKeyApplicationComponent = "component" ) // AddonPhase defines the phase of an addon @@ -29,93 +49,126 @@ type AddonPhase string const ( // AddonPhaseDisabled indicates the addon is disabled AddonPhaseDisabled AddonPhase = "disabled" - // AddonPhaseDisabling indicates the addon is disabling - AddonPhaseDisabling AddonPhase = "disabling" // AddonPhaseEnabled indicates the addon is enabled AddonPhaseEnabled AddonPhase = "enabled" // AddonPhaseEnabling indicates the addon is enabling AddonPhaseEnabling AddonPhase = "enabling" ) -// CreateAddonRequest defines the format for addon create request -type CreateAddonRequest struct { - Name string `json:"name" validate:"name"` +// EmptyResponse empty response, it will used for delete api +type EmptyResponse struct{} - Version string `json:"version" validate:"required"` +// CreateAddonRegistryRequest defines the format for addon registry create request +type CreateAddonRegistryRequest struct { + Name string `json:"name" validate:"checkname"` + Git *addon.GitAddonSource `json:"git,omitempty" validate:"required"` +} - // Short description about the addon. - Description string `json:"description,omitempty"` +// UpdateAddonRegistryRequest defines the format for addon registry update request +type UpdateAddonRegistryRequest struct { + Git *addon.GitAddonSource `json:"git,omitempty" validate:"required"` +} - Icon string `json:"icon"` +// AddonRegistryMeta defines the format for a single addon registry +type AddonRegistryMeta struct { + Name string `json:"name" validate:"required"` + Git *addon.GitAddonSource `json:"git,omitempty"` +} - Tags []string `json:"tags"` +// ListAddonRegistryResponse list addon registry +type ListAddonRegistryResponse struct { + Registrys []*AddonRegistryMeta `json:"registrys"` +} - // The detail of the addon. Could be the entire README data. - Detail string `json:"detail,omitempty"` - - // DeployData is the object to deploy to the cluster to enable addon - DeployData string `json:"deploy_data,omitempty" validate:"required_without=deploy_url"` - - // DeployURL is the URL to the data file location in a Git repository - DeployURL string `json:"deploy_url,omitempty" validate:"required_without=deploy_data"` +// EnableAddonRequest defines the format for enable addon request +type EnableAddonRequest struct { + // Args is the key-value environment variables, e.g. AK/SK credentials. + Args map[string]string `json:"args,omitempty"` } // ListAddonResponse defines the format for addon list response type ListAddonResponse struct { - Addons []AddonMeta `json:"addons"` -} - -// AddonMeta defines the format for a single addon -type AddonMeta struct { - Name string `json:"name"` - - Version string `json:"version"` - - Description string `json:"description"` - - Icon string `json:"icon"` - - Tags []string `json:"tags"` - - Phase AddonPhase `json:"phase"` + Addons []*types.AddonMeta `json:"addons"` } // DetailAddonResponse defines the format for showing the addon details type DetailAddonResponse struct { - AddonMeta + types.AddonMeta - Detail string `json:"detail,omitempty"` + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` - // DeployData is the object to deploy to the cluster to enable addon - DeployData string `json:"deploy_data,omitempty"` + // More details about the addon, e.g. README + Detail string `json:"detail,omitempty"` + Definitions []*AddonDefinition `json:"definitions"` +} - // DeployURL is the URL to the data file location in a Git repository - DeployURL string `json:"deploy_url,omitempty"` +// AddonDefinition is definition an addon can provide +type AddonDefinition struct { + Name string `json:"name,omitempty"` + // can be component/trait...definition + DefType string `json:"type,omitempty"` + Description string `json:"description,omitempty"` } // AddonStatusResponse defines the format of addon status response type AddonStatusResponse struct { - Phase AddonPhase `json:"phase"` + Phase AddonPhase `json:"phase"` + Args map[string]string `json:"args"` + + EnablingProgress *EnablingProgress `json:"enabling_progress,omitempty"` +} + +// EnablingProgress defines the progress of enabling an addon +type EnablingProgress struct { + EnabledComponents int `json:"enabled_components"` + TotalComponents int `json:"total_components"` +} + +// AddonArgsResponse defines the response of addon args +type AddonArgsResponse struct { + Args map[string]string `json:"args"` +} + +// AccessKeyRequest request parameters to access cloud provider +type AccessKeyRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` } // CreateClusterRequest request parameters to create a cluster type CreateClusterRequest struct { - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` Description string `json:"description,omitempty"` Icon string `json:"icon"` - KubeConfig string `json:"kubeConfig" validate:"required_without=kubeConfigSecret"` - KubeConfigSecret string `json:"kubeConfigSecret,omitempty" validate:"required_without=kubeConfig"` + KubeConfig string `json:"kubeConfig,omitempty" validate:"required_without=KubeConfigSecret"` + KubeConfigSecret string `json:"kubeConfigSecret,omitempty" validate:"required_without=KubeConfig"` Labels map[string]string `json:"labels,omitempty"` + DashboardURL string `json:"dashboardURL,omitempty"` } -// DetailClusterResponse cluster detail information model -type DetailClusterResponse struct { - ClusterBase - ResourceInfo ClusterResourceInfo `json:"resourceInfo"` - // remote manage url, eg. ACK cluster manage url. - RemoteManageURL string `json:"remoteManageURL,omitempty"` - // Dashboard URL - DashboardURL string `json:"dashboardURL,omitempty"` +// ConnectCloudClusterRequest request parameters to create a cluster from cloud cluster +type ConnectCloudClusterRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` + ClusterID string `json:"clusterID"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" optional:"true" validate:"checkalias"` + Description string `json:"description,omitempty" optional:"true"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` +} + +// CreateCloudClusterRequest request parameters to create a cloud cluster (buy one) +type CreateCloudClusterRequest struct { + AccessKeyID string `json:"accessKeyID"` + AccessKeySecret string `json:"accessKeySecret"` + Name string `json:"name" validate:"checkname"` + Zone string `json:"zone"` + WorkerNumber int `json:"workerNumber"` + CPUCoresPerWorker int64 `json:"cpuCoresPerWorker"` + MemoryPerWorker int64 `json:"memoryPerWorker"` } // ClusterResourceInfo resource info of cluster @@ -125,22 +178,75 @@ type ClusterResourceInfo struct { MemoryCapacity int64 `json:"memoryCapacity"` CPUCapacity int64 `json:"cpuCapacity"` GPUCapacity int64 `json:"gpuCapacity,omitempty"` + PodCapacity int64 `json:"podCapacity"` + MemoryUsed int64 `json:"memoryUsed"` + CPUUsed int64 `json:"cpuUsed"` + GPUUsed int64 `json:"gpuUsed,omitempty"` + PodUsed int64 `json:"podUsed"` StorageClassList []string `json:"storageClassList,omitempty"` } +// CreateClusterNamespaceRequest request parameter to create namespace in cluster +type CreateClusterNamespaceRequest struct { + Namespace string `json:"namespace"` +} + +// CreateClusterNamespaceResponse response parameter for created namespace in cluster +type CreateClusterNamespaceResponse struct { + Exists bool `json:"exists"` +} + +// DetailClusterResponse cluster detail information model +type DetailClusterResponse struct { + model.Cluster + ResourceInfo ClusterResourceInfo `json:"resourceInfo"` +} + // ListClusterResponse list cluster type ListClusterResponse struct { Clusters []ClusterBase `json:"clusters"` + Total int64 `json:"total"` +} + +// ListCloudClusterResponse list cloud clusters +type ListCloudClusterResponse struct { + Clusters []cloudprovider.CloudCluster `json:"clusters"` + Total int `json:"total"` +} + +// CreateCloudClusterResponse return values for cloud cluster create request +type CreateCloudClusterResponse struct { + Name string `json:"clusterName"` + ClusterID string `json:"clusterID"` + Status string `json:"status"` +} + +// ListCloudClusterCreationResponse return the cluster names of creation process of cloud clusters +type ListCloudClusterCreationResponse struct { + Creations []CreateCloudClusterResponse `json:"creations"` } // ClusterBase cluster base model type ClusterBase struct { Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels"` - Status string `json:"status"` - Reason string `json:"reason"` + Alias string `json:"alias" optional:"true" validate:"checkalias"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels" optional:"true"` + + Provider model.ProviderInfo `json:"providerInfo"` + APIServerURL string `json:"apiServerURL"` + DashboardURL string `json:"dashboardURL"` + + Status string `json:"status"` + Reason string `json:"reason"` +} + +// ListApplicatioOptions list application query options +type ListApplicatioOptions struct { + Namespace string `json:"namespace"` + TargetName string `json:"targetName"` + Query string `json:"query"` } // ListApplicationResponse list applications by query params @@ -148,59 +254,112 @@ type ListApplicationResponse struct { Applications []*ApplicationBase `json:"applications"` } +// EnvBindingList env binding list +type EnvBindingList []*EnvBinding + +// ContainTarget contain cluster name +func (e EnvBindingList) ContainTarget(name string) bool { + for _, eb := range e { + if utils.StringsContain(eb.TargetNames, name) { + return true + } + } + return false +} + // ApplicationBase application base model type ApplicationBase struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Description string `json:"description"` - CreateTime time.Time `json:"createTime"` - UpdateTime time.Time `json:"updateTime"` - Icon string `json:"icon"` - Labels map[string]string `json:"labels,omitempty"` - ClusterBindList []ClusterBase `json:"clusterList,omitempty"` - Status string `json:"status"` - GatewayRuleList []GatewayRule `json:"gatewayRule"` -} - -// RuleType gateway rule type -type RuleType string - -const ( - // HTTPRule Layer 7 HTTP policy. - HTTPRule RuleType = "http" - // StreamRule Layer 4 policy, such as TCP and UDP - StreamRule RuleType = "stream" -) - -// GatewayRule application gateway rule -type GatewayRule struct { - RuleType RuleType `json:"ruleType"` - Address string `json:"address"` - Protocol string `json:"protocol"` - ComponentName string `json:"componentName"` - ComponentPort int32 `json:"componentPort"` -} - -// CreateApplicationRequest create application request body -type CreateApplicationRequest struct { - Name string `json:"name" validate:"checkname"` - Namespace string `json:"namespace" validate:"checkname"` + Name string `json:"name"` + Alias string `json:"alias"` + Namespace string `json:"namespace"` Description string `json:"description"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` - ClusterList []string `json:"clusterList,omitempty"` - YamlConfig string `json:"yamlConfig,omitempty"` - // Deploy Setting this to true means that the application is deployed directly after creation. - Deploy bool `json:"deploy,omitempty"` } -// DetailApplicationResponse application detail +// ApplicationStatusResponse application status response body +type ApplicationStatusResponse struct { + EnvName string `json:"envName"` + Status *common.AppStatus `json:"status"` +} + +// ApplicationStatisticsResponse application statistics response body +type ApplicationStatisticsResponse struct { + EnvCount int64 `json:"envCount"` + DeliveryTargetCount int64 `json:"deliveryTargetCount"` + RevisonCount int64 `json:"revisonCount"` + WorkflowCount int64 `json:"workflowCount"` +} + +// CreateApplicationRequest create application request body +type CreateApplicationRequest struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Namespace string `json:"namespace" validate:"checkname"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` + EnvBinding []*EnvBinding `json:"envBinding,omitempty"` + YamlConfig string `json:"yamlConfig,omitempty"` + Component *CreateComponentRequest `json:"component"` +} + +// UpdateApplicationRequest update application base config +type UpdateApplicationRequest struct { + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels,omitempty"` +} + +// EnvBinding application env binding +type EnvBinding struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` + ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` +} + +// EnvBindingBase application env binding +type EnvBindingBase struct { + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` + Targets []DeliveryTargetBase `json:"deliveryTargets,omitempty"` + ComponentSelector *ComponentSelector `json:"componentSelector" optional:"true"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + AppDeployName string `json:"appDeployName"` +} + +// DetailEnvBindingResponse defines the response of env-binding details +type DetailEnvBindingResponse struct { + EnvBindingBase +} + +// ClusterSelector cluster selector +type ClusterSelector struct { + Name string `json:"name" validate:"checkname"` + // Adapt to a scenario where only one Namespace is available or a user-defined Namespace is available. + Namespace string `json:"namespace,omitempty"` +} + +// ComponentSelector component selector +type ComponentSelector struct { + Components []string `json:"components"` +} + +// DetailApplicationResponse application detail type DetailApplicationResponse struct { ApplicationBase - Policies []string `json:"policies"` - Status string `json:"status"` - ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` - WorkflowStatus []WorkflowStepStatus `json:"workflowStatus"` + Policies []string `json:"policies"` + EnvBindings []string `json:"envBindings"` + Status string `json:"status"` + ResourceInfo ApplicationResourceInfo `json:"resourceInfo"` } // WorkflowStepStatus workflow step status model @@ -212,42 +371,67 @@ type WorkflowStepStatus struct { // ApplicationResourceInfo application-level resource consumption statistics type ApplicationResourceInfo struct { - ComponentNum int `json:"componentNum"` + ComponentNum int64 `json:"componentNum"` // Others, such as: Memory、CPU、GPU、Storage } -// ComponentBase component base model +// ComponentBase component base model type ComponentBase struct { Name string `json:"name"` + Alias string `json:"alias"` Description string `json:"description"` Labels map[string]string `json:"labels,omitempty"` ComponentType string `json:"componentType"` - BindClusters []string `json:"bindClusters"` + EnvNames []string `json:"envNames"` Icon string `json:"icon,omitempty"` DependsOn []string `json:"dependsOn"` Creator string `json:"creator,omitempty"` DeployVersion string `json:"deployVersion"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // ComponentListResponse list component type ComponentListResponse struct { - Components []ComponentBase `json:"components"` + Components []*ComponentBase `json:"components"` } -// CreateComponentRequest create component request model +// CreateComponentRequest create component request model type CreateComponentRequest struct { - ApplicationName string `json:"appName" validate:"name"` - Name string `json:"name" validate:"required"` - Description string `json:"description"` - Labels map[string]string `json:"labels,omitempty"` - ComponentType string `json:"componentType" validate:"required"` - BindClusters []string `json:"bindClusters"` - Properties string `json:"properties,omitempty"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Icon string `json:"icon" optional:"true"` + Labels map[string]string `json:"labels,omitempty"` + ComponentType string `json:"componentType" validate:"checkname"` + Properties string `json:"properties,omitempty"` + DependsOn []string `json:"dependsOn" optional:"true"` + Traits []*CreateApplicationTraitRequest `json:"traits,omitempty" optional:"true"` +} + +// UpdateApplicationComponentRequest update component request body +type UpdateApplicationComponentRequest struct { + Alias *string `json:"alias" validate:"checkalias" optional:"true"` + Description *string `json:"description" optional:"true"` + Icon *string `json:"icon" optional:"true"` + Labels *map[string]string `json:"labels,omitempty"` + Properties *string `json:"properties,omitempty"` + DependsOn *[]string `json:"dependsOn" optional:"true"` +} + +// DetailComponentResponse detail component response body +type DetailComponentResponse struct { + model.ApplicationComponent +} + +// ListApplicationComponentOptions list app component list +type ListApplicationComponentOptions struct { + EnvName string `json:"envName"` } // CreateApplicationTemplateRequest create app template request model type CreateApplicationTemplateRequest struct { - TemplateName string `json:"templateName" validate:"required"` + TemplateName string `json:"templateName" validate:"checkname"` Version string `json:"version" validate:"required"` Description string `json:"description"` } @@ -256,6 +440,8 @@ type CreateApplicationTemplateRequest struct { type ApplicationTemplateBase struct { TemplateName string `json:"templateName"` Versions []*ApplicationTemplateVersion `json:"versions,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // ApplicationTemplateVersion template version model @@ -269,62 +455,90 @@ type ApplicationTemplateVersion struct { // ListNamespaceResponse namesace list model type ListNamespaceResponse struct { - Namespaces []NamesapceBase `json:"namesapces"` + Namespaces []NamespaceBase `json:"namespaces"` } -// NamesapceBase namespace base model -type NamesapceBase struct { - Name string `json:"name"` - Description string `json:"description"` +// NamespaceBase namespace base model +type NamespaceBase struct { + Name string `json:"name"` + Description string `json:"description"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` } // CreateNamespaceRequest create namespace request body type CreateNamespaceRequest struct { - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` Description string `json:"description"` } -// NamesapceDetailResponse namespace detail response -type NamesapceDetailResponse struct { - NamesapceBase - ClusterBind map[string]string `json:"clusterBind"` +// NamespaceDetailResponse namespace detail response +type NamespaceDetailResponse struct { + NamespaceBase } -// ListComponentDefinitionResponse list component dedinition response model -type ListComponentDefinitionResponse struct { - ComponentDefinitions []ComponentDefinitionBase `json:"componentDefinitions"` +// ListDefinitionResponse list definition response model +type ListDefinitionResponse struct { + Definitions []*DefinitionBase `json:"definitions"` } -// ComponentDefinitionBase component definition base model -type ComponentDefinitionBase struct { - Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - Parameter []types.Parameter `json:"requiredParams"` +// DetailDefinitionResponse get definition detail +type DetailDefinitionResponse struct { + APISchema *openapi3.Schema `json:"schema"` + UISchema []*utils.UIParameter `json:"uiSchema"` +} + +// DefinitionBase is the definition base model +type DefinitionBase struct { + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` } // CreatePolicyRequest create app policy type CreatePolicyRequest struct { // Name is the unique name of the policy. - Name string `json:"name" validate:"name"` + Name string `json:"name" validate:"checkname"` - Type string `json:"type" validate:"required"` + Description string `json:"description"` + + Type string `json:"type" validate:"checkname"` // Properties json data Properties string `json:"properties"` } -// DetailPolicyResponse app policy detail model -type DetailPolicyResponse struct { - // Name is the unique name of the policy. - Name string `json:"name"` - - Type string `json:"type"` - +// UpdatePolicyRequest update policy +type UpdatePolicyRequest struct { + Description string `json:"description"` + Type string `json:"type" validate:"checkname"` // Properties json data Properties string `json:"properties"` } +// PolicyBase application policy base info +type PolicyBase struct { + // Name is the unique name of the policy. + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Creator string `json:"creator"` + // Properties json data + Properties *model.JSONStruct `json:"properties"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// DetailPolicyResponse app policy detail model +type DetailPolicyResponse struct { + PolicyBase +} + +// ListApplicationPolicy list app policies +type ListApplicationPolicy struct { + Policies []*PolicyBase `json:"policies"` +} + // ListPolicyDefinitionResponse list available type ListPolicyDefinitionResponse struct { PolicyDefinitions []PolicyDefinition `json:"policyDefinitions"` @@ -337,31 +551,61 @@ type PolicyDefinition struct { Parameters []types.Parameter `json:"parameters"` } +// CreateWorkflowRequest create workflow request +type CreateWorkflowRequest struct { + AppName string `json:"appName" validate:"checkname"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Steps []WorkflowStep `json:"steps,omitempty"` + Default bool `json:"default"` + EnvName string `json:"envName"` +} + // UpdateWorkflowRequest update or create application workflow type UpdateWorkflowRequest struct { - Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Description string `json:"description" optional:"true"` + Steps []WorkflowStep `json:"steps,omitempty"` + Enable bool `json:"enable"` + Default bool `json:"default"` + EnvName string `json:"envName"` } // WorkflowStep workflow step config type WorkflowStep struct { // Name is the unique name of the workflow step. - Name string `json:"name"` - - Type string `json:"type"` - - Properties string `json:"properties,omitempty"` - - Inputs common.StepInputs `json:"inputs,omitempty"` - - Outputs common.StepOutputs `json:"outputs,omitempty"` + Name string `json:"name" validate:"checkname"` + Alias string `json:"alias" validate:"checkalias" optional:"true"` + Type string `json:"type" validate:"checkname"` + Description string `json:"description" optional:"true"` + DependsOn []string `json:"dependsOn" optional:"true"` + Properties string `json:"properties,omitempty"` + Inputs common.StepInputs `json:"inputs,omitempty" optional:"true"` + Outputs common.StepOutputs `json:"outputs,omitempty" optional:"true"` } // DetailWorkflowResponse detail workflow response type DetailWorkflowResponse struct { - Steps []WorkflowStep `json:"steps,omitempty"` - Enable bool `json:"enable"` - LastRecord *WorkflowRecord `json:"workflowRecord"` + WorkflowBase +} + +// ListWorkflowResponse list application workflows +type ListWorkflowResponse struct { + Workflows []*WorkflowBase `json:"workflows"` +} + +// WorkflowBase workflow base model +type WorkflowBase struct { + Name string `json:"name"` + Alias string `json:"alias"` + Description string `json:"description"` + Enable bool `json:"enable"` + Default bool `json:"default"` + EnvName string `json:"envName"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + Steps []WorkflowStep `json:"steps,omitempty"` } // ListWorkflowRecordsResponse list workflow execution record @@ -370,6 +614,157 @@ type ListWorkflowRecordsResponse struct { Total int64 `json:"total"` } +// DetailWorkflowRecordResponse get workflow record detail +type DetailWorkflowRecordResponse struct { + WorkflowRecord + DeployTime time.Time `json:"deployTime"` + DeployUser string `json:"deployUser"` + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType"` +} + // WorkflowRecord workflow record type WorkflowRecord struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + StartTime time.Time `json:"startTime,omitempty"` + Status string `json:"status"` + Steps []common.WorkflowStepStatus `json:"steps,omitempty"` +} + +// ApplicationDeployRequest the application deploy or update event request +type ApplicationDeployRequest struct { + WorkflowName string `json:"workflowName"` + // User note message, optional + Note string `json:"note"` + // TriggerType the event trigger source, Web or API + TriggerType string `json:"triggerType" validate:"oneof=web api"` + // Force set to True to ignore unfinished events. + Force bool `json:"force"` +} + +// ApplicationDeployResponse application deploy response body +type ApplicationDeployResponse struct { + ApplicationRevisionBase +} + +// VelaQLViewResponse query response +type VelaQLViewResponse map[string]interface{} + +// PutApplicationEnvRequest set diff request +type PutApplicationEnvRequest struct { + ComponentSelector *ComponentSelector `json:"componentSelector,omitempty"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + TargetNames []string `json:"targetNames"` +} + +// ListApplicationEnvBinding list app envBindings +type ListApplicationEnvBinding struct { + EnvBindings []*EnvBindingBase `json:"envBindings"` +} + +// CreateApplicationEnvRequest new application env +type CreateApplicationEnvRequest struct { + EnvBinding +} + +// CreateApplicationTraitRequest create application triat req +type CreateApplicationTraitRequest struct { + Type string `json:"type" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Properties string `json:"properties"` +} + +// UpdateApplicationTraitRequest update application trait req +type UpdateApplicationTraitRequest struct { + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Properties string `json:"properties"` +} + +// ApplicationTrait application trait +type ApplicationTrait struct { + Name string `json:"name"` + Type string `json:"type"` + Alias string `json:"alias,omitempty"` + Description string `json:"description,omitempty"` + // Properties json data + Properties *model.JSONStruct `json:"properties"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` +} + +// CreateDeliveryTargetRequest create delivery target request body +type CreateDeliveryTargetRequest struct { + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"cluster,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` +} + +// UpdateDeliveryTargetRequest only support full quantity update +type UpdateDeliveryTargetRequest struct { + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"cluster,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` +} + +// ClusterTarget kubernetes delivery target +type ClusterTarget struct { + ClusterName string `json:"clusterName" validate:"checkname"` + Namespace string `json:"namespace" optional:"true"` +} + +// DetailDeliveryTargetResponse detail deliveryTarget response +type DetailDeliveryTargetResponse struct { + DeliveryTargetBase +} + +// ListDeliveryTargetResponse list delivery target response body +type ListDeliveryTargetResponse struct { + DeliveryTargets []DeliveryTargetBase `json:"deliveryTargets"` + Total int64 `json:"total"` +} + +// DeliveryTargetBase deliveryTarget base model +type DeliveryTargetBase struct { + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` + Alias string `json:"alias,omitempty" validate:"checkalias" optional:"true"` + Description string `json:"description,omitempty" optional:"true"` + Cluster *ClusterTarget `json:"cluster,omitempty"` + Variable map[string]interface{} `json:"variable,omitempty"` + CreateTime time.Time `json:"createTime"` + UpdateTime time.Time `json:"updateTime"` + AppNum int64 `json:"appNum,omitempty"` +} + +// ApplicationRevisionBase application revision base spec +type ApplicationRevisionBase struct { + CreateTime time.Time `json:"createTime"` + Version string `json:"version"` + Status string `json:"status"` + Reason string `json:"reason"` + DeployUser string `json:"deployUser"` + Note string `json:"note"` + EnvName string `json:"envName"` + // SourceType the event trigger source, Web or API + TriggerType string `json:"triggerType"` +} + +// ListRevisionsResponse list application revisions +type ListRevisionsResponse struct { + Revisions []ApplicationRevisionBase `json:"revisions"` + Total int64 `json:"total"` +} + +// DetailRevisionResponse get application revision detail +type DetailRevisionResponse struct { + model.ApplicationRevision } diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index 8685f2166..2c74cc86d 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -20,15 +20,23 @@ import ( "context" "fmt" "net/http" + "os" + "time" restfulspec "github.com/emicklei/go-restful-openapi/v2" - restful "github.com/emicklei/go-restful/v3" + "github.com/emicklei/go-restful/v3" "github.com/go-openapi/spec" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi" "github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb" "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/apiserver/rest/webservice" ) @@ -43,11 +51,21 @@ type Config struct { // Datastore config Datastore datastore.Config + + // LeaderConfig for leader election + LeaderConfig leaderConfig +} + +type leaderConfig struct { + ID string + LockName string + Duration time.Duration } // APIServer interface for call api server type APIServer interface { Run(context.Context) error + RegisterServices() restfulspec.Config } type restServer struct { @@ -68,11 +86,12 @@ func New(cfg Config) (a APIServer, err error) { case "kubeapi": ds, err = kubeapi.New(context.Background(), cfg.Datastore) if err != nil { - return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + return nil, fmt.Errorf("create kubeapi datastore instance failure %w", err) } default: return nil, fmt.Errorf("not support datastore type %s", cfg.Datastore.Type) } + s := &restServer{ webContainer: restful.NewContainer(), cfg: cfg, @@ -82,16 +101,76 @@ func New(cfg Config) (a APIServer, err error) { } func (s *restServer) Run(ctx context.Context) error { - webservice.Init(ctx, s.dataStore) - err := s.registerServices() + s.RegisterServices() + + l, err := s.setupLeaderElection() if err != nil { return err } + + go func() { + leaderelection.RunOrDie(ctx, *l) + }() + return s.startHTTP(ctx) } -func (s *restServer) registerServices() error { +func (s *restServer) setupLeaderElection() (*leaderelection.LeaderElectionConfig, error) { + restCfg := ctrl.GetConfigOrDie() + rl, err := resourcelock.NewFromKubeconfig(resourcelock.LeasesResourceLock, types.DefaultKubeVelaNS, s.cfg.LeaderConfig.LockName, resourcelock.ResourceLockConfig{ + Identity: s.cfg.LeaderConfig.ID, + }, restCfg, time.Second*10) + if err != nil { + klog.ErrorS(err, "Unable to setup the resource lock") + return nil, err + } + + return &leaderelection.LeaderElectionConfig{ + Lock: rl, + LeaseDuration: time.Second * 15, + RenewDeadline: time.Second * 10, + RetryPeriod: time.Second * 2, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(ctx context.Context) { + s.runLeader(ctx, s.cfg.LeaderConfig.Duration) + }, + OnStoppedLeading: func() { + klog.Infof("leader lost: %s", s.cfg.LeaderConfig.ID) + os.Exit(0) + }, + OnNewLeader: func(identity string) { + if identity == s.cfg.LeaderConfig.ID { + return + } + klog.Infof("new leader elected: %s", identity) + }, + }, + ReleaseOnCancel: true, + }, nil +} + +func (s restServer) runLeader(ctx context.Context, duration time.Duration) { + w := usecase.NewWorkflowUsecase(s.dataStore) + + t := time.NewTicker(duration) + defer t.Stop() + + for { + select { + case <-t.C: + if err := w.SyncWorkflowRecord(ctx); err != nil { + klog.ErrorS(err, "syncWorkflowRecordError") + } + case <-ctx.Done(): + return + } + } +} + +// RegisterServices register web service +func (s *restServer) RegisterServices() restfulspec.Config { + webservice.Init(s.dataStore) /* ************************************************************** */ /* ************* Open API Route Group ***************** */ /* ************************************************************** */ @@ -118,7 +197,7 @@ func (s *restServer) registerServices() error { APIPath: "/apidocs.json", PostBuildSwaggerObjectHandler: enrichSwaggerObject} s.webContainer.Add(restfulspec.NewOpenAPIService(config)) - return nil + return config } func enrichSwaggerObject(swo *spec.Swagger) { diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go new file mode 100644 index 000000000..5a1cb1e9d --- /dev/null +++ b/pkg/apiserver/rest/usecase/addon.go @@ -0,0 +1,445 @@ +package usecase + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "time" + + v1 "k8s.io/api/core/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + + common2 "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" + pkgaddon "github.com/oam-dev/kubevela/pkg/addon" + "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" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + restutils "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +// AddonUsecase addon usecase +type AddonUsecase interface { + GetAddonRegistry(ctx context.Context, name string) (*model.AddonRegistry, error) + CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) + DeleteAddonRegistry(ctx context.Context, name string) error + UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) + ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) + ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) + StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) + GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) + EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error + DisableAddon(ctx context.Context, name string) error +} + +// AddonImpl2AddonRes convert types.Addon to the type apiserver need +func AddonImpl2AddonRes(impl *types.Addon) (*apis.DetailAddonResponse, error) { + var defs []*apis.AddonDefinition + for _, def := range impl.Definitions { + obj := &unstructured.Unstructured{} + dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + _, _, err := dec.Decode([]byte(def.Data), nil, obj) + if err != nil { + return nil, fmt.Errorf("convert %s file content to definition fail", def.Name) + } + defs = append(defs, &apis.AddonDefinition{ + Name: obj.GetName(), + DefType: obj.GetKind(), + Description: obj.GetAnnotations()["definition.oam.dev/description"], + }) + } + return &apis.DetailAddonResponse{ + AddonMeta: impl.AddonMeta, + APISchema: impl.APISchema, + UISchema: impl.UISchema, + Detail: impl.Detail, + Definitions: defs, + }, nil +} + +// NewAddonUsecase returns a addon usecase +func NewAddonUsecase(ds datastore.DataStore) AddonUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + panic(err) + } + return &addonUsecaseImpl{ + addonRegistryCache: make(map[string]*restutils.MemoryCache), + addonRegistryDS: ds, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), + } +} + +type addonUsecaseImpl struct { + addonRegistryCache map[string]*restutils.MemoryCache + addonRegistryDS datastore.DataStore + kubeClient client.Client + apply apply.Applicator +} + +// GetAddon will get addon information +func (u *addonUsecaseImpl) GetAddon(ctx context.Context, name string, registry string) (*apis.DetailAddonResponse, error) { + var addon *types.Addon + var err error + var exist bool + + if registry == "" { + registries, err := u.ListAddonRegistries(ctx) + if err != nil { + return nil, err + } + for _, r := range registries { + if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { + addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.GetLevelOptions) + } + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { + return nil, err + } + if addon != nil { + break + } + } + } else if addon, exist = u.tryGetAddonFromCache(registry, name); !exist { + addonRegistry, err := u.GetAddonRegistry(ctx, registry) + if err != nil { + return nil, err + } + addon, err = pkgaddon.GetAddon(name, addonRegistry.Git, pkgaddon.GetLevelOptions) + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { + return nil, err + } + } + + if addon == nil { + return nil, bcode.ErrAddonNotExist + } + a, err := AddonImpl2AddonRes(addon) + if err != nil { + return nil, err + } + return a, nil +} + +func (u *addonUsecaseImpl) StatusAddon(ctx context.Context, name string) (*apis.AddonStatusResponse, error) { + var app v1beta1.Application + err := u.kubeClient.Get(context.Background(), client.ObjectKey{ + Namespace: types.DefaultKubeVelaNS, + Name: pkgaddon.Convert2AppName(name), + }, &app) + if err != nil { + if errors2.IsNotFound(err) { + return &apis.AddonStatusResponse{ + Phase: apis.AddonPhaseDisabled, + EnablingProgress: nil, + }, nil + } + return nil, bcode.ErrGetAddonApplication + } + + switch app.Status.Phase { + case common2.ApplicationRunning: + res := apis.AddonStatusResponse{ + Phase: apis.AddonPhaseEnabled, + EnablingProgress: nil, + } + var sec v1.Secret + err := u.kubeClient.Get(ctx, client.ObjectKey{ + Namespace: types.DefaultKubeVelaNS, + Name: pkgaddon.Convert2SecName(name), + }, &sec) + if err != nil { + return nil, bcode.ErrAddonSecretGet + } + res.Args = make(map[string]string, len(sec.Data)) + for k, v := range sec.Data { + res.Args[k] = string(v) + } + return &res, nil + default: + return &apis.AddonStatusResponse{ + Phase: apis.AddonPhaseEnabling, + EnablingProgress: nil, + }, nil + } +} + +func (u *addonUsecaseImpl) ListAddons(ctx context.Context, registry, query string) ([]*apis.DetailAddonResponse, error) { + var addons []*types.Addon + var listAddons []*types.Addon + rs, err := u.ListAddonRegistries(ctx) + if err != nil { + return nil, err + } + + for _, r := range rs { + if registry != "" && r.Name != registry { + continue + } + if u.isRegistryCacheUpToDate(r.Name) { + listAddons = u.getRegistryCache(r.Name) + } else { + listAddons, err = pkgaddon.ListAddons(r.Git, pkgaddon.GetLevelOptions) + if err != nil { + log.Logger.Errorf("fail to get addons from registry %s, %v", r.Name, err) + continue + } + // if list addons, details will be retrieved later + go func() { + addonDetails, err := pkgaddon.ListAddons(r.Git, pkgaddon.EnableLevelOptions) + if err != nil { + return + } + u.putRegistryCache(r.Name, addonDetails) + }() + } + addons = mergeAddons(addons, listAddons) + } + + if query != "" { + var filtered []*types.Addon + for i, addon := range addons { + if strings.Contains(addon.Name, query) || strings.Contains(addon.Description, query) { + filtered = append(filtered, addons[i]) + } + } + addons = filtered + } + sort.Slice(addons, func(i, j int) bool { + return addons[i].Name < addons[j].Name + }) + + for _, addon := range addons { + // render default ui schema + addon.UISchema = renderDefaultUISchema(addon.APISchema) + } + + var addonReses []*apis.DetailAddonResponse + for _, a := range addons { + addonRes, err := AddonImpl2AddonRes(a) + if err != nil { + log.Logger.Errorf("err while converting AddonImpl to DetailAddonResponse: %v", err) + continue + } + addonReses = append(addonReses, addonRes) + } + return addonReses, nil +} + +func (u *addonUsecaseImpl) DeleteAddonRegistry(ctx context.Context, name string) error { + return u.addonRegistryDS.Delete(ctx, &model.AddonRegistry{Name: name}) +} + +func (u *addonUsecaseImpl) CreateAddonRegistry(ctx context.Context, req apis.CreateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { + r := addonRegistryModelFromCreateAddonRegistryRequest(req) + + err := u.addonRegistryDS.Add(ctx, r) + if err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrAddonRegistryExist + } + return nil, err + } + + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + }, nil +} + +func (u *addonUsecaseImpl) GetAddonRegistry(ctx context.Context, name string) (*model.AddonRegistry, error) { + var r = model.AddonRegistry{ + Name: name, + } + err := u.addonRegistryDS.Get(ctx, &r) + if err != nil { + return nil, err + } + return &r, nil +} + +func (u addonUsecaseImpl) UpdateAddonRegistry(ctx context.Context, name string, req apis.UpdateAddonRegistryRequest) (*apis.AddonRegistryMeta, error) { + var r = model.AddonRegistry{ + Name: name, + } + err := u.addonRegistryDS.Get(ctx, &r) + if err != nil { + return nil, bcode.ErrAddonRegistryNotExist + } + r.Git = req.Git + err = u.addonRegistryDS.Put(ctx, &r) + if err != nil { + return nil, err + } + + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + }, nil +} + +func (u *addonUsecaseImpl) ListAddonRegistries(ctx context.Context) ([]*apis.AddonRegistryMeta, error) { + var r = model.AddonRegistry{} + + var list []*apis.AddonRegistryMeta + entities, err := u.addonRegistryDS.List(ctx, &r, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + for _, entity := range entities { + list = append(list, ConvertAddonRegistryModel2AddonRegistryMeta(entity.(*model.AddonRegistry))) + } + sort.Slice(list, func(i, j int) bool { + return list[i].Name < list[j].Name + }) + return list, nil +} + +func (u *addonUsecaseImpl) tryGetAddonFromCache(registry, addonName string) (*types.Addon, bool) { + if u.isRegistryCacheUpToDate(registry) { + addons := u.getRegistryCache(registry) + for _, a := range addons { + if a.Name == addonName { + return a, true + } + } + } + return nil, false +} + +func (u *addonUsecaseImpl) EnableAddon(ctx context.Context, name string, args apis.EnableAddonRequest) error { + var addon *types.Addon + var err error + registries, err := u.ListAddonRegistries(ctx) + if err != nil { + return err + } + for _, r := range registries { + var exist bool + if addon, exist = u.tryGetAddonFromCache(r.Name, name); !exist { + addon, err = pkgaddon.GetAddon(name, r.Git, pkgaddon.EnableLevelOptions) + } + if err != nil && !errors.Is(err, pkgaddon.ErrNotExist) { + return bcode.WrapGithubRateLimitErr(err) + } + if addon == nil { + continue + } + + app, defs, err := pkgaddon.RenderApplication(addon, args.Args) + if err != nil { + return bcode.ErrAddonRender + } + + err = u.kubeClient.Get(ctx, client.ObjectKey{Namespace: app.GetNamespace(), Name: app.GetName()}, app) + if err == nil { + return bcode.ErrAddonIsEnabled + } + + err = u.apply.Apply(ctx, app) + if err != nil { + log.Logger.Errorf("create application fail: %s", err.Error()) + return bcode.ErrAddonApply + } + + for _, def := range defs { + addOwner(def, app) + err = u.apply.Apply(ctx, def) + if err != nil { + log.Logger.Errorf("apply definition fail: %v", err) + return bcode.ErrAddonApply + } + } + + sec := pkgaddon.RenderArgsSecret(addon, args.Args) + err = u.apply.Apply(ctx, sec) + if err != nil { + return bcode.ErrAddonSecretApply + } + + return nil + } + return bcode.ErrAddonNotExist +} + +func addOwner(child *unstructured.Unstructured, app *v1beta1.Application) { + child.SetOwnerReferences(append(child.GetOwnerReferences(), + *metav1.NewControllerRef(app, v1beta1.ApplicationKindVersionKind))) +} + +func (u *addonUsecaseImpl) getRegistryCache(name string) []*types.Addon { + return u.addonRegistryCache[name].GetData().([]*types.Addon) +} + +func (u *addonUsecaseImpl) putRegistryCache(name string, addons []*types.Addon) { + u.addonRegistryCache[name] = restutils.NewMemoryCache(addons, time.Minute*10) +} + +func (u *addonUsecaseImpl) isRegistryCacheUpToDate(name string) bool { + d, ok := u.addonRegistryCache[name] + if !ok { + return false + } + return !d.IsExpired() +} + +func (u *addonUsecaseImpl) DisableAddon(ctx context.Context, name string) error { + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"}, + ObjectMeta: metav1.ObjectMeta{ + Name: pkgaddon.Convert2AppName(name), + Namespace: types.DefaultKubeVelaNS, + }, + } + err := u.kubeClient.Delete(ctx, app) + if err != nil { + log.Logger.Errorf("delete application fail: %s", err.Error()) + return err + } + return nil +} + +func addonRegistryModelFromCreateAddonRegistryRequest(req apis.CreateAddonRegistryRequest) *model.AddonRegistry { + return &model.AddonRegistry{ + Name: req.Name, + Git: req.Git, + } +} + +func mergeAddons(a1, a2 []*types.Addon) []*types.Addon { + for _, item := range a2 { + if hasAddon(a1, item.Name) { + continue + } + a1 = append(a1, item) + } + return a1 +} + +func hasAddon(addons []*types.Addon, name string) bool { + for _, addon := range addons { + if addon.Name == name { + return true + } + } + return false +} + +// ConvertAddonRegistryModel2AddonRegistryMeta will convert from model to AddonRegistryMeta +func ConvertAddonRegistryModel2AddonRegistryMeta(r *model.AddonRegistry) *apis.AddonRegistryMeta { + return &apis.AddonRegistryMeta{ + Name: r.Name, + Git: r.Git, + } +} diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index 783223c68..6b795d865 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -18,58 +18,278 @@ package usecase import ( "context" - "encoding/json" "errors" + "fmt" + "sort" + "strings" + "time" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/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/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +// PolicyType build-in policy type +type PolicyType string + +const ( + // EnvBindingPolicy Multiple environment distribution policy + EnvBindingPolicy PolicyType = "env-binding" + + // EnvBindingPolicyDefaultName default policy name + EnvBindingPolicyDefaultName string = "env-bindings" ) // ApplicationUsecase application usecase type ApplicationUsecase interface { + ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) + GetApplication(ctx context.Context, appName string) (*model.Application, error) + GetApplicationStatus(ctx context.Context, app *model.Application, envName string) (*common.AppStatus, error) + DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) + PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) + UpdateApplication(context.Context, *model.Application, apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) + DeleteApplication(ctx context.Context, app *model.Application) error + Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) + GetApplicationComponent(ctx context.Context, app *model.Application, componentName string) (*model.ApplicationComponent, error) + ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) + AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) + DetailComponent(ctx context.Context, app *model.Application, componentName string) (*apisv1.DetailComponentResponse, error) + DeleteComponent(ctx context.Context, app *model.Application, componentName string) error + UpdateComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.UpdateApplicationComponentRequest) (*apisv1.ComponentBase, error) + ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) + AddPolicy(ctx context.Context, app *model.Application, policy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) + DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) + DeletePolicy(ctx context.Context, app *model.Application, policyName string) error + UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policy apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) + CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) + DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error + UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) + ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) + DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error) + Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error) + ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error) } type applicationUsecaseImpl struct { - ds datastore.DataStore + ds datastore.DataStore + kubeClient client.Client + apply apply.Applicator + workflowUsecase WorkflowUsecase + envBindingUsecase EnvBindingUsecase + deliveryTargetUsecase DeliveryTargetUsecase } -// NewApplicationUsecase new cluster usecase -func NewApplicationUsecase(ds datastore.DataStore) ApplicationUsecase { - return &applicationUsecaseImpl{ds: ds} +// NewApplicationUsecase new application usecase +func NewApplicationUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase, envBindingUsecase EnvBindingUsecase, deliveryTargetUsecase DeliveryTargetUsecase) ApplicationUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &applicationUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + envBindingUsecase: envBindingUsecase, + deliveryTargetUsecase: deliveryTargetUsecase, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), + } +} + +// ListApplications list applications +func (c *applicationUsecaseImpl) ListApplications(ctx context.Context, listOptions apisv1.ListApplicatioOptions) ([]*apisv1.ApplicationBase, error) { + var app = model.Application{} + if listOptions.Namespace != "" { + app.Namespace = listOptions.Namespace + } + entitys, err := c.ds.List(ctx, &app, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.ApplicationBase + for _, entity := range entitys { + appBase := c.converAppModelToBase(entity.(*model.Application)) + if listOptions.Query != "" && + !(strings.Contains(appBase.Alias, listOptions.Query) || + strings.Contains(appBase.Name, listOptions.Query) || + strings.Contains(appBase.Description, listOptions.Query)) { + continue + } + if listOptions.TargetName != "" { + targetIsContain, _ := c.envBindingUsecase.CheckAppEnvBindingsContainTarget(ctx, &app, listOptions.TargetName) + if targetIsContain { + continue + } + } + list = append(list, appBase) + } + sort.Slice(list, func(i, j int) bool { + return list[i].UpdateTime.Unix() > list[j].UpdateTime.Unix() + }) + return list, nil +} + +// GetApplication get application model +func (c *applicationUsecaseImpl) GetApplication(ctx context.Context, appName string) (*model.Application, error) { + var app = model.Application{ + Name: appName, + } + if err := c.ds.Get(ctx, &app); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationNotExist + } + return nil, err + } + return &app, nil +} + +// DetailApplication detail application info +func (c *applicationUsecaseImpl) DetailApplication(ctx context.Context, app *model.Application) (*apisv1.DetailApplicationResponse, error) { + base := c.converAppModelToBase(app) + policys, err := c.queryApplicationPolicys(ctx, app) + if err != nil { + return nil, err + } + componentNum, err := c.ds.Count(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) + if err != nil { + return nil, err + } + envBindings, err := c.envBindingUsecase.GetEnvBindings(ctx, app) + if err != nil { + return nil, err + } + var policyNames []string + var envBindingNames []string + for _, p := range policys { + policyNames = append(policyNames, p.Name) + } + for _, e := range envBindings { + envBindingNames = append(envBindingNames, e.Name) + } + var detail = &apisv1.DetailApplicationResponse{ + ApplicationBase: *base, + Policies: policyNames, + EnvBindings: envBindingNames, + ResourceInfo: apisv1.ApplicationResourceInfo{ + ComponentNum: componentNum, + }, + } + return detail, nil +} + +// GetApplicationStatus get application status from controller cluster +func (c *applicationUsecaseImpl) GetApplicationStatus(ctx context.Context, appmodel *model.Application, envName string) (*common.AppStatus, error) { + var app v1beta1.Application + err := c.kubeClient.Get(ctx, types.NamespacedName{Namespace: appmodel.Namespace, Name: convertAppName(appmodel.Name, envName)}, &app) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return &app.Status, nil +} + +// GetApplicationCR get application cr in cluster +func (c *applicationUsecaseImpl) GetApplicationCR(ctx context.Context, appModel *model.Application) (*v1beta1.ApplicationList, error) { + var apps v1beta1.ApplicationList + selector := labels.NewSelector() + re, err := labels.NewRequirement(oam.AnnotationAppName, selection.Equals, []string{appModel.Name}) + if err != nil { + return nil, err + } + selector = selector.Add(*re) + err = c.kubeClient.List(ctx, &apps, &client.ListOptions{ + LabelSelector: selector, + Namespace: appModel.Namespace, + }) + if err != nil { + if apierrors.IsNotFound(err) { + return &apps, nil + } + return nil, err + } + return &apps, nil +} + +// PublishApplicationTemplate publish app template +func (c *applicationUsecaseImpl) PublishApplicationTemplate(ctx context.Context, app *model.Application) (*apisv1.ApplicationTemplateBase, error) { + //TODO: + return nil, nil } // CreateApplication create application func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) { application := model.Application{ Name: req.Name, + Alias: req.Alias, Description: req.Description, + Namespace: req.Namespace, Icon: req.Icon, Labels: req.Labels, - ClusterList: req.ClusterList, } - // check clusters. - - // check can deploy - var canDeploy bool + // check app name. + exit, err := c.ds.IsExist(ctx, &application) + if err != nil { + log.Logger.Errorf("check application name is exist failure %s", err.Error()) + return nil, bcode.ErrApplicationExist + } + if exit { + return nil, bcode.ErrApplicationExist + } if req.YamlConfig != "" { var oamApp v1beta1.Application - if err := json.Unmarshal([]byte(req.YamlConfig), &oamApp); err != nil { + if err := yaml.Unmarshal([]byte(req.YamlConfig), &oamApp); err != nil { log.Logger.Errorf("application yaml config is invalid,%s", err.Error()) return nil, bcode.ErrApplicationConfig } - // TODO: check oam spec - // TODO: split the configuration and store it in the database. - - canDeploy = true + // split the configuration and store it in the database. + if err := c.saveApplicationComponent(ctx, &application, oamApp.Spec.Components); err != nil { + log.Logger.Errorf("save applictaion component failure,%s", err.Error()) + return nil, err + } + if len(oamApp.Spec.Policies) > 0 { + if err := c.saveApplicationPolicy(ctx, &application, oamApp.Spec.Policies); err != nil { + log.Logger.Errorf("save applictaion polocies failure,%s", err.Error()) + return nil, err + } + } } - // add to db. + // build-in create env binding + if len(req.EnvBinding) > 0 { + err := c.saveApplicationEnvBinding(ctx, application, req.EnvBinding) + if err != nil { + return nil, err + } + } + + if req.Component != nil { + _, err = c.AddComponent(ctx, &application, *req.Component) + if err != nil { + return nil, err + } + } + // add application to db. if err := c.ds.Add(ctx, &application); err != nil { if errors.Is(err, datastore.ErrRecordExist) { return nil, bcode.ErrApplicationExist @@ -77,31 +297,928 @@ func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apis return nil, err } // render app base info. - base := c.renderAppBase(&application) - // deploy to cluster if need. - if req.Deploy && canDeploy { - if err := c.Deploy(ctx, req.Name); err != nil { + base := c.converAppModelToBase(&application) + return base, nil +} + +func (c *applicationUsecaseImpl) genPolicyByEnv(ctx context.Context, app *model.Application, envName string) (v1beta1.AppPolicy, error) { + appPolicy := v1beta1.AppPolicy{} + envBinding, err := c.envBindingUsecase.GetEnvBinding(ctx, app, envName) + if err != nil { + return appPolicy, err + } + appPolicy.Name = genPolicyName(envBinding.Name) + appPolicy.Type = string(EnvBindingPolicy) + + var envBindingSpec v1alpha1.EnvBindingSpec + for _, targetName := range envBinding.TargetNames { + target, err := c.deliveryTargetUsecase.GetDeliveryTarget(ctx, targetName) + if err != nil || target == nil { + return appPolicy, bcode.ErrFoundEnvbindingDeliveryTarget + } + envBindingSpec.Envs = append(envBindingSpec.Envs, createTargetClusterEnv(envBinding, target)) + } + properties, err := model.NewJSONStructByStruct(envBindingSpec) + if err != nil { + return appPolicy, bcode.ErrInvalidProperties + } + appPolicy.Properties = properties.RawExtension() + return appPolicy, nil +} + +func (c *applicationUsecaseImpl) saveApplicationEnvBinding(ctx context.Context, app model.Application, envBindings []*apisv1.EnvBinding) error { + err := c.envBindingUsecase.BatchCreateEnvBinding(ctx, &app, envBindings) + if err != nil { + return err + } + return nil +} + +func (c *applicationUsecaseImpl) UpdateApplication(ctx context.Context, app *model.Application, req apisv1.UpdateApplicationRequest) (*apisv1.ApplicationBase, error) { + app.Alias = req.Alias + app.Description = req.Description + app.Labels = req.Labels + app.Icon = req.Icon + if err := c.ds.Put(ctx, app); err != nil { + return nil, err + } + return c.converAppModelToBase(app), nil +} + +func (c *applicationUsecaseImpl) saveApplicationComponent(ctx context.Context, app *model.Application, components []common.ApplicationComponent) error { + var componentModels []datastore.Entity + for _, component := range components { + // TODO: Check whether the component type is supported. + var traits []model.ApplicationTrait + for _, trait := range component.Traits { + properties, err := model.NewJSONStruct(trait.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return bcode.ErrInvalidProperties + } + traits = append(traits, model.ApplicationTrait{ + Type: trait.Type, + Properties: properties, + }) + } + properties, err := model.NewJSONStruct(component.Properties) + if err != nil { + log.Logger.Errorf("parse component properties failire %w", err) + return bcode.ErrInvalidProperties + } + componentModel := model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + Type: component.Type, + ExternalRevision: component.ExternalRevision, + DependsOn: component.DependsOn, + Inputs: component.Inputs, + Outputs: component.Outputs, + Scopes: component.Scopes, + Traits: traits, + Properties: properties, + } + componentModels = append(componentModels, &componentModel) + } + log.Logger.Infof("batch add %d components for app %s", len(componentModels), app.PrimaryKey()) + return c.ds.BatchAdd(ctx, componentModels) +} + +// ListRecords list application record +func (c *applicationUsecaseImpl) ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error) { + var record = model.WorkflowRecord{ + AppPrimaryKey: appName, + Status: model.RevisionStatusRunning, + } + records, err := c.ds.List(ctx, &record, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if len(records) == 0 { + record.Status = model.RevisionStatusComplete + records, err = c.ds.List(ctx, &record, &datastore.ListOptions{ + Page: 1, + PageSize: 1, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) + if err != nil { return nil, err } } - return base, nil + + resp := &apisv1.ListWorkflowRecordsResponse{ + Records: []apisv1.WorkflowRecord{}, + } + for _, raw := range records { + record, ok := raw.(*model.WorkflowRecord) + if ok { + resp.Records = append(resp.Records, *convertFromRecordModel(record)) + } + } + resp.Total = int64(len(records)) + + return resp, nil +} + +func (c *applicationUsecaseImpl) ListComponents(ctx context.Context, app *model.Application, op apisv1.ListApplicationComponentOptions) ([]*apisv1.ComponentBase, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + } + components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + envComponents := map[string]bool{} + componentSelectorDefine := false + if op.EnvName != "" { + envbindings, err := c.envBindingUsecase.GetEnvBindings(ctx, app) + if err != nil && !errors.Is(err, bcode.ErrApplicationNotEnv) { + log.Logger.Errorf("query app env binding policy config failure %s", err.Error()) + } + if len(envbindings) > 0 { + for _, env := range envbindings { + if env != nil && env.Name == op.EnvName { + componentSelectorDefine = true + for _, componentName := range env.ComponentSelector.Components { + envComponents[componentName] = true + } + } + } + } + } + + var list []*apisv1.ComponentBase + for _, component := range components { + pm := component.(*model.ApplicationComponent) + if !componentSelectorDefine || envComponents[pm.Name] { + list = append(list, c.converComponentModelToBase(pm)) + } + } + return list, nil +} + +// DetailComponent detail app component +// TODO: Add status data about the component. +func (c *applicationUsecaseImpl) DetailComponent(ctx context.Context, app *model.Application, compName string) (*apisv1.DetailComponentResponse, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: compName, + } + err := c.ds.Get(ctx, &component) + if err != nil { + return nil, err + } + return &apisv1.DetailComponentResponse{ + ApplicationComponent: component, + }, nil +} + +func (c *applicationUsecaseImpl) converComponentModelToBase(m *model.ApplicationComponent) *apisv1.ComponentBase { + return &apisv1.ComponentBase{ + Name: m.Name, + Alias: m.Alias, + Description: m.Description, + Labels: m.Labels, + ComponentType: m.Type, + Icon: m.Icon, + DependsOn: m.DependsOn, + Creator: m.Creator, + CreateTime: m.CreateTime, + UpdateTime: m.UpdateTime, + } +} + +// ListPolicies list application policies +func (c *applicationUsecaseImpl) ListPolicies(ctx context.Context, app *model.Application) ([]*apisv1.PolicyBase, error) { + policies, err := c.queryApplicationPolicys(ctx, app) + if err != nil { + return nil, err + } + var list []*apisv1.PolicyBase + for _, policy := range policies { + list = append(list, c.converPolicyModelToBase(policy)) + } + return list, nil +} + +func (c *applicationUsecaseImpl) converPolicyModelToBase(policy *model.ApplicationPolicy) *apisv1.PolicyBase { + pb := &apisv1.PolicyBase{ + Name: policy.Name, + Type: policy.Type, + Properties: policy.Properties, + Description: policy.Description, + Creator: policy.Creator, + CreateTime: policy.CreateTime, + UpdateTime: policy.UpdateTime, + } + return pb +} + +func (c *applicationUsecaseImpl) saveApplicationPolicy(ctx context.Context, app *model.Application, policys []v1beta1.AppPolicy) error { + var policyModels []datastore.Entity + for _, policy := range policys { + properties, err := model.NewJSONStruct(policy.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return bcode.ErrInvalidProperties + } + appPolicy := &model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policy.Name, + Type: policy.Type, + Properties: properties, + } + if policy.Type != string(EnvBindingPolicy) { + policyModels = append(policyModels, appPolicy) + } + } + return c.ds.BatchAdd(ctx, policyModels) +} + +func (c *applicationUsecaseImpl) queryApplicationPolicys(ctx context.Context, app *model.Application) (list []*model.ApplicationPolicy, err error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + } + policys, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + for _, policy := range policys { + pm := policy.(*model.ApplicationPolicy) + list = append(list, pm) + } + return +} + +// DetailPolicy detail app policy +// TODO: Add status data about the policy. +func (c *applicationUsecaseImpl) DetailPolicy(ctx context.Context, app *model.Application, policyName string) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + err := c.ds.Get(ctx, &policy) + if err != nil { + return nil, err + } + return &apisv1.DetailPolicyResponse{ + PolicyBase: *c.converPolicyModelToBase(&policy), + }, nil } // Deploy deploy app to cluster // means to render oam application config and apply to cluster. // An event record is generated for each deploy. -func (c *applicationUsecaseImpl) Deploy(ctx context.Context, appName string) error { - // TODO: - return nil +func (c *applicationUsecaseImpl) Deploy(ctx context.Context, app *model.Application, req apisv1.ApplicationDeployRequest) (*apisv1.ApplicationDeployResponse, error) { + // TODO: rollback to handle all the error case + // step1: Render oam application + version := utils.GenerateVersion("") + oamApp, err := c.renderOAMApplication(ctx, app, req.WorkflowName, version) + if err != nil { + return nil, err + } + configByte, _ := yaml.Marshal(oamApp) + + workflow, err := c.workflowUsecase.GetWorkflow(ctx, app, oamApp.Annotations[oam.AnnotationWorkflowName]) + if err != nil { + return nil, err + } + + // step2: check and create deploy event + if !req.Force { + var lastVersion = model.ApplicationRevision{ + AppPrimaryKey: app.PrimaryKey(), + EnvName: workflow.EnvName, + } + list, err := c.ds.List(ctx, &lastVersion, &datastore.ListOptions{ + PageSize: 1, Page: 1, SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}}}) + if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("query app latest revision failure %s", err.Error()) + return nil, bcode.ErrDeployConflict + } + if len(list) > 0 && list[0].(*model.ApplicationRevision).Status != model.RevisionStatusComplete { + log.Logger.Warnf("last app revision can not complete %s/%s", list[0].(*model.ApplicationRevision).AppPrimaryKey, list[0].(*model.ApplicationRevision).Version) + return nil, bcode.ErrDeployConflict + } + } + + var appRevision = &model.ApplicationRevision{ + AppPrimaryKey: app.PrimaryKey(), + Version: version, + ApplyAppConfig: string(configByte), + Status: model.RevisionStatusInit, + // TODO: Get user information from ctx and assign a value. + DeployUser: "", + Note: req.Note, + TriggerType: req.TriggerType, + WorkflowName: oamApp.Annotations[oam.AnnotationWorkflowName], + EnvName: workflow.EnvName, + } + + if err := c.ds.Add(ctx, appRevision); err != nil { + return nil, err + } + // step3: create workflow record + if err := c.workflowUsecase.CreateWorkflowRecord(ctx, app, oamApp, workflow); err != nil { + return nil, err + } + // step4: check and create namespace + var namespace corev1.Namespace + if err := c.kubeClient.Get(ctx, types.NamespacedName{Name: oamApp.Namespace}, &namespace); apierrors.IsNotFound(err) { + namespace.Name = oamApp.Namespace + if err := c.kubeClient.Create(ctx, &namespace); err != nil { + log.Logger.Errorf("auto create namespace failure %s", err.Error()) + return nil, bcode.ErrCreateNamespace + } + } + // step5: apply to controller cluster + err = c.apply.Apply(ctx, oamApp) + if err != nil { + appRevision.Status = model.RevisionStatusFail + appRevision.Reason = err.Error() + if err := c.ds.Put(ctx, appRevision); err != nil { + log.Logger.Warnf("update deploy event failure %s", err.Error()) + } + log.Logger.Errorf("deploy app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, bcode.ErrDeployApplyFail + } + appRevision.Status = model.RevisionStatusRunning + if err := c.ds.Put(ctx, appRevision); err != nil { + log.Logger.Warnf("update deploy event failure %s", err.Error()) + } + + // step6: update deploy event status + return &apisv1.ApplicationDeployResponse{ + ApplicationRevisionBase: apisv1.ApplicationRevisionBase{ + Version: appRevision.Version, + Status: appRevision.Status, + Reason: appRevision.Reason, + DeployUser: appRevision.DeployUser, + Note: appRevision.Note, + TriggerType: appRevision.TriggerType, + }, + }, nil } -func (c *applicationUsecaseImpl) renderAppBase(app *model.Application) *apisv1.ApplicationBase { - appBeas := &apisv1.ApplicationBase{ +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 . + var workflow *model.Workflow + var err error + if reqWorkflowName != "" { + workflow, err = c.workflowUsecase.GetWorkflow(ctx, appModel, reqWorkflowName) + if err != nil { + return nil, err + } + } else { + workflow, err = c.workflowUsecase.GetApplicationDefaultWorkflow(ctx, appModel) + if err != nil && !errors.Is(err, bcode.ErrWorkflowNoDefault) { + return nil, err + } + } + if workflow == nil || workflow.EnvName == "" { + return nil, bcode.ErrWorkflowNotExist + } + + labels := make(map[string]string) + for key, value := range appModel.Labels { + labels[key] = value + } + labels[oam.AnnotationAppName] = appModel.Name + + var app = &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: convertAppName(appModel.Name, workflow.EnvName), + Namespace: appModel.Namespace, + Labels: labels, + Annotations: map[string]string{ + oam.AnnotationDeployVersion: version, + // publish version is the identifier of workflow record + oam.AnnotationPublishVersion: utils.GenerateVersion(reqWorkflowName), + oam.AnnotationAppName: appModel.Name, + oam.AnnotationAppAlias: appModel.Alias, + }, + }, + } + var component = model.ApplicationComponent{ + AppPrimaryKey: appModel.PrimaryKey(), + } + components, err := c.ds.List(ctx, &component, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if err != nil || len(components) == 0 { + return nil, bcode.ErrNoComponent + } + + var policy = model.ApplicationPolicy{ + AppPrimaryKey: appModel.PrimaryKey(), + } + policies, err := c.ds.List(ctx, &policy, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + + for _, entity := range components { + component := entity.(*model.ApplicationComponent) + var traits []common.ApplicationTrait + for _, trait := range component.Traits { + aTrait := common.ApplicationTrait{ + Type: trait.Type, + } + if trait.Properties != nil { + aTrait.Properties = trait.Properties.RawExtension() + } + traits = append(traits, aTrait) + } + bc := common.ApplicationComponent{ + Name: converComponentName(component.Name, workflow.EnvName), + Type: component.Type, + ExternalRevision: component.ExternalRevision, + DependsOn: component.DependsOn, + Inputs: component.Inputs, + Outputs: component.Outputs, + Traits: traits, + Scopes: component.Scopes, + Properties: component.Properties.RawExtension(), + } + if component.Properties != nil { + bc.Properties = component.Properties.RawExtension() + } + app.Spec.Components = append(app.Spec.Components, bc) + } + + for _, entity := range policies { + policy := entity.(*model.ApplicationPolicy) + apolicy := v1beta1.AppPolicy{ + Name: policy.Name, + Type: policy.Type, + } + if policy.Properties != nil { + apolicy.Properties = policy.Properties.RawExtension() + } + app.Spec.Policies = append(app.Spec.Policies, apolicy) + } + if workflow.EnvName != "" { + envPolicy, err := c.genPolicyByEnv(ctx, appModel, workflow.EnvName) + if err != nil { + return nil, err + } + app.Spec.Policies = append(app.Spec.Policies, envPolicy) + } + app.Annotations[oam.AnnotationWorkflowName] = workflow.Name + var steps []v1beta1.WorkflowStep + for _, step := range workflow.Steps { + var wstep = v1beta1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, + } + if step.Properties != nil { + wstep.Properties = step.Properties.RawExtension() + } + steps = append(steps, wstep) + } + app.Spec.Workflow = &v1beta1.Workflow{ + Steps: steps, + } + + return app, nil +} + +func (c *applicationUsecaseImpl) converAppModelToBase(app *model.Application) *apisv1.ApplicationBase { + appBase := &apisv1.ApplicationBase{ Name: app.Name, + Alias: app.Alias, + Namespace: app.Namespace, + CreateTime: app.CreateTime, + UpdateTime: app.UpdateTime, Description: app.Description, Icon: app.Icon, Labels: app.Labels, } - // TODO: get and render app status - return appBeas + return appBase +} + +// DeleteApplication delete application +func (c *applicationUsecaseImpl) DeleteApplication(ctx context.Context, app *model.Application) error { + // TODO: check app can be deleted + crs, err := c.GetApplicationCR(ctx, app) + if err != nil { + return err + } + if len(crs.Items) > 0 { + return bcode.ErrApplicationRefusedDelete + } + // query all components to deleted + components, err := c.ListComponents(ctx, app, apisv1.ListApplicationComponentOptions{}) + if err != nil { + return err + } + // query all policies to deleted + policies, err := c.ListPolicies(ctx, app) + if err != nil { + return err + } + + // delete workflow + if err := c.workflowUsecase.DeleteWorkflowByApp(ctx, app); err != nil && !errors.Is(err, bcode.ErrWorkflowNotExist) { + log.Logger.Errorf("delete workflow %s failure %s", app.Name, err.Error()) + } + + for _, component := range components { + err := c.ds.Delete(ctx, &model.ApplicationComponent{AppPrimaryKey: app.PrimaryKey(), Name: component.Name}) + if err != nil && !errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("delete component %s in app %s failure %s", component.Name, app.Name, err.Error()) + } + } + + for _, policy := range policies { + err := c.ds.Delete(ctx, &model.ApplicationPolicy{AppPrimaryKey: app.PrimaryKey(), Name: policy.Name}) + if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + log.Logger.Errorf("delete policy %s in app %s failure %s", policy.Name, app.Name, err.Error()) + } + } + + if err := c.envBindingUsecase.BatchDeleteEnvBinding(ctx, app); err != nil { + log.Logger.Errorf("delete envbindings in app %s failure %s", app.Name, err.Error()) + } + + return c.ds.Delete(ctx, app) +} + +func (c *applicationUsecaseImpl) GetApplicationComponent(ctx context.Context, app *model.Application, componentName string) (*model.ApplicationComponent, error) { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: componentName, + } + err := c.ds.Get(ctx, &component) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationComponetNotExist + } + return nil, err + } + return &component, nil +} + +func (c *applicationUsecaseImpl) UpdateComponent(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.UpdateApplicationComponentRequest) (*apisv1.ComponentBase, error) { + if req.Alias != nil { + component.Alias = *req.Alias + } + if req.Description != nil { + component.Description = *req.Description + } + if req.DependsOn != nil { + component.DependsOn = *req.DependsOn + } + if req.Icon != nil { + component.Icon = *req.Icon + } + if req.Labels != nil { + component.Labels = *req.Labels + } + if req.Properties != nil { + properties, err := model.NewJSONStructByString(*req.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + component.Properties = properties + } + if err := c.ds.Put(ctx, component); err != nil { + return nil, err + } + return converComponentModelToBase(component), nil +} + +func (c *applicationUsecaseImpl) AddComponent(ctx context.Context, app *model.Application, com apisv1.CreateComponentRequest) (*apisv1.ComponentBase, error) { + componentModel := model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Description: com.Description, + Labels: com.Labels, + Icon: com.Icon, + // TODO: Get user information from ctx and assign a value. + Creator: "", + Name: com.Name, + Type: com.ComponentType, + DependsOn: com.DependsOn, + Alias: com.Alias, + } + properties, err := model.NewJSONStructByString(com.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + componentModel.Properties = properties + if err := c.ds.Add(ctx, &componentModel); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrApplicationComponetExist + } + log.Logger.Warnf("add component for app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + return converComponentModelToBase(&componentModel), nil +} + +func converComponentModelToBase(componentModel *model.ApplicationComponent) *apisv1.ComponentBase { + if componentModel == nil { + return nil + } + return &apisv1.ComponentBase{ + Name: componentModel.Name, + Description: componentModel.Description, + Labels: componentModel.Labels, + ComponentType: componentModel.Type, + Icon: componentModel.Icon, + DependsOn: componentModel.DependsOn, + Creator: componentModel.Creator, + CreateTime: componentModel.CreateTime, + UpdateTime: componentModel.UpdateTime, + } +} + +func (c *applicationUsecaseImpl) DeleteComponent(ctx context.Context, app *model.Application, componentName string) error { + var component = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: componentName, + } + if err := c.ds.Delete(ctx, &component); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationComponetNotExist + } + log.Logger.Warnf("delete app component %s failure %s", app.PrimaryKey(), err.Error()) + return err + } + return nil +} + +func (c *applicationUsecaseImpl) AddPolicy(ctx context.Context, app *model.Application, createpolicy apisv1.CreatePolicyRequest) (*apisv1.PolicyBase, error) { + policyModel := model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Description: createpolicy.Description, + // TODO: Get user information from ctx and assign a value. + Creator: "", + Name: createpolicy.Name, + Type: createpolicy.Type, + } + properties, err := model.NewJSONStructByString(createpolicy.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + policyModel.Properties = properties + if err := c.ds.Add(ctx, &policyModel); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrApplicationPolicyExist + } + log.Logger.Warnf("add policy for app %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + return &apisv1.PolicyBase{ + Name: policyModel.Name, + Description: policyModel.Description, + Type: policyModel.Type, + Creator: policyModel.Creator, + CreateTime: policyModel.CreateTime, + UpdateTime: policyModel.UpdateTime, + Properties: policyModel.Properties, + }, nil +} + +func (c *applicationUsecaseImpl) DeletePolicy(ctx context.Context, app *model.Application, policyName string) error { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + if err := c.ds.Delete(ctx, &policy); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationPolicyNotExist + } + log.Logger.Warnf("delete app policy %s failure %s", app.PrimaryKey(), err.Error()) + return err + } + return nil +} + +func (c *applicationUsecaseImpl) UpdatePolicy(ctx context.Context, app *model.Application, policyName string, policyUpdate apisv1.UpdatePolicyRequest) (*apisv1.DetailPolicyResponse, error) { + var policy = model.ApplicationPolicy{ + AppPrimaryKey: app.PrimaryKey(), + Name: policyName, + } + err := c.ds.Get(ctx, &policy) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationPolicyNotExist + } + log.Logger.Warnf("update app policy %s failure %s", app.PrimaryKey(), err.Error()) + return nil, err + } + policy.Type = policyUpdate.Type + properties, err := model.NewJSONStructByString(policyUpdate.Properties) + if err != nil { + return nil, bcode.ErrInvalidProperties + } + policy.Properties = properties + policy.Description = policyUpdate.Description + + if err := c.ds.Put(ctx, &policy); err != nil { + return nil, err + } + return &apisv1.DetailPolicyResponse{ + PolicyBase: *c.converPolicyModelToBase(&policy), + }, nil +} + +func (c *applicationUsecaseImpl) CreateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, req apisv1.CreateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return nil, err + } + for _, trait := range comp.Traits { + if trait.Type == req.Type { + return nil, bcode.ErrTraitAlreadyExist + } + } + properties, err := model.NewJSONStructByString(req.Properties) + if err != nil { + log.Logger.Errorf("new trait failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + trait := model.ApplicationTrait{CreateTime: time.Now(), Type: req.Type, Properties: properties, Alias: req.Alias, Description: req.Description} + comp.Traits = append(comp.Traits, trait) + if err := c.ds.Put(ctx, &comp); err != nil { + return nil, err + } + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties, Alias: req.Alias, Description: req.Description, CreateTime: trait.CreateTime}, nil +} + +func (c *applicationUsecaseImpl) DeleteApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string) error { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return err + } + for i, trait := range comp.Traits { + if trait.Type == traitType { + comp.Traits = append(comp.Traits[:i], comp.Traits[i+1:]...) + if err := c.ds.Put(ctx, &comp); err != nil { + return err + } + return nil + } + } + return bcode.ErrTraitNotExist +} + +func (c *applicationUsecaseImpl) UpdateApplicationTrait(ctx context.Context, app *model.Application, component *model.ApplicationComponent, traitType string, req apisv1.UpdateApplicationTraitRequest) (*apisv1.ApplicationTrait, error) { + var comp = model.ApplicationComponent{ + AppPrimaryKey: app.PrimaryKey(), + Name: component.Name, + } + if err := c.ds.Get(ctx, &comp); err != nil { + return nil, err + } + for i, trait := range comp.Traits { + if trait.Type == traitType { + properties, err := model.NewJSONStructByString(req.Properties) + if err != nil { + log.Logger.Errorf("update trait failure,%s", err.Error()) + return nil, bcode.ErrInvalidProperties + } + updatedTrait := model.ApplicationTrait{CreateTime: trait.CreateTime, UpdateTime: time.Now(), Properties: properties, Type: traitType, Alias: req.Alias, Description: req.Description} + comp.Traits[i] = updatedTrait + if err := c.ds.Put(ctx, &comp); err != nil { + return nil, err + } + return &apisv1.ApplicationTrait{Type: trait.Type, Properties: properties, + Alias: updatedTrait.Alias, Description: updatedTrait.Description, CreateTime: updatedTrait.CreateTime, UpdateTime: updatedTrait.UpdateTime}, nil + } + } + return nil, bcode.ErrTraitNotExist +} + +func (c *applicationUsecaseImpl) ListRevisions(ctx context.Context, appName, envName, status string, page, pageSize int) (*apisv1.ListRevisionsResponse, error) { + var revision = model.ApplicationRevision{ + AppPrimaryKey: appName, + } + if envName != "" { + revision.EnvName = envName + } + if status != "" { + revision.Status = status + } + + revisions, err := c.ds.List(ctx, &revision, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) + if err != nil { + return nil, err + } + + resp := &apisv1.ListRevisionsResponse{ + Revisions: []apisv1.ApplicationRevisionBase{}, + } + for _, raw := range revisions { + r, ok := raw.(*model.ApplicationRevision) + if ok { + resp.Revisions = append(resp.Revisions, apisv1.ApplicationRevisionBase{ + CreateTime: r.CreateTime, + Version: r.Version, + Status: r.Status, + Reason: r.Reason, + DeployUser: r.DeployUser, + Note: r.Note, + EnvName: r.EnvName, + TriggerType: r.TriggerType, + }) + } + } + count, err := c.ds.Count(ctx, &revision, nil) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +func (c *applicationUsecaseImpl) DetailRevision(ctx context.Context, appName, revisionVersion string) (*apisv1.DetailRevisionResponse, error) { + var revision = model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: revisionVersion, + } + if err := c.ds.Get(ctx, &revision); err != nil { + return nil, err + } + return &apisv1.DetailRevisionResponse{ + ApplicationRevision: revision, + }, nil +} + +func (c *applicationUsecaseImpl) Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error) { + var targetMap = make(map[string]int) + envbinding, err := c.envBindingUsecase.GetEnvBindings(ctx, app) + if err != nil { + log.Logger.Errorf("query app envbinding failure %s", err.Error()) + } + for _, env := range envbinding { + for _, target := range env.TargetNames { + targetMap[target]++ + } + } + count, err := c.ds.Count(ctx, &model.ApplicationRevision{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) + if err != nil { + return nil, err + } + return &apisv1.ApplicationStatisticsResponse{ + EnvCount: int64(len(envbinding)), + DeliveryTargetCount: int64(len(targetMap)), + RevisonCount: count, + WorkflowCount: c.workflowUsecase.CountWorkflow(ctx, app), + }, nil +} + +func createTargetClusterEnv(envBind *model.EnvBinding, target *model.DeliveryTarget) v1alpha1.EnvConfig { + placement := v1alpha1.EnvPlacement{} + var componentSelector *v1alpha1.EnvSelector + if envBind.ComponentSelector != nil { + componentSelector = &v1alpha1.EnvSelector{ + Components: envBind.ComponentSelector.Components, + } + } + if target.Cluster != nil { + placement.ClusterSelector = &common.ClusterSelector{Name: target.Cluster.ClusterName} + placement.NamespaceSelector = &v1alpha1.NamespaceSelector{Name: target.Cluster.Namespace} + } + return v1alpha1.EnvConfig{ + Name: genPolicyEnvName(target.Name), + Placement: placement, + Selector: componentSelector, + } +} + +func convertAppName(appModelName, envName string) string { + return fmt.Sprintf("%s-%s", appModelName, envName) +} + +func converComponentName(componentModelName, envName string) string { + return fmt.Sprintf("%s-%s", componentModelName, envName) +} + +func genPolicyName(envName string) string { + return fmt.Sprintf("%s-%s", EnvBindingPolicyDefaultName, envName) +} + +func genPolicyEnvName(targetName string) string { + return targetName } diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go new file mode 100644 index 000000000..5cbdbe44a --- /dev/null +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -0,0 +1,522 @@ +/* +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 usecase + +import ( + "context" + "fmt" + "io/ioutil" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "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/pkg/apiserver/model" + 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/apply" +) + +var _ = Describe("Test application usecase function", func() { + var ( + appUsecase *applicationUsecaseImpl + workflowUsecase *workflowUsecaseImpl + envBindingUsecase *envBindingUsecaseImpl + deliveryTargetUsecase *deliveryTargetUsecaseImpl + ) + BeforeEach(func() { + workflowUsecase = &workflowUsecaseImpl{ds: ds} + envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase, kubeClient: k8sClient} + deliveryTargetUsecase = &deliveryTargetUsecaseImpl{ds: ds} + appUsecase = &applicationUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + apply: apply.NewAPIApplicator(k8sClient), + kubeClient: k8sClient, + envBindingUsecase: envBindingUsecase, + deliveryTargetUsecase: deliveryTargetUsecase, + } + }) + It("Test CreateApplication function", func() { + By("test sample create") + req := v1.CreateApplicationRequest{ + Name: "test-app", + Namespace: "test-app-namespace", + Description: "this is a test app", + EnvBinding: []*v1.EnvBinding{{ + Name: "dev", + Description: "dev env", + TargetNames: []string{"dev-target"}, + }, { + Name: "test", + Description: "test env", + TargetNames: []string{"test-target"}, + }}, + } + base, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + _, err = appUsecase.CreateApplication(context.TODO(), req) + equal := cmp.Equal(err, bcode.ErrApplicationExist, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + + By("test with oam yaml config create") + bs, err := ioutil.ReadFile("./testdata/example-app.yaml") + Expect(err).Should(Succeed()) + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: string(bs), + } + base, err = appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd2", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: "asdasdasdasd", + } + base, err = appUsecase.CreateApplication(context.TODO(), req) + equal = cmp.Equal(err, bcode.ErrApplicationConfig, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + Expect(base).Should(BeNil()) + + bs, err = ioutil.ReadFile("./testdata/example-app-error.yaml") + Expect(err).Should(Succeed()) + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd3", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: string(bs), + } + _, err = appUsecase.CreateApplication(context.TODO(), req) + equal = cmp.Equal(err, bcode.ErrInvalidProperties, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + + By("Test create app with env binding") + req = v1.CreateApplicationRequest{ + Name: "test-app-sadasd4", + Namespace: "test-app-namespace", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + EnvBinding: []*v1.EnvBinding{ + { + Name: "dev", + Alias: "Chinese Word", + Description: "This is a dev env", + TargetNames: []string{"dev-target"}, + }, + { + Name: "prod", + Description: "This is a prod env", + TargetNames: []string{"prod-target"}, + }, + }, + } + appBase, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appBase.Name, "test-app-sadasd4")).Should(BeEmpty()) + + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd4") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + }) + + It("Test ListApplications function", func() { + _, err := appUsecase.ListApplications(context.TODO(), v1.ListApplicatioOptions{}) + Expect(err).Should(BeNil()) + }) + + It("Test DetailApplication function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := appUsecase.DetailApplication(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detail.ResourceInfo.ComponentNum, int64(2))).Should(BeEmpty()) + Expect(cmp.Diff(len(detail.Policies), 0)).Should(BeEmpty()) + }) + + It("Test ListComponents function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{}) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].ComponentType, "worker")).Should(BeEmpty()) + Expect(components[1].UpdateTime).ShouldNot(BeNil()) + + components, err = appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ + EnvName: "test", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "data-worker")).Should(BeEmpty()) + + components, err = appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{ + EnvName: "staging", + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "data-worker")).Should(BeEmpty()) + }) + + It("Test DetailComponent function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + detail, err := appUsecase.DetailComponent(context.TODO(), appModel, "hello-world-server") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(detail.Traits), 1)).Should(BeEmpty()) + Expect(cmp.Diff(detail.Type, "webservice")).Should(BeEmpty()) + Expect(cmp.Diff(strings.Contains((*detail.Properties)["image"].(string), "crccheck/hello-world"), true)).Should(BeEmpty()) + }) + + It("Test AddComponent function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + base, err := appUsecase.AddComponent(context.TODO(), appModel, v1.CreateComponentRequest{ + Name: "test2", + Description: "this is a test2 component", + Labels: map[string]string{}, + ComponentType: "worker", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + DependsOn: []string{"data-worker"}, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.ComponentType, "worker")).Should(BeEmpty()) + }) + + It("Test DetailComponent function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + detailResponse, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(detailResponse.DependsOn[0], "data-worker")).Should(BeEmpty()) + Expect(detailResponse.Properties).ShouldNot(BeNil()) + Expect(cmp.Diff((*detailResponse.Properties)["image"], "busybox")).Should(BeEmpty()) + }) + + It("Test AddPolicy function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ + Name: EnvBindingPolicyDefaultName, + Description: "this is a test2 policy", + Type: "env-binding", + Properties: `{"envs":{ "name": "test", "placement":{"namespaceSelector":{ "name": "TEST_NAMESPACE"}}, "selector":{ "components": ["data-worker"]}}}`, + }) + Expect(err).Should(BeNil()) + + _, err = appUsecase.AddPolicy(context.TODO(), appModel, v1.CreatePolicyRequest{ + Name: EnvBindingPolicyDefaultName, + Description: "this is a test2 policy", + Type: "env-binding", + Properties: ``, + }) + Expect(cmp.Equal(err, bcode.ErrApplicationPolicyExist, cmpopts.EquateErrors())).Should(BeTrue()) + }) + + It("Test ListPolicies function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + + policies, err := appUsecase.ListPolicies(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(policies), 1)).Should(BeEmpty()) + }) + + It("Test DetailPolicy function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + detail, err := appUsecase.DetailPolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) + Expect(err).Should(BeNil()) + Expect(detail.Properties).ShouldNot(BeNil()) + Expect((*detail.Properties)["envs"]).ShouldNot(BeEmpty()) + }) + + It("Test UpdatePolicy function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + base, err := appUsecase.UpdatePolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName, v1.UpdatePolicyRequest{ + Type: "env-binding", + Properties: `{"envs":{}}`, + }) + Expect(err).Should(BeNil()) + Expect(base.Properties).ShouldNot(BeNil()) + Expect((*base.Properties)["envs"]).Should(BeEmpty()) + }) + It("Test DeletePolicy function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + err = appUsecase.DeletePolicy(context.TODO(), appModel, EnvBindingPolicyDefaultName) + Expect(err).Should(BeNil()) + }) + + It("Test add application trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + alias := "alias" + description := "description" + res, err := appUsecase.CreateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, v1.CreateApplicationTraitRequest{ + Type: "Ingress", + Properties: `{"domain":"www.test.com"}`, + Alias: alias, + Description: description, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) + comp, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(comp).ShouldNot(BeNil()) + Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) + Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test.com"}`)) + Expect(comp.Traits[0].Alias).Should(BeEquivalentTo(alias)) + Expect(comp.Traits[0].Description).Should(BeEquivalentTo(description)) + }) + + It("Test add application a dup trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + _, err = appUsecase.CreateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, v1.CreateApplicationTraitRequest{ + Type: "Ingress", + Properties: `{"domain":"www.dup.com"}`, + }) + Expect(err).ShouldNot(BeNil()) + }) + + It("Test update application trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + alias := "newAlias" + description := "newDescription" + res, err := appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress", v1.UpdateApplicationTraitRequest{ + Properties: `{"domain":"www.test1.com"}`, + Alias: alias, + Description: description, + }) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(res.Type, "Ingress")).Should(BeEmpty()) + comp, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(comp).ShouldNot(BeNil()) + Expect(len(comp.Traits)).Should(BeEquivalentTo(1)) + Expect(comp.Traits[0].Properties.JSON()).Should(BeEquivalentTo(`{"domain":"www.test1.com"}`)) + Expect(comp.Traits[0].Alias).Should(BeEquivalentTo(alias)) + Expect(comp.Traits[0].Description).Should(BeEquivalentTo(description)) + }) + + It("Test update a not exist", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + _, err = appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress-1-20", v1.UpdateApplicationTraitRequest{ + Properties: `{"domain":"www.test1.com"}`, + }) + Expect(err).ShouldNot(BeNil()) + }) + + It("Test delete an exist trait", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + err = appUsecase.DeleteApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "test2"}, "Ingress") + Expect(err).Should(BeNil()) + app, err := appUsecase.DetailComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + Expect(app).ShouldNot(BeNil()) + Expect(len(app.Traits)).Should(BeEquivalentTo(0)) + }) + + It("Test DeleteComponent function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(appModel.Namespace, "test-app-namespace")).Should(BeEmpty()) + err = appUsecase.DeleteComponent(context.TODO(), appModel, "test2") + Expect(err).Should(BeNil()) + }) + + It("Test DeleteApplication function", func() { + appModel, err := appUsecase.GetApplication(context.TODO(), "test-app-sadasd") + Expect(err).Should(BeNil()) + err = appUsecase.DeleteApplication(context.TODO(), appModel) + Expect(err).Should(BeNil()) + components, err := appUsecase.ListComponents(context.TODO(), appModel, v1.ListApplicationComponentOptions{}) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 0)).Should(BeEmpty()) + policies, err := appUsecase.ListPolicies(context.TODO(), appModel) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(policies), 0)).Should(BeEmpty()) + }) + + It("Test ListRevisions function", func() { + for i := 0; i < 3; i++ { + appModel := &model.ApplicationRevision{ + AppPrimaryKey: "test-app", + Version: fmt.Sprintf("%d", i), + EnvName: fmt.Sprintf("env-%d", i), + Status: model.RevisionStatusRunning, + } + if i == 0 { + appModel.Status = model.RevisionStatusTerminated + } + err := workflowUsecase.createTestApplicationRevision(context.TODO(), appModel) + Expect(err).Should(BeNil()) + } + revisions, err := appUsecase.ListRevisions(context.TODO(), "test-app", "", "", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(3))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "env-0", "", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(1))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "", "terminated", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(1))) + + revisions, err = appUsecase.ListRevisions(context.TODO(), "test-app", "env-1", "terminated", 0, 10) + Expect(err).Should(BeNil()) + Expect(revisions.Total).Should(Equal(int64(0))) + }) + + It("Test DetailRevision function", func() { + err := workflowUsecase.createTestApplicationRevision(context.TODO(), &model.ApplicationRevision{ + AppPrimaryKey: "test-app", + Version: "123", + DeployUser: "test-user", + }) + Expect(err).Should(BeNil()) + revision, err := appUsecase.DetailRevision(context.TODO(), "test-app", "123") + Expect(err).Should(BeNil()) + Expect(revision.Version).Should(Equal("123")) + Expect(revision.DeployUser).Should(Equal("test-user")) + }) + + It("Test ApplicationEnvRecycle function", func() { + req := v1.CreateApplicationRequest{ + Name: "app-env-recycle" + "-dev", + Namespace: "test-app-namespace", + Description: "this is a test app with env", + } + base, err := appUsecase.CreateApplication(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + + err = envBindingUsecase.ApplicationEnvRecycle(context.TODO(), &model.Application{ + Name: "app-env-recycle", + Namespace: "test-app-namespace", + }, &model.EnvBinding{Name: "dev"}) + Expect(err).Should(BeNil()) + }) + + It("Test ListRecords function", func() { + By("no running records in application") + ctx := context.TODO() + for i := 0; i < 2; i++ { + appUsecase.ds.Add(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "app-records", + Name: fmt.Sprintf("list-%d", i), + Status: model.RevisionStatusComplete, + }) + } + + resp, err := appUsecase.ListRecords(context.TODO(), "app-records") + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(1))) + + By("3 running records in application") + for i := 0; i < 3; i++ { + appUsecase.ds.Add(ctx, &model.WorkflowRecord{ + AppPrimaryKey: "app-records", + Name: fmt.Sprintf("list-running-%d", i), + Status: model.RevisionStatusRunning, + }) + } + + resp, err = appUsecase.ListRecords(context.TODO(), "app-records") + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(3))) + }) +}) + +func createTestSuspendApp(ctx context.Context, appName, envName, revisionVersion, wfName, recordName string, kubeClient client.Client) (*v1beta1.Application, error) { + testapp := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: convertAppName(appName, envName), + Namespace: "default", + Annotations: map[string]string{ + oam.AnnotationDeployVersion: revisionVersion, + oam.AnnotationWorkflowName: wfName, + oam.AnnotationPublishVersion: recordName, + oam.AnnotationAppName: appName, + }, + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "test-component", + Type: "worker", + Properties: &runtime.RawExtension{Raw: []byte(`{"test":"test"}`)}, + Traits: []common.ApplicationTrait{}, + Scopes: map[string]string{}, + }}, + }, + Status: common.AppStatus{ + Workflow: &common.WorkflowStatus{ + Suspend: true, + }, + }, + } + + if err := kubeClient.Create(ctx, testapp); err != nil { + return nil, err + } + if err := kubeClient.Status().Patch(ctx, testapp, client.Merge); err != nil { + return nil, err + } + + return testapp, nil +} diff --git a/pkg/apiserver/rest/usecase/cluster.go b/pkg/apiserver/rest/usecase/cluster.go index 7c54406c5..a0e11998c 100644 --- a/pkg/apiserver/rest/usecase/cluster.go +++ b/pkg/apiserver/rest/usecase/cluster.go @@ -18,25 +18,601 @@ package usecase import ( "context" + "fmt" + "io/ioutil" + "os" + "strings" + "time" + "github.com/oam-dev/terraform-controller/api/types" + "github.com/oam-dev/terraform-controller/api/v1beta1" + "github.com/pkg/errors" + v12 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "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" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + utils2 "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/cloudprovider" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/pkg/utils/util" ) // ClusterUsecase cluster manage type ClusterUsecase interface { + ListKubeClusters(context.Context, string, int, int) (*apis.ListClusterResponse, error) CreateKubeCluster(context.Context, apis.CreateClusterRequest) (*apis.ClusterBase, error) + GetKubeCluster(context.Context, string) (*apis.DetailClusterResponse, error) + ModifyKubeCluster(context.Context, apis.CreateClusterRequest, string) (*apis.ClusterBase, error) + DeleteKubeCluster(context.Context, string) (*apis.ClusterBase, error) + + CreateClusterNamespace(context.Context, string, apis.CreateClusterNamespaceRequest) (*apis.CreateClusterNamespaceResponse, error) + + ListCloudClusters(context.Context, string, apis.AccessKeyRequest, int, int) (*apis.ListCloudClusterResponse, error) + ConnectCloudCluster(context.Context, string, apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) + CreateCloudCluster(context.Context, string, apis.CreateCloudClusterRequest) (*apis.CreateCloudClusterResponse, error) + GetCloudClusterCreationStatus(context.Context, string, string) (*apis.CreateCloudClusterResponse, error) + ListCloudClusterCreation(context.Context, string) (*apis.ListCloudClusterCreationResponse, error) + DeleteCloudClusterCreation(context.Context, string, string) (*apis.CreateCloudClusterResponse, error) } type clusterUsecaseImpl struct { - ds datastore.DataStore + ds datastore.DataStore + caches map[string]*utils2.MemoryCache + k8sClient client.Client } // NewClusterUsecase new cluster usecase func NewClusterUsecase(ds datastore.DataStore) ClusterUsecase { - return &clusterUsecaseImpl{ds: ds} + k8sClient, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get k8sClient failure: %s", err.Error()) + } + c := &clusterUsecaseImpl{ds: ds, k8sClient: k8sClient, caches: make(map[string]*utils2.MemoryCache)} + if err = c.preAddLocalCluster(context.Background()); err != nil { + log.Logger.Fatalf("preAdd local cluster failure: %s", err.Error()) + } + return c } -func (c *clusterUsecaseImpl) CreateKubeCluster(context.Context, apis.CreateClusterRequest) (*apis.ClusterBase, error) { - return nil, nil +func (c *clusterUsecaseImpl) getClusterFromDataStore(ctx context.Context, clusterName string) (*model.Cluster, error) { + cluster := &model.Cluster{ + Name: clusterName, + } + if err := c.ds.Get(ctx, cluster); err != nil { + return nil, err + } + return cluster, nil +} + +func (c *clusterUsecaseImpl) rollbackAddedClusterInDataStore(ctx context.Context, cluster *model.Cluster) { + if e := c.ds.Delete(ctx, cluster); e != nil { + log.Logger.Errorf("failed to rollback added cluster %s in data store: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackDeletedClusterInDataStore(ctx context.Context, cluster *model.Cluster) { + if e := c.ds.Add(ctx, cluster); e != nil { + log.Logger.Errorf("failed to rollback deleted cluster %s in data store: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackJoinedKubeCluster(ctx context.Context, cluster *model.Cluster) { + if e := multicluster.DetachCluster(ctx, c.k8sClient, cluster.Name); e != nil { + log.Logger.Errorf("failed to rollback joined cluster %s in kubevela: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) rollbackDetachedKubeCluster(ctx context.Context, cluster *model.Cluster) { + if _, e := joinClusterByKubeConfigString(ctx, c.k8sClient, cluster.Name, cluster.KubeConfig); e != nil { + log.Logger.Errorf("failed to rollback detached cluster %s in kubevela: %s", cluster.Name, e.Error()) + } +} + +func (c *clusterUsecaseImpl) preAddLocalCluster(ctx context.Context) error { + cfg, err := clients.GetKubeConfig() + if err != nil { + return err + } + localCluster := &model.Cluster{ + Name: multicluster.ClusterLocalName, + Description: "The hub manage cluster where KubeVela runs on.", + Status: model.ClusterStatusHealthy, + APIServerURL: cfg.Host + cfg.APIPath, + } + if err = c.ds.Get(ctx, localCluster); err != nil { + // no local cluster in datastore + if errors.Is(err, datastore.ErrRecordNotExist) { + if err = c.ds.Add(ctx, localCluster); err != nil { + // local cluster already added in datastore + if errors.Is(err, datastore.ErrRecordExist) { + return nil + } + return err + } + return nil + } + return err + } + if localCluster.CreateTime.Before(model.LocalClusterCreatedTime) { + localCluster.CreateTime = model.LocalClusterCreatedTime + if err = c.ds.Put(ctx, localCluster); err != nil { + return err + } + } + return nil +} + +func (c *clusterUsecaseImpl) ListKubeClusters(ctx context.Context, query string, page int, pageSize int) (*apis.ListClusterResponse, error) { + var queries []datastore.FuzzyQueryOption + if query != "" { + queries = append(queries, datastore.FuzzyQueryOption{Key: "name", Query: query}) + } + fo := datastore.FilterOptions{Queries: queries} + clusters, err := c.ds.List(ctx, &model.Cluster{}, &datastore.ListOptions{ + Page: page, + PageSize: pageSize, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + FilterOptions: fo, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to list cluster with query %s in data store", query) + } + resp := &apis.ListClusterResponse{ + Clusters: []apis.ClusterBase{}, + } + for _, raw := range clusters { + cluster, ok := raw.(*model.Cluster) + if ok { + resp.Clusters = append(resp.Clusters, *newClusterBaseFromCluster(cluster)) + } + } + total, err := c.ds.Count(ctx, &model.Cluster{}, &fo) + if err != nil { + return nil, errors.Wrapf(err, "failed to count cluster with query %s in data store", query) + } + resp.Total = total + return resp, nil +} + +func joinClusterByKubeConfigString(ctx context.Context, k8sClient client.Client, clusterName string, kubeConfig string) (string, error) { + tmpFileName := fmt.Sprintf("/tmp/cluster-secret-%s-%s-%d.kubeconfig", clusterName, utils.RandomString(8), time.Now().UnixNano()) + if err := ioutil.WriteFile(tmpFileName, []byte(kubeConfig), 0600); err != nil { + return "", errors.Wrapf(err, "failed to write kubeconfig to temp file %s", tmpFileName) + } + defer func() { + _ = os.Remove(tmpFileName) + }() + cluster, err := multicluster.JoinClusterByKubeConfig(ctx, k8sClient, tmpFileName, clusterName) + if err != nil { + if errors.Is(err, multicluster.ErrClusterExists) { + return "", bcode.ErrClusterExistsInKubernetes + } + return "", errors.Wrapf(err, "failed to join cluster") + } + return cluster.Server, nil +} + +func createClusterModelFromRequest(req apis.CreateClusterRequest, oldCluster *model.Cluster) (newCluster *model.Cluster) { + if oldCluster != nil { + newCluster = oldCluster.DeepCopy() + } else { + newCluster = &model.Cluster{} + } + newCluster.Name = req.Name + newCluster.Alias = req.Alias + newCluster.Description = req.Description + newCluster.Icon = req.Icon + newCluster.Labels = req.Labels + newCluster.KubeConfig = req.KubeConfig + newCluster.KubeConfigSecret = req.KubeConfigSecret + newCluster.DashboardURL = req.DashboardURL + return newCluster +} + +func (c *clusterUsecaseImpl) createKubeCluster(ctx context.Context, req apis.CreateClusterRequest, providerCluster *cloudprovider.CloudCluster) (*apis.ClusterBase, error) { + var err error + cluster := createClusterModelFromRequest(req, nil) + if cluster.Name == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterReserved + } + t := time.Now() + cluster.SetCreateTime(t) + cluster.SetUpdateTime(t) + if providerCluster != nil { + cluster.Provider = model.ProviderInfo{ + Provider: providerCluster.Provider, + ClusterName: providerCluster.Name, + ClusterID: providerCluster.ID, + Zone: providerCluster.Zone, + ZoneID: providerCluster.ZoneID, + RegionID: providerCluster.RegionID, + VpcID: providerCluster.VpcID, + Labels: providerCluster.Labels, + } + cluster.DashboardURL = providerCluster.DashBoardURL + } + if err = c.ds.Get(ctx, cluster); err == nil { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } else if !errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } + if req.KubeConfig != "" { + cluster.APIServerURL, err = joinClusterByKubeConfigString(ctx, c.k8sClient, req.Name, req.KubeConfig) + if err != nil { + return nil, err + } + c.setClusterStatusAndResourceInfo(ctx, cluster) + if err = c.ds.Add(ctx, cluster); err != nil { + c.rollbackJoinedKubeCluster(ctx, cluster) + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } + return nil, err + } + return newClusterBaseFromCluster(cluster), nil + } + if req.KubeConfigSecret != "" { + return nil, bcode.ErrKubeConfigSecretNotSupport + } + return nil, bcode.ErrKubeConfigAndSecretIsNotSet +} + +func (c *clusterUsecaseImpl) CreateKubeCluster(ctx context.Context, req apis.CreateClusterRequest) (*apis.ClusterBase, error) { + return c.createKubeCluster(ctx, req, nil) +} + +func (c *clusterUsecaseImpl) GetKubeCluster(ctx context.Context, clusterName string) (*apis.DetailClusterResponse, error) { + cluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + resourceInfo := c.setClusterStatusAndResourceInfo(ctx, cluster) + if err = c.ds.Put(ctx, cluster); err != nil { + return nil, errors.Wrapf(err, "failed to update cluster %s status info", clusterName) + } + return &apis.DetailClusterResponse{ + Cluster: *cluster, + ResourceInfo: resourceInfo, + }, nil +} + +func (c *clusterUsecaseImpl) ModifyKubeCluster(ctx context.Context, req apis.CreateClusterRequest, clusterName string) (*apis.ClusterBase, error) { + oldCluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + + newCluster := createClusterModelFromRequest(req, oldCluster) + newCluster.SetUpdateTime(time.Now()) + if oldCluster.Name != newCluster.Name || oldCluster.KubeConfig != newCluster.KubeConfig || oldCluster.KubeConfigSecret != newCluster.KubeConfigSecret { + if clusterName == multicluster.ClusterLocalName || newCluster.Name == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterImmutable + } + if newCluster.KubeConfig == "" && newCluster.KubeConfigSecret != "" { + return nil, bcode.ErrKubeConfigSecretNotSupport + } + newClusterTempName := newCluster.Name + "_tmp_" + utils.RandomString(8) + newCluster.APIServerURL, err = joinClusterByKubeConfigString(ctx, c.k8sClient, newCluster.Name, newCluster.KubeConfig) + if err != nil { + return nil, errors.Wrapf(err, "failed to join new cluster %s", newCluster.Name) + } + c.setClusterStatusAndResourceInfo(ctx, newCluster) + rollbackTempCluster := func() { + rollBackCluster := newCluster.DeepCopy() + rollBackCluster.Name = newClusterTempName + c.rollbackJoinedKubeCluster(ctx, rollBackCluster) + } + if err = multicluster.DetachCluster(ctx, c.k8sClient, oldCluster.Name); err != nil { + rollbackTempCluster() + return nil, errors.Wrapf(err, "failed to detach old cluster %s", oldCluster.Name) + } + if err = c.ds.Delete(ctx, oldCluster); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to delete old cluster %s from datastore", oldCluster.Name) + } + if err = c.ds.Add(ctx, newCluster); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + c.rollbackDeletedClusterInDataStore(ctx, oldCluster) + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrClusterAlreadyExistInDataStore + } + return nil, errors.Wrapf(err, "failed to add new cluster %s to datastore", newCluster.Name) + } + if err = multicluster.RenameCluster(ctx, c.k8sClient, newClusterTempName, newCluster.Name); err != nil { + rollbackTempCluster() + c.rollbackDetachedKubeCluster(ctx, oldCluster) + c.rollbackDeletedClusterInDataStore(ctx, oldCluster) + c.rollbackAddedClusterInDataStore(ctx, newCluster) + return nil, errors.Wrapf(err, "failed to rename temporary cluster %s to %s", newClusterTempName, newCluster.Name) + } + } else { + newCluster.Status = oldCluster.Status + newCluster.Reason = oldCluster.Reason + if err = c.ds.Put(ctx, newCluster); err != nil { + return nil, errors.Wrapf(err, "failed to update cluster %s", newCluster.Name) + } + } + return newClusterBaseFromCluster(newCluster), nil +} + +func (c *clusterUsecaseImpl) DeleteKubeCluster(ctx context.Context, clusterName string) (*apis.ClusterBase, error) { + if clusterName == multicluster.ClusterLocalName { + return nil, bcode.ErrLocalClusterImmutable + } + cluster, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + if err = c.ds.Delete(ctx, cluster); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to delete cluster %s in data store", clusterName) + } + if err = multicluster.DetachCluster(ctx, c.k8sClient, clusterName); err != nil { + c.rollbackDeletedClusterInDataStore(ctx, cluster) + return nil, errors.Wrapf(err, "failed to delete cluster %s in kubernetes", clusterName) + } + return newClusterBaseFromCluster(cluster), nil +} + +func (c *clusterUsecaseImpl) CreateClusterNamespace(ctx context.Context, clusterName string, req apis.CreateClusterNamespaceRequest) (*apis.CreateClusterNamespaceResponse, error) { + _, err := c.getClusterFromDataStore(ctx, clusterName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrClusterNotFoundInDataStore + } + return nil, errors.Wrapf(err, "failed to found cluster %s in data store", clusterName) + } + ns := &v12.Namespace{} + ns.Name = req.Namespace + if err = c.k8sClient.Create(multicluster.ContextWithClusterName(ctx, clusterName), ns); err != nil { + if kerrors.IsAlreadyExists(err) { + return &apis.CreateClusterNamespaceResponse{Exists: true}, nil + } + if kerrors.IsForbidden(err) { + return nil, bcode.ErrClusterCreateNamespaceNoPermission + } + return nil, errors.Wrapf(err, "failed to create namespace %s in cluster %s", req.Namespace, clusterName) + } + return &apis.CreateClusterNamespaceResponse{Exists: false}, nil +} + +func (c *clusterUsecaseImpl) setClusterStatusAndResourceInfo(ctx context.Context, cluster *model.Cluster) apis.ClusterResourceInfo { + resourceInfo, err := c.getClusterResourceInfoFromK8s(ctx, cluster.Name) + if err != nil { + cluster.Status = model.ClusterStatusUnhealthy + cluster.Reason = fmt.Sprintf("Failed to get cluster resource info: %s", err.Error()) + } else { + cluster.Status = model.ClusterStatusHealthy + cluster.Reason = "" + } + return resourceInfo +} + +func (c *clusterUsecaseImpl) getClusterResourceInfoCacheKey(clusterName string) string { + return "cluster-resource-info::" + clusterName +} + +func (c *clusterUsecaseImpl) getClusterResourceInfoFromK8s(ctx context.Context, clusterName string) (apis.ClusterResourceInfo, error) { + cacheKey := c.getClusterResourceInfoCacheKey(clusterName) + if cache, exists := c.caches[cacheKey]; exists && !cache.IsExpired() { + return cache.GetData().(apis.ClusterResourceInfo), nil + } + clusterInfo, err := multicluster.GetClusterInfo(ctx, c.k8sClient, clusterName) + if err != nil { + return apis.ClusterResourceInfo{}, err + } + var storageClassList []string + for _, cls := range clusterInfo.StorageClasses.Items { + storageClassList = append(storageClassList, cls.Name) + } + getUsed := func(cap resource.Quantity, alloc resource.Quantity) *resource.Quantity { + used := cap.DeepCopy() + used.Sub(alloc) + return &used + } + // TODO add support for gpu capacity + clusterResourceInfo := apis.ClusterResourceInfo{ + WorkerNumber: clusterInfo.WorkerNumber, + MasterNumber: clusterInfo.MasterNumber, + MemoryCapacity: clusterInfo.MemoryCapacity.Value(), + CPUCapacity: clusterInfo.CPUCapacity.Value(), + GPUCapacity: 0, + PodCapacity: clusterInfo.PodCapacity.Value(), + MemoryUsed: getUsed(clusterInfo.MemoryCapacity, clusterInfo.MemoryAllocatable).Value(), + CPUUsed: getUsed(clusterInfo.CPUCapacity, clusterInfo.CPUAllocatable).Value(), + GPUUsed: 0, + PodUsed: getUsed(clusterInfo.PodCapacity, clusterInfo.PodAllocatable).Value(), + StorageClassList: storageClassList, + } + c.caches[cacheKey] = utils2.NewMemoryCache(clusterResourceInfo, time.Minute) + return clusterResourceInfo, nil +} + +func (c *clusterUsecaseImpl) ListCloudClusters(ctx context.Context, provider string, req apis.AccessKeyRequest, pageNumber int, pageSize int) (*apis.ListCloudClusterResponse, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + clusters, total, err := p.ListCloudClusters(pageNumber, pageSize) + if err != nil { + if p.IsInvalidKey(err) { + return nil, bcode.ErrInvalidAccessKeyOrSecretKey + } + log.Logger.Errorf("failed to list cloud clusters: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + resp := &apis.ListCloudClusterResponse{ + Clusters: []cloudprovider.CloudCluster{}, + Total: total, + } + for _, cluster := range clusters { + resp.Clusters = append(resp.Clusters, *cluster) + } + return resp, nil +} + +func (c *clusterUsecaseImpl) ConnectCloudCluster(ctx context.Context, provider string, req apis.ConnectCloudClusterRequest) (*apis.ClusterBase, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + kubeConfig, err := p.GetClusterKubeConfig(req.ClusterID) + if err != nil { + log.Logger.Errorf("failed to get cluster kubeConfig: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + cluster, err := p.GetClusterInfo(req.ClusterID) + if err != nil { + if p.IsInvalidKey(err) { + return nil, bcode.ErrInvalidAccessKeyOrSecretKey + } + log.Logger.Errorf("failed to get cluster info: %s", err.Error()) + return nil, bcode.ErrGetCloudClusterFailure + } + createReq := apis.CreateClusterRequest{ + Name: req.Name, + Alias: req.Alias, + Description: req.Description, + Icon: req.Icon, + Labels: req.Labels, + KubeConfig: kubeConfig, + } + return c.createKubeCluster(ctx, createReq, cluster) +} + +func (c *clusterUsecaseImpl) CreateCloudCluster(ctx context.Context, provider string, req apis.CreateCloudClusterRequest) (*apis.CreateCloudClusterResponse, error) { + p, err := cloudprovider.GetClusterProvider(provider, req.AccessKeyID, req.AccessKeySecret, c.k8sClient) + if err != nil { + log.Logger.Errorf("failed to get cluster provider: %s", err.Error()) + return nil, bcode.ErrInvalidCloudClusterProvider + } + _, err = p.CreateCloudCluster(ctx, req.Name, req.Zone, req.WorkerNumber, req.CPUCoresPerWorker, req.MemoryPerWorker) + if err != nil { + if kerrors.IsAlreadyExists(err) { + return nil, bcode.ErrCloudClusterAlreadyExists + } + log.Logger.Errorf("failed to bootstrap terraform configuration: %s", err.Error()) + return nil, bcode.ErrBootstrapTerraformConfiguration + } + return c.GetCloudClusterCreationStatus(ctx, provider, req.Name) +} + +func (c *clusterUsecaseImpl) convertTerraformConfigurationStateIntoCloudClusterCreationStatus(cfg v1beta1.Configuration) (status string, clusterID string, err error) { + status = string(cfg.Status.Apply.State) + if status == "" { + return "Initializing", "", nil + } + if cfg.DeletionTimestamp != nil { + return "Deleting", "", nil + } + if status == string(types.Available) { + cid, ok := cfg.Status.Apply.Outputs["CLUSTER_ID"] + if !ok { + status = "ClusterIDNotFound" + return status, "", bcode.ErrClusterIDNotFoundInTerraformConfiguration + } + return status, cid.Value, nil + } + return status, "", nil +} + +func (c *clusterUsecaseImpl) getCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, *v1beta1.Configuration, error) { + terraformConfigurationName := cloudprovider.GetCloudClusterFullName(provider, cloudClusterName) + cfg := &v1beta1.Configuration{ + ObjectMeta: v1.ObjectMeta{ + Name: terraformConfigurationName, + Namespace: util.GetRuntimeNamespace(), + }, + } + if err := c.k8sClient.Get(ctx, client.ObjectKeyFromObject(cfg), cfg); err != nil { + if kerrors.IsNotFound(err) { + return nil, nil, bcode.ErrTerraformConfigurationNotFound + } + return nil, nil, err + } + status, clusterID, err := c.convertTerraformConfigurationStateIntoCloudClusterCreationStatus(*cfg) + if err != nil { + return nil, cfg, err + } + return &apis.CreateCloudClusterResponse{Name: cloudClusterName, Status: status, ClusterID: clusterID}, cfg, nil +} + +func (c *clusterUsecaseImpl) GetCloudClusterCreationStatus(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, error) { + resp, _, err := c.getCloudClusterCreationStatus(ctx, provider, cloudClusterName) + return resp, err +} + +func (c *clusterUsecaseImpl) ListCloudClusterCreation(ctx context.Context, provider string) (*apis.ListCloudClusterCreationResponse, error) { + cfgs := v1beta1.ConfigurationList{} + if err := c.k8sClient.List(ctx, &cfgs, client.HasLabels{cloudprovider.CloudClusterCreatorLabelKey}, client.InNamespace(util.GetRuntimeNamespace())); err != nil { + return nil, err + } + var creations []apis.CreateCloudClusterResponse + for _, cfg := range cfgs.Items { + prefix := "cloud-cluster-" + provider + "-" + if strings.HasPrefix(cfg.Name, prefix) { + status, clusterID, _ := c.convertTerraformConfigurationStateIntoCloudClusterCreationStatus(cfg) + name := strings.TrimPrefix(cfg.Name, prefix) + creations = append(creations, apis.CreateCloudClusterResponse{Name: name, Status: status, ClusterID: clusterID}) + } + } + return &apis.ListCloudClusterCreationResponse{Creations: creations}, nil +} + +func (c *clusterUsecaseImpl) DeleteCloudClusterCreation(ctx context.Context, provider string, cloudClusterName string) (*apis.CreateCloudClusterResponse, error) { + resp, cfg, err := c.getCloudClusterCreationStatus(ctx, provider, cloudClusterName) + if err != nil { + return resp, err + } + if err = c.k8sClient.Delete(ctx, cfg); err != nil { + if kerrors.IsNotFound(err) { + return resp, nil + } + return nil, err + } + return resp, err +} + +func newClusterBaseFromCluster(cluster *model.Cluster) *apis.ClusterBase { + return &apis.ClusterBase{ + Name: cluster.Name, + Alias: cluster.Alias, + Description: cluster.Description, + Icon: cluster.Icon, + Labels: cluster.Labels, + + APIServerURL: cluster.APIServerURL, + DashboardURL: cluster.DashboardURL, + Provider: cluster.Provider, + + Status: cluster.Status, + Reason: cluster.Reason, + } } diff --git a/pkg/apiserver/rest/usecase/definition.go b/pkg/apiserver/rest/usecase/definition.go new file mode 100644 index 000000000..bd32c48eb --- /dev/null +++ b/pkg/apiserver/rest/usecase/definition.go @@ -0,0 +1,299 @@ +/* +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 usecase + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/getkin/kin-openapi/openapi3" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + 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" +) + +// DefinitionUsecase definition usecase, Implement the management of ComponentDefinition、TraitDefinition and WorkflowStepDefinition. +type DefinitionUsecase interface { + // ListDefinitions list definition base info + ListDefinitions(ctx context.Context, envName, defType string) ([]*apisv1.DefinitionBase, error) + // DetailDefinition get definition detail + DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) + // AddDefinitionUISchema add or update custom definition ui schema + AddDefinitionUISchema(ctx context.Context, name, defType, configRaw string) ([]*utils.UIParameter, error) +} + +type definitionUsecaseImpl struct { + kubeClient client.Client + caches map[string]*utils.MemoryCache +} + +const ( + definitionAPIVersion = "core.oam.dev/v1beta1" + kindComponentDefinition = "ComponentDefinition" + kindTraitDefinition = "TraitDefinition" + kindWorkflowStepDefinition = "WorkflowStepDefinition" +) + +// NewDefinitionUsecase new definition usecase +func NewDefinitionUsecase() DefinitionUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &definitionUsecaseImpl{kubeClient: kubecli, caches: make(map[string]*utils.MemoryCache)} +} + +func (d *definitionUsecaseImpl) ListDefinitions(ctx context.Context, envName, defType string) ([]*apisv1.DefinitionBase, error) { + defs := &unstructured.UnstructuredList{} + switch defType { + case "component": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindComponentDefinition) + return d.listDefinitions(ctx, defs, kindComponentDefinition) + + case "trait": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindTraitDefinition) + return d.listDefinitions(ctx, defs, kindTraitDefinition) + + case "workflowstep": + defs.SetAPIVersion(definitionAPIVersion) + defs.SetKind(kindWorkflowStepDefinition) + return d.listDefinitions(ctx, defs, kindWorkflowStepDefinition) + + default: + return nil, bcode.ErrDefinitionTypeNotSupport + } +} + +func (d *definitionUsecaseImpl) listDefinitions(ctx context.Context, list *unstructured.UnstructuredList, cache string) ([]*apisv1.DefinitionBase, error) { + if mc := d.caches[cache]; mc != nil && !mc.IsExpired() { + return mc.GetData().([]*apisv1.DefinitionBase), nil + } + if err := d.kubeClient.List(ctx, list, &client.ListOptions{ + Namespace: types.DefaultKubeVelaNS, + }); err != nil { + return nil, err + } + var defs []*apisv1.DefinitionBase + for _, def := range list.Items { + defs = append(defs, &apisv1.DefinitionBase{ + Name: def.GetName(), + Description: def.GetAnnotations()[types.AnnDescription], + }) + } + d.caches[cache] = utils.NewMemoryCache(defs, time.Minute*3) + return defs, nil +} + +// DetailDefinition get definition detail +func (d *definitionUsecaseImpl) DetailDefinition(ctx context.Context, name, defType string) (*apisv1.DetailDefinitionResponse, error) { + if !utils.StringsContain([]string{"component", "trait", "workflowstep"}, defType) { + return nil, bcode.ErrDefinitionTypeNotSupport + } + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-schema-%s", defType, name), + }, &cm); err != nil { + if apierrors.IsNotFound(err) { + return nil, bcode.ErrDefinitionNoSchema + } + return nil, err + } + + data, ok := cm.Data[types.OpenapiV3JSONSchema] + if !ok { + return nil, bcode.ErrDefinitionNoSchema + } + schema := &openapi3.Schema{} + if err := schema.UnmarshalJSON([]byte(data)); err != nil { + return nil, err + } + // render default ui schema + defaultUISchema := renderDefaultUISchema(schema) + // patch from custom ui schema + customUISchema := d.renderCustomUISchema(ctx, name, defType, defaultUISchema) + return &apisv1.DetailDefinitionResponse{ + APISchema: schema, + UISchema: customUISchema, + }, nil +} + +func (d *definitionUsecaseImpl) renderCustomUISchema(ctx context.Context, name, defType string, defaultSchema []*utils.UIParameter) []*utils.UIParameter { + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, &cm); err != nil { + if !apierrors.IsNotFound(err) { + log.Logger.Errorf("find uischema configmap from cluster failure %s", err.Error()) + } + return defaultSchema + } + data, ok := cm.Data[types.UISchema] + if !ok { + return defaultSchema + } + schema := []*utils.UIParameter{} + if err := json.Unmarshal([]byte(data), &schema); err != nil { + log.Logger.Errorf("unmarshal ui schema failure %s", err.Error()) + return defaultSchema + } + return patchSchema(defaultSchema, schema) +} + +// AddDefinitionUISchema add definition custom ui schema config +func (d *definitionUsecaseImpl) AddDefinitionUISchema(ctx context.Context, name, defType, configRaw string) ([]*utils.UIParameter, error) { + var uiParameters []*utils.UIParameter + err := yaml.Unmarshal([]byte(configRaw), &uiParameters) + if err != nil { + log.Logger.Errorf("yaml unmarshal failure %s", err.Error()) + return nil, bcode.ErrInvalidDefinitionUISchema + } + dataBate, err := json.Marshal(uiParameters) + if err != nil { + log.Logger.Errorf("json marshal failure %s", err.Error()) + return nil, bcode.ErrInvalidDefinitionUISchema + } + var cm v1.ConfigMap + if err := d.kubeClient.Get(ctx, k8stypes.NamespacedName{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, &cm); err != nil { + if apierrors.IsNotFound(err) { + err = d.kubeClient.Create(ctx, &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: types.DefaultKubeVelaNS, + Name: fmt.Sprintf("%s-uischema-%s", defType, name), + }, + Data: map[string]string{ + types.UISchema: string(dataBate), + }, + }) + } + if err != nil { + return nil, err + } + } else { + cm.Data[types.UISchema] = string(dataBate) + err := d.kubeClient.Update(ctx, &cm) + if err != nil { + return nil, err + } + } + return uiParameters, nil +} + +func patchSchema(defaultSchema, customSchema []*utils.UIParameter) []*utils.UIParameter { + var customSchemaMap = make(map[string]*utils.UIParameter, len(customSchema)) + for i, custom := range customSchema { + customSchemaMap[custom.JSONKey] = customSchema[i] + } + for i := range defaultSchema { + dSchema := defaultSchema[i] + if cusSchema, exist := customSchemaMap[dSchema.JSONKey]; exist { + if cusSchema.Description != "" { + dSchema.Description = cusSchema.Description + } + if cusSchema.Label != "" { + dSchema.Label = cusSchema.Label + } + if cusSchema.SubParameterGroupOption != nil { + dSchema.SubParameterGroupOption = cusSchema.SubParameterGroupOption + } + if cusSchema.Validate != nil { + dSchema.Validate = cusSchema.Validate + } + if cusSchema.UIType != "" { + dSchema.UIType = cusSchema.UIType + } + if cusSchema.Disable != nil { + dSchema.Disable = cusSchema.Disable + } + if cusSchema.SubParameters != nil { + dSchema.SubParameters = patchSchema(dSchema.SubParameters, cusSchema.SubParameters) + } + if cusSchema.Sort != 0 { + dSchema.Sort = cusSchema.Sort + } + } + } + sort.Slice(defaultSchema, func(i, j int) bool { + return defaultSchema[i].Sort < defaultSchema[j].Sort + }) + return defaultSchema +} + +func renderDefaultUISchema(apiSchema *openapi3.Schema) []*utils.UIParameter { + if apiSchema == nil { + return nil + } + var params []*utils.UIParameter + for key, property := range apiSchema.Properties { + if property.Value != nil { + param := renderUIParameter(key, utils.FirstUpper(key), property, apiSchema.Required) + params = append(params, param) + } + } + return params +} + +func renderUIParameter(key, label string, property *openapi3.SchemaRef, required []string) *utils.UIParameter { + var parameter utils.UIParameter + subType := "" + if property.Value.Items != nil { + if property.Value.Items.Value != nil { + subType = property.Value.Items.Value.Type + } + parameter.SubParameters = renderDefaultUISchema(property.Value.Items.Value) + } + if property.Value.Properties != nil { + parameter.SubParameters = renderDefaultUISchema(property.Value) + } + parameter.Validate = &utils.Validate{} + parameter.Validate.DefaultValue = property.Value.Default + for _, enum := range property.Value.Enum { + parameter.Validate.Options = append(parameter.Validate.Options, utils.Option{Label: utils.RenderLabel(enum), Value: enum}) + } + parameter.JSONKey = key + parameter.Description = property.Value.Description + parameter.Label = label + parameter.UIType = utils.GetDefaultUIType(property.Value.Type, len(parameter.Validate.Options) != 0, subType) + parameter.Validate.Max = property.Value.Max + parameter.Validate.MaxLength = property.Value.MaxLength + parameter.Validate.Min = property.Value.Min + parameter.Validate.MinLength = property.Value.MinLength + parameter.Validate.Pattern = property.Value.Pattern + parameter.Validate.Required = utils.StringsContain(required, property.Value.Title) + parameter.Sort = 100 + return ¶meter +} diff --git a/pkg/apiserver/rest/usecase/definition_test.go b/pkg/apiserver/rest/usecase/definition_test.go new file mode 100644 index 000000000..fc61e110c --- /dev/null +++ b/pkg/apiserver/rest/usecase/definition_test.go @@ -0,0 +1,172 @@ +/* +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 usecase + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +var _ = Describe("Test namespace usecase functions", func() { + var ( + definitionUsecase *definitionUsecaseImpl + ) + + BeforeEach(func() { + definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: make(map[string]*utils.MemoryCache)} + err := k8sClient.Create(context.Background(), &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vela-system", + }, + }) + Expect(err).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + }) + It("Test ListDefinitions function", func() { + By("List component definitions") + webserver, err := ioutil.ReadFile("./testdata/webserver-cd.yaml") + Expect(err).Should(Succeed()) + var cd v1beta1.ComponentDefinition + err = yaml.Unmarshal(webserver, &cd) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &cd) + Expect(err).Should(Succeed()) + components, err := definitionUsecase.ListDefinitions(context.TODO(), "", "component") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(components), 1)).Should(BeEmpty()) + Expect(cmp.Diff(components[0].Name, "webservice-test")).Should(BeEmpty()) + Expect(components[0].Description).ShouldNot(BeEmpty()) + + By("List trait definitions") + myingress, err := ioutil.ReadFile("./testdata/myingress-td.yaml") + Expect(err).Should(Succeed()) + var td v1beta1.TraitDefinition + err = yaml.Unmarshal(myingress, &td) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &td) + Expect(err).Should(Succeed()) + traits, err := definitionUsecase.ListDefinitions(context.TODO(), "", "trait") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(traits), 1)).Should(BeEmpty()) + Expect(cmp.Diff(traits[0].Name, "myingress")).Should(BeEmpty()) + Expect(traits[0].Description).ShouldNot(BeEmpty()) + + By("List workflow step definitions") + step, err := ioutil.ReadFile("./testdata/applyapplication-sd.yaml") + Expect(err).Should(Succeed()) + var sd v1beta1.WorkflowStepDefinition + err = yaml.Unmarshal(step, &sd) + Expect(err).Should(Succeed()) + err = k8sClient.Create(context.Background(), &sd) + Expect(err).Should(Succeed()) + wfstep, err := definitionUsecase.ListDefinitions(context.TODO(), "", "workflowstep") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(wfstep), 1)).Should(BeEmpty()) + Expect(cmp.Diff(wfstep[0].Name, "apply-application")).Should(BeEmpty()) + Expect(wfstep[0].Description).ShouldNot(BeEmpty()) + }) + + It("Test DetailDefinition function", func() { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "workflowstep-schema-apply-object", + Namespace: "vela-system", + }, + Data: map[string]string{ + types.OpenapiV3JSONSchema: `{"properties":{"batchPartition":{"title":"batchPartition","type":"integer"},"volumes": {"description":"Specify volume type, options: pvc, configMap, secret, emptyDir","enum":["pvc","configMap","secret","emptyDir"],"title":"volumes","type":"string"}, "rolloutBatches":{"items":{"properties":{"replicas":{"title":"replicas","type":"integer"}},"required":["replicas"],"type":"object"},"title":"rolloutBatches","type":"array"},"targetRevision":{"title":"targetRevision","type":"string"},"targetSize":{"title":"targetSize","type":"integer"}},"required":["targetRevision","targetSize"],"type":"object"}`, + }, + } + err := k8sClient.Create(context.Background(), cm) + Expect(err).Should(Succeed()) + schema, err := definitionUsecase.DetailDefinition(context.TODO(), "apply-object", "workflowstep") + Expect(err).Should(Succeed()) + + schemaFromCM := &openapi3.Schema{} + err = schemaFromCM.UnmarshalJSON([]byte(cm.Data["openapi-v3-json-schema"])) + Expect(err).Should(Succeed()) + + Expect(schema.APISchema).Should(Equal(schemaFromCM)) + }) + + It("Test renderDefaultUISchema", func() { + schema := &v1.DetailDefinitionResponse{} + data, err := ioutil.ReadFile("./testdata/api-schema.json") + Expect(err).Should(Succeed()) + err = json.Unmarshal(data, schema) + Expect(err).Should(Succeed()) + Expect(cmp.Diff(len(schema.APISchema.Required), 3)).Should(BeEmpty()) + uiSchema := renderDefaultUISchema(schema.APISchema) + Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) + }) + + It("Test patchSchema", func() { + ddr := &v1.DetailDefinitionResponse{} + data, err := ioutil.ReadFile("./testdata/api-schema.json") + Expect(err).Should(Succeed()) + err = json.Unmarshal(data, ddr) + Expect(err).Should(Succeed()) + Expect(cmp.Diff(len(ddr.APISchema.Required), 3)).Should(BeEmpty()) + defaultschema := renderDefaultUISchema(ddr.APISchema) + + customschema := []*utils.UIParameter{} + cdata, err := ioutil.ReadFile("./testdata/ui-custom-schema.yaml") + Expect(err).Should(Succeed()) + err = yaml.Unmarshal(cdata, &customschema) + Expect(err).Should(Succeed()) + + uiSchema := patchSchema(defaultschema, customschema) + for _, schema := range uiSchema { + fmt.Printf("%s=> %d", schema.JSONKey, schema.Sort) + } + Expect(cmp.Diff(len(uiSchema), 12)).Should(BeEmpty()) + Expect(cmp.Diff(uiSchema[7].JSONKey, "readinessProbe")).Should(BeEmpty()) + Expect(cmp.Diff(len(uiSchema[7].SubParameters), 8)).Should(BeEmpty()) + + outdata, err := yaml.Marshal(uiSchema) + Expect(err).Should(Succeed()) + err = ioutil.WriteFile("./testdata/ui-schema.yaml", outdata, 0755) + Expect(err).Should(Succeed()) + }) +}) + +func TestAddDefinitionUISchema(t *testing.T) { + du := NewDefinitionUsecase() + cdata, err := ioutil.ReadFile("./testdata/ui-custom-schema.yaml") + if err != nil { + t.Fatal(err) + } + _, err = du.AddDefinitionUISchema(context.TODO(), "webservice", "component", string(cdata)) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/apiserver/rest/usecase/delivery_target.go b/pkg/apiserver/rest/usecase/delivery_target.go new file mode 100644 index 000000000..cbb25dd59 --- /dev/null +++ b/pkg/apiserver/rest/usecase/delivery_target.go @@ -0,0 +1,170 @@ +/* +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 usecase + +import ( + "context" + "errors" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// DeliveryTargetUsecase deliveryTarget manage api +type DeliveryTargetUsecase interface { + GetDeliveryTarget(ctx context.Context, deliveryTargetName string) (*model.DeliveryTarget, error) + DetailDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget) (*apisv1.DetailDeliveryTargetResponse, error) + DeleteDeliveryTarget(ctx context.Context, deliveryTargetName string) error + CreateDeliveryTarget(ctx context.Context, req apisv1.CreateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) + UpdateDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) + ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) +} + +type deliveryTargetUsecaseImpl struct { + ds datastore.DataStore +} + +// NewDeliveryTargetUsecase new DeliveryTarget usecase +func NewDeliveryTargetUsecase(ds datastore.DataStore) DeliveryTargetUsecase { + return &deliveryTargetUsecaseImpl{ + ds: ds, + } +} + +func (dt *deliveryTargetUsecaseImpl) ListDeliveryTargets(ctx context.Context, page, pageSize int, namespace string) (*apisv1.ListDeliveryTargetResponse, error) { + deliveryTarget := model.DeliveryTarget{} + if namespace != "" { + deliveryTarget.Namespace = namespace + } + deliveryTargets, err := dt.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, err + } + + resp := &apisv1.ListDeliveryTargetResponse{ + DeliveryTargets: []apisv1.DeliveryTargetBase{}, + } + for _, raw := range deliveryTargets { + dt, ok := raw.(*model.DeliveryTarget) + if ok { + resp.DeliveryTargets = append(resp.DeliveryTargets, *convertFromDeliveryTargetModel(dt)) + } + } + count, err := dt.ds.Count(ctx, &deliveryTarget, nil) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +// DeleteDeliveryTarget delete application DeliveryTarget +func (dt *deliveryTargetUsecaseImpl) DeleteDeliveryTarget(ctx context.Context, deliveryTargetName string) error { + deliveryTarget := &model.DeliveryTarget{ + Name: deliveryTargetName, + } + if err := dt.ds.Delete(ctx, deliveryTarget); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrDeliveryTargetNotExist + } + return err + } + return nil +} + +func (dt *deliveryTargetUsecaseImpl) CreateDeliveryTarget(ctx context.Context, req apisv1.CreateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) { + deliveryTarget := convertCreateReqToDeliveryTargetModel(req) + // check deliveryTarget name. + exit, err := dt.ds.IsExist(ctx, &deliveryTarget) + if err != nil { + log.Logger.Errorf("check application name is exist failure %s", err.Error()) + return nil, bcode.ErrDeliveryTargetExist + } + if exit { + return nil, bcode.ErrDeliveryTargetExist + } + if err := dt.ds.Add(ctx, &deliveryTarget); err != nil { + return nil, err + } + return dt.DetailDeliveryTarget(ctx, &deliveryTarget) +} + +func (dt *deliveryTargetUsecaseImpl) UpdateDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) (*apisv1.DetailDeliveryTargetResponse, error) { + deliveryTargetModel := convertUpdateReqToDeliveryTargetModel(deliveryTarget, req) + if err := dt.ds.Put(ctx, deliveryTargetModel); err != nil { + return nil, err + } + return dt.DetailDeliveryTarget(ctx, deliveryTargetModel) +} + +// DetailDeliveryTarget detail DeliveryTarget +func (dt *deliveryTargetUsecaseImpl) DetailDeliveryTarget(ctx context.Context, deliveryTarget *model.DeliveryTarget) (*apisv1.DetailDeliveryTargetResponse, error) { + return &apisv1.DetailDeliveryTargetResponse{ + DeliveryTargetBase: *convertFromDeliveryTargetModel(deliveryTarget), + }, nil +} + +// GetDeliveryTarget get DeliveryTarget model +func (dt *deliveryTargetUsecaseImpl) GetDeliveryTarget(ctx context.Context, deliveryTargetName string) (*model.DeliveryTarget, error) { + deliveryTarget := &model.DeliveryTarget{ + Name: deliveryTargetName, + } + if err := dt.ds.Get(ctx, deliveryTarget); err != nil { + return nil, err + } + return deliveryTarget, nil +} + +func convertUpdateReqToDeliveryTargetModel(deliveryTarget *model.DeliveryTarget, req apisv1.UpdateDeliveryTargetRequest) *model.DeliveryTarget { + deliveryTarget.Alias = req.Alias + deliveryTarget.Description = req.Description + deliveryTarget.Cluster = (*model.ClusterTarget)(req.Cluster) + deliveryTarget.Variable = req.Variable + return deliveryTarget +} + +func convertCreateReqToDeliveryTargetModel(req apisv1.CreateDeliveryTargetRequest) model.DeliveryTarget { + deliveryTarget := model.DeliveryTarget{ + Name: req.Name, + Namespace: req.Namespace, + Alias: req.Alias, + Description: req.Description, + Cluster: (*model.ClusterTarget)(req.Cluster), + Variable: req.Variable, + } + return deliveryTarget +} + +func convertFromDeliveryTargetModel(deliveryTarget *model.DeliveryTarget) *apisv1.DeliveryTargetBase { + var appNum int64 = 0 + // TODO: query app num in target + return &apisv1.DeliveryTargetBase{ + Name: deliveryTarget.Name, + Namespace: deliveryTarget.Namespace, + Alias: deliveryTarget.Alias, + Description: deliveryTarget.Description, + Cluster: (*apisv1.ClusterTarget)(deliveryTarget.Cluster), + Variable: deliveryTarget.Variable, + CreateTime: deliveryTarget.CreateTime, + UpdateTime: deliveryTarget.UpdateTime, + AppNum: appNum, + } +} diff --git a/pkg/apiserver/rest/usecase/delivery_target_test.go b/pkg/apiserver/rest/usecase/delivery_target_test.go new file mode 100644 index 000000000..78f466136 --- /dev/null +++ b/pkg/apiserver/rest/usecase/delivery_target_test.go @@ -0,0 +1,76 @@ +/* +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 usecase + +import ( + "context" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test delivery target usecase functions", func() { + var ( + deliveryTargetUsecase *deliveryTargetUsecaseImpl + ) + BeforeEach(func() { + deliveryTargetUsecase = &deliveryTargetUsecaseImpl{ds: ds} + }) + It("Test CreateDeliveryTarget function", func() { + req := apisv1.CreateDeliveryTargetRequest{ + Name: "test-delivery-target", + Namespace: "test-namespace", + Alias: "test-alias", + Description: "this is a deliveryTarget", + Cluster: &apisv1.ClusterTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Variable: map[string]interface{}{"terraform-provider": "provider", "region": "us-1"}, + } + base, err := deliveryTargetUsecase.CreateDeliveryTarget(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + }) + + It("Test GetDeliveryTarget function", func() { + deliveryTarget, err := deliveryTargetUsecase.GetDeliveryTarget(context.TODO(), "test-delivery-target") + Expect(err).Should(BeNil()) + Expect(deliveryTarget).ShouldNot(BeNil()) + Expect(cmp.Diff(deliveryTarget.Name, "test-delivery-target")).Should(BeEmpty()) + }) + + It("Test ListDeliveryTargets function", func() { + _, err := deliveryTargetUsecase.ListDeliveryTargets(context.TODO(), 1, 1, "") + Expect(err).Should(BeNil()) + }) + + It("Test DetailDeliveryTarget function", func() { + detail, err := deliveryTargetUsecase.DetailDeliveryTarget(context.TODO(), + &model.DeliveryTarget{ + Name: "test-delivery-target", + Namespace: "test-namespace", + Alias: "test-alias", + Description: "this is a deliveryTarget", + Cluster: &model.ClusterTarget{ClusterName: "cluster-dev", Namespace: "dev"}, + Variable: map[string]interface{}{"terraform-provider": "provider", "region": "us-1"}}) + Expect(err).Should(BeNil()) + Expect(detail.Name).Should(Equal("test-delivery-target")) + }) +}) diff --git a/pkg/apiserver/rest/usecase/envbinding.go b/pkg/apiserver/rest/usecase/envbinding.go new file mode 100644 index 000000000..1ff122f2d --- /dev/null +++ b/pkg/apiserver/rest/usecase/envbinding.go @@ -0,0 +1,388 @@ +/* +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 usecase + +import ( + "context" + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam/util" +) + +// EnvBindingUsecase envbinding usecase +type EnvBindingUsecase interface { + GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) + GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) + CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) + CreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) + BatchCreateEnvBinding(ctx context.Context, app *model.Application, env apisv1.EnvBindingList) error + UpdateEnvBinding(ctx context.Context, app *model.Application, envName string, diff apisv1.PutApplicationEnvRequest) (*apisv1.DetailEnvBindingResponse, error) + DeleteEnvBinding(ctx context.Context, app *model.Application, envName string) error + BatchDeleteEnvBinding(ctx context.Context, app *model.Application) error + DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) + ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error +} + +type envBindingUsecaseImpl struct { + ds datastore.DataStore + workflowUsecase WorkflowUsecase + kubeClient client.Client +} + +// NewEnvBindingUsecase new envBinding usecase +func NewEnvBindingUsecase(ds datastore.DataStore, workflowUsecase WorkflowUsecase) EnvBindingUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &envBindingUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + kubeClient: kubecli, + } +} + +func (e *envBindingUsecaseImpl) GetEnvBindings(ctx context.Context, app *model.Application) ([]*apisv1.EnvBindingBase, error) { + var envBinding = model.EnvBinding{ + AppPrimaryKey: app.PrimaryKey(), + } + envBindings, err := e.ds.List(ctx, &envBinding, &datastore.ListOptions{}) + if err != nil { + return nil, bcode.ErrEnvBindingsNotExist + } + deliveryTarget := model.DeliveryTarget{ + Namespace: app.Namespace, + } + deliveryTargets, err := e.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.EnvBindingBase + for _, ebd := range envBindings { + eb := ebd.(*model.EnvBinding) + list = append(list, convertEnvbindingModelToBase(app, eb, deliveryTargets)) + } + return list, nil +} + +func (e *envBindingUsecaseImpl) GetEnvBinding(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrEnvBindingsNotExist + } + return nil, err + } + return envBinding, nil +} + +func (e *envBindingUsecaseImpl) CheckAppEnvBindingsContainTarget(ctx context.Context, app *model.Application, targetName string) (bool, error) { + envBindings, err := e.GetEnvBindings(ctx, app) + if err != nil { + return false, err + } + var filteredList []*apisv1.EnvBindingBase + for _, envBinding := range envBindings { + if utils.StringsContain(envBinding.TargetNames, targetName) { + filteredList = append(filteredList, envBinding) + } + } + return len(filteredList) > 0, nil +} + +func (e *envBindingUsecaseImpl) CreateEnvBinding(ctx context.Context, app *model.Application, envReq apisv1.CreateApplicationEnvRequest) (*apisv1.EnvBinding, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envReq.Name) + if err != nil { + if !errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } + } + if envBinding != nil { + return nil, bcode.ErrEnvBindingExist + } + envBindingModel := convertCreateReqToEnvBindingModel(app, envReq) + if err := e.ds.Add(ctx, &envBindingModel); err != nil { + return nil, err + } + err = e.createEnvWorkflow(ctx, app, &envBindingModel) + if err != nil { + return nil, err + } + return &envReq.EnvBinding, nil +} + +func (e *envBindingUsecaseImpl) BatchCreateEnvBinding(ctx context.Context, app *model.Application, envbindings apisv1.EnvBindingList) error { + for _, envBinding := range envbindings { + envBindingModel := convertToEnvBindingModel(app, *envBinding) + if err := e.ds.Add(ctx, envBindingModel); err != nil { + return err + } + err := e.createEnvWorkflow(ctx, app, envBindingModel) + if err != nil { + return err + } + } + return nil +} + +func (e *envBindingUsecaseImpl) getBindingByEnv(ctx context.Context, app *model.Application, envName string) (*model.EnvBinding, error) { + var envBinding = model.EnvBinding{ + AppPrimaryKey: app.PrimaryKey(), + Name: envName, + } + err := e.ds.Get(ctx, &envBinding) + if err != nil { + return nil, err + } + return &envBinding, nil +} + +func (e *envBindingUsecaseImpl) UpdateEnvBinding(ctx context.Context, app *model.Application, envName string, envUpdate apisv1.PutApplicationEnvRequest) (*apisv1.DetailEnvBindingResponse, error) { + envBinding, err := e.getBindingByEnv(ctx, app, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrEnvBindingNotExist + } + return nil, err + } + convertUpdateReqToEnvBindingModel(envBinding, envUpdate) + //update env + if err := e.ds.Put(ctx, envBinding); err != nil { + return nil, err + } + //update env workflow + if err := e.updateEnvWorkflow(ctx, app, envBinding); err != nil { + return nil, bcode.ErrEnvBindingUpdateWorkflow + } + return e.DetailEnvBinding(ctx, app, envBinding) +} + +func (e *envBindingUsecaseImpl) DeleteEnvBinding(ctx context.Context, appModel *model.Application, envName string) error { + envBinding, err := e.getBindingByEnv(ctx, appModel, envName) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrEnvBindingNotExist + } + return err + } + var app v1beta1.Application + err = e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) + if err == nil || !apierrors.IsNotFound(err) { + return bcode.ErrApplicationEnvRefusedDelete + } + if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: appModel.PrimaryKey(), Name: envBinding.Name}); err != nil { + return err + } + //delete env workflow + if err := e.deleteEnvWorkflow(ctx, appModel, envBinding.Name); err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) BatchDeleteEnvBinding(ctx context.Context, app *model.Application) error { + envBindings, err := e.GetEnvBindings(ctx, app) + if err != nil { + return err + } + for _, envBinding := range envBindings { + //delete env + if err := e.ds.Delete(ctx, &model.EnvBinding{AppPrimaryKey: app.PrimaryKey(), Name: envBinding.Name}); err != nil { + return err + } + //delete env workflow + err := e.deleteEnvWorkflow(ctx, app, envBinding.Name) + if err != nil { + return err + } + } + return nil +} + +func (e *envBindingUsecaseImpl) createEnvWorkflow(ctx context.Context, app *model.Application, env *model.EnvBinding) error { + steps := genEnvWorkflowSteps(env, app) + _, err := e.workflowUsecase.CreateOrUpdateWorkflow(ctx, app, apisv1.CreateWorkflowRequest{ + AppName: app.PrimaryKey(), + Name: env.Name, + Alias: fmt.Sprintf("%s env workflow", env.Alias), + Description: "Created automatically by envbinding.", + EnvName: env.Name, + Steps: steps, + Default: false, + }) + if err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) updateEnvWorkflow(ctx context.Context, app *model.Application, env *model.EnvBinding) error { + steps := genEnvWorkflowSteps(env, app) + workflow, err := e.workflowUsecase.GetWorkflow(ctx, app, env.Name) + if err != nil { + return err + } + _, err = e.workflowUsecase.UpdateWorkflow(ctx, workflow, apisv1.UpdateWorkflowRequest{ + Steps: steps, + Description: workflow.Description, + EnvName: workflow.EnvName, + }) + if err != nil { + return err + } + return nil +} + +func (e *envBindingUsecaseImpl) deleteEnvWorkflow(ctx context.Context, app *model.Application, workflowName string) error { + return e.workflowUsecase.DeleteWorkflow(ctx, app, workflowName) +} + +func (e *envBindingUsecaseImpl) DetailEnvBinding(ctx context.Context, app *model.Application, envBinding *model.EnvBinding) (*apisv1.DetailEnvBindingResponse, error) { + deliveryTarget := model.DeliveryTarget{ + Namespace: app.Namespace, + } + deliveryTargets, err := e.ds.List(ctx, &deliveryTarget, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + return &apisv1.DetailEnvBindingResponse{ + EnvBindingBase: *convertEnvbindingModelToBase(app, envBinding, deliveryTargets), + }, nil +} + +func (e *envBindingUsecaseImpl) ApplicationEnvRecycle(ctx context.Context, appModel *model.Application, envBinding *model.EnvBinding) error { + var app v1beta1.Application + err := e.kubeClient.Get(ctx, types.NamespacedName{Namespace: appModel.Namespace, Name: convertAppName(appModel.Name, envBinding.Name)}, &app) + if err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + return e.kubeClient.Delete(ctx, &app) +} + +func convertCreateReqToEnvBindingModel(app *model.Application, req apisv1.CreateApplicationEnvRequest) model.EnvBinding { + envBinding := model.EnvBinding{ + AppPrimaryKey: app.Name, + Name: req.Name, + Alias: req.Alias, + Description: req.Description, + TargetNames: req.TargetNames, + } + return envBinding +} + +func convertEnvbindingModelToBase(app *model.Application, envBinding *model.EnvBinding, deliveryTargets []datastore.Entity) *apisv1.EnvBindingBase { + var dtMap = make(map[string]*model.DeliveryTarget, len(deliveryTargets)) + for _, dte := range deliveryTargets { + dt := dte.(*model.DeliveryTarget) + dtMap[dt.Name] = dt + } + var targets []apisv1.DeliveryTargetBase + for _, targetName := range envBinding.TargetNames { + dt := dtMap[targetName] + if dt != nil { + targets = append(targets, *convertFromDeliveryTargetModel(dt)) + } + } + ebb := &apisv1.EnvBindingBase{ + Name: envBinding.Name, + Alias: envBinding.Alias, + Description: envBinding.Description, + TargetNames: envBinding.TargetNames, + Targets: targets, + ComponentSelector: (*apisv1.ComponentSelector)(envBinding.ComponentSelector), + CreateTime: envBinding.CreateTime, + UpdateTime: envBinding.UpdateTime, + AppDeployName: convertAppName(app.Name, envBinding.Name), + } + return ebb +} + +func convertUpdateReqToEnvBindingModel(envBinding *model.EnvBinding, envUpdate apisv1.PutApplicationEnvRequest) *model.EnvBinding { + envBinding.Alias = envUpdate.Alias + envBinding.Description = envUpdate.Description + envBinding.TargetNames = envUpdate.TargetNames + if envUpdate.ComponentSelector != nil { + envBinding.ComponentSelector = (*model.ComponentSelector)(envUpdate.ComponentSelector) + } + return envBinding +} + +func convertToEnvBindingModel(app *model.Application, envBind apisv1.EnvBinding) *model.EnvBinding { + re := model.EnvBinding{ + AppPrimaryKey: app.Name, + Name: envBind.Name, + Description: envBind.Description, + Alias: envBind.Alias, + TargetNames: envBind.TargetNames, + } + if envBind.ComponentSelector != nil { + re.ComponentSelector = (*model.ComponentSelector)(envBind.ComponentSelector) + } + return &re +} + +func genEnvWorkflowSteps(env *model.EnvBinding, app *model.Application) []apisv1.WorkflowStep { + var workflowSteps []v1beta1.WorkflowStep + for _, targetName := range env.TargetNames { + step := v1beta1.WorkflowStep{ + Name: genPolicyEnvName(targetName), + Type: "deploy2env", + Properties: util.Object2RawExtension(map[string]string{ + "policy": genPolicyName(env.Name), + "env": genPolicyEnvName(targetName), + }), + } + workflowSteps = append(workflowSteps, step) + } + var steps []apisv1.WorkflowStep + for _, step := range workflowSteps { + var propertyStr string + if step.Properties != nil { + properties, err := model.NewJSONStruct(step.Properties) + if err != nil { + log.Logger.Errorf("workflow %s step %s properties is invalid %s", app.Name, step.Name, err.Error()) + continue + } + propertyStr = properties.JSON() + } + steps = append(steps, apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + DependsOn: step.DependsOn, + Properties: propertyStr, + Inputs: step.Inputs, + Outputs: step.Outputs, + }) + } + return steps +} diff --git a/pkg/apiserver/rest/usecase/envbinding_test.go b/pkg/apiserver/rest/usecase/envbinding_test.go new file mode 100644 index 000000000..0e8c1c29a --- /dev/null +++ b/pkg/apiserver/rest/usecase/envbinding_test.go @@ -0,0 +1,137 @@ +/* +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 usecase + +import ( + "context" + "github.com/google/go-cmp/cmp" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test envBindingUsecase functions", func() { + var ( + envBindingUsecase *envBindingUsecaseImpl + workflowUsecase *workflowUsecaseImpl + envBindingDemo1 apisv1.EnvBinding + envBindingDemo2 apisv1.EnvBinding + testApp *model.Application + ) + BeforeEach(func() { + testApp = &model.Application{ + Name: "test-app-env", + Namespace: "default", + } + workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient} + envBindingUsecase = &envBindingUsecaseImpl{ds: ds, workflowUsecase: workflowUsecase, kubeClient: k8sClient} + envBindingDemo1 = apisv1.EnvBinding{ + Name: "dev", + Alias: "dev alias", + TargetNames: []string{"dev-target"}, + } + envBindingDemo2 = apisv1.EnvBinding{ + Name: "prod", + Alias: "prod alias", + TargetNames: []string{"prod-target"}, + } + }) + + It("Test Create Application Env function", func() { + By("create two envbinding") + req := apisv1.CreateApplicationEnvRequest{EnvBinding: envBindingDemo1} + base, err := envBindingUsecase.CreateEnvBinding(context.TODO(), testApp, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + req = apisv1.CreateApplicationEnvRequest{EnvBinding: envBindingDemo2} + base, err = envBindingUsecase.CreateEnvBinding(context.TODO(), testApp, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + By("auto create two workflow") + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "dev-target")).Should(BeEmpty()) + + workflow, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "prod-target")).Should(BeEmpty()) + }) + + It("Test GetApplication Envs function", func() { + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(envBindings).ShouldNot(BeNil()) + Expect(cmp.Diff(len(envBindings), 2)).Should(BeEmpty()) + }) + + It("Test GetApplication Env function", func() { + envBinding, err := envBindingUsecase.GetEnvBinding(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + Expect(envBinding).ShouldNot(BeNil()) + Expect(cmp.Diff(envBinding.Name, "dev")).Should(BeEmpty()) + }) + + It("Test CheckAppEnvBindingsContainTarget function", func() { + isContain, err := envBindingUsecase.CheckAppEnvBindingsContainTarget(context.TODO(), testApp, "dev-target") + Expect(err).Should(BeNil()) + Expect(isContain).ShouldNot(BeNil()) + Expect(cmp.Diff(isContain, true)).Should(BeEmpty()) + }) + + It("Test Application UpdateEnv function", func() { + envBinding, err := envBindingUsecase.UpdateEnvBinding(context.TODO(), testApp, "prod", apisv1.PutApplicationEnvRequest{ + TargetNames: []string{"prod-target-new1"}, + }) + + Expect(envBinding).ShouldNot(BeNil()) + Expect(cmp.Diff(envBinding.TargetNames[0], "prod-target-new1")).Should(BeEmpty()) + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + Expect(cmp.Diff(workflow.Steps[0].Name, "prod-target-new1")).Should(BeEmpty()) + }) + + It("Test Application DeleteEnv function", func() { + err := envBindingUsecase.DeleteEnvBinding(context.TODO(), testApp, "dev") + Expect(err).Should(BeNil()) + _, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "dev") + Expect(err).ShouldNot(BeNil()) + err = envBindingUsecase.DeleteEnvBinding(context.TODO(), testApp, "prod") + Expect(err).Should(BeNil()) + _, err = workflowUsecase.GetWorkflow(context.TODO(), testApp, "prod") + Expect(err).ShouldNot(BeNil()) + }) + + It("Test Application BatchCreateEnv function", func() { + err := envBindingUsecase.BatchCreateEnvBinding(context.TODO(), testApp, apisv1.EnvBindingList{&envBindingDemo1, &envBindingDemo2}) + Expect(err).Should(BeNil()) + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(envBindings), 2)).Should(BeEmpty()) + }) + + It("Test BatchDeleteEnvBinding function", func() { + err := envBindingUsecase.BatchDeleteEnvBinding(context.TODO(), testApp) + Expect(err).Should(BeNil()) + envBindings, err := envBindingUsecase.GetEnvBindings(context.TODO(), testApp) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(envBindings), 0)).Should(BeEmpty()) + }) + +}) diff --git a/pkg/apiserver/rest/usecase/namespace.go b/pkg/apiserver/rest/usecase/namespace.go new file mode 100644 index 000000000..7f7fdf4a1 --- /dev/null +++ b/pkg/apiserver/rest/usecase/namespace.go @@ -0,0 +1,106 @@ +/* +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 usecase + +import ( + "context" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NamespaceUsecase namespace manage usecase. +// Namespace acts as the tenant isolation model on the control side. +type NamespaceUsecase interface { + ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error) + CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error) +} + +// AnnotationDescription set namespace description in annotation +const AnnotationDescription string = "description" + +// LabelCreator set namesapce creator in labels +const LabelCreator string = "creator" + +type namespaceUsecaseImpl struct { + kubeClient client.Client +} + +// NewNamespaceUsecase new namespace usecase +func NewNamespaceUsecase() NamespaceUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &namespaceUsecaseImpl{kubeClient: kubecli} +} + +// ListNamespaces list controller cluster namespaces +func (n *namespaceUsecaseImpl) ListNamespaces(ctx context.Context) ([]apisv1.NamespaceBase, error) { + + // TODO: Consider whether to query only namespaces created by Vela + var kubeNamespaces corev1.NamespaceList + if err := n.kubeClient.List(ctx, &kubeNamespaces, &client.ListOptions{}); err != nil { + log.Logger.Errorf("query namespace list from cluster failure %s", err.Error()) + return nil, bcode.ErrNamespaceQuery + } + var namespaces []apisv1.NamespaceBase + for _, namesapce := range kubeNamespaces.Items { + namespaces = append(namespaces, apisv1.NamespaceBase{ + Name: namesapce.Name, + Description: namesapce.Annotations[AnnotationDescription], + CreateTime: namesapce.CreationTimestamp.Time, + UpdateTime: namesapce.CreationTimestamp.Time, + }) + } + return namespaces, nil +} + +// CreateNamespace create namespace to controller cluster +func (n *namespaceUsecaseImpl) CreateNamespace(ctx context.Context, req apisv1.CreateNamespaceRequest) (*apisv1.NamespaceBase, error) { + if err := n.kubeClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.Name, + Labels: map[string]string{ + LabelCreator: "kubevela", + }, + Annotations: map[string]string{ + AnnotationDescription: req.Description, + }, + }, + Spec: corev1.NamespaceSpec{}, + }); err != nil { + if apierrors.IsAlreadyExists(err) { + return nil, bcode.ErrNamespaceIsExist + } + return nil, err + } + return &apisv1.NamespaceBase{ + Name: req.Name, + Description: req.Description, + CreateTime: time.Now(), + UpdateTime: time.Now(), + }, nil +} diff --git a/pkg/apiserver/rest/usecase/namespace_test.go b/pkg/apiserver/rest/usecase/namespace_test.go new file mode 100644 index 000000000..ff6e11d2b --- /dev/null +++ b/pkg/apiserver/rest/usecase/namespace_test.go @@ -0,0 +1,50 @@ +/* +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 usecase + +import ( + "context" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test namespace usecase functions", func() { + var ( + namespaceUsecase *namespaceUsecaseImpl + ) + BeforeEach(func() { + namespaceUsecase = &namespaceUsecaseImpl{kubeClient: k8sClient} + }) + It("Test CreateNamespace function", func() { + req := apisv1.CreateNamespaceRequest{ + Name: "test-namespace", + Description: "this is a namespace description 王二", + } + base, err := namespaceUsecase.CreateNamespace(context.TODO(), req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req.Description)).Should(BeEmpty()) + }) + + It("Test ListNamespace function", func() { + _, err := namespaceUsecase.ListNamespaces(context.TODO()) + Expect(err).Should(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/usecase/oam_application.go b/pkg/apiserver/rest/usecase/oam_application.go new file mode 100644 index 000000000..6005c9f3d --- /dev/null +++ b/pkg/apiserver/rest/usecase/oam_application.go @@ -0,0 +1,109 @@ +/* + 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 usecase + +import ( + "context" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +// OAMApplicationUsecase oam_application usecase +type OAMApplicationUsecase interface { + CreateOrUpdateOAMApplication(context.Context, apisv1.ApplicationRequest, string, string) error + GetOAMApplication(context.Context, string, string) (*apisv1.ApplicationResponse, error) + DeleteOAMApplication(context.Context, string, string) error +} + +// NewOAMApplicationUsecase new oam_application usecase +func NewOAMApplicationUsecase() OAMApplicationUsecase { + kubeClient, _ := clients.GetKubeClient() + return &oamApplicationUsecaseImpl{kubeClient: kubeClient} +} + +type oamApplicationUsecaseImpl struct { + kubeClient client.Client +} + +// CreateOrUpdateOAMApplication create or update application +func (o oamApplicationUsecaseImpl) CreateOrUpdateOAMApplication(ctx context.Context, request apisv1.ApplicationRequest, name, namespace string) error { + ns := new(v1.Namespace) + err := o.kubeClient.Get(ctx, client.ObjectKey{Name: namespace}, ns) + if kerrors.IsNotFound(err) { + ns.Name = namespace + if err = o.kubeClient.Create(ctx, ns); err != nil { + return err + } + } + + app := &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: request.Components, + Policies: request.Policies, + Workflow: request.Workflow, + }, + } + + existApp := new(v1beta1.Application) + err = o.kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, existApp) + if err != nil { + if kerrors.IsNotFound(err) { + return o.kubeClient.Create(ctx, app) + } + return err + } + + existApp.Spec = app.Spec + return o.kubeClient.Update(ctx, existApp) +} + +// GetOAMApplication get application +func (o oamApplicationUsecaseImpl) GetOAMApplication(ctx context.Context, name, namespace string) (*apisv1.ApplicationResponse, error) { + app := new(v1beta1.Application) + if err := o.kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, app); err != nil { + return nil, err + } + return &apisv1.ApplicationResponse{ + APIVersion: app.APIVersion, + Kind: app.Kind, + Spec: app.Spec, + Status: app.Status, + }, nil +} + +// DeleteOAMApplication delete application +func (o oamApplicationUsecaseImpl) DeleteOAMApplication(ctx context.Context, name, namespace string) error { + return client.IgnoreNotFound(o.kubeClient.Delete(ctx, &v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + })) +} diff --git a/pkg/apiserver/rest/usecase/oam_application_test.go b/pkg/apiserver/rest/usecase/oam_application_test.go new file mode 100644 index 000000000..a8fff0087 --- /dev/null +++ b/pkg/apiserver/rest/usecase/oam_application_test.go @@ -0,0 +1,120 @@ +/* + 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 usecase + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test oam application usecase function", func() { + var oamAppUsecase *oamApplicationUsecaseImpl + var ctx context.Context + var baseApp v1beta1.Application + var ns corev1.Namespace + var namespace string + + BeforeEach(func() { + ctx = context.Background() + namespace = randomNamespaceName("test-oam-app") + ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + oamAppUsecase = &oamApplicationUsecaseImpl{ + kubeClient: k8sClient, + } + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &baseApp)).Should(BeNil()) + + Eventually(func() error { + return k8sClient.Create(ctx, &ns) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + baseApp.SetNamespace(namespace) + Eventually(func() error { + return k8sClient.Create(ctx, &baseApp) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + }) + + AfterEach(func() { + By("Clean up resources after a test") + k8sClient.DeleteAllOf(ctx, &v1beta1.Application{}, client.InNamespace(namespace)) + baseApp = v1beta1.Application{} + By(fmt.Sprintf("Delete the entire namespaceName %s", ns.Name)) + Expect(k8sClient.Delete(ctx, &ns, client.PropagationPolicy(metav1.DeletePropagationForeground))).Should(Succeed()) + }) + + It("Test CreateOrUpdateOAMApplication function", func() { + By("test create application") + appName := "test-new-app" + appNs := randomNamespaceName("test-new-app") + req := apiv1.ApplicationRequest{ + Components: baseApp.Spec.Components, + Policies: baseApp.Spec.Policies, + Workflow: baseApp.Spec.Workflow, + } + Expect(oamAppUsecase.CreateOrUpdateOAMApplication(ctx, req, appName, appNs)).Should(BeNil()) + + app := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: appNs, Name: appName}, app)).Should(BeNil()) + Expect(app.Spec.Components).Should(Equal(req.Components)) + Expect(app.Spec.Policies).Should(Equal(req.Policies)) + Expect(app.Spec.Workflow).Should(Equal(req.Workflow)) + + By("test update application") + updateReq := apiv1.ApplicationRequest{ + Components: baseApp.Spec.Components[1:], + } + Expect(oamAppUsecase.CreateOrUpdateOAMApplication(ctx, updateReq, appName, appNs)).Should(BeNil()) + + updatedApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: appNs, Name: appName}, updatedApp)).Should(BeNil()) + Expect(updatedApp.Spec.Components).Should(Equal(updateReq.Components)) + Expect(updatedApp.Spec.Policies).Should(BeNil()) + Expect(updatedApp.Spec.Workflow).Should(BeNil()) + }) + + It("Test GetOAMApplication function", func() { + By("test get an existed application") + resp, err := oamAppUsecase.GetOAMApplication(ctx, baseApp.Name, namespace) + Expect(err).Should(BeNil()) + + Expect(resp.Spec.Components).Should(Equal(baseApp.Spec.Components)) + Expect(resp.Spec.Policies).Should(Equal(baseApp.Spec.Policies)) + Expect(resp.Spec.Workflow).Should(Equal(baseApp.Spec.Workflow)) + }) + + It("Test DeleteOAMApplication function", func() { + By("test delete application") + app := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: baseApp.Name}, app)).Should(BeNil()) + + Expect(oamAppUsecase.DeleteOAMApplication(ctx, baseApp.Name, namespace)).Should(BeNil()) + err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: baseApp.Name}, app) + Expect(kerrors.IsNotFound(err)).Should(BeTrue()) + }) +}) diff --git a/pkg/apiserver/rest/usecase/testdata/api-schema.json b/pkg/apiserver/rest/usecase/testdata/api-schema.json new file mode 100644 index 000000000..0ba443081 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/api-schema.json @@ -0,0 +1,386 @@ +{ + "schema": { + "properties": { + "addRevisionLabel": { + "type": "boolean", + "default": false, + "description": "If addRevisionLabel is true, the appRevision label will be added to the underlying pods", + "title": "addRevisionLabel" + }, + "cmd": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Commands to run in the container", + "title": "cmd" + }, + "cpu": { + "type": "string", + "description": "Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)", + "title": "cpu" + }, + "env": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "title": "name" + }, + "value": { + "type": "string", + "description": "The value of the environment variable", + "title": "value" + }, + "valueFrom": { + "properties": { + "secretKeyRef": { + "properties": { + "key": { + "type": "string", + "description": "The key of the secret to select from. Must be a valid secret key", + "title": "key" + }, + "name": { + "type": "string", + "description": "The name of the secret in the pod's namespace to select from", + "title": "name" + } + }, + "required": [ + "name", + "key" + ], + "type": "object", + "description": "Selects a key of a secret in the pod's namespace", + "title": "secretKeyRef" + } + }, + "required": [ + "secretKeyRef" + ], + "type": "object", + "description": "Specifies a source the value of this var should come from", + "title": "valueFrom" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "description": "Define arguments by using environment variables", + "title": "env" + }, + "image": { + "type": "string", + "description": "Which image would you like to use for your service", + "title": "image" + }, + "imagePullPolicy": { + "type": "string", + "description": "Specify image pull policy for your service", + "title": "imagePullPolicy" + }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Specify image pull secrets for your service", + "title": "imagePullSecrets" + }, + "livenessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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.", + "title": "command" + } + }, + "required": [ + "command" + ], + "type": "object", + "description": "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.", + "title": "exec" + }, + "failureThreshold": { + "type": "integer", + "default": 3, + "description": "Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).", + "title": "failureThreshold" + }, + "httpGet": { + "properties": { + "httpHeaders": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "title": "name" + }, + "value": { + "type": "string", + "title": "value" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "title": "httpHeaders" + }, + "path": { + "type": "string", + "description": "The endpoint, relative to the port, to which the HTTP GET request should be directed.", + "title": "path" + }, + "port": { + "type": "integer", + "description": "The TCP socket within the container to which the HTTP GET request should be directed.", + "title": "port" + } + }, + "required": [ + "path", + "port" + ], + "type": "object", + "description": "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.", + "title": "httpGet" + }, + "initialDelaySeconds": { + "type": "integer", + "default": 0, + "description": "Number of seconds after the container is started before the first probe is initiated.", + "title": "initialDelaySeconds" + }, + "periodSeconds": { + "type": "integer", + "default": 10, + "description": "How often, in seconds, to execute the probe.", + "title": "periodSeconds" + }, + "successThreshold": { + "type": "integer", + "default": 1, + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.", + "title": "successThreshold" + }, + "tcpSocket": { + "properties": { + "port": { + "type": "integer", + "description": "The TCP socket within the container that should be probed to assess container health.", + "title": "port" + } + }, + "required": [ + "port" + ], + "type": "object", + "description": "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.", + "title": "tcpSocket" + }, + "timeoutSeconds": { + "type": "integer", + "default": 1, + "description": "Number of seconds after which the probe times out.", + "title": "timeoutSeconds" + } + }, + "required": [ + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "successThreshold", + "failureThreshold" + ], + "type": "object", + "description": "Instructions for assessing whether the container is alive.", + "title": "livenessProbe" + }, + "memory": { + "type": "string", + "description": "Specifies the attributes of the memory resource required for the container.", + "title": "memory" + }, + "port": { + "type": "integer", + "default": 80, + "description": "Which port do you want customer traffic sent to", + "title": "port" + }, + "readinessProbe": { + "properties": { + "exec": { + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + }, + "description": "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.", + "title": "command" + } + }, + "required": [ + "command" + ], + "type": "object", + "description": "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.", + "title": "exec" + }, + "failureThreshold": { + "type": "integer", + "default": 3, + "description": "Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).", + "title": "failureThreshold" + }, + "httpGet": { + "properties": { + "httpHeaders": { + "type": "array", + "items": { + "properties": { + "name": { + "type": "string", + "title": "name" + }, + "value": { + "type": "string", + "title": "value" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "title": "httpHeaders" + }, + "path": { + "type": "string", + "description": "The endpoint, relative to the port, to which the HTTP GET request should be directed.", + "title": "path" + }, + "port": { + "type": "integer", + "description": "The TCP socket within the container to which the HTTP GET request should be directed.", + "title": "port" + } + }, + "required": [ + "path", + "port" + ], + "type": "object", + "description": "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.", + "title": "httpGet" + }, + "initialDelaySeconds": { + "type": "integer", + "default": 0, + "description": "Number of seconds after the container is started before the first probe is initiated.", + "title": "initialDelaySeconds" + }, + "periodSeconds": { + "type": "integer", + "default": 10, + "description": "How often, in seconds, to execute the probe.", + "title": "periodSeconds" + }, + "successThreshold": { + "type": "integer", + "default": 1, + "description": "Minimum consecutive successes for the probe to be considered successful after having failed.", + "title": "successThreshold" + }, + "tcpSocket": { + "properties": { + "port": { + "type": "integer", + "description": "The TCP socket within the container that should be probed to assess container health.", + "title": "port" + } + }, + "required": [ + "port" + ], + "type": "object", + "description": "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.", + "title": "tcpSocket" + }, + "timeoutSeconds": { + "type": "integer", + "default": 1, + "description": "Number of seconds after which the probe times out.", + "title": "timeoutSeconds" + } + }, + "required": [ + "initialDelaySeconds", + "periodSeconds", + "timeoutSeconds", + "successThreshold", + "failureThreshold" + ], + "type": "object", + "description": "Instructions for assessing whether the container is in a suitable state to serve traffic.", + "title": "readinessProbe" + }, + "volumes": { + "type": "array", + "items": { + "properties": { + "mountPath": { + "type": "string", + "title": "mountPath" + }, + "name": { + "type": "string", + "title": "name" + }, + "type": { + "type": "string", + "enum": [ + "pvc", + "configMap", + "secret", + "emptyDir" + ], + "description": "Specify volume type, options: \"pvc\",\"configMap\",\"secret\",\"emptyDir\"", + "title": "type" + } + }, + "required": [ + "name", + "mountPath", + "type" + ], + "type": "object" + }, + "description": "Declare volumes and volumeMounts", + "title": "volumes" + } + }, + "required": [ + "addRevisionLabel", + "image", + "port" + ], + "type": "object" + } +} \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml b/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml new file mode 100644 index 000000000..a7af45553 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/applyapplication-sd.yaml @@ -0,0 +1,20 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/apply-application.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Apply application for your workflow steps + name: apply-application + namespace: vela-system +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + // apply application + output: op.#ApplyApplication & {} + diff --git a/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml b/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml new file mode 100644 index 000000000..86b2567d3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/example-app-error.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: | + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod diff --git a/pkg/apiserver/rest/usecase/testdata/example-app.yaml b/pkg/apiserver/rest/usecase/testdata/example-app.yaml new file mode 100644 index 000000000..4e77d8bf3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/example-app.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod diff --git a/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml b/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml new file mode 100644 index 000000000..9345a04a3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/myingress-td.yaml @@ -0,0 +1,65 @@ +apiVersion: core.oam.dev/v1beta1 +kind: TraitDefinition +metadata: + annotations: + definition.oam.dev/description: Enable public web traffic for the component. + name: myingress + namespace: vela-system +spec: + appliesToWorkloads: + - "*" + podDisruptive: false + schematic: + cue: + template: | + import ( + kubev1 "kube/v1" + network "kube/networking.k8s.io/v1beta1" + ) + + parameter: { + domain: string + http: [string]: int + } + + outputs: { + service: kubev1.#Service + ingress: network.#Ingress + } + + // trait template can have multiple outputs in one trait + outputs: service: { + metadata: + name: context.name + spec: { + selector: + "app.oam.dev/component": context.name + ports: [ + for k, v in parameter.http { + port: v + targetPort: v + }, + ] + } + } + + outputs: ingress: { + metadata: + name: context.name + spec: { + rules: [{ + host: parameter.domain + http: { + paths: [ + for k, v in parameter.http { + path: k + backend: { + serviceName: context.name + servicePort: v + } + }, + ] + } + }] + } + } diff --git a/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml new file mode 100755 index 000000000..03bfd5081 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-custom-schema.yaml @@ -0,0 +1,77 @@ +- uiType: ImageInput + jsonKey: image + label: 服务镜像 + sort: 1 +- description: Specifies the attributes of the memory resource required for the container. + disable: false + jsonKey: memory + uiType: MemoryNumber + sort: 3 + label: 分配内存 +- uiType: CPUNumber + jsonKey: cpu + sort: 5 + label: 分配CPU +- jsonKey: cmd + label: 启动参数 + sort: 7 +- description: Define arguments by using environment variables + disable: false + jsonKey: env + label: 环境变量 + sort: 9 + subParameterGroupOption: + - label: Add By Value + keys: + - name + - value + - label: Add By Secret + keys: + - name + - valueFrom + subParameters: + - description: Specifies a source the value of this var should come from + disable: false + jsonKey: valueFrom + label: Secret选择器 + uiType: InnerGroup + subParameters: + - jsonKey: secretKeyRef + uiType: Ignore + subParameters: + - jsonKey: name + label: Secret选择 + uiType: SecretSelect + - jsonKey: key + label: SecretKey选择 + uiType: SecretKeySelect + uiType: Structs + validate: {} +- jsonKey: port + label: 端口设置 + sort: 10 +- jsonKey: volumes + label: 持久化存储 + sort: 11 +- jsonKey: readinessProbe + uiType: Group + label: ReadinessProbe检测 + sort: 13 +- jsonKey: livenessProbe + uiType: Group + label: LivenessProbe检测 + sort: 15 +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + uiType: Select + sort: 17 + validate: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never \ No newline at end of file diff --git a/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml new file mode 100755 index 000000000..ef7d72dd8 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-default-schema.yaml @@ -0,0 +1,132 @@ +- description: Which image would you like to use for your service + jsonKey: image + label: Image + uiType: Input + validete: + required: true +- description: Specify image pull policy for your service + jsonKey: imagePullPolicy + label: ImagePullPolicy + uiType: Input + validete: {} +- description: Instructions for assessing whether the container is alive. + jsonKey: livenessProbe + label: LivenessProbe + uiType: KV + validete: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + uiType: Switch + validete: + defaultValue: false + required: true +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: Cpu + uiType: Input + validete: {} +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + uiType: Structs + validete: {} +- description: Specifies the attributes of the memory resource required for the container. + jsonKey: memory + label: Memory + uiType: Input + validete: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: Port + uiType: Number + validete: + defaultValue: 80 + required: true +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe + uiType: KV + validete: {} +- description: Declare volumes and volumeMounts + jsonKey: volumes + label: Volumes + subParameters: + - description: "" + jsonKey: volumes.mountPath + label: MountPath + uiType: Input + validete: + required: true + - description: "" + jsonKey: volumes.name + label: Name + uiType: Input + validete: + required: true + - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' + jsonKey: volumes.type + label: Type + uiType: Select + validete: + options: + - label: Pvc + value: pvc + - label: ConfigMap + value: configMap + - label: Secret + value: secret + - label: EmptyDir + value: emptyDir + required: true + uiType: Structs + validete: {} +- description: Commands to run in the container + jsonKey: cmd + label: Cmd + uiType: Structs + validete: {} +- description: Define arguments by using environment variables + jsonKey: env + label: Env + subParameters: + - description: The value of the environment variable + jsonKey: env.value + label: Value + uiType: Input + validete: {} + - description: Specifies a source the value of this var should come from + jsonKey: env.valueFrom + label: ValueFrom + subParameters: + - description: "" + jsonKey: env.valueFrom.secretKeyRef + label: SecretKeyRef + subParameters: + - description: secret name + jsonKey: env.valueFrom.secretKeyRef.name + label: Name + uiType: Input + validete: + required: true + - description: secret key + jsonKey: env.valueFrom.secretKeyRef.key + label: Key + uiType: Input + validete: + required: true + uiType: KV + validete: {} + uiType: KV + validete: {} + - description: Environment variable name + jsonKey: env.name + label: Name + uiType: Input + validete: + required: true + uiType: Structs + validete: {} diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml new file mode 100755 index 000000000..60e7eeace --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -0,0 +1,433 @@ +- description: Which image would you like to use for your service + jsonKey: image + label: 服务镜像 + sort: 1 + uiType: ImageInput + validate: + required: true +- description: Specifies the attributes of the memory resource required for the container. + disable: false + jsonKey: memory + label: 分配内存 + sort: 3 + uiType: MemoryNumber + validate: {} +- description: Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` + (1 CPU core) + jsonKey: cpu + label: 分配CPU + sort: 5 + uiType: CPUNumber + validate: {} +- description: Commands to run in the container + jsonKey: cmd + label: 启动参数 + sort: 7 + uiType: Strings + validate: {} +- description: Define arguments by using environment variables + disable: false + jsonKey: env + label: 环境变量 + sort: 9 + subParameterGroupOption: + - keys: + - name + - value + label: Add By Value + - keys: + - name + - valueFrom + label: Add By Secret + subParameters: + - description: Specifies a source the value of this var should come from + disable: false + jsonKey: valueFrom + label: Secret选择器 + sort: 100 + subParameters: + - description: Selects a key of a secret in the pod's namespace + jsonKey: secretKeyRef + label: SecretKeyRef + sort: 100 + subParameters: + - description: The key of the secret to select from. Must be a valid secret + key + jsonKey: key + label: SecretKey选择 + sort: 100 + uiType: SecretKeySelect + validate: + required: true + - description: The name of the secret in the pod's namespace to select from + jsonKey: name + label: Secret选择 + sort: 100 + uiType: SecretSelect + validate: + required: true + uiType: Ignore + validate: + required: true + uiType: InnerGroup + validate: {} + - description: Environment variable name + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: The value of the environment variable + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: {} + uiType: Structs + validate: {} +- description: Which port do you want customer traffic sent to + jsonKey: port + label: 端口设置 + sort: 10 + uiType: Number + validate: + defaultValue: 80 + required: true +- description: Declare volumes and volumeMounts + jsonKey: volumes + label: 持久化存储 + sort: 11 + subParameters: + - description: 'Specify volume type, options: "pvc","configMap","secret","emptyDir"' + jsonKey: type + label: Type + sort: 100 + uiType: Select + validate: + options: + - label: Pvc + value: pvc + - label: ConfigMap + value: configMap + - label: Secret + value: secret + - label: EmptyDir + value: emptyDir + required: true + - description: "" + jsonKey: mountPath + label: MountPath + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + uiType: Structs + validate: {} +- description: Instructions for assessing whether the container is in a suitable state + to serve traffic. + jsonKey: readinessProbe + label: ReadinessProbe检测 + sort: 13 + subParameters: + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validate: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: 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. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: 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. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: 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. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 3 + required: true + - description: 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. + jsonKey: httpGet + label: HttpGet + sort: 100 + subParameters: + - description: "" + jsonKey: httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: + required: true + uiType: Structs + validate: {} + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: path + label: Path + sort: 100 + uiType: Input + validate: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + uiType: Group + validate: {} +- description: Instructions for assessing whether the container is alive. + jsonKey: livenessProbe + label: LivenessProbe检测 + sort: 15 + subParameters: + - description: 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. + jsonKey: tcpSocket + label: TcpSocket + sort: 100 + subParameters: + - description: The TCP socket within the container that should be probed to assess + container health. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + uiType: KV + validate: {} + - description: Number of seconds after which the probe times out. + jsonKey: timeoutSeconds + label: TimeoutSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + - description: 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. + jsonKey: exec + label: Exec + sort: 100 + subParameters: + - description: 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. + jsonKey: command + label: Command + sort: 100 + uiType: Strings + validate: + required: true + uiType: KV + validate: {} + - description: Number of consecutive failures required to determine the container + is not alive (liveness probe) or not ready (readiness probe). + jsonKey: failureThreshold + label: FailureThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 3 + required: true + - description: 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. + jsonKey: httpGet + label: HttpGet + sort: 100 + subParameters: + - description: The endpoint, relative to the port, to which the HTTP GET request + should be directed. + jsonKey: path + label: Path + sort: 100 + uiType: Input + validate: + required: true + - description: The TCP socket within the container to which the HTTP GET request + should be directed. + jsonKey: port + label: Port + sort: 100 + uiType: Number + validate: + required: true + - description: "" + jsonKey: httpHeaders + label: HttpHeaders + sort: 100 + subParameters: + - description: "" + jsonKey: name + label: Name + sort: 100 + uiType: Input + validate: + required: true + - description: "" + jsonKey: value + label: Value + sort: 100 + uiType: Input + validate: + required: true + uiType: Structs + validate: {} + uiType: KV + validate: {} + - description: Number of seconds after the container is started before the first + probe is initiated. + jsonKey: initialDelaySeconds + label: InitialDelaySeconds + sort: 100 + uiType: Number + validate: + defaultValue: 0 + required: true + - description: How often, in seconds, to execute the probe. + jsonKey: periodSeconds + label: PeriodSeconds + sort: 100 + uiType: Number + validate: + defaultValue: 10 + required: true + - description: Minimum consecutive successes for the probe to be considered successful + after having failed. + jsonKey: successThreshold + label: SuccessThreshold + sort: 100 + uiType: Number + validate: + defaultValue: 1 + required: true + uiType: Group + validate: {} +- description: Specify image pull policy for your service + disable: false + jsonKey: imagePullPolicy + label: 镜像更新策略 + sort: 17 + uiType: Select + validate: + options: + - label: 镜像不存在时更新 + value: IfNotPresent + - label: 总是更新 + value: Always + - label: 永不更新 + value: Never +- description: Specify image pull secrets for your service + jsonKey: imagePullSecrets + label: ImagePullSecrets + sort: 100 + uiType: Strings + validate: {} +- description: If addRevisionLabel is true, the appRevision label will be added to + the underlying pods + jsonKey: addRevisionLabel + label: AddRevisionLabel + sort: 100 + uiType: Switch + validate: + defaultValue: false + required: true diff --git a/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml new file mode 100644 index 000000000..b407974a8 --- /dev/null +++ b/pkg/apiserver/rest/usecase/testdata/webserver-cd.yaml @@ -0,0 +1,255 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/webservice.cue +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + annotations: + definition.oam.dev/description: Describes long-running, scalable, containerized services that have a stable network endpoint to receive external network traffic from customers. + name: webservice-test + namespace: vela-system +spec: + schematic: + cue: + template: | + output: { + apiVersion: "apps/v1" + kind: "Deployment" + spec: { + selector: matchLabels: "app.oam.dev/component": context.name + + template: { + metadata: labels: { + "app.oam.dev/component": context.name + if parameter.addRevisionLabel { + "app.oam.dev/appRevision": context.appRevision + } + "app.oam.dev/revision": context.revision + } + + spec: { + containers: [{ + name: context.name + image: parameter.image + ports: [{ + containerPort: parameter.port + }] + + if parameter["imagePullPolicy"] != _|_ { + imagePullPolicy: parameter.imagePullPolicy + } + + if parameter["cmd"] != _|_ { + command: parameter.cmd + } + + if parameter["env"] != _|_ { + env: parameter.env + } + + if context["config"] != _|_ { + env: context.config + } + + if parameter["cpu"] != _|_ { + resources: { + limits: cpu: parameter.cpu + requests: cpu: parameter.cpu + } + } + + if parameter["memory"] != _|_ { + resources: { + limits: memory: parameter.memory + requests: memory: parameter.memory + } + } + + if parameter["volumes"] != _|_ { + volumeMounts: [ for v in parameter.volumes { + { + mountPath: v.mountPath + name: v.name + }}] + } + + if parameter["livenessProbe"] != _|_ { + livenessProbe: parameter.livenessProbe + } + + if parameter["readinessProbe"] != _|_ { + readinessProbe: parameter.readinessProbe + } + + }] + + if parameter["imagePullSecrets"] != _|_ { + imagePullSecrets: [ for v in parameter.imagePullSecrets { + name: v + }, + ] + } + + if parameter["volumes"] != _|_ { + volumes: [ for v in parameter.volumes { + { + name: v.name + if v.type == "pvc" { + persistentVolumeClaim: claimName: v.claimName + } + if v.type == "configMap" { + configMap: { + defaultMode: v.defaultMode + name: v.cmName + if v.items != _|_ { + items: v.items + } + } + } + if v.type == "secret" { + secret: { + defaultMode: v.defaultMode + secretName: v.secretName + if v.items != _|_ { + items: v.items + } + } + } + if v.type == "emptyDir" { + emptyDir: medium: v.medium + } + }}] + } + } + } + } + } + parameter: { + // +usage=Which image would you like to use for your service + // +short=i + image: string + + // +usage=Specify image pull policy for your service + imagePullPolicy?: string + + // +usage=Specify image pull secrets for your service + imagePullSecrets?: [...string] + + // +usage=Which port do you want customer traffic sent to + // +short=p + port: *80 | int + + // +ignore + // +usage=If addRevisionLabel is true, the appRevision label will be added to the underlying pods + addRevisionLabel: *false | bool + + // +usage=Commands to run in the container + cmd?: [...string] + + // +usage=Define arguments by using environment variables + env?: [...{ + // +usage=Environment variable name + name: string + // +usage=The value of the environment variable + value?: string + // +usage=Specifies a source the value of this var should come from + valueFrom?: { + // +usage=Selects a key of a secret in the pod's namespace + secretKeyRef: { + // +usage=The name of the secret in the pod's namespace to select from + name: string + // +usage=The key of the secret to select from. Must be a valid secret key + key: string + } + } + }] + + // +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core) + cpu?: string + + // +usage=Specifies the attributes of the memory resource required for the container. + memory?: string + + // +usage=Declare volumes and volumeMounts + volumes?: [...{ + name: string + mountPath: string + // +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir" + type: "pvc" | "configMap" | "secret" | "emptyDir" + if type == "pvc" { + claimName: string + } + if type == "configMap" { + defaultMode: *420 | int + cmName: string + items?: [...{ + key: string + path: string + mode: *511 | int + }] + } + if type == "secret" { + defaultMode: *420 | int + secretName: string + items?: [...{ + key: string + path: string + mode: *511 | int + }] + } + if type == "emptyDir" { + medium: *"" | "Memory" + } + }] + + // +usage=Instructions for assessing whether the container is alive. + livenessProbe?: #HealthProbe + + // +usage=Instructions for assessing whether the container is in a suitable state to serve traffic. + readinessProbe?: #HealthProbe + } + #HealthProbe: { + + // +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute. + exec?: { + // +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures. + command: [...string] + } + + // +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute. + httpGet?: { + // +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed. + path: string + // +usage=The TCP socket within the container to which the HTTP GET request should be directed. + port: int + httpHeaders?: [...{ + name: string + value: string + }] + } + + // +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute. + tcpSocket?: { + // +usage=The TCP socket within the container that should be probed to assess container health. + port: int + } + + // +usage=Number of seconds after the container is started before the first probe is initiated. + initialDelaySeconds: *0 | int + + // +usage=How often, in seconds, to execute the probe. + periodSeconds: *10 | int + + // +usage=Number of seconds after which the probe times out. + timeoutSeconds: *1 | int + + // +usage=Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold: *1 | int + + // +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe). + failureThreshold: *3 | int + } + workload: + definition: + apiVersion: apps/v1 + kind: Deployment + diff --git a/pkg/apiserver/rest/usecase/usecase_suite_test.go b/pkg/apiserver/rest/usecase/usecase_suite_test.go new file mode 100644 index 000000000..e77f33b77 --- /dev/null +++ b/pkg/apiserver/rest/usecase/usecase_suite_test.go @@ -0,0 +1,107 @@ +/* +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 usecase + +import ( + "context" + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/client-go/rest" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore/kubeapi" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore/mongodb" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ds datastore.DataStore + +var _ = BeforeSuite(func(done Done) { + rand.Seed(time.Now().UnixNano()) + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + clients.SetKubeClient(k8sClient) + ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "kubevela"}) + Expect(err).Should(BeNil()) + Expect(ds).ToNot(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func NewDatastore(cfg datastore.Config) (ds datastore.DataStore, err error) { + switch cfg.Type { + case "mongodb": + ds, err = mongodb.New(context.Background(), cfg) + if err != nil { + return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + } + case "kubeapi": + ds, err = kubeapi.New(context.Background(), cfg) + if err != nil { + return nil, fmt.Errorf("create mongodb datastore instance failure %w", err) + } + default: + return nil, fmt.Errorf("not support datastore type %s", cfg.Type) + } + return ds, nil +} + +func TestUsecase(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Usecase Suite") +} + +func randomNamespaceName(basic string) string { + return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) +} diff --git a/pkg/apiserver/rest/usecase/velaql.go b/pkg/apiserver/rest/usecase/velaql.go new file mode 100644 index 000000000..0d42ec8db --- /dev/null +++ b/pkg/apiserver/rest/usecase/velaql.go @@ -0,0 +1,86 @@ +/* + 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 usecase + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "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/utils/bcode" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/velaql" +) + +// VelaQLUsecase velaQL usecase +type VelaQLUsecase interface { + QueryView(context.Context, string) (*apis.VelaQLViewResponse, error) +} + +type velaQLUsecaseImpl struct { + kubeClient client.Client + dm discoverymapper.DiscoveryMapper + pd *packages.PackageDiscover +} + +// NewVelaQLUsecase new velaQL usecase +func NewVelaQLUsecase() VelaQLUsecase { + k8sClient, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + + dm, err := clients.GetDiscoverMapper() + if err != nil { + log.Logger.Fatalf("get discover mapper failure %s", err.Error()) + } + + pd, err := clients.GetPackageDiscover() + if err != nil { + log.Logger.Fatalf("get package discover failure %s", err.Error()) + } + return &velaQLUsecaseImpl{ + kubeClient: k8sClient, + dm: dm, + pd: pd, + } +} + +// QueryView get the view query results +func (v *velaQLUsecaseImpl) QueryView(ctx context.Context, velaQL string) (*apis.VelaQLViewResponse, error) { + query, err := velaql.ParseVelaQL(velaQL) + if err != nil { + return nil, bcode.ErrParseVelaQL + } + + queryValue, err := velaql.NewViewHandler(v.kubeClient, v.dm, v.pd).QueryView(ctx, query) + if err != nil { + log.Logger.Errorf("fail to query the view %s", err.Error()) + return nil, bcode.ErrViewQuery + } + + resp := apis.VelaQLViewResponse{} + err = queryValue.UnmarshalTo(&resp) + if err != nil { + return nil, bcode.ErrParseQuery2Json + } + return &resp, err +} diff --git a/pkg/apiserver/rest/usecase/workflow.go b/pkg/apiserver/rest/usecase/workflow.go new file mode 100644 index 000000000..2d2336cf3 --- /dev/null +++ b/pkg/apiserver/rest/usecase/workflow.go @@ -0,0 +1,614 @@ +/* +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 usecase + +import ( + "context" + "errors" + "fmt" + "strconv" + + "helm.sh/helm/v3/pkg/time" + appsv1 "k8s.io/api/apps/v1" + "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/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +// WorkflowUsecase workflow manage api +type WorkflowUsecase interface { + ListApplicationWorkflow(ctx context.Context, app *model.Application) ([]*apisv1.WorkflowBase, error) + GetWorkflow(ctx context.Context, app *model.Application, workflowName string) (*model.Workflow, error) + DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) + GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) + DeleteWorkflow(ctx context.Context, app *model.Application, workflowName string) error + DeleteWorkflowByApp(ctx context.Context, app *model.Application) error + CreateOrUpdateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) + CreateWorkflowRecord(ctx context.Context, appModel *model.Application, app *v1beta1.Application, workflow *model.Workflow) error + ListWorkflowRecords(ctx context.Context, workflow *model.Workflow, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) + DetailWorkflowRecord(ctx context.Context, workflow *model.Workflow, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) + SyncWorkflowRecord(ctx context.Context) error + ResumeRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error + TerminateRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error + RollbackRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName, revisionName string) error + CountWorkflow(ctx context.Context, app *model.Application) int64 +} + +// NewWorkflowUsecase new workflow usecase +func NewWorkflowUsecase(ds datastore.DataStore) WorkflowUsecase { + kubecli, err := clients.GetKubeClient() + if err != nil { + log.Logger.Fatalf("get kubeclient failure %s", err.Error()) + } + return &workflowUsecaseImpl{ + ds: ds, + kubeClient: kubecli, + apply: apply.NewAPIApplicator(kubecli), + } +} + +type workflowUsecaseImpl struct { + ds datastore.DataStore + kubeClient client.Client + apply apply.Applicator +} + +// DeleteWorkflow delete application workflow +func (w *workflowUsecaseImpl) DeleteWorkflow(ctx context.Context, app *model.Application, workflowName string) error { + var workflow = &model.Workflow{ + Name: workflowName, + AppPrimaryKey: app.PrimaryKey(), + } + if err := w.ds.Delete(ctx, workflow); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrWorkflowNotExist + } + return err + } + return nil +} + +func (w *workflowUsecaseImpl) DeleteWorkflowByApp(ctx context.Context, app *model.Application) error { + var workflow = &model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + } + + workflows, err := w.ds.List(ctx, workflow, &datastore.ListOptions{}) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil + } + return err + } + for i := range workflows { + if err := w.ds.Delete(ctx, workflows[i]); err != nil { + log.Logger.Errorf("delete workflow %s failure %s", workflows[i].PrimaryKey(), err.Error()) + } + } + return nil +} + +func (w *workflowUsecaseImpl) CreateOrUpdateWorkflow(ctx context.Context, app *model.Application, req apisv1.CreateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { + if req.EnvName == "" { + return nil, bcode.ErrWorkflowNoEnv + } + workflow, err := w.GetWorkflow(ctx, app, req.Name) + if err != nil && errors.Is(err, datastore.ErrRecordNotExist) { + return nil, err + } + var steps []model.WorkflowStep + for _, step := range req.Steps { + properties, err := model.NewJSONStructByString(step.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return nil, bcode.ErrInvalidProperties + } + steps = append(steps, model.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, + Description: step.Description, + DependsOn: step.DependsOn, + Properties: properties, + }) + } + if workflow != nil { + workflow.Steps = steps + workflow.Alias = req.Alias + workflow.Description = req.Description + workflow.Default = req.Default + if err := w.ds.Put(ctx, workflow); err != nil { + return nil, err + } + } else { + // It is allowed to set multiple workflows as default, and only one takes effect. + workflow = &model.Workflow{ + Steps: steps, + Name: req.Name, + Description: req.Description, + Default: req.Default, + EnvName: req.EnvName, + AppPrimaryKey: app.PrimaryKey(), + } + if err := w.ds.Add(ctx, workflow); err != nil { + return nil, err + } + } + return w.DetailWorkflow(ctx, workflow) +} + +func (w *workflowUsecaseImpl) UpdateWorkflow(ctx context.Context, workflow *model.Workflow, req apisv1.UpdateWorkflowRequest) (*apisv1.DetailWorkflowResponse, error) { + var steps []model.WorkflowStep + for _, step := range req.Steps { + properties, err := model.NewJSONStructByString(step.Properties) + if err != nil { + log.Logger.Errorf("parse trait properties failire %w", err) + return nil, bcode.ErrInvalidProperties + } + steps = append(steps, model.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: properties, + }) + } + workflow.Steps = steps + workflow.Description = req.Description + // It is allowed to set multiple workflows as default, and only one takes effect. + workflow.Default = req.Default + workflow.EnvName = req.EnvName + if err := w.ds.Put(ctx, workflow); err != nil { + return nil, err + } + return w.DetailWorkflow(ctx, workflow) +} + +func converWorkflowBase(workflow *model.Workflow) apisv1.WorkflowBase { + var steps []apisv1.WorkflowStep + for _, step := range workflow.Steps { + apiStep := apisv1.WorkflowStep{ + Name: step.Name, + Type: step.Type, + Description: step.Description, + Inputs: step.Inputs, + Outputs: step.Outputs, + Properties: step.Properties.JSON(), + DependsOn: step.DependsOn, + } + if step.Properties != nil { + apiStep.Properties = step.Properties.JSON() + } + steps = append(steps, apiStep) + } + return apisv1.WorkflowBase{ + Name: workflow.Name, + Description: workflow.Description, + Default: workflow.Default, + EnvName: workflow.EnvName, + CreateTime: workflow.CreateTime, + UpdateTime: workflow.UpdateTime, + Steps: steps, + } +} + +// DetailWorkflow detail workflow +func (w *workflowUsecaseImpl) DetailWorkflow(ctx context.Context, workflow *model.Workflow) (*apisv1.DetailWorkflowResponse, error) { + return &apisv1.DetailWorkflowResponse{ + WorkflowBase: converWorkflowBase(workflow), + }, nil +} + +// GetWorkflow get workflow model +func (w *workflowUsecaseImpl) GetWorkflow(ctx context.Context, app *model.Application, workflowName string) (*model.Workflow, error) { + var workflow = model.Workflow{ + Name: workflowName, + AppPrimaryKey: app.PrimaryKey(), + } + if err := w.ds.Get(ctx, &workflow); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrWorkflowNotExist + } + return nil, err + } + return &workflow, nil +} + +// ListApplicationWorkflow list application workflows +func (w *workflowUsecaseImpl) ListApplicationWorkflow(ctx context.Context, app *model.Application) ([]*apisv1.WorkflowBase, error) { + var workflow = model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + } + workflows, err := w.ds.List(ctx, &workflow, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + var list []*apisv1.WorkflowBase + for _, workflow := range workflows { + wm := workflow.(*model.Workflow) + base := converWorkflowBase(wm) + list = append(list, &base) + } + return list, nil +} + +// GetApplicationDefaultWorkflow get application default workflow +func (w *workflowUsecaseImpl) GetApplicationDefaultWorkflow(ctx context.Context, app *model.Application) (*model.Workflow, error) { + var workflow = model.Workflow{ + AppPrimaryKey: app.PrimaryKey(), + Default: true, + } + workflows, err := w.ds.List(ctx, &workflow, &datastore.ListOptions{}) + if err != nil { + return nil, err + } + if len(workflows) > 0 { + return workflows[0].(*model.Workflow), nil + } + return nil, bcode.ErrWorkflowNoDefault +} + +// ListWorkflowRecords list workflow record +func (w *workflowUsecaseImpl) ListWorkflowRecords(ctx context.Context, workflow *model.Workflow, page, pageSize int) (*apisv1.ListWorkflowRecordsResponse, error) { + var record = model.WorkflowRecord{ + AppPrimaryKey: workflow.AppPrimaryKey, + WorkflowName: workflow.Name, + } + records, err := w.ds.List(ctx, &record, &datastore.ListOptions{Page: page, PageSize: pageSize}) + if err != nil { + return nil, err + } + + resp := &apisv1.ListWorkflowRecordsResponse{ + Records: []apisv1.WorkflowRecord{}, + } + for _, raw := range records { + record, ok := raw.(*model.WorkflowRecord) + if ok { + resp.Records = append(resp.Records, *convertFromRecordModel(record)) + } + } + count, err := w.ds.Count(ctx, &record, nil) + if err != nil { + return nil, err + } + resp.Total = count + + return resp, nil +} + +// DetailWorkflowRecord get workflow record detail with name +func (w *workflowUsecaseImpl) DetailWorkflowRecord(ctx context.Context, workflow *model.Workflow, recordName string) (*apisv1.DetailWorkflowRecordResponse, error) { + var record = model.WorkflowRecord{ + AppPrimaryKey: workflow.AppPrimaryKey, + WorkflowName: workflow.Name, + Name: recordName, + } + err := w.ds.Get(ctx, &record) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrWorkflowRecordNotExist + } + return nil, err + } + + var revision = model.ApplicationRevision{ + AppPrimaryKey: record.AppPrimaryKey, + Version: record.RevisionPrimaryKey, + } + err = w.ds.Get(ctx, &revision) + if err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return nil, bcode.ErrApplicationRevisionNotExist + } + return nil, err + } + + return &apisv1.DetailWorkflowRecordResponse{ + WorkflowRecord: *convertFromRecordModel(&record), + DeployTime: revision.CreateTime, + DeployUser: revision.DeployUser, + Note: revision.Note, + TriggerType: revision.TriggerType, + }, nil +} + +func (w *workflowUsecaseImpl) SyncWorkflowRecord(ctx context.Context) error { + var record = model.WorkflowRecord{ + Finished: "false", + } + // list all unfinished workflow records + records, err := w.ds.List(ctx, &record, &datastore.ListOptions{}) + if err != nil { + return err + } + + for _, item := range records { + app := &v1beta1.Application{} + record := item.(*model.WorkflowRecord) + workflow := &model.Workflow{ + Name: record.WorkflowName, + AppPrimaryKey: record.AppPrimaryKey, + } + if err := w.ds.Get(ctx, workflow); err != nil { + log.Logger.Errorf("get workflow %s/%s failure %s", record.AppPrimaryKey, record.WorkflowName, err.Error()) + continue + } + if err := w.kubeClient.Get(ctx, types.NamespacedName{ + Name: convertAppName(record.AppPrimaryKey, workflow.EnvName), + Namespace: record.Namespace, + }, app); err != nil { + klog.ErrorS(err, "failed to get app", "app name", record.AppPrimaryKey) + return err + } + + // try to sync the status from the running application + if app.Annotations != nil && app.Annotations[oam.AnnotationPublishVersion] == record.Name { + if err := w.syncWorkflowStatus(ctx, app, record.Name); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", record.AppPrimaryKey, "workflow record name", record.Name) + } + continue + } + + // try to sync the status from the controller revision + cr := &appsv1.ControllerRevision{} + if err := w.kubeClient.Get(ctx, types.NamespacedName{ + Name: fmt.Sprintf("record-%s-%s", record.AppPrimaryKey, record.Name), + Namespace: record.Namespace, + }, cr); err != nil { + klog.ErrorS(err, "failed to get controller revision", "app name", record.AppPrimaryKey, "workflow record name", record.Name) + continue + } + appInRevision, err := util.RawExtension2Application(cr.Data) + if err != nil { + klog.ErrorS(err, "failed to get app data in controller revision", "controller revision name", cr.Name, "app name", record.AppPrimaryKey, "workflow record name", record.Name) + continue + } + if err := w.syncWorkflowStatus(ctx, appInRevision, record.Name); err != nil { + klog.ErrorS(err, "failed to sync workflow status", "app name", record.AppPrimaryKey, "workflow record version", record.Name) + continue + } + + } + + return nil +} + +func (w *workflowUsecaseImpl) syncWorkflowStatus(ctx context.Context, app *v1beta1.Application, recordName string) error { + + var record = &model.WorkflowRecord{ + AppPrimaryKey: app.Annotations[oam.AnnotationAppName], + Name: recordName, + } + if err := w.ds.Get(ctx, record); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrWorkflowRecordNotExist + } + return err + } + var revision = &model.ApplicationRevision{ + AppPrimaryKey: app.Annotations[oam.AnnotationAppName], + Version: record.RevisionPrimaryKey, + } + + if err := w.ds.Get(ctx, revision); err != nil { + if errors.Is(err, datastore.ErrRecordNotExist) { + return bcode.ErrApplicationRevisionNotExist + } + return err + } + + if app.Status.Workflow != nil { + status := app.Status.Workflow + summaryStatus := model.RevisionStatusRunning + if status.Finished { + summaryStatus = model.RevisionStatusComplete + } + if status.Terminated { + summaryStatus = model.RevisionStatusTerminated + } + + record.Status = summaryStatus + record.Steps = status.Steps + record.Finished = strconv.FormatBool(status.Finished) + + if err := w.ds.Put(ctx, record); err != nil { + return err + } + + revision.Status = summaryStatus + if err := w.ds.Put(ctx, revision); err != nil { + return err + } + } + + return nil +} + +func (w *workflowUsecaseImpl) CreateWorkflowRecord(ctx context.Context, appModel *model.Application, app *v1beta1.Application, workflow *model.Workflow) error { + if app.Annotations == nil { + return fmt.Errorf("empty annotations in application") + } + if app.Annotations[oam.AnnotationPublishVersion] == "" { + return fmt.Errorf("failed to get record version from application") + } + if app.Annotations[oam.AnnotationDeployVersion] == "" { + return fmt.Errorf("failed to get deploy version from application") + } + + return w.ds.Add(ctx, &model.WorkflowRecord{ + WorkflowName: workflow.Name, + AppPrimaryKey: appModel.PrimaryKey(), + RevisionPrimaryKey: app.Annotations[oam.AnnotationDeployVersion], + Name: app.Annotations[oam.AnnotationPublishVersion], + Namespace: appModel.Namespace, + Finished: "false", + StartTime: time.Now().Time, + Status: model.RevisionStatusInit, + }) +} +func (w *workflowUsecaseImpl) CountWorkflow(ctx context.Context, app *model.Application) int64 { + count, err := w.ds.Count(ctx, &model.Workflow{AppPrimaryKey: app.PrimaryKey()}, &datastore.FilterOptions{}) + if err != nil { + log.Logger.Errorf("count app %s workflow failure %s", app.PrimaryKey(), err.Error()) + } + return count +} + +func (w *workflowUsecaseImpl) ResumeRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) + if err != nil { + return err + } + + oamApp.Status.Workflow.Suspend = false + if err := w.kubeClient.Status().Patch(ctx, oamApp, client.Merge); err != nil { + return err + } + if err := w.syncWorkflowStatus(ctx, oamApp, recordName); err != nil { + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) TerminateRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName string) error { + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) + if err != nil { + return err + } + + oamApp.Status.Workflow.Terminated = true + if err := w.kubeClient.Status().Patch(ctx, oamApp, client.Merge); err != nil { + return err + } + if err := w.syncWorkflowStatus(ctx, oamApp, recordName); err != nil { + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) RollbackRecord(ctx context.Context, appModel *model.Application, workflow *model.Workflow, recordName, revisionVersion string) error { + if revisionVersion == "" { + // find the latest complete revision version + var revision = model.ApplicationRevision{ + AppPrimaryKey: appModel.Name, + Status: model.RevisionStatusComplete, + } + + revisions, err := w.ds.List(ctx, &revision, &datastore.ListOptions{ + Page: 1, + PageSize: 1, + SortBy: []datastore.SortOption{{Key: "model.createTime", Order: datastore.SortOrderDescending}}, + }) + if err != nil { + return err + } + if len(revisions) == 0 { + return bcode.ErrApplicationNoReadyRevision + } + revisionVersion = revisions[0].Index()["version"] + } + + var record = &model.WorkflowRecord{ + AppPrimaryKey: appModel.PrimaryKey(), + Name: recordName, + } + if err := w.ds.Get(ctx, record); err != nil { + return err + } + + oamApp, err := w.checkRecordRunning(ctx, appModel, workflow.EnvName) + if err != nil { + return err + } + + var rollbackRevision = model.ApplicationRevision{ + AppPrimaryKey: appModel.Name, + Version: revisionVersion, + } + if err := w.ds.Get(ctx, &rollbackRevision); err != nil { + return err + } + + rollBackApp := &v1beta1.Application{} + if err := yaml.Unmarshal([]byte(rollbackRevision.ApplyAppConfig), rollBackApp); err != nil { + return err + } + // replace the application spec + oamApp.Spec.Components = rollBackApp.Spec.Components + oamApp.Spec.Policies = rollBackApp.Spec.Policies + if oamApp.Annotations == nil { + oamApp.Annotations = make(map[string]string) + } + newRecordName := utils.GenerateVersion(record.WorkflowName) + oamApp.Annotations[oam.AnnotationDeployVersion] = revisionVersion + oamApp.Annotations[oam.AnnotationPublishVersion] = newRecordName + // create a new workflow record + if err := w.CreateWorkflowRecord(ctx, appModel, oamApp, workflow); err != nil { + return err + } + + if err := w.apply.Apply(ctx, oamApp); err != nil { + // rollback error case + if err := w.ds.Delete(ctx, &model.WorkflowRecord{Name: newRecordName}); err != nil { + klog.Error(err, "failed to delete record", newRecordName) + } + return err + } + + return nil +} + +func (w *workflowUsecaseImpl) checkRecordRunning(ctx context.Context, appModel *model.Application, envName string) (*v1beta1.Application, error) { + oamApp := &v1beta1.Application{} + if err := w.kubeClient.Get(ctx, types.NamespacedName{Name: convertAppName(appModel.Name, envName), Namespace: appModel.Namespace}, oamApp); err != nil { + return nil, err + } + if oamApp.Status.Workflow != nil && !oamApp.Status.Workflow.Suspend && !oamApp.Status.Workflow.Terminated && !oamApp.Status.Workflow.Finished { + return nil, fmt.Errorf("workflow is still running, can not operate a running workflow") + } + + oamApp.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind) + return oamApp, nil +} + +func convertFromRecordModel(record *model.WorkflowRecord) *apisv1.WorkflowRecord { + return &apisv1.WorkflowRecord{ + Name: record.Name, + Namespace: record.Namespace, + StartTime: record.StartTime, + Status: record.Status, + Steps: record.Steps, + } +} diff --git a/pkg/apiserver/rest/usecase/workflow_test.go b/pkg/apiserver/rest/usecase/workflow_test.go new file mode 100644 index 000000000..076c06043 --- /dev/null +++ b/pkg/apiserver/rest/usecase/workflow_test.go @@ -0,0 +1,503 @@ +/* +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 usecase + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/utils/apply" +) + +var appName = "app-workflow" +var _ = Describe("Test workflow usecase functions", func() { + var ( + workflowUsecase *workflowUsecaseImpl + appUsecase *applicationUsecaseImpl + ) + BeforeEach(func() { + workflowUsecase = &workflowUsecaseImpl{ds: ds, kubeClient: k8sClient, apply: apply.NewAPIApplicator(k8sClient)} + appUsecase = &applicationUsecaseImpl{ds: ds, kubeClient: k8sClient, apply: apply.NewAPIApplicator(k8sClient), envBindingUsecase: &envBindingUsecaseImpl{ + ds: ds, + workflowUsecase: workflowUsecase, + }} + }) + It("Test CreateWorkflow function", func() { + reqApp := apisv1.CreateApplicationRequest{ + Name: appName, + Namespace: "default", + Description: "this is a test app", + EnvBinding: []*apisv1.EnvBinding{{ + Name: "dev", + Description: "dev env", + TargetNames: []string{"dev-target"}, + }}, + } + _, err := appUsecase.CreateApplication(context.TODO(), reqApp) + Expect(err).Should(BeNil()) + + req := apisv1.CreateWorkflowRequest{ + Name: "test-workflow-1", + Description: "this is a workflow", + EnvName: "dev", + } + + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + req2 := apisv1.CreateWorkflowRequest{ + Name: "test-workflow-1", + Description: "change description", + EnvName: "dev2", + } + + base, err = workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req2) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Description, req2.Description)).Should(BeEmpty()) + Expect(cmp.Diff(base.EnvName, req2.EnvName)).ShouldNot(BeEmpty()) + + req = apisv1.CreateWorkflowRequest{ + Name: "test-workflow-2", + Description: "this is test workflow", + EnvName: "dev", + Default: true, + } + base, err = workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + }) + + It("Test GetApplicationDefaultWorkflow function", func() { + workflow, err := workflowUsecase.GetApplicationDefaultWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }) + Expect(err).Should(BeNil()) + Expect(workflow).ShouldNot(BeNil()) + Expect(cmp.Diff(workflow.Name, "test-workflow-2")).Should(BeEmpty()) + }) + + It("Test ListWorkflowRecords function", func() { + By("create some workflow records to test list workflow records") + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-2" + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + for i := 0; i < 3; i++ { + app.Annotations[oam.AnnotationPublishVersion] = fmt.Sprintf("list-workflow-name-%d", i) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + } + + resp, err := workflowUsecase.ListWorkflowRecords(context.TODO(), workflow, 0, 10) + Expect(err).Should(BeNil()) + Expect(resp.Total).Should(Equal(int64(3))) + }) + + It("Test DetailWorkflowRecord function", func() { + By("create one workflow record to test detail workflow record") + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-123" + app.Annotations[oam.AnnotationDeployVersion] = "1234" + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + var revision = &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "1234", + Status: model.RevisionStatusInit, + DeployUser: "test-user", + Note: "test-commit", + TriggerType: "API", + WorkflowName: "test-workflow-2", + } + + err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) + Expect(err).Should(BeNil()) + + detail, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-123") + Expect(err).Should(BeNil()) + Expect(detail.WorkflowRecord.Name).Should(Equal("test-workflow-2-123")) + Expect(detail.DeployUser).Should(Equal("test-user")) + }) + + It("Test SyncWorkflowRecord function", func() { + By("create one workflow record to test sync status from application") + raw, err := yaml.YAMLToJSON([]byte(yamlStr)) + Expect(err).Should(BeNil()) + app := &v1beta1.Application{} + err = json.Unmarshal(raw, app) + Expect(err).Should(BeNil()) + app.Status.Workflow.Finished = false + app.Annotations[oam.AnnotationWorkflowName] = "test-workflow-2" + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-233" + app.Annotations[oam.AnnotationDeployVersion] = "4321" + workflow, err := workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + By("create one revision to test sync workflow record") + var revision = &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "4321", + Status: model.RevisionStatusInit, + DeployUser: "test-user", + WorkflowName: "test-workflow-2", + } + err = workflowUsecase.createTestApplicationRevision(context.TODO(), revision) + Expect(err).Should(BeNil()) + + By("create the application to sync") + ctx := context.Background() + app.Status.Workflow.Finished = true + err = workflowUsecase.kubeClient.Create(ctx, app) + Expect(err).Should(BeNil()) + err = workflowUsecase.kubeClient.Status().Patch(ctx, app, client.Merge) + Expect(err).Should(BeNil()) + err = workflowUsecase.SyncWorkflowRecord(ctx) + Expect(err).Should(BeNil()) + + workflow, err = workflowUsecase.GetWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, "test-workflow-2") + Expect(err).Should(BeNil()) + By("check the record") + record, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-233") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusComplete)) + + By("check the application revision") + err = workflowUsecase.ds.Get(ctx, revision) + Expect(err).Should(BeNil()) + Expect(revision.Status).Should(Equal(model.RevisionStatusComplete)) + + By("create another workflow record to test sync status from controller revision") + app.Status.Workflow.Finished = false + app.Annotations[oam.AnnotationPublishVersion] = "test-workflow-2-111" + app.Annotations[oam.AnnotationDeployVersion] = "1111" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + By("create another revision to test sync workflow record") + var anotherRevision = &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "1111", + Status: model.RevisionStatusInit, + DeployUser: "test-user", + WorkflowName: "test-workflow-2", + } + err = workflowUsecase.createTestApplicationRevision(context.TODO(), anotherRevision) + Expect(err).Should(BeNil()) + + By("create one controller revision to test sync workflow record") + Expect(err).Should(BeNil()) + cr := &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: "record-" + appName + "-test-workflow-2-111", + Namespace: "default", + Labels: map[string]string{"vela.io/wf-revision": "test-workflow-2-111"}, + }, + Data: runtime.RawExtension{Raw: raw}, + } + err = workflowUsecase.kubeClient.Create(ctx, cr) + Expect(err).Should(BeNil()) + + err = workflowUsecase.SyncWorkflowRecord(ctx) + Expect(err).Should(BeNil()) + + By("check the record") + anotherRecord, err := workflowUsecase.DetailWorkflowRecord(context.TODO(), workflow, "test-workflow-2-111") + Expect(err).Should(BeNil()) + Expect(anotherRecord.Status).Should(Equal(model.RevisionStatusComplete)) + + By("check the application revision") + err = workflowUsecase.ds.Get(ctx, anotherRevision) + Expect(err).Should(BeNil()) + Expect(anotherRevision.Status).Should(Equal(model.RevisionStatusComplete)) + }) + + It("Test ResumeRecord function", func() { + ctx := context.TODO() + + ResumeWorkflow := "resume-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: ResumeWorkflow, + Description: "this is a workflow", + EnvName: "resume", + } + + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "resume", "revision-resume1", ResumeWorkflow, "workflow-resume-1", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, &model.Workflow{Name: ResumeWorkflow}) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "revision-resume1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.ResumeRecord(ctx, &model.Application{ + Name: appName, + Namespace: "default", + }, &model.Workflow{Name: ResumeWorkflow, EnvName: "resume"}, "workflow-resume-1") + Expect(err).Should(BeNil()) + + record, err := workflowUsecase.DetailWorkflowRecord(ctx, &model.Workflow{Name: ResumeWorkflow, AppPrimaryKey: appName}, "workflow-resume-1") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusRunning)) + }) + + It("Test TerminateRecord function", func() { + ctx := context.TODO() + + workflowName := "terminate-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: workflowName, + Description: "this is a workflow", + EnvName: "terminate", + } + workflow := &model.Workflow{Name: workflowName, EnvName: "terminate"} + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "terminate", "revision-terminate1", workflow.Name, "test-workflow-2-1", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "revision-terminate1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.TerminateRecord(ctx, &model.Application{ + Name: appName, + Namespace: "default", + }, workflow, "test-workflow-2-1") + Expect(err).Should(BeNil()) + + record, err := workflowUsecase.DetailWorkflowRecord(ctx, workflow, "test-workflow-2-1") + Expect(err).Should(BeNil()) + Expect(record.Status).Should(Equal(model.RevisionStatusTerminated)) + }) + + It("Test RollbackRecord function", func() { + ctx := context.TODO() + + workflowName := "rollback-workflow" + req := apisv1.CreateWorkflowRequest{ + Name: workflowName, + Description: "this is a workflow", + EnvName: "rollback", + } + workflow := &model.Workflow{Name: workflowName, EnvName: "rollback"} + base, err := workflowUsecase.CreateOrUpdateWorkflow(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, req) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(base.Name, req.Name)).Should(BeEmpty()) + + app, err := createTestSuspendApp(ctx, appName, "rollback", "revision-rollback1", workflow.Name, "test-workflow-2-2", workflowUsecase.kubeClient) + Expect(err).Should(BeNil()) + + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "revision-rollback1", + Status: model.RevisionStatusRunning, + }) + Expect(err).Should(BeNil()) + err = workflowUsecase.createTestApplicationRevision(ctx, &model.ApplicationRevision{ + AppPrimaryKey: appName, + Version: "revision-rollback0", + ApplyAppConfig: `{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"annotations":{"app.oam.dev/workflowName":"test-workflow-2-2","app.oam.dev/deployVersion":"revision-rollback1","vela.io/publish-version":"workflow-rollback1"},"name":"first-vela-app","namespace":"default"},"spec":{"components":[{"name":"express-server","properties":{"image":"crccheck/hello-world","port":8000},"traits":[{"properties":{"domain":"testsvc.example.com","http":{"/":8000}},"type":"ingress-1-20"}],"type":"webservice"}]}}`, + Status: model.RevisionStatusComplete, + }) + Expect(err).Should(BeNil()) + + err = workflowUsecase.RollbackRecord(ctx, &model.Application{ + Name: appName, + Namespace: "default", + }, workflow, "test-workflow-2-2", "revision-rollback0") + Expect(err).Should(BeNil()) + + recordsNum, err := workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ + AppPrimaryKey: appName, + WorkflowName: workflow.Name, + RevisionPrimaryKey: "revision-rollback0", + }, nil) + Expect(err).Should(BeNil()) + Expect(recordsNum).Should(Equal(int64(1))) + + By("rollback application without revision version") + app.Annotations[oam.AnnotationPublishVersion] = "workflow-rollback-2" + err = workflowUsecase.CreateWorkflowRecord(context.TODO(), &model.Application{ + Name: appName, + Namespace: "default", + }, app, workflow) + Expect(err).Should(BeNil()) + + err = workflowUsecase.RollbackRecord(ctx, &model.Application{ + Name: appName, + Namespace: "default", + }, workflow, "workflow-rollback-2", "") + Expect(err).Should(BeNil()) + + recordsNum, err = workflowUsecase.ds.Count(ctx, &model.WorkflowRecord{ + AppPrimaryKey: appName, + WorkflowName: workflow.Name, + RevisionPrimaryKey: "revision-rollback0", + }, nil) + Expect(err).Should(BeNil()) + Expect(recordsNum).Should(Equal(int64(2))) + }) +}) + +var yamlStr = `apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + annotations: + app.oam.dev/workflowName: test-workflow-2 + app.oam.dev/deployVersion: "1234" + app.oam.dev/publishVersion: "test-workflow-name-111" + app.oam.dev/appName: "app-workflow" + name: app-workflow-dev + namespace: default +spec: + components: + - name: express-server + properties: + image: crccheck/hello-world + port: 8000 + type: webservice + workflow: + steps: + - name: apply-server + properties: + component: express-server + type: apply-component +status: + workflow: + steps: + - firstExecuteTime: "2021-10-26T11:19:33Z" + id: t8bpvi88d1 + lastExecuteTime: "2021-10-26T11:19:33Z" + name: apply-pvc + phase: succeeded + type: apply-object + - firstExecuteTime: "2021-10-26T11:19:33Z" + id: 9fou7rbq9r + lastExecuteTime: "2021-10-26T11:19:33Z" + name: apply-server + phase: succeeded + type: apply-component + suspend: false + terminated: false + finished: true` + +func (w *workflowUsecaseImpl) createTestApplicationRevision(ctx context.Context, revision *model.ApplicationRevision) error { + if err := w.ds.Add(ctx, revision); err != nil { + return err + } + return nil +} diff --git a/pkg/apiserver/rest/utils/bcode/addon.go b/pkg/apiserver/rest/utils/bcode/addon.go new file mode 100644 index 000000000..2e7566a10 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/addon.go @@ -0,0 +1,79 @@ +/* +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 bcode + +import ( + "github.com/pkg/errors" + + pkgaddon "github.com/oam-dev/kubevela/pkg/addon" +) + +var ( + // ErrAddonNotExist addon registry not exist + ErrAddonNotExist = NewBcode(404, 50001, "addon not exist") + + // ErrAddonRegistryExist addon registry already exist + ErrAddonRegistryExist = NewBcode(400, 50002, "addon registry already exists") + + // ErrAddonRegistryInvalid addon registry is exist + ErrAddonRegistryInvalid = NewBcode(400, 50003, "addon registry invalid") + + // ErrAddonRegistryRateLimit addon registry is rate limited by Github + ErrAddonRegistryRateLimit = NewBcode(400, 50004, "Exceed Github rate limit") + + // ErrAddonRegistryNotExist addon registry doesn't exist + ErrAddonRegistryNotExist = NewBcode(400, 50006, "addon registry doesn't exist") + + // ErrAddonRender fail to render addon application + ErrAddonRender = NewBcode(500, 50010, "addon render fail") + + // ErrAddonApply fail to apply application to cluster + ErrAddonApply = NewBcode(500, 50011, "fail to apply addon application") + + // ErrReadGit fail to get addon application + ErrReadGit = NewBcode(500, 50012, "fail to read git repo") + + // ErrGetAddonApplication fail to get addon application + ErrGetAddonApplication = NewBcode(500, 50013, "fail to get addon application") + + // ErrAddonIsEnabled means addon has been enabled + ErrAddonIsEnabled = NewBcode(500, 50014, "addon has been enabled") + + // ErrAddonSecretApply means fail to apply addon argument secret + ErrAddonSecretApply = NewBcode(500, 50015, "fail to apply addon argument secret") + + // ErrAddonSecretGet means fail to get addon argument secret + ErrAddonSecretGet = NewBcode(500, 50016, "fail to get addon argument secret") +) + +// isGithubRateLimit check if error is github rate limit +func isGithubRateLimit(err error) bool { + return errors.Is(err, pkgaddon.ErrRateLimit) +} + +// WrapGithubRateLimitErr wraps error if it is github rate limit +func WrapGithubRateLimitErr(err error) error { + if isGithubRateLimit(err) { + return ErrAddonRegistryRateLimit + } + return err +} + +// NewBcodeWrapErr new bcode error +func NewBcodeWrapErr(httpCode, businessCode int32, err error, message string) error { + return NewBcode(httpCode, businessCode, errors.Wrap(err, message).Error()) +} diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go index 273e12f3f..21e9743d9 100644 --- a/pkg/apiserver/rest/utils/bcode/application.go +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -24,3 +24,57 @@ var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component // ErrApplicationExist application is exist var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") + +// ErrInvalidProperties properties(trait or component or others) is invalid +var ErrInvalidProperties = NewBcode(400, 10003, "properties is invalid") + +// ErrDeployConflict Occurs when a new event is triggered before the last deployment event has completed. +var ErrDeployConflict = NewBcode(400, 10004, "application deploy conflict") + +// ErrDeployApplyFail Failed to update an application to the control cluster. +var ErrDeployApplyFail = NewBcode(500, 10005, "application deploy apply failure") + +// ErrNoComponent no component +var ErrNoComponent = NewBcode(200, 10006, "application not have components, can not deploy") + +// ErrApplicationComponetExist application component is exist +var ErrApplicationComponetExist = NewBcode(400, 10007, "application component is exist") + +// ErrApplicationComponetNotExist application component is not exist +var ErrApplicationComponetNotExist = NewBcode(404, 10008, "application component is not exist") + +// ErrApplicationPolicyExist application policy is exist +var ErrApplicationPolicyExist = NewBcode(400, 10009, "application policy is exist") + +// ErrApplicationPolicyNotExist application policy is not exist +var ErrApplicationPolicyNotExist = NewBcode(404, 10010, "application policy is not exist") + +// ErrCreateNamespace auto create namespace failure before deploy app +var ErrCreateNamespace = NewBcode(500, 10011, "auto create namespace failure") + +// ErrApplicationNotExist application is not exist +var ErrApplicationNotExist = NewBcode(404, 10012, "application name is not exist") + +// ErrApplicationNotEnv no env binding policy +var ErrApplicationNotEnv = NewBcode(404, 10013, "application not set env binding") + +// ErrApplicationEnvExist application env is exist +var ErrApplicationEnvExist = NewBcode(400, 10014, "application env is exist") + +// ErrTraitNotExist trait is not exist +var ErrTraitNotExist = NewBcode(400, 10015, "trait is not exist") + +// ErrTraitAlreadyExist trait is already exist +var ErrTraitAlreadyExist = NewBcode(400, 10016, "trait is already exist") + +// ErrApplicationNoReadyRevision application not have ready revision +var ErrApplicationNoReadyRevision = NewBcode(400, 10017, "application not have ready revision") + +// ErrApplicationRevisionNotExist application revision is not exist +var ErrApplicationRevisionNotExist = NewBcode(404, 10018, "application revision is not exist") + +// ErrApplicationRefusedDelete The application cannot be deleted because it has been deployed +var ErrApplicationRefusedDelete = NewBcode(400, 10019, "The application cannot be deleted because it has been deployed") + +// ErrApplicationEnvRefusedDelete The application env cannot be deleted because it has been deployed +var ErrApplicationEnvRefusedDelete = NewBcode(400, 10020, "The application envbinding cannot be deleted because it has been deployed") diff --git a/pkg/apiserver/rest/utils/bcode/bcode.go b/pkg/apiserver/rest/utils/bcode/bcode.go index 3fab7ac7d..0454779bf 100644 --- a/pkg/apiserver/rest/utils/bcode/bcode.go +++ b/pkg/apiserver/rest/utils/bcode/bcode.go @@ -23,9 +23,13 @@ import ( restful "github.com/emicklei/go-restful/v3" "github.com/go-playground/validator/v10" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" "github.com/oam-dev/kubevela/pkg/apiserver/log" ) +// ErrServer an unexpected mistake. +var ErrServer = NewBcode(500, 500, "The service has lapsed.") + // Bcode business error code type Bcode struct { HTTPCode int32 `json:"-"` @@ -61,6 +65,13 @@ func ReturnError(req *restful.Request, res *restful.Response, err error) { } return } + + if errors.Is(err, datastore.ErrRecordNotExist) { + if err := res.WriteHeaderAndEntity(int(404), err); err != nil { + log.Logger.Error("write entity failure %s", err.Error()) + } + return + } var restfulerr restful.ServiceError if errors.As(err, &restfulerr) { if err := res.WriteHeaderAndEntity(restfulerr.Code, Bcode{HTTPCode: int32(restfulerr.Code), BusinessCode: int32(restfulerr.Code), Message: restfulerr.Message}); err != nil { diff --git a/pkg/apiserver/rest/utils/bcode/bcode_suite_test.go b/pkg/apiserver/rest/utils/bcode/bcode_suite_test.go new file mode 100644 index 000000000..fcbf6c8e2 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/bcode_suite_test.go @@ -0,0 +1,29 @@ +/* +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 bcode + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestBcode(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Bcode Suite") +} diff --git a/pkg/apiserver/rest/utils/bcode/bcode_test.go b/pkg/apiserver/rest/utils/bcode/bcode_test.go new file mode 100644 index 000000000..7ef5e62c7 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/bcode_test.go @@ -0,0 +1,31 @@ +/* +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 bcode + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test bcode package", func() { + It("Test New bcode funtion", func() { + bcode := NewBcode(400, 4000, "test") + Expect(bcode).ShouldNot(BeNil()) + Expect(bcode.Message).ShouldNot(BeNil()) + Expect(bcode.Error()).ShouldNot(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/utils/bcode/cluster.go b/pkg/apiserver/rest/utils/bcode/cluster.go new file mode 100644 index 000000000..52f515107 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/cluster.go @@ -0,0 +1,62 @@ +/* +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 bcode + +// ErrInvalidCloudClusterProvider provider is not support now +var ErrInvalidCloudClusterProvider = NewBcode(400, 40000, "provider is not support") + +// ErrKubeConfigSecretNotSupport kubeConfig secret is not support +var ErrKubeConfigSecretNotSupport = NewBcode(400, 40001, "kubeConfig secret is not supported now") + +// ErrKubeConfigAndSecretIsNotSet kubeConfig and kubeConfigSecret are not set +var ErrKubeConfigAndSecretIsNotSet = NewBcode(400, 40002, "kubeConfig or kubeConfig secret must be provided") + +// ErrClusterNotFoundInDataStore cluster not found in datastore +var ErrClusterNotFoundInDataStore = NewBcode(404, 40003, "cluster not found in data store") + +// ErrClusterAlreadyExistInDataStore cluster exists in datastore +var ErrClusterAlreadyExistInDataStore = NewBcode(400, 40004, "cluster already exists in data store") + +// ErrGetCloudClusterFailure get cloud cluster failed +var ErrGetCloudClusterFailure = NewBcode(500, 40005, "get cloud cluster information failed") + +// ErrClusterExistsInKubernetes cluster exists in kubernetes +var ErrClusterExistsInKubernetes = NewBcode(400, 40006, "cluster already exists in kubernetes") + +// ErrLocalClusterReserved cluster name reserved for local +var ErrLocalClusterReserved = NewBcode(400, 40007, "local cluster is reserved") + +// ErrLocalClusterImmutable local cluster kubeConfig is immutable +var ErrLocalClusterImmutable = NewBcode(400, 40008, "local cluster is immutable") + +// ErrCloudClusterAlreadyExists cloud cluster already exists +var ErrCloudClusterAlreadyExists = NewBcode(400, 40009, "cloud cluster already exists") + +// ErrTerraformConfigurationNotFound cannot find terraform configuration +var ErrTerraformConfigurationNotFound = NewBcode(404, 40010, "cannot find terraform configuration") + +// ErrClusterIDNotFoundInTerraformConfiguration cannot find cluster_id in terraform configuration +var ErrClusterIDNotFoundInTerraformConfiguration = NewBcode(500, 40011, "cannot find cluster_id in terraform configuration") + +// ErrBootstrapTerraformConfiguration failed to bootstrap terraform configuration +var ErrBootstrapTerraformConfiguration = NewBcode(500, 40012, "failed to bootstrap terraform configuration") + +// ErrInvalidAccessKeyOrSecretKey access key or secret key is invalid +var ErrInvalidAccessKeyOrSecretKey = NewBcode(400, 40013, "access key or secret key is invalid") + +// ErrClusterCreateNamespaceNoPermission cluster create namespace is forbidden +var ErrClusterCreateNamespaceNoPermission = NewBcode(401, 40014, "no permission to create namespace in cluster") diff --git a/pkg/apiserver/rest/utils/bcode/definition.go b/pkg/apiserver/rest/utils/bcode/definition.go new file mode 100644 index 000000000..3a89f6b5f --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/definition.go @@ -0,0 +1,29 @@ +/* +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 bcode + +// ErrDefinitionNotFound definition is not exist +var ErrDefinitionNotFound = NewBcode(404, 70001, "definition is not exist") + +// ErrDefinitionNoSchema definition not have schema +var ErrDefinitionNoSchema = NewBcode(400, 70002, "definition not have schema") + +// ErrDefinitionTypeNotSupport definition type not support +var ErrDefinitionTypeNotSupport = NewBcode(400, 70003, "definition type not support") + +// ErrInvalidDefinitionUISchema invalid custom definition ui schema +var ErrInvalidDefinitionUISchema = NewBcode(400, 70004, "invalid custom defnition ui schema") diff --git a/pkg/apiserver/rest/utils/bcode/delivery_target.go b/pkg/apiserver/rest/utils/bcode/delivery_target.go new file mode 100644 index 000000000..727cf72f7 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/delivery_target.go @@ -0,0 +1,26 @@ +/* +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 bcode + +// ErrDeliveryTargetExist deliveryTarget is exist +var ErrDeliveryTargetExist = NewBcode(400, 80001, "deliveryTarget is exist") + +// ErrDeliveryTargetNotExist deliveryTarget is not exist +var ErrDeliveryTargetNotExist = NewBcode(404, 80002, "deliveryTarget is not exist") + +// ErrDeliveryTargetInUseCantDeleted deliveryTarget being used +var ErrDeliveryTargetInUseCantDeleted = NewBcode(404, 80003, "deliveryTarget in use, can't be deleted") diff --git a/pkg/apiserver/rest/utils/bcode/envbinding.go b/pkg/apiserver/rest/utils/bcode/envbinding.go new file mode 100644 index 000000000..6d4ad0ad1 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/envbinding.go @@ -0,0 +1,35 @@ +/* +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 bcode + +// ErrEnvbindingDeliveryTargetNotAllExist application envbinding deliveryTarget is not exist +var ErrEnvbindingDeliveryTargetNotAllExist = NewBcode(400, 90001, "application envbinding deliveryTarget is not all exist") + +// ErrFoundEnvbindingDeliveryTarget found application envbinding deliveryTarget failure +var ErrFoundEnvbindingDeliveryTarget = NewBcode(400, 90002, "found application envbinding deliveryTarget failure") + +// ErrEnvBindingNotExist application envbinding is not exist +var ErrEnvBindingNotExist = NewBcode(400, 90003, "application envbinding not exist") + +// ErrEnvBindingsNotExist application envbindings is not exist +var ErrEnvBindingsNotExist = NewBcode(400, 90004, "application envbinding is not exist") + +// ErrEnvBindingExist application envbinding is exist +var ErrEnvBindingExist = NewBcode(400, 90005, "application envbinding is exist") + +// ErrEnvBindingUpdateWorkflow application envbinding update workflow error +var ErrEnvBindingUpdateWorkflow = NewBcode(400, 90006, "application envbinding update workflow error") diff --git a/pkg/apiserver/rest/utils/bcode/namespace.go b/pkg/apiserver/rest/utils/bcode/namespace.go new file mode 100644 index 000000000..d9145f9c9 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/namespace.go @@ -0,0 +1,23 @@ +/* +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 bcode + +// ErrNamespaceQuery query namespace failure from k8s api +var ErrNamespaceQuery = NewBcode(500, 30001, "query namespace list from cluster failure") + +// ErrNamespaceIsExist namespace name is exist +var ErrNamespaceIsExist = NewBcode(400, 30002, "namespace name is exist") diff --git a/pkg/apiserver/rest/utils/bcode/velaql.go b/pkg/apiserver/rest/utils/bcode/velaql.go new file mode 100644 index 000000000..89c77e025 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/velaql.go @@ -0,0 +1,26 @@ +/* + 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 bcode + +// ErrParseVelaQL failed to parse velaQL +var ErrParseVelaQL = NewBcode(400, 60001, "fail to parse the velaQL") + +// ErrViewQuery failed to query view +var ErrViewQuery = NewBcode(400, 60002, "view query failed") + +// ErrParseQuery2Json failed to parse query result to response +var ErrParseQuery2Json = NewBcode(400, 60003, "fail to parse query result to json format") diff --git a/pkg/apiserver/rest/utils/bcode/workflow.go b/pkg/apiserver/rest/utils/bcode/workflow.go new file mode 100644 index 000000000..7296bac83 --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/workflow.go @@ -0,0 +1,35 @@ +/* +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 bcode + +// ErrWorkflowNotExist application workflow is not exist +var ErrWorkflowNotExist = NewBcode(404, 20002, "application workflow is not exist") + +// ErrWorkflowExist application workflow is exist +var ErrWorkflowExist = NewBcode(404, 20003, "application workflow is exist") + +// ErrWorkflowNoDefault application default workflow is not exist +var ErrWorkflowNoDefault = NewBcode(404, 20004, "application default workflow is not exist") + +// ErrMustQueryByApp you can only query the Workflow list based on applications. +var ErrMustQueryByApp = NewBcode(404, 20005, "you can only query the Workflow list based on applications.") + +// ErrWorkflowNoEnv workflow have not env +var ErrWorkflowNoEnv = NewBcode(400, 20006, "workflow must set env name") + +// ErrWorkflowRecordNotExist workflow record is not exist +var ErrWorkflowRecordNotExist = NewBcode(404, 20007, "workflow record is not exist") diff --git a/pkg/apiserver/rest/utils/cache.go b/pkg/apiserver/rest/utils/cache.go new file mode 100644 index 000000000..616e0db92 --- /dev/null +++ b/pkg/apiserver/rest/utils/cache.go @@ -0,0 +1,41 @@ +/* +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 "time" + +// MemoryCache memory cache, support time expired +type MemoryCache struct { + data interface{} + cacheDuration time.Duration + startTime time.Time +} + +// NewMemoryCache new memory cache instance +func NewMemoryCache(data interface{}, cacheDuration time.Duration) *MemoryCache { + return &MemoryCache{data: data, cacheDuration: cacheDuration, startTime: time.Now()} +} + +// IsExpired whether the cache data expires +func (m *MemoryCache) IsExpired() bool { + return time.Now().After(m.startTime.Add(m.cacheDuration)) +} + +// GetData get cache data +func (m *MemoryCache) GetData() interface{} { + return m.data +} diff --git a/pkg/apiserver/rest/utils/cache_test.go b/pkg/apiserver/rest/utils/cache_test.go new file mode 100644 index 000000000..222112360 --- /dev/null +++ b/pkg/apiserver/rest/utils/cache_test.go @@ -0,0 +1,15 @@ +package utils + +import ( + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test cache utils", func() { + It("should return false for IsExpired()", func() { + c := NewMemoryCache("test", 10*time.Hour) + Expect(c.IsExpired()).Should(BeFalse()) + }) +}) diff --git a/pkg/apiserver/rest/utils/params.go b/pkg/apiserver/rest/utils/params.go new file mode 100644 index 000000000..141721aab --- /dev/null +++ b/pkg/apiserver/rest/utils/params.go @@ -0,0 +1,58 @@ +/* +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 ( + "strconv" + + "github.com/emicklei/go-restful/v3" + "github.com/pkg/errors" +) + +const defaultPageSize = "10" + +// ExtractPagingParams extract `page` and `pageSize` params from request +func ExtractPagingParams(req *restful.Request, minPageSize, maxPageSize int) (int, int, error) { + pageStr := req.QueryParameter("page") + pageSizeStr := req.QueryParameter("pageSize") + if pageStr == "" { + pageStr = "0" + } + if pageSizeStr == "" { + pageSizeStr = defaultPageSize + } + page64, err := strconv.ParseInt(pageStr, 10, 32) + if err != nil { + return 0, 0, errors.Errorf("invalid page %s: %v", pageStr, err) + } + pageSize64, err := strconv.ParseInt(pageSizeStr, 10, 32) + if err != nil { + return 0, 0, errors.Errorf("invalid pageSize %s: %v", pageSizeStr, err) + } + page := int(page64) + pageSize := int(pageSize64) + if page < 0 { + page = 0 + } + if pageSize < minPageSize { + pageSize = minPageSize + } + if pageSize > maxPageSize { + pageSize = maxPageSize + } + return page, pageSize, nil +} diff --git a/pkg/apiserver/rest/utils/uiswagger.go b/pkg/apiserver/rest/utils/uiswagger.go new file mode 100644 index 000000000..c635382d5 --- /dev/null +++ b/pkg/apiserver/rest/utils/uiswagger.go @@ -0,0 +1,132 @@ +/* +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 ( + "fmt" + "strings" +) + +// UIParameter Structured import table simple UI model +type UIParameter struct { + Sort uint `json:"sort"` + Label string `json:"label"` + Description string `json:"description"` + Validate *Validate `json:"validate,omitempty"` + JSONKey string `json:"jsonKey"` + UIType string `json:"uiType"` + // means only can be read. + Disable *bool `json:"disable,omitempty"` + SubParameterGroupOption []GroupOption `json:"subParameterGroupOption,omitempty"` + SubParameters []*UIParameter `json:"subParameters,omitempty"` +} + +// GroupOption define multiple data structure composition options. +type GroupOption struct { + Label string `json:"label"` + Keys []string `json:"keys"` +} + +// Validate parameter validate rule +type Validate struct { + Required bool `json:"required,omitempty"` + Max *float64 `json:"max,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty"` + Min *float64 `json:"min,omitempty"` + MinLength uint64 `json:"minLength,omitempty"` + Pattern string `json:"pattern,omitempty"` + Options []Option `json:"options,omitempty"` + DefaultValue interface{} `json:"defaultValue,omitempty"` +} + +// Option select option +type Option struct { + Label string `json:"label"` + Value interface{} `json:"value"` +} + +// ParseUIParameterFromDefinition cue of parameter in Definitions was analyzed to obtain the form description model. +func ParseUIParameterFromDefinition(definition []byte) ([]*UIParameter, error) { + var params []*UIParameter + + return params, nil +} + +// FirstUpper Sets the first letter of the string to upper. +func FirstUpper(s string) string { + if s == "" { + return "" + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// FirstLower Sets the first letter of the string to lowercase. +func FirstLower(s string) string { + if s == "" { + return "" + } + return strings.ToLower(s[:1]) + s[1:] +} + +// GetDefaultUIType Set the default mapping for API Schema Type +func GetDefaultUIType(apiType string, haveOptions bool, subType string) string { + switch apiType { + case "string": + if haveOptions { + return "Select" + } + return "Input" + case "number", "integer": + return "Number" + case "boolean": + return "Switch" + case "array": + if subType == "string" { + return "Strings" + } + if subType == "number" || subType == "integer" { + return "Numbers" + } + return "Structs" + case "object": + return "KV" + default: + return "Input" + } +} + +// RenderLabel render option label +func RenderLabel(source interface{}) string { + switch v := source.(type) { + case int: + return fmt.Sprintf("%d", v) + case string: + return FirstUpper(v) + default: + return FirstUpper(fmt.Sprintf("%v", v)) + } +} + +// StringsContain strings contain +func StringsContain(items []string, source string) bool { + for _, item := range items { + if item == source { + return true + } + } + return false +} diff --git a/pkg/apiserver/rest/utils/utils_suite_test.go b/pkg/apiserver/rest/utils/utils_suite_test.go new file mode 100644 index 000000000..a75298da7 --- /dev/null +++ b/pkg/apiserver/rest/utils/utils_suite_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestUtils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Suite") +} diff --git a/pkg/apiserver/rest/utils/version.go b/pkg/apiserver/rest/utils/version.go new file mode 100644 index 000000000..153eae651 --- /dev/null +++ b/pkg/apiserver/rest/utils/version.go @@ -0,0 +1,34 @@ +/* +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 ( + "fmt" + "time" + + "cuelang.org/go/pkg/strings" +) + +// GenerateVersion Generate version numbers by time +func GenerateVersion(pre string) string { + timeStr := time.Now().Format("20060102150405.000") + timeStr = strings.Replace(timeStr, ".", "", 1) + if pre != "" { + return fmt.Sprintf("%s-%s", pre, timeStr) + } + return timeStr +} diff --git a/pkg/apiserver/rest/utils/version_test.go b/pkg/apiserver/rest/utils/version_test.go new file mode 100644 index 000000000..18f38e6a7 --- /dev/null +++ b/pkg/apiserver/rest/utils/version_test.go @@ -0,0 +1,35 @@ +/* +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 ( + "strings" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Test version utils", func() { + It("Test New version funtion", func() { + s := GenerateVersion("") + Expect(s).ShouldNot(BeNil()) + + s2 := GenerateVersion("pre") + Expect(cmp.Diff(strings.HasPrefix(s2, "pre-"), true)).ShouldNot(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/webservice/addon.go b/pkg/apiserver/rest/webservice/addon.go index 182266c31..b92f421ff 100644 --- a/pkg/apiserver/rest/webservice/addon.go +++ b/pkg/apiserver/rest/webservice/addon.go @@ -20,15 +20,27 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/apis/types" + 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" ) -type addonWebService struct { +// NewAddonWebService returns addon web service +func NewAddonWebService(u usecase.AddonUsecase) WebService { + return &addonWebService{ + addonUsecase: u, + } } -func (c *addonWebService) GetWebService() *restful.WebService { +type addonWebService struct { + addonUsecase usecase.AddonUsecase +} + +func (s *addonWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) - ws.Path("/v1/addons"). + ws.Path(versionPrefix+"/addons"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for addon management") @@ -36,53 +48,136 @@ func (c *addonWebService) GetWebService() *restful.WebService { tags := []string{"addon"} // List - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(s.listAddons). Doc("list all addons"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). - Writes(apis.ListAddonResponse{}).Do(returns200, returns500)) - - // Create - ws.Route(ws.POST("/").To(noop). - Doc("create an addon"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Reads(apis.CreateAddonRequest{}). - Writes(apis.AddonMeta{})) - - // Delete - ws.Route(ws.DELETE("/{name}").To(noop). - Doc("delete an addon"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). - Writes(apis.AddonMeta{})) + 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, "", apis.ListAddonResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListAddonResponse{})) // GET - ws.Route(ws.GET("/{name}").To(noop). + ws.Route(ws.GET("/{name}").To(s.detailAddon). Doc("show details of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailAddonResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.PathParameter("name", "addon name to query detail").DataType("string").Required(true)). + Param(ws.QueryParameter("registry", "filter addons from given registry").DataType("string")). Writes(apis.DetailAddonResponse{})) // GET status - ws.Route(ws.GET("/{name}/status").To(noop). + ws.Route(ws.GET("/{name}/status").To(s.statusAddon). Doc("show status of an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the addon").DataType("string")). + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.PathParameter("name", "addon name to query status").DataType("string").Required(true)). Writes(apis.AddonStatusResponse{})) - // vela enable addon - ws.Route(ws.POST("/{name}/enable").To(noop). - Doc("enable an addon on a cluster"). + // enable addon + ws.Route(ws.POST("/{name}/enable").To(s.enableAddon). + Doc("enable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "cluster name").DataType("string")). - Writes(apis.AddonMeta{})) + Reads(apis.EnableAddonRequest{}). + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). + Writes(apis.AddonStatusResponse{})) - // vela disable addon - ws.Route(ws.POST("/{name}/disable").To(noop). - Doc("disable an addon on a cluster"). + // disable addon + ws.Route(ws.POST("/{name}/disable").To(s.disableAddon). + Doc("disable an addon"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("cluster", "cluster name").DataType("string")). - Writes(apis.AddonMeta{})) + Returns(200, "", apis.AddonStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Param(ws.PathParameter("name", "addon name to enable").DataType("string").Required(true)). + Writes(apis.AddonStatusResponse{})) return ws } + +func (s *addonWebService) listAddons(req *restful.Request, res *restful.Response) { + detailAddons, err := s.addonUsecase.ListAddons(req.Request.Context(), req.QueryParameter("registry"), req.QueryParameter("query")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + var addons []*types.AddonMeta + + for _, d := range detailAddons { + addons = append(addons, &d.AddonMeta) + } + + err = res.WriteEntity(apis.ListAddonResponse{Addons: addons}) + if err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonWebService) detailAddon(req *restful.Request, res *restful.Response) { + name := req.PathParameter("name") + addon, err := s.addonUsecase.GetAddon(req.Request.Context(), name, req.QueryParameter("registry")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + err = res.WriteEntity(addon) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + +} + +func (s *addonWebService) enableAddon(req *restful.Request, res *restful.Response) { + var createReq apis.EnableAddonRequest + err := req.ReadEntity(&createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err = validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + name := req.PathParameter("name") + err = s.addonUsecase.EnableAddon(req.Request.Context(), name, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + s.statusAddon(req, res) +} + +func (s *addonWebService) disableAddon(req *restful.Request, res *restful.Response) { + name := req.PathParameter("name") + err := s.addonUsecase.DisableAddon(req.Request.Context(), name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + s.statusAddon(req, res) +} + +func (s *addonWebService) statusAddon(req *restful.Request, res *restful.Response) { + name := req.PathParameter("name") + status, err := s.addonUsecase.StatusAddon(req.Request.Context(), name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + err = res.WriteEntity(*status) + if err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/addon_registry.go b/pkg/apiserver/rest/webservice/addon_registry.go new file mode 100644 index 000000000..2fa89d573 --- /dev/null +++ b/pkg/apiserver/rest/webservice/addon_registry.go @@ -0,0 +1,164 @@ +/* +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 webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + "github.com/emicklei/go-restful/v3" + + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NewAddonRegistryWebService returns addon registry web service +func NewAddonRegistryWebService(u usecase.AddonUsecase) WebService { + return &addonRegistryWebService{ + addonUsecase: u, + } +} + +type addonRegistryWebService struct { + addonUsecase usecase.AddonUsecase +} + +func (s *addonRegistryWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/addon_registries"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for addon registry management") + + tags := []string{"addon_registry"} + + // Create + ws.Route(ws.POST("/").To(s.createAddonRegistry). + Doc("create an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateAddonRegistryRequest{}). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + + ws.Route(ws.GET("/").To(s.listAddonRegistry). + Doc("list all addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListAddonRegistryResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListAddonRegistryResponse{})) + + // Delete + ws.Route(ws.DELETE("/{name}").To(s.deleteAddonRegistry). + Doc("delete an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + + ws.Route(ws.PUT("/{name}").To(s.updateAddonRegistry). + Doc("update an addon registry"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateAddonRegistryRequest{}). + Param(ws.PathParameter("name", "identifier of the addon registry").DataType("string")). + Returns(200, "", apis.AddonRegistryMeta{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.AddonRegistryMeta{})) + + return ws +} + +func (s *addonRegistryWebService) createAddonRegistry(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateAddonRegistryRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + meta, err := s.addonUsecase.CreateAddonRegistry(req.Request.Context(), createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(meta); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonRegistryWebService) deleteAddonRegistry(req *restful.Request, res *restful.Response) { + r, err := s.addonUsecase.GetAddonRegistry(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + err = s.addonUsecase.DeleteAddonRegistry(req.Request.Context(), r.Name) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(*usecase.ConvertAddonRegistryModel2AddonRegistryMeta(r)); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonRegistryWebService) listAddonRegistry(req *restful.Request, res *restful.Response) { + registrys, err := s.addonUsecase.ListAddonRegistries(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListAddonRegistryResponse{Registrys: registrys}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (s *addonRegistryWebService) updateAddonRegistry(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var updateReq apis.UpdateAddonRegistryRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + meta, err := s.addonUsecase.UpdateAddonRegistry(req.Request.Context(), req.PathParameter("name"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(meta); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 3ae3f2f22..cc3ab60cf 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -5,7 +5,7 @@ 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 + http://wwc.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, @@ -17,22 +17,35 @@ limitations under the License. package webservice import ( + "context" + + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "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/apiserver/rest/usecase" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) type applicationWebService struct { + workflowWebService applicationUsecase usecase.ApplicationUsecase + envBindingUsecase usecase.EnvBindingUsecase } // NewApplicationWebService new application manage webservice -func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService { +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase, envBindingUsecase usecase.EnvBindingUsecase, workflowUsecase usecase.WorkflowUsecase) WebService { return &applicationWebService{ + workflowWebService: workflowWebService{ + workflowUsecase: workflowUsecase, + applicationUsecase: applicationUsecase, + }, applicationUsecase: applicationUsecase, + envBindingUsecase: envBindingUsecase, } } @@ -49,75 +62,417 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Doc("list all applications"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). - Param(ws.QueryParameter("namespace", "Namespace-based search").DataType("string")). - Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). + Param(ws.QueryParameter("namespace", "The namespace of the managed cluster").DataType("string")). + Param(ws.QueryParameter("target", "Name of the application delivery target").DataType("string")). + Returns(200, "", apis.ListApplicationResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ListApplicationResponse{})) ws.Route(ws.POST("/").To(c.createApplication). - Doc("create one application"). + Doc("create one application "). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationBase{})) - ws.Route(ws.DELETE("/{name}").To(noop). + ws.Route(ws.DELETE("/{name}").To(c.deleteApplication). Doc("delete one application"). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Writes(apis.ApplicationBase{})) + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) - ws.Route(ws.GET("/{name}").To(noop). - Doc("detail one application"). + ws.Route(ws.GET("/{name}").To(c.detailApplication). + Doc("detail one application "). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.DetailApplicationResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailApplicationResponse{})) - ws.Route(ws.POST("/{name}/template").To(noop). - Doc("create one application template"). + ws.Route(ws.PUT("/{name}").To(c.updateApplication). + Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Reads(apis.CreateApplicationTemplateRequest{}). - Writes(apis.ApplicationTemplateBase{})) + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.UpdateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationBase{})) + ws.Route(ws.GET("/{name}/statistics").To(c.applicationStatistics). + Doc("detail one application "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.ApplicationStatisticsResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationStatisticsResponse{})) - ws.Route(ws.POST("/{name}/deploy").To(noop). - Doc("deploy or update the application"). + ws.Route(ws.PUT("/{name}").To(c.updateApplication). + Doc("update one application "). Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.UpdateApplicationRequest{}). + Returns(200, "", apis.ApplicationBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ApplicationBase{})) - ws.Route(ws.GET("/{name}/components").To(noop). - Doc("gets the component topology of the application"). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). - Param(ws.PathParameter("cluster", "list components that deployed in define cluster").DataType("string")). + ws.Route(ws.POST("/{name}/template").To(c.publishApplicationTemplate). + Doc("create one application template"). Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.CreateApplicationTemplateRequest{}). + Returns(200, "", apis.ApplicationTemplateBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationTemplateBase{})) + + ws.Route(ws.POST("/{name}/deploy").To(c.deployApplication). + Doc("deploy or upgrade the application"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Returns(200, "", apis.ApplicationDeployRequest{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationDeployResponse{})) + + ws.Route(ws.GET("/{name}/components").To(c.listApplicationComponents). + Doc("gets the list of application components"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.QueryParameter("envName", "list components that deployed in define env").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ComponentListResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ComponentListResponse{})) - ws.Route(ws.POST("/{name}/components").To(noop). - Doc("create component for application"). - Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + ws.Route(ws.POST("/{name}/components").To(c.createComponent). + Doc("create component for application "). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateComponentRequest{}). + Returns(200, "", apis.ComponentBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ComponentBase{})) - ws.Route(ws.POST("/{name}/policies").To(noop). + ws.Route(ws.GET("/{name}/components/{componentName}").To(c.detailComponent). + Doc("detail component for application "). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailComponentResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailComponentResponse{})) + + ws.Route(ws.PUT("/{name}/components/{componentName}").To(c.updateComponent). + Doc("update component config"). + Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateApplicationComponentRequest{}). + Returns(200, "", apis.ComponentBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ComponentBase{})) + + ws.Route(ws.GET("/{name}/policies").To(c.listApplicationPolicies). + Doc("list policy for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListApplicationPolicy{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListApplicationPolicy{})) + + ws.Route(ws.POST("/{name}/policies").To(c.createApplicationPolicy). Doc("create policy for application"). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreatePolicyRequest{}). - Writes(apis.DetailPolicyResponse{})) + Returns(200, "", apis.PolicyBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.PolicyBase{})) - ws.Route(ws.GET("/{name}/policies/{policyName}").To(noop). + ws.Route(ws.GET("/{name}/policies/{policyName}").To(c.detailApplicationPolicy). Doc("detail policy for application"). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailPolicyResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) - ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(noop). + ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(c.deleteApplicationPolicy). Doc("detail policy for application"). + Filter(c.appCheckFilter). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.PUT("/{name}/policies/{policyName}").To(c.updateApplicationPolicy). + Doc("update policy for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdatePolicyRequest{}). + Returns(200, "", apis.DetailPolicyResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailPolicyResponse{})) + + ws.Route(ws.POST("/{name}/components/{compName}/traits").To(c.addApplicationTrait). + Doc("add trait for a component"). + Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateApplicationTraitRequest{}). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationTrait{})) + + ws.Route(ws.PUT("/{name}/components/{compName}/traits/{traitType}").To(c.updateApplicationTrait). + Doc("update trait from a component"). + Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.UpdateApplicationTraitRequest{}). + Returns(200, "", apis.ApplicationTrait{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationTrait{})) + + ws.Route(ws.DELETE("/{name}/components/{compName}/traits/{traitType}").To(c.deleteApplicationTrait). + Doc("delete trait from a component"). + Filter(c.appCheckFilter). + Filter(c.componentCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("compName", "identifier of the component").DataType("string")). + Param(ws.PathParameter("traitType", "identifier of the type of trait").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ApplicationTrait{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{name}/revisions").To(c.listApplicationRevisions). + Doc("list revisions for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.QueryParameter("envName", "query identifier of the env").DataType("string")). + Param(ws.QueryParameter("status", "query identifier of the status").DataType("string")). + Param(ws.QueryParameter("page", "query the page number").DataType("integer")). + Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListRevisionsResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListRevisionsResponse{})) + + ws.Route(ws.GET("/{name}/revisions/{revision}").To(c.detailApplicationRevision). + Doc("detail revision for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("revision", "identifier of the application revision").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.DetailRevisionResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailRevisionResponse{})) + + ws.Route(ws.GET("/{name}/envs").To(c.listApplicationEnvs). + Doc("list policy for application"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListApplicationEnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListApplicationEnvBinding{})) + + ws.Route(ws.POST("/{name}/envs").To(c.createApplicationEnv). + Doc("creating an application environment "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Reads(apis.CreateApplicationEnvRequest{}). + Returns(200, "", apis.EnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.PUT("/{name}/envs/{envName}").To(c.updateApplicationEnv). + Doc("set application differences in the specified environment"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). + Reads(apis.PutApplicationEnvRequest{}). + Returns(200, "", apis.EnvBinding{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EnvBinding{})) + + ws.Route(ws.DELETE("/{name}/envs/{envName}").To(c.deleteApplicationEnv). + Doc("delete an application environment "). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the envBinding ").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Returns(404, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{name}/envs/{envName}/status").To(c.getApplicationStatus). + Doc("get application status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string")). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string")). + Returns(200, "", apis.ApplicationStatusResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ApplicationStatusResponse{})) + + ws.Route(ws.POST("/{name}/envs/{envName}/recycle").To(c.recycleApplicationEnv). + Doc("get application status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.envCheckFilter). + Param(ws.PathParameter("name", "identifier of the application ").DataType("string").Required(true)). + Param(ws.PathParameter("envName", "identifier of the application envbinding").DataType("string").Required(true)). + Returns(200, "", apis.EmptyResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.EmptyResponse{})) + + ws.Route(ws.GET("/{name}/workflows").To(c.listApplicationWorkflows). + Doc("list application workflow"). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListWorkflowResponse{}). + Writes(apis.ListWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.POST("/{name}/workflows").To(c.createOrUpdateApplicationWorkflow). + Doc("create application workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateWorkflowRequest{}). + Filter(c.appCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Returns(400, "create failure", bcode.Bcode{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}").To(c.detailWorkflow). + Doc("detail application workflow"). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workfloc.").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.workflowCheckFilter). + Returns(200, "create success", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}/workflows/{workflowName}").To(c.updateWorkflow). + Doc("update application workflow config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Reads(apis.UpdateWorkflowRequest{}). + Returns(200, "", apis.DetailWorkflowResponse{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.DELETE("/{name}/workflows/{workflowName}").To(c.deleteWorkflow). + Doc("deletet workflow"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records").To(c.listWorkflowRecords). + Doc("query application workflow execution record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Param(ws.QueryParameter("page", "query the page number").DataType("integer")). + Param(ws.QueryParameter("pageSize", "query the page size number").DataType("integer")). + Returns(200, "", apis.ListWorkflowRecordsResponse{}). + Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}").To(c.detailWorkflowRecord). + Doc("query application workflow execution record detail"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", apis.DetailWorkflowRecordResponse{}). + Writes(apis.DetailWorkflowRecordResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/resume").To(c.resumeWorkflowRecord). + Doc("resume suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/terminate").To(c.terminateWorkflowRecord). + Doc("terminate suspend workflow record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/workflows/{workflowName}/records/{record}/rollback").To(c.rollbackWorkflowRecord). + Doc("rollback suspend application record"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Param(ws.PathParameter("workflowName", "identifier of the workflow").DataType("string")). + Param(ws.PathParameter("record", "identifier of the workflow record").DataType("string")). + Param(ws.QueryParameter("rollbackVersion", "identifier of the rollback revision").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Filter(c.workflowCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.DetailWorkflowRecordResponse{})) + + ws.Route(ws.GET("/{name}/records").To(c.listApplicationRecords). + Doc("list application records"). + Param(ws.PathParameter("name", "identifier of the application.").DataType("string").Required(true)). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(c.appCheckFilter). + Returns(200, "", nil). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListWorkflowRecordsResponse{})) return ws } @@ -135,6 +490,7 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res // Call the usecase layer code appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq) if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) bcode.ReturnError(req, res, err) return } @@ -147,5 +503,510 @@ func (c *applicationWebService) createApplication(req *restful.Request, res *res } func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { - + apps, err := c.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{ + Namespace: req.QueryParameter("namespace"), + TargetName: req.QueryParameter("target"), + Query: req.QueryParameter("query"), + }) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationResponse{Applications: apps}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailApplication(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) publishApplicationTemplate(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + base, err := c.applicationUsecase.PublishApplicationTemplate(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +// deployApplication TODO: return event model +func (c *applicationWebService) deployApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.ApplicationDeployRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + deployRes, err := c.applicationUsecase.Deploy(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(deployRes); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplication(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationComponents(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + components, err := c.applicationUsecase.ListComponents(req.Request.Context(), app, apis.ListApplicationComponentOptions{ + EnvName: req.QueryParameter("envName"), + }) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ComponentListResponse{Components: components}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreateComponentRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.AddComponent(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailComponent(req.Request.Context(), app, req.PathParameter("componentName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateComponent(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + component := req.Request.Context().Value(&apis.CtxKeyApplicationComponent).(*model.ApplicationComponent) + // Verify the validity of parameters + var updateReq apis.UpdateApplicationComponentRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.UpdateComponent(req.Request.Context(), app, component, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreatePolicyRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.AddPolicy(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationPolicies(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + policies, err := c.applicationUsecase.ListPolicies(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationPolicy{Policies: policies}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.DetailPolicy(req.Request.Context(), app, req.PathParameter("policyName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeletePolicy(req.Request.Context(), app, req.PathParameter("policyName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplicationPolicy(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var updateReq apis.UpdatePolicyRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + response, err := c.applicationUsecase.UpdatePolicy(req.Request.Context(), app, req.PathParameter("policyName"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(response); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplication(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var updateReq apis.UpdateApplicationRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.applicationUsecase.UpdateApplication(req.Request.Context(), app, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) addApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + var createReq apis.CreateApplicationTraitRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + trait, err := c.applicationUsecase.CreateApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(trait); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + var updateReq apis.UpdateApplicationTraitRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + trait, err := c.applicationUsecase.UpdateApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, req.PathParameter("traitType"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(trait); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationTrait(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.applicationUsecase.DeleteApplicationTrait(req.Request.Context(), app, + &model.ApplicationComponent{Name: req.PathParameter("compName")}, req.PathParameter("traitType")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) getApplicationStatus(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + status, err := c.applicationUsecase.GetApplicationStatus(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.ApplicationStatusResponse{Status: status, EnvName: req.PathParameter("envName")}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationRevisions(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + revisions, err := c.applicationUsecase.ListRevisions(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("envName"), req.QueryParameter("status"), page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(revisions); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) detailApplicationRevision(req *restful.Request, res *restful.Response) { + detail, err := c.applicationUsecase.DetailRevision(req.Request.Context(), req.PathParameter("name"), req.PathParameter("revision")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) updateApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var updateReq apis.PutApplicationEnvRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + diff, err := c.envBindingUsecase.UpdateEnvBinding(req.Request.Context(), app, req.PathParameter("envName"), updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(diff); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationEnvs(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + envBindings, err := c.envBindingUsecase.GetEnvBindings(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListApplicationEnvBinding{EnvBindings: envBindings}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) createApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Verify the validity of parameters + var createReq apis.CreateApplicationEnvRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + base, err := c.envBindingUsecase.CreateEnvBinding(req.Request.Context(), app, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(base); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) deleteApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + err := c.envBindingUsecase.DeleteEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) appCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app, err := c.applicationUsecase.GetApplication(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplication, app)) + chain.ProcessFilter(req, res) +} + +func (c *applicationWebService) componentCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + component, err := c.applicationUsecase.GetApplicationComponent(req.Request.Context(), app, req.PathParameter("compName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationComponent, component)) + chain.ProcessFilter(req, res) +} + +func (c *applicationWebService) envCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + envBinding, err := c.envBindingUsecase.GetEnvBinding(req.Request.Context(), app, req.PathParameter("envName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyApplicationEnvBinding, envBinding)) + chain.ProcessFilter(req, res) +} + +func (c *applicationWebService) applicationStatistics(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + detail, err := c.applicationUsecase.Statistics(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) recycleApplicationEnv(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + env := req.Request.Context().Value(&apis.CtxKeyApplicationEnvBinding).(*model.EnvBinding) + err := c.envBindingUsecase.ApplicationEnvRecycle(req.Request.Context(), app, env) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplicationRecords(req *restful.Request, res *restful.Response) { + records, err := c.applicationUsecase.ListRecords(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(records); err != nil { + bcode.ReturnError(req, res, err) + return + } } diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 9d6fed598..1f7698bb5 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -22,6 +22,7 @@ import ( 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" "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) @@ -45,39 +46,138 @@ func (c *ClusterWebService) GetWebService() *restful.WebService { tags := []string{"cluster"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(c.listKubeClusters). Doc("list all clusters"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). + Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). + Returns(200, "", apis.ListClusterResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ListClusterResponse{}).Do(returns200, returns500)) ws.Route(ws.POST("/").To(c.createKubeCluster). Doc("create cluster"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(&apis.CreateClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.ClusterBase{})) - ws.Route(ws.GET("/{clusterName}").To(noop). + ws.Route(ws.GET("/{clusterName}").To(c.getKubeCluster). Doc("detail cluster info"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Returns(200, "", apis.DetailClusterResponse{}). + Returns(400, "", bcode.Bcode{}). Writes(apis.DetailClusterResponse{})) - // Do not implement this dimension for now. - // ws.Route(ws.GET("/{clusterName}/addons").To(noop). - // Doc("list cluster addons info"). - // Metadata(restfulspec.KeyOpenAPITags, tags). - // Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). - // Writes(apis.ListClusterAddonResponse{})) + ws.Route(ws.PUT("/{clusterName}").To(c.modifyKubeCluster). + Doc("modify cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Reads(apis.CreateClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) + + ws.Route(ws.DELETE("/{clusterName}").To(c.deleteKubeCluster). + Doc("delete cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) + + ws.Route(ws.POST("/{clusterName}/namespaces").To(c.createNamespace). + Doc("create namespace in cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("clusterName", "name of the target cluster").DataType("string")). + Reads(apis.CreateClusterNamespaceRequest{}). + Returns(200, "", apis.CreateClusterNamespaceResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateClusterNamespaceResponse{})) + + ws.Route(ws.POST("/cloud-clusters/{provider}").To(c.listCloudClusters). + Doc("list cloud clusters"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Param(ws.QueryParameter("page", "Page for paging").DataType("int").DefaultValue("0")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("int").DefaultValue("20")). + Reads(apis.AccessKeyRequest{}). + Returns(200, "", apis.ListCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListCloudClusterResponse{})) + + ws.Route(ws.POST("/cloud-clusters/{provider}/connect").To(c.connectCloudCluster). + Doc("create cluster from cloud cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Reads(apis.ConnectCloudClusterRequest{}). + Returns(200, "", apis.ClusterBase{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ClusterBase{})) + + ws.Route(ws.POST("/cloud-clusters/{provider}/create").To(c.createCloudCluster). + Doc("create cloud cluster"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string").Required(true)). + Reads(apis.CreateCloudClusterRequest{}). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) + + ws.Route(ws.GET("/cloud-clusters/{provider}/creation/{cloudClusterName}").To(c.getCloudClusterCreationStatus). + Doc("check cloud cluster create status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) + + ws.Route(ws.GET("/cloud-clusters/{provider}/creation").To(c.listCloudClusterCreation). + Doc("list cloud cluster creation"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Returns(200, "", apis.ListCloudClusterCreationResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.ListCloudClusterCreationResponse{})) + + ws.Route(ws.DELETE("/cloud-clusters/{provider}/creation/{cloudClusterName}").To(c.deleteCloudClusterCreation). + Doc("delete cloud cluster creation"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("provider", "identifier of the cloud provider").DataType("string")). + Param(ws.PathParameter("cloudClusterName", "identifier for cloud cluster which is creating").DataType("string")). + Returns(200, "", apis.CreateCloudClusterResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.CreateCloudClusterResponse{})) - // ws.Route(ws.POST("/{clusterName}/addons").To(noop). - // Doc("add addon for the cluster"). - // Metadata(restfulspec.KeyOpenAPITags, tags). - // Param(ws.PathParameter("clusterName", "identifier of the cluster").DataType("string")). - // Writes(apis.DeatilClusterAddonResponse{}).Returns(200, "", apis.DeatilClusterAddonResponse{})) return ws } +func (c *ClusterWebService) listKubeClusters(req *restful.Request, res *restful.Response) { + query := req.QueryParameter("query") + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + clusters, err := c.clusterUsecase.ListKubeClusters(req.Request.Context(), query, page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusters); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateClusterRequest @@ -102,3 +202,234 @@ func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful return } } + +func (c *ClusterWebService) getKubeCluster(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterDetail, err := c.clusterUsecase.GetKubeCluster(req.Request.Context(), clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) modifyKubeCluster(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateClusterRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterBase, err := c.clusterUsecase.ModifyKubeCluster(req.Request.Context(), createReq, clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) deleteKubeCluster(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Call the usecase layer code + clusterBase, err := c.clusterUsecase.DeleteKubeCluster(req.Request.Context(), clusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clusterBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) createNamespace(req *restful.Request, res *restful.Response) { + clusterName := req.PathParameter("clusterName") + + // Verify the validity of parameters + var createReq apis.CreateClusterNamespaceRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + resp, err := c.clusterUsecase.CreateClusterNamespace(req.Request.Context(), clusterName, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) listCloudClusters(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Verify the validity of parameters + var accessKeyRequest apis.AccessKeyRequest + if err := req.ReadEntity(&accessKeyRequest); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&accessKeyRequest); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + clustersResp, err := c.clusterUsecase.ListCloudClusters(req.Request.Context(), provider, accessKeyRequest, page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(clustersResp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) connectCloudCluster(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Verify the validity of parameters + var connectReq apis.ConnectCloudClusterRequest + if err := req.ReadEntity(&connectReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&connectReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + cluster, err := c.clusterUsecase.ConnectCloudCluster(req.Request.Context(), provider, connectReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(cluster); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) createCloudCluster(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Verify the validity of parameters + var createReq apis.CreateCloudClusterRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Call the usecase layer code + resp, err := c.clusterUsecase.CreateCloudCluster(req.Request.Context(), provider, createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) getCloudClusterCreationStatus(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + cloudClusterName := req.PathParameter("cloudClusterName") + + // Call the usecase layer code + resp, err := c.clusterUsecase.GetCloudClusterCreationStatus(req.Request.Context(), provider, cloudClusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) listCloudClusterCreation(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + + // Call the usecase layer code + resp, err := c.clusterUsecase.ListCloudClusterCreation(req.Request.Context(), provider) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *ClusterWebService) deleteCloudClusterCreation(req *restful.Request, res *restful.Response) { + provider := req.PathParameter("provider") + cloudClusterName := req.PathParameter("cloudClusterName") + + // Call the usecase layer code + resp, err := c.clusterUsecase.DeleteCloudClusterCreation(req.Request.Context(), provider, cloudClusterName) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(resp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/component_definition.go b/pkg/apiserver/rest/webservice/component_definition.go deleted file mode 100644 index f26d922ea..000000000 --- a/pkg/apiserver/rest/webservice/component_definition.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -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 webservice - -import ( - restfulspec "github.com/emicklei/go-restful-openapi/v2" - restful "github.com/emicklei/go-restful/v3" - - apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" -) - -type componentDefinitionWebservice struct { -} - -func (c *componentDefinitionWebservice) GetWebService() *restful.WebService { - ws := new(restful.WebService) - ws.Path(versionPrefix+"/componentdefinitions"). - Consumes(restful.MIME_XML, restful.MIME_JSON). - Produces(restful.MIME_JSON, restful.MIME_XML). - Doc("api for componentdefinition manage") - - tags := []string{"componentdefinition"} - - ws.Route(ws.GET("/").To(noop). - Doc("list all componentdefinition"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.QueryParameter("appName", "if specified, query the componentdefinition supported by the cluster where the application resides.").DataType("string")). - Param(ws.QueryParameter("clusterName", "if specified, query the componentdefinition supported by the cluster.").DataType("string")). - Writes(apis.ListComponentDefinitionResponse{})) - return ws -} diff --git a/pkg/apiserver/rest/webservice/definition.go b/pkg/apiserver/rest/webservice/definition.go new file mode 100644 index 000000000..d5e508045 --- /dev/null +++ b/pkg/apiserver/rest/webservice/definition.go @@ -0,0 +1,88 @@ +/* +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 webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + restful "github.com/emicklei/go-restful/v3" + + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +type definitionWebservice struct { + definitionUsecase usecase.DefinitionUsecase +} + +func (d *definitionWebservice) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/definitions"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for definition manage") + + tags := []string{"definition"} + + ws.Route(ws.GET("/").To(d.listDefinitions). + Doc("list all definitions"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("type", "query the definition type").DataType("string").Required(true).AllowableValues(map[string]string{"component": "", "trait": "", "workflowstep": ""})). + Param(ws.QueryParameter("envName", "if specified, query the definition supported by the env.").DataType("string")). + Returns(200, "", apis.ListDefinitionResponse{}). + Writes(apis.ListDefinitionResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}").To(d.detailDefinition). + Doc("detail definition"). + Param(ws.PathParameter("name", "identifier of the definition").DataType("string")). + Param(ws.QueryParameter("type", "query the definition type").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "create success", apis.DetailDefinitionResponse{}). + Writes(apis.DetailDefinitionResponse{}).Do(returns200, returns500)) + return ws +} + +// NewDefinitionWebservice new definition webservice +func NewDefinitionWebservice(du usecase.DefinitionUsecase) WebService { + return &definitionWebservice{ + definitionUsecase: du, + } +} + +func (d *definitionWebservice) listDefinitions(req *restful.Request, res *restful.Response) { + definitions, err := d.definitionUsecase.ListDefinitions(req.Request.Context(), req.QueryParameter("envName"), req.QueryParameter("type")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListDefinitionResponse{Definitions: definitions}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (d *definitionWebservice) detailDefinition(req *restful.Request, res *restful.Response) { + definition, err := d.definitionUsecase.DetailDefinition(req.Request.Context(), req.PathParameter("name"), req.QueryParameter("type")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(definition); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/delivery_target.go b/pkg/apiserver/rest/webservice/delivery_target.go new file mode 100644 index 000000000..814e1ace6 --- /dev/null +++ b/pkg/apiserver/rest/webservice/delivery_target.go @@ -0,0 +1,216 @@ +/* +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 webservice + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + + restfulspec "github.com/emicklei/go-restful-openapi/v2" + restful "github.com/emicklei/go-restful/v3" + + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "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/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// NewDeliveryTargetWebService new deliveryTarget webservice +func NewDeliveryTargetWebService(deliveryTargetUsecase usecase.DeliveryTargetUsecase, applicationUsecase usecase.ApplicationUsecase) WebService { + return &DeliveryTargetWebService{ + deliveryTargetUsecase: deliveryTargetUsecase, + applicationUsecase: applicationUsecase, + } +} + +// DeliveryTargetWebService delivery target web service +type DeliveryTargetWebService struct { + deliveryTargetUsecase usecase.DeliveryTargetUsecase + applicationUsecase usecase.ApplicationUsecase +} + +// GetWebService get web service +func (dt *DeliveryTargetWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/deliveryTargets"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for deliveryTarget manage") + + tags := []string{"deliveryTarget"} + + ws.Route(ws.GET("/").To(dt.listDeliveryTargets). + Doc("list deliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("namesapce", "Query the delivery target belonging to a namespace").DataType("string")). + Param(ws.QueryParameter("page", "Page for paging").DataType("integer")). + Param(ws.QueryParameter("pageSize", "PageSize for paging").DataType("integer")). + Returns(200, "", apis.ListDeliveryTargetResponse{}). + Writes(apis.ListDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.POST("/").To(dt.createDeliveryTarget). + Doc("create deliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreateDeliveryTargetRequest{}). + Returns(200, "create success", apis.DetailDeliveryTargetResponse{}). + Returns(400, "create failure", bcode.Bcode{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}").To(dt.detailDeliveryTarget). + Doc("detail deliveryTarget"). + Param(ws.PathParameter("name", "identifier of the deliveryTarget.").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Returns(200, "create success", apis.DetailDeliveryTargetResponse{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}").To(dt.updateDeliveryTarget). + Doc("update application DeliveryTarget config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Param(ws.PathParameter("name", "identifier of the deliveryTarget").DataType("string")). + Reads(apis.UpdateDeliveryTargetRequest{}). + Returns(200, "", apis.DetailDeliveryTargetResponse{}). + Writes(apis.DetailDeliveryTargetResponse{}).Do(returns200, returns500)) + + ws.Route(ws.DELETE("/{name}").To(dt.deleteDeliveryTarget). + Doc("deletet DeliveryTarget"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Filter(dt.deliveryTargetCheckFilter). + Param(ws.PathParameter("name", "identifier of the deliveryTarget").DataType("string")). + Returns(200, "", apis.EmptyResponse{}). + Writes(apis.EmptyResponse{}).Do(returns200, returns500)) + + return ws +} + +func (dt *DeliveryTargetWebService) createDeliveryTarget(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateDeliveryTargetRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + deliveryTargetDetail, err := dt.deliveryTargetUsecase.CreateDeliveryTarget(req.Request.Context(), createReq) + if err != nil { + log.Logger.Errorf("create delivery-target failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + // Write back response data + if err := res.WriteEntity(deliveryTargetDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) deliveryTargetCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + deliveryTarget, err := dt.deliveryTargetUsecase.GetDeliveryTarget(req.Request.Context(), req.PathParameter("name")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyDeliveryTarget, deliveryTarget)) + chain.ProcessFilter(req, res) +} + +func (dt *DeliveryTargetWebService) detailDeliveryTarget(req *restful.Request, res *restful.Response) { + deliveryTarget := req.Request.Context().Value(&apis.CtxKeyDeliveryTarget).(*model.DeliveryTarget) + detail, err := dt.deliveryTargetUsecase.DetailDeliveryTarget(req.Request.Context(), deliveryTarget) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) updateDeliveryTarget(req *restful.Request, res *restful.Response) { + deliveryTarget := req.Request.Context().Value(&apis.CtxKeyDeliveryTarget).(*model.DeliveryTarget) + // Verify the validity of parameters + var updateReq apis.UpdateDeliveryTargetRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + detail, err := dt.deliveryTargetUsecase.UpdateDeliveryTarget(req.Request.Context(), deliveryTarget, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) deleteDeliveryTarget(req *restful.Request, res *restful.Response) { + deliveryTargetName := req.PathParameter("name") + // deliveryTarget in use, can't be deleted + applications, err := dt.applicationUsecase.ListApplications(req.Request.Context(), apis.ListApplicatioOptions{TargetName: deliveryTargetName}) + if err != nil { + if !errors.Is(err, datastore.ErrRecordNotExist) { + bcode.ReturnError(req, res, err) + return + } + } + if applications != nil { + bcode.ReturnError(req, res, bcode.ErrDeliveryTargetInUseCantDeleted) + return + } + if err := dt.deliveryTargetUsecase.DeleteDeliveryTarget(req.Request.Context(), deliveryTargetName); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (dt *DeliveryTargetWebService) listDeliveryTargets(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + deliveryTargets, err := dt.deliveryTargetUsecase.ListDeliveryTargets(req.Request.Context(), page, pageSize, req.QueryParameter("namespace")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(deliveryTargets); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/namespace.go b/pkg/apiserver/rest/webservice/namespace.go index 31af705dc..013d0d2a1 100644 --- a/pkg/apiserver/rest/webservice/namespace.go +++ b/pkg/apiserver/rest/webservice/namespace.go @@ -20,13 +20,22 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "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" ) type namespaceWebService struct { + namespaceUsecase usecase.NamespaceUsecase } -func (c *namespaceWebService) GetWebService() *restful.WebService { +// NewNamespaceWebService new namespace webservice +func NewNamespaceWebService(namespaceUsecase usecase.NamespaceUsecase) WebService { + return &namespaceWebService{namespaceUsecase: namespaceUsecase} +} + +func (n *namespaceWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/namespaces"). Consumes(restful.MIME_XML, restful.MIME_JSON). @@ -35,42 +44,55 @@ func (c *namespaceWebService) GetWebService() *restful.WebService { tags := []string{"namespace"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(n.listNamespaces). Doc("list all namespaces"). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListNamespaceResponse{}). Writes(apis.ListNamespaceResponse{})) - ws.Route(ws.POST("/").To(noop). + ws.Route(ws.POST("/").To(n.createNamespace). Doc("create namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateNamespaceRequest{}). - Writes(apis.NamesapceDetailResponse{})) - - ws.Route(ws.GET("/{namespace}").To(noop). - Doc("get one namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Writes(apis.NamesapceDetailResponse{})) - - // Compatible with historical apis - ws.Route(ws.GET("/{namespace}/applications/:appname").To(noop). - Doc("get the specified oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). - Writes(apis.ApplicationResponse{})) - - ws.Route(ws.POST("/{namespace}/applications/:appname").To(noop). - Doc("create or update oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). - Reads(apis.ApplicationRequest{})) - - ws.Route(ws.DELETE("/{namespace}/applications/:appname").To(noop). - Doc("create or update oam application in the specified namespace"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). - Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string"))) + Returns(200, "", apis.NamespaceDetailResponse{}). + Writes(apis.NamespaceDetailResponse{})) return ws } + +func (n *namespaceWebService) listNamespaces(req *restful.Request, res *restful.Response) { + namespaces, err := n.namespaceUsecase.ListNamespaces(req.Request.Context()) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListNamespaceResponse{Namespaces: namespaces}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (n *namespaceWebService) createNamespace(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateNamespaceRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + namespaceBase, err := n.namespaceUsecase.CreateNamespace(req.Request.Context(), createReq) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(apis.NamespaceDetailResponse{NamespaceBase: *namespaceBase}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/oam_application.go b/pkg/apiserver/rest/webservice/oam_application.go index 7e19b3648..c842868de 100644 --- a/pkg/apiserver/rest/webservice/oam_application.go +++ b/pkg/apiserver/rest/webservice/oam_application.go @@ -18,12 +18,23 @@ package webservice import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" - restful "github.com/emicklei/go-restful/v3" + "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" ) type oamApplicationWebService struct { + oamApplicationUsecase usecase.OAMApplicationUsecase +} + +// NewOAMApplication new oam application +func NewOAMApplication(oamApplicationUsecase usecase.OAMApplicationUsecase) WebService { + return &oamApplicationWebService{ + oamApplicationUsecase: oamApplicationUsecase, + } } func (c *oamApplicationWebService) GetWebService() *restful.WebService { @@ -33,26 +44,85 @@ func (c *oamApplicationWebService) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for oam application manage") - tags := []string{"oam"} + tags := []string{"oam-application"} - ws.Route(ws.GET("/{namespace}/applications/:appname").To(noop). + ws.Route(ws.GET("/namespaces/{namespace}/applications/{appname}").To(c.getApplication). Doc("get the specified oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). + Returns(200, "", apis.ApplicationResponse{}). Writes(apis.ApplicationResponse{})) - ws.Route(ws.POST("/{namespace}/applications/{appname}").To(noop). + ws.Route(ws.POST("/namespaces/{namespace}/applications/{appname}").To(c.createOrUpdateApplication). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). Reads(apis.ApplicationRequest{})) - ws.Route(ws.DELETE("/{namespace}/applications/:appname").To(noop). + ws.Route(ws.DELETE("/namespaces/{namespace}/applications/{appname}").To(c.deleteApplication). Doc("create or update oam application in the specified namespace"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string"))) + return ws } + +func (c *oamApplicationWebService) getApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + appRes, err := c.oamApplicationUsecase.GetOAMApplication(req.Request.Context(), appName, namespace) + if err != nil { + log.Logger.Errorf("get application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(appRes); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *oamApplicationWebService) createOrUpdateApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + + var createReq apis.ApplicationRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + + err := c.oamApplicationUsecase.CreateOrUpdateOAMApplication(req.Request.Context(), createReq, appName, namespace) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *oamApplicationWebService) deleteApplication(req *restful.Request, res *restful.Response) { + namespace := req.PathParameter("namespace") + appName := req.PathParameter("appname") + + err := c.oamApplicationUsecase.DeleteOAMApplication(req.Request.Context(), appName, namespace) + if err != nil { + log.Logger.Errorf("delete application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/policy_definition.go b/pkg/apiserver/rest/webservice/policy_definition.go index 01f54ca17..e389abc03 100644 --- a/pkg/apiserver/rest/webservice/policy_definition.go +++ b/pkg/apiserver/rest/webservice/policy_definition.go @@ -33,11 +33,12 @@ func (c *policyDefinitionWebservice) GetWebService() *restful.WebService { Produces(restful.MIME_JSON, restful.MIME_XML). Doc("api for policydefinition manage") - tags := []string{"policydefinition"} + tags := []string{"definition"} ws.Route(ws.GET("/").To(noop). Doc("list all policydefinition"). Metadata(restfulspec.KeyOpenAPITags, tags). + Returns(200, "", apis.ListPolicyDefinitionResponse{}). Writes(apis.ListPolicyDefinitionResponse{})) return ws } diff --git a/pkg/apiserver/rest/webservice/validate.go b/pkg/apiserver/rest/webservice/validate.go index ff4f063ea..f5714e80b 100644 --- a/pkg/apiserver/rest/webservice/validate.go +++ b/pkg/apiserver/rest/webservice/validate.go @@ -26,10 +26,18 @@ var validate = validator.New() var nameRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) +const ( + minPageSize = 5 + maxPageSize = 100 +) + func init() { if err := validate.RegisterValidation("checkname", ValidateName); err != nil { panic(err) } + if err := validate.RegisterValidation("checkalias", ValidateAlias); err != nil { + panic(err) + } } // ValidateName custom check name field @@ -40,3 +48,12 @@ func ValidateName(fl validator.FieldLevel) bool { } return nameRegexp.MatchString(value) } + +// ValidateAlias custom check alias field +func ValidateAlias(fl validator.FieldLevel) bool { + value := fl.Field().String() + if value != "" && (len(value) > 64 || len(value) < 2) { + return false + } + return true +} diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go index f6fd675b7..ba0e2102d 100644 --- a/pkg/apiserver/rest/webservice/validate_test.go +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -29,28 +29,42 @@ var _ = Describe("Test validate function", func() { Expect(cmp.Diff(nameRegexp.MatchString("///Asd asda "), false)).Should(BeEmpty()) var app0 = apisv1.CreateApplicationRequest{ Name: "a", - Namespace: "namesapce", + Namespace: "namespace", } err := validate.Struct(&app0) Expect(err).ShouldNot(BeNil()) var app1 = apisv1.CreateApplicationRequest{ Name: "Asdasd", - Namespace: "namesapce", + Namespace: "namespace", } err = validate.Struct(&app1) Expect(err).ShouldNot(BeNil()) var app2 = apisv1.CreateApplicationRequest{ Name: "asdasd asdasd ++", - Namespace: "namesapce", + Namespace: "namespace", } err = validate.Struct(&app2) Expect(err).ShouldNot(BeNil()) var app3 = apisv1.CreateApplicationRequest{ Name: "asdasd", - Namespace: "namesapce", + Namespace: "namespace", } err = validate.Struct(&app3) Expect(err).Should(BeNil()) + + var app4 = apisv1.CreateApplicationRequest{ + Name: "asdasd-asdasd", + Namespace: "namespace", + } + err = validate.Struct(&app4) + Expect(err).Should(BeNil()) + + var component = apisv1.CreateComponentRequest{ + Name: "asdasd-asdasd", + ComponentType: "alibaba-ack", + } + err = validate.Struct(&component) + Expect(err).Should(BeNil()) }) }) diff --git a/pkg/apiserver/rest/webservice/velaql.go b/pkg/apiserver/rest/webservice/velaql.go new file mode 100644 index 000000000..533675cdb --- /dev/null +++ b/pkg/apiserver/rest/webservice/velaql.go @@ -0,0 +1,73 @@ +/* + 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 webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + "github.com/emicklei/go-restful/v3" + + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +type velaQLWebService struct { + velaQLUsecase usecase.VelaQLUsecase +} + +// NewVelaQLWebService new velaQL webservice +func NewVelaQLWebService(velaQLUsecase usecase.VelaQLUsecase) WebService { + return &velaQLWebService{ + velaQLUsecase: velaQLUsecase, + } +} + +func (v *velaQLWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/query"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for velaQL") + + tags := []string{"velaQL"} + + ws.Route(ws.GET("/").To(v.queryView). + Doc("use velaQL to query resource status"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.QueryParameter("velaql", "velaql query statement").DataType("string")). + Returns(200, "", apis.VelaQLViewResponse{}). + Returns(400, "", bcode.Bcode{}). + Writes(apis.VelaQLViewResponse{})) + + return ws +} + +func (v *velaQLWebService) queryView(req *restful.Request, res *restful.Response) { + velaQL := req.QueryParameter("velaql") + + qlResp, err := v.velaQLUsecase.QueryView(req.Request.Context(), velaQL) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err = res.WriteEntity(qlResp); err != nil { + bcode.ReturnError(req, res, err) + return + } +} diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 86b11edaa..907e41f2e 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -17,7 +17,6 @@ limitations under the License. package webservice import ( - "context" "net/http" "github.com/emicklei/go-restful/v3" @@ -58,15 +57,25 @@ func returns500(b *restful.RouteBuilder) { // Init init all webservice, pass in the required parameter object. // It can be implemented using the idea of dependency injection. -func Init(ctx context.Context, ds datastore.DataStore) { +func Init(ds datastore.DataStore) { clusterUsecase := usecase.NewClusterUsecase(ds) - applicationUsecase := usecase.NewApplicationUsecase(ds) + workflowUsecase := usecase.NewWorkflowUsecase(ds) + deliveryTargetUsecase := usecase.NewDeliveryTargetUsecase(ds) + namespaceUsecase := usecase.NewNamespaceUsecase() + oamApplicationUsecase := usecase.NewOAMApplicationUsecase() + velaQLUsecase := usecase.NewVelaQLUsecase() + definitionUsecase := usecase.NewDefinitionUsecase() + addonUsecase := usecase.NewAddonUsecase(ds) + envBindingUsecase := usecase.NewEnvBindingUsecase(ds, workflowUsecase) + applicationUsecase := usecase.NewApplicationUsecase(ds, workflowUsecase, envBindingUsecase, deliveryTargetUsecase) RegistWebService(NewClusterWebService(clusterUsecase)) - RegistWebService(NewApplicationWebService(applicationUsecase)) - RegistWebService(&namespaceWebService{}) - RegistWebService(&componentDefinitionWebservice{}) - RegistWebService(&addonWebService{}) - RegistWebService(&oamApplicationWebService{}) + RegistWebService(NewApplicationWebService(applicationUsecase, envBindingUsecase, workflowUsecase)) + RegistWebService(NewNamespaceWebService(namespaceUsecase)) + RegistWebService(NewDefinitionWebservice(definitionUsecase)) + RegistWebService(NewAddonWebService(addonUsecase)) + RegistWebService(NewAddonRegistryWebService(addonUsecase)) + RegistWebService(NewOAMApplication(oamApplicationUsecase)) RegistWebService(&policyDefinitionWebservice{}) - RegistWebService(&workflowWebService{}) + RegistWebService(NewDeliveryTargetWebService(deliveryTargetUsecase, applicationUsecase)) + RegistWebService(NewVelaQLWebService(velaQLUsecase)) } diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go index 9fef7662b..60f48b56a 100644 --- a/pkg/apiserver/rest/webservice/workflow.go +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -17,44 +17,193 @@ limitations under the License. package webservice import ( - restfulspec "github.com/emicklei/go-restful-openapi/v2" + "context" + restful "github.com/emicklei/go-restful/v3" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "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/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) type workflowWebService struct { + workflowUsecase usecase.WorkflowUsecase + applicationUsecase usecase.ApplicationUsecase } -func (c *workflowWebService) GetWebService() *restful.WebService { - ws := new(restful.WebService) - ws.Path(versionPrefix+"/workflows"). - Consumes(restful.MIME_XML, restful.MIME_JSON). - Produces(restful.MIME_JSON, restful.MIME_XML). - Doc("api for cluster manage") - - tags := []string{"cluster"} - - ws.Route(ws.GET("/{name}").To(noop). - Doc("detail application workflow"). - Param(ws.PathParameter("name", "identifier of the workflow, Currently, the application name is used.").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.PUT("/{name}").To(noop). - Doc("create or update application workflow config"). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Reads(apis.UpdateWorkflowRequest{}). - Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) - - ws.Route(ws.GET("/{name}/records").To(noop). - Doc("query application workflow execution record"). - Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). - Metadata(restfulspec.KeyOpenAPITags, tags). - Param(ws.PathParameter("page", "Query the page number.").DataType("integer")). - Param(ws.PathParameter("pageSize", "Query the page size number.").DataType("integer")). - Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) - - return ws +func (w *workflowWebService) workflowCheckFilter(req *restful.Request, res *restful.Response, chain *restful.FilterChain) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow, err := w.workflowUsecase.GetWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + req.Request = req.Request.WithContext(context.WithValue(req.Request.Context(), &apis.CtxKeyWorkflow, workflow)) + chain.ProcessFilter(req, res) +} + +func (w *workflowWebService) listApplicationWorkflows(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflows, err := w.workflowUsecase.ListApplicationWorkflow(req.Request.Context(), app) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.ListWorkflowResponse{Workflows: workflows}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) createOrUpdateApplicationWorkflow(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateWorkflowRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + // Call the usecase layer code + workflowDetail, err := w.workflowUsecase.CreateOrUpdateWorkflow(req.Request.Context(), app, createReq) + if err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(workflowDetail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) detailWorkflow(req *restful.Request, res *restful.Response) { + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + detail, err := w.workflowUsecase.DetailWorkflow(req.Request.Context(), workflow) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) updateWorkflow(req *restful.Request, res *restful.Response) { + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + // Verify the validity of parameters + var updateReq apis.UpdateWorkflowRequest + if err := req.ReadEntity(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&updateReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + detail, err := w.workflowUsecase.UpdateWorkflow(req.Request.Context(), workflow, updateReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(detail); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) deleteWorkflow(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + if err := w.workflowUsecase.DeleteWorkflow(req.Request.Context(), app, req.PathParameter("workflowName")); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) listWorkflowRecords(req *restful.Request, res *restful.Response) { + page, pageSize, err := utils.ExtractPagingParams(req, minPageSize, maxPageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + records, err := w.workflowUsecase.ListWorkflowRecords(req.Request.Context(), workflow, page, pageSize) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(records); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) detailWorkflowRecord(req *restful.Request, res *restful.Response) { + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + record, err := w.workflowUsecase.DetailWorkflowRecord(req.Request.Context(), workflow, req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + if err := res.WriteEntity(record); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) resumeWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.ResumeRecord(req.Request.Context(), app, workflow, req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) terminateWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.TerminateRecord(req.Request.Context(), app, workflow, req.PathParameter("record")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (w *workflowWebService) rollbackWorkflowRecord(req *restful.Request, res *restful.Response) { + app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application) + workflow := req.Request.Context().Value(&apis.CtxKeyWorkflow).(*model.Workflow) + err := w.workflowUsecase.RollbackRecord(req.Request.Context(), app, workflow, req.PathParameter("record"), req.QueryParameter("rollbackVersion")) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { + bcode.ReturnError(req, res, err) + return + } } diff --git a/pkg/cloudprovider/aliyun.go b/pkg/cloudprovider/aliyun.go new file mode 100644 index 000000000..72621c9cc --- /dev/null +++ b/pkg/cloudprovider/aliyun.go @@ -0,0 +1,211 @@ +/* +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 cloudprovider + +import ( + "context" + "encoding/json" + "strings" + + cs20151215 "github.com/alibabacloud-go/cs-20151215/v2/client" + openapi "github.com/alibabacloud-go/darabonba-openapi/client" + "github.com/alibabacloud-go/tea/tea" + types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" + v1beta12 "github.com/oam-dev/terraform-controller/api/v1beta1" + "github.com/pkg/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/klog/v2" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/utils/util" +) + +const ( + aliyunAPIEndpoint = "cs.cn-hangzhou.aliyuncs.com" +) + +// AliyunCloudProvider describes the cloud provider in aliyun +type AliyunCloudProvider struct { + *cs20151215.Client + k8sClient client.Client + accessKeyID string + accessKeySecret string +} + +// NewAliyunCloudProvider create aliyun cloud provider +func NewAliyunCloudProvider(accessKeyID string, accessKeySecret string, k8sClient client.Client) (*AliyunCloudProvider, error) { + config := &openapi.Config{ + AccessKeyId: pointer.String(accessKeyID), + AccessKeySecret: pointer.String(accessKeySecret), + } + config.Endpoint = tea.String(aliyunAPIEndpoint) + c, err := cs20151215.NewClient(config) + if err != nil { + return nil, err + } + return &AliyunCloudProvider{Client: c, k8sClient: k8sClient, accessKeyID: accessKeyID, accessKeySecret: accessKeySecret}, nil +} + +// IsInvalidKey check if error is InvalidAccessKey or InvalidSecretKey +func (provider *AliyunCloudProvider) IsInvalidKey(err error) bool { + return strings.Contains(err.Error(), "InvalidAccessKeyId") || strings.Contains(err.Error(), "Code: SignatureDoesNotMatch") +} + +func (provider *AliyunCloudProvider) decodeClusterLabels(tags []*cs20151215.Tag) map[string]string { + labels := map[string]string{} + for _, tag := range tags { + labels[*tag.Key] = *tag.Value + } + return labels +} + +func (provider *AliyunCloudProvider) decodeClusterURL(masterURL string) (url struct { + APIServerEndpoint string `json:"api_server_endpoint"` + DashboardEndpoint string `json:"dashboardEndpoint"` + IntranetAPIServerEndpoint string `json:"intranet_api_server_endpoint"` +}) { + if err := json.Unmarshal([]byte(masterURL), &url); err != nil { + klog.Info("failed to unmarshal masterUrl %s", masterURL) + } + return +} + +// ListCloudClusters list clusters with page info, return clusters, total count and error +func (provider *AliyunCloudProvider) ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) { + describeClustersV1Request := &cs20151215.DescribeClustersV1Request{ + PageSize: pointer.Int64(int64(pageSize)), + PageNumber: pointer.Int64(int64(pageNumber)), + } + resp, err := provider.DescribeClustersV1(describeClustersV1Request) + if err != nil { + return nil, 0, err + } + var clusters []*CloudCluster + for _, cluster := range resp.Body.Clusters { + labels := provider.decodeClusterLabels(cluster.Tags) + url := provider.decodeClusterURL(*cluster.MasterUrl) + clusters = append(clusters, &CloudCluster{ + ID: *cluster.ClusterId, + Name: *cluster.Name, + Type: *cluster.ClusterType, + Zone: *cluster.ZoneId, + ZoneID: *cluster.ZoneId, + RegionID: *cluster.RegionId, + VpcID: *cluster.VpcId, + Labels: labels, + Status: *cluster.State, + APIServerURL: url.APIServerEndpoint, + DashBoardURL: url.DashboardEndpoint, + }) + } + return clusters, int(*resp.Body.PageInfo.TotalCount), nil +} + +// GetClusterKubeConfig get cluster kubeconfig by clusterID +func (provider *AliyunCloudProvider) GetClusterKubeConfig(clusterID string) (string, error) { + req := &cs20151215.DescribeClusterUserKubeconfigRequest{} + resp, err := provider.DescribeClusterUserKubeconfig(pointer.String(clusterID), req) + if err != nil { + return "", err + } + return *resp.Body.Config, nil +} + +// GetClusterInfo retrieves cluster info by clusterID +func (provider *AliyunCloudProvider) GetClusterInfo(clusterID string) (*CloudCluster, error) { + resp, err := provider.DescribeClusterDetail(pointer.String(clusterID)) + if err != nil { + return nil, err + } + cluster := resp.Body + labels := provider.decodeClusterLabels(cluster.Tags) + url := provider.decodeClusterURL(*cluster.MasterUrl) + return &CloudCluster{ + Provider: ProviderAliyun, + ID: *cluster.ClusterId, + Name: *cluster.Name, + Type: *cluster.ClusterType, + Zone: *cluster.ZoneId, + ZoneID: *cluster.ZoneId, + RegionID: *cluster.RegionId, + VpcID: *cluster.VpcId, + Labels: labels, + Status: *cluster.State, + APIServerURL: url.APIServerEndpoint, + DashBoardURL: url.DashboardEndpoint, + }, nil +} + +// CreateCloudCluster create cloud cluster +func (provider *AliyunCloudProvider) CreateCloudCluster(ctx context.Context, clusterName string, zone string, worker int, cpu int64, mem int64) (string, error) { + name := GetCloudClusterFullName(ProviderAliyun, clusterName) + ns := util.GetRuntimeNamespace() + terraformProviderName, err := bootstrapTerraformProvider(ctx, provider.k8sClient, ns, ProviderAliyun, "alibaba", provider.accessKeyID, provider.accessKeySecret, "cn-hongkong") + if err != nil { + return "", errors.Wrapf(err, "failed to bootstrap terraform provider") + } + properties := map[string]interface{}{ + "k8s_name_prefix": clusterName, + } + if zone != "" { + properties["zone_id"] = zone + } + if cpu != 0 { + properties["cpu_core_count"] = cpu + } + if mem != 0 { + properties["memory_size"] = mem + } + if worker != 0 { + properties["k8s_worker_number"] = worker + } + bs, err := json.Marshal(properties) + if err != nil { + return name, errors.Wrapf(err, "failed to marshal cloud cluster app properties") + } + + cfg := v1beta12.Configuration{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: map[string]string{ + CloudClusterCreatorLabelKey: ProviderAliyun, + }, + }, + Spec: v1beta12.ConfigurationSpec{ + Path: "alibaba/cs/dedicated-kubernetes", + Remote: "https://github.com/kubevela-contrib/terraform-modules.git", + Variable: &runtime.RawExtension{Raw: bs}, + ProviderReference: &types.Reference{ + Name: terraformProviderName, + Namespace: ns, + }, + WriteConnectionSecretToReference: &types.SecretReference{ + Name: name, + Namespace: ns, + }, + }, + } + + if err = provider.k8sClient.Create(ctx, &cfg); err != nil { + return name, errors.Wrapf(err, "failed to create cloud cluster terraform configuration") + } + + return name, nil +} diff --git a/pkg/cloudprovider/cluster.go b/pkg/cloudprovider/cluster.go new file mode 100644 index 000000000..eb0324551 --- /dev/null +++ b/pkg/cloudprovider/cluster.go @@ -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 cloudprovider + +import ( + "context" + + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // CloudClusterCreatorLabelKey labels the creator of cloud cluster + CloudClusterCreatorLabelKey = "api.core.oam.dev/cloud-cluster-creator" +) + +// CloudClusterProvider abstracts the cloud provider to provide cluster access +type CloudClusterProvider interface { + IsInvalidKey(err error) bool + ListCloudClusters(pageNumber int, pageSize int) ([]*CloudCluster, int, error) + GetClusterKubeConfig(clusterID string) (string, error) + GetClusterInfo(clusterID string) (*CloudCluster, error) + CreateCloudCluster(ctx context.Context, clusterName string, zone string, worker int, cpu int64, mem int64) (string, error) +} + +// GetClusterProvider creates interface for getting cloud cluster provider +func GetClusterProvider(provider string, accessKeyID string, accessKeySecret string, k8sClient client.Client) (CloudClusterProvider, error) { + switch provider { + case ProviderAliyun: + return NewAliyunCloudProvider(accessKeyID, accessKeySecret, k8sClient) + default: + return nil, errors.Errorf("cluster provider %s is not implemented", provider) + } +} diff --git a/pkg/cloudprovider/terraform.go b/pkg/cloudprovider/terraform.go new file mode 100644 index 000000000..36b7e10d9 --- /dev/null +++ b/pkg/cloudprovider/terraform.go @@ -0,0 +1,94 @@ +/* +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 cloudprovider + +import ( + "context" + "crypto/sha256" + "fmt" + "strings" + + types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime" + v1beta12 "github.com/oam-dev/terraform-controller/api/v1beta1" + "github.com/pkg/errors" + v12 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func computeProviderHashKey(provider string, accessKeyID string, accessKeySecret string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join([]string{provider, accessKeyID, accessKeySecret}, "::"))))[:8] // #nosec +} + +// GetCloudClusterFullName construct the full name of cloud cluster which will be used as the name of terraform configuration +func GetCloudClusterFullName(provider string, clusterName string) string { + return fmt.Sprintf("cloud-cluster-%s-%s", provider, clusterName) +} + +func bootstrapTerraformProvider(ctx context.Context, k8sClient client.Client, ns string, provider string, tfProvider string, accessKeyID string, accessKeySecret string, region string) (string, error) { + hashKey := computeProviderHashKey(provider, accessKeyID, accessKeySecret) + secretName := fmt.Sprintf("tf-provider-cred-%s-%s", provider, hashKey) + terraformProviderName := fmt.Sprintf("tf-provider-%s-%s", provider, hashKey) + secret := v12.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + StringData: map[string]string{"credentials": fmt.Sprintf("accessKeyID: %s\naccessKeySecret: %s\nsecurityToken:\n", accessKeyID, accessKeySecret)}, + Type: v12.SecretTypeOpaque, + } + var err error + if err = k8sClient.Get(ctx, client.ObjectKeyFromObject(&secret), &v12.Secret{}); err != nil { + if kerrors.IsNotFound(err) { + err = k8sClient.Create(ctx, &secret) + } + if err != nil { + return "", errors.Wrapf(err, "failed to upsert terraform provider secret") + } + } + + terraformProvider := v1beta12.Provider{ + ObjectMeta: v1.ObjectMeta{ + Name: terraformProviderName, + Namespace: ns, + }, + Spec: v1beta12.ProviderSpec{ + Credentials: v1beta12.ProviderCredentials{ + SecretRef: &types.SecretKeySelector{ + Key: "credentials", + SecretReference: types.SecretReference{ + Name: secretName, + Namespace: ns, + }, + }, + Source: types.CredentialsSourceSecret, + }, + Provider: tfProvider, + Region: region, + }, + } + if err = k8sClient.Get(ctx, client.ObjectKeyFromObject(&terraformProvider), &v1beta12.Provider{}); err != nil { + if kerrors.IsNotFound(err) { + err = k8sClient.Create(ctx, &terraformProvider) + } + if err != nil { + return "", errors.Wrapf(err, "failed to upsert terraform provider") + } + } + return terraformProviderName, nil +} diff --git a/pkg/cloudprovider/types.go b/pkg/cloudprovider/types.go new file mode 100644 index 000000000..699943d7d --- /dev/null +++ b/pkg/cloudprovider/types.go @@ -0,0 +1,38 @@ +/* +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 cloudprovider + +const ( + // ProviderAliyun cloud provider aliyun + ProviderAliyun = "aliyun" +) + +// CloudCluster describes the interface that cloud provider should return +type CloudCluster struct { + Provider string `json:"provider"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Zone string `json:"zone"` + ZoneID string `json:"zoneID"` + RegionID string `json:"regionID"` + VpcID string `json:"vpcID"` + Labels map[string]string `json:"labels"` + Status string `json:"status"` + APIServerURL string `json:"apiServerURL"` + DashBoardURL string `json:"dashboardURL"` +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go index 9e6880d82..53dcc7d31 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller_test.go @@ -55,7 +55,6 @@ import ( "github.com/oam-dev/kubevela/pkg/oam/testutil" "github.com/oam-dev/kubevela/pkg/oam/util" common2 "github.com/oam-dev/kubevela/pkg/utils/common" - wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" ) // TODO: Refactor the tests to not copy and paste duplicated code 10 times @@ -2134,7 +2133,7 @@ var _ = Describe("Test Application Controller", func() { app := appwithNoTrait.DeepCopy() app.Name = "vela-test-app-trace" app.SetNamespace(ns.Name) - app.Annotations = map[string]string{wfTypes.AnnotationPublishVersion: "v134"} + app.Annotations = map[string]string{oam.AnnotationPublishVersion: "v134"} Expect(k8sClient.Create(ctx, ns)).Should(BeNil()) Expect(k8sClient.Create(ctx, app)).Should(BeNil()) @@ -2161,7 +2160,7 @@ var _ = Describe("Test Application Controller", func() { web.Spec.Replicas = pointer.Int32(0) Expect(k8sClient.Update(ctx, web)).Should(BeNil()) - checkApp.Annotations[wfTypes.AnnotationPublishVersion] = "v135" + checkApp.Annotations[oam.AnnotationPublishVersion] = "v135" Expect(k8sClient.Update(ctx, checkApp)).Should(BeNil()) testutil.ReconcileOnceAfterFinalizer(reconciler, reconcile.Request{NamespacedName: appKey}) checkApp = &v1beta1.Application{} diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go index 07555df4a..0c6271119 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch.go @@ -209,7 +209,7 @@ func (a *AppManifestsDispatcher) retrieveLegacyResourceTrackers(ctx context.Cont oldRtList := &v1beta1.ResourceTrackerList{} if err := a.c.List(ctx, oldRtList, client.MatchingLabels{ oam.LabelAppName: ExtractAppName(a.currentRTName, a.namespace), - "app.oam.dev/namesapce": a.namespace, + "app.oam.dev/namespace": a.namespace, }); err != nil { return errors.Wrap(err, "cannot retrieve legacy resource trackers with miss-spell label") } diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go index 16f31b140..61abbacc0 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/dispatch/dispatch_suite_test.go @@ -495,7 +495,7 @@ var _ = Describe("Test compatibility code", func() { ObjectMeta: metav1.ObjectMeta{ Name: appName + "-v2-" + namespaceName, Labels: map[string]string{ - "app.oam.dev/namesapce": namespaceName, + "app.oam.dev/namespace": namespaceName, oam.LabelAppName: appName, }, }, diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go index 584f9ed20..d00bf6af0 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/suite_test.go @@ -245,7 +245,7 @@ func NewFakeRecorder(bufferSize int) *FakeRecorder { // randomNamespaceName generates a random name based on the basic name. // Running each ginkgo case in a new namespace with a random name can avoid -// waiting a long time to GC namesapce. +// waiting a long time to GC namespace. func randomNamespaceName(basic string) string { return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) } diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go index 42311ff13..5660b69c5 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/scopes/healthscope/healthscope.go @@ -308,7 +308,7 @@ func getVersioningPeerWorkloadRefs(ctx context.Context, c client.Reader, wlRef c compName := getComponentNameFromLabel(o) appName := getAppConfigNameFromLabel(o) if compName == "" || appName == "" { - // if missing these lables, cannot get peer workloads + // if missing these labels, cannot get peer workloads return nil, nil } diff --git a/pkg/controller/utils/capability.go b/pkg/controller/utils/capability.go index 7c50ee02b..cf405d2e0 100644 --- a/pkg/controller/utils/capability.go +++ b/pkg/controller/utils/capability.go @@ -482,7 +482,7 @@ func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) if err != nil { return nil, err } - fixOpenAPISchema("", schema) + FixOpenAPISchema("", schema) parameter, err := schema.MarshalJSON() if err != nil { @@ -493,7 +493,7 @@ func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) // generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format func generateOpenAPISchemaFromCapabilityParameter(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) { - template, err := prepareParameterCue(capability.Name, capability.CueTemplate) + template, err := PrepareParameterCue(capability.Name, capability.CueTemplate) if err != nil { if errors.As(err, &ErrNoSectionParameterInCue{}) { // return OpenAPI with empty object parameter, making it possible to generate ConfigMap @@ -535,8 +535,8 @@ func GenerateOpenAPISchemaFromDefinition(definitionName, cueTemplate string) ([] return generateOpenAPISchemaFromCapabilityParameter(capability, nil) } -// prepareParameterCue cuts `parameter` section form definition .cue file -func prepareParameterCue(capabilityName, capabilityTemplate string) (string, error) { +// PrepareParameterCue cuts `parameter` section form definition .cue file +func PrepareParameterCue(capabilityName, capabilityTemplate string) (string, error) { var template string var withParameterFlag bool r := regexp.MustCompile(`[[:space:]]*parameter:[[:space:]]*`) @@ -558,18 +558,18 @@ func prepareParameterCue(capabilityName, capabilityTemplate string) (string, err return template, nil } -// fixOpenAPISchema fixes tainted `description` filed, missing of title `field`. -func fixOpenAPISchema(name string, schema *openapi3.Schema) { +// FixOpenAPISchema fixes tainted `description` filed, missing of title `field`. +func FixOpenAPISchema(name string, schema *openapi3.Schema) { t := schema.Type switch t { case "object": for k, v := range schema.Properties { s := v.Value - fixOpenAPISchema(k, s) + FixOpenAPISchema(k, s) } case "array": if schema.Items != nil { - fixOpenAPISchema("", schema.Items.Value) + FixOpenAPISchema("", schema.Items.Value) } } if name != "" { diff --git a/pkg/controller/utils/capability_test.go b/pkg/controller/utils/capability_test.go index b8ef7caa1..44d0b490a 100644 --- a/pkg/controller/utils/capability_test.go +++ b/pkg/controller/utils/capability_test.go @@ -216,7 +216,7 @@ func TestFixOpenAPISchema(t *testing.T) { t.Run(name, func(t *testing.T) { swagger, _ := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(filepath.Join(TestDir, tc.inputFile)) schema := swagger.Components.Schemas[model.ParameterFieldName].Value - fixOpenAPISchema("", schema) + FixOpenAPISchema("", schema) fixedSchema, _ := schema.MarshalJSON() expectedSchema, _ := os.ReadFile(filepath.Join(TestDir, tc.fixedFile)) assert.Equal(t, string(fixedSchema), string(expectedSchema)) diff --git a/pkg/multicluster/cluster_management.go b/pkg/multicluster/cluster_management.go new file mode 100644 index 000000000..69d4c91f6 --- /dev/null +++ b/pkg/multicluster/cluster_management.go @@ -0,0 +1,272 @@ +/* +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 multicluster + +import ( + "context" + "fmt" + + v1alpha12 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + v14 "k8s.io/api/storage/v1" + v13 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + errors2 "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + types2 "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/policy/envbinding" + errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" +) + +// ensureResourceTrackerCRDInstalled ensures resourcetracker to be installed in child cluster +func ensureResourceTrackerCRDInstalled(ctx context.Context, c client.Client, clusterName string) error { + remoteCtx := ContextWithClusterName(ctx, clusterName) + crdName := types2.NamespacedName{Name: "resourcetrackers." + v1beta1.Group} + if err := c.Get(remoteCtx, crdName, &v13.CustomResourceDefinition{}); err != nil { + if !errors2.IsNotFound(err) { + return errors.Wrapf(err, "failed to check resourcetracker crd in cluster %s", clusterName) + } + crd := &v13.CustomResourceDefinition{} + if err = c.Get(ctx, crdName, crd); err != nil { + return errors.Wrapf(err, "failed to get resourcetracker crd in hub cluster") + } + crd.ObjectMeta = v12.ObjectMeta{ + Name: crdName.Name, + Annotations: crd.Annotations, + Labels: crd.Labels, + } + if err = c.Create(remoteCtx, crd); err != nil { + return errors.Wrapf(err, "failed to create resourcetracker crd in cluster %s", clusterName) + } + } + return nil +} + +// ensureClusterNotExists checks if child cluster has already been joined, if joined, error is returned +func ensureClusterNotExists(ctx context.Context, c client.Client, clusterName string) error { + secret := &v1.Secret{} + err := c.Get(ctx, types2.NamespacedName{Name: clusterName, Namespace: ClusterGatewaySecretNamespace}, secret) + if err == nil { + return ErrClusterExists + } + if !errors2.IsNotFound(err) { + return errors.Wrapf(err, "failed to check duplicate cluster secret") + } + return nil +} + +// GetMutableClusterSecret retrieves the cluster secret and check if any application is using the cluster +func GetMutableClusterSecret(ctx context.Context, c client.Client, clusterName string) (*v1.Secret, error) { + clusterSecret := &v1.Secret{} + if err := c.Get(ctx, types2.NamespacedName{Namespace: ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil { + return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName) + } + labels := clusterSecret.GetLabels() + if labels == nil || labels[v1alpha12.LabelKeyClusterCredentialType] == "" { + return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, v1alpha12.LabelKeyClusterCredentialType) + } + apps := &v1beta1.ApplicationList{} + if err := c.List(ctx, apps); err != nil { + return nil, errors.Wrap(err, "failed to find applications to check clusters") + } + errs := errors3.ErrorList{} + for _, app := range apps.Items { + status, err := envbinding.GetEnvBindingPolicyStatus(app.DeepCopy(), "") + if err == nil && status != nil { + for _, env := range status.Envs { + for _, placement := range env.Placements { + if placement.Cluster == clusterName { + errs.Append(fmt.Errorf("application %s/%s (env: %s) is currently using cluster %s", app.Namespace, app.Name, env.Env, clusterName)) + } + } + } + } + } + if errs.HasError() { + return nil, errors.Wrapf(errs, "cluster %s is in use now", clusterName) + } + return clusterSecret, nil +} + +// JoinClusterByKubeConfig add child cluster by kubeconfig path, return cluster info and error +func JoinClusterByKubeConfig(_ctx context.Context, k8sClient client.Client, kubeconfigPath string, clusterName string) (*api.Cluster, error) { + config, err := clientcmd.LoadFromFile(kubeconfigPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to get kubeconfig") + } + if len(config.CurrentContext) == 0 { + return nil, fmt.Errorf("current-context is not set") + } + ctx, ok := config.Contexts[config.CurrentContext] + if !ok { + return nil, fmt.Errorf("current-context %s not found", config.CurrentContext) + } + cluster, ok := config.Clusters[ctx.Cluster] + if !ok { + return nil, fmt.Errorf("cluster %s not found", ctx.Cluster) + } + authInfo, ok := config.AuthInfos[ctx.AuthInfo] + if !ok { + return nil, fmt.Errorf("authInfo %s not found", ctx.AuthInfo) + } + + if clusterName == "" { + clusterName = ctx.Cluster + } + if clusterName == ClusterLocalName { + return cluster, fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", ClusterLocalName) + } + + if err := ensureClusterNotExists(_ctx, k8sClient, clusterName); err != nil { + return cluster, errors.Wrapf(err, "cannot use cluster name %s", clusterName) + } + + var credentialType v1alpha12.CredentialType + data := map[string][]byte{ + "endpoint": []byte(cluster.Server), + "ca.crt": cluster.CertificateAuthorityData, + } + if len(authInfo.Token) > 0 { + credentialType = v1alpha12.CredentialTypeServiceAccountToken + data["token"] = []byte(authInfo.Token) + } else { + credentialType = v1alpha12.CredentialTypeX509Certificate + data["tls.crt"] = authInfo.ClientCertificateData + data["tls.key"] = authInfo.ClientKeyData + } + secret := &v1.Secret{ + ObjectMeta: v12.ObjectMeta{ + Name: clusterName, + Namespace: ClusterGatewaySecretNamespace, + Labels: map[string]string{ + v1alpha12.LabelKeyClusterCredentialType: string(credentialType), + }, + }, + Type: v1.SecretTypeOpaque, + Data: data, + } + + if err := k8sClient.Create(_ctx, secret); err != nil { + return cluster, errors.Wrapf(err, "failed to add cluster to kubernetes") + } + if err := ensureResourceTrackerCRDInstalled(_ctx, k8sClient, clusterName); err != nil { + _ = k8sClient.Delete(_ctx, secret) + return cluster, errors.Wrapf(err, "failed to ensure resourcetracker crd installed in cluster %s", clusterName) + } + return cluster, nil +} + +// DetachCluster detach cluster by name, if cluster is using by application, it will return error +func DetachCluster(ctx context.Context, k8sClient client.Client, clusterName string) error { + if clusterName == ClusterLocalName { + return ErrReservedLocalClusterName + } + clusterSecret, err := GetMutableClusterSecret(ctx, k8sClient, clusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) + } + return k8sClient.Delete(ctx, clusterSecret) +} + +// RenameCluster rename cluster +func RenameCluster(ctx context.Context, k8sClient client.Client, oldClusterName string, newClusterName string) error { + if newClusterName == ClusterLocalName { + return ErrReservedLocalClusterName + } + clusterSecret, err := GetMutableClusterSecret(ctx, k8sClient, oldClusterName) + if err != nil { + return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) + } + if err := ensureClusterNotExists(ctx, k8sClient, newClusterName); err != nil { + return errors.Wrapf(err, "cannot set cluster name to %s", newClusterName) + } + if err := k8sClient.Delete(ctx, clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + } + clusterSecret.ObjectMeta = v12.ObjectMeta{ + Name: newClusterName, + Namespace: ClusterGatewaySecretNamespace, + Labels: clusterSecret.Labels, + Annotations: clusterSecret.Annotations, + } + if err := k8sClient.Create(ctx, clusterSecret); err != nil { + return errors.Wrapf(err, "failed to rename cluster from %s to %s", oldClusterName, newClusterName) + } + return nil +} + +// ClusterInfo describes the basic information of a cluster +type ClusterInfo struct { + Nodes *v1.NodeList + WorkerNumber int + MasterNumber int + MemoryCapacity resource.Quantity + CPUCapacity resource.Quantity + PodCapacity resource.Quantity + MemoryAllocatable resource.Quantity + CPUAllocatable resource.Quantity + PodAllocatable resource.Quantity + StorageClasses *v14.StorageClassList +} + +// GetClusterInfo retrieves current cluster info from cluster +func GetClusterInfo(_ctx context.Context, k8sClient client.Client, clusterName string) (*ClusterInfo, error) { + ctx := ContextWithClusterName(_ctx, clusterName) + nodes := &v1.NodeList{} + if err := k8sClient.List(ctx, nodes); err != nil { + return nil, errors.Wrapf(err, "failed to list cluster nodes") + } + var workerNumber, masterNumber int + var memoryCapacity, cpuCapacity, podCapacity, memoryAllocatable, cpuAllocatable, podAllcatable resource.Quantity + for _, node := range nodes.Items { + if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok { + masterNumber++ + } else { + workerNumber++ + } + capacity := node.Status.Capacity + memoryCapacity.Add(*capacity.Memory()) + cpuCapacity.Add(*capacity.Cpu()) + podCapacity.Add(*capacity.Pods()) + allocatable := node.Status.Allocatable + memoryAllocatable.Add(*allocatable.Memory()) + cpuAllocatable.Add(*allocatable.Cpu()) + podAllcatable.Add(*allocatable.Pods()) + } + storageClasses := &v14.StorageClassList{} + if err := k8sClient.List(ctx, storageClasses); err != nil { + return nil, errors.Wrapf(err, "failed to list storage classes") + } + return &ClusterInfo{ + Nodes: nodes, + WorkerNumber: workerNumber, + MasterNumber: masterNumber, + MemoryCapacity: memoryCapacity, + CPUCapacity: cpuCapacity, + PodCapacity: podCapacity, + MemoryAllocatable: memoryAllocatable, + CPUAllocatable: cpuAllocatable, + PodAllocatable: podAllcatable, + StorageClasses: storageClasses, + }, nil +} diff --git a/pkg/multicluster/errors.go b/pkg/multicluster/errors.go new file mode 100644 index 000000000..99b1432e9 --- /dev/null +++ b/pkg/multicluster/errors.go @@ -0,0 +1,29 @@ +/* +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 multicluster + +import "fmt" + +var ( + // ErrClusterExists cluster already exists + ErrClusterExists = ClusterManagementError(fmt.Errorf("cluster already exists")) + // ErrReservedLocalClusterName reserved cluster name is used + ErrReservedLocalClusterName = ClusterManagementError(fmt.Errorf("cluster name `local` is reserved for kubevela hub cluster")) +) + +// ClusterManagementError multicluster management error +type ClusterManagementError error diff --git a/pkg/multicluster/utils.go b/pkg/multicluster/utils.go index 4059362c9..06b19193d 100644 --- a/pkg/multicluster/utils.go +++ b/pkg/multicluster/utils.go @@ -33,6 +33,7 @@ import ( "k8s.io/klog/v2" apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" "github.com/oam-dev/kubevela/pkg/utils/common" errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" @@ -114,23 +115,25 @@ func WaitUntilClusterGatewayReady(ctx context.Context, c client.Client, maxRetry // Initialize prepare multicluster environment by checking cluster gateway service in clusters and hack rest config to use cluster gateway // if cluster gateway service is not ready, it will wait up to 5 minutes -func Initialize(restConfig *rest.Config) error { +func Initialize(restConfig *rest.Config, autoUpgrade bool) (client.Client, error) { c, err := client.New(restConfig, client.Options{Scheme: common.Scheme}) if err != nil { - return errors2.Wrapf(err, "unable to get client to find cluster gateway service") + return nil, errors2.Wrapf(err, "unable to get client to find cluster gateway service") } svc, err := WaitUntilClusterGatewayReady(context.Background(), c, 60, 5*time.Second) if err != nil { - return errors2.Wrapf(err, "failed to wait for cluster gateway, unable to use multi-cluster") + return nil, errors2.Wrapf(err, "failed to wait for cluster gateway, unable to use multi-cluster") } ClusterGatewaySecretNamespace = svc.Namespace klog.Infof("find cluster gateway service %s/%s:%d", svc.Namespace, svc.Name, *svc.Port) restConfig.Wrap(NewSecretModeMultiClusterRoundTripper) - if err = UpgradeExistingClusterSecret(context.Background(), c); err != nil { - // this error do not affect the running of current version - klog.ErrorS(err, "error encountered while grading existing cluster secret to the latest version") + if autoUpgrade { + if err = UpgradeExistingClusterSecret(context.Background(), c); err != nil { + // this error do not affect the running of current version + klog.ErrorS(err, "error encountered while grading existing cluster secret to the latest version") + } } - return nil + return c, nil } // UpgradeExistingClusterSecret upgrade outdated cluster secrets in v1.1.1 to latest @@ -158,6 +161,16 @@ func UpgradeExistingClusterSecret(ctx context.Context, c client.Client) error { return nil } +// GetMulticlusterKubernetesClient get client with multicluster function enabled +func GetMulticlusterKubernetesClient() (client.Client, *rest.Config, error) { + k8sConfig, err := config.GetConfig() + if err != nil { + return nil, nil, err + } + k8sClient, err := Initialize(k8sConfig, false) + return k8sClient, k8sConfig, err +} + // ListExistingClusterSecrets list existing cluster secrets func ListExistingClusterSecrets(ctx context.Context, c client.Client) ([]v1.Secret, error) { secrets := &v1.SecretList{} diff --git a/pkg/oam/labels.go b/pkg/oam/labels.go index a1bed0740..23e887c53 100644 --- a/pkg/oam/labels.go +++ b/pkg/oam/labels.go @@ -61,6 +61,9 @@ const ( // LabelAddonsName records the name of initializer stored in configMap LabelAddonsName = "addons.oam.dev/type" + + // LabelAddonName indicates the name of the corresponding Addon + LabelAddonName = "addons.oam.dev/name" ) const ( @@ -126,6 +129,21 @@ const ( // AnnotationLastAppliedConfiguration is kubectl annotations for 3-way merge AnnotationLastAppliedConfiguration = "kubectl.kubernetes.io/last-applied-configuration" + // AnnotationDeployVersion know the version number of the deployment. + AnnotationDeployVersion = "app.oam.dev/deployVersion" + + // AnnotationPublishVersion is annotation that record the application workflow version. + AnnotationPublishVersion = "app.oam.dev/publishVersion" + + // AnnotationWorkflowName specifies the workflow name for execution. + AnnotationWorkflowName = "app.oam.dev/workflowName" + + // AnnotationAppName specifies the name for application in db. + AnnotationAppName = "app.oam.dev/appName" + + // AnnotationAppAlias specifies the alias for application in db. + AnnotationAppAlias = "app.oam.dev/appAlias" + // AnnotationWorkloadGVK indicates the managed workload's GVK by trait AnnotationWorkloadGVK = "trait.oam.dev/workload-gvk" diff --git a/pkg/oam/util/helper_test.go b/pkg/oam/util/helper_test.go index bb9dfc3bc..8d35c6de3 100644 --- a/pkg/oam/util/helper_test.go +++ b/pkg/oam/util/helper_test.go @@ -1338,7 +1338,7 @@ func TestGetDefinitionWithClusterScope(t *testing.T) { }, }, } - // old cluster workload trait scope definition crd is cluster scope, the namesapce field is empty + // old cluster workload trait scope definition crd is cluster scope, the namespace field is empty noNs := v1alpha2.TraitDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: "noNsDefinition", diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index 428ea7352..cfe561853 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -23,7 +23,7 @@ import ( #Delete: kube.#Delete #ApplyApplication: #Steps & { - load: oam.#LoadComponets @step(1) + load: oam.#LoadComponetsInOrder @step(1) components: #Steps & { for name, c in load.value { "\(name)": oam.#ApplyComponent & { @@ -129,10 +129,16 @@ import ( #ConvertString: convert.#String +#DateToTimestamp: time.#DateToTimestamp + +#TimestampToDate: time.#TimestampToDate + #SendEmail: email.#Send #Load: oam.#LoadComponets +#LoadInOrder: oam.#LoadComponetsInOrder + #Steps: { #do: "steps" ... diff --git a/pkg/stdlib/packages.go b/pkg/stdlib/packages.go index 9a2ed616f..869211ad8 100644 --- a/pkg/stdlib/packages.go +++ b/pkg/stdlib/packages.go @@ -26,7 +26,7 @@ import ( ) var ( - //go:embed pkgs op.cue + //go:embed pkgs op.cue ql.cue fs embed.FS ) @@ -43,17 +43,26 @@ func GetPackages(tagTempl string) (map[string]string, error) { return nil, err } - pkgContent := string(opBytes) + "\n" + qlBytes, err := fs.ReadFile("ql.cue") + if err != nil { + return nil, err + } + + opContent := string(opBytes) + "\n" + qlContent := string(qlBytes) + "\n" for _, file := range files { body, err := fs.ReadFile("pkgs/" + file.Name()) if err != nil { return nil, err } - pkgContent += fmt.Sprintf("%s: {\n%s\n}\n", strings.TrimSuffix(file.Name(), ".cue"), string(body)) + pkgContent := fmt.Sprintf("%s: {\n%s\n}\n", strings.TrimSuffix(file.Name(), ".cue"), string(body)) + opContent += pkgContent + qlContent += pkgContent } return map[string]string{ - "vela/op": pkgContent + "\n" + tagTempl, + "vela/op": opContent + "\n" + tagTempl, + "vela/ql": qlContent + "\n" + tagTempl, }, nil } diff --git a/pkg/stdlib/pkgs/kube.cue b/pkg/stdlib/pkgs/kube.cue index a825ef1e3..0e6727f09 100644 --- a/pkg/stdlib/pkgs/kube.cue +++ b/pkg/stdlib/pkgs/kube.cue @@ -27,6 +27,7 @@ matchingLabels?: {...} } list?: {...} + ... } #Delete: { diff --git a/pkg/stdlib/pkgs/query.cue b/pkg/stdlib/pkgs/query.cue new file mode 100644 index 000000000..4f2c708c9 --- /dev/null +++ b/pkg/stdlib/pkgs/query.cue @@ -0,0 +1,32 @@ +#ListResourcesInApp: { + #do: "listResourcesInApp" + #provider: "query" + app: { + name: string + namespace: string + components?: [...string] + filter?: { + cluster?: string + clusterNamespace?: string + } + clusterNamespace?: string + enableHistoryQuery?: bool + } + ... +} + +#CollectPods: { + #do: "collectPods" + #provider: "query" + value: {...} + cluster: string + ... +} + +#SearchEvents: { + #do: "searchEvents" + #provider: "query" + value: {...} + cluster: string + ... +} diff --git a/pkg/stdlib/pkgs/time.cue b/pkg/stdlib/pkgs/time.cue new file mode 100644 index 000000000..f3945b6a2 --- /dev/null +++ b/pkg/stdlib/pkgs/time.cue @@ -0,0 +1,22 @@ +#DateToTimestamp: { + #do: "timestamp" + #provider: "time" + + date: string + layout: *"" | string + + timestamp?: int64 + ... +} + +#TimestampToDate: { + #do: "date" + #provider: "time" + + timestamp: int64 + layout: *"" | string + location: *"" | string + + date?: string + ... +} diff --git a/pkg/stdlib/ql.cue b/pkg/stdlib/ql.cue new file mode 100644 index 000000000..48e3a7d4a --- /dev/null +++ b/pkg/stdlib/ql.cue @@ -0,0 +1,11 @@ +#Read: kube.#Read + +#List: kube.#List + +#Delete: kube.#Delete + +#ListResourcesInApp: query.#ListResourcesInApp + +#CollectPods: query.#CollectPods + +#SearchEvents: query.#SearchEvents diff --git a/pkg/utils/parse.go b/pkg/utils/parse.go new file mode 100644 index 000000000..fb1872957 --- /dev/null +++ b/pkg/utils/parse.go @@ -0,0 +1,120 @@ +package utils + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// TypeLocal represents github +const TypeLocal = "local" + +// TypeOss represent oss +const TypeOss = "oss" + +// TypeGithub represents github +const TypeGithub = "github" + +// TypeUnknown represents parse failed +const TypeUnknown = "unknown" + +// Content contains different type of content needed when building Registry +type Content struct { + OssContent + GithubContent + LocalContent +} + +// LocalContent for local registry +type LocalContent struct { + AbsDir string `json:"abs_dir"` +} + +// OssContent for oss registry +type OssContent struct { + BucketURL string `json:"bucket_url"` +} + +// GithubContent for cap center +type GithubContent struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + Path string `json:"path"` + Ref string `json:"ref"` +} + +// Parse will parse config from address +func Parse(addr string) (string, *Content, error) { + URL, err := url.Parse(addr) + if err != nil { + return "", nil, err + } + l := strings.Split(strings.TrimPrefix(URL.Path, "/"), "/") + switch URL.Scheme { + case "http", "https": + switch URL.Host { + case "github.com": + // We support two valid format: + // 1. https://github.com///tree// + // 2. https://github.com/// + if len(l) < 3 { + return "", nil, errors.New("invalid format " + addr) + } + if l[2] == "tree" { + // https://github.com///tree// + if len(l) < 5 { + return "", nil, errors.New("invalid format " + addr) + } + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[4:], "/"), + Ref: l[3], + }, + }, nil + } + // https://github.com/// + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[0], + Repo: l[1], + Path: strings.Join(l[2:], "/"), + Ref: "", // use default branch + }, + }, + nil + case "api.github.com": + if len(l) != 5 { + return "", nil, errors.New("invalid format " + addr) + } + //https://api.github.com/repos///contents/ + return TypeGithub, &Content{ + GithubContent: GithubContent{ + Owner: l[1], + Repo: l[2], + Path: l[4], + Ref: URL.Query().Get("ref"), + }, + }, + nil + default: + } + case "oss": + return TypeOss, &Content{ + OssContent: OssContent{ + BucketURL: URL.Host, + }, + }, nil + case "file": + return TypeLocal, &Content{ + LocalContent: LocalContent{ + AbsDir: URL.Path, + }, + }, nil + + } + + return TypeUnknown, nil, nil +} diff --git a/pkg/utils/util/k8s.go b/pkg/utils/util/k8s.go new file mode 100644 index 000000000..a8d1765b2 --- /dev/null +++ b/pkg/utils/util/k8s.go @@ -0,0 +1,32 @@ +/* +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 util + +import ( + "io/ioutil" + + "github.com/oam-dev/kubevela/apis/types" +) + +// GetRuntimeNamespace get namespace of the current running pod, fall back to default vela system +func GetRuntimeNamespace() string { + ns, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") + if err != nil { + return types.DefaultKubeVelaNS + } + return string(ns) +} diff --git a/pkg/velaql/context.go b/pkg/velaql/context.go new file mode 100644 index 000000000..38d3273f5 --- /dev/null +++ b/pkg/velaql/context.go @@ -0,0 +1,96 @@ +/* + 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 velaql + +import ( + "encoding/json" + + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" +) + +// NewViewContext new view context +func NewViewContext() (wfContext.Context, error) { + viewContext := &ViewContext{} + var err error + viewContext.vars, err = value.NewValue("", nil, "") + return viewContext, err +} + +// ViewContext is view context +type ViewContext struct { + vars *value.Value +} + +// GetComponent Get ComponentManifest from workflow context. +func (c ViewContext) GetComponent(name string) (*wfContext.ComponentManifest, error) { + return nil, errors.New("not support func GetComponent") +} + +// GetComponents Get All ComponentManifest from workflow context. +func (c ViewContext) GetComponents() map[string]*wfContext.ComponentManifest { + return nil +} + +// PatchComponent patch component with value. +func (c ViewContext) PatchComponent(name string, patchValue *value.Value) error { + return errors.New("not support func PatchComponent") +} + +// GetVar get variable from workflow context. +func (c ViewContext) GetVar(paths ...string) (*value.Value, error) { + return c.vars.LookupValue(paths...) +} + +// SetVar set variable to workflow context. +func (c ViewContext) SetVar(v *value.Value, paths ...string) error { + str, err := v.String() + if err != nil { + return errors.WithMessage(err, "compile var") + } + if err := c.vars.FillRaw(str, paths...); err != nil { + return err + } + return c.vars.Error() +} + +// Commit the workflow context and persist it's content. +func (c ViewContext) Commit() error { + return errors.New("not support func Commit") +} + +// MakeParameter make 'value' with interface{} +func (c ViewContext) MakeParameter(parameter interface{}) (*value.Value, error) { + var s = "{}" + if parameter != nil { + bt, err := json.Marshal(parameter) + if err != nil { + return nil, err + } + s = string(bt) + } + + return c.vars.MakeValue(s) +} + +// StoreRef return the store reference of workflow context. +func (c ViewContext) StoreRef() *corev1.ObjectReference { + return nil +} diff --git a/pkg/velaql/parse.go b/pkg/velaql/parse.go new file mode 100644 index 000000000..6aab4b18e --- /dev/null +++ b/pkg/velaql/parse.go @@ -0,0 +1,146 @@ +/* + 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 velaql + +import ( + "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// QueryView contains query data +type QueryView struct { + View string + Parameter map[string]interface{} + Export string +} + +const ( + // PatternQL is the pattern string of velaQL, velaQL's query syntax is `ViewName{key1=value1 ,key2="value2",}.Export` + PatternQL = `(?P[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?)(?P{.*?})?\.?(?P[_a-zA-Z][\._a-zA-Z0-9]*)?` + // PatternKV is the pattern string of parameter + PatternKV = `(?P[^=]+)=(?P[^=]*?)(?:,|$)` + // KeyWordView represent view keyword + KeyWordView = "view" + // KeyWordParameter represent parameter keyword + KeyWordParameter = "parameter" + // KeyWordExport represent export keyword + KeyWordExport = "export" + // DefaultExportValue is the default Export value + DefaultExportValue = "status" +) + +var ( + qlRegexp *regexp.Regexp + kvRegexp *regexp.Regexp +) + +func init() { + qlRegexp = regexp.MustCompile(PatternQL) + kvRegexp = regexp.MustCompile(PatternKV) +} + +// ParseVelaQL parse velaQL to QueryView +func ParseVelaQL(ql string) (QueryView, error) { + qv := QueryView{ + Export: DefaultExportValue, + } + + groupNames := qlRegexp.SubexpNames() + matched := qlRegexp.FindStringSubmatch(ql) + if len(matched) != len(groupNames) || (len(matched) != 0 && matched[0] != ql) { + return qv, errors.New("fail to parse the velaQL") + } + + result := make(map[string]string, len(groupNames)) + for i, name := range groupNames { + if i != 0 && name != "" { + result[name] = strings.TrimSpace(matched[i]) + } + } + + if len(result["view"]) == 0 { + return qv, errors.New("view name shouldn't be empty") + } + + qv.View = result[KeyWordView] + if len(result[KeyWordExport]) != 0 { + qv.Export = result[KeyWordExport] + } + var err error + qv.Parameter, err = ParseParameter(result[KeyWordParameter]) + if err != nil { + return qv, err + } + return qv, nil +} + +// ParseParameter parse parameter to map[string]interface{} +func ParseParameter(parameter string) (map[string]interface{}, error) { + parameter = strings.TrimLeft(parameter, "{") + parameter = strings.TrimRight(parameter, "}") + parameter = strings.TrimSpace(parameter) + + if len(parameter) == 0 { + return nil, errors.New("parameter shouldn't be empty") + } + + groupNames := kvRegexp.SubexpNames() + matchKVs := kvRegexp.FindAllStringSubmatch(parameter, -1) + + result := make(map[string]interface{}, len(matchKVs)) + for _, kv := range matchKVs { + kvMap := make(map[string]string, 2) + if len(kv) != len(groupNames) { + return nil, errors.New("failed to parse the parameter") + } + + for i, name := range groupNames { + if i != 0 && name != "" { + kvMap[name] = strings.TrimSpace(kv[i]) + } + } + + if len(kvMap["key"]) == 0 || len(kvMap["value"]) == 0 { + return nil, errors.New("key or value in parameter shouldn't be empty") + } + result[kvMap["key"]] = string2OtherType(kvMap["value"]) + } + + return result, nil +} + +// string2OtherType convert string to other type +func string2OtherType(s string) interface{} { + i, err := strconv.ParseInt(s, 10, 64) + if err == nil { + return i + } + + b, err := strconv.ParseBool(s) + if err == nil { + return b + } + + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + return strings.Trim(s, "\"") +} diff --git a/pkg/velaql/parse_test.go b/pkg/velaql/parse_test.go new file mode 100644 index 000000000..1867cf606 --- /dev/null +++ b/pkg/velaql/parse_test.go @@ -0,0 +1,129 @@ +/* + 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 velaql + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestParseVelaQL(t *testing.T) { + testcases := []struct { + ql string + query QueryView + err error + }{{ + ql: `view{test=,test1=hello}.output`, + err: errors.New("key or value in parameter shouldn't be empty"), + }, { + ql: `{test=1,app="name"}.Export`, + err: errors.New("fail to parse the velaQL"), + }, { + ql: `view.{test=true}.output.value.spec"`, + err: errors.New("fail to parse the velaQL"), + }, { + ql: `view{test=1,app="name"}`, + query: QueryView{ + View: "view", + Export: "status", + }, + err: nil, + }, { + ql: `view{test=1,app="name"}.Export`, + query: QueryView{ + View: "view", + Export: "Export", + }, + err: nil, + }, { + ql: `view{test=true}.output.value.spec`, + query: QueryView{ + View: "view", + Export: "output.value.spec", + }, + err: nil, + }} + + for _, testcase := range testcases { + q, err := ParseVelaQL(testcase.ql) + assert.Equal(t, testcase.err != nil, err != nil) + if err == nil { + assert.Equal(t, testcase.query.View, q.View) + assert.Equal(t, testcase.query.Export, q.Export) + } else { + assert.Equal(t, testcase.err.Error(), err.Error()) + } + } +} + +func TestParseParameter(t *testing.T) { + testcases := []struct { + parameter string + parameterMap map[string]interface{} + err error + }{{ + parameter: `{ }`, + err: errors.New("parameter shouldn't be empty"), + }, { + parameter: `{}`, + err: errors.New("parameter shouldn't be empty"), + }, { + parameter: `{ testString = "pod" , testFloat= , testBoolean=true}`, + err: errors.New("key or value in parameter shouldn't be empty"), + }, { + parameter: `{testString="pod",testFloat=1000.10,testBoolean=true,testInt=1}`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }, { + parameter: `{testString="pod",testFloat=1000.10,testBoolean=true,testInt=1,}`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }, { + parameter: `{ testString = "pod" , testFloat=1000.10 , testBoolean=true , testInt=1, }`, + parameterMap: map[string]interface{}{ + "testString": "pod", + "testFloat": 1000.1, + "testBoolean": true, + "testInt": int64(1), + }, + err: nil, + }} + + for _, testcase := range testcases { + result, err := ParseParameter(testcase.parameter) + assert.Equal(t, testcase.err != nil, err != nil) + if err == nil { + for k, v := range result { + assert.Equal(t, testcase.parameterMap[k], v) + } + } else { + assert.Equal(t, testcase.err.Error(), err.Error()) + } + } +} diff --git a/pkg/velaql/providers/query/collector.go b/pkg/velaql/providers/query/collector.go new file mode 100644 index 000000000..b52a32b19 --- /dev/null +++ b/pkg/velaql/providers/query/collector.go @@ -0,0 +1,428 @@ +/* + 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 query + +import ( + "context" + "fmt" + "reflect" + + kruise "github.com/openkruise/kruise-api/apps/v1alpha1" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime/schema" + "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/pkg/controller/core.oam.dev/v1alpha2/application/dispatch" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam" + oamutil "github.com/oam-dev/kubevela/pkg/oam/util" +) + +// AppCollector collect resource created by application +type AppCollector struct { + k8sClient client.Client + opt Option +} + +// NewAppCollector create a app collector +func NewAppCollector(cli client.Client, opt Option) *AppCollector { + return &AppCollector{ + k8sClient: cli, + opt: opt, + } +} + +// CollectResourceFromApp collect resources created by application +func (c *AppCollector) CollectResourceFromApp() ([]AppResources, error) { + if c.opt.EnableHistoryQuery { + return c.CollectHistoryResourceFromApp() + } + return c.CollectLatestResourceFromApp() +} + +// CollectLatestResourceFromApp collect resources created by latest application +func (c *AppCollector) CollectLatestResourceFromApp() ([]AppResources, error) { + ctx := context.Background() + app := new(v1beta1.Application) + appKey := client.ObjectKey{Name: c.opt.Name, Namespace: c.opt.Namespace} + if err := c.k8sClient.Get(ctx, appKey, app); err != nil { + return nil, err + } + + var revision int64 + if app.Status.LatestRevision != nil { + revision = app.Status.LatestRevision.Revision + } + publishVersion := app.GetAnnotations()[oam.AnnotationPublishVersion] + deployVersion := app.GetAnnotations()[oam.AnnotationDeployVersion] + + appRevName := fmt.Sprintf("%s-v%d", app.Name, revision) + comps := make(map[string][]Resource, len(app.Spec.Components)) + for _, rsrcRef := range app.Status.AppliedResources { + if !isTargetResource(c.opt.Filter, rsrcRef) { + continue + } + compName, obj, err := getObjectCreatedByComponent(c.k8sClient, rsrcRef.ObjectReference, rsrcRef.Cluster, appRevName) + if err != nil { + return nil, err + } + if len(compName) == 0 { + continue + } + comps[compName] = append(comps[compName], Resource{ + Cluster: rsrcRef.Cluster, + Object: obj, + }) + } + compResList := c.extractComponentResourceWithOption(comps) + if len(compResList) == 0 { + return nil, errors.Errorf("fail to find resources created by %v", c.opt.Components) + } + + return []AppResources{{ + Revision: revision, + Metadata: app.ObjectMeta, + Components: compResList, + PublishVersion: publishVersion, + DeployVersion: deployVersion, + }}, nil +} + +// CollectHistoryResourceFromApp collect history resources created by application +func (c *AppCollector) CollectHistoryResourceFromApp() ([]AppResources, error) { + var appResList []AppResources + rts, err := listResourceTrackers(c.k8sClient, c.opt.Name, c.opt.Namespace) + if err != nil { + return nil, err + } + appResList = make([]AppResources, 0, len(rts)) + for _, rt := range rts { + if len(rt.Status.TrackedResources) == 0 { + continue + } + appRevName := dispatch.ExtractAppRevisionName(rt.Name, c.opt.Namespace) + revision, err := oamutil.ExtractRevisionNum(appRevName, "-") + if err != nil { + return nil, err + } + comps := make(map[string][]Resource) + for _, trackedResourceRef := range rt.Status.TrackedResources { + compName, obj, err := getObjectCreatedByComponent(c.k8sClient, trackedResourceRef, "", appRevName) + if err != nil { + return nil, err + } + if len(compName) == 0 { + continue + } + comps[compName] = append(comps[compName], Resource{ + Cluster: "", + Object: obj, + }) + } + compResList := c.extractComponentResourceWithOption(comps) + if len(compResList) != 0 { + appResList = append(appResList, AppResources{ + Revision: int64(revision), + Metadata: rt.ObjectMeta, + Components: compResList, + }) + } + } + if len(appResList) == 0 { + return nil, errors.Errorf("fail to find resources created by %v", c.opt.Components) + } + return appResList, nil +} + +func (c *AppCollector) extractComponentResourceWithOption(comps map[string][]Resource) []Component { + var result []Component + + // if not specify component, return all components resource created by app + if len(c.opt.Components) == 0 { + for name, resource := range comps { + if len(resource) == 0 { + continue + } + result = append(result, Component{ + Name: name, + Resources: resource, + }) + } + return result + } + + for _, compName := range c.opt.Components { + if len(comps[compName]) == 0 { + continue + } + result = append(result, Component{ + Name: compName, + Resources: comps[compName], + }) + } + return result +} + +// listResourceTrackers list all resourceTracker with specified app +func listResourceTrackers(cli client.Client, appName, appNs string) ([]v1beta1.ResourceTracker, error) { + listOpts := []client.ListOption{ + client.MatchingLabels{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }} + rtList := &v1beta1.ResourceTrackerList{} + ctx := context.Background() + if err := cli.List(ctx, rtList, listOpts...); err != nil { + klog.ErrorS(err, "Failed to list Resource tracker of app", "name", appName) + return nil, err + } + return rtList.Items, nil +} + +// getObjectCreatedByComponent get k8s obj created by components +func getObjectCreatedByComponent(cli client.Client, objRef corev1.ObjectReference, cluster string, appRevName string) (componentName string, obj *unstructured.Unstructured, err error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + obj = new(unstructured.Unstructured) + obj.SetGroupVersionKind(objRef.GroupVersionKind()) + obj.SetNamespace(objRef.Namespace) + obj.SetName(objRef.Name) + + key := client.ObjectKeyFromObject(obj) + if key.Namespace == "" { + key.Namespace = "default" + } + if err = cli.Get(ctx, key, obj); err != nil { + if kerrors.IsNotFound(err) { + return "", nil, nil + } + return "", nil, err + } + if obj.GetLabels()[oam.LabelAppRevision] != appRevName { + return + } + componentName = obj.GetLabels()[oam.LabelAppComponent] + return +} + +var standardWorkloads = []schema.GroupVersionKind{ + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.Deployment{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.ReplicaSet{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.StatefulSet{}).Name()), + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.DaemonSet{}).Name()), + batchv1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1.Job{}).Name()), + kruise.SchemeGroupVersion.WithKind(reflect.TypeOf(kruise.CloneSet{}).Name()), +} + +var podCollectorMap = map[schema.GroupVersionKind]PodCollector{ + batchv1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1.CronJob{}).Name()): cronJobPodCollector, + batchv1beta1.SchemeGroupVersion.WithKind(reflect.TypeOf(batchv1beta1.CronJob{}).Name()): cronJobPodCollector, +} + +// PodCollector collector pod created by workload +type PodCollector func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) + +// NewPodCollector create a PodCollector +func NewPodCollector(gvk schema.GroupVersionKind) PodCollector { + for _, workload := range standardWorkloads { + if gvk == workload { + return standardWorkloadPodCollector + } + } + if collector, ok := podCollectorMap[gvk]; ok { + return collector + } + return func(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + return nil, nil + } +} + +// standardWorkloadPodCollector collect pods created by standard workload +func standardWorkloadPodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + selectorPath := []string{"spec", "selector", "matchLabels"} + labels, found, err := unstructured.NestedStringMap(obj.UnstructuredContent(), selectorPath...) + + if err != nil { + return nil, err + } + if !found { + return nil, errors.Errorf("fail to find matchLabels from %s %s", obj.GroupVersionKind().String(), klog.KObj(obj)) + } + + listOpts := []client.ListOption{ + client.MatchingLabels(labels), + client.InNamespace(obj.GetNamespace()), + } + + podList := corev1.PodList{} + if err := cli.List(ctx, &podList, listOpts...); err != nil { + return nil, err + } + + pods := make([]*unstructured.Unstructured, len(podList.Items)) + for i := range podList.Items { + pod, err := oamutil.Object2Unstructured(podList.Items[i]) + if err != nil { + return nil, err + } + pod.SetGroupVersionKind( + corev1.SchemeGroupVersion.WithKind( + reflect.TypeOf(corev1.Pod{}).Name(), + ), + ) + pods[i] = pod + } + return pods, nil +} + +// cronJobPodCollector collect pods created by cronjob +func cronJobPodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + + jobList := new(batchv1.JobList) + if err := cli.List(ctx, jobList, client.InNamespace(obj.GetNamespace())); err != nil { + return nil, err + } + + uid := obj.GetUID() + var jobs []batchv1.Job + for _, job := range jobList.Items { + for _, owner := range job.GetOwnerReferences() { + if owner.Kind == reflect.TypeOf(batchv1.CronJob{}).Name() && owner.UID == uid { + jobs = append(jobs, job) + } + } + } + var pods []*unstructured.Unstructured + podGVK := corev1.SchemeGroupVersion.WithKind(reflect.TypeOf(corev1.Pod{}).Name()) + for _, job := range jobs { + labels := job.Spec.Selector.MatchLabels + listOpts := []client.ListOption{ + client.MatchingLabels(labels), + client.InNamespace(job.GetNamespace()), + } + podList := corev1.PodList{} + if err := cli.List(ctx, &podList, listOpts...); err != nil { + return nil, err + } + + items := make([]*unstructured.Unstructured, len(podList.Items)) + for i := range podList.Items { + pod, err := oamutil.Object2Unstructured(podList.Items[i]) + if err != nil { + return nil, err + } + pod.SetGroupVersionKind(podGVK) + items[i] = pod + } + pods = append(pods, items...) + } + return pods, nil +} + +// HelmReleaseCollector HelmRelease resources collector +type HelmReleaseCollector struct { + matchLabels map[string]string + workloadsGVK []schema.GroupVersionKind + cli client.Client +} + +// NewHelmReleaseCollector create a HelmRelease collector +func NewHelmReleaseCollector(cli client.Client, hr *unstructured.Unstructured) *HelmReleaseCollector { + return &HelmReleaseCollector{ + // matchLabels for resources created by HelmRelease refer to + // https://github.com/fluxcd/helm-controller/blob/main/internal/runner/post_renderer_origin_labels.go#L31 + matchLabels: map[string]string{ + "helm.toolkit.fluxcd.io/name": hr.GetName(), + "helm.toolkit.fluxcd.io/namespace": hr.GetNamespace(), + }, + workloadsGVK: []schema.GroupVersionKind{ + appsv1.SchemeGroupVersion.WithKind(reflect.TypeOf(appsv1.Deployment{}).Name()), + }, + cli: cli, + } +} + +// CollectWorkloads collect workloads of HelmRelease +func (c *HelmReleaseCollector) CollectWorkloads(cluster string) ([]*unstructured.Unstructured, error) { + ctx := multicluster.ContextWithClusterName(context.Background(), cluster) + listOptions := []client.ListOption{ + client.MatchingLabels(c.matchLabels), + } + var workloads []*unstructured.Unstructured + for _, workloadGVK := range c.workloadsGVK { + unstructuredObjList := &unstructured.UnstructuredList{} + unstructuredObjList.SetGroupVersionKind(workloadGVK) + if err := c.cli.List(ctx, unstructuredObjList, listOptions...); err != nil { + return nil, err + } + items := unstructuredObjList.Items + for i := range items { + items[i].SetGroupVersionKind(workloadGVK) + workloads = append(workloads, &items[i]) + } + } + return workloads, nil +} + +// helmReleasePodCollector collect pods created by helmRelease +func helmReleasePodCollector(cli client.Client, obj *unstructured.Unstructured, cluster string) ([]*unstructured.Unstructured, error) { + hc := NewHelmReleaseCollector(cli, obj) + workloads, err := hc.CollectWorkloads(cluster) + if err != nil { + return nil, err + } + var pods []*unstructured.Unstructured + for _, workload := range workloads { + collector := NewPodCollector(workload.GroupVersionKind()) + podList, err := collector(cli, workload, cluster) + if err != nil { + return nil, err + } + pods = append(pods, podList...) + } + return pods, nil +} + +func getEventFieldSelector(obj *unstructured.Unstructured) fields.Selector { + field := fields.Set{} + field["involvedObject.name"] = obj.GetName() + field["involvedObject.namespace"] = obj.GetNamespace() + field["involvedObject.kind"] = obj.GetObjectKind().GroupVersionKind().Kind + field["involvedObject.uid"] = string(obj.GetUID()) + return field.AsSelector() +} + +func isTargetResource(opt ClusterFilter, resource common.ClusterObjectReference) bool { + if opt.Cluster == "" && opt.ClusterNamespace == "" { + return true + } + if opt.Cluster == resource.Cluster && opt.ClusterNamespace == resource.ObjectReference.Namespace { + return true + } + return false +} diff --git a/pkg/velaql/providers/query/handler.go b/pkg/velaql/providers/query/handler.go new file mode 100644 index 000000000..e41fa1a09 --- /dev/null +++ b/pkg/velaql/providers/query/handler.go @@ -0,0 +1,169 @@ +/* + 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 query + +import ( + stdctx "context" + + fluxcdv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/multicluster" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "query" +) + +type provider struct { + cli client.Client +} + +// AppResources represent resources created by app +type AppResources struct { + Revision int64 `json:"revision"` + PublishVersion string `json:"publishVersion"` + DeployVersion string `json:"deployVersion"` + Metadata metav1.ObjectMeta `json:"metadata"` + Components []Component `json:"components"` +} + +// Component group resources rendered by ApplicationComponent +type Component struct { + Name string `json:"name"` + Resources []Resource `json:"resources"` +} + +// Resource refer to an object with cluster info +type Resource struct { + Cluster string `json:"cluster"` + Object *unstructured.Unstructured `json:"object"` +} + +// Option is the query option +type Option struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Components []string `json:"components,omitempty"` + Filter ClusterFilter `json:"filter,omitempty"` + EnableHistoryQuery bool `json:"enableHistoryQuery,omitempty"` +} + +// ClusterFilter filter resource created by component +type ClusterFilter struct { + Cluster string `json:"cluster,omitempty"` + ClusterNamespace string `json:"clusterNamespace,omitempty"` +} + +// ListResourcesInApp lists CRs created by Application +func (h *provider) ListResourcesInApp(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("app") + if err != nil { + return err + } + opt := Option{} + if err = val.UnmarshalTo(&opt); err != nil { + return err + } + collector := NewAppCollector(h.cli, opt) + appResList, err := collector.CollectResourceFromApp() + if err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(appResList, "list") +} + +func (h *provider) CollectPods(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("value") + if err != nil { + return err + } + cluster, err := v.GetString("cluster") + if err != nil { + return err + } + obj := new(unstructured.Unstructured) + if err = val.UnmarshalTo(obj); err != nil { + return err + } + + var pods []*unstructured.Unstructured + var collector PodCollector + + switch obj.GroupVersionKind() { + case fluxcdv2beta1.GroupVersion.WithKind(fluxcdv2beta1.HelmReleaseKind): + collector = helmReleasePodCollector + default: + collector = NewPodCollector(obj.GroupVersionKind()) + } + + pods, err = collector(h.cli, obj, cluster) + if err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(pods, "list") +} + +func (h *provider) SearchEvents(ctx wfContext.Context, v *value.Value, act types.Action) error { + val, err := v.LookupValue("value") + if err != nil { + return err + } + cluster, err := v.GetString("cluster") + if err != nil { + return err + } + obj := new(unstructured.Unstructured) + if err = val.UnmarshalTo(obj); err != nil { + return err + } + + listCtx := multicluster.ContextWithClusterName(stdctx.Background(), cluster) + fieldSelector := getEventFieldSelector(obj) + eventList := corev1.EventList{} + listOpts := []client.ListOption{ + client.InNamespace(obj.GetNamespace()), + client.MatchingFieldsSelector{ + Selector: fieldSelector, + }, + } + if err := h.cli.List(listCtx, &eventList, listOpts...); err != nil { + return v.FillObject(err.Error(), "err") + } + return v.FillObject(eventList.Items, "list") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers, cli client.Client) { + prd := &provider{ + cli: cli, + } + + p.Register(ProviderName, map[string]providers.Handler{ + "listResourcesInApp": prd.ListResourcesInApp, + "collectPods": prd.CollectPods, + "searchEvents": prd.SearchEvents, + }) +} diff --git a/pkg/velaql/providers/query/handler_test.go b/pkg/velaql/providers/query/handler_test.go new file mode 100644 index 000000000..270fa920b --- /dev/null +++ b/pkg/velaql/providers/query/handler_test.go @@ -0,0 +1,624 @@ +/* + 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 query + +import ( + "encoding/json" + "fmt" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + v1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "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/v1beta1" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/oam" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/workflow/providers" +) + +type AppResourcesList struct { + List []AppResources `json:"list,omitempty"` + App interface{} `json:"app"` + Err string `json:"err,omitempty"` +} + +type PodList struct { + List []*unstructured.Unstructured `json:"list"` + Value interface{} `json:"value"` + Cluster string `json:"cluster"` +} + +var _ = Describe("Test Query Provider", func() { + var baseDeploy *v1.Deployment + var baseService *corev1.Service + var basePod *corev1.Pod + + BeforeEach(func() { + baseDeploy = new(v1.Deployment) + Expect(yaml.Unmarshal([]byte(deploymentYaml), baseDeploy)).Should(BeNil()) + + baseService = new(corev1.Service) + Expect(yaml.Unmarshal([]byte(serviceYaml), baseService)).Should(BeNil()) + + basePod = new(corev1.Pod) + Expect(yaml.Unmarshal([]byte(podYaml), basePod)).Should(BeNil()) + }) + + Context("Test ListResourcesInApp", func() { + It("Test list latest resources created by application", func() { + namespace := "test" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + + app := v1beta1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1beta1.ApplicationSpec{ + Components: []common.ApplicationComponent{{ + Name: "web", + Type: "webservice", + Properties: util.Object2RawExtension(map[string]string{ + "image": "busybox", + }), + Traits: []common.ApplicationTrait{{ + Type: "expose", + Properties: util.Object2RawExtension(map[string]interface{}{ + "ports": []int{8000}, + }), + }}, + }}, + }, + } + + Expect(k8sClient.Create(ctx, &app)).Should(BeNil()) + oldApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(&app), oldApp)).Should(BeNil()) + oldApp.Status.LatestRevision = &common.Revision{ + Revision: 1, + } + oldApp.Status.AppliedResources = []common.ClusterObjectReference{{ + Cluster: "", + Creator: "workflow", + ObjectReference: corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Service", + Namespace: namespace, + Name: "web", + }, + }, { + Cluster: "", + Creator: "workflow", + ObjectReference: corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: namespace, + Name: "web", + }, + }} + Eventually(func() error { + err := k8sClient.Status().Update(ctx, oldApp) + if err != nil { + return err + } + return nil + }, 300*time.Microsecond, 3*time.Second).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName("web") + appDeploy.SetNamespace(namespace) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: "test-v1", + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName("web") + appService.SetNamespace(namespace) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: "test-v1", + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test" + namespace: "test" + components: ["web"] + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + type AppResourcesList struct { + List []AppResources `json:"list"` + App interface{} `json:"app"` + } + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + + Expect(len(appResList.List)).Should(Equal(1)) + Expect(len(appResList.List[0].Components)).Should(Equal(1)) + Expect(len(appResList.List[0].Components[0].Resources)).Should(Equal(2)) + + Expect(appResList.List[0].Components[0].Resources[0].Object.GroupVersionKind()).Should(Equal(oldApp.Status.AppliedResources[0].GroupVersionKind())) + Expect(appResList.List[0].Components[0].Resources[1].Object.GroupVersionKind()).Should(Equal(oldApp.Status.AppliedResources[1].GroupVersionKind())) + }) + + It("Test list legacy resources created by application", func() { + appName := "test-legacy" + appNs := "test-legacy" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: appNs}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + for i := 1; i <= 5; i++ { + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, i, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName(fmt.Sprintf("web-v%d", i)) + appDeploy.SetNamespace(appNs) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", i)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + } + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test-legacy" + namespace: "test-legacy" + components: ["web"] + enableHistoryQuery: true + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + type AppResourcesList struct { + List []AppResources `json:"list"` + App interface{} `json:"app"` + } + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + + Expect(len(appResList.List)).Should(Equal(5)) + for _, app := range appResList.List { + Expect(len(app.Components)).Should(Equal(1)) + Expect(app.Components[0].Resources[0].Object.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Service", + }).GroupVersionKind())) + Expect(app.Components[0].Resources[1].Object.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + }).GroupVersionKind())) + } + }) + + It("Test list legacy resources meet complex scene", func() { + appName := "test-legacy-complex" + appNs := "test-legacy-complex" + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: appNs}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + for i := 1; i <= 2; i++ { + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, i, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", i), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appDeploy := baseDeploy.DeepCopy() + appDeploy.SetName(fmt.Sprintf("web-v%d", i)) + appDeploy.SetNamespace(appNs) + appDeploy.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appDeploy)).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", i)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, i), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + } + + By("create resourceTracker without trackedResource") + emptyRT := new(v1beta1.ResourceTracker) + emptyRT.SetName(fmt.Sprintf("%s-%s", appName, appNs)) + emptyRT.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, emptyRT)).Should(BeNil()) + + prd := provider{cli: k8sClient} + opt := `app: { + name: "test-legacy-complex" + namespace: "test-legacy-complex" + components: [] + enableHistoryQuery: true + }` + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, v, nil)).Should(BeNil()) + + appResList := new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.List)).Should(Equal(2)) + + By("create resourceTracker tracked an un-exist resource") + rt := new(v1beta1.ResourceTracker) + rt.SetName(fmt.Sprintf("%s-v%d-%s", appName, 3, appNs)) + rt.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, rt)).Should(BeNil()) + + oldRT := new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(rt), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 3), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 3), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + appService := baseService.DeepCopy() + appService.SetName(fmt.Sprintf("web-v%d", 4)) + appService.SetNamespace(appNs) + appService.SetLabels(map[string]string{ + oam.LabelAppComponent: "web", + oam.LabelAppRevision: fmt.Sprintf("%s-v%d", appName, 4), + }) + Expect(k8sClient.Create(ctx, appService)).Should(BeNil()) + + newV, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, newV, nil)).Should(BeNil()) + appResList = new(AppResourcesList) + Expect(v.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.List)).Should(Equal(2)) + + By("create resourceTracker tracked with wrong name") + wrongNameRT := new(v1beta1.ResourceTracker) + wrongNameRT.SetName("test-1") + wrongNameRT.SetLabels(map[string]string{ + oam.LabelAppName: appName, + oam.LabelAppNamespace: appNs, + }) + Expect(k8sClient.Create(ctx, wrongNameRT)).Should(BeNil()) + + oldRT = new(v1beta1.ResourceTracker) + Eventually(func() error { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(wrongNameRT), oldRT); err != nil { + return err + } + oldRT.Status.TrackedResources = []corev1.ObjectReference{{ + APIVersion: "v1", + Kind: "Service", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 4), + }, { + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: appNs, + Name: fmt.Sprintf("web-v%d", 4), + }} + if err := k8sClient.Status().Update(ctx, oldRT); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newV, err = value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.ListResourcesInApp(nil, newV, nil)).Should(BeNil()) + appResList = new(AppResourcesList) + Expect(newV.UnmarshalTo(appResList)).Should(BeNil()) + Expect(len(appResList.Err)).ShouldNot(Equal(0)) + }) + + It("Test list resource with incomplete parameter", func() { + optWithoutApp := "" + prd := provider{cli: k8sClient} + newV, err := value.NewValue(optWithoutApp, nil, "") + Expect(err).Should(BeNil()) + err = prd.ListResourcesInApp(nil, newV, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=app) not exist")) + }) + }) + + Context("Test CollectPods", func() { + It("Test collect pod from workload deployment", func() { + deploy := baseDeploy.DeepCopy() + deploy.SetName("test-collect-pod") + deploy.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{ + oam.LabelAppComponent: "test", + }, + } + deploy.Spec.Template.ObjectMeta.SetLabels(map[string]string{ + oam.LabelAppComponent: "test", + }) + Expect(k8sClient.Create(ctx, deploy)).Should(BeNil()) + for i := 1; i <= 5; i++ { + pod := basePod.DeepCopy() + pod.SetName(fmt.Sprintf("test-collect-pod-%d", i)) + pod.SetLabels(map[string]string{ + oam.LabelAppComponent: "test", + }) + Expect(k8sClient.Create(ctx, pod)).Should(BeNil()) + } + + prd := provider{cli: k8sClient} + unstructuredDeploy, err := util.Object2Unstructured(deploy) + Expect(err).Should(BeNil()) + unstructuredDeploy.SetGroupVersionKind((&corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + }).GroupVersionKind()) + + deployJson, err := json.Marshal(unstructuredDeploy) + Expect(err).Should(BeNil()) + opt := fmt.Sprintf(`value: %s +cluster: ""`, deployJson) + v, err := value.NewValue(opt, nil, "") + Expect(err).Should(BeNil()) + Expect(prd.CollectPods(nil, v, nil)).Should(BeNil()) + + podList := new(PodList) + Expect(v.UnmarshalTo(podList)).Should(BeNil()) + Expect(len(podList.List)).Should(Equal(5)) + for _, pod := range podList.List { + Expect(pod.GroupVersionKind()).Should(Equal((&corev1.ObjectReference{ + APIVersion: "v1", + Kind: "Pod", + }).GroupVersionKind())) + } + }) + + It("Test collect pod with incomplete parameter", func() { + emptyOpt := "" + prd := provider{cli: k8sClient} + v, err := value.NewValue(emptyOpt, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=value) not exist")) + + optWithoutCluster := `value: {}` + v, err = value.NewValue(optWithoutCluster, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=cluster) not exist")) + + optWithWrongValue := `value: {test: 1} +cluster: "test"` + v, err = value.NewValue(optWithWrongValue, nil, "") + Expect(err).Should(BeNil()) + err = prd.CollectPods(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + }) + }) + + Context("Test search event from k8s object", func() { + It("Test search event with incomplete parameter", func() { + emptyOpt := "" + prd := provider{cli: k8sClient} + v, err := value.NewValue(emptyOpt, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=value) not exist")) + + optWithoutCluster := `value: {}` + v, err = value.NewValue(optWithoutCluster, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal("var(path=cluster) not exist")) + + optWithWrongValue := `value: {} +cluster: "test"` + v, err = value.NewValue(optWithWrongValue, nil, "") + Expect(err).Should(BeNil()) + err = prd.SearchEvents(nil, v, nil) + Expect(err).ShouldNot(BeNil()) + }) + }) + + It("Test install provider", func() { + p := providers.NewProviders() + Install(p, k8sClient) + h, ok := p.GetHandler("query", "listResourcesInApp") + Expect(h).ShouldNot(BeNil()) + Expect(ok).Should(Equal(true)) + h, ok = p.GetHandler("query", "collectPods") + Expect(h).ShouldNot(BeNil()) + Expect(ok).Should(Equal(true)) + h, ok = p.GetHandler("query", "searchEvents") + Expect(ok).Should(Equal(true)) + Expect(h).ShouldNot(BeNil()) + }) +}) + +var deploymentYaml = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.oam.dev/app-revision-hash: ee69f7ed168cd8fa + app.oam.dev/appRevision: first-vela-app-v1 + app.oam.dev/component: express-server + app.oam.dev/name: first-vela-app + app.oam.dev/resourceType: WORKLOAD + app.oam.dev/revision: express-server-v1 + oam.dev/render-hash: ee2d39b553b6ef03 + workload.oam.dev/type: webservice + name: express-server + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app.oam.dev/component: express-server + template: + metadata: + labels: + app.oam.dev/component: express-server + spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: express-server + ports: + - containerPort: 8000 + protocol: TCP +` + +var serviceYaml = ` +apiVersion: v1 +kind: Service +metadata: + labels: + app.oam.dev/app-revision-hash: ee69f7ed168cd8fa + app.oam.dev/appRevision: first-vela-app-v1 + app.oam.dev/component: express-server + app.oam.dev/name: first-vela-app + app.oam.dev/resourceType: TRAIT + app.oam.dev/revision: express-server-v1 + oam.dev/render-hash: bebe99ac3e9607d0 + trait.oam.dev/resource: service + trait.oam.dev/type: ingress-1-20 + name: express-server + namespace: default +spec: + ports: + - port: 8000 + protocol: TCP + targetPort: 8000 + selector: + app.oam.dev/component: express-server +` + +var podYaml = ` +apiVersion: v1 +kind: Pod +metadata: + labels: + app.oam.dev/component: express-server + name: express-server-b77f4476b-4mt5m + namespace: default +spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: express-server-1 + ports: + - containerPort: 8000 + protocol: TCP +` diff --git a/pkg/velaql/providers/query/suite_test.go b/pkg/velaql/providers/query/suite_test.go new file mode 100644 index 000000000..a1afed4ac --- /dev/null +++ b/pkg/velaql/providers/query/suite_test.go @@ -0,0 +1,77 @@ +/* + 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 query + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "k8s.io/utils/pointer" + + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context + +var _ = BeforeSuite(func(done Done) { + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + + ctx = context.Background() + Expect(err).To(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func TestQueryProvider(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VelaQL Suite") +} diff --git a/pkg/velaql/suite_test.go b/pkg/velaql/suite_test.go new file mode 100644 index 000000000..3187742f3 --- /dev/null +++ b/pkg/velaql/suite_test.go @@ -0,0 +1,104 @@ +/* + 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 velaql + +import ( + "context" + "math/rand" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var viewHandler *ViewHandler +var pod corev1.Pod +var readView corev1.ConfigMap +var applyView corev1.ConfigMap + +var _ = BeforeSuite(func(done Done) { + rand.Seed(time.Now().UnixNano()) + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + CRDDirectoryPaths: []string{"../../charts/vela-core/crds"}, + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + clients.SetKubeClient(k8sClient) + + dm, err := discoverymapper.New(cfg) + Expect(err).To(BeNil()) + pd, err := packages.NewPackageDiscover(cfg) + Expect(err).To(BeNil()) + + viewHandler = NewViewHandler(k8sClient, dm, pd) + ctx := context.Background() + + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}} + Expect(k8sClient.Create(ctx, &ns)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/example-pod.yaml", &pod)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &pod)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/read-object.yaml", &readView)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &readView)).Should(BeNil()) + + Expect(common.ReadYamlToObject("./testdata/apply-object.yaml", &applyView)).Should(BeNil()) + Expect(k8sClient.Create(ctx, &applyView)).Should(BeNil()) + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func TestVelaQL(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "VelaQL Suite") +} diff --git a/pkg/velaql/testdata/apply-object.yaml b/pkg/velaql/testdata/apply-object.yaml new file mode 100644 index 000000000..8155e28b2 --- /dev/null +++ b/pkg/velaql/testdata/apply-object.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: apply-object + namespace: vela-system +data: + template: | + import ( + "vela/op" + ) + + apply: op.#Apply & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + } + } + } + + objStatus: { + apply.value + } + parameter: { + apiVersion: string + kind: string + name: string + } diff --git a/pkg/velaql/testdata/example-pod.yaml b/pkg/velaql/testdata/example-pod.yaml new file mode 100644 index 000000000..b858d4b87 --- /dev/null +++ b/pkg/velaql/testdata/example-pod.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Pod +metadata: + name: hello-world-server-55559d5dd9-t8wt2 + namespace: default +spec: + containers: + - image: crccheck/hello-world + imagePullPolicy: Always + name: hello-world-server + ports: + - containerPort: 8000 + protocol: TCP + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-9wklg + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + nodeName: kind-control-plane + preemptionPolicy: PreemptLowerPriority + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccountName: default + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + tolerationSeconds: 300 + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + tolerationSeconds: 300 + volumes: + - name: kube-api-access-9wklg + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace \ No newline at end of file diff --git a/pkg/velaql/testdata/read-object.yaml b/pkg/velaql/testdata/read-object.yaml new file mode 100644 index 000000000..061beeb5e --- /dev/null +++ b/pkg/velaql/testdata/read-object.yaml @@ -0,0 +1,55 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: read-object + namespace: vela-system +data: + template: | + import ( + "vela/op" + ) + + output: { + if parameter.apiVersion == _|_ && parameter.kind == _|_ { + op.#Read & { + value: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + if parameter.apiVersion != _|_ || parameter.kind != _|_ { + op.#Read & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + } + objStatus: { + output.value.status + } + parameter: { + // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 + apiVersion?: string + // +usage=Specify the kind of the object, defaults to Application + kind?: string + // +usage=Specify the name of the object + name: string + // +usage=Specify the namespace of the object + namespace?: string + } + diff --git a/pkg/velaql/view.go b/pkg/velaql/view.go new file mode 100644 index 000000000..8c8f1dc07 --- /dev/null +++ b/pkg/velaql/view.go @@ -0,0 +1,137 @@ +/* + 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 velaql + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "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/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/cue/packages" + "github.com/oam-dev/kubevela/pkg/multicluster" + "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + oamutil "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils" + "github.com/oam-dev/kubevela/pkg/utils/apply" + "github.com/oam-dev/kubevela/pkg/workflow/tasks" + wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + qlNs = "vela-system" + + // ViewTaskPhaseSucceeded means view task run succeeded. + ViewTaskPhaseSucceeded = "succeeded" +) + +// ViewHandler view handler +type ViewHandler struct { + cli client.Client + viewTask v1beta1.WorkflowStep + dm discoverymapper.DiscoveryMapper + pd *packages.PackageDiscover + namespace string +} + +// NewViewHandler new view handler +func NewViewHandler(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover) *ViewHandler { + return &ViewHandler{ + cli: cli, + dm: dm, + pd: pd, + namespace: qlNs, + } +} + +// QueryView generate view step +func (handler *ViewHandler) QueryView(ctx context.Context, qv QueryView) (*value.Value, error) { + outputsTemplate := fmt.Sprintf(OutputsTemplate, qv.Export, qv.Export) + queryKey := QueryParameterKey{} + if err := json.Unmarshal([]byte(outputsTemplate), &queryKey); err != nil { + return nil, err + } + + handler.viewTask = v1beta1.WorkflowStep{ + Name: fmt.Sprintf("%s-%s", qv.View, qv.Export), + Type: qv.View, + Properties: oamutil.Object2RawExtension(qv.Parameter), + Outputs: queryKey.Outputs, + } + + taskDiscover := tasks.NewViewTaskDiscover(handler.pd, handler.cli, handler.dispatch, handler.delete, handler.namespace) + genTask, err := taskDiscover.GetTaskGenerator(ctx, handler.viewTask.Type) + if err != nil { + return nil, err + } + + runner, err := genTask(handler.viewTask, &wfTypes.GeneratorOptions{ID: utils.RandomString(10)}) + if err != nil { + return nil, err + } + + viewCtx, err := NewViewContext() + if err != nil { + return nil, err + } + status, _, err := runner.Run(viewCtx, &wfTypes.TaskRunOptions{}) + if err != nil { + return nil, err + } + if string(status.Phase) != ViewTaskPhaseSucceeded { + return nil, errors.Errorf("failed to query the view %s", status.Message) + } + return viewCtx.GetVar(qv.Export) +} + +func (handler *ViewHandler) dispatch(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifests ...*unstructured.Unstructured) error { + ctx = multicluster.ContextWithClusterName(ctx, cluster) + applicator := apply.NewAPIApplicator(handler.cli) + for _, manifest := range manifests { + if err := applicator.Apply(ctx, manifest); err != nil { + return err + } + } + return nil +} + +func (handler *ViewHandler) delete(ctx context.Context, cluster string, owner common.ResourceCreatorRole, manifest *unstructured.Unstructured) error { + return handler.cli.Delete(ctx, manifest) +} + +// QueryParameterKey query parameter key +type QueryParameterKey struct { + Outputs common.StepOutputs `json:"outputs"` +} + +// OutputsTemplate output template +var OutputsTemplate = ` +{ + "outputs": [ + { + "valueFrom": "%s", + "name": "%s" + } + ] +} +` diff --git a/pkg/velaql/view_test.go b/pkg/velaql/view_test.go new file mode 100644 index 000000000..d72d98448 --- /dev/null +++ b/pkg/velaql/view_test.go @@ -0,0 +1,95 @@ +/* + 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 velaql + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("Test VelaQL View", func() { + var ctx = context.Background() + + It("Test query a sample view", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Pod", + "name": pod.Name, + } + + velaQL := fmt.Sprintf("%s{%s}.%s", readView.Name, Map2URLParameter(parameter), "objStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + + queryValue, err := viewHandler.QueryView(context.Background(), query) + Expect(err).Should(BeNil()) + + podStatus := corev1.PodStatus{} + Expect(queryValue.UnmarshalTo(&podStatus)).Should(BeNil()) + }) + + It("Test query view with wrong request", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Pod", + "name": pod.Name, + } + + By("query view with an non-existent result") + velaQL := fmt.Sprintf("%s{%s}.%s", readView.Name, Map2URLParameter(parameter), "appStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).Should(HaveOccurred()) + + By("query an non-existent view") + velaQL = fmt.Sprintf("%s{%s}.%s", "view-resource", Map2URLParameter(parameter), "objStatus") + query, err = ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).Should(HaveOccurred()) + }) + + It("Test apply resource in view", func() { + parameter := map[string]string{ + "apiVersion": "v1", + "kind": "Namespace", + "name": "test-namespace", + } + velaQL := fmt.Sprintf("%s{%s}.%s", applyView.Name, Map2URLParameter(parameter), "objStatus") + query, err := ParseVelaQL(velaQL) + Expect(err).ShouldNot(HaveOccurred()) + _, err = viewHandler.QueryView(context.Background(), query) + Expect(err).ShouldNot(HaveOccurred()) + + ns := corev1.Namespace{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "test-namespace"}, &ns)).Should(BeNil()) + }) +}) + +func Map2URLParameter(parameter map[string]string) string { + var res string + for k, v := range parameter { + res += fmt.Sprintf("%s=\"%s\",", k, v) + } + return res +} diff --git a/pkg/workflow/providers/time/date.go b/pkg/workflow/providers/time/date.go new file mode 100644 index 000000000..bf79e64a2 --- /dev/null +++ b/pkg/workflow/providers/time/date.go @@ -0,0 +1,89 @@ +/* + 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 time + +import ( + "time" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "time" +) + +type provider struct { +} + +func (h *provider) Timestamp(ctx wfContext.Context, v *value.Value, act types.Action) error { + date, err := v.GetString("date") + if err != nil { + return err + } + layout, err := v.GetString("layout") + if err != nil { + return err + } + if layout == "" { + layout = time.RFC3339 + } + t, err := time.Parse(layout, date) + if err != nil { + return err + } + return v.FillObject(t.Unix(), "timestamp") +} + +func (h *provider) Date(ctx wfContext.Context, v *value.Value, act types.Action) error { + timestamp, err := v.GetInt64("timestamp") + if err != nil { + return err + } + layout, err := v.GetString("layout") + if err != nil { + return err + } + locationName, err := v.GetString("location") + if err != nil { + return err + } + + if layout == "" { + layout = time.RFC3339 + } + + location, err := time.LoadLocation(locationName) + if err != nil { + return err + } + t := time.Unix(timestamp, 0) + t.In(location) + return v.FillObject(t.Format(layout), "date") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers) { + prd := &provider{} + p.Register(ProviderName, map[string]providers.Handler{ + "timestamp": prd.Timestamp, + "date": prd.Date, + }) +} diff --git a/pkg/workflow/providers/time/date_test.go b/pkg/workflow/providers/time/date_test.go new file mode 100644 index 000000000..a92b070bd --- /dev/null +++ b/pkg/workflow/providers/time/date_test.go @@ -0,0 +1,170 @@ +/* + 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 time + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/workflow/providers" +) + +func TestTimestamp(t *testing.T) { + testcases := map[string]struct { + from string + expected int64 + expectedErr error + }{ + "test convert date with default time layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: ""`, + expected: 1636249671, + expectedErr: nil, + }, + "test convert date with RFC3339 layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: "2006-01-02T15:04:05Z07:00"`, + expected: 1636249671, + expectedErr: nil, + }, + "test convert date with RFC1123 layout": { + from: `date: "Fri, 01 Mar 2019 15:00:00 GMT" +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: 1551452400, + expectedErr: nil, + }, + "test convert date without time layout": { + from: `date: "2021-11-07T01:47:51Z"`, + expected: 0, + expectedErr: errors.New("var(path=layout) not exist"), + }, + "test convert without date": { + from: ``, + expected: 0, + expectedErr: errors.New("var(path=date) not exist"), + }, + "test convert date with wrong time layout": { + from: `date: "2021-11-07T01:47:51Z" +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: 0, + expectedErr: errors.New(`parsing time "2021-11-07T01:47:51Z" as "Mon, 02 Jan 2006 15:04:05 MST": cannot parse "2021-11-07T01:47:51Z" as "Mon"`), + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + r := require.New(t) + v, err := value.NewValue(tc.from, nil, "") + r.NoError(err) + prd := &provider{} + err = prd.Timestamp(nil, v, nil) + if tc.expectedErr != nil { + r.Equal(tc.expectedErr.Error(), err.Error()) + return + } + r.NoError(err) + expected, err := v.LookupValue("timestamp") + r.NoError(err) + ret, err := expected.CueValue().Int64() + r.NoError(err) + r.Equal(tc.expected, ret) + }) + } +} + +func TestDate(t *testing.T) { + testcases := map[string]struct { + from string + expected string + expectedErr error + }{ + "test convert timestamp to default time layout": { + from: `timestamp: 1636249671 +layout: "" +location: ""`, + expected: "2021-11-07T09:47:51+08:00", + expectedErr: nil, + }, + "test convert date to RFC3339 layout": { + from: `timestamp: 1636249671 +layout: "2006-01-02T15:04:05Z07:00" +location: ""`, + expected: "2021-11-07T09:47:51+08:00", + expectedErr: nil, + }, + "test convert date to RFC1123 layout": { + from: `timestamp: 1551452400 +layout: "Mon, 02 Jan 2006 15:04:05 MST" +location: ""`, + expected: "Fri, 01 Mar 2019 23:00:00 CST", + expectedErr: nil, + }, + "test convert date without time layout": { + from: `timestamp: 1551452400 +location: ""`, + expected: "", + expectedErr: errors.New("var(path=layout) not exist"), + }, + "test convert date without time location": { + from: `timestamp: 1551452400 +layout: "Mon, 02 Jan 2006 15:04:05 MST"`, + expected: "", + expectedErr: errors.New("var(path=location) not exist"), + }, + "test convert without timestamp": { + from: ``, + expected: "", + expectedErr: errors.New("var(path=timestamp) not exist"), + }, + } + + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + r := require.New(t) + v, err := value.NewValue(tc.from, nil, "") + r.NoError(err) + prd := &provider{} + err = prd.Date(nil, v, nil) + if tc.expectedErr != nil { + r.Equal(tc.expectedErr.Error(), err.Error()) + return + } + r.NoError(err) + expected, err := v.LookupValue("date") + r.NoError(err) + ret, err := expected.CueValue().String() + r.NoError(err) + r.Equal(tc.expected, ret) + }) + } +} + +func TestInstall(t *testing.T) { + p := providers.NewProviders() + Install(p) + h, ok := p.GetHandler("time", "timestamp") + r := require.New(t) + r.Equal(ok, true) + r.Equal(h != nil, true) + + h, ok = p.GetHandler("time", "date") + r.Equal(ok, true) + r.Equal(h != nil, true) +} diff --git a/pkg/workflow/recorder/recorder.go b/pkg/workflow/recorder/recorder.go index bbfd0e713..a2ea343cf 100644 --- a/pkg/workflow/recorder/recorder.go +++ b/pkg/workflow/recorder/recorder.go @@ -91,7 +91,16 @@ func (r *recorder) Save(version string, data []byte) Store { } if err := r.cli.Create(context.Background(), rv); err != nil { if kerrors.IsAlreadyExists(err) { - r.err = r.cli.Update(context.Background(), rv) + // ControllerRevision implements an immutable snapshot of state data + // Once a ControllerRevision has been successfully created, it can not be updated. + // So we need to delete the old one and create a new one. + r.err = r.cli.Delete(context.Background(), &apps.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: rv.Name, + Namespace: rv.Namespace, + }, + }) + r.err = r.cli.Create(context.Background(), rv) } else { r.err = errors.WithMessagef(err, "save record %s/%s", rv.Namespace, rv.Name) } diff --git a/pkg/workflow/tasks/discover.go b/pkg/workflow/tasks/discover.go index 387b01667..fb0f5d88b 100644 --- a/pkg/workflow/tasks/discover.go +++ b/pkg/workflow/tasks/discover.go @@ -26,11 +26,14 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/cue/packages" "github.com/oam-dev/kubevela/pkg/oam/discoverymapper" + "github.com/oam-dev/kubevela/pkg/velaql/providers/query" wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" "github.com/oam-dev/kubevela/pkg/workflow/providers" "github.com/oam-dev/kubevela/pkg/workflow/providers/convert" "github.com/oam-dev/kubevela/pkg/workflow/providers/email" "github.com/oam-dev/kubevela/pkg/workflow/providers/http" + "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" + "github.com/oam-dev/kubevela/pkg/workflow/providers/time" "github.com/oam-dev/kubevela/pkg/workflow/providers/workspace" "github.com/oam-dev/kubevela/pkg/workflow/tasks/custom" "github.com/oam-dev/kubevela/pkg/workflow/tasks/template" @@ -40,7 +43,7 @@ import ( type taskDiscover struct { builtins map[string]types.TaskGenerator remoteTaskDiscover *custom.TaskLoader - templateLoader *template.Loader + templateLoader template.Loader } // GetTaskGenerator get task generator by name. @@ -76,7 +79,7 @@ func NewTaskDiscover(providerHandlers providers.Providers, pd *packages.PackageD http.Install(providerHandlers) convert.Install(providerHandlers) email.Install(providerHandlers) - templateLoader := template.NewTemplateLoader(cli, dm) + templateLoader := template.NewWorkflowStepTemplateLoader(cli, dm) return &taskDiscover{ builtins: map[string]types.TaskGenerator{ "suspend": suspend, @@ -110,3 +113,22 @@ func (tr *suspendTaskRunner) Run(ctx wfContext.Context, options *types.TaskRunOp func (tr *suspendTaskRunner) Pending(ctx wfContext.Context) bool { return false } + +// NewViewTaskDiscover will create a client for load task generator. +func NewViewTaskDiscover(pd *packages.PackageDiscover, cli client.Client, apply kube.Dispatcher, delete kube.Deleter, viewNs string) types.TaskDiscover { + handlerProviders := providers.NewProviders() + + // install builtin provider + query.Install(handlerProviders, cli) + time.Install(handlerProviders) + kube.Install(handlerProviders, cli, apply, delete) + http.Install(handlerProviders) + convert.Install(handlerProviders) + email.Install(handlerProviders) + + templateLoader := template.NewViewTemplateLoader(cli, viewNs) + return &taskDiscover{ + remoteTaskDiscover: custom.NewTaskLoader(templateLoader.LoadTaskTemplate, pd, handlerProviders), + templateLoader: templateLoader, + } +} diff --git a/pkg/workflow/tasks/template/load.go b/pkg/workflow/tasks/template/load.go index ec75c1fdc..29316aa6e 100644 --- a/pkg/workflow/tasks/template/load.go +++ b/pkg/workflow/tasks/template/load.go @@ -22,6 +22,7 @@ import ( "path/filepath" "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/types" @@ -39,13 +40,18 @@ const ( ) // Loader load task definition template. -type Loader struct { +type Loader interface { + LoadTaskTemplate(ctx context.Context, name string) (string, error) +} + +// WorkflowStepLoader load workflowStep task definition template. +type WorkflowStepLoader struct { client client.Client dm discoverymapper.DiscoveryMapper } // LoadTaskTemplate gets the workflowStep definition. -func (loader *Loader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { +func (loader *WorkflowStepLoader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { files, err := templateFS.ReadDir(templateDir) if err != nil { return "", err @@ -71,10 +77,34 @@ func (loader *Loader) LoadTaskTemplate(ctx context.Context, name string) (string return "", errors.New("custom workflowStep only support cue") } -// NewTemplateLoader create a task template loader. -func NewTemplateLoader(client client.Client, dm discoverymapper.DiscoveryMapper) *Loader { - return &Loader{ +// NewWorkflowStepTemplateLoader create a task template loader. +func NewWorkflowStepTemplateLoader(client client.Client, dm discoverymapper.DiscoveryMapper) Loader { + return &WorkflowStepLoader{ client: client, dm: dm, } } + +// ViewLoader load view task definition template. +type ViewLoader struct { + client client.Client + namespace string +} + +// LoadTaskTemplate gets the workflowStep definition. +func (loader *ViewLoader) LoadTaskTemplate(ctx context.Context, name string) (string, error) { + cm := new(corev1.ConfigMap) + cmKey := client.ObjectKey{Name: name, Namespace: loader.namespace} + if err := loader.client.Get(ctx, cmKey, cm); err != nil { + return "", errors.Wrapf(err, "fail to get view template %v from configMap", cmKey) + } + return cm.Data["template"], nil +} + +// NewViewTemplateLoader create a view task template loader. +func NewViewTemplateLoader(client client.Client, namespace string) Loader { + return &ViewLoader{ + client: client, + namespace: namespace, + } +} diff --git a/pkg/workflow/tasks/template/load_test.go b/pkg/workflow/tasks/template/load_test.go index 0658c8967..b0a2192f4 100644 --- a/pkg/workflow/tasks/template/load_test.go +++ b/pkg/workflow/tasks/template/load_test.go @@ -51,7 +51,7 @@ func TestLoad(t *testing.T) { }, } tdm := mock.NewMockDiscoveryMapper() - loader := NewTemplateLoader(cli, tdm) + loader := NewWorkflowStepTemplateLoader(cli, tdm) tmpl, err := loader.LoadTaskTemplate(context.Background(), "builtin-apply-component") assert.NilError(t, err) diff --git a/pkg/workflow/types/types.go b/pkg/workflow/types/types.go index d15878c3b..7dc8e1984 100644 --- a/pkg/workflow/types/types.go +++ b/pkg/workflow/types/types.go @@ -79,6 +79,4 @@ type Action interface { const ( // ContextKeyMetadata is key that refer to application metadata. ContextKeyMetadata = "metadata__" - // AnnotationPublishVersion is annotation that record the application workflow version. - AnnotationPublishVersion = "vela.io/publish-version" ) diff --git a/pkg/workflow/workflow.go b/pkg/workflow/workflow.go index b74526a23..a5bb701a4 100644 --- a/pkg/workflow/workflow.go +++ b/pkg/workflow/workflow.go @@ -31,6 +31,7 @@ import ( "github.com/oam-dev/kubevela/pkg/cue/model/value" monitorContext "github.com/oam-dev/kubevela/pkg/monitor/context" "github.com/oam-dev/kubevela/pkg/monitor/metrics" + "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" "github.com/oam-dev/kubevela/pkg/workflow/recorder" @@ -343,7 +344,7 @@ func (e *engine) needStop() bool { func computeAppRevisionHash(rev string, app *oamcore.Application) (string, error) { version := "" if annos := app.Annotations; annos != nil { - version = annos[wfTypes.AnnotationPublishVersion] + version = annos[oam.AnnotationPublishVersion] } if version == "" { specHash, err := utils.ComputeSpecHash(app.Spec) diff --git a/references/cli/cluster.go b/references/cli/cluster.go index 44de4ddd0..f95b61a6d 100644 --- a/references/cli/cluster.go +++ b/references/cli/cluster.go @@ -42,10 +42,8 @@ import ( "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/clustermanager" "github.com/oam-dev/kubevela/pkg/multicluster" - "github.com/oam-dev/kubevela/pkg/policy/envbinding" "github.com/oam-dev/kubevela/pkg/utils" "github.com/oam-dev/kubevela/pkg/utils/common" - errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" "github.com/oam-dev/kubevela/references/a/preimport" ) @@ -328,38 +326,6 @@ func registerClusterManagedByOCM(hubConfig *rest.Config, spokeConfig *clientcmda return nil } -func getMutableClusterSecret(c client.Client, clusterName string) (*v1.Secret, error) { - clusterSecret := &v1.Secret{} - if err := c.Get(context.Background(), types2.NamespacedName{Namespace: multicluster.ClusterGatewaySecretNamespace, Name: clusterName}, clusterSecret); err != nil { - return nil, errors.Wrapf(err, "failed to find target cluster secret %s", clusterName) - } - labels := clusterSecret.GetLabels() - if labels == nil || labels[v1alpha12.LabelKeyClusterCredentialType] == "" { - return nil, fmt.Errorf("invalid cluster secret %s: cluster credential type label %s is not set", clusterName, v1alpha12.LabelKeyClusterCredentialType) - } - apps := &v1beta1.ApplicationList{} - if err := c.List(context.Background(), apps); err != nil { - return nil, errors.Wrap(err, "failed to find applications to check clusters") - } - errs := errors3.ErrorList{} - for _, app := range apps.Items { - status, err := envbinding.GetEnvBindingPolicyStatus(app.DeepCopy(), "") - if err == nil && status != nil { - for _, env := range status.Envs { - for _, placement := range env.Placements { - if placement.Cluster == clusterName { - errs.Append(fmt.Errorf("application %s/%s (env: %s) is currently using cluster %s", app.Namespace, app.Name, env.Env, clusterName)) - } - } - } - } - } - if errs.HasError() { - return nil, errors.Wrapf(errs, "cluster %s is in use now", clusterName) - } - return clusterSecret, nil -} - // NewClusterRenameCommand create command to help user rename cluster func NewClusterRenameCommand(c *common.Args) *cobra.Command { cmd := &cobra.Command{ @@ -372,7 +338,7 @@ func NewClusterRenameCommand(c *common.Args) *cobra.Command { if newClusterName == multicluster.ClusterLocalName { return fmt.Errorf("cannot use `%s` as cluster name, it is reserved as the local cluster", multicluster.ClusterLocalName) } - clusterSecret, err := getMutableClusterSecret(c.Client, oldClusterName) + clusterSecret, err := multicluster.GetMutableClusterSecret(context.Background(), c.Client, oldClusterName) if err != nil { return errors.Wrapf(err, "cluster %s is not mutable now", oldClusterName) } @@ -425,7 +391,7 @@ func NewClusterDetachCommand(c *common.Args) *cobra.Command { switch clusterType { case string(v1alpha12.CredentialTypeX509Certificate), string(v1alpha12.CredentialTypeServiceAccountToken): - clusterSecret, err := getMutableClusterSecret(c.Client, clusterName) + clusterSecret, err := multicluster.GetMutableClusterSecret(context.Background(), c.Client, clusterName) if err != nil { return errors.Wrapf(err, "cluster %s is not mutable now", clusterName) } diff --git a/test/e2e-apiserver-test/addon_test.go b/test/e2e-apiserver-test/addon_test.go new file mode 100644 index 000000000..7df0ea650 --- /dev/null +++ b/test/e2e-apiserver-test/addon_test.go @@ -0,0 +1,137 @@ +package e2e_apiserver_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + "github.com/pkg/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/addon" + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const baseURL = "http://127.0.0.1:8000" + +func post(path string, body interface{}) *http.Response { + b, err := json.Marshal(body) + Expect(err).Should(BeNil()) + + res, err := http.Post(baseURL+path, "application/json", bytes.NewBuffer(b)) + Expect(err).Should(BeNil()) + return res +} + +func get(path string) *http.Response { + res, err := http.Get(baseURL + path) + Expect(err).Should(BeNil()) + return res +} + +var _ = Describe("Test addon rest api", func() { + createReq := apis.CreateAddonRegistryRequest{ + Name: "test-addon-registry-1", + Git: &addon.GitAddonSource{ + URL: "https://github.com/oam-dev/catalog", + Path: "addons/", + Token: os.Getenv("GITHUB_TOKEN"), + }, + } + It("should add a registry and list addons from it", func() { + defer GinkgoRecover() + + By("add registry") + createRes := post("/api/v1/addon_registries", createReq) + Expect(createRes).ShouldNot(BeNil()) + Expect(createRes.Body).ShouldNot(BeNil()) + Expect(createRes.StatusCode).Should(Equal(200)) + + defer createRes.Body.Close() + + var rmeta apis.AddonRegistryMeta + err := json.NewDecoder(createRes.Body).Decode(&rmeta) + Expect(err).Should(BeNil()) + Expect(rmeta.Name).Should(Equal(createReq.Name)) + Expect(rmeta.Git).Should(Equal(createReq.Git)) + + By("list addons") + listRes := get("/api/v1/addons/") + defer listRes.Body.Close() + + var lres apis.ListAddonResponse + err = json.NewDecoder(listRes.Body).Decode(&lres) + Expect(err).Should(BeNil()) + Expect(lres.Addons).ShouldNot(BeZero()) + firstAddon := lres.Addons[0] + Expect(firstAddon.Name).Should(Equal("example")) + + }) + + It("should enable and disable an addon", func() { + defer GinkgoRecover() + req := apis.EnableAddonRequest{ + Args: map[string]string{ + "example": "test-args", + }, + } + testAddon := "example" + res := post("/api/v1/addons/"+testAddon+"/enable", req) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + Expect(res.Body).ShouldNot(BeNil()) + + defer res.Body.Close() + + var statusRes apis.AddonStatusResponse + err := json.NewDecoder(res.Body).Decode(&statusRes) + + Expect(err).Should(BeNil()) + Expect(statusRes.Phase).Should(Equal(apis.AddonPhaseEnabling)) + + // Wait for addon enabled + + period := 10 * time.Second + timeout := 2 * time.Minute + Eventually(func() error { + res = get("/api/v1/addons/" + testAddon + "/status") + err = json.NewDecoder(res.Body).Decode(&statusRes) + Expect(err).Should(BeNil()) + if statusRes.Phase == apis.AddonPhaseEnabled { + return nil + } + var app v1beta1.Application + err = k8sClient.Get(context.Background(), client.ObjectKey{Name: "addon-example", Namespace: "vela-system"}, &app) + Expect(err).Should(BeNil()) + data, err := json.Marshal(app) + Expect(err).Should(BeNil()) + fmt.Println(data) + return errors.New("not ready") + }, timeout, period).Should(BeNil()) + + res = post("/api/v1/addons/"+testAddon+"/disable", req) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + Expect(res.Body).ShouldNot(BeNil()) + + err = json.NewDecoder(res.Body).Decode(&statusRes) + Expect(err).Should(BeNil()) + }) + + It("should delete test registry", func() { + defer GinkgoRecover() + deleteReq, err := http.NewRequest(http.MethodDelete, baseURL+"/api/v1/addon_registries/"+createReq.Name, nil) + Expect(err).Should(BeNil()) + deleteRes, err := http.DefaultClient.Do(deleteReq) + Expect(err).Should(BeNil()) + Expect(deleteRes).ShouldNot(BeNil()) + Expect(deleteRes.StatusCode).Should(Equal(200)) + }) +}) diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go index adb82a2d1..62192b71e 100644 --- a/test/e2e-apiserver-test/application_test.go +++ b/test/e2e-apiserver-test/application_test.go @@ -18,26 +18,34 @@ package e2e_apiserver_test import ( "bytes" + "context" "encoding/json" + "io/ioutil" "net/http" "github.com/google/go-cmp/cmp" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/model" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" ) +var appName = "app-e2e" +var appProject = "test-app-project" + var _ = Describe("Test application rest api", func() { It("Test create app", func() { defer GinkgoRecover() var req = apisv1.CreateApplicationRequest{ - Name: "test-app-sadasd", - Namespace: "test-app-namesapce", + Name: appName, + Namespace: appProject, Description: "this is a test app", Icon: "", Labels: map[string]string{"test": "true"}, - ClusterList: []string{}, + EnvBinding: []*apisv1.EnvBinding{{Name: "dev-env", TargetNames: []string{"test-target"}}}, } bodyByte, err := json.Marshal(req) Expect(err).ShouldNot(HaveOccurred()) @@ -52,6 +60,309 @@ var _ = Describe("Test application rest api", func() { Expect(err).ShouldNot(HaveOccurred()) Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Namespace, req.Namespace)).Should(BeEmpty()) Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) }) + + It("Test delete app", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName, nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + + It("Test create app with oamspec", func() { + defer GinkgoRecover() + bs, err := ioutil.ReadFile("./testdata/example-app.yaml") + Expect(err).Should(Succeed()) + var req = apisv1.CreateApplicationRequest{ + Name: appName, + Namespace: appProject, + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + YamlConfig: string(bs), + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var appBase apisv1.ApplicationBase + err = json.NewDecoder(res.Body).Decode(&appBase) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Namespace, req.Namespace)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) + }) + + It("Test list components", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/components") + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var components apisv1.ComponentListResponse + err = json.NewDecoder(res.Body).Decode(&components) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(components.Components), 2)).Should(BeEmpty()) + }) + + It("Test detail application", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var detail apisv1.DetailApplicationResponse + err = json.NewDecoder(res.Body).Decode(&detail) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(detail.Policies), 0)).Should(BeEmpty()) + }) + + It("Test deploy application", func() { + defer GinkgoRecover() + var targetName = "dev-default" + var envName = "dev" + var namespace = "default" + // create target + var createTarget = apisv1.CreateDeliveryTargetRequest{ + Name: targetName, + Namespace: appProject, + Cluster: &apisv1.ClusterTarget{ + ClusterName: "local", + Namespace: namespace, + }, + } + bodyByte, err := json.Marshal(createTarget) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/deliveryTargets", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + + // create env + var createEnvReq = apisv1.CreateApplicationEnvRequest{ + EnvBinding: apisv1.EnvBinding{ + Name: envName, + TargetNames: []string{targetName}, + }, + } + bodyByte, err = json.Marshal(createEnvReq) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/envs", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + + // deploy app + var req = apisv1.ApplicationDeployRequest{ + Note: "test apply", + TriggerType: "web", + WorkflowName: "dev", + Force: false, + } + bodyByte, err = json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/deploy", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.ApplicationDeployResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Status, model.RevisionStatusRunning)).Should(BeEmpty()) + + var oam v1beta1.Application + err = k8sClient.Get(context.TODO(), types.NamespacedName{Name: appName + "-" + envName, Namespace: appProject}, &oam) + Expect(err).Should(BeNil()) + Expect(cmp.Diff(len(oam.Spec.Components), 2)).Should(BeEmpty()) + Expect(cmp.Diff(len(oam.Spec.Policies), 1)).Should(BeEmpty()) + }) + + It("Test create component", func() { + defer GinkgoRecover() + var req = apisv1.CreateComponentRequest{ + Name: "test2", + Description: "this is a test2 component", + Labels: map[string]string{}, + ComponentType: "worker", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + DependsOn: []string{"data-worker"}, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/components", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.ComponentBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.ComponentType, "worker")).Should(BeEmpty()) + }) + + It("Test detail component", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/components/test2") + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.DetailComponentResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(len(response.DependsOn), 1)).Should(BeEmpty()) + }) + + It("Test add trait", func() { + defer GinkgoRecover() + var req = apisv1.CreateApplicationTraitRequest{ + Type: "ingress", + Properties: `{"domain": "www.test.com"}`, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.ApplicationTrait + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Properties.JSON(), `{"domain":"www.test.com"}`)).Should(BeEmpty()) + }) + + It("Test update trait", func() { + defer GinkgoRecover() + var req2 = apisv1.CreateApplicationTraitRequest{ + Type: "ingress", + Properties: `{"domain": "www.test1.com"}`, + } + bodyByte, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits/ingress", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.ApplicationTrait + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Properties.JSON(), `{"domain":"www.test1.com"}`)).Should(BeEmpty()) + }) + + It("Test delete trait", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/components/test2/traits/ingress", nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + + It("Test create application policy", func() { + defer GinkgoRecover() + var req = apisv1.CreatePolicyRequest{ + Name: "test2", + Description: "this is a test2 component", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 400)).Should(BeEmpty()) + var req2 = apisv1.CreatePolicyRequest{ + Name: "test2", + Description: "this is a test2 policy", + Type: "wqsdasd", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte2, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + res, err = http.Post("http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies", "application/json", bytes.NewBuffer(bodyByte2)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.PolicyBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Type, "wqsdasd")).Should(BeEmpty()) + }) + + It("Test detail application policy", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/applications/" + appName + "/policies/test2") + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.DetailPolicyResponse + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Description, "this is a test2 policy")).Should(BeEmpty()) + }) + + It("Test update application policy", func() { + var req2 = apisv1.UpdatePolicyRequest{ + Description: "this is a test2 policy update", + Type: "wqsdasd", + Properties: `{"image": "busybox","cmd":["sleep", "1000"],"lives": "3","enemies": "alien"}`, + } + bodyByte2, err := json.Marshal(req2) + Expect(err).ShouldNot(HaveOccurred()) + req, err := http.NewRequest(http.MethodPut, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies/test2", bytes.NewBuffer(bodyByte2)) + Expect(err).ShouldNot(HaveOccurred()) + req.Header.Set("Content-Type", "application/json") + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var response apisv1.PolicyBase + err = json.NewDecoder(res.Body).Decode(&response) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(response.Description, "this is a test2 policy update")).Should(BeEmpty()) + }) + + It("Test delete application policy", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, "http://127.0.0.1:8000/api/v1/applications/"+appName+"/policies/test2", nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) + }) diff --git a/test/e2e-apiserver-test/cluster_test.go b/test/e2e-apiserver-test/cluster_test.go new file mode 100644 index 000000000..d4c8cdf58 --- /dev/null +++ b/test/e2e-apiserver-test/cluster_test.go @@ -0,0 +1,157 @@ +/* +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 e2e_apiserver + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/multicluster" + util "github.com/oam-dev/kubevela/pkg/utils" +) + +const ( + WorkerClusterName = "cluster-worker" + WorkerClusterKubeConfigPath = "/tmp/worker.kubeconfig" +) + +var _ = Describe("Test cluster rest api", func() { + + Context("Test basic cluster CURD", func() { + + var clusterName string + + BeforeEach(func() { + clusterName = WorkerClusterName + "-" + util.RandomString(8) + kubeconfigBytes, err := ioutil.ReadFile(WorkerClusterKubeConfigPath) + Expect(err).Should(Succeed()) + resp, err := CreateRequest(http.MethodPost, "/clusters", v1.CreateClusterRequest{ + Name: clusterName, + KubeConfig: string(kubeconfigBytes), + }) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + AfterEach(func() { + resp, err := CreateRequest(http.MethodDelete, "/clusters/"+clusterName, nil) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + It("Test get cluster", func() { + resp, err := CreateRequest(http.MethodGet, "/clusters/"+clusterName, nil) + clusterResp := &v1.DetailClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(clusterResp.Status).Should(Equal("Healthy")) + }) + + It("Test list clusters", func() { + resp, err := CreateRequest(http.MethodGet, "/clusters/?page=1&pageSize=5", nil) + clusterResp := &v1.ListClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters) >= 2).Should(BeTrue()) + Expect(clusterResp.Clusters[0].Name).Should(Equal(multicluster.ClusterLocalName)) + Expect(clusterResp.Clusters[1].Name).Should(Equal(clusterName)) + resp, err = CreateRequest(http.MethodGet, "/clusters/?page=1&pageSize=5&query="+WorkerClusterName, nil) + clusterResp = &v1.ListClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters) >= 1).Should(BeTrue()) + Expect(clusterResp.Clusters[0].Name).Should(Equal(clusterName)) + }) + + It("Test modify cluster", func() { + kubeconfigBytes, err := ioutil.ReadFile(WorkerClusterKubeConfigPath) + Expect(err).Should(Succeed()) + resp, err := CreateRequest(http.MethodPut, "/clusters/"+clusterName, v1.CreateClusterRequest{ + Name: clusterName, + KubeConfig: string(kubeconfigBytes), + Description: "Example description", + }) + clusterResp := &v1.ClusterBase{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(clusterResp.Description).ShouldNot(Equal("")) + }) + + It("Test create ns in cluster", func() { + testNamespace := fmt.Sprintf("test-%d", time.Now().Unix()) + resp, err := CreateRequest(http.MethodPost, "/clusters/"+clusterName+"/namespaces", v1.CreateClusterNamespaceRequest{Namespace: testNamespace}) + Expect(err).Should(Succeed()) + nsResp := &v1.CreateClusterNamespaceResponse{} + Expect(DecodeResponseBody(resp, err, nsResp)).Should(Succeed()) + Expect(nsResp.Exists).Should(Equal(false)) + resp, err = CreateRequest(http.MethodPost, "/clusters/"+clusterName+"/namespaces", v1.CreateClusterNamespaceRequest{Namespace: testNamespace}) + Expect(err).Should(Succeed()) + nsResp = &v1.CreateClusterNamespaceResponse{} + Expect(DecodeResponseBody(resp, err, nsResp)).Should(Succeed()) + Expect(nsResp.Exists).Should(Equal(true)) + }) + + }) + + PContext("Test cloud cluster rest api", func() { + + var clusterName string + + BeforeEach(func() { + clusterName = WorkerClusterName + "-" + util.RandomString(8) + }) + + AfterEach(func() { + resp, err := CreateRequest(http.MethodDelete, "/clusters/"+clusterName, nil) + Expect(err).Should(Succeed()) + Expect(resp.StatusCode).Should(Equal(200)) + Expect(resp.Body).ShouldNot(BeNil()) + Expect(resp.Body.Close()).Should(Succeed()) + }) + + It("Test list aliyun cloud cluster and connect", func() { + AccessKeyID := os.Getenv("ALIYUN_ACCESS_KEY_ID") + AccessKeySecret := os.Getenv("ALIYUN_ACCESS_KEY_SECRET") + resp, err := CreateRequest(http.MethodPost, "/clusters/cloud-clusters/aliyun/?page=1&pageSize=5", v1.AccessKeyRequest{ + AccessKeyID: AccessKeyID, + AccessKeySecret: AccessKeySecret, + }) + clusterResp := &v1.ListCloudClusterResponse{} + Expect(DecodeResponseBody(resp, err, clusterResp)).Should(Succeed()) + Expect(len(clusterResp.Clusters)).ShouldNot(Equal(0)) + + ClusterID := clusterResp.Clusters[0].ID + resp, err = CreateRequest(http.MethodPost, "/clusters/cloud-clusters/aliyun/connect", v1.ConnectCloudClusterRequest{ + AccessKeyID: AccessKeyID, + AccessKeySecret: AccessKeySecret, + ClusterID: ClusterID, + Name: clusterName, + }) + clusterBase := &v1.ClusterBase{} + Expect(DecodeResponseBody(resp, err, clusterBase)).Should(Succeed()) + Expect(clusterBase.Status).Should(Equal("Healthy")) + }) + + }) +}) diff --git a/test/e2e-apiserver-test/definition_test.go b/test/e2e-apiserver-test/definition_test.go new file mode 100644 index 000000000..c0abf49f9 --- /dev/null +++ b/test/e2e-apiserver-test/definition_test.go @@ -0,0 +1,45 @@ +/* +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 e2e_apiserver_test + +import ( + "encoding/json" + "net/http" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test definitions rest api", func() { + + It("Test list definitions", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/definitions?type=component") + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var definitions apisv1.ListDefinitionResponse + err = json.NewDecoder(res.Body).Decode(&definitions) + Expect(err).ShouldNot(HaveOccurred()) + }) + +}) diff --git a/test/e2e-apiserver-test/namespace_test.go b/test/e2e-apiserver-test/namespace_test.go new file mode 100644 index 000000000..6fa313610 --- /dev/null +++ b/test/e2e-apiserver-test/namespace_test.go @@ -0,0 +1,65 @@ +/* +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 e2e_apiserver_test + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test namespace rest api", func() { + It("Test create namespace", func() { + defer GinkgoRecover() + var req = apisv1.CreateNamespaceRequest{ + Name: "dev-team", + Description: "开发环境租户", + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/namespaces", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var namespaceBase apisv1.NamespaceBase + err = json.NewDecoder(res.Body).Decode(&namespaceBase) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(namespaceBase.Name, req.Name)).Should(BeEmpty()) + Expect(cmp.Diff(namespaceBase.Description, req.Description)).Should(BeEmpty()) + }) + + It("Test list namespace", func() { + defer GinkgoRecover() + res, err := http.Get("http://127.0.0.1:8000/api/v1/namespaces") + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var namespaces apisv1.ListNamespaceResponse + err = json.NewDecoder(res.Body).Decode(&namespaces) + Expect(err).ShouldNot(HaveOccurred()) + }) +}) diff --git a/test/e2e-apiserver-test/oam_application_test.go b/test/e2e-apiserver-test/oam_application_test.go new file mode 100644 index 000000000..06f11cf08 --- /dev/null +++ b/test/e2e-apiserver-test/oam_application_test.go @@ -0,0 +1,122 @@ +/* + 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 e2e_apiserver_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var _ = Describe("Test oam application rest api", func() { + namespace := "test-oam-app" + appName := "example-app" + var app v1beta1.Application + + It("Test create and update oam app", func() { + defer GinkgoRecover() + By("test create app") + + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &app)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: app.Spec.Components, + Policies: app.Spec.Policies, + Workflow: app.Spec.Workflow, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "application/json", + bytes.NewBuffer(bodyByte), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + + ctx := context.Background() + oldApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, oldApp)).Should(BeNil()) + Expect(oldApp.Spec.Components).Should(Equal(req.Components)) + Expect(oldApp.Spec.Policies).Should(Equal(req.Policies)) + Expect(oldApp.Spec.Workflow).Should(Equal(req.Workflow)) + + By("test update app") + updateReq := apiv1.ApplicationRequest{ + Components: app.Spec.Components[1:], + } + bodyByte, err = json.Marshal(updateReq) + Expect(err).Should(BeNil()) + Eventually(func(g Gomega) { + res, err = http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "application/json", + bytes.NewBuffer(bodyByte), + ) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(res).ShouldNot(BeNil()) + g.Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + g.Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + }, time.Minute).Should(Succeed()) + newApp := new(v1beta1.Application) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: appName, Namespace: namespace}, newApp)).Should(BeNil()) + Expect(newApp.Spec.Components).Should(Equal(updateReq.Components)) + Expect(newApp.Spec.Policies).Should(BeNil()) + Expect(newApp.Spec.Workflow).Should(BeNil()) + }) + + It("Test get oam app", func() { + defer GinkgoRecover() + res, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + + defer res.Body.Close() + var appResp apiv1.ApplicationResponse + err = json.NewDecoder(res.Body).Decode(&appResp) + Expect(err).ShouldNot(HaveOccurred()) + + Expect(len(appResp.Spec.Components)).Should(Equal(1)) + }) + + It("Test delete oam app", func() { + defer GinkgoRecover() + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), nil) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.DefaultClient.Do(req) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + }) +}) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go index dfe95b2bb..e0364e87b 100644 --- a/test/e2e-apiserver-test/suite_test.go +++ b/test/e2e-apiserver-test/suite_test.go @@ -18,81 +18,68 @@ package e2e_apiserver_test import ( "context" + "errors" + "net/http" "testing" "time" + "github.com/google/uuid" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" arest "github.com/oam-dev/kubevela/pkg/apiserver/rest" ) -var cfg *rest.Config var k8sClient client.Client -var testEnv *envtest.Environment -var testScheme = runtime.NewScheme() func TestE2eApiserverTest(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "E2eApiserverTest Suite") } +// Suite test in e2e-apiserver-test relies on the pre-setup kubernetes environment var _ = BeforeSuite(func() { - - By("bootstrapping test environment") - - testEnv = &envtest.Environment{ - ControlPlaneStartTimeout: time.Minute * 3, - ControlPlaneStopTimeout: time.Minute, - UseExistingCluster: pointer.BoolPtr(false), - } - - By("start kube test env") - var err error - cfg, err = testEnv.Start() - Expect(err).ShouldNot(HaveOccurred()) - Expect(cfg).ToNot(BeNil()) - - err = scheme.AddToScheme(testScheme) - Expect(err).NotTo(HaveOccurred()) - By("new kube client") - cfg.Timeout = time.Minute * 2 - k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + var err error + k8sClient, err = clients.GetKubeClient() Expect(err).Should(BeNil()) Expect(k8sClient).ToNot(BeNil()) By("new kube client success") - clients.SetKubeClient(k8sClient) ctx := context.Background() - server, err := arest.New(arest.Config{ + cfg := arest.Config{ BindAddr: "127.0.0.1:8000", Datastore: datastore.Config{ Type: "kubeapi", Database: "kubevela", }, - }) + } + cfg.LeaderConfig.ID = uuid.New().String() + cfg.LeaderConfig.LockName = "apiserver-lock" + cfg.LeaderConfig.Duration = time.Second * 10 + + server, err := arest.New(cfg) Expect(err).ShouldNot(HaveOccurred()) Expect(server).ShouldNot(BeNil()) go func() { err = server.Run(ctx) Expect(err).ShouldNot(HaveOccurred()) }() + By("wait for api server to start") + Eventually( + func() error { + res, err := http.Get("http://127.0.0.1:8000/api/v1/namespaces") + if err != nil { + return err + } + if res.StatusCode == http.StatusOK { + return nil + } + return errors.New("rest service not ready") + }, time.Second*5, time.Millisecond*200).Should(BeNil()) By("api server started") - time.Sleep(time.Second * 2) -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).ToNot(HaveOccurred()) }) diff --git a/test/e2e-apiserver-test/testdata/component-pod-view.yaml b/test/e2e-apiserver-test/testdata/component-pod-view.yaml new file mode 100644 index 000000000..290547d2d --- /dev/null +++ b/test/e2e-apiserver-test/testdata/component-pod-view.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-component-pod-view + namespace: vela-system +data: + template: | + import ( + "vela/ql" + "vela/op" + ) + + parameter: { + appName: string + appNs: string + name: string + cluster?: string + clusterNs?: string + } + + application: ql.#ListResourcesInApp & { + app: { + name: parameter.appName + namespace: parameter.appNs + components: [parameter.name] + filter: { + if parameter.cluster != _|_ { + cluster: parameter.cluster + } + if parameter.clusterNs != _|_ { + clusterNamespace: parameter.clusterNs + } + } + } + } + + app: application.list[0] + resources: app.components[0].resources + + podsMap: op.#Steps & { + for i, resource in resources { + "\(i)": ql.#CollectPods & { + value: resource.object + cluster: resource.cluster + } + } + } + + podsWithCluster: [ for i, pods in podsMap for podObj in pods.list { + cluster: pods.cluster + obj: podObj + }] + + podStatus: op.#Steps & { + for i, pod in podsWithCluster { + "\(i)": op.#Steps & { + name: pod.obj.metadata.name + containers: {for container in pod.obj.status.containerStatuses { + "\(container.name)": { + image: container.image + state: container.state + } + }} + events: ql.#SearchEvents & { + value: pod.obj + cluster: pod.cluster + } + metrics: ql.#Read & { + cluster: pod.cluster + value: { + apiVersion: "metrics.k8s.io/v1beta1" + kind: "PodMetrics" + metadata: { + name: pod.obj.metadata.name + namespace: pod.obj.metadata.namespace + } + } + } + } + } + } + + status: { + podList: [ for podInfo in podStatus { + name: podInfo.name + containers: [ for containerName, container in podInfo.containers { + containerName + }] + events: podInfo.events.list + }] + } diff --git a/test/e2e-apiserver-test/testdata/example-app.yaml b/test/e2e-apiserver-test/testdata/example-app.yaml new file mode 100644 index 000000000..4e77d8bf3 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/example-app.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: example-app + namespace: default +spec: + components: + - name: hello-world-server + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 + - name: data-worker + type: worker + properties: + image: busybox + cmd: + - sleep + - '1000000' + policies: + - name: example-multi-env-policy + type: env-binding + properties: + envs: + - name: test + placement: # selecting the namespace (in local cluster) to deploy to + namespaceSelector: + name: TEST_NAMESPACE + selector: + components: + - data-worker + + - name: staging + placement: # selecting the cluster to deploy to + clusterSelector: + name: cluster-worker + + - name: prod + placement: # selecting both namespace and cluster to deploy to + clusterSelector: + name: cluster-worker + namespaceSelector: + name: PROD_NAMESPACE + patch: # overlay patch on above components + components: + - name: hello-world-server + type: webservice + traits: + - type: scaler + properties: + replicas: 3 + + workflow: + steps: + # deploy to test env + - name: deploy-test + type: deploy2env + properties: + policy: example-multi-env-policy + env: test + + # deploy to staging env + - name: deploy-staging + type: deploy2env + properties: + policy: example-multi-env-policy + env: staging + + # deploy to prod env + - name: deploy-prod + type: deploy2env + properties: + policy: example-multi-env-policy + env: prod diff --git a/test/e2e-apiserver-test/testdata/read-view.yaml b/test/e2e-apiserver-test/testdata/read-view.yaml new file mode 100644 index 000000000..8d3199b76 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/read-view.yaml @@ -0,0 +1,53 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: read-view + namespace: vela-system +data: + template: | + import ( + "vela/op" + ) + + output: { + if parameter.apiVersion == _|_ && parameter.kind == _|_ { + op.#Read & { + value: { + apiVersion: "core.oam.dev/v1beta1" + kind: "Application" + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + if parameter.apiVersion != _|_ || parameter.kind != _|_ { + op.#Read & { + value: { + apiVersion: parameter.apiVersion + kind: parameter.kind + metadata: { + name: parameter.name + if parameter.namespace != _|_ { + namespace: parameter.namespace + } + } + } + } + } + } + + parameter: { + // +usage=Specify the apiVersion of the object, defaults to core.oam.dev/v1beta1 + apiVersion?: string + // +usage=Specify the kind of the object, defaults to Application + kind?: string + // +usage=Specify the name of the object + name: string + // +usage=Specify the namespace of the object + namespace?: string + } + diff --git a/test/e2e-apiserver-test/testdata/workflow.json b/test/e2e-apiserver-test/testdata/workflow.json new file mode 100644 index 000000000..0255aa1eb --- /dev/null +++ b/test/e2e-apiserver-test/testdata/workflow.json @@ -0,0 +1,15 @@ +{ + "appName": "appName", + "name": "workflowName", + "alias": "workflowAlias", + "description": "workflow description", + "enable": true, + "default": true, + "steps": [ + { + "name": "deploy-test", + "type": "deploy2env", + "properties": "" + } + ] +} \ No newline at end of file diff --git a/test/e2e-apiserver-test/utils.go b/test/e2e-apiserver-test/utils.go new file mode 100644 index 000000000..c3725b650 --- /dev/null +++ b/test/e2e-apiserver-test/utils.go @@ -0,0 +1,59 @@ +/* +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 e2e_apiserver + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" +) + +// CreateRequest wraps request +func CreateRequest(method string, path string, body interface{}) (*http.Response, error) { + if body == nil { + body = map[string]string{} + } + bs, err := json.Marshal(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest(method, "http://127.0.0.1:8000/api/v1"+path, bytes.NewBuffer(bs)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + return http.DefaultClient.Do(req) +} + +// DecodeResponseBody decode response and close response +func DecodeResponseBody(resp *http.Response, err error, dst interface{}) error { + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("response code is not 200: %d", resp.StatusCode) + } + if resp.Body == nil { + return fmt.Errorf("response body is nil") + } + err = json.NewDecoder(resp.Body).Decode(dst) + if err != nil { + return err + } + return resp.Body.Close() +} diff --git a/test/e2e-apiserver-test/velaql_test.go b/test/e2e-apiserver-test/velaql_test.go new file mode 100644 index 000000000..8bb475e48 --- /dev/null +++ b/test/e2e-apiserver-test/velaql_test.go @@ -0,0 +1,349 @@ +/* + 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 e2e_apiserver_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/pkg/errors" + batchv1beta1 "k8s.io/api/batch/v1beta1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + apiv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/oam/util" + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +type PodStatus struct { + Name string `json:"name"` + Containers []string `json:"containers"` + Events interface{} `json:"events"` +} +type Status struct { + PodList []PodStatus `json:"podList"` +} + +var _ = Describe("Test velaQL rest api", func() { + namespace := "test-velaql" + appName := "example-app" + component1Name := "ql-webservice" + component2Name := "ql-worker" + var app v1beta1.Application + var readView corev1.ConfigMap + + It("Test query application status via view", func() { + Expect(common.ReadYamlToObject("./testdata/read-view.yaml", &readView)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), &readView)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &app)).Should(BeNil()) + app.Spec.Components[0].Name = component1Name + app.Spec.Components[1].Name = component2Name + + req := apiv1.ApplicationRequest{ + Components: app.Spec.Components, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appName), + "application/json", + bytes.NewBuffer(bodyByte), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + if len(oldApp.Status.AppliedResources) != 2 { + return errors.Errorf("expect the applied resources number is %d, but get %d", 2, len(oldApp.Status.AppliedResources)) + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{name=%s,namespace=%s}.%s", "read-view", appName, namespace, "output.value.spec"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(200)) + + defer queryRes.Body.Close() + var appSpec v1beta1.ApplicationSpec + err = json.NewDecoder(queryRes.Body).Decode(&appSpec) + Expect(err).ShouldNot(HaveOccurred()) + + var existApp v1beta1.Application + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, &existApp)).Should(BeNil()) + + Expect(len(appSpec.Components)).Should(Equal(len(existApp.Spec.Components))) + }) + + It("Test query application status with wrong velaQL", func() { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{err=,name=%s,namespace=%s}.%s", "read-object", appName, namespace, "output.value.spec"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(400)) + }) + + It("Test query application component view", func() { + componentView := new(corev1.ConfigMap) + Expect(common.ReadYamlToObject("./testdata/component-pod-view.yaml", componentView)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), componentView)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + if len(oldApp.Status.AppliedResources) != 2 { + return errors.Errorf("expect the applied resources number is %d, but get %d", 2, len(oldApp.Status.AppliedResources)) + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component1Name, "status"), + ) + Expect(err).Should(BeNil()) + Expect(queryRes.StatusCode).Should(Equal(200)) + + defer queryRes.Body.Close() + status := new(Status) + err = json.NewDecoder(queryRes.Body).Decode(status) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(status.PodList)).Should(Equal(1)) + Expect(status.PodList[0].Containers[0]).Should(Equal(component1Name)) + + Eventually(func() error { + queryRes1, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), + ) + if err != nil { + return err + } + if queryRes1.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes1.StatusCode) + } + defer queryRes1.Body.Close() + status1 := new(Status) + err = json.NewDecoder(queryRes1.Body).Decode(status1) + if err != nil { + return err + } + if len(status1.PodList) != 1 { + return errors.New("pod number is zero") + } + if status1.PodList[0].Containers[0] != component2Name { + return errors.New("container name is not correct") + } + return nil + }, 10*time.Second, 300*time.Microsecond).Should(BeNil()) + }) + + It("Test collect pod from cronJob", func() { + cronJob := new(v1beta1.ComponentDefinition) + Expect(yaml.Unmarshal([]byte(cronJobComponentDefinition), cronJob)).Should(BeNil()) + Expect(k8sClient.Create(context.Background(), cronJob)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + oldApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appName, Namespace: namespace}, oldApp); err != nil { + return err + } + oldApp.Spec.Components[1].Type = "cronjob" + oldApp.Spec.Components[1].Properties = util.Object2RawExtension(map[string]interface{}{ + "image": "busybox", + "cmd": []string{"sleep", "1"}, + }) + if err := k8sClient.Update(context.Background(), oldApp); err != nil { + return err + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(oldApp), newApp); err != nil { + return err + } + appliedCronJob := false + for _, resource := range newApp.Status.AppliedResources { + if resource.ObjectReference.Kind == "CronJob" { + appliedCronJob = true + break + } + } + if !appliedCronJob { + return errors.New("fail to apply cronjob") + } + return nil + }, 3*time.Second, 300*time.Microsecond).Should(BeNil()) + + newWorkload := new(batchv1beta1.CronJob) + Eventually(func() error { + return k8sClient.Get(context.Background(), client.ObjectKey{Name: component2Name, Namespace: namespace}, newWorkload) + }, 10*time.Second, 300*time.Microsecond).Should(BeNil()) + + Eventually(func() error { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "test-component-pod-view", appName, namespace, component2Name, "status"), + ) + if err != nil { + return err + } + if queryRes.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes.StatusCode) + } + defer queryRes.Body.Close() + status := new(Status) + err = json.NewDecoder(queryRes.Body).Decode(status) + if err != nil { + return err + } + if len(status.PodList) == 0 { + return errors.New("pod list is 0") + } + return nil + }, 2*time.Minute, 3*time.Microsecond).Should(BeNil()) + }) + + It("Test collect pod from helmRelease", func() { + appWithHelm := new(v1beta1.Application) + Expect(yaml.Unmarshal([]byte(podInfoApp), appWithHelm)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: appWithHelm.Spec.Components, + } + bodyByte, err := json.Marshal(req) + Expect(err).Should(BeNil()) + res, err := http.Post( + fmt.Sprintf("http://127.0.0.1:8000/v1/namespaces/%s/applications/%s", namespace, appWithHelm.Name), + "application/json", + bytes.NewBuffer(bodyByte), + ) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(res.StatusCode).Should(Equal(200)) + + newApp := new(v1beta1.Application) + Eventually(func() error { + if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: appWithHelm.Name, Namespace: namespace}, newApp); err != nil { + return err + } + if newApp.Status.Phase != common2.ApplicationRunning { + return errors.New("application is not ready") + } + return nil + }, 2*time.Minute, 1*time.Second).Should(BeNil()) + + Eventually(func() error { + queryRes, err := http.Get( + fmt.Sprintf("http://127.0.0.1:8000/api/v1/query?velaql=%s{appName=%s,appNs=%s,name=%s}.%s", "component-pod-view", appWithHelm.Name, namespace, "podinfo", "status"), + ) + if err != nil { + return err + } + if queryRes.StatusCode != 200 { + return errors.Errorf("status code is %d", queryRes.StatusCode) + } + defer queryRes.Body.Close() + + type queryResult struct { + PodList []interface{} `json:"podList,omitempty"` + Error interface{} `json:"error,omitempty"` + } + status := new(queryResult) + err = json.NewDecoder(queryRes.Body).Decode(status) + if err != nil { + return err + } + if status.Error != nil { + return errors.Errorf("error %v", status.Error) + } + if len(status.PodList) == 0 { + return errors.New("pod list is 0") + } + return nil + }, 2*time.Minute, 300*time.Microsecond).Should(BeNil()) + }) +}) + +var cronJobComponentDefinition = ` +apiVersion: core.oam.dev/v1beta1 +kind: ComponentDefinition +metadata: + annotations: {} + name: cronjob + namespace: vela-system +spec: + schematic: + cue: + template: | + output: { + apiVersion: "batch/v1beta1" + kind: "CronJob" + metadata: name: context.name + spec: { + schedule: "*/1 * * * *" + jobTemplate: spec: template: spec: { + containers: [{ + name: context.name + image: parameter.image + imagePullPolicy: "IfNotPresent" + command: parameter.cmd + }] + restartPolicy: "OnFailure" + } + } + } + parameter: { + image: string + cmd: [...string] + } + workload: + type: autodetects.core.oam.dev +` + +var podInfoApp = ` +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: podinfo +spec: + components: + - name: podinfo + type: helm + properties: + chart: podinfo + url: https://stefanprodan.github.io/podinfo + repoType: helm + version: 5.1.2 +` diff --git a/test/e2e-test/helm_app_test.go b/test/e2e-test/helm_app_test.go index d980e7df8..a140d56f0 100644 --- a/test/e2e-test/helm_app_test.go +++ b/test/e2e-test/helm_app_test.go @@ -378,7 +378,7 @@ var _ = Describe("Test application containing helm module", func() { if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { return err } - if cm.Data["openapi-v3-json-schema"] == "" { + if cm.Data[types.OpenapiV3JSONSchema] == "" { return errors.New("json schema is not found in the ConfigMap") } return nil diff --git a/test/e2e-test/kube_app_test.go b/test/e2e-test/kube_app_test.go index 545b3cf7a..f79c07011 100644 --- a/test/e2e-test/kube_app_test.go +++ b/test/e2e-test/kube_app_test.go @@ -32,6 +32,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/oam/util" . "github.com/onsi/ginkgo" @@ -347,7 +348,7 @@ spec: if err := k8sClient.Get(ctx, client.ObjectKey{Name: cmName, Namespace: namespace}, cm); err != nil { return err } - if cm.Data["openapi-v3-json-schema"] == "" { + if cm.Data[types.OpenapiV3JSONSchema] == "" { return errors.New("json schema is not found in the ConfigMap") } return nil diff --git a/test/e2e-test/suite_test.go b/test/e2e-test/suite_test.go index 0b893d533..43ac3b3d8 100644 --- a/test/e2e-test/suite_test.go +++ b/test/e2e-test/suite_test.go @@ -302,7 +302,7 @@ func RequestReconcileNow(ctx context.Context, o client.Object) { // randomNamespaceName generates a random name based on the basic name. // Running each ginkgo case in a new namespace with a random name can avoid -// waiting a long time to GC namesapce. +// waiting a long time to GC namespace. func randomNamespaceName(basic string) string { return fmt.Sprintf("%s-%s", basic, strconv.FormatInt(rand.Int63(), 16)) } diff --git a/vela-templates/addons/fluxcd/readme.md b/vela-templates/addons/fluxcd/readme.md new file mode 100644 index 000000000..d0ea0da55 --- /dev/null +++ b/vela-templates/addons/fluxcd/readme.md @@ -0,0 +1,18 @@ +# fluxcd + +This addon is built based [FluxCD](https://fluxcd.io/) + +## install + +```shell +vela addon enable fluxcd +``` + +## X-Definitions + +Enable fluxcd addon to use these X-definitions + +- [helm](https://kubevela.io/docs/end-user/components/helm) helps to deploy a helm chart from everywhere: +git repo / helm repo / S3 compatible bucket. + +- [kustomize](https://kubevela.io/docs/end-user/components/kustomize) helps to deploy a kustomize style artifact. diff --git a/vela-templates/addons/istio/readme.md b/vela-templates/addons/istio/readme.md new file mode 100644 index 000000000..de9bdab97 --- /dev/null +++ b/vela-templates/addons/istio/readme.md @@ -0,0 +1,3 @@ +# istio + +This addon provides istio support for vela rollout. \ No newline at end of file diff --git a/vela-templates/addons/kruise/readme.md b/vela-templates/addons/kruise/readme.md new file mode 100644 index 000000000..775584c2c --- /dev/null +++ b/vela-templates/addons/kruise/readme.md @@ -0,0 +1,3 @@ +# kruise + +This addon provides [open-kruise](https://github.com/openkruise/kruise) workload. \ No newline at end of file diff --git a/vela-templates/addons/observability/readme.md b/vela-templates/addons/observability/readme.md new file mode 100644 index 000000000..de09d6dd8 --- /dev/null +++ b/vela-templates/addons/observability/readme.md @@ -0,0 +1,3 @@ +# observability + +This addon expose system and application level metrics for KubeVela. \ No newline at end of file diff --git a/vela-templates/addons/ocm-cluster-manager/readme.md b/vela-templates/addons/ocm-cluster-manager/readme.md new file mode 100644 index 000000000..f9b214285 --- /dev/null +++ b/vela-templates/addons/ocm-cluster-manager/readme.md @@ -0,0 +1,3 @@ +# ocm-cluster-manager + +This addon aims to support multi-cluster application deployment. \ No newline at end of file diff --git a/vela-templates/addons/terraform-provider-alibaba/readme.md b/vela-templates/addons/terraform-provider-alibaba/readme.md new file mode 100644 index 000000000..b092c1804 --- /dev/null +++ b/vela-templates/addons/terraform-provider-alibaba/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-alibaba + +This addon contains terraform provider for Alibaba Cloud. \ No newline at end of file diff --git a/vela-templates/addons/terraform-provider-aws/readme.md b/vela-templates/addons/terraform-provider-aws/readme.md new file mode 100644 index 000000000..82ddc7e7c --- /dev/null +++ b/vela-templates/addons/terraform-provider-aws/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-aws + +This addon contains terraform provider for AWS \ No newline at end of file diff --git a/vela-templates/addons/terraform-provider-azure/readme.md b/vela-templates/addons/terraform-provider-azure/readme.md new file mode 100644 index 000000000..dfc36ff1f --- /dev/null +++ b/vela-templates/addons/terraform-provider-azure/readme.md @@ -0,0 +1,3 @@ +# terraform/provider-azure + +This addon contains terraform provider for Azure. \ No newline at end of file diff --git a/vela-templates/addons/terraform/readme.md b/vela-templates/addons/terraform/readme.md new file mode 100644 index 000000000..55016b4ea --- /dev/null +++ b/vela-templates/addons/terraform/readme.md @@ -0,0 +1,4 @@ +# Terraform + +This addon contains terraform operation kit, which allows you to arrange, +generate and use cloud service from different cloud vendor. \ No newline at end of file diff --git a/vela-templates/definitions/internal/deploy2runtime.cue b/vela-templates/definitions/internal/deploy2runtime.cue index b7862e878..504d11580 100644 --- a/vela-templates/definitions/internal/deploy2runtime.cue +++ b/vela-templates/definitions/internal/deploy2runtime.cue @@ -26,10 +26,10 @@ template: { "\(cluster_)-\(name)": op.#ApplyComponent & { value: c cluster: cluster_ - } @step(3) + } } } - } + } @step(3) } parameter: { diff --git a/vela-templates/gen_addons.go b/vela-templates/gen_addons.go index 3643b2033..4fd68ea32 100644 --- a/vela-templates/gen_addons.go +++ b/vela-templates/gen_addons.go @@ -29,6 +29,8 @@ import ( "strings" "text/template" + addonutil "github.com/oam-dev/kubevela/pkg/utils/addon" + "github.com/Masterminds/sprig" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -39,20 +41,22 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" "github.com/oam-dev/kubevela/pkg/oam/util" - "github.com/oam-dev/kubevela/references/cli" ) const ( + // DetailFileName is readme for each addon + DetailFileName = "readme.md" + // TemplateName represents the Application template file of addons TemplateName = "template.yaml" // ApplicationFileDir is where we store generated application & component definition ApplicationFileDir = "auto-gen" - // ComponentDefDir is where we store correspond componentDefinition for addon - ComponentDefDir = "definitions" + // DefinitionDir is where we store correspond X-Definition for addon + DefinitionDir = "definitions" - // ResourceDir is where we store correspond componentDefinition for addon + // ResourceDir is where we store correspond resources for addon ResourceDir = "resource" // DescAnnotation records the description of addon @@ -66,6 +70,12 @@ const ( // NameAnnotation marked the addon's name if exist, or application's name NameAnnotation = "addons.oam.dev/name" + + // ApplicationKey is the key to store application in ConfigMap + ApplicationKey = "application" + + // DetailKey is the key to store detail information in ConfigMap + DetailKey = "detail" ) // DefaultEnableAddons is default enabled addons @@ -77,11 +87,11 @@ type velaFile struct { Content string } -// AddonInfo records addon's metadata -type AddonInfo struct { +// AddonGenerateInfo records addon's metadata used in addon generation +type AddonGenerateInfo struct { ResourceFiles []velaFile DefinitionFiles []velaFile - HasDefs bool + DetailFile velaFile Name string StoreName string Description string @@ -134,13 +144,14 @@ func newWalkFn(files *[]velaFile) filepath.WalkFunc { } } -func getAddonInfo(addon string, addonsPath string) (*AddonInfo, error) { +func getAddonInfo(addon string, addonsPath string) (*AddonGenerateInfo, error) { addonRoot := filepath.Clean(addonsPath + "/" + addon) resourceRoot := filepath.Clean(addonRoot + "/" + ResourceDir) - defRoot := filepath.Clean(addonRoot + "/" + ComponentDefDir) + defRoot := filepath.Clean(addonRoot + "/" + DefinitionDir) + detailFile := filepath.Clean(addonRoot + "/" + DetailFileName) resourcesFiles := make([]velaFile, 0) defFiles := make([]velaFile, 0) - addInfo := &AddonInfo{ + addInfo := &AddonGenerateInfo{ TemplatePath: filepath.Join(addonRoot, TemplateName), } // raw resources directory @@ -155,9 +166,20 @@ func getAddonInfo(addon string, addonsPath string) (*AddonInfo, error) { if err := filepath.Walk(defRoot, newWalkFn(&defFiles)); err != nil { return nil, err } - addInfo.HasDefs = true addInfo.DefinitionFiles = defFiles } + + if pathExist(detailFile) { + content, err := os.ReadFile(detailFile) + if err != nil { + return nil, errors.Wrapf(err, "read %s detail file fail", addon) + } + addInfo.DetailFile = velaFile{ + RelativePath: detailFile, + Name: filepath.Base(detailFile), + Content: string(content), + } + } return addInfo, nil } @@ -180,7 +202,7 @@ func WriteToFile(filename string, data string) error { return file.Sync() } -func generateApplication(addon *AddonInfo) (*v1beta1.Application, error) { +func generateApplication(addon *AddonGenerateInfo) (*v1beta1.Application, error) { templatePath := strings.Split(addon.TemplatePath, "/") templateName := templatePath[len(templatePath)-1] t, err := template.New(templateName).Funcs(sprig.TxtFuncMap()).ParseFiles(addon.TemplatePath) @@ -202,12 +224,12 @@ func generateApplication(addon *AddonInfo) (*v1beta1.Application, error) { return app, err } -func setConfigMapLabels(addonInfo *AddonInfo) map[string]string { +func setConfigMapLabels(addonInfo *AddonGenerateInfo) map[string]string { return map[string]string{ MarkLabel: addonInfo.StoreName, } } -func setConfigMapAnnotations(addonInfo *AddonInfo) map[string]string { +func setConfigMapAnnotations(addonInfo *AddonGenerateInfo) map[string]string { return map[string]string{ NameAnnotation: addonInfo.Name, DescAnnotation: addonInfo.Description, @@ -229,7 +251,7 @@ func removeUselessInplace(s *string) { } // storeConfigMap store configMap in helm chart -func storeConfigMap(addonInfo *AddonInfo, application *v1beta1.Application, storePath string) error { +func storeConfigMap(addonInfo *AddonGenerateInfo, application *v1beta1.Application, storePath string) error { configMap := &corev1.ConfigMap{ TypeMeta: v1.TypeMeta{ APIVersion: "v1", @@ -247,7 +269,8 @@ func storeConfigMap(addonInfo *AddonInfo, application *v1beta1.Application, stor if err != nil { return err } - data["application"] = string(initContent) + data[ApplicationKey] = string(initContent) + data[DetailKey] = addonInfo.DetailFile.Content configMap.Data = data content, err := yaml.Marshal(configMap) if err != nil { @@ -329,7 +352,7 @@ func main() { } } -func setAddonName(addInfo *AddonInfo, app *v1beta1.Application) { +func setAddonName(addInfo *AddonGenerateInfo, app *v1beta1.Application) { var name string if val, ok := app.Annotations[NameAnnotation]; ok { name = val @@ -337,5 +360,5 @@ func setAddonName(addInfo *AddonInfo, app *v1beta1.Application) { name = app.Name } addInfo.Name = name - addInfo.StoreName = cli.TransAddonName(name) + addInfo.StoreName = addonutil.TransAddonName(name) }