This is an automated email from the ASF dual-hosted git repository. chenjunxu pushed a commit to branch refactor in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/refactor by this push: new caccfb7 feat: refactor apis for existing check and other apis (#535) caccfb7 is described below commit caccfb75c57eb2e85466b42cf5b7a4368f1e96a8 Author: nic-chen <33000667+nic-c...@users.noreply.github.com> AuthorDate: Fri Oct 9 16:21:49 2020 +0800 feat: refactor apis for existing check and other apis (#535) * fix code style * fix code style * feat support query * feat: support query * change: `like` to `equal` * fix api status * feat: upstream existing check * feat: refactor api for upstream names * fix: license * test: add unit test cases * fix: update bug * test: add test cases for route * fix: unified respond format * test: remove test bug * feat: ssl existing check * fix bug: auto generate id * fix: improve consumer * fix: remove key and keys in ssl respond * fix: when list is empty, should respond an empty array * fix code style * feat: plugin orchestration * fix delete bug * fix bug * fix: keep the same request params and respond with the old format --- api/errno/error.go | 22 +- api/internal/core/entity/entity.go | 32 ++- api/internal/core/entity/query.go | 153 +++++++++++ api/internal/core/store/query.go | 151 +++++++++++ api/internal/core/store/selector.go | 144 +++++++++++ api/internal/core/store/selector_test.go | 285 +++++++++++++++++++++ api/internal/core/store/store.go | 38 ++- api/internal/core/store/storehub.go | 80 ++++++ api/internal/handler/consumer/consumer.go | 20 +- api/internal/handler/route/route.go | 158 +++++++++++- api/internal/handler/service/service.go | 4 +- api/internal/handler/ssl/ssl.go | 123 ++++++++- api/internal/handler/upstream/upstream.go | 81 +++++- .../storehub.go => utils/consts/api_error.go} | 57 ++--- api/main.go | 76 +----- api/route/base_test.go | 67 +---- api/route/route_test.go | 195 ++++++++++++++ 17 files changed, 1468 insertions(+), 218 deletions(-) diff --git a/api/errno/error.go b/api/errno/error.go index effe063..0c18dec 100644 --- a/api/errno/error.go +++ b/api/errno/error.go @@ -23,8 +23,8 @@ import ( type Message struct { Code string - Msg string - Status int `json:"-"` + Msg string `json:"message"` + Status int `json:"-"` } var ( @@ -128,25 +128,25 @@ func New(m Message, args ...interface{}) *ManagerError { func (e *ManagerError) Response() map[string]interface{} { return map[string]interface{}{ - "code": e.Code, - "msg": e.Msg, + "code": e.Code, + "message": e.Msg, } } func (e *ManagerError) ItemResponse(data interface{}) map[string]interface{} { return map[string]interface{}{ - "code": e.Code, - "msg": e.Msg, - "data": data, + "code": e.Code, + "message": e.Msg, + "data": data, } } func (e *ManagerError) ListResponse(count, list interface{}) map[string]interface{} { return map[string]interface{}{ - "code": e.Code, - "msg": e.Msg, - "count": count, - "list": list, + "code": e.Code, + "message": e.Msg, + "count": count, + "list": list, } } diff --git a/api/internal/core/entity/entity.go b/api/internal/core/entity/entity.go index 0a38c82..8a2fae4 100644 --- a/api/internal/core/entity/entity.go +++ b/api/internal/core/entity/entity.go @@ -44,7 +44,7 @@ type Route struct { RemoteAddrs []string `json:"remote_addrs,omitempty"` Vars string `json:"vars,omitempty"` FilterFunc string `json:"filter_func,omitempty"` - Script string `json:"script,omitempty"` + Script interface{} `json:"script,omitempty"` Plugins interface{} `json:"plugins,omitempty"` Upstream Upstream `json:"upstream,omitempty"` ServiceID string `json:"service_id,omitempty"` @@ -113,6 +113,7 @@ type HealthChecker struct { } type Upstream struct { + BaseInfo Nodes []Node `json:"nodes,omitempty"` Retries int `json:"retries,omitempty"` Timeout Timeout `json:"timeout,omitempty"` @@ -127,26 +128,38 @@ type Upstream struct { Name string `json:"name,omitempty"` Desc string `json:"desc,omitempty"` ServiceName string `json:"service_name,omitempty"` - ID string `json:"id,omitempty"` +} + +type UpstreamNameResponse struct { + ID string `json:"id"` + Name string `json:"name"` +} + +func (upstream *Upstream) Parse2NameResponse() (*UpstreamNameResponse, error) { + nameResp := &UpstreamNameResponse{ + ID: upstream.ID, + Name: upstream.Name, + } + return nameResp, nil } // --- structures for upstream end --- type Consumer struct { - ID string `json:"id"` + BaseInfo Username string `json:"username"` Desc string `json:"desc,omitempty"` Plugins interface{} `json:"plugins,omitempty"` } type SSL struct { - ID string `json:"id"` + BaseInfo Cert string `json:"cert"` - Key string `json:"key"` + Key string `json:"key,omitempty"` Sni string `json:"sni"` Snis []string `json:"snis"` Certs []string `json:"certs"` - Keys []string `json:"keys"` + Keys []string `json:"keys,omitempty"` ExpTime int64 `json:"exptime"` Status int `json:"status"` ValidityStart int64 `json:"validity_start"` @@ -154,7 +167,7 @@ type SSL struct { } type Service struct { - ID string `json:"id"` + BaseInfo Name string `json:"name,omitempty"` Desc string `json:"desc,omitempty"` Upstream Upstream `json:"upstream,omitempty"` @@ -162,3 +175,8 @@ type Service struct { Plugins interface{} `json:"plugins,omitempty"` Script string `json:"script,omitempty"` } + +type Script struct { + ID string `json:"id"` + Script interface{} `json:"script,omitempty"` +} diff --git a/api/internal/core/entity/query.go b/api/internal/core/entity/query.go new file mode 100644 index 0000000..ea3e658 --- /dev/null +++ b/api/internal/core/entity/query.go @@ -0,0 +1,153 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 entity + +import "strings" + +type PropertyName string + +const ( + IdProperty = "id" + NameProperty = "name" + SniProperty = "sni" + SnisProperty = "snis" + CreateTimeProperty = "create_time" + UpdateTimeProperty = "update_time" +) + +type ComparableValue interface { + Compare(ComparableValue) int + Contains(ComparableValue) bool +} + +type ComparingString string + +func (comparing ComparingString) Compare(compared ComparableValue) int { + other := compared.(ComparingString) + return strings.Compare(string(comparing), string(other)) +} + +func (comparing ComparingString) Contains(compared ComparableValue) bool { + other := compared.(ComparingString) + return strings.Contains(string(comparing), string(other)) +} + +type ComparingStringArray []string + +func (comparing ComparingStringArray) Compare(compared ComparableValue) int { + other := compared.(ComparingString) + res := -1 + for _, str := range comparing { + result := strings.Compare(str, string(other)) + if result == 0 { + res = 0 + break + } + } + return res +} + +func (comparing ComparingStringArray) Contains(compared ComparableValue) bool { + other := compared.(ComparingString) + res := false + for _, str := range comparing { + if strings.Contains(str, string(other)) { + res = true + break + } + } + return res +} + +type ComparingInt int64 + +func int64Compare(a, b int64) int { + if a > b { + return 1 + } else if a == b { + return 0 + } + return -1 +} + +func (comparing ComparingInt) Compare(compared ComparableValue) int { + other := compared.(ComparingInt) + return int64Compare(int64(comparing), int64(other)) +} + +func (comparing ComparingInt) Contains(compared ComparableValue) bool { + return comparing.Compare(compared) == 0 +} + +func (info BaseInfo) GetProperty(name PropertyName) ComparableValue { + switch name { + case IdProperty: + return ComparingString(info.ID) + case CreateTimeProperty: + return ComparingInt(info.CreateTime) + case UpdateTimeProperty: + return ComparingInt(info.UpdateTime) + default: + return nil + } +} + +func (route Route) GetProperty(name PropertyName) ComparableValue { + switch name { + case NameProperty: + return ComparingString(route.Name) + default: + return nil + } +} + +func (upstream Upstream) GetProperty(name PropertyName) ComparableValue { + switch name { + case NameProperty: + return ComparingString(upstream.Name) + default: + return nil + } +} + +func (ssl SSL) GetProperty(name PropertyName) ComparableValue { + switch name { + case SniProperty: + return ComparingString(ssl.Sni) + case SnisProperty: + return ComparingStringArray(ssl.Snis) + default: + return nil + } +} diff --git a/api/internal/core/store/query.go b/api/internal/core/store/query.go new file mode 100644 index 0000000..c05a7cd --- /dev/null +++ b/api/internal/core/store/query.go @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +// Copyright 2017 The Kubernetes 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 store + +import ( + "github.com/apisix/manager-api/internal/core/entity" +) + +type Query struct { + Sort *Sort + Filter *Filter + Pagination *Pagination +} + +type Sort struct { + List []SortBy +} + +type SortBy struct { + Property entity.PropertyName + Ascending bool +} + +var NoSort = &Sort{ + List: []SortBy{}, +} + +type Filter struct { + List []FilterBy +} + +type FilterBy struct { + Property entity.PropertyName + Value entity.ComparableValue +} + +var NoFilter = &Filter{ + List: []FilterBy{}, +} + +type Pagination struct { + PageSize int + PageNumber int +} + +func NewPagination(PageSize, pageNumber int) *Pagination { + return &Pagination{PageSize, pageNumber} +} + +func (p *Pagination) IsValid() bool { + return p.PageSize >= 0 && p.PageNumber >= 0 +} + +func (p *Pagination) IsAvailable(itemsCount, startingIndex int) bool { + return itemsCount > startingIndex && p.PageSize > 0 +} + +func (p *Pagination) Index(itemsCount int) (startIndex int, endIndex int) { + startIndex = p.PageSize * p.PageNumber + endIndex = startIndex + p.PageSize + + if endIndex > itemsCount { + endIndex = itemsCount + } + + return startIndex, endIndex +} + +func NewQuery(sort *Sort, filter *Filter, pagination *Pagination) *Query { + return &Query{ + Sort: sort, + Filter: filter, + Pagination: pagination, + } +} + +func NewSort(sortRaw []string) *Sort { + if sortRaw == nil || len(sortRaw)%2 == 1 { + // Empty sort list or invalid (odd) length + return NoSort + } + list := []SortBy{} + for i := 0; i+1 < len(sortRaw); i += 2 { + var ascending bool + orderOption := sortRaw[i] + if orderOption == "a" { + ascending = true + } else if orderOption == "d" { + ascending = false + } else { + return NoSort + } + + propertyName := sortRaw[i+1] + sortBy := SortBy{ + Property: entity.PropertyName(propertyName), + Ascending: ascending, + } + list = append(list, sortBy) + } + return &Sort{ + List: list, + } +} + +func NewFilter(filterRaw []string) *Filter { + if filterRaw == nil || len(filterRaw)%2 == 1 { + return NoFilter + } + list := []FilterBy{} + for i := 0; i+1 < len(filterRaw); i += 2 { + propertyName := filterRaw[i] + propertyValue := filterRaw[i+1] + filterBy := FilterBy{ + Property: entity.PropertyName(propertyName), + Value: entity.ComparingString(propertyValue), + } + list = append(list, filterBy) + } + return &Filter{ + List: list, + } +} diff --git a/api/internal/core/store/selector.go b/api/internal/core/store/selector.go new file mode 100644 index 0000000..1fc8892 --- /dev/null +++ b/api/internal/core/store/selector.go @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +// Copyright 2017 The Kubernetes 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 store + +import ( + "sort" + + "github.com/apisix/manager-api/internal/core/entity" +) + +type Row interface { + GetProperty(entity.PropertyName) entity.ComparableValue +} + +type Selector struct { + List []Row + Query *Query +} + +func (self Selector) Len() int { return len(self.List) } + +func (self Selector) Swap(i, j int) { + self.List[i], self.List[j] = self.List[j], self.List[i] +} + +func (self Selector) Less(i, j int) bool { + for _, sortBy := range self.Query.Sort.List { + a := self.List[i].GetProperty(sortBy.Property) + b := self.List[j].GetProperty(sortBy.Property) + if a == nil || b == nil { + break + } + cmp := a.Compare(b) + if cmp == 0 { + continue + } else { + return (cmp == -1 && sortBy.Ascending) || (cmp == 1 && !sortBy.Ascending) + } + } + return false +} + +func (self *Selector) Sort() *Selector { + sort.Sort(*self) + return self +} + +func (self *Selector) Filter() *Selector { + filteredList := []Row{} + for _, c := range self.List { + matches := true + for _, filterBy := range self.Query.Filter.List { + v := c.GetProperty(filterBy.Property) + if v == nil || v.Compare(filterBy.Value) != 0 { + matches = false + break + } + } + if matches { + filteredList = append(filteredList, c) + } + } + + self.List = filteredList + return self +} + +func (self *Selector) Paginate() *Selector { + pagination := self.Query.Pagination + dataList := self.List + TotalSize := len(dataList) + startIndex, endIndex := pagination.Index(TotalSize) + + if startIndex == 0 && endIndex == 0 { + return self + } + + if !pagination.IsValid() { + self.List = []Row{} + return self + } + + if startIndex > TotalSize { + self.List = []Row{} + return self + } + + if endIndex >= TotalSize { + self.List = dataList[startIndex:] + return self + } + + self.List = dataList[startIndex:endIndex] + return self +} + +func NewFilterSelector(list []Row, query *Query) []Row { + selector := Selector{ + List: list, + Query: query, + } + filtered := selector.Filter() + paged := filtered.Paginate() + return paged.List +} + +func DefaultSelector(list []Row, query *Query) ([]Row, int) { + selector := Selector{ + List: list, + Query: query, + } + filtered := selector.Filter() + filteredTotal := len(filtered.List) + paged := filtered.Sort().Paginate() + return paged.List, filteredTotal +} diff --git a/api/internal/core/store/selector_test.go b/api/internal/core/store/selector_test.go new file mode 100644 index 0000000..cb99f42 --- /dev/null +++ b/api/internal/core/store/selector_test.go @@ -0,0 +1,285 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +// Copyright 2017 The Kubernetes 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 store + +import ( + "reflect" + "testing" + + "github.com/apisix/manager-api/internal/core/entity" +) + +type PaginationTestCase struct { + Info string + Pagination *Pagination + ExpectedOrder []int +} + +type SortTestCase struct { + Info string + Sort *Sort + ExpectedOrder []int +} + +type FilterTestCase struct { + Info string + Filter *Filter + ExpectedOrder []int +} + +type TestRow struct { + Name string + CreateTime int64 + Id int + Snis []string +} + +func (self TestRow) GetProperty(name entity.PropertyName) entity.ComparableValue { + switch name { + case entity.NameProperty: + return entity.ComparingString(self.Name) + case entity.SnisProperty: + return entity.ComparingStringArray(self.Snis) + case entity.CreateTimeProperty: + return entity.ComparingInt(self.CreateTime) + default: + return nil + } +} + +func toRows(std []TestRow) []Row { + rows := make([]Row, len(std)) + for i := range std { + rows[i] = std[i] + } + return rows +} + +func fromRows(rows []Row) []TestRow { + std := make([]TestRow, len(rows)) + for i := range std { + std[i] = rows[i].(TestRow) + } + return std +} + +func getDataList() []Row { + return toRows([]TestRow{ + {"b", 1, 1, []string{"a", "b"}}, + {"a", 2, 2, []string{"c", "d"}}, + {"a", 3, 3, []string{"f", "e"}}, + {"c", 4, 4, []string{"g", "h"}}, + {"c", 5, 5, []string{"k", "j"}}, + {"d", 6, 6, []string{"i", "h"}}, + {"e", 7, 7, []string{"t", "r"}}, + {"e", 8, 8, []string{"q", "w"}}, + {"f", 9, 9, []string{"x", "z"}}, + {"a", 10, 10, []string{"v", "n"}}, + }) +} + +func getOrder(dataList []TestRow) []int { + ordered := []int{} + for _, e := range dataList { + ordered = append(ordered, e.Id) + } + return ordered +} + +func TestSort(t *testing.T) { + testCases := []SortTestCase{ + { + "no sort - do not change the original order", + NoSort, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "ascending sort by 1 property - all items sorted by this property", + NewSort([]string{"a", "create_time"}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "descending sort by 1 property - all items sorted by this property", + NewSort([]string{"d", "create_time"}), + []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, + }, + { + "sort by 2 properties - items should first be sorted by first property and later by second", + NewSort([]string{"a", "name", "d", "create_time"}), + []int{10, 3, 2, 1, 5, 4, 6, 8, 7, 9}, + }, + { + "empty sort list - no sort", + NewSort([]string{}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "nil - no sort", + NewSort(nil), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + // Invalid arguments to the NewSortQuery + { + "sort by few properties where at least one property name is invalid - no sort", + NewSort([]string{"a", "INVALID_PROPERTY", "d", "creationTimestamp"}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "sort by few properties where at least one order option is invalid - no sort", + NewSort([]string{"d", "name", "INVALID_ORDER", "creationTimestamp"}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "sort by few properties where one order tag is missing property - no sort", + NewSort([]string{""}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "sort by few properties where one order tag is missing property - no sort", + NewSort([]string{"d", "name", "a", "creationTimestamp", "a"}), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + } + for _, testCase := range testCases { + selector := Selector{ + List: getDataList(), + Query: &Query{Sort: testCase.Sort}, + } + sortedData := fromRows(selector.Sort().List) + order := getOrder(sortedData) + if !reflect.DeepEqual(order, testCase.ExpectedOrder) { + t.Errorf(`Sort: %s. Received invalid items for %+v. Got %v, expected %v.`, + testCase.Info, testCase.Sort, order, testCase.ExpectedOrder) + } + } + +} + +func TestPagination(t *testing.T) { + testCases := []PaginationTestCase{ + { + "no pagination - all existing elements should be returned", + NewPagination(0, 0), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "request one item from existing page - element should be returned", + NewPagination(1, 5), + []int{6}, + }, + { + "request one item from non existing page - no elements should be returned", + NewPagination(1, 10), + []int{}, + }, + { + "request 2 items from existing page - 2 elements should be returned", + NewPagination(2, 1), + []int{3, 4}, + }, + { + "request 3 items from partially existing page - last few existing should be returned", + NewPagination(3, 3), + []int{10}, + }, + { + "request more than total number of elements from page 1 - all existing elements should be returned", + NewPagination(11, 0), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "request 3 items from non existing page - no elements should be returned", + NewPagination(3, 4), + []int{}, + }, + { + "Invalid pagination - all elements should be returned", + NewPagination(-1, 4), + []int{}, + }, + { + "Invalid pagination - all elements should be returned", + NewPagination(1, -4), + []int{}, + }, + } + for _, testCase := range testCases { + selector := Selector{ + List: getDataList(), + Query: &Query{Pagination: testCase.Pagination}, + } + paginatedData := fromRows(selector.Paginate().List) + order := getOrder(paginatedData) + if !reflect.DeepEqual(order, testCase.ExpectedOrder) { + t.Errorf(`Pagination: %s. Received invalid items for %+v. Got %v, expected %v.`, + testCase.Info, testCase.Pagination, order, testCase.ExpectedOrder) + } + } + +} + +func TestFilter(t *testing.T) { + testCases := []FilterTestCase{ + { + "no sort - do not change the original order", + NewFilter(nil), + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + { + "string filter", + NewFilter([]string{"name", "a"}), + []int{2, 3, 10}, + }, + { + "string array filter", + NewFilter([]string{"snis", "x"}), + []int{9}, + }, + { + "multi filter", + NewFilter([]string{"snis", "t", "name", "e"}), + []int{7}, + }, + } + for _, testCase := range testCases { + selector := Selector{ + List: getDataList(), + Query: &Query{Filter: testCase.Filter}, + } + filteredData := fromRows(selector.Filter().List) + order := getOrder(filteredData) + if !reflect.DeepEqual(order, testCase.ExpectedOrder) { + t.Errorf(`Filter: %s. Received invalid items for %+v. Got %v, expected %v.`, + testCase.Info, testCase.Filter, order, testCase.ExpectedOrder) + } + } + +} diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go index 75c74a0..263a49f 100644 --- a/api/internal/core/store/store.go +++ b/api/internal/core/store/store.go @@ -14,19 +14,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package store import ( "context" "encoding/json" "fmt" - "github.com/apisix/manager-api/internal/core/entity" - "github.com/apisix/manager-api/internal/core/storage" - "github.com/apisix/manager-api/internal/utils" - "github.com/shiningrush/droplet/data" "log" "reflect" "time" + + "github.com/shiningrush/droplet/data" + + "github.com/apisix/manager-api/internal/core/entity" + "github.com/apisix/manager-api/internal/core/storage" + "github.com/apisix/manager-api/internal/utils" ) type Interface interface { @@ -200,15 +203,6 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { return err } - key := s.opt.KeyFunc(obj) - if key == "" { - return fmt.Errorf("key is required") - } - _, ok := s.cache[key] - if ok { - return fmt.Errorf("key: %s is conflicted", key) - } - if getter, ok := obj.(entity.BaseInfoGetter); ok { info := getter.GetBaseInfo() if info.ID == "" { @@ -218,6 +212,15 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { info.UpdateTime = time.Now().Unix() } + key := s.opt.KeyFunc(obj) + if key == "" { + return fmt.Errorf("key is required") + } + _, ok := s.cache[key] + if ok { + return fmt.Errorf("key: %s is conflicted", key) + } + bs, err := json.Marshal(obj) if err != nil { return fmt.Errorf("json marshal failed: %s", err) @@ -238,13 +241,20 @@ func (s *GenericStore) Update(ctx context.Context, obj interface{}) error { if key == "" { return fmt.Errorf("key is required") } - _, ok := s.cache[key] + oldObj, ok := s.cache[key] if !ok { return fmt.Errorf("key: %s is not found", key) } + createTime := int64(0) + if oldGetter, ok := oldObj.(entity.BaseInfoGetter); ok { + oldInfo := oldGetter.GetBaseInfo() + createTime = oldInfo.CreateTime + } + if getter, ok := obj.(entity.BaseInfoGetter); ok { info := getter.GetBaseInfo() + info.CreateTime = createTime info.UpdateTime = time.Now().Unix() } diff --git a/api/internal/core/store/storehub.go b/api/internal/core/store/storehub.go index 203c5bc..d07210a 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/core/store/storehub.go @@ -18,6 +18,9 @@ package store import ( "fmt" + "reflect" + + "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/utils" ) @@ -29,6 +32,7 @@ const ( HubKeyService HubKey = "service" HubKeySsl HubKey = "ssl" HubKeyUpstream HubKey = "upstream" + HubKeyScript HubKey = "script" ) var ( @@ -55,3 +59,79 @@ func GetStore(key HubKey) *GenericStore { } panic(fmt.Sprintf("no store with key: %s", key)) } + +func InitStores() error { + err := InitStore(HubKeyConsumer, GenericStoreOption{ + BasePath: "/apisix/consumers", + ObjType: reflect.TypeOf(entity.Consumer{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.Consumer) + return r.Username + }, + }) + if err != nil { + return err + } + + err = InitStore(HubKeyRoute, GenericStoreOption{ + BasePath: "/apisix/routes", + ObjType: reflect.TypeOf(entity.Route{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.Route) + return r.ID + }, + }) + if err != nil { + return err + } + + err = InitStore(HubKeyService, GenericStoreOption{ + BasePath: "/apisix/services", + ObjType: reflect.TypeOf(entity.Service{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.Service) + return r.ID + }, + }) + if err != nil { + return err + } + + err = InitStore(HubKeySsl, GenericStoreOption{ + BasePath: "/apisix/ssl", + ObjType: reflect.TypeOf(entity.SSL{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.SSL) + return r.ID + }, + }) + if err != nil { + return err + } + + err = InitStore(HubKeyUpstream, GenericStoreOption{ + BasePath: "/apisix/upstreams", + ObjType: reflect.TypeOf(entity.Upstream{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.Upstream) + return r.ID + }, + }) + if err != nil { + return err + } + + err = InitStore(HubKeyScript, GenericStoreOption{ + BasePath: "/apisix/scripts", + ObjType: reflect.TypeOf(entity.Script{}), + KeyFunc: func(obj interface{}) string { + r := obj.(*entity.Script) + return r.ID + }, + }) + if err != nil { + return err + } + + return nil +} diff --git a/api/internal/handler/consumer/consumer.go b/api/internal/handler/consumer/consumer.go index 71d4b03..d2533e7 100644 --- a/api/internal/handler/consumer/consumer.go +++ b/api/internal/handler/consumer/consumer.go @@ -17,6 +17,7 @@ package consumer import ( + "fmt" "reflect" "strings" @@ -95,8 +96,11 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { func (h *Handler) Create(c droplet.Context) (interface{}, error) { input := c.Input().(*entity.Consumer) + if input.ID != "" && input.ID != input.Username { + return nil, fmt.Errorf("consumer's id and username must be a same value") + } + input.ID = input.Username - //TODO: check duplicate username if err := h.consumerStore.Create(c.Context(), input); err != nil { return nil, err } @@ -111,11 +115,21 @@ type UpdateInput struct { func (h *Handler) Update(c droplet.Context) (interface{}, error) { input := c.Input().(*UpdateInput) + if input.ID != "" && input.ID != input.Username { + return nil, fmt.Errorf("consumer's id and username must be a same value") + } input.Consumer.Username = input.Username + input.Consumer.ID = input.Username - //TODO: if not exists, create if err := h.consumerStore.Update(c.Context(), &input.Consumer); err != nil { - return nil, err + //if not exists, create + if err.Error() == fmt.Sprintf("key: %s is not found", input.Username) { + if err := h.consumerStore.Create(c.Context(), &input.Consumer); err != nil { + return nil, err + } + } else { + return nil, err + } } return nil, nil diff --git a/api/internal/handler/route/route.go b/api/internal/handler/route/route.go index c46ad12..45662b9 100644 --- a/api/internal/handler/route/route.go +++ b/api/internal/handler/route/route.go @@ -17,7 +17,10 @@ package route import ( + "encoding/json" "fmt" + "io/ioutil" + "os/exec" "reflect" "strings" @@ -30,12 +33,15 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/handler" + "github.com/apisix/manager-api/internal/utils" + "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { routeStore store.Interface svcStore store.Interface upstreamStore store.Interface + scriptStore store.Interface } func NewHandler() (handler.RouteRegister, error) { @@ -43,6 +49,7 @@ func NewHandler() (handler.RouteRegister, error) { routeStore: store.GetStore(store.HubKeyRoute), svcStore: store.GetStore(store.HubKeyService), upstreamStore: store.GetStore(store.HubKeyUpstream), + scriptStore: store.GetStore(store.HubKeyScript), }, nil } @@ -55,8 +62,10 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { wrapper.InputType(reflect.TypeOf(entity.Route{})))) r.PUT("/apisix/admin/routes/:id", wgin.Wraps(h.Update, wrapper.InputType(reflect.TypeOf(UpdateInput{})))) - r.DELETE("/apisix/admin/routes", wgin.Wraps(h.BatchDelete, + r.DELETE("/apisix/admin/routes/:ids", wgin.Wraps(h.BatchDelete, wrapper.InputType(reflect.TypeOf(BatchDelete{})))) + + r.GET("/apisix/admin/notexist/routes", consts.ErrorWrapper(Exist)) } type GetInput struct { @@ -70,7 +79,15 @@ func (h *Handler) Get(c droplet.Context) (interface{}, error) { if err != nil { return nil, err } - return r, nil + + //format respond + route := r.(*entity.Route) + script, _ := h.scriptStore.Get(input.ID) + if script != nil { + route.Script = script.(*entity.Script).Script + } + + return route, nil } type ListInput struct { @@ -95,11 +112,45 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { return nil, err } + //format respond + var route *entity.Route + for i, item := range ret.Rows { + route = item.(*entity.Route) + script, _ := h.scriptStore.Get(route.ID) + if script != nil { + route.Script = script.(*entity.Script).Script + } + ret.Rows[i] = route + } + return ret, nil } +func generateLuaCode(script map[string]interface{}) (string, error) { + scriptString, err := json.Marshal(script) + if err != nil { + return "", err + } + + cmd := exec.Command("sh", "-c", + "cd /go/manager-api/dag-to-lua/ && lua cli.lua "+ + "'"+string(scriptString)+"'") + + stdout, _ := cmd.StdoutPipe() + defer stdout.Close() + if err := cmd.Start(); err != nil { + return "", err + } + + result, _ := ioutil.ReadAll(stdout) + resData := string(result) + + return resData, nil +} + func (h *Handler) Create(c droplet.Context) (interface{}, error) { input := c.Input().(*entity.Route) + //check depend if input.ServiceID != "" { _, err := h.upstreamStore.Get(input.ServiceID) if err != nil { @@ -119,6 +170,25 @@ func (h *Handler) Create(c droplet.Context) (interface{}, error) { } } + if input.Script != nil { + if input.ID == "" { + input.ID = utils.GetFlakeUidStr() + } + script := &entity.Script{} + script.ID = input.ID + script.Script = input.Script + //to lua + var err error + input.Script, err = generateLuaCode(input.Script.(map[string]interface{})) + if err != nil { + return nil, err + } + //save original conf + if err = h.scriptStore.Create(c.Context(), script); err != nil { + return nil, err + } + } + if err := h.routeStore.Create(c.Context(), input); err != nil { return nil, err } @@ -135,6 +205,42 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { input := c.Input().(*UpdateInput) input.Route.ID = input.ID + //check depend + if input.ServiceID != "" { + _, err := h.upstreamStore.Get(input.ServiceID) + if err != nil { + if err == data.ErrNotFound { + return nil, fmt.Errorf("service id: %s not found", input.ServiceID) + } + return nil, err + } + } + if input.UpstreamID != "" { + _, err := h.upstreamStore.Get(input.ServiceID) + if err != nil { + if err == data.ErrNotFound { + return nil, fmt.Errorf("upstream id: %s not found", input.UpstreamID) + } + return nil, err + } + } + + if input.Script != nil { + script := entity.Script{} + script.ID = input.ID + script.Script = input.Script + //to lua + var err error + input.Route.Script, err = generateLuaCode(input.Script.(map[string]interface{})) + if err != nil { + return nil, err + } + //save original conf + if err = h.scriptStore.Create(c.Context(), script); err != nil { + return nil, err + } + } + if err := h.routeStore.Update(c.Context(), &input.Route); err != nil { return nil, err } @@ -143,7 +249,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { } type BatchDelete struct { - IDs string `auto_read:"ids,query"` + IDs string `auto_read:"ids,path"` } func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { @@ -155,3 +261,49 @@ func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { return nil, nil } + +type ExistInput struct { + Name string `auto_read:"name,query"` +} + +func toRows(list *store.ListOutput) []store.Row { + rows := make([]store.Row, list.TotalSize) + for i := range list.Rows { + rows[i] = list.Rows[i].(*entity.Route) + } + return rows +} + +func Exist(c *gin.Context) (interface{}, error) { + //input := c.Input().(*ExistInput) + + //temporary + name := c.Query("name") + exclude := c.Query("exclude") + routeStore := store.GetStore(store.HubKeyRoute) + + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + + if err != nil { + return nil, err + } + + sort := store.NewSort(nil) + filter := store.NewFilter([]string{"name", name}) + pagination := store.NewPagination(0, 0) + query := store.NewQuery(sort, filter, pagination) + rows := store.NewFilterSelector(toRows(ret), query) + + if len(rows) > 0 { + r := rows[0].(*entity.Route) + if r.ID != exclude { + return nil, consts.InvalidParam("Route name is reduplicate") + } + } + + return nil, nil +} diff --git a/api/internal/handler/service/service.go b/api/internal/handler/service/service.go index ac4619a..47689ee 100644 --- a/api/internal/handler/service/service.go +++ b/api/internal/handler/service/service.go @@ -53,7 +53,7 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { wrapper.InputType(reflect.TypeOf(UpdateInput{})))) r.PATCH("/apisix/admin/services/:id", wgin.Wraps(h.Patch, wrapper.InputType(reflect.TypeOf(UpdateInput{})))) - r.DELETE("/apisix/admin/services", wgin.Wraps(h.BatchDelete, + r.DELETE("/apisix/admin/services/:ids", wgin.Wraps(h.BatchDelete, wrapper.InputType(reflect.TypeOf(BatchDelete{})))) } @@ -120,7 +120,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { } type BatchDelete struct { - IDs string `auto_read:"ids,query"` + IDs string `auto_read:"ids,path"` } func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { diff --git a/api/internal/handler/ssl/ssl.go b/api/internal/handler/ssl/ssl.go index b7bd4bb..641ddee 100644 --- a/api/internal/handler/ssl/ssl.go +++ b/api/internal/handler/ssl/ssl.go @@ -19,9 +19,11 @@ package ssl import ( "crypto/tls" "crypto/x509" + "encoding/json" "encoding/pem" "errors" "fmt" + "log" "reflect" "strings" @@ -35,6 +37,7 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/handler" + "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { @@ -54,14 +57,16 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { wrapper.InputType(reflect.TypeOf(ListInput{})))) r.POST("/apisix/admin/ssl", wgin.Wraps(h.Create, wrapper.InputType(reflect.TypeOf(entity.SSL{})))) - r.POST("/apisix/admin/check_ssl_cert", wgin.Wraps(h.Validate, - wrapper.InputType(reflect.TypeOf(entity.SSL{})))) r.PUT("/apisix/admin/ssl/:id", wgin.Wraps(h.Update, wrapper.InputType(reflect.TypeOf(UpdateInput{})))) r.PATCH("/apisix/admin/ssl/:id", wgin.Wraps(h.Patch, wrapper.InputType(reflect.TypeOf(UpdateInput{})))) - r.DELETE("/apisix/admin/ssl", wgin.Wraps(h.BatchDelete, + r.DELETE("/apisix/admin/ssl/:ids", wgin.Wraps(h.BatchDelete, wrapper.InputType(reflect.TypeOf(BatchDelete{})))) + r.POST("/apisix/admin/check_ssl_cert", wgin.Wraps(h.Validate, + wrapper.InputType(reflect.TypeOf(entity.SSL{})))) + + r.POST("/apisix/admin/check_ssl_exists", consts.ErrorWrapper(Exist)) } type GetInput struct { @@ -71,11 +76,17 @@ type GetInput struct { func (h *Handler) Get(c droplet.Context) (interface{}, error) { input := c.Input().(*GetInput) - r, err := h.sslStore.Get(input.ID) + ret, err := h.sslStore.Get(input.ID) if err != nil { return nil, err } - return r, nil + + //format respond + ssl := ret.(*entity.SSL) + ssl.Key = "" + ssl.Keys = nil + + return ssl, nil } type ListInput struct { @@ -100,19 +111,31 @@ func (h *Handler) List(c droplet.Context) (interface{}, error) { return nil, err } + //format respond + var list []interface{} + var ssl *entity.SSL + for _, item := range ret.Rows { + ssl = item.(*entity.SSL) + ssl.Key = "" + ssl.Keys = nil + list = append(list, ssl) + } + if list == nil { + list = []interface{}{} + } + ret.Rows = list + return ret, nil } func (h *Handler) Create(c droplet.Context) (interface{}, error) { input := c.Input().(*entity.SSL) - ssl, err := ParseCert(input.Cert, input.Key) if err != nil { return nil, err } ssl.ID = input.ID - if err := h.sslStore.Create(c.Context(), ssl); err != nil { return nil, err } @@ -127,9 +150,14 @@ type UpdateInput struct { func (h *Handler) Update(c droplet.Context) (interface{}, error) { input := c.Input().(*UpdateInput) - input.SSL.ID = input.ID + ssl, err := ParseCert(input.Cert, input.Key) + if err != nil { + return nil, err + } - if err := h.sslStore.Update(c.Context(), &input.SSL); err != nil { + ssl.ID = input.ID + log.Println("ssl", ssl) + if err := h.sslStore.Update(c.Context(), ssl); err != nil { return nil, err } @@ -177,7 +205,7 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) { } type BatchDelete struct { - Ids string `auto_read:"ids,query"` + Ids string `auto_read:"ids,path"` } func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { @@ -262,3 +290,78 @@ func (h *Handler) Validate(c droplet.Context) (interface{}, error) { return ssl, nil } + +type ExistInput struct { + Name string `auto_read:"name,query"` +} + +func toRows(list *store.ListOutput) []store.Row { + rows := make([]store.Row, list.TotalSize) + for i := range list.Rows { + rows[i] = list.Rows[i].(*entity.SSL) + } + return rows +} + +func checkValueExists(rows []store.Row, field, value string) bool { + selector := store.Selector{ + List: rows, + Query: &store.Query{Filter: store.NewFilter([]string{field, value})}, + } + + list := selector.Filter().List + + return len(list) > 0 +} + +func checkSniExists(rows []store.Row, sni string) bool { + if res := checkValueExists(rows, "sni", sni); res { + return true + } + if res := checkValueExists(rows, "snis", sni); res { + return true + } + //extensive domain + firstDot := strings.Index(sni, ".") + if firstDot > 0 && sni[0:1] != "*" { + sni = "*" + sni[firstDot:] + if res := checkValueExists(rows, "sni", sni); res { + return true + } + if res := checkValueExists(rows, "snis", sni); res { + return true + } + } + + return false +} + +func Exist(c *gin.Context) (interface{}, error) { + //input := c.Input().(*ExistInput) + //temporary + reqBody, _ := c.GetRawData() + var hosts []string + if err := json.Unmarshal(reqBody, &hosts); err != nil { + return nil, err + } + + routeStore := store.GetStore(store.HubKeySsl) + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + + if err != nil { + return nil, err + } + + for _, host := range hosts { + res := checkSniExists(toRows(ret), host) + if !res { + return nil, consts.InvalidParam("SSL cert not exists for sniļ¼" + host) + } + } + + return nil, nil +} diff --git a/api/internal/handler/upstream/upstream.go b/api/internal/handler/upstream/upstream.go index 9efb944..3b24ade 100644 --- a/api/internal/handler/upstream/upstream.go +++ b/api/internal/handler/upstream/upstream.go @@ -30,6 +30,7 @@ import ( "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/internal/handler" + "github.com/apisix/manager-api/internal/utils/consts" ) type Handler struct { @@ -53,8 +54,11 @@ func (h *Handler) ApplyRoute(r *gin.Engine) { wrapper.InputType(reflect.TypeOf(UpdateInput{})))) r.PATCH("/apisix/admin/upstreams/:id", wgin.Wraps(h.Patch, wrapper.InputType(reflect.TypeOf(UpdateInput{})))) - r.DELETE("/apisix/admin/upstreams", wgin.Wraps(h.BatchDelete, + r.DELETE("/apisix/admin/upstreams/:ids", wgin.Wraps(h.BatchDelete, wrapper.InputType(reflect.TypeOf(BatchDelete{})))) + + r.GET("/apisix/admin/notexist/upstreams", consts.ErrorWrapper(Exist)) + r.GET("/apisix/admin/names/upstreams", consts.ErrorWrapper(listUpstreamNames)) } type GetInput struct { @@ -123,7 +127,7 @@ func (h *Handler) Update(c droplet.Context) (interface{}, error) { } type BatchDelete struct { - IDs string `auto_read:"ids,query"` + IDs string `auto_read:"ids,path"` } func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { @@ -175,3 +179,76 @@ func (h *Handler) Patch(c droplet.Context) (interface{}, error) { return nil, nil } + +type ExistInput struct { + Name string `auto_read:"name,query"` +} + +func toRows(list *store.ListOutput) []store.Row { + rows := make([]store.Row, list.TotalSize) + for i := range list.Rows { + rows[i] = list.Rows[i].(*entity.Upstream) + } + return rows +} + +func Exist(c *gin.Context) (interface{}, error) { + //input := c.Input().(*ExistInput) + + //temporary + name := c.Query("name") + exclude := c.Query("exclude") + routeStore := store.GetStore(store.HubKeyUpstream) + + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + + if err != nil { + return nil, err + } + + sort := store.NewSort(nil) + filter := store.NewFilter([]string{"name", name}) + pagination := store.NewPagination(0, 0) + query := store.NewQuery(sort, filter, pagination) + rows := store.NewFilterSelector(toRows(ret), query) + + if len(rows) > 0 { + r := rows[0].(*entity.Upstream) + if r.ID != exclude { + return nil, consts.InvalidParam("Upstream name is reduplicate") + } + } + + return nil, nil +} + +func listUpstreamNames(c *gin.Context) (interface{}, error) { + routeStore := store.GetStore(store.HubKeyUpstream) + + ret, err := routeStore.List(store.ListInput{ + Predicate: nil, + PageSize: 0, + PageNumber: 0, + }) + + if err != nil { + return nil, err + } + + rows := make([]interface{}, ret.TotalSize) + for i := range ret.Rows { + row := ret.Rows[i].(*entity.Upstream) + rows[i], _ = row.Parse2NameResponse() + } + + output := &store.ListOutput{ + Rows: rows, + TotalSize: ret.TotalSize, + } + + return output, nil +} diff --git a/api/internal/core/store/storehub.go b/api/internal/utils/consts/api_error.go similarity index 52% copy from api/internal/core/store/storehub.go copy to api/internal/utils/consts/api_error.go index 203c5bc..cd9bc32 100644 --- a/api/internal/core/store/storehub.go +++ b/api/internal/utils/consts/api_error.go @@ -14,44 +14,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package store +package consts import ( - "fmt" - "github.com/apisix/manager-api/internal/utils" + "github.com/gin-gonic/gin" + "net/http" ) -type HubKey string +type WrapperHandle func(c *gin.Context) (interface{}, error) -const ( - HubKeyConsumer HubKey = "consumer" - HubKeyRoute HubKey = "route" - HubKeyService HubKey = "service" - HubKeySsl HubKey = "ssl" - HubKeyUpstream HubKey = "upstream" -) +func ErrorWrapper(handle WrapperHandle) gin.HandlerFunc { + return func(c *gin.Context) { + data, err := handle(c) + if err != nil { + apiError := err.(*ApiError) + c.JSON(apiError.Status, apiError) + return + } + c.JSON(http.StatusOK, gin.H{"data": data, "code": 200, "message": "success"}) + } +} -var ( - storeHub = map[HubKey]*GenericStore{} -) +type ApiError struct { + Status int `json:"-"` + Code int `json:"code"` + Message string `json:"message"` +} -func InitStore(key HubKey, opt GenericStoreOption) error { - s, err := NewGenericStore(opt) - if err != nil { - return err - } - if err := s.Init(); err != nil { - return err - } +func (err ApiError) Error() string { + return err.Message +} - utils.AppendToClosers(s.Close) - storeHub[key] = s - return nil +func InvalidParam(message string) *ApiError { + return &ApiError{400, 400, message} } -func GetStore(key HubKey) *GenericStore { - if s, ok := storeHub[key]; ok { - return s - } - panic(fmt.Sprintf("no store with key: %s", key)) +func SystemError(message string) *ApiError { + return &ApiError{500, 500, message} } diff --git a/api/main.go b/api/main.go index 36541b7..937243e 100644 --- a/api/main.go +++ b/api/main.go @@ -18,20 +18,19 @@ package main import ( "fmt" - "github.com/apisix/manager-api/internal/core/entity" - "github.com/apisix/manager-api/internal/core/storage" - "github.com/apisix/manager-api/internal/core/store" - "github.com/apisix/manager-api/internal/utils" "github.com/spf13/viper" "net/http" - "reflect" "strings" "time" + dlog "github.com/shiningrush/droplet/log" + "github.com/apisix/manager-api/conf" + "github.com/apisix/manager-api/internal/core/storage" + "github.com/apisix/manager-api/internal/core/store" + "github.com/apisix/manager-api/internal/utils" "github.com/apisix/manager-api/log" "github.com/apisix/manager-api/route" - dlog "github.com/shiningrush/droplet/log" ) var logger = log.GetLogger() @@ -44,7 +43,7 @@ func main() { if err := storage.InitETCDClient(strings.Split(viper.GetString("etcd_endpoints"), ",")); err != nil { panic(err) } - if err := initStores(); err != nil { + if err := store.InitStores(); err != nil { panic(err) } @@ -65,66 +64,3 @@ func main() { utils.CloseAll() } - -func initStores() error { - err := store.InitStore(store.HubKeyConsumer, store.GenericStoreOption{ - BasePath: "/apisix/consumers", - ObjType: reflect.TypeOf(entity.Consumer{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Consumer) - return r.Username - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyRoute, store.GenericStoreOption{ - BasePath: "/apisix/routes", - ObjType: reflect.TypeOf(entity.Route{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Route) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyService, store.GenericStoreOption{ - BasePath: "/apisix/services", - ObjType: reflect.TypeOf(entity.Service{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Service) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeySsl, store.GenericStoreOption{ - BasePath: "/apisix/ssl", - ObjType: reflect.TypeOf(entity.SSL{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.SSL) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyUpstream, store.GenericStoreOption{ - BasePath: "/apisix/upstreams", - ObjType: reflect.TypeOf(entity.Upstream{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Upstream) - return r.ID - }, - }) - if err != nil { - return err - } - return nil -} diff --git a/api/route/base_test.go b/api/route/base_test.go index fdfd6f9..838f4b7 100644 --- a/api/route/base_test.go +++ b/api/route/base_test.go @@ -17,14 +17,12 @@ package route import ( - "reflect" "strings" "github.com/api7/apitest" dlog "github.com/shiningrush/droplet/log" "github.com/spf13/viper" - "github.com/apisix/manager-api/internal/core/entity" "github.com/apisix/manager-api/internal/core/storage" "github.com/apisix/manager-api/internal/core/store" "github.com/apisix/manager-api/log" @@ -46,7 +44,7 @@ func init() { panic(err) } - if err := initStores(); err != nil { + if err := store.InitStores(); err != nil { panic(err) } @@ -56,66 +54,3 @@ func init() { New(). Handler(r) } - -func initStores() error { - err := store.InitStore(store.HubKeyConsumer, store.GenericStoreOption{ - BasePath: "/apisix/consumers", - ObjType: reflect.TypeOf(entity.Consumer{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Consumer) - return r.Username - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyRoute, store.GenericStoreOption{ - BasePath: "/apisix/routes", - ObjType: reflect.TypeOf(entity.Route{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Route) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyService, store.GenericStoreOption{ - BasePath: "/apisix/services", - ObjType: reflect.TypeOf(entity.Service{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Service) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeySsl, store.GenericStoreOption{ - BasePath: "/apisix/ssl", - ObjType: reflect.TypeOf(entity.SSL{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.SSL) - return r.ID - }, - }) - if err != nil { - return err - } - - err = store.InitStore(store.HubKeyUpstream, store.GenericStoreOption{ - BasePath: "/apisix/upstreams", - ObjType: reflect.TypeOf(entity.Upstream{}), - KeyFunc: func(obj interface{}) string { - r := obj.(*entity.Upstream) - return r.ID - }, - }) - if err != nil { - return err - } - return nil -} diff --git a/api/route/route_test.go b/api/route/route_test.go new file mode 100644 index 0000000..5b66679 --- /dev/null +++ b/api/route/route_test.go @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 route + +import ( + "net/http" + "testing" +) + +func TestRoute(t *testing.T) { + // create ok + + testHandler. + Post(uriPrefix + "/routes"). + JSON(`{ + "id": "11", + "name": "e2e-test-route1", + "desc": "route created by java sdk", + "priority": 0, + "methods": [ + "GET" + ], + "uris": [ + "/helloworld", + "/hello2*" + ], + "hosts": [ + "s.com" + ], + "protocols": [ + "http", + "https", + "websocket" + ], + "redirect":{ + "code": 302, + "uri": "/hello" + }, + "vars": [ + ["arg_name", "==", "json"], + ["arg_age", ">", "18"], + ["arg_address", "~~", "China.*"] + ], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 100 + }, + "timeout": { + "connect":15, + "send":15, + "read":15 + } + }, + "upstream_protocol": "keep", + "upstream_path": { + "type" : "static", + "from": "", + "to": "/hello" + }, + "upstream_header": { + "header_name1": "header_value1", + "header_name2": "header_value2" + }, + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + }, + "prometheus": {} + } + }`). + Headers(map[string]string{"Authorization": token}). + Expect(t). + Status(http.StatusOK). + End() + + //update ok + testHandler. + Put(uriPrefix + "/routes/11"). + JSON(`{ + "id": "11", + "name": "e2e-test-route1", + "desc": "route created by java sdk", + "priority": 0, + "methods": [ + "GET" + ], + "uris": [ + "/helloworld", + "/hello2*" + ], + "hosts": [ + "s.com" + ], + "protocols": [ + "http", + "https", + "websocket" + ], + "redirect":{ + "code": 302, + "uri": "/hello" + }, + "vars": [ + ["arg_name", "==", "json"], + ["arg_age", ">", "18"], + ["arg_address", "~~", "China.*"] + ], + "upstream": { + "type": "roundrobin", + "nodes": { + "39.97.63.215:80": 100 + }, + "timeout": { + "connect":15, + "send":15, + "read":15 + } + }, + "upstream_protocol": "keep", + "upstream_path": { + "type" : "static", + "from": "", + "to": "/hello" + }, + "upstream_header": { + "header_name1": "header_value1", + "header_name2": "header_value2" + }, + "plugins": { + "limit-count": { + "count": 2, + "time_window": 60, + "rejected_code": 503, + "key": "remote_addr" + }, + "prometheus": {} + } + }`). + Expect(t). + Status(http.StatusOK). + End() + + //list + testHandler. + Get(uriPrefix + "/routes"). + Headers(map[string]string{"Authorization": token}). + Expect(t). + Status(http.StatusOK). + End() + + //not exist + testHandler. + Get(uriPrefix + "/notexist/routes"). + //Query("name", "notexists"). + QueryCollection(map[string][]string{"name": {"notexists"}}). + Headers(map[string]string{"Authorization": token}). + Expect(t). + Status(http.StatusOK). + End() + + //existed todo: fix bug + //testHandler. + // Get(uriPrefix + "/notexist/routes"). + // QueryCollection(map[string][]string{"name": {""}}). + // Headers(map[string]string{"Authorization": token}). + // Expect(t). + // Status(http.StatusBadRequest). + // End() + + //delete + testHandler. + Delete(uriPrefix + "/routes/11"). + Expect(t). + Status(http.StatusOK). + End() + +}