Feat: initialize the Apiserver framework (#2417)

* Feat: add kubeapi and mongodb datastore implementation

* Style: change kubeapi import code style

* Style: change mongodb package import code style

* Style: add some comment

* Style: change databasePrefix to tableNamePrefix

* Chore: install mongodb in unit-test job

* Chore: install mongodb in compatibility-test job

* Feat: add apiserver e2e test case

* Docs: change developer guide doc

* Feat: use common.Scheme

Co-authored-by: barnettZQG <yiyun.pro>
This commit is contained in:
barnettZQG
2021-10-12 11:53:24 +08:00
committed by GitHub
co-authored by lnx01
parent 2e20f4862f
commit 0d036e7449
36 changed files with 4478 additions and 132 deletions
+93
View File
@@ -0,0 +1,93 @@
name: apiServer-test
on:
push:
branches:
- master
- release-*
workflow_dispatch: {}
pull_request:
branches:
- master
- release-*
env:
# Common versions
GO_VERSION: '1.16'
GOLANGCI_VERSION: 'v1.38'
KIND_VERSION: 'v0.7.0'
jobs:
detect-noop:
runs-on: ubuntu-20.04
outputs:
noop: ${{ steps.noop.outputs.should_skip }}
steps:
- name: Detect No-op Changes
id: noop
uses: fkirc/skip-duplicate-actions@v3.3.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
paths_ignore: '["**.md", "**.mdx", "**.png", "**.jpg"]'
do_not_skip: '["workflow_dispatch", "schedule", "push"]'
concurrent_skipping: false
apiserver-unit-tests:
runs-on: ubuntu-20.04
needs: detect-noop
if: needs.detect-noop.outputs.noop != 'true'
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
with:
submodules: true
- name: Cache Go Dependencies
uses: actions/cache@v2
with:
path: .work/pkg
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
restore-keys: ${{ runner.os }}-pkg-
- name: Install ginkgo
run: |
sudo apt-get install -y golang-ginkgo-dev
- name: Setup Kind Cluster
uses: engineerd/setup-kind@v0.5.0
with:
version: ${{ env.KIND_VERSION }}
- name: install Kubebuilder
uses: RyanSiu1995/kubebuilder-action@v1.2
with:
version: 3.1.0
kubebuilderOnly: false
kubernetesVersion: v1.21.2
- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.6.0
with:
mongodb-version: 4.4
- name: Run apiserver unit test
run: make unit-test-apiserver
- name: Upload coverage report
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.txt
flags: apiserver-unittests
name: codecov-umbrella
- name: Run apiserver e2e test
run: make e2e-apiserver-test
+2 -2
View File
@@ -72,7 +72,7 @@ jobs:
version: 3.1.0
kubebuilderOnly: false
kubernetesVersion: v1.21.2
- name: Run Make test
run: make test
@@ -81,5 +81,5 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./coverage.txt
flags: unittests
flags: core-unittests
name: codecov-umbrella
+11 -4
View File
@@ -43,11 +43,14 @@ VELA_RUNTIME_ROLLOUT_IMAGE ?= vela-runtime-rollout:latest
all: build
# Run tests
test: vet lint staticcheck
go test -coverprofile=coverage.txt ./pkg/... ./cmd/...
go test ./references/appfile/... ./references/cli/... ./references/common/... ./references/plugins/...
test: vet lint staticcheck unit-test-core
@$(OK) unit-tests pass
unit-test-core:
go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... ./references/... | grep -v apiserver)
unit-test-apiserver:
go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep apiserver)
# Build vela cli binary
build: fmt vet lint staticcheck vela-cli kubectl-vela
@$(OK) build succeed
@@ -163,6 +166,10 @@ e2e-api-test:
ginkgo -v -skipPackage capability,setup,application -r e2e
ginkgo -v -r e2e/application
e2e-apiserver-test:
ginkgo -v ./test/e2e-apiserver-test
@$(OK) tests pass
e2e-test:
# Run e2e test
ginkgo -v --skip="rollout related e2e-test." ./test/e2e-test
@@ -178,7 +185,7 @@ e2e-multicluster-test:
compatibility-test: vet lint staticcheck generate-compatibility-testdata
# Run compatibility test with old crd
COMPATIBILITY_TEST=TRUE go test -race ./pkg/...
COMPATIBILITY_TEST=TRUE go test -race $(shell go list ./pkg/... | grep -v apiserver)
@$(OK) compatibility-test pass
generate-compatibility-testdata:
+4 -3
View File
@@ -30,7 +30,7 @@ import (
)
func main() {
s := &server{}
s := &Server{}
flag.StringVar(&s.restCfg.BindAddr, "bind-addr", "0.0.0.0:8000", "The bind address used to serve the http APIs.")
flag.StringVar(&s.restCfg.MetricPath, "metrics-path", "/metrics", "The path to expose the metrics.")
flag.StringVar(&s.restCfg.Datastore.Type, "datastore-type", "kubeapi", "Metadata storage driver type, support kubeapi and mongodb")
@@ -57,11 +57,12 @@ func main() {
log.Logger.Infof("See you next time!")
}
type server struct {
// Server apiserver
type Server struct {
restCfg rest.Config
}
func (s *server) run() error {
func (s *Server) run() error {
log.Logger.Infof("KubeVela information: version: %v, gitRevision: %v", version.VelaVersion, version.GitRevision)
ctx := context.Background()
+13 -1
View File
@@ -115,6 +115,12 @@ You can try use your local built binaries follow [the documentation](https://kub
make test
```
To execute the unit test of the API module, the mongodb service needs to exist locally.
```shell script
make unit-test-apiserver
```
### E2E test
**Before e2e test start, make sure you have vela-core running.**
@@ -125,10 +131,16 @@ make core-run
Start to test.
```
```shell script
make e2e-test
```
To execute the e2e test of the API module, the mongodb service needs to exist locally.
```shell script
make e2e-apiserver-test
```
## Contribute Docs
Please read [the documentation](https://github.com/oam-dev/kubevela/tree/master/docs/README.md) before contributing to the docs.
File diff suppressed because it is too large Load Diff
+3
View File
@@ -19,6 +19,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/fsnotify/fsnotify v1.5.1 // indirect
github.com/gertd/go-pluralize v0.1.7
github.com/getkin/kin-openapi v0.34.0
github.com/go-logr/logr v0.4.0
@@ -51,6 +52,8 @@ require (
go.mongodb.org/mongo-driver v1.5.1
go.uber.org/zap v1.18.1
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
golang.org/x/tools v0.1.6 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible
helm.sh/helm/v3 v3.6.1
+10 -4
View File
@@ -462,8 +462,9 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
@@ -1592,6 +1593,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
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=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI=
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE=
@@ -1842,8 +1844,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1978,8 +1981,10 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
@@ -2126,8 +2131,9 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4=
golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+47
View File
@@ -0,0 +1,47 @@
/*
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 clients
import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
var kubeClient client.Client
// SetKubeClient for test
func SetKubeClient(c client.Client) {
kubeClient = c
}
// GetKubeClient create and return kube runtime client
func GetKubeClient() (client.Client, error) {
if kubeClient != nil {
return kubeClient, nil
}
conf, err := config.GetConfig()
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
}
+70 -20
View File
@@ -18,8 +18,41 @@ package datastore
import (
"context"
"fmt"
"reflect"
)
var (
// ErrPrimaryEmpty Error that primary key is empty.
ErrPrimaryEmpty = NewDBError(fmt.Errorf("entity primary is empty"))
// ErrTableNameEmpty Error that table name is empty.
ErrTableNameEmpty = NewDBError(fmt.Errorf("entity table name is empty"))
// ErrNilEntity Error that entity is nil
ErrNilEntity = NewDBError(fmt.Errorf("entity is nil"))
// ErrRecordExist Error that entity primary key is exist
ErrRecordExist = NewDBError(fmt.Errorf("data record is exist"))
// ErrRecordNotExist Error that entity primary key is not exist
ErrRecordNotExist = NewDBError(fmt.Errorf("data record is not exist"))
)
// DBError datastore error
type DBError struct {
err error
}
func (d *DBError) Error() string {
return d.err.Error()
}
// NewDBError new datastore error
func NewDBError(err error) error {
return &DBError{err: err}
}
// Config datastore config
type Config struct {
Type string
@@ -27,31 +60,48 @@ type Config struct {
Database string
}
// Entity database data model
type Entity interface {
PrimaryKey() string
TableName() string
}
// NewEntity Create a new object based on the input type
func NewEntity(in Entity) (Entity, error) {
if in == nil {
return nil, ErrNilEntity
}
t := reflect.TypeOf(in)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
new := reflect.New(t)
return new.Interface().(Entity), nil
}
// ListOptions list api options
type ListOptions struct {
Page int
PageSize int
}
// DataStore datastore interface
type DataStore interface {
Add(ctx context.Context, kind string, entity interface{}) error
// add entity to database, Name() and TableName() can't return zero value.
Add(ctx context.Context, entity Entity) error
Put(ctx context.Context, kind, name string, entity interface{}) error
// Update entity to database, Name() and TableName() can't return zero value.
Put(ctx context.Context, entity Entity) error
Delete(ctx context.Context, kind, name string) error
// Delete entity from database, Name() and TableName() can't return zero value.
Delete(ctx context.Context, entity Entity) error
Get(ctx context.Context, kind, name string, decodeTo interface{}) error
// Get entity from database, Name() and TableName() can't return zero value.
Get(ctx context.Context, entity Entity) error
// Find executes a find command and returns an iterator over the matching items.
Find(ctx context.Context, kind string) (Iterator, error)
// TableName() can't return zero value.
List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error)
FindOne(ctx context.Context, kind, name string) (Iterator, error)
IsExist(ctx context.Context, kind, name string) (bool, error)
}
// Iterator dataset query
type Iterator interface {
// Next gets the next item for this cursor.
Next(ctx context.Context) bool
// Decode will unmarshal the current item into given entity.
Decode(entity interface{}) error
Close(ctx context.Context) error
// IsExist Name() and TableName() can't return zero value.
IsExist(ctx context.Context, entity Entity) (bool, error)
}
@@ -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 datastore
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestDatastore(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Datastore Suite")
}
+60
View File
@@ -0,0 +1,60 @@
/*
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 datastore
import (
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
)
var _ = Describe("Test new entity function", func() {
It("Test new application entity", func() {
var app model.Application
new, err := NewEntity(&app)
Expect(err).To(BeNil())
json.Unmarshal([]byte(`{"name":"demo"}`), new)
Expect(err).To(BeNil())
diff := cmp.Diff(new.PrimaryKey(), "demo")
Expect(diff).Should(BeEmpty())
})
It("Test new multiple application entity", func() {
var app model.Application
var list []Entity
var n = 3
for n > 0 {
new, err := NewEntity(&app)
Expect(err).To(BeNil())
json.Unmarshal([]byte(fmt.Sprintf(`{"name":"demo %d"}`, n)), new)
Expect(err).To(BeNil())
diff := cmp.Diff(new.PrimaryKey(), fmt.Sprintf("demo %d", n))
Expect(diff).Should(BeEmpty())
list = append(list, new)
n--
}
diff := cmp.Diff(list[0].PrimaryKey(), "demo 3")
Expect(diff).Should(BeEmpty())
})
})
+191 -17
View File
@@ -18,50 +18,224 @@ package kubeapi
import (
"context"
"encoding/json"
"fmt"
"strings"
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/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/pkg/apiserver/clients"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
)
type kubeapi struct {
// kubeclient client.Client
kubeclient client.Client
namespace string
}
// New new kubeapi datastore instance
// Data is stored using ConfigMap.
func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) {
return &kubeapi{}, nil
kubeClient, err := clients.GetKubeClient()
if err != nil {
return nil, err
}
if cfg.Database == "" {
cfg.Database = "kubevela_store"
}
var namespace corev1.Namespace
if err := kubeClient.Get(ctx, types.NamespacedName{Name: cfg.Database}, &namespace); apierrors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
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 &kubeapi{
kubeclient: kubeClient,
namespace: cfg.Database,
}, nil
}
func generateName(entity datastore.Entity) string {
name := fmt.Sprintf("veladatabase-%s-%s", entity.TableName(), entity.PrimaryKey())
return strings.ReplaceAll(name, "_", "-")
}
func (m *kubeapi) generateConfigMap(entity datastore.Entity) *corev1.ConfigMap {
data, _ := json.Marshal(entity)
var configMap = corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: generateName(entity),
Namespace: m.namespace,
Labels: map[string]string{
"table": entity.TableName(),
"primaryKey": entity.PrimaryKey(),
},
},
BinaryData: map[string][]byte{
"data": data,
},
}
return &configMap
}
// Add add data model
func (m *kubeapi) Add(ctx context.Context, kind string, entity interface{}) error {
func (m *kubeapi) Add(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
configMap := m.generateConfigMap(entity)
if err := m.kubeclient.Create(ctx, configMap); err != nil {
if apierrors.IsAlreadyExists(err) {
return datastore.ErrRecordExist
}
return datastore.NewDBError(err)
}
return nil
}
// Get get data model
func (m *kubeapi) Get(ctx context.Context, kind, name string, decodeTo interface{}) error {
func (m *kubeapi) Get(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
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) {
return datastore.ErrRecordNotExist
}
return datastore.NewDBError(err)
}
if err := json.Unmarshal(configMap.BinaryData["data"], entity); err != nil {
return datastore.NewDBError(err)
}
return nil
}
// Put update data model
func (m *kubeapi) Put(ctx context.Context, kind, name string, entity interface{}) error {
func (m *kubeapi) Put(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
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) {
return datastore.ErrRecordNotExist
}
return datastore.NewDBError(err)
}
data, err := json.Marshal(entity)
if err != nil {
return datastore.NewDBError(err)
}
configMap.BinaryData["data"] = data
if err := m.kubeclient.Update(ctx, &configMap); err != nil {
return datastore.NewDBError(err)
}
return nil
}
// Find find data model
func (m *kubeapi) Find(ctx context.Context, kind string) (datastore.Iterator, error) {
return nil, nil
}
// FindOne find one data model
func (m *kubeapi) FindOne(ctx context.Context, kind, name string) (datastore.Iterator, error) {
return nil, nil
}
// IsExist determine whether data exists.
func (m *kubeapi) IsExist(ctx context.Context, kind, name string) (bool, error) {
func (m *kubeapi) IsExist(ctx context.Context, entity datastore.Entity) (bool, error) {
if entity.PrimaryKey() == "" {
return false, datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return false, datastore.ErrTableNameEmpty
}
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) {
return false, nil
}
return false, datastore.NewDBError(err)
}
return true, nil
}
// Delete delete data
func (m *kubeapi) Delete(ctx context.Context, kind, name string) error {
func (m *kubeapi) Delete(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
if err := m.kubeclient.Delete(ctx, m.generateConfigMap(entity)); err != nil {
if apierrors.IsNotFound(err) {
return datastore.ErrRecordNotExist
}
return datastore.NewDBError(err)
}
return nil
}
// 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() == "" {
return nil, datastore.ErrTableNameEmpty
}
selector, err := labels.Parse(fmt.Sprintf("table=%s", entity.TableName()))
if err != nil {
return nil, datastore.NewDBError(err)
}
options := &client.ListOptions{
LabelSelector: selector,
}
var skip, limit int64
if op != nil && op.PageSize > 0 && op.Page > 0 {
skip = int64(op.PageSize * (op.Page - 1))
limit = int64(op.PageSize * op.Page)
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 {
if apierrors.IsNotFound(err) {
return nil, nil
}
return nil, datastore.NewDBError(err)
}
items := configMaps.Items
if op != nil && op.PageSize > 0 && op.Page > 0 {
if len(configMaps.Items) > int(limit) {
items = configMaps.Items[skip:limit]
} else {
items = configMaps.Items[skip:]
}
}
var list []datastore.Entity
for _, item := range items {
ent, err := datastore.NewEntity(entity)
if err != nil {
return nil, datastore.NewDBError(err)
}
if err := json.Unmarshal(item.BinaryData["data"], ent); err != nil {
return nil, datastore.NewDBError(err)
}
list = append(list, ent)
}
return list, nil
}
@@ -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 kubeapi
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestKubeapi(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Kubeapi Suite")
}
@@ -0,0 +1,167 @@
/*
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 kubeapi
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
. "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"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var testScheme = runtime.NewScheme()
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),
}
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})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
By("new kube client success")
close(done)
}, 240)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
var _ = Describe("Test kubeapi datastore driver", func() {
clients.SetKubeClient(k8sClient)
kubeStore, err := New(context.TODO(), datastore.Config{Database: "test"})
Expect(err).Should(BeNil())
Expect(kubeStore).ToNot(BeNil())
It("Test add funtion", func() {
err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"})
Expect(err).ToNot(HaveOccurred())
})
It("Test get funtion", func() {
app := &model.Application{Name: "kubevela-app"}
err := kubeStore.Get(context.TODO(), app)
Expect(err).Should(BeNil())
diff := cmp.Diff(app.Description, "default")
Expect(diff).Should(BeEmpty())
})
It("Test put funtion", func() {
err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"})
Expect(err).ToNot(HaveOccurred())
})
It("Test list funtion", func() {
err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"})
Expect(err).ShouldNot(HaveOccurred())
err = kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"})
Expect(err).ShouldNot(HaveOccurred())
var app model.Application
list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1})
Expect(err).ShouldNot(HaveOccurred())
fmt.Printf("%+v", list[0])
diff := cmp.Diff(len(list), 3)
Expect(diff).Should(BeEmpty())
list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: 2, PageSize: 2})
Expect(err).ShouldNot(HaveOccurred())
diff = cmp.Diff(len(list), 1)
Expect(diff).Should(BeEmpty())
list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: 1, PageSize: 2})
Expect(err).ShouldNot(HaveOccurred())
diff = cmp.Diff(len(list), 2)
Expect(diff).Should(BeEmpty())
list, err = kubeStore.List(context.TODO(), &app, nil)
Expect(err).ShouldNot(HaveOccurred())
diff = cmp.Diff(len(list), 3)
Expect(diff).Should(BeEmpty())
})
It("Test isExist funtion", func() {
var app model.Application
app.Name = "kubevela-app-3"
exist, err := kubeStore.IsExist(context.TODO(), &app)
Expect(err).ShouldNot(HaveOccurred())
diff := cmp.Diff(exist, true)
Expect(diff).Should(BeEmpty())
app.Name = "kubevela-app-4"
notexist, err := kubeStore.IsExist(context.TODO(), &app)
Expect(err).ShouldNot(HaveOccurred())
diff = cmp.Diff(notexist, false)
Expect(diff).Should(BeEmpty())
})
It("Test delete funtion", func() {
var app model.Application
app.Name = "kubevela-app"
err := kubeStore.Delete(context.TODO(), &app)
Expect(err).ShouldNot(HaveOccurred())
app.Name = "kubevela-app-2"
err = kubeStore.Delete(context.TODO(), &app)
Expect(err).ShouldNot(HaveOccurred())
app.Name = "kubevela-app-3"
err = kubeStore.Delete(context.TODO(), &app)
Expect(err).ShouldNot(HaveOccurred())
app.Name = "kubevela-app-3"
err = kubeStore.Delete(context.TODO(), &app)
equal := cmp.Equal(err, datastore.ErrRecordNotExist, cmpopts.EquateErrors())
Expect(equal).Should(BeTrue())
})
})
+109 -42
View File
@@ -27,6 +27,7 @@ import (
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/log"
)
type mongodb struct {
@@ -36,7 +37,7 @@ type mongodb struct {
// New new mongodb datastore instance
func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) {
if strings.HasPrefix(cfg.URL, "mongodb://") {
if !strings.HasPrefix(cfg.URL, "mongodb://") {
cfg.URL = fmt.Sprintf("mongodb://%s", cfg.URL)
}
clientOpts := options.Client().ApplyURI(cfg.URL)
@@ -53,70 +54,93 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error)
}
// Add add data model
func (m *mongodb) Add(ctx context.Context, kind string, entity interface{}) error {
collection := m.client.Database(m.database).Collection(kind)
func (m *mongodb) Add(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
if err := m.Get(ctx, entity); err == nil {
return datastore.ErrRecordExist
}
collection := m.client.Database(m.database).Collection(entity.TableName())
_, err := collection.InsertOne(ctx, entity)
if err != nil {
return err
return datastore.NewDBError(err)
}
return nil
}
// Get get data model
func (m *mongodb) Get(ctx context.Context, kind, name string, decodeTo interface{}) error {
collection := m.client.Database(m.database).Collection(kind)
return collection.FindOne(ctx, makeNameFilter(name)).Decode(decodeTo)
}
// Put update data model
func (m *mongodb) Put(ctx context.Context, kind, name string, entity interface{}) error {
collection := m.client.Database(m.database).Collection(kind)
_, err := collection.UpdateOne(ctx, makeNameFilter(name), makeEntityUpdate(entity))
if err != nil {
return err
func (m *mongodb) Get(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
collection := m.client.Database(m.database).Collection(entity.TableName())
if err := collection.FindOne(ctx, makeNameFilter(entity.PrimaryKey())).Decode(entity); err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return datastore.ErrRecordNotExist
}
return datastore.NewDBError(err)
}
return nil
}
// Find find data model
func (m *mongodb) Find(ctx context.Context, kind string) (datastore.Iterator, error) {
collection := m.client.Database(m.database).Collection(kind)
// bson.D{{}} specifies 'all documents'
filter := bson.D{}
cur, err := collection.Find(ctx, filter)
if err != nil {
return nil, err
// Put update data model
func (m *mongodb) Put(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
return &Iterator{cur: cur}, nil
}
// FindOne find one data model
func (m *mongodb) FindOne(ctx context.Context, kind, name string) (datastore.Iterator, error) {
collection := m.client.Database(m.database).Collection(kind)
filter := bson.M{"name": name}
cur, err := collection.Find(ctx, filter)
if err != nil {
return nil, err
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
return &Iterator{cur: cur}, nil
collection := m.client.Database(m.database).Collection(entity.TableName())
_, err := collection.UpdateOne(ctx, makeNameFilter(entity.PrimaryKey()), makeEntityUpdate(entity))
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return datastore.ErrRecordNotExist
}
return datastore.NewDBError(err)
}
return nil
}
// IsExist determine whether data exists.
func (m *mongodb) IsExist(ctx context.Context, kind, name string) (bool, error) {
collection := m.client.Database(m.database).Collection(kind)
err := collection.FindOne(ctx, makeNameFilter(name)).Err()
func (m *mongodb) IsExist(ctx context.Context, entity datastore.Entity) (bool, error) {
if entity.PrimaryKey() == "" {
return false, datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return false, datastore.ErrTableNameEmpty
}
collection := m.client.Database(m.database).Collection(entity.TableName())
err := collection.FindOne(ctx, makeNameFilter(entity.PrimaryKey())).Err()
if errors.Is(err, mongo.ErrNoDocuments) {
return false, nil
} else if err != nil {
return false, err
return false, datastore.NewDBError(err)
}
return true, nil
}
// Delete delete data
func (m *mongodb) Delete(ctx context.Context, kind, name string) error {
collection := m.client.Database(m.database).Collection(kind)
func (m *mongodb) Delete(ctx context.Context, entity datastore.Entity) error {
if entity.PrimaryKey() == "" {
return datastore.ErrPrimaryEmpty
}
if entity.TableName() == "" {
return datastore.ErrTableNameEmpty
}
// check entity is exist
if err := m.Get(ctx, entity); err != nil {
return err
}
collection := m.client.Database(m.database).Collection(entity.TableName())
// delete at most one document in which the "name" field is "Bob" or "bob"
// specify the SetCollation option to provide a collation that will ignore case for string comparisons
opts := options.Delete().SetCollation(&options.Collation{
@@ -124,8 +148,51 @@ func (m *mongodb) Delete(ctx context.Context, kind, name string) error {
Strength: 1,
CaseLevel: false,
})
_, err := collection.DeleteOne(ctx, makeNameFilter(name), opts)
return err
_, err := collection.DeleteOne(ctx, makeNameFilter(entity.PrimaryKey()), opts)
if err != nil {
log.Logger.Errorf("delete document failure %w", err)
return datastore.NewDBError(err)
}
return nil
}
// List list entity function
func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) {
if entity.TableName() == "" {
return nil, datastore.ErrTableNameEmpty
}
collection := m.client.Database(m.database).Collection(entity.TableName())
// bson.D{{}} specifies 'all documents'
filter := bson.D{}
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))
}
cur, err := collection.Find(ctx, filter, &findOptions)
if err != nil {
return nil, datastore.NewDBError(err)
}
defer func() {
if err := cur.Close(ctx); err != nil {
log.Logger.Warnf("close mongodb cursor failure %s", err.Error())
}
}()
var list []datastore.Entity
for cur.Next(ctx) {
item, err := datastore.NewEntity(entity)
if err != nil {
return nil, datastore.NewDBError(err)
}
if err := cur.Decode(item); err != nil {
return nil, datastore.NewDBError(fmt.Errorf("decode entity failure %w", err))
}
list = append(list, item)
}
if err := cur.Err(); err != nil {
return nil, datastore.NewDBError(err)
}
return list, nil
}
func makeNameFilter(name string) bson.D {
@@ -0,0 +1,40 @@
/*
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 mongodb
import (
"context"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
)
func TestMongodb(t *testing.T) {
_, err := New(context.TODO(), datastore.Config{
URL: "mongodb://localhost:27017",
Database: "kubevela",
})
if err != nil {
t.Fatal(err)
}
RegisterFailHandler(Fail)
RunSpecs(t, "Mongodb Suite")
}
@@ -0,0 +1,136 @@
/*
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 mongodb
import (
"context"
"math/rand"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/model"
)
var mongodbDriver datastore.DataStore
var _ = BeforeSuite(func(done Done) {
rand.Seed(time.Now().UnixNano())
By("bootstrapping mongodb test environment")
var err error
mongodbDriver, err = New(context.TODO(), datastore.Config{
URL: "mongodb://localhost:27017",
Database: "kubevela",
})
Expect(err).ToNot(HaveOccurred())
Expect(mongodbDriver).ToNot(BeNil())
mongodbDriver, err = New(context.TODO(), datastore.Config{
URL: "localhost:27017",
Database: "kubevela",
})
Expect(err).ToNot(HaveOccurred())
Expect(mongodbDriver).ToNot(BeNil())
By("create mongodb driver success")
close(done)
}, 120)
var _ = Describe("Test mongodb datastore driver", func() {
It("Test add funtion", func() {
err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"})
Expect(err).ToNot(HaveOccurred())
})
It("Test get funtion", func() {
app := &model.Application{Name: "kubevela-app"}
err := mongodbDriver.Get(context.TODO(), app)
Expect(err).ToNot(HaveOccurred())
diff := cmp.Diff(app.Description, "default")
Expect(diff).Should(BeEmpty())
})
It("Test put funtion", func() {
err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"})
Expect(err).ToNot(HaveOccurred())
})
It("Test list funtion", func() {
err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"})
Expect(err).ToNot(HaveOccurred())
err = mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"})
Expect(err).ToNot(HaveOccurred())
var app model.Application
list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1})
Expect(err).ToNot(HaveOccurred())
diff := cmp.Diff(len(list), 3)
Expect(diff).Should(BeEmpty())
list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: 2, PageSize: 1})
Expect(err).ToNot(HaveOccurred())
diff = cmp.Diff(len(list), 1)
Expect(diff).Should(BeEmpty())
list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: 1, PageSize: 2})
Expect(err).ToNot(HaveOccurred())
diff = cmp.Diff(len(list), 2)
Expect(diff).Should(BeEmpty())
list, err = mongodbDriver.List(context.TODO(), &app, nil)
Expect(err).ToNot(HaveOccurred())
diff = cmp.Diff(len(list), 3)
Expect(diff).Should(BeEmpty())
})
It("Test isExist funtion", func() {
var app model.Application
app.Name = "kubevela-app-3"
exist, err := mongodbDriver.IsExist(context.TODO(), &app)
Expect(err).ToNot(HaveOccurred())
diff := cmp.Diff(exist, true)
Expect(diff).Should(BeEmpty())
app.Name = "kubevela-app-4"
notexist, err := mongodbDriver.IsExist(context.TODO(), &app)
Expect(err).ToNot(HaveOccurred())
diff = cmp.Diff(notexist, false)
Expect(diff).Should(BeEmpty())
})
It("Test delete funtion", func() {
var app model.Application
app.Name = "kubevela-app"
err := mongodbDriver.Delete(context.TODO(), &app)
Expect(err).ToNot(HaveOccurred())
app.Name = "kubevela-app-2"
err = mongodbDriver.Delete(context.TODO(), &app)
Expect(err).ToNot(HaveOccurred())
app.Name = "kubevela-app-3"
err = mongodbDriver.Delete(context.TODO(), &app)
Expect(err).ToNot(HaveOccurred())
app.Name = "kubevela-app-3"
err = mongodbDriver.Delete(context.TODO(), &app)
equal := cmp.Equal(err, datastore.ErrRecordNotExist, cmpopts.EquateErrors())
Expect(equal).Should(BeTrue())
})
})
+36
View File
@@ -0,0 +1,36 @@
/*
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
// Application database model
type Application struct {
Name string `json:"name"`
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
func (a *Application) TableName() string {
return tableNamePrefix + "application"
}
// PrimaryKey return custom primary key
func (a *Application) PrimaryKey() string {
return a.Name
}
+19
View File
@@ -0,0 +1,19 @@
/*
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
var tableNamePrefix = "vela_"
+85 -18
View File
@@ -18,6 +18,9 @@ package v1
import (
"time"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
)
// AddonPhase defines the phase of an addon
@@ -36,7 +39,7 @@ const (
// CreateAddonRequest defines the format for addon create request
type CreateAddonRequest struct {
Name string `json:"name" validate:"required"`
Name string `json:"name" validate:"name"`
Version string `json:"version" validate:"required"`
@@ -97,7 +100,7 @@ type AddonStatusResponse struct {
// CreateClusterRequest request parameters to create a cluster
type CreateClusterRequest struct {
Name string `json:"name" validate:"required"`
Name string `json:"name" validate:"name"`
Description string `json:"description,omitempty"`
Icon string `json:"icon"`
KubeConfig string `json:"kubeConfig" validate:"required_without=kubeConfigSecret"`
@@ -180,13 +183,15 @@ type GatewayRule struct {
// CreateApplicationRequest create application request body
type CreateApplicationRequest struct {
Name string `json:"name" validate:"required"`
Namespace string `json:"namespace" validate:"required"`
Name string `json:"name" validate:"checkname"`
Namespace string `json:"namespace" validate:"checkname"`
Description string `json:"description"`
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
@@ -219,7 +224,7 @@ type ComponentBase struct {
ComponentType string `json:"componentType"`
BindClusters []string `json:"bindClusters"`
Icon string `json:"icon,omitempty"`
DependOn []string `json:"dependOn"`
DependsOn []string `json:"dependsOn"`
Creator string `json:"creator,omitempty"`
DeployVersion string `json:"deployVersion"`
}
@@ -231,7 +236,7 @@ type ComponentListResponse struct {
// CreateComponentRequest create component request model
type CreateComponentRequest struct {
ApplicationName string `json:"appName" validate:"required"`
ApplicationName string `json:"appName" validate:"name"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Labels map[string]string `json:"labels,omitempty"`
@@ -275,7 +280,7 @@ type NamesapceBase struct {
// CreateNamespaceRequest create namespace request body
type CreateNamespaceRequest struct {
Name string `json:"name" validate:"required"`
Name string `json:"name" validate:"name"`
Description string `json:"description"`
}
@@ -292,17 +297,79 @@ type ListComponentDefinitionResponse struct {
// ComponentDefinitionBase component definition base model
type ComponentDefinitionBase struct {
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
RequiredParams []Param `json:"requiredParams"`
Name string `json:"name"`
Description string `json:"description"`
Icon string `json:"icon"`
Parameter []types.Parameter `json:"requiredParams"`
}
// Param For rendering forms
type Param struct {
Key string `json:"key"`
Name string `json:"name"`
DefaultValue interface{} `json:"defaultValue"`
Type string `json:"type"`
Description string `json:"description"`
// CreatePolicyRequest create app policy
type CreatePolicyRequest struct {
// Name is the unique name of the policy.
Name string `json:"name" validate:"name"`
Type string `json:"type" validate:"required"`
// 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"`
// Properties json data
Properties string `json:"properties"`
}
// ListPolicyDefinitionResponse list available
type ListPolicyDefinitionResponse struct {
PolicyDefinitions []PolicyDefinition `json:"policyDefinitions"`
}
// PolicyDefinition application policy definition
type PolicyDefinition struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters []types.Parameter `json:"parameters"`
}
// UpdateWorkflowRequest update or create application workflow
type UpdateWorkflowRequest struct {
Steps []WorkflowStep `json:"steps,omitempty"`
Enable bool `json:"enable"`
}
// 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"`
}
// DetailWorkflowResponse detail workflow response
type DetailWorkflowResponse struct {
Steps []WorkflowStep `json:"steps,omitempty"`
Enable bool `json:"enable"`
LastRecord *WorkflowRecord `json:"workflowRecord"`
}
// ListWorkflowRecordsResponse list workflow execution record
type ListWorkflowRecordsResponse struct {
Records []WorkflowRecord `json:"records"`
Total int64 `json:"total"`
}
// WorkflowRecord workflow record
type WorkflowRecord struct {
}
+1 -1
View File
@@ -82,7 +82,7 @@ func New(cfg Config) (a APIServer, err error) {
}
func (s *restServer) Run(ctx context.Context) error {
webservice.Init(ctx)
webservice.Init(ctx, s.dataStore)
err := s.registerServices()
if err != nil {
return err
+107
View File
@@ -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"
"encoding/json"
"errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
"github.com/oam-dev/kubevela/pkg/apiserver/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"
)
// ApplicationUsecase application usecase
type ApplicationUsecase interface {
CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error)
}
type applicationUsecaseImpl struct {
ds datastore.DataStore
}
// NewApplicationUsecase new cluster usecase
func NewApplicationUsecase(ds datastore.DataStore) ApplicationUsecase {
return &applicationUsecaseImpl{ds: ds}
}
// CreateApplication create application
func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) {
application := model.Application{
Name: req.Name,
Description: req.Description,
Icon: req.Icon,
Labels: req.Labels,
ClusterList: req.ClusterList,
}
// check clusters.
// check can deploy
var canDeploy bool
if req.YamlConfig != "" {
var oamApp v1beta1.Application
if err := json.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
}
// add to db.
if err := c.ds.Add(ctx, &application); err != nil {
if errors.Is(err, datastore.ErrRecordExist) {
return nil, bcode.ErrApplicationExist
}
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 {
return nil, err
}
}
return base, 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) renderAppBase(app *model.Application) *apisv1.ApplicationBase {
appBeas := &apisv1.ApplicationBase{
Name: app.Name,
Description: app.Description,
Icon: app.Icon,
Labels: app.Labels,
}
// TODO: get and render app status
return appBeas
}
@@ -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
// ErrApplicationConfig application config does not comply with OAM specification
var ErrApplicationConfig = NewBcode(400, 10000, "application config does not comply with OAM specification")
// ErrComponentTypeNotSupport an unsupported component type was used.
var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component type was used.")
// ErrApplicationExist application is exist
var ErrApplicationExist = NewBcode(400, 10002, "application name is exist")
+26 -9
View File
@@ -37,31 +37,48 @@ func (b *Bcode) Error() string {
return fmt.Sprintf("HTTPCode:%d BusinessCode:%d Message:%s", b.HTTPCode, b.BusinessCode, b.Message)
}
var bcodeMap map[int32]*Bcode
// NewBcode new business code
func NewBcode(httpCode, businessCode int32, message string) *Bcode {
if bcodeMap == nil {
bcodeMap = make(map[int32]*Bcode)
}
if _, exit := bcodeMap[businessCode]; exit {
panic("bcode business code is exist")
}
bcode := &Bcode{HTTPCode: httpCode, BusinessCode: businessCode, Message: message}
bcodeMap[businessCode] = bcode
return bcode
}
// ReturnError Unified handling of all types of errors, generating a standard return structure.
func ReturnError(req *restful.Request, res *restful.Response, err error) {
var bcode *Bcode
if errors.As(err, &bcode) {
if err := res.WriteEntity(err); err != nil {
if err := res.WriteHeaderAndEntity(int(bcode.HTTPCode), 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.WriteEntity(Bcode{HTTPCode: int32(restfulerr.Code), BusinessCode: int32(restfulerr.Code), Message: restfulerr.Message}); err != nil {
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 {
log.Logger.Error("write entity failure %s", err.Error())
}
return
}
var validErr *validator.ValidationErrors
if errors.As(err, validErr) {
if err := res.WriteEntity(Bcode{HTTPCode: 400, BusinessCode: 400, Message: err.Error()}); err != nil {
var validErr validator.ValidationErrors
if errors.As(err, &validErr) {
if err := res.WriteHeaderAndEntity(400, Bcode{HTTPCode: 400, BusinessCode: 400, Message: err.Error()}); err != nil {
log.Logger.Error("write entity failure %s", err.Error())
}
return
}
log.Logger.Errorf("Business exceptions, message %s, path:%s method:%s", err.Error(), req.Request.URL, req.Request.Method)
if err := res.WriteEntity(Bcode{HTTPCode: 500, BusinessCode: 500, Message: err.Error()}); err != nil {
log.Logger.Errorf("Business exceptions, error message: %s, path:%s method:%s", err.Error(), req.Request.URL, req.Request.Method)
if err := res.WriteHeaderAndEntity(500, Bcode{HTTPCode: 500, BusinessCode: 500, Message: err.Error()}); err != nil {
log.Logger.Error("write entity failure %s", err.Error())
}
}
+63 -2
View File
@@ -21,9 +21,19 @@ import (
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 applicationWebService struct {
applicationUsecase usecase.ApplicationUsecase
}
// NewApplicationWebService new application manage webservice
func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService {
return &applicationWebService{
applicationUsecase: applicationUsecase,
}
}
func (c *applicationWebService) GetWebService() *restful.WebService {
@@ -35,7 +45,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService {
tags := []string{"application"}
ws.Route(ws.GET("/").To(noop).
ws.Route(ws.GET("/").To(c.listApplications).
Doc("list all applications").
Metadata(restfulspec.KeyOpenAPITags, tags).
Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")).
@@ -43,7 +53,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService {
Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")).
Writes(apis.ListApplicationResponse{}))
ws.Route(ws.POST("/").To(noop).
ws.Route(ws.POST("/").To(c.createApplication).
Doc("create one application").
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(apis.CreateApplicationRequest{}).
@@ -77,6 +87,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService {
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")).
Metadata(restfulspec.KeyOpenAPITags, tags).
Writes(apis.ComponentListResponse{}))
@@ -86,5 +97,55 @@ func (c *applicationWebService) GetWebService() *restful.WebService {
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(apis.CreateComponentRequest{}).
Writes(apis.ComponentBase{}))
ws.Route(ws.POST("/{name}/policies").To(noop).
Doc("create policy for application").
Param(ws.PathParameter("name", "identifier of the application").DataType("string")).
Metadata(restfulspec.KeyOpenAPITags, tags).
Reads(apis.CreatePolicyRequest{}).
Writes(apis.DetailPolicyResponse{}))
ws.Route(ws.GET("/{name}/policies/{policyName}").To(noop).
Doc("detail policy for application").
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).
Writes(apis.DetailPolicyResponse{}))
ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(noop).
Doc("detail policy for application").
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).
Writes(apis.DetailPolicyResponse{}))
return ws
}
func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var createReq apis.CreateApplicationRequest
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
appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
// Write back response data
if err := res.WriteEntity(appBase); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) {
}
+10 -3
View File
@@ -25,11 +25,18 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
)
type clusterWebService struct {
// ClusterWebService cluster manage webservice
type ClusterWebService struct {
clusterUsecase usecase.ClusterUsecase
}
func (c *clusterWebService) GetWebService() *restful.WebService {
// NewClusterWebService new cluster webservice
func NewClusterWebService(clusterUsecase usecase.ClusterUsecase) *ClusterWebService {
return &ClusterWebService{clusterUsecase: clusterUsecase}
}
// GetWebService -
func (c *ClusterWebService) GetWebService() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/clusters").
Consumes(restful.MIME_XML, restful.MIME_JSON).
@@ -71,7 +78,7 @@ func (c *clusterWebService) GetWebService() *restful.WebService {
return ws
}
func (c *clusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) {
func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var createReq apis.CreateClusterRequest
if err := req.ReadEntity(&createReq); err != nil {
@@ -0,0 +1,43 @@
/*
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 policyDefinitionWebservice struct {
}
func (c *policyDefinitionWebservice) GetWebService() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/policydefinitions").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for policydefinition manage")
tags := []string{"policydefinition"}
ws.Route(ws.GET("/").To(noop).
Doc("list all policydefinition").
Metadata(restfulspec.KeyOpenAPITags, tags).
Writes(apis.ListPolicyDefinitionResponse{}))
return ws
}
+42
View File
@@ -0,0 +1,42 @@
/*
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 (
"regexp"
"github.com/go-playground/validator/v10"
)
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])?)*$`)
func init() {
if err := validate.RegisterValidation("checkname", ValidateName); err != nil {
panic(err)
}
}
// ValidateName custom check name field
func ValidateName(fl validator.FieldLevel) bool {
value := fl.Field().String()
if len(value) > 32 || len(value) < 2 {
return false
}
return nameRegexp.MatchString(value)
}
@@ -0,0 +1,56 @@
/*
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 (
"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 validate function", func() {
It("Test check name validate ", func() {
Expect(cmp.Diff(nameRegexp.MatchString("///Asd asda "), false)).Should(BeEmpty())
var app0 = apisv1.CreateApplicationRequest{
Name: "a",
Namespace: "namesapce",
}
err := validate.Struct(&app0)
Expect(err).ShouldNot(BeNil())
var app1 = apisv1.CreateApplicationRequest{
Name: "Asdasd",
Namespace: "namesapce",
}
err = validate.Struct(&app1)
Expect(err).ShouldNot(BeNil())
var app2 = apisv1.CreateApplicationRequest{
Name: "asdasd asdasd ++",
Namespace: "namesapce",
}
err = validate.Struct(&app2)
Expect(err).ShouldNot(BeNil())
var app3 = apisv1.CreateApplicationRequest{
Name: "asdasd",
Namespace: "namesapce",
}
err = validate.Struct(&app3)
Expect(err).Should(BeNil())
})
})
+11 -6
View File
@@ -21,14 +21,14 @@ import (
"net/http"
"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/rest/usecase"
)
// versionPrefix API version prefix.
var versionPrefix = "/api/v1"
var validate = validator.New()
// WebService webservice interface
type WebService interface {
GetWebService() *restful.WebService
@@ -57,11 +57,16 @@ func returns500(b *restful.RouteBuilder) {
}
// Init init all webservice, pass in the required parameter object.
func Init(ctx context.Context) {
RegistWebService(&clusterWebService{})
RegistWebService(&applicationWebService{})
// It can be implemented using the idea of dependency injection.
func Init(ctx context.Context, ds datastore.DataStore) {
clusterUsecase := usecase.NewClusterUsecase(ds)
applicationUsecase := usecase.NewApplicationUsecase(ds)
RegistWebService(NewClusterWebService(clusterUsecase))
RegistWebService(NewApplicationWebService(applicationUsecase))
RegistWebService(&namespaceWebService{})
RegistWebService(&componentDefinitionWebservice{})
RegistWebService(&addonWebService{})
RegistWebService(&oamApplicationWebService{})
RegistWebService(&policyDefinitionWebservice{})
RegistWebService(&workflowWebService{})
}
@@ -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 webservice_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestWebservice(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Webservice Suite")
}
+60
View File
@@ -0,0 +1,60 @@
/*
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 workflowWebService struct {
}
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
}
@@ -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 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 application rest api", func() {
It("Test create app", func() {
defer GinkgoRecover()
var req = apisv1.CreateApplicationRequest{
Name: "test-app-sadasd",
Namespace: "test-app-namesapce",
Description: "this is a test app",
Icon: "",
Labels: map[string]string{"test": "true"},
ClusterList: []string{},
}
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.Labels["test"], req.Labels["test"])).Should(BeEmpty())
})
})
+98
View File
@@ -0,0 +1,98 @@
/*
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 (
"context"
"testing"
"time"
. "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")
}
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})
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{
BindAddr: "127.0.0.1:8000",
Datastore: datastore.Config{
Type: "kubeapi",
Database: "kubevela",
},
})
Expect(err).ShouldNot(HaveOccurred())
Expect(server).ShouldNot(BeNil())
go func() {
err = server.Run(ctx)
Expect(err).ShouldNot(HaveOccurred())
}()
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())
})