mirror of https://github.com/kubevela/kubevela.git
Compare commits
3 Commits
ebf73d03c2
...
21d9d24b07
| Author | SHA1 | Date |
|---|---|---|
|
|
21d9d24b07 | |
|
|
3f5b698dac | |
|
|
4b1d1601c8 |
|
|
@ -20,6 +20,8 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -41,3 +43,13 @@ func TestGetAvailableVersion(t *testing.T) {
|
|||
assert.NotEmpty(t, err)
|
||||
assert.Equal(t, version, "")
|
||||
}
|
||||
|
||||
func TestWrapErrRateLimit(t *testing.T) {
|
||||
regularErr := errors.New("regular error")
|
||||
wrappedErr := WrapErrRateLimit(regularErr)
|
||||
assert.Equal(t, regularErr, wrappedErr)
|
||||
|
||||
rateLimitErr := &github.RateLimitError{}
|
||||
wrappedErr = WrapErrRateLimit(rateLimitErr)
|
||||
assert.Equal(t, ErrRateLimit, wrappedErr)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package addon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
)
|
||||
|
||||
var files = []*loader.BufferedFile{
|
||||
{
|
||||
Name: "metadata.yaml",
|
||||
Data: []byte(`name: test-helm-addon
|
||||
version: 1.0.0
|
||||
description: This is a addon for test when install addon from helm repo
|
||||
icon: https://www.terraform.io/assets/images/logo-text-8c3ba8a6.svg
|
||||
url: https://terraform.io/
|
||||
|
||||
tags: []
|
||||
|
||||
deployTo:
|
||||
control_plane: true
|
||||
runtime_cluster: false
|
||||
|
||||
dependencies: []
|
||||
|
||||
invisible: false`),
|
||||
},
|
||||
{
|
||||
Name: "/resources/parameter.cue",
|
||||
Data: []byte(`parameter: {
|
||||
// test wrong parameter
|
||||
example: *"default"
|
||||
}`),
|
||||
},
|
||||
}
|
||||
|
||||
func TestMemoryReader(t *testing.T) {
|
||||
m := MemoryReader{
|
||||
Name: "fluxcd",
|
||||
Files: files,
|
||||
}
|
||||
|
||||
meta, err := m.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(meta["fluxcd"].Items), 2)
|
||||
|
||||
metaFile, err := m.ReadFile("metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, metaFile)
|
||||
|
||||
parameterData, err := m.ReadFile("/resources/parameter.cue")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, parameterData)
|
||||
}
|
||||
|
|
@ -97,3 +97,155 @@ func TestGiteeReader(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestNewGiteeClient(t *testing.T) {
|
||||
defaultURL, _ := url.Parse(DefaultGiteeURL)
|
||||
|
||||
testCases := map[string]struct {
|
||||
httpClient *http.Client
|
||||
baseURL *url.URL
|
||||
wantClient *http.Client
|
||||
wantURL *url.URL
|
||||
}{
|
||||
"Nil inputs": {
|
||||
httpClient: nil,
|
||||
baseURL: nil,
|
||||
wantClient: &http.Client{},
|
||||
wantURL: defaultURL,
|
||||
},
|
||||
"Custom inputs": {
|
||||
httpClient: &http.Client{Timeout: 10},
|
||||
baseURL: &url.URL{Host: "my-gitee.com"},
|
||||
wantClient: &http.Client{Timeout: 10},
|
||||
wantURL: &url.URL{Host: "my-gitee.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
client := NewGiteeClient(tc.httpClient, tc.baseURL)
|
||||
assert.Equal(t, tc.wantClient.Timeout, client.Client.Timeout)
|
||||
assert.Equal(t, tc.wantURL.Host, client.BaseURL.Host)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiteeReaderRelativePath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
basePath string
|
||||
itemPath string
|
||||
expectedPath string
|
||||
}{
|
||||
"No base path": {
|
||||
basePath: "",
|
||||
itemPath: "fluxcd/metadata.yaml",
|
||||
expectedPath: "fluxcd/metadata.yaml",
|
||||
},
|
||||
"With base path": {
|
||||
basePath: "addons",
|
||||
itemPath: "addons/fluxcd/metadata.yaml",
|
||||
expectedPath: "fluxcd/metadata.yaml",
|
||||
},
|
||||
"With deep base path": {
|
||||
basePath: "official/addons",
|
||||
itemPath: "official/addons/fluxcd/template.cue",
|
||||
expectedPath: "fluxcd/template.cue",
|
||||
},
|
||||
"Item at root of base path": {
|
||||
basePath: "addons",
|
||||
itemPath: "addons/README.md",
|
||||
expectedPath: "README.md",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gith := &giteeHelper{
|
||||
Meta: &utils.Content{GiteeContent: utils.GiteeContent{
|
||||
Path: tc.basePath,
|
||||
}},
|
||||
}
|
||||
r := &giteeReader{h: gith}
|
||||
item := &github.RepositoryContent{Path: &tc.itemPath}
|
||||
|
||||
result := r.RelativePath(item)
|
||||
assert.Equal(t, tc.expectedPath, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiteeReader_ListAddonMeta(t *testing.T) {
|
||||
client, mux, teardown := giteeSetup()
|
||||
defer teardown()
|
||||
|
||||
giteePattern := "/repos/o/r/contents/"
|
||||
mux.HandleFunc(giteePattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
var contents []*github.RepositoryContent
|
||||
queryPath := strings.TrimPrefix(req.URL.Path, giteePattern)
|
||||
|
||||
switch queryPath {
|
||||
case "": // Root directory
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("dir"), Name: String("fluxcd"), Path: String("fluxcd")},
|
||||
{Type: String("dir"), Name: String("velaux"), Path: String("velaux")},
|
||||
{Type: String("file"), Name: String("README.md"), Path: String("README.md")},
|
||||
}
|
||||
case "fluxcd":
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("file"), Name: String("metadata.yaml"), Path: String("fluxcd/metadata.yaml")},
|
||||
{Type: String("dir"), Name: String("resources"), Path: String("fluxcd/resources")},
|
||||
}
|
||||
case "fluxcd/resources":
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("file"), Name: String("parameter.cue"), Path: String("fluxcd/resources/parameter.cue")},
|
||||
}
|
||||
case "velaux":
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("file"), Name: String("metadata.yaml"), Path: String("velaux/metadata.yaml")},
|
||||
}
|
||||
default:
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
res, _ := json.Marshal(contents)
|
||||
rw.Write(res)
|
||||
})
|
||||
|
||||
gith := &giteeHelper{
|
||||
Client: client,
|
||||
Meta: &utils.Content{GiteeContent: utils.GiteeContent{
|
||||
Owner: "o",
|
||||
Repo: "r",
|
||||
}},
|
||||
}
|
||||
r := &giteeReader{h: gith}
|
||||
|
||||
meta, err := r.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, meta)
|
||||
assert.Equal(t, 2, len(meta), "Expected to find 2 addons, root files should be ignored")
|
||||
|
||||
t.Run("fluxcd addon discovery", func(t *testing.T) {
|
||||
addon, ok := meta["fluxcd"]
|
||||
assert.True(t, ok, "fluxcd addon should be discovered")
|
||||
assert.Equal(t, "fluxcd", addon.Name)
|
||||
|
||||
// Should find 2 items recursively: metadata.yaml and resources/parameter.cue
|
||||
assert.Equal(t, 2, len(addon.Items), "fluxcd should contain 2 files")
|
||||
|
||||
foundPaths := make(map[string]bool)
|
||||
for _, item := range addon.Items {
|
||||
foundPaths[item.GetPath()] = true
|
||||
}
|
||||
assert.True(t, foundPaths["fluxcd/metadata.yaml"], "should find fluxcd/metadata.yaml")
|
||||
assert.True(t, foundPaths["fluxcd/resources/parameter.cue"], "should find fluxcd/resources/parameter.cue")
|
||||
})
|
||||
|
||||
t.Run("velaux addon discovery", func(t *testing.T) {
|
||||
addon, ok := meta["velaux"]
|
||||
assert.True(t, ok, "velaux addon should be discovered")
|
||||
assert.Equal(t, "velaux", addon.Name)
|
||||
assert.Equal(t, 1, len(addon.Items), "velaux should contain 1 file")
|
||||
assert.Equal(t, "velaux/metadata.yaml", addon.Items[0].GetPath())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,8 +64,10 @@ func setup() (client *github.Client, mux *http.ServeMux, teardown func()) {
|
|||
return client, mux, server.Close
|
||||
}
|
||||
|
||||
func TestGitHubReader(t *testing.T) {
|
||||
func TestGitHubReader_ReadFile(t *testing.T) {
|
||||
client, mux, teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
githubPattern := "/repos/o/r/contents/"
|
||||
mux.HandleFunc(githubPattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
queryPath := strings.TrimPrefix(req.URL.Path, githubPattern)
|
||||
|
|
@ -76,6 +78,7 @@ func TestGitHubReader(t *testing.T) {
|
|||
content := &github.RepositoryContent{Type: String("file"), Name: String(path.Base(queryPath)), Size: Int(len(file)), Encoding: String(""), Path: String(queryPath), Content: String(string(file))}
|
||||
res, _ := json.Marshal(content)
|
||||
rw.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, it could be directory
|
||||
|
|
@ -91,11 +94,11 @@ func TestGitHubReader(t *testing.T) {
|
|||
}
|
||||
dRes, _ := json.Marshal(contents)
|
||||
rw.Write(dRes)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Write([]byte("invalid github query"))
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
defer teardown()
|
||||
|
||||
gith := &gitHelper{
|
||||
Client: client,
|
||||
|
|
@ -107,7 +110,95 @@ func TestGitHubReader(t *testing.T) {
|
|||
var r AsyncReader = &gitReader{gith}
|
||||
_, err := r.ReadFile("example/metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGitReader_RelativePath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
basePath string
|
||||
itemPath string
|
||||
expectedPath string
|
||||
}{
|
||||
"No base path": {
|
||||
basePath: "",
|
||||
itemPath: "fluxcd/metadata.yaml",
|
||||
expectedPath: "fluxcd/metadata.yaml",
|
||||
},
|
||||
"With base path": {
|
||||
basePath: "addons",
|
||||
itemPath: "addons/fluxcd/metadata.yaml",
|
||||
expectedPath: "fluxcd/metadata.yaml",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
gith := &gitHelper{
|
||||
Meta: &utils.Content{GithubContent: utils.GithubContent{
|
||||
Path: tc.basePath,
|
||||
}},
|
||||
}
|
||||
r := &gitReader{h: gith}
|
||||
item := &github.RepositoryContent{Path: &tc.itemPath}
|
||||
|
||||
result := r.RelativePath(item)
|
||||
assert.Equal(t, tc.expectedPath, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitReader_ListAddonMeta(t *testing.T) {
|
||||
client, mux, teardown := setup()
|
||||
defer teardown()
|
||||
|
||||
githubPattern := "/repos/o/r/contents/"
|
||||
mux.HandleFunc(githubPattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
var contents []*github.RepositoryContent
|
||||
queryPath := strings.TrimPrefix(req.URL.Path, githubPattern)
|
||||
|
||||
switch queryPath {
|
||||
case "": // Root directory
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("dir"), Name: String("fluxcd"), Path: String("fluxcd")},
|
||||
{Type: String("file"), Name: String("README.md"), Path: String("README.md")},
|
||||
}
|
||||
case "fluxcd":
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("file"), Name: String("metadata.yaml"), Path: String("fluxcd/metadata.yaml")},
|
||||
{Type: String("dir"), Name: String("resources"), Path: String("fluxcd/resources")},
|
||||
{Type: String("file"), Name: String("template.cue"), Path: String("fluxcd/template.cue")},
|
||||
}
|
||||
case "fluxcd/resources":
|
||||
contents = []*github.RepositoryContent{
|
||||
{Type: String("file"), Name: String("parameter.cue"), Path: String("fluxcd/resources/parameter.cue")},
|
||||
}
|
||||
default:
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
res, _ := json.Marshal(contents)
|
||||
rw.Write(res)
|
||||
})
|
||||
|
||||
gith := &gitHelper{
|
||||
Client: client,
|
||||
Meta: &utils.Content{GithubContent: utils.GithubContent{
|
||||
Owner: "o",
|
||||
Repo: "r",
|
||||
}},
|
||||
}
|
||||
r := &gitReader{h: gith}
|
||||
|
||||
meta, err := r.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, meta)
|
||||
assert.Equal(t, 1, len(meta), "Expected to find 1 addon, root files should be ignored")
|
||||
|
||||
t.Run("fluxcd addon discovery", func(t *testing.T) {
|
||||
addon, ok := meta["fluxcd"]
|
||||
assert.True(t, ok, "fluxcd addon should be discovered")
|
||||
assert.Equal(t, "fluxcd", addon.Name)
|
||||
assert.Equal(t, 3, len(addon.Items), "fluxcd should contain 3 files")
|
||||
})
|
||||
}
|
||||
|
||||
// Int is a helper routine that allocates a new int value
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ func (g GitLabItem) GetType() string {
|
|||
|
||||
// GetPath get addon's sub item path
|
||||
func (g GitLabItem) GetPath() string {
|
||||
if g.basePath == "" {
|
||||
return g.path
|
||||
}
|
||||
return g.path[len(g.basePath)+1:]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"strings"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -33,81 +32,190 @@ import (
|
|||
|
||||
var baseUrl = "/api/v4"
|
||||
|
||||
func gitlabSetup() (client *gitlab.Client, mux *http.ServeMux, teardown func()) {
|
||||
// mux is the HTTP request multiplexer used with the test server.
|
||||
mux = http.NewServeMux()
|
||||
|
||||
func gitlabSetup(t *testing.T) (*gitlab.Client, *http.ServeMux, func()) {
|
||||
mux := http.NewServeMux()
|
||||
apiHandler := http.NewServeMux()
|
||||
apiHandler.Handle(baseUrl+"/", http.StripPrefix(baseUrl, mux))
|
||||
|
||||
// server is a test HTTP server used to provide mock API responses.
|
||||
server := httptest.NewServer(apiHandler)
|
||||
|
||||
// client is the Gitlab client being tested and is
|
||||
// configured to use test server.
|
||||
client, err := gitlab.NewClient("", gitlab.WithBaseURL(server.URL+baseUrl+"/"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
|
||||
return client, mux, server.Close
|
||||
}
|
||||
|
||||
func TestGitlabReader(t *testing.T) {
|
||||
client, mux, teardown := gitlabSetup()
|
||||
gitlabPattern := "/projects/9999/repository/files/"
|
||||
mux.HandleFunc(gitlabPattern, func(rw http.ResponseWriter, req *http.Request) {
|
||||
queryPath := strings.TrimPrefix(req.URL.Path, gitlabPattern)
|
||||
localPath := path.Join(testdataPrefix, queryPath)
|
||||
file, err := testdata.ReadFile(localPath)
|
||||
// test if it's a file
|
||||
if err == nil {
|
||||
content := &gitlab.File{
|
||||
FilePath: localPath,
|
||||
FileName: path.Base(queryPath),
|
||||
Size: *Int(len(file)),
|
||||
Encoding: "base64",
|
||||
Ref: "master",
|
||||
Content: base64.StdEncoding.EncodeToString(file),
|
||||
}
|
||||
res, _ := json.Marshal(content)
|
||||
rw.Write(res)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise, it could be directory
|
||||
dir, err := testdata.ReadDir(localPath)
|
||||
if err == nil {
|
||||
contents := make([]*gitlab.TreeNode, 0)
|
||||
for _, item := range dir {
|
||||
tp := "file"
|
||||
if item.IsDir() {
|
||||
tp = "dir"
|
||||
}
|
||||
contents = append(contents, &gitlab.TreeNode{
|
||||
ID: "",
|
||||
Name: item.Name(),
|
||||
Type: tp,
|
||||
Path: localPath + "/" + item.Name(),
|
||||
Mode: "",
|
||||
})
|
||||
}
|
||||
dRes, _ := json.Marshal(contents)
|
||||
rw.Write(dRes)
|
||||
return
|
||||
}
|
||||
|
||||
rw.Write([]byte("invalid gitlab query"))
|
||||
})
|
||||
func TestGitlabReader_ReadFile(t *testing.T) {
|
||||
client, mux, teardown := gitlabSetup(t)
|
||||
defer teardown()
|
||||
|
||||
// The gitlab client URL-encodes the file path, so we must match the encoded path.
|
||||
mux.HandleFunc("/projects/9999/repository/files/example%2Fmetadata.yaml", func(rw http.ResponseWriter, req *http.Request) {
|
||||
content := &gitlab.File{
|
||||
Content: base64.StdEncoding.EncodeToString([]byte("hello world")),
|
||||
}
|
||||
res, err := json.Marshal(content)
|
||||
assert.NoError(t, err)
|
||||
_, err = rw.Write(res)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
mux.HandleFunc("/projects/9999/repository/files/example%2Fnot%2Ffound.yaml", func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
|
||||
gith := &gitlabHelper{
|
||||
Client: client,
|
||||
Meta: &utils.Content{GitlabContent: utils.GitlabContent{PId: 9999, Path: "example"}},
|
||||
}
|
||||
var r AsyncReader = &gitlabReader{h: gith}
|
||||
|
||||
t.Run("success case", func(t *testing.T) {
|
||||
content, err := r.ReadFile("metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", content)
|
||||
})
|
||||
|
||||
t.Run("not found case", func(t *testing.T) {
|
||||
_, err := r.ReadFile("not/found.yaml")
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitlabReader_ListAddonMeta(t *testing.T) {
|
||||
client, mux, teardown := gitlabSetup(t)
|
||||
defer teardown()
|
||||
|
||||
projectID := 9999
|
||||
projectPath := "addons"
|
||||
|
||||
mux.HandleFunc("/projects/"+strconv.Itoa(projectID)+"/repository/tree", func(rw http.ResponseWriter, req *http.Request) {
|
||||
pathParam := req.URL.Query().Get("path")
|
||||
pageParam := req.URL.Query().Get("page")
|
||||
if pageParam == "" {
|
||||
pageParam = "1"
|
||||
}
|
||||
|
||||
var tree []*gitlab.TreeNode
|
||||
|
||||
switch pathParam {
|
||||
case projectPath:
|
||||
rw.Header().Set("X-Total-Pages", "2")
|
||||
if pageParam == "1" {
|
||||
tree = []*gitlab.TreeNode{{ID: "1", Name: "fluxcd", Type: "tree", Path: "addons/fluxcd"}}
|
||||
} else if pageParam == "2" {
|
||||
tree = []*gitlab.TreeNode{{ID: "2", Name: "velaux", Type: "tree", Path: "addons/velaux"}}
|
||||
}
|
||||
case "addons/fluxcd":
|
||||
tree = []*gitlab.TreeNode{{ID: "3", Name: "metadata.yaml", Type: "blob", Path: "addons/fluxcd/metadata.yaml"}}
|
||||
case "addons/velaux":
|
||||
tree = []*gitlab.TreeNode{{ID: "4", Name: "template.cue", Type: "blob", Path: "addons/velaux/template.cue"}}
|
||||
default:
|
||||
rw.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
res, err := json.Marshal(tree)
|
||||
assert.NoError(t, err)
|
||||
_, err = rw.Write(res)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
gith := &gitlabHelper{
|
||||
Client: client,
|
||||
Meta: &utils.Content{GitlabContent: utils.GitlabContent{
|
||||
PId: 9999,
|
||||
PId: projectID,
|
||||
Path: projectPath,
|
||||
}},
|
||||
}
|
||||
var r AsyncReader = &gitlabReader{gith}
|
||||
_, err := r.ReadFile("example/metadata.yaml")
|
||||
r := &gitlabReader{h: gith}
|
||||
|
||||
meta, err := r.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, meta)
|
||||
assert.Equal(t, 2, len(meta))
|
||||
|
||||
expectedAddons := map[string]struct {
|
||||
itemCount int
|
||||
itemPath string
|
||||
}{
|
||||
"fluxcd": {itemCount: 1, itemPath: "fluxcd/metadata.yaml"},
|
||||
"velaux": {itemCount: 1, itemPath: "velaux/template.cue"},
|
||||
}
|
||||
|
||||
for name, expected := range expectedAddons {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
addon, ok := meta[name]
|
||||
assert.True(t, ok, "addon not found in result")
|
||||
assert.Equal(t, name, addon.Name)
|
||||
assert.Equal(t, expected.itemCount, len(addon.Items))
|
||||
assert.Equal(t, expected.itemPath, addon.Items[0].GetPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitlabReader_Getters(t *testing.T) {
|
||||
t.Run("GetRef", func(t *testing.T) {
|
||||
githWithRef := &gitlabHelper{Meta: &utils.Content{GitlabContent: utils.GitlabContent{Ref: "develop"}}}
|
||||
rWithRef := &gitlabReader{h: githWithRef}
|
||||
assert.Equal(t, "develop", rWithRef.GetRef())
|
||||
|
||||
githWithoutRef := &gitlabHelper{Meta: &utils.Content{GitlabContent: utils.GitlabContent{Ref: ""}}}
|
||||
rWithoutRef := &gitlabReader{h: githWithoutRef}
|
||||
assert.Equal(t, "master", rWithoutRef.GetRef())
|
||||
})
|
||||
|
||||
t.Run("GetProjectID and GetProjectPath", func(t *testing.T) {
|
||||
gith := &gitlabHelper{
|
||||
Meta: &utils.Content{GitlabContent: utils.GitlabContent{
|
||||
PId: 12345,
|
||||
Path: "my/project/path",
|
||||
}},
|
||||
}
|
||||
r := &gitlabReader{h: gith}
|
||||
assert.Equal(t, 12345, r.GetProjectID())
|
||||
assert.Equal(t, "my/project/path", r.GetProjectPath())
|
||||
})
|
||||
|
||||
t.Run("RelativePath", func(t *testing.T) {
|
||||
r := &gitlabReader{}
|
||||
item := &GitLabItem{
|
||||
basePath: "addons",
|
||||
path: "addons/fluxcd/metadata.yaml",
|
||||
}
|
||||
assert.Equal(t, "fluxcd/metadata.yaml", r.RelativePath(item))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitLabItem(t *testing.T) {
|
||||
t.Run("Getters", func(t *testing.T) {
|
||||
item := GitLabItem{
|
||||
tp: "blob",
|
||||
name: "metadata.yaml",
|
||||
}
|
||||
assert.Equal(t, "blob", item.GetType())
|
||||
assert.Equal(t, "metadata.yaml", item.GetName())
|
||||
})
|
||||
|
||||
t.Run("GetPath", func(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
basePath string
|
||||
fullPath string
|
||||
expected string
|
||||
}{
|
||||
"no base path": {
|
||||
basePath: "",
|
||||
fullPath: "fluxcd/metadata.yaml",
|
||||
expected: "fluxcd/metadata.yaml",
|
||||
},
|
||||
"with base path": {
|
||||
basePath: "addons",
|
||||
fullPath: "addons/fluxcd/metadata.yaml",
|
||||
expected: "fluxcd/metadata.yaml",
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
item := GitLabItem{basePath: tc.basePath, path: tc.fullPath}
|
||||
assert.Equal(t, tc.expected, item.GetPath())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package addon
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -25,17 +26,66 @@ import (
|
|||
|
||||
func TestLocalReader(t *testing.T) {
|
||||
r := localReader{name: "local", dir: "./testdata/local"}
|
||||
m, err := r.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(m["local"].Items), 2)
|
||||
|
||||
file, err := r.ReadFile("metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, file, metaFile)
|
||||
t.Run("ListAddonMeta", func(t *testing.T) {
|
||||
m, err := r.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, m["local"])
|
||||
assert.Equal(t, 2, len(m["local"].Items))
|
||||
|
||||
file, err = r.ReadFile("resources/parameter.cue")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, strings.Contains(file, parameterFile))
|
||||
// Check that the correct files are found, regardless of order.
|
||||
foundPaths := make(map[string]bool)
|
||||
for _, item := range m["local"].Items {
|
||||
// Normalize path separators for consistent checking
|
||||
foundPaths[filepath.ToSlash(item.GetPath())] = true
|
||||
}
|
||||
assert.True(t, foundPaths[filepath.ToSlash("testdata/local/metadata.yaml")])
|
||||
assert.True(t, foundPaths[filepath.ToSlash("testdata/local/resources/parameter.cue")])
|
||||
})
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
t.Run("read root file", func(t *testing.T) {
|
||||
file, err := r.ReadFile("metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, file, metaFile)
|
||||
})
|
||||
t.Run("read nested file", func(t *testing.T) {
|
||||
file, err := r.ReadFile("resources/parameter.cue")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, strings.Contains(file, parameterFile))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestLocalReader_RelativePath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
dir string
|
||||
addonName string
|
||||
itemPath string
|
||||
expected string
|
||||
}{
|
||||
"item in root": {
|
||||
dir: "./testdata/local",
|
||||
addonName: "my-addon",
|
||||
itemPath: filepath.Join("./testdata/local", "metadata.yaml"),
|
||||
expected: filepath.Join("my-addon", "metadata.yaml"),
|
||||
},
|
||||
"item in subdirectory": {
|
||||
dir: "./testdata/local",
|
||||
addonName: "my-addon",
|
||||
itemPath: filepath.Join("./testdata/local", "resources", "parameter.cue"),
|
||||
expected: filepath.Join("my-addon", "resources", "parameter.cue"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r := localReader{name: tc.addonName, dir: tc.dir}
|
||||
item := OSSItem{path: tc.itemPath}
|
||||
result := r.RelativePath(item)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package addon
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"helm.sh/helm/v3/pkg/chart/loader"
|
||||
)
|
||||
|
||||
var files = []*loader.BufferedFile{
|
||||
{
|
||||
Name: "metadata.yaml",
|
||||
Data: []byte(`name: test-helm-addon
|
||||
version: 1.0.0
|
||||
description: This is a addon for test when install addon from helm repo
|
||||
icon: https://www.terraform.io/assets/images/logo-text-8c3ba8a6.svg
|
||||
url: https://terraform.io/
|
||||
|
||||
tags: []
|
||||
|
||||
deployTo:
|
||||
control_plane: true
|
||||
runtime_cluster: false
|
||||
|
||||
dependencies: []
|
||||
|
||||
invisible: false`),
|
||||
},
|
||||
{
|
||||
Name: "resources/parameter.cue",
|
||||
Data: []byte(`parameter: {
|
||||
// test wrong parameter
|
||||
example: *"default"
|
||||
}`),
|
||||
},
|
||||
}
|
||||
|
||||
func TestMemoryReader(t *testing.T) {
|
||||
m := MemoryReader{
|
||||
Name: "fluxcd",
|
||||
Files: files,
|
||||
}
|
||||
|
||||
t.Run("ListAddonMeta", func(t *testing.T) {
|
||||
meta, err := m.ListAddonMeta()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(meta["fluxcd"].Items))
|
||||
// Verify the internal fileData map was populated
|
||||
_, metadataExists := m.fileData["metadata.yaml"]
|
||||
assert.True(t, metadataExists, "metadata.yaml should exist in fileData map")
|
||||
_, parameterExists := m.fileData["resources/parameter.cue"]
|
||||
assert.True(t, parameterExists, "resources/parameter.cue should exist in fileData map")
|
||||
})
|
||||
|
||||
t.Run("ReadFile", func(t *testing.T) {
|
||||
// Ensure ListAddonMeta has been called to populate the internal map
|
||||
_, _ = m.ListAddonMeta()
|
||||
|
||||
t.Run("read by exact name", func(t *testing.T) {
|
||||
metaFile, err := m.ReadFile("metadata.yaml")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, metaFile)
|
||||
})
|
||||
|
||||
t.Run("read by prefixed name", func(t *testing.T) {
|
||||
parameterData, err := m.ReadFile("fluxcd/resources/parameter.cue")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, parameterData)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestMemoryReader_RelativePath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
addonName string
|
||||
itemName string
|
||||
expected string
|
||||
}{
|
||||
"name without prefix": {
|
||||
addonName: "my-addon",
|
||||
itemName: "metadata.yaml",
|
||||
expected: filepath.Join("my-addon", "metadata.yaml"),
|
||||
},
|
||||
"name with prefix": {
|
||||
addonName: "my-addon",
|
||||
itemName: "my-addon/template.cue",
|
||||
expected: "my-addon/template.cue",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r := MemoryReader{Name: tc.addonName}
|
||||
item := OSSItem{name: tc.itemName}
|
||||
result := r.RelativePath(item)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,127 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// mockItem implements the Item interface for testing
|
||||
type mockItem struct {
|
||||
path string
|
||||
name string
|
||||
typeName string
|
||||
}
|
||||
|
||||
func (m mockItem) GetType() string { return m.typeName }
|
||||
func (m mockItem) GetPath() string { return m.path }
|
||||
func (m mockItem) GetName() string { return m.name }
|
||||
|
||||
// mockReader implements the AsyncReader interface for testing
|
||||
type mockReader struct{}
|
||||
|
||||
func (m mockReader) ListAddonMeta() (map[string]SourceMeta, error) { return nil, nil }
|
||||
func (m mockReader) ReadFile(path string) (string, error) { return "", nil }
|
||||
func (m mockReader) RelativePath(item Item) string { return item.GetPath() }
|
||||
|
||||
func TestClassifyItemByPattern(t *testing.T) {
|
||||
addonName := "my-addon"
|
||||
meta := &SourceMeta{
|
||||
Name: addonName,
|
||||
Items: []Item{
|
||||
mockItem{path: "my-addon/metadata.yaml"},
|
||||
mockItem{path: "my-addon/template.cue"},
|
||||
mockItem{path: "my-addon/definitions/def.cue"},
|
||||
mockItem{path: "my-addon/resources/res.yaml"},
|
||||
mockItem{path: "my-addon/schemas/schema.cue"},
|
||||
mockItem{path: "my-addon/views/view.cue"},
|
||||
mockItem{path: "my-addon/some-other-file.txt"}, // Should be ignored
|
||||
},
|
||||
}
|
||||
|
||||
r := mockReader{}
|
||||
classified := ClassifyItemByPattern(meta, r)
|
||||
|
||||
assert.Contains(t, classified, MetadataFileName)
|
||||
assert.Len(t, classified[MetadataFileName], 1)
|
||||
|
||||
assert.Contains(t, classified, AppTemplateCueFileName)
|
||||
assert.Len(t, classified[AppTemplateCueFileName], 1)
|
||||
|
||||
assert.Contains(t, classified, DefinitionsDirName)
|
||||
assert.Len(t, classified[DefinitionsDirName], 1)
|
||||
|
||||
assert.Contains(t, classified, ResourcesDirName)
|
||||
assert.Len(t, classified[ResourcesDirName], 1)
|
||||
|
||||
assert.Contains(t, classified, DefSchemaName)
|
||||
assert.Len(t, classified[DefSchemaName], 1)
|
||||
|
||||
assert.Contains(t, classified, ViewDirName)
|
||||
assert.Len(t, classified[ViewDirName], 1)
|
||||
|
||||
assert.NotContains(t, classified, "some-other-file.txt")
|
||||
}
|
||||
|
||||
func TestNewAsyncReader(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
baseURL string
|
||||
bucket string
|
||||
repo string
|
||||
subPath string
|
||||
token string
|
||||
rdType ReaderType
|
||||
wantType interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
"git type": {
|
||||
baseURL: "https://github.com/kubevela/catalog",
|
||||
subPath: "addons",
|
||||
rdType: gitType,
|
||||
wantType: &gitReader{},
|
||||
wantErr: false,
|
||||
},
|
||||
"gitee type": {
|
||||
baseURL: "https://gitee.com/kubevela/catalog",
|
||||
subPath: "addons",
|
||||
rdType: giteeType,
|
||||
wantType: &giteeReader{},
|
||||
wantErr: false,
|
||||
},
|
||||
"oss type": {
|
||||
baseURL: "oss-cn-hangzhou.aliyuncs.com",
|
||||
bucket: "kubevela-addons",
|
||||
rdType: ossType,
|
||||
wantType: &ossReader{},
|
||||
wantErr: false,
|
||||
},
|
||||
"invalid url": {
|
||||
baseURL: "://invalid-url",
|
||||
rdType: gitType,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid type": {
|
||||
baseURL: "https://github.com/kubevela/catalog",
|
||||
rdType: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Note: This test does not cover the gitlab case as it requires a live API call
|
||||
// or a complex mock setup, which is beyond the scope of this unit test.
|
||||
if tc.rdType == gitlabType {
|
||||
t.Skip("Skipping gitlab test in this unit test suite.")
|
||||
}
|
||||
|
||||
reader, err := NewAsyncReader(tc.baseURL, tc.bucket, tc.repo, tc.subPath, tc.token, tc.rdType)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.IsType(t, tc.wantType, reader)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathWithParent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
readPath string
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/chartutil"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -402,6 +403,90 @@ func TestCheckAddonPackageValid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsRegistryFuncs(t *testing.T) {
|
||||
t.Run("IsLocalRegistry", func(t *testing.T) {
|
||||
assert.True(t, IsLocalRegistry(Registry{Name: "local"}))
|
||||
assert.False(t, IsLocalRegistry(Registry{Name: "KubeVela"}))
|
||||
})
|
||||
t.Run("IsVersionRegistry", func(t *testing.T) {
|
||||
assert.True(t, IsVersionRegistry(Registry{Helm: &HelmSource{}}))
|
||||
assert.False(t, IsVersionRegistry(Registry{Git: &GitAddonSource{}}))
|
||||
assert.False(t, IsVersionRegistry(Registry{}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestInstallOptions(t *testing.T) {
|
||||
t.Run("SkipValidateVersion", func(t *testing.T) {
|
||||
installer := &Installer{}
|
||||
SkipValidateVersion(installer)
|
||||
assert.True(t, installer.skipVersionValidate)
|
||||
})
|
||||
t.Run("DryRunAddon", func(t *testing.T) {
|
||||
installer := &Installer{}
|
||||
DryRunAddon(installer)
|
||||
assert.True(t, installer.dryRun)
|
||||
})
|
||||
t.Run("OverrideDefinitions", func(t *testing.T) {
|
||||
installer := &Installer{}
|
||||
OverrideDefinitions(installer)
|
||||
assert.True(t, installer.overrideDefs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProduceDefConflictError(t *testing.T) {
|
||||
t.Run("no conflicts", func(t *testing.T) {
|
||||
err := produceDefConflictError(map[string]string{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("with conflicts", func(t *testing.T) {
|
||||
conflicts := map[string]string{
|
||||
"def1": "error message 1",
|
||||
"def2": "error message 2",
|
||||
}
|
||||
err := produceDefConflictError(conflicts)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "error message 1")
|
||||
assert.Contains(t, err.Error(), "error message 2")
|
||||
assert.Contains(t, err.Error(), "--override-definitions")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGenerateChartMetadata(t *testing.T) {
|
||||
addonDir := t.TempDir()
|
||||
|
||||
// Corrected YAML content with no leading whitespace
|
||||
metaFileContent := `name: my-addon
|
||||
version: 1.2.3
|
||||
description: my addon description
|
||||
icon: http://my-icon.com
|
||||
url: http://my-home.com
|
||||
tags:
|
||||
- tag1
|
||||
- tag2
|
||||
system:
|
||||
vela: ">=1.5.0"
|
||||
kubernetes: "1.20.0"
|
||||
`
|
||||
err := os.WriteFile(filepath.Join(addonDir, MetadataFileName), []byte(metaFileContent), 0644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
chartMeta, err := generateChartMetadata(addonDir)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, chartMeta)
|
||||
|
||||
assert.Equal(t, "my-addon", chartMeta.Name)
|
||||
assert.Equal(t, "1.2.3", chartMeta.Version)
|
||||
assert.Equal(t, "my addon description", chartMeta.Description)
|
||||
assert.Equal(t, "library", chartMeta.Type)
|
||||
assert.Equal(t, chart.APIVersionV2, chartMeta.APIVersion)
|
||||
assert.Equal(t, "http://my-icon.com", chartMeta.Icon)
|
||||
assert.Equal(t, "http://my-home.com", chartMeta.Home)
|
||||
assert.Equal(t, []string{"tag1", "tag2"}, chartMeta.Keywords)
|
||||
assert.Equal(t, ">=1.5.0", chartMeta.Annotations[velaSystemRequirement])
|
||||
assert.Equal(t, "1.20.0", chartMeta.Annotations[kubernetesSystemRequirement])
|
||||
assert.Equal(t, "my-addon", chartMeta.Annotations[addonSystemRequirement])
|
||||
}
|
||||
|
||||
const (
|
||||
compDefYaml = `
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package addon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -28,6 +29,7 @@ import (
|
|||
"helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/helm"
|
||||
|
|
@ -227,3 +229,159 @@ func TestToVersionedRegistry(t *testing.T) {
|
|||
assert.EqualError(t, err, "registry 'git-based-registry' is not a versioned registry")
|
||||
assert.Nil(t, actual)
|
||||
}
|
||||
|
||||
func TestResolveAddonListFromIndex(t *testing.T) {
|
||||
r := &versionedRegistry{name: "test-repo"}
|
||||
indexFile := &repo.IndexFile{
|
||||
Entries: map[string]repo.ChartVersions{
|
||||
"addon-good": {
|
||||
{Metadata: &chart.Metadata{Name: "addon-good", Version: "1.0.0", Description: "old desc", Icon: "old_icon", Keywords: []string{"tag1"}}},
|
||||
{Metadata: &chart.Metadata{Name: "addon-good", Version: "1.2.0", Description: "latest desc", Icon: "latest_icon", Keywords: []string{"tag2"}}},
|
||||
{Metadata: &chart.Metadata{Name: "addon-good", Version: "1.1.0", Description: "middle desc", Icon: "middle_icon", Keywords: []string{"tag3"}}},
|
||||
},
|
||||
"addon-empty": {},
|
||||
"addon-single": {
|
||||
{Metadata: &chart.Metadata{Name: "addon-single", Version: "0.1.0", Description: "single desc"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := r.resolveAddonListFromIndex(r.name, indexFile)
|
||||
|
||||
assert.Equal(t, 2, len(result))
|
||||
|
||||
var addonGood, addonSingle *UIData
|
||||
for _, addon := range result {
|
||||
if addon.Name == "addon-good" {
|
||||
addonGood = addon
|
||||
}
|
||||
if addon.Name == "addon-single" {
|
||||
addonSingle = addon
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, addonGood)
|
||||
assert.Equal(t, "addon-good", addonGood.Name)
|
||||
assert.Equal(t, "test-repo", addonGood.RegistryName)
|
||||
assert.Equal(t, "1.2.0", addonGood.Version)
|
||||
assert.Equal(t, "latest desc", addonGood.Description)
|
||||
assert.Equal(t, "latest_icon", addonGood.Icon)
|
||||
assert.Equal(t, []string{"tag2"}, addonGood.Tags)
|
||||
assert.Equal(t, []string{"1.2.0", "1.1.0", "1.0.0"}, addonGood.AvailableVersions)
|
||||
|
||||
require.NotNil(t, addonSingle)
|
||||
assert.Equal(t, "addon-single", addonSingle.Name)
|
||||
assert.Equal(t, "0.1.0", addonSingle.Version)
|
||||
assert.Equal(t, []string{"0.1.0"}, addonSingle.AvailableVersions)
|
||||
}
|
||||
|
||||
// setupAddonTestServer creates a mock HTTP server for testing addon loading.
|
||||
// It can simulate success, 404 errors, or serving corrupt data based on the handlerType.
|
||||
func setupAddonTestServer(t *testing.T, handlerType string) string {
|
||||
var server *httptest.Server
|
||||
// This handler rewrites URLs in the index file to point to the server it's running on.
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.URL.Path, "index.yaml") {
|
||||
content, err := os.ReadFile("./testdata/multiversion-helm-repo/index.yaml")
|
||||
assert.NoError(t, err)
|
||||
newContent := strings.ReplaceAll(string(content), "http://127.0.0.1:18083/multi", server.URL)
|
||||
_, err = w.Write([]byte(newContent))
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
// After serving the index, the next request depends on the handler type.
|
||||
switch handlerType {
|
||||
case "success":
|
||||
multiVersionHandler(w, r)
|
||||
case "notfound":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case "corrupt":
|
||||
_, err := w.Write([]byte("this is not a valid tgz file"))
|
||||
assert.NoError(t, err)
|
||||
default:
|
||||
t.Errorf("unknown handler type: %s", handlerType)
|
||||
}
|
||||
})
|
||||
server = httptest.NewServer(handler)
|
||||
t.Cleanup(server.Close)
|
||||
return server.URL
|
||||
}
|
||||
|
||||
func TestLoadAddon(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
handlerType string
|
||||
addonName string
|
||||
addonVersion string
|
||||
expectErr bool
|
||||
expectedErrStr string
|
||||
checkFunc func(t *testing.T, pkg *WholeAddonPackage)
|
||||
}{
|
||||
{
|
||||
name: "Success case",
|
||||
handlerType: "success",
|
||||
addonName: "fluxcd",
|
||||
addonVersion: "1.0.0",
|
||||
expectErr: false,
|
||||
checkFunc: func(t *testing.T, pkg *WholeAddonPackage) {
|
||||
assert.NotNil(t, pkg)
|
||||
assert.Equal(t, "fluxcd", pkg.Name)
|
||||
assert.Equal(t, "1.0.0", pkg.Version)
|
||||
assert.NotEmpty(t, pkg.YAMLTemplates)
|
||||
assert.Equal(t, []string{"2.0.0", "1.0.0"}, pkg.AvailableVersions)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Version not found",
|
||||
handlerType: "success",
|
||||
addonName: "fluxcd",
|
||||
addonVersion: "3.0.0",
|
||||
expectErr: true,
|
||||
expectedErrStr: "specified version 3.0.0 for addon fluxcd not exist",
|
||||
},
|
||||
{
|
||||
name: "Chart download fails",
|
||||
handlerType: "notfound",
|
||||
addonName: "fluxcd",
|
||||
addonVersion: "1.0.0",
|
||||
expectErr: true,
|
||||
expectedErrStr: ErrFetch.Error(),
|
||||
},
|
||||
{
|
||||
name: "Corrupt chart file",
|
||||
handlerType: "corrupt",
|
||||
addonName: "fluxcd",
|
||||
addonVersion: "1.0.0",
|
||||
expectErr: true,
|
||||
expectedErrStr: ErrFetch.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
serverURL := setupAddonTestServer(t, tc.handlerType)
|
||||
reg := &versionedRegistry{
|
||||
name: "test-registry",
|
||||
url: serverURL,
|
||||
h: helm.NewHelperWithCache(),
|
||||
Opts: nil,
|
||||
}
|
||||
|
||||
pkg, err := reg.loadAddon(context.Background(), tc.addonName, tc.addonVersion)
|
||||
|
||||
if tc.expectErr {
|
||||
assert.Error(t, err)
|
||||
if tc.expectedErrStr != "" {
|
||||
assert.Contains(t, err.Error(), tc.expectedErrStr)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if tc.checkFunc != nil {
|
||||
tc.checkFunc(t, pkg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -24,8 +24,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
errors2 "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
|
@ -34,16 +33,217 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
)
|
||||
|
||||
func TestParsePolicies(t *testing.T) {
|
||||
overrideCompDef := &v1beta1.ComponentDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "webservice", Namespace: "vela-system"},
|
||||
Spec: v1beta1.ComponentDefinitionSpec{Workload: common.WorkloadTypeDescriptor{Type: "Deployment"}},
|
||||
}
|
||||
customPolicyDef := &v1beta1.PolicyDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "custom-policy", Namespace: "vela-system"},
|
||||
Spec: v1beta1.PolicyDefinitionSpec{
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {name: string}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
schemes := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(schemes)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
appfile *Appfile
|
||||
client client.Client
|
||||
wantErrContain string
|
||||
assertFunc func(*testing.T, *Appfile)
|
||||
}{
|
||||
{
|
||||
name: "policy with nil properties",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "gc-policy",
|
||||
Type: v1alpha1.GarbageCollectPolicyType,
|
||||
Properties: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).Build(),
|
||||
wantErrContain: "must not have empty properties",
|
||||
},
|
||||
{
|
||||
name: "debug policy",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "debug-policy",
|
||||
Type: v1alpha1.DebugPolicyType,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).Build(),
|
||||
assertFunc: func(t *testing.T, af *Appfile) {
|
||||
assert.True(t, af.Debug)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "override policy fails to get definition",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "comp1",
|
||||
Type: "webservice",
|
||||
},
|
||||
},
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "override-policy",
|
||||
Type: v1alpha1.OverridePolicyType,
|
||||
Properties: util.Object2RawExtension(v1alpha1.OverridePolicySpec{
|
||||
Components: []v1alpha1.EnvComponentPatch{
|
||||
{
|
||||
Name: "comp1",
|
||||
Type: "webservice",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RelatedComponentDefinitions: make(map[string]*v1beta1.ComponentDefinition),
|
||||
RelatedTraitDefinitions: make(map[string]*v1beta1.TraitDefinition),
|
||||
},
|
||||
client: &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
return fmt.Errorf("get definition error")
|
||||
},
|
||||
},
|
||||
wantErrContain: "get definition error",
|
||||
},
|
||||
{
|
||||
name: "override policy success",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Components: []common.ApplicationComponent{
|
||||
{
|
||||
Name: "comp1",
|
||||
Type: "webservice",
|
||||
},
|
||||
},
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "override-policy",
|
||||
Type: v1alpha1.OverridePolicyType,
|
||||
Properties: util.Object2RawExtension(v1alpha1.OverridePolicySpec{
|
||||
Components: []v1alpha1.EnvComponentPatch{
|
||||
{
|
||||
Name: "comp1",
|
||||
Type: "webservice",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
RelatedComponentDefinitions: make(map[string]*v1beta1.ComponentDefinition),
|
||||
RelatedTraitDefinitions: make(map[string]*v1beta1.TraitDefinition),
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).WithObjects(overrideCompDef).Build(),
|
||||
assertFunc: func(t *testing.T, af *Appfile) {
|
||||
assert.Contains(t, af.RelatedComponentDefinitions, "webservice")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom policy definition not found",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "my-policy",
|
||||
Type: "custom-policy",
|
||||
Properties: util.Object2RawExtension(map[string]string{"name": "test"}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: &test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if _, ok := obj.(*v1beta1.PolicyDefinition); ok {
|
||||
return errors2.NewNotFound(v1beta1.Resource("policydefinition"), "custom-policy")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
wantErrContain: "fetch component/policy type of my-policy",
|
||||
},
|
||||
{
|
||||
name: "custom policy success",
|
||||
appfile: &Appfile{
|
||||
app: &v1beta1.Application{
|
||||
Spec: v1beta1.ApplicationSpec{
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: "my-policy",
|
||||
Type: "custom-policy",
|
||||
Properties: util.Object2RawExtension(map[string]string{"name": "test"}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).WithObjects(customPolicyDef).Build(),
|
||||
assertFunc: func(t *testing.T, af *Appfile) {
|
||||
assert.Equal(t, 1, len(af.ParsedPolicies))
|
||||
assert.Equal(t, "my-policy", af.ParsedPolicies[0].Name)
|
||||
assert.Equal(t, "custom-policy", af.ParsedPolicies[0].Type)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := NewApplicationParser(tc.client)
|
||||
// This function is tested separated, mock it for parsePolicies
|
||||
if tc.appfile.app != nil {
|
||||
tc.appfile.Policies = tc.appfile.app.Spec.Policies
|
||||
}
|
||||
err := p.parsePolicies(context.Background(), tc.appfile)
|
||||
|
||||
if tc.wantErrContain != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErrContain)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tc.assertFunc != nil {
|
||||
tc.assertFunc(t, tc.appfile)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var expectedExceptApp = &Appfile{
|
||||
Name: "application-sample",
|
||||
ParsedComponents: []*Component{
|
||||
|
|
@ -81,10 +281,10 @@ var expectedExceptApp = &Appfile{
|
|||
}
|
||||
}
|
||||
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
|
|
@ -149,7 +349,7 @@ spec:
|
|||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
|
|
@ -235,11 +435,11 @@ spec:
|
|||
properties:
|
||||
`
|
||||
|
||||
var _ = Describe("Test application parser", func() {
|
||||
It("Test parse an application", func() {
|
||||
func TestApplicationParser(t *testing.T) {
|
||||
t.Run("Test parse an application", func(t *testing.T) {
|
||||
o := v1beta1.Application{}
|
||||
err := yaml.Unmarshal([]byte(appfileYaml), &o)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a mock client
|
||||
tclient := test.MockClient{
|
||||
|
|
@ -266,24 +466,24 @@ var _ = Describe("Test application parser", func() {
|
|||
}
|
||||
|
||||
appfile, err := NewApplicationParser(&tclient).GenerateAppFile(context.TODO(), &o)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(equal(expectedExceptApp, appfile)).Should(BeTrue())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, equal(expectedExceptApp, appfile))
|
||||
|
||||
notfound := v1beta1.Application{}
|
||||
err = yaml.Unmarshal([]byte(appfileYaml2), ¬found)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
assert.NoError(t, err)
|
||||
_, err = NewApplicationParser(&tclient).GenerateAppFile(context.TODO(), ¬found)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
assert.Error(t, err)
|
||||
|
||||
By("app with empty policy")
|
||||
t.Log("app with empty policy")
|
||||
emptyPolicy := v1beta1.Application{}
|
||||
err = yaml.Unmarshal([]byte(appfileYamlEmptyPolicy), &emptyPolicy)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
assert.NoError(t, err)
|
||||
_, err = NewApplicationParser(&tclient).GenerateAppFile(context.TODO(), &emptyPolicy)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(err.Error()).Should(ContainSubstring("have empty properties"))
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "have empty properties")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func equal(af, dest *Appfile) bool {
|
||||
if af.Name != dest.Name || len(af.ParsedComponents) != len(dest.ParsedComponents) {
|
||||
|
|
@ -313,29 +513,28 @@ func equal(af, dest *Appfile) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
var _ = Describe("Test application parser", func() {
|
||||
func TestApplicationParserWithLegacyRevision(t *testing.T) {
|
||||
var app v1beta1.Application
|
||||
var apprev v1beta1.ApplicationRevision
|
||||
var wsd v1beta1.WorkflowStepDefinition
|
||||
var expectedExceptAppfile *Appfile
|
||||
var mockClient test.MockClient
|
||||
|
||||
BeforeEach(func() {
|
||||
// prepare WorkflowStepDefinition
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/wsd.yaml", &wsd)).Should(BeNil())
|
||||
// prepare WorkflowStepDefinition
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/wsd.yaml", &wsd))
|
||||
|
||||
// prepare verify data
|
||||
expectedExceptAppfile = &Appfile{
|
||||
Name: "backport-1-2-test-demo",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "backport-1-2-test-demo",
|
||||
Type: "webservice",
|
||||
Params: map[string]interface{}{
|
||||
"image": "nginx",
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: `
|
||||
// prepare verify data
|
||||
expectedExceptAppfile = &Appfile{
|
||||
Name: "backport-1-2-test-demo",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "backport-1-2-test-demo",
|
||||
Type: "webservice",
|
||||
Params: map[string]interface{}{
|
||||
"image": "nginx",
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: `
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
|
|
@ -361,10 +560,10 @@ var _ = Describe("Test application parser", func() {
|
|||
}
|
||||
}
|
||||
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
|
|
@ -374,14 +573,14 @@ var _ = Describe("Test application parser", func() {
|
|||
|
||||
cmd?: [...string]
|
||||
}`,
|
||||
},
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "scaler",
|
||||
Params: map[string]interface{}{
|
||||
"replicas": float64(1),
|
||||
},
|
||||
Template: `
|
||||
},
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "scaler",
|
||||
Params: map[string]interface{}{
|
||||
"replicas": float64(1),
|
||||
},
|
||||
Template: `
|
||||
parameter: {
|
||||
// +usage=Specify the number of workload
|
||||
replicas: *1 | int
|
||||
|
|
@ -390,62 +589,59 @@ parameter: {
|
|||
patch: spec: replicas: parameter.replicas
|
||||
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
WorkflowSteps: []workflowv1alpha1.WorkflowStep{
|
||||
{
|
||||
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
|
||||
Name: "apply",
|
||||
Type: "apply-application",
|
||||
},
|
||||
},
|
||||
WorkflowSteps: []workflowv1alpha1.WorkflowStep{
|
||||
{
|
||||
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
|
||||
Name: "apply",
|
||||
Type: "apply-application",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Create mock client
|
||||
mockClient = test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if strings.Contains(key.Name, "unknown") {
|
||||
return &errors2.StatusError{ErrStatus: metav1.Status{Reason: "NotFound", Message: "not found"}}
|
||||
// Create mock client
|
||||
mockClient = test.MockClient{
|
||||
MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error {
|
||||
if strings.Contains(key.Name, "unknown") {
|
||||
return &errors2.StatusError{ErrStatus: metav1.Status{Reason: "NotFound", Message: "not found"}}
|
||||
}
|
||||
switch o := obj.(type) {
|
||||
case *v1beta1.ComponentDefinition:
|
||||
wd, err := util.UnMarshalStringToComponentDefinition(componentDefinition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch o := obj.(type) {
|
||||
case *v1beta1.ComponentDefinition:
|
||||
wd, err := util.UnMarshalStringToComponentDefinition(componentDefinition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*o = *wd
|
||||
case *v1beta1.WorkflowStepDefinition:
|
||||
*o = wsd
|
||||
case *v1beta1.ApplicationRevision:
|
||||
*o = apprev
|
||||
default:
|
||||
// skip
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
})
|
||||
*o = *wd
|
||||
case *v1beta1.WorkflowStepDefinition:
|
||||
*o = wsd
|
||||
case *v1beta1.ApplicationRevision:
|
||||
*o = apprev
|
||||
default:
|
||||
// skip
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
When("with apply-application workflowStep", func() {
|
||||
BeforeEach(func() {
|
||||
// prepare application
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil())
|
||||
// prepare application revision
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev1.yaml", &apprev)).Should(BeNil())
|
||||
})
|
||||
t.Run("with apply-application workflowStep", func(t *testing.T) {
|
||||
// prepare application
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app))
|
||||
// prepare application revision
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/apprev1.yaml", &apprev))
|
||||
|
||||
It("Test we can parse an application revision to an appFile 1", func() {
|
||||
t.Run("Test we can parse an application revision to an appFile 1", func(t *testing.T) {
|
||||
|
||||
appfile, err := NewApplicationParser(&mockClient).GenerateAppFile(context.TODO(), &app)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(equal(expectedExceptAppfile, appfile)).Should(BeTrue())
|
||||
Expect(len(appfile.WorkflowSteps) > 0 &&
|
||||
len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions)).Should(BeTrue())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, equal(expectedExceptAppfile, appfile))
|
||||
assert.True(t, len(appfile.WorkflowSteps) > 0 &&
|
||||
len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions))
|
||||
|
||||
Expect(len(appfile.WorkflowSteps) > 0 && func() bool {
|
||||
assert.True(t, len(appfile.WorkflowSteps) > 0 && func() bool {
|
||||
this := appfile.RelatedWorkflowStepDefinitions
|
||||
that := appfile.AppRevision.Spec.WorkflowStepDefinitions
|
||||
for i, w := range this {
|
||||
|
|
@ -455,27 +651,25 @@ patch: spec: replicas: parameter.replicas
|
|||
}
|
||||
}
|
||||
return true
|
||||
}()).Should(BeTrue())
|
||||
}())
|
||||
})
|
||||
})
|
||||
|
||||
When("with apply-application and apply-component build-in workflowStep", func() {
|
||||
BeforeEach(func() {
|
||||
// prepare application
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil())
|
||||
// prepare application revision
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev2.yaml", &apprev)).Should(BeNil())
|
||||
})
|
||||
t.Run("with apply-application and apply-component build-in workflowStep", func(t *testing.T) {
|
||||
// prepare application
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app))
|
||||
// prepare application revision
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/apprev2.yaml", &apprev))
|
||||
|
||||
It("Test we can parse an application revision to an appFile 2", func() {
|
||||
t.Run("Test we can parse an application revision to an appFile 2", func(t *testing.T) {
|
||||
|
||||
appfile, err := NewApplicationParser(&mockClient).GenerateAppFile(context.TODO(), &app)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(equal(expectedExceptAppfile, appfile)).Should(BeTrue())
|
||||
Expect(len(appfile.WorkflowSteps) > 0 &&
|
||||
len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions)).Should(BeTrue())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, equal(expectedExceptAppfile, appfile))
|
||||
assert.True(t, len(appfile.WorkflowSteps) > 0 &&
|
||||
len(appfile.RelatedWorkflowStepDefinitions) == len(appfile.AppRevision.Spec.WorkflowStepDefinitions))
|
||||
|
||||
Expect(len(appfile.WorkflowSteps) > 0 && func() bool {
|
||||
assert.True(t, len(appfile.WorkflowSteps) > 0 && func() bool {
|
||||
this := appfile.RelatedWorkflowStepDefinitions
|
||||
that := appfile.AppRevision.Spec.WorkflowStepDefinitions
|
||||
for i, w := range this {
|
||||
|
|
@ -486,29 +680,25 @@ patch: spec: replicas: parameter.replicas
|
|||
}
|
||||
}
|
||||
return true
|
||||
}()).Should(BeTrue())
|
||||
}())
|
||||
})
|
||||
})
|
||||
|
||||
When("with unknown workflowStep", func() {
|
||||
BeforeEach(func() {
|
||||
// prepare application
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app)).Should(BeNil())
|
||||
// prepare application revision
|
||||
Expect(common2.ReadYamlToObject("testdata/backport-1-2/apprev3.yaml", &apprev)).Should(BeNil())
|
||||
})
|
||||
t.Run("with unknown workflowStep", func(t *testing.T) {
|
||||
// prepare application
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/app.yaml", &app))
|
||||
// prepare application revision
|
||||
assert.NoError(t, common2.ReadYamlToObject("testdata/backport-1-2/apprev3.yaml", &apprev))
|
||||
|
||||
It("Test we can parse an application revision to an appFile 3", func() {
|
||||
t.Run("Test we can parse an application revision to an appFile 3", func(t *testing.T) {
|
||||
|
||||
_, err := NewApplicationParser(&mockClient).GenerateAppFile(context.TODO(), &app)
|
||||
Expect(err).Should(HaveOccurred())
|
||||
Expect(err.Error()).Should(SatisfyAll(
|
||||
ContainSubstring("failed to get workflow step definition apply-application-unknown: not found"),
|
||||
ContainSubstring("failed to parseWorkflowStepsForLegacyRevision")),
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to get workflow step definition apply-application-unknown: not found")
|
||||
assert.Contains(t, err.Error(), "failed to parseWorkflowStepsForLegacyRevision")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestParser_parseTraits(t *testing.T) {
|
||||
type args struct {
|
||||
|
|
@ -707,3 +897,145 @@ func TestParser_parseTraitsFromRevision(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseComponentFromRevisionAndClient(t *testing.T) {
|
||||
compDef := &v1beta1.ComponentDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "webservice", Namespace: "vela-system"},
|
||||
Spec: v1beta1.ComponentDefinitionSpec{
|
||||
Workload: common.WorkloadTypeDescriptor{Type: "Deployment"},
|
||||
Schematic: &common.Schematic{CUE: &common.CUE{Template: "parameter: {image: string}"}},
|
||||
},
|
||||
}
|
||||
traitDef := &v1beta1.TraitDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "scaler", Namespace: "vela-system"},
|
||||
Spec: v1beta1.TraitDefinitionSpec{
|
||||
Schematic: &common.Schematic{CUE: &common.CUE{Template: "parameter: {replicas: int}"}},
|
||||
},
|
||||
}
|
||||
|
||||
appComp := common.ApplicationComponent{
|
||||
Name: "my-comp",
|
||||
Type: "webservice",
|
||||
Properties: util.Object2RawExtension(map[string]string{"image": "nginx"}),
|
||||
Traits: []common.ApplicationTrait{
|
||||
{
|
||||
Type: "scaler",
|
||||
Properties: util.Object2RawExtension(map[string]int{"replicas": 2}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
schemes := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(schemes)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
appRev *v1beta1.ApplicationRevision
|
||||
client client.Client
|
||||
wantErr bool
|
||||
assertFunc func(*testing.T, *Component)
|
||||
}{
|
||||
{
|
||||
name: "component and trait found in revision",
|
||||
appRev: &v1beta1.ApplicationRevision{
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
ComponentDefinitions: map[string]*v1beta1.ComponentDefinition{"webservice": compDef},
|
||||
TraitDefinitions: map[string]*v1beta1.TraitDefinition{"scaler": traitDef},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).Build(),
|
||||
wantErr: false,
|
||||
assertFunc: func(t *testing.T, c *Component) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "my-comp", c.Name)
|
||||
assert.Equal(t, "webservice", c.Type)
|
||||
assert.Equal(t, 1, len(c.Traits))
|
||||
assert.Equal(t, "scaler", c.Traits[0].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "component not in revision, but in cluster",
|
||||
appRev: &v1beta1.ApplicationRevision{
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
TraitDefinitions: map[string]*v1beta1.TraitDefinition{"scaler": traitDef},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).WithObjects(compDef).Build(),
|
||||
wantErr: false,
|
||||
assertFunc: func(t *testing.T, c *Component) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "webservice", c.Type)
|
||||
assert.Equal(t, 1, len(c.Traits))
|
||||
assert.Equal(t, "scaler", c.Traits[0].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trait not in revision, but in cluster",
|
||||
appRev: &v1beta1.ApplicationRevision{
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
ComponentDefinitions: map[string]*v1beta1.ComponentDefinition{"webservice": compDef},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).WithObjects(traitDef).Build(),
|
||||
wantErr: false,
|
||||
assertFunc: func(t *testing.T, c *Component) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "webservice", c.Type)
|
||||
assert.Equal(t, 1, len(c.Traits))
|
||||
assert.Equal(t, "scaler", c.Traits[0].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "component and trait not in revision, but in cluster",
|
||||
appRev: &v1beta1.ApplicationRevision{},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).WithObjects(compDef, traitDef).Build(),
|
||||
wantErr: false,
|
||||
assertFunc: func(t *testing.T, c *Component) {
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "webservice", c.Type)
|
||||
assert.Equal(t, 1, len(c.Traits))
|
||||
assert.Equal(t, "scaler", c.Traits[0].Name)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "component not found anywhere",
|
||||
appRev: &v1beta1.ApplicationRevision{},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).Build(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "trait not found anywhere",
|
||||
appRev: &v1beta1.ApplicationRevision{
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
ComponentDefinitions: map[string]*v1beta1.ComponentDefinition{"webservice": compDef},
|
||||
},
|
||||
},
|
||||
},
|
||||
client: fake.NewClientBuilder().WithScheme(schemes).Build(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewApplicationParser(tt.client)
|
||||
comp, err := p.ParseComponentFromRevisionAndClient(context.Background(), appComp, tt.appRev)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tt.assertFunc != nil {
|
||||
tt.assertFunc(t, comp)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package appfile
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var cfg *rest.Config
|
||||
var scheme *runtime.Scheme
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestAppFile(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Cli Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
|
||||
By("bootstrapping test environment")
|
||||
useExistCluster := false
|
||||
testEnv = &envtest.Environment{
|
||||
ControlPlaneStartTimeout: time.Minute,
|
||||
ControlPlaneStopTimeout: time.Minute,
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "charts", "vela-core", "crds")},
|
||||
UseExistingCluster: &useExistCluster,
|
||||
}
|
||||
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
scheme = runtime.NewScheme()
|
||||
Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred())
|
||||
Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
|
||||
Expect(v1.AddToScheme(scheme)).NotTo(HaveOccurred())
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(k8sClient).ToNot(BeNil())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
|
@ -18,14 +18,19 @@ package appfile
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"github.com/crossplane/crossplane-runtime/pkg/test"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
ktypes "k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
|
|
@ -35,6 +40,24 @@ import (
|
|||
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
type fakeRESTMapper struct {
|
||||
meta.RESTMapper
|
||||
}
|
||||
|
||||
func (f fakeRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
|
||||
if resource.Resource == "deployments" {
|
||||
return schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, nil
|
||||
}
|
||||
return schema.GroupVersionKind{}, errors.New("no mapping for KindFor")
|
||||
}
|
||||
|
||||
func (f fakeRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
|
||||
if resource.Resource == "deployments" {
|
||||
return []schema.GroupVersionKind{{Group: "apps", Version: "v1", Kind: "Deployment"}}, nil
|
||||
}
|
||||
return nil, errors.New("no mapping for KindsFor")
|
||||
}
|
||||
|
||||
func TestLoadComponentTemplate(t *testing.T) {
|
||||
cueTemplate := `
|
||||
context: {
|
||||
|
|
@ -47,35 +70,35 @@ func TestLoadComponentTemplate(t *testing.T) {
|
|||
selector: matchLabels: {
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
|
||||
|
||||
template: {
|
||||
metadata: labels: {
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
|
||||
|
||||
spec: {
|
||||
containers: [{
|
||||
name: context.name
|
||||
image: parameter.image
|
||||
|
||||
|
||||
if parameter["cmd"] != _|_ {
|
||||
command: parameter.cmd
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
selector:
|
||||
matchLabels:
|
||||
"app.oam.dev/component": context.name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
parameter: {
|
||||
// +usage=Which image would you like to use for your service
|
||||
// +short=i
|
||||
image: string
|
||||
|
||||
|
||||
cmd?: [...string]
|
||||
}
|
||||
`
|
||||
|
|
@ -316,7 +339,7 @@ metadata:
|
|||
spec:
|
||||
status:
|
||||
customStatus: testCustomStatus
|
||||
healthPolicy: testHealthPolicy
|
||||
healthPolicy: testHealthPolicy
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
|
|
@ -333,7 +356,7 @@ metadata:
|
|||
spec:
|
||||
status:
|
||||
customStatus: testCustomStatus
|
||||
healthPolicy: testHealthPolicy
|
||||
healthPolicy: testHealthPolicy
|
||||
appliesToWorkloads:
|
||||
- deployments.apps
|
||||
schematic:
|
||||
|
|
@ -385,3 +408,349 @@ spec:
|
|||
t.Fatal("failed load template of trait definition ", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTemplateFromRevision(t *testing.T) {
|
||||
compDef := v1beta1.ComponentDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: v1beta1.ComponentDefinitionKind,
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-comp"},
|
||||
Spec: v1beta1.ComponentDefinitionSpec{
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {name: string}"},
|
||||
},
|
||||
Workload: common.WorkloadTypeDescriptor{
|
||||
Definition: common.WorkloadGVK{APIVersion: "v1", Kind: "Pod"},
|
||||
},
|
||||
},
|
||||
}
|
||||
traitDef := v1beta1.TraitDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: v1beta1.TraitDefinitionKind,
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-trait"},
|
||||
Spec: v1beta1.TraitDefinitionSpec{
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {port: int}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
policyDef := v1beta1.PolicyDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: v1beta1.PolicyDefinitionKind,
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-policy"},
|
||||
Spec: v1beta1.PolicyDefinitionSpec{
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {replicas: int}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
wfStepDef := v1beta1.WorkflowStepDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: v1beta1.WorkflowStepDefinitionKind,
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-step"},
|
||||
Spec: v1beta1.WorkflowStepDefinitionSpec{
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {image: string}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
wlDef := v1beta1.WorkloadDefinition{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: v1beta1.WorkloadDefinitionKind,
|
||||
APIVersion: v1beta1.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-workload"},
|
||||
Spec: v1beta1.WorkloadDefinitionSpec{
|
||||
Reference: common.DefinitionReference{
|
||||
Name: "deployments.apps",
|
||||
},
|
||||
Schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "output: {apiVersion: 'apps/v1', kind: 'Deployment'}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
appRev := &v1beta1.ApplicationRevision{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "my-app-rev"},
|
||||
Spec: v1beta1.ApplicationRevisionSpec{
|
||||
ApplicationRevisionCompressibleFields: v1beta1.ApplicationRevisionCompressibleFields{
|
||||
ComponentDefinitions: map[string]*v1beta1.ComponentDefinition{
|
||||
"my-comp": &compDef,
|
||||
},
|
||||
TraitDefinitions: map[string]*v1beta1.TraitDefinition{
|
||||
"my-trait": &traitDef,
|
||||
},
|
||||
PolicyDefinitions: map[string]v1beta1.PolicyDefinition{
|
||||
"my-policy": policyDef,
|
||||
},
|
||||
WorkflowStepDefinitions: map[string]*v1beta1.WorkflowStepDefinition{
|
||||
"my-step": &wfStepDef,
|
||||
},
|
||||
WorkloadDefinitions: map[string]v1beta1.WorkloadDefinition{
|
||||
"my-workload": wlDef,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mapper := fakeRESTMapper{}
|
||||
|
||||
testCases := map[string]struct {
|
||||
capName string
|
||||
capType types.CapType
|
||||
apprev *v1beta1.ApplicationRevision
|
||||
checkFunc func(t *testing.T, tmpl *Template, err error)
|
||||
}{
|
||||
"load component definition": {
|
||||
capName: "my-comp",
|
||||
capType: types.TypeComponentDefinition,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "parameter: {name: string}", tmpl.TemplateStr)
|
||||
assert.Equal(t, v1beta1.ComponentDefinitionKind, tmpl.ComponentDefinition.Kind)
|
||||
},
|
||||
},
|
||||
"load trait definition": {
|
||||
capName: "my-trait",
|
||||
capType: types.TypeTrait,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "parameter: {port: int}", tmpl.TemplateStr)
|
||||
assert.Equal(t, v1beta1.TraitDefinitionKind, tmpl.TraitDefinition.Kind)
|
||||
},
|
||||
},
|
||||
"load policy definition": {
|
||||
capName: "my-policy",
|
||||
capType: types.TypePolicy,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "parameter: {replicas: int}", tmpl.TemplateStr)
|
||||
assert.Equal(t, v1beta1.PolicyDefinitionKind, tmpl.PolicyDefinition.Kind)
|
||||
},
|
||||
},
|
||||
"load workflow step definition": {
|
||||
capName: "my-step",
|
||||
capType: types.TypeWorkflowStep,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "parameter: {image: string}", tmpl.TemplateStr)
|
||||
assert.Equal(t, v1beta1.WorkflowStepDefinitionKind, tmpl.WorkflowStepDefinition.Kind)
|
||||
},
|
||||
},
|
||||
"fallback to workload definition": {
|
||||
capName: "my-workload",
|
||||
capType: types.TypeComponentDefinition,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "output: {apiVersion: 'apps/v1', kind: 'Deployment'}", tmpl.TemplateStr)
|
||||
assert.NotNil(t, tmpl.WorkloadDefinition)
|
||||
assert.Equal(t, v1beta1.WorkloadDefinitionKind, tmpl.WorkloadDefinition.Kind)
|
||||
assert.Equal(t, "apps/v1", tmpl.Reference.Definition.APIVersion)
|
||||
assert.Equal(t, "Deployment", tmpl.Reference.Definition.Kind)
|
||||
},
|
||||
},
|
||||
"definition not found": {
|
||||
capName: "not-exist",
|
||||
capType: types.TypeComponentDefinition,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, tmpl)
|
||||
assert.True(t, IsNotFoundInAppRevision(err))
|
||||
assert.Contains(t, err.Error(), "component definition [not-exist] not found in app revision my-app-rev")
|
||||
},
|
||||
},
|
||||
"nil app revision": {
|
||||
capName: "any",
|
||||
capType: types.TypeComponentDefinition,
|
||||
apprev: nil,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, tmpl)
|
||||
assert.Contains(t, err.Error(), "fail to find template for any as app revision is empty")
|
||||
},
|
||||
},
|
||||
"unsupported type": {
|
||||
capName: "any",
|
||||
capType: "unsupported",
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, tmpl)
|
||||
assert.Contains(t, err.Error(), "kind(unsupported) of any not supported")
|
||||
},
|
||||
},
|
||||
"verify revision name": {
|
||||
capName: "my-comp@my-ns",
|
||||
capType: types.TypeComponentDefinition,
|
||||
apprev: appRev,
|
||||
checkFunc: func(t *testing.T, tmpl *Template, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, tmpl)
|
||||
assert.Equal(t, "parameter: {name: string}", tmpl.TemplateStr)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
tmpl, err := LoadTemplateFromRevision(tc.capName, tc.capType, tc.apprev, mapper)
|
||||
tc.checkFunc(t, tmpl, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTemplateJSON2Object(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
capName string
|
||||
in *runtime.RawExtension
|
||||
schematic *common.Schematic
|
||||
wantCap types.Capability
|
||||
wantErr bool
|
||||
}{
|
||||
"with schematic CUE": {
|
||||
capName: "test-cap",
|
||||
schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {name: string}"},
|
||||
},
|
||||
wantCap: types.Capability{
|
||||
Name: "test-cap",
|
||||
CueTemplate: "parameter: {name: string}",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"with RawExtension": {
|
||||
capName: "test-cap-2",
|
||||
in: &runtime.RawExtension{
|
||||
Raw: []byte(`{"template": "parameter: {age: int}"}`),
|
||||
},
|
||||
wantCap: types.Capability{
|
||||
Name: "test-cap-2",
|
||||
CueTemplate: "parameter: {age: int}",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"with both schematic and RawExtension": {
|
||||
capName: "test-cap-3",
|
||||
in: &runtime.RawExtension{
|
||||
Raw: []byte(`{"description": "test"}`),
|
||||
},
|
||||
schematic: &common.Schematic{
|
||||
CUE: &common.CUE{Template: "parameter: {name: string}"},
|
||||
},
|
||||
wantCap: types.Capability{
|
||||
Name: "test-cap-3",
|
||||
Description: "test",
|
||||
CueTemplate: "parameter: {name: string}",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
"with invalid JSON in RawExtension": {
|
||||
capName: "test-cap-4",
|
||||
in: &runtime.RawExtension{
|
||||
Raw: []byte(`{"template": "parameter: {age: int}"`),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"with no template": {
|
||||
capName: "test-cap-5",
|
||||
in: &runtime.RawExtension{Raw: []byte(`{"description": "test"}`)},
|
||||
wantCap: types.Capability{
|
||||
Name: "test-cap-5",
|
||||
Description: "test",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cap, err := ConvertTemplateJSON2Object(tc.capName, tc.in, tc.schematic)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if diff := cmp.Diff(tc.wantCap, cap); diff != "" {
|
||||
t.Errorf("ConvertTemplateJSON2Object() (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateAsStatusRequest(t *testing.T) {
|
||||
tmpl := &Template{
|
||||
Health: "isHealth: true",
|
||||
CustomStatus: "message: 'Ready'",
|
||||
Details: "details: 'some details'",
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"param1": "value1",
|
||||
}
|
||||
statusReq := tmpl.AsStatusRequest(params)
|
||||
|
||||
assert.Equal(t, "isHealth: true", statusReq.Health)
|
||||
assert.Equal(t, "message: 'Ready'", statusReq.Custom)
|
||||
assert.Equal(t, "details: 'some details'", statusReq.Details)
|
||||
assert.Equal(t, params, statusReq.Parameter)
|
||||
}
|
||||
|
||||
func TestIsNotFoundInAppRevision(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
"component definition not found": {
|
||||
err: fmt.Errorf("component definition [my-comp] not found in app revision [my-app-rev]"),
|
||||
expected: true,
|
||||
},
|
||||
"trait definition not found": {
|
||||
err: fmt.Errorf("trait definition [my-trait] not found in app revision [my-app-rev]"),
|
||||
expected: true,
|
||||
},
|
||||
"policy definition not found": {
|
||||
err: fmt.Errorf("policy definition [my-policy] not found in app revision [my-app-rev]"),
|
||||
expected: true,
|
||||
},
|
||||
"workflow step definition not found": {
|
||||
err: fmt.Errorf("workflow step definition [my-step] not found in app revision [my-app-rev]"),
|
||||
expected: true,
|
||||
},
|
||||
"different error": {
|
||||
err: errors.New("a completely different error"),
|
||||
expected: false,
|
||||
},
|
||||
"nil error": {
|
||||
err: nil,
|
||||
expected: false,
|
||||
},
|
||||
"error with similar text but not exactly": {
|
||||
err: fmt.Errorf("this resource is not found in revision of app"),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result := IsNotFoundInAppRevision(tc.err)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
Copyright 2023 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 appfile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
)
|
||||
|
||||
func TestIsNotFoundInAppFile(t *testing.T) {
|
||||
require.True(t, IsNotFoundInAppFile(fmt.Errorf("ComponentDefinition XXX not found in appfile")))
|
||||
}
|
||||
|
||||
func TestIsNotFoundInAppRevision(t *testing.T) {
|
||||
require.True(t, IsNotFoundInAppRevision(fmt.Errorf("ComponentDefinition XXX not found in app revision")))
|
||||
}
|
||||
|
||||
func TestParseComponentFromRevisionAndClient(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cli := fake.NewClientBuilder().WithScheme(scheme).Build()
|
||||
p := &Parser{
|
||||
client: cli,
|
||||
tmplLoader: LoadTemplate,
|
||||
}
|
||||
comp := common.ApplicationComponent{
|
||||
Name: "test",
|
||||
Type: "test",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{}`)},
|
||||
Traits: []common.ApplicationTrait{{
|
||||
Type: "tr",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{}`)},
|
||||
}, {
|
||||
Type: "internal",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{}`)},
|
||||
}},
|
||||
}
|
||||
appRev := &v1beta1.ApplicationRevision{}
|
||||
cd := &v1beta1.ComponentDefinition{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
|
||||
td := &v1beta1.TraitDefinition{ObjectMeta: metav1.ObjectMeta{Name: "tr"}}
|
||||
require.NoError(t, cli.Create(ctx, cd))
|
||||
require.NoError(t, cli.Create(ctx, td))
|
||||
appRev.Spec.TraitDefinitions = map[string]*v1beta1.TraitDefinition{"internal": {}}
|
||||
_, err := p.ParseComponentFromRevisionAndClient(ctx, comp, appRev)
|
||||
require.NoError(t, err)
|
||||
|
||||
_comp1 := comp.DeepCopy()
|
||||
_comp1.Type = "bad"
|
||||
_, err = p.ParseComponentFromRevisionAndClient(ctx, *_comp1, appRev)
|
||||
require.Error(t, err)
|
||||
|
||||
_comp2 := comp.DeepCopy()
|
||||
_comp2.Traits[0].Type = "bad"
|
||||
_, err = p.ParseComponentFromRevisionAndClient(ctx, *_comp2, appRev)
|
||||
require.Error(t, err)
|
||||
|
||||
_comp3 := comp.DeepCopy()
|
||||
_comp3.Traits[0].Properties = &runtime.RawExtension{Raw: []byte(`bad`)}
|
||||
_, err = p.ParseComponentFromRevisionAndClient(ctx, *_comp3, appRev)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
@ -17,67 +17,32 @@ limitations under the License.
|
|||
package appfile
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"testing"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"github.com/stretchr/testify/assert"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/cue/definition"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
)
|
||||
|
||||
var _ = Describe("Test validate CUE schematic Appfile", func() {
|
||||
func TestTrait_EvalContext_OutputNameUniqueness(t *testing.T) {
|
||||
type SubTestCase struct {
|
||||
name string
|
||||
compDefTmpl string
|
||||
traitDefTmpl1 string
|
||||
traitDefTmpl2 string
|
||||
wantErrMsg string
|
||||
}
|
||||
|
||||
DescribeTable("Test validate outputs name unique", func(tc SubTestCase) {
|
||||
Expect("").Should(BeEmpty())
|
||||
wl := &Component{
|
||||
Name: "myweb",
|
||||
Type: "worker",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "myscaler",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: tc.traitDefTmpl1,
|
||||
engine: definition.NewTraitAbstractEngine("myscaler"),
|
||||
},
|
||||
{
|
||||
Name: "myingress",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: tc.traitDefTmpl2,
|
||||
engine: definition.NewTraitAbstractEngine("myingress"),
|
||||
},
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: tc.compDefTmpl,
|
||||
},
|
||||
engine: definition.NewWorkloadAbstractEngine("myweb"),
|
||||
}
|
||||
|
||||
ctxData := GenerateContextDataFromAppFile(&Appfile{
|
||||
Name: "myapp",
|
||||
Namespace: "test-ns",
|
||||
AppRevisionName: "myapp-v1",
|
||||
}, wl.Name)
|
||||
pCtx, err := newValidationProcessContext(wl, ctxData)
|
||||
Expect(err).Should(BeNil())
|
||||
Eventually(func() string {
|
||||
for _, tr := range wl.Traits {
|
||||
if err := tr.EvalContext(pCtx); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}).Should(ContainSubstring(tc.wantErrMsg))
|
||||
},
|
||||
Entry("Succeed", SubTestCase{
|
||||
testCases := []SubTestCase{
|
||||
{
|
||||
name: "Succeed",
|
||||
compDefTmpl: `
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
}
|
||||
outputs: mysvc: {
|
||||
|
|
@ -98,11 +63,12 @@ var _ = Describe("Test validate CUE schematic Appfile", func() {
|
|||
}
|
||||
`,
|
||||
wantErrMsg: "",
|
||||
}),
|
||||
Entry("CompDef and TraitDef have same outputs", SubTestCase{
|
||||
},
|
||||
{
|
||||
name: "CompDef and TraitDef have same outputs",
|
||||
compDefTmpl: `
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
}
|
||||
outputs: mysvc1: {
|
||||
|
|
@ -123,11 +89,12 @@ var _ = Describe("Test validate CUE schematic Appfile", func() {
|
|||
}
|
||||
`,
|
||||
wantErrMsg: `auxiliary "mysvc1" already exits`,
|
||||
}),
|
||||
Entry("TraitDefs have same outputs", SubTestCase{
|
||||
},
|
||||
{
|
||||
name: "TraitDefs have same outputs",
|
||||
compDefTmpl: `
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
}
|
||||
outputs: mysvc: {
|
||||
|
|
@ -148,41 +115,72 @@ var _ = Describe("Test validate CUE schematic Appfile", func() {
|
|||
}
|
||||
`,
|
||||
wantErrMsg: `auxiliary "mysvc1" already exits`,
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
var _ = Describe("Test ValidateComponentParams", func() {
|
||||
type ParamTestCase struct {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wl := &Component{
|
||||
Name: "myweb",
|
||||
Type: "worker",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "myscaler",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: tc.traitDefTmpl1,
|
||||
engine: definition.NewTraitAbstractEngine("myscaler"),
|
||||
},
|
||||
{
|
||||
Name: "myingress",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: tc.traitDefTmpl2,
|
||||
engine: definition.NewTraitAbstractEngine("myingress"),
|
||||
},
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: tc.compDefTmpl,
|
||||
},
|
||||
engine: definition.NewWorkloadAbstractEngine("myweb"),
|
||||
}
|
||||
|
||||
ctxData := GenerateContextDataFromAppFile(&Appfile{
|
||||
Name: "myapp",
|
||||
Namespace: "test-ns",
|
||||
AppRevisionName: "myapp-v1",
|
||||
}, wl.Name)
|
||||
pCtx, err := newValidationProcessContext(wl, ctxData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var evalErr error
|
||||
for _, tr := range wl.Traits {
|
||||
if err := tr.EvalContext(pCtx); err != nil {
|
||||
evalErr = err
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if tc.wantErrMsg != "" {
|
||||
assert.Error(t, evalErr)
|
||||
assert.Contains(t, evalErr.Error(), tc.wantErrMsg)
|
||||
} else {
|
||||
assert.NoError(t, evalErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParser_ValidateComponentParams(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
compName string
|
||||
template string
|
||||
params map[string]interface{}
|
||||
wantErr string
|
||||
}
|
||||
|
||||
DescribeTable("ValidateComponentParams cases", func(tc ParamTestCase) {
|
||||
wl := &Component{
|
||||
Name: tc.name,
|
||||
Type: "worker",
|
||||
FullTemplate: &Template{TemplateStr: tc.template},
|
||||
Params: tc.params,
|
||||
}
|
||||
app := &Appfile{
|
||||
Name: "myapp",
|
||||
Namespace: "test-ns",
|
||||
}
|
||||
ctxData := GenerateContextDataFromAppFile(app, wl.Name)
|
||||
parser := &Parser{}
|
||||
err := parser.ValidateComponentParams(ctxData, wl, app)
|
||||
if tc.wantErr == "" {
|
||||
Expect(err).To(BeNil())
|
||||
} else {
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring(tc.wantErr))
|
||||
}
|
||||
},
|
||||
Entry("valid params and template", ParamTestCase{
|
||||
name: "valid",
|
||||
}{
|
||||
{
|
||||
name: "valid params and template",
|
||||
compName: "valid",
|
||||
template: `
|
||||
parameter: {
|
||||
replicas: int | *1
|
||||
|
|
@ -196,9 +194,10 @@ var _ = Describe("Test ValidateComponentParams", func() {
|
|||
"replicas": 2,
|
||||
},
|
||||
wantErr: "",
|
||||
}),
|
||||
Entry("invalid CUE in template", ParamTestCase{
|
||||
name: "invalid-cue",
|
||||
},
|
||||
{
|
||||
name: "invalid CUE in template",
|
||||
compName: "invalid-cue",
|
||||
template: `
|
||||
parameter: {
|
||||
replicas: int | *1
|
||||
|
|
@ -213,9 +212,10 @@ var _ = Describe("Test ValidateComponentParams", func() {
|
|||
"replicas": 2,
|
||||
},
|
||||
wantErr: "CUE compile error",
|
||||
}),
|
||||
Entry("missing required parameter", ParamTestCase{
|
||||
name: "missing-required",
|
||||
},
|
||||
{
|
||||
name: "missing required parameter",
|
||||
compName: "missing-required",
|
||||
template: `
|
||||
parameter: {
|
||||
replicas: int
|
||||
|
|
@ -227,9 +227,10 @@ var _ = Describe("Test ValidateComponentParams", func() {
|
|||
`,
|
||||
params: map[string]interface{}{},
|
||||
wantErr: "component \"missing-required\": missing parameters: replicas",
|
||||
}),
|
||||
Entry("parameter constraint violation", ParamTestCase{
|
||||
name: "constraint-violation",
|
||||
},
|
||||
{
|
||||
name: "parameter constraint violation",
|
||||
compName: "constraint-violation",
|
||||
template: `
|
||||
parameter: {
|
||||
replicas: int & >0
|
||||
|
|
@ -243,9 +244,10 @@ var _ = Describe("Test ValidateComponentParams", func() {
|
|||
"replicas": -1,
|
||||
},
|
||||
wantErr: "parameter constraint violation",
|
||||
}),
|
||||
Entry("invalid parameter block", ParamTestCase{
|
||||
name: "invalid-param-block",
|
||||
},
|
||||
{
|
||||
name: "invalid parameter block",
|
||||
compName: "invalid-param-block",
|
||||
template: `
|
||||
parameter: {
|
||||
replicas: int | *1
|
||||
|
|
@ -259,6 +261,340 @@ var _ = Describe("Test ValidateComponentParams", func() {
|
|||
"replicas": "not-an-int",
|
||||
},
|
||||
wantErr: "parameter constraint violation",
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wl := &Component{
|
||||
Name: tc.compName,
|
||||
Type: "worker",
|
||||
FullTemplate: &Template{TemplateStr: tc.template},
|
||||
Params: tc.params,
|
||||
}
|
||||
app := &Appfile{
|
||||
Name: "myapp",
|
||||
Namespace: "test-ns",
|
||||
}
|
||||
ctxData := GenerateContextDataFromAppFile(app, wl.Name)
|
||||
parser := &Parser{}
|
||||
err := parser.ValidateComponentParams(ctxData, wl, app)
|
||||
if tc.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationHelpers(t *testing.T) {
|
||||
t.Run("renderTemplate", func(t *testing.T) {
|
||||
tmpl := "output: {}"
|
||||
expected := "output: {}\ncontext: _\nparameter: _\n"
|
||||
assert.Equal(t, expected, renderTemplate(tmpl))
|
||||
})
|
||||
|
||||
t.Run("cueParamBlock", func(t *testing.T) {
|
||||
t.Run("should handle empty params", func(t *testing.T) {
|
||||
out, err := cueParamBlock(map[string]any{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "parameter: {}", out)
|
||||
})
|
||||
|
||||
t.Run("should handle valid params", func(t *testing.T) {
|
||||
params := map[string]any{"key": "value"}
|
||||
out, err := cueParamBlock(params)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `parameter: {"key":"value"}`, out)
|
||||
})
|
||||
|
||||
t.Run("should return error for unmarshallable params", func(t *testing.T) {
|
||||
params := map[string]any{"key": make(chan int)}
|
||||
_, err := cueParamBlock(params)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("filterMissing", func(t *testing.T) {
|
||||
t.Run("should filter missing keys", func(t *testing.T) {
|
||||
keys := []string{"a", "b.c", "d"}
|
||||
provided := map[string]any{
|
||||
"a": 1,
|
||||
"b": map[string]any{
|
||||
"c": 2,
|
||||
},
|
||||
}
|
||||
out, err := filterMissing(keys, provided)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"d"}, out)
|
||||
})
|
||||
|
||||
t.Run("should handle no missing keys", func(t *testing.T) {
|
||||
keys := []string{"a"}
|
||||
provided := map[string]any{"a": 1}
|
||||
out, err := filterMissing(keys, provided)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, out)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("requiredFields", func(t *testing.T) {
|
||||
t.Run("should identify required fields", func(t *testing.T) {
|
||||
cueStr := `
|
||||
parameter: {
|
||||
name: string
|
||||
age: int
|
||||
nested: {
|
||||
field1: string
|
||||
field2: bool
|
||||
}
|
||||
}
|
||||
`
|
||||
var r cue.Runtime
|
||||
inst, err := r.Compile("", cueStr)
|
||||
assert.NoError(t, err)
|
||||
val := inst.Value()
|
||||
paramVal := val.LookupPath(cue.ParsePath("parameter"))
|
||||
|
||||
fields, err := requiredFields(paramVal)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{"name", "age", "nested.field1", "nested.field2"}, fields)
|
||||
})
|
||||
|
||||
t.Run("should ignore optional and default fields", func(t *testing.T) {
|
||||
cueStr := `
|
||||
parameter: {
|
||||
name: string
|
||||
age?: int
|
||||
location: string | *"unknown"
|
||||
nested: {
|
||||
field1: string
|
||||
field2?: bool
|
||||
}
|
||||
}
|
||||
`
|
||||
var r cue.Runtime
|
||||
inst, err := r.Compile("", cueStr)
|
||||
assert.NoError(t, err)
|
||||
val := inst.Value()
|
||||
paramVal := val.LookupPath(cue.ParsePath("parameter"))
|
||||
|
||||
fields, err := requiredFields(paramVal)
|
||||
assert.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{"name", "nested.field1"}, fields)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnforceRequiredParams(t *testing.T) {
|
||||
var r cue.Runtime
|
||||
cueStr := `
|
||||
parameter: {
|
||||
image: string
|
||||
replicas: int
|
||||
port: int
|
||||
data: {
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
`
|
||||
inst, err := r.Compile("", cueStr)
|
||||
assert.NoError(t, err)
|
||||
root := inst.Value()
|
||||
|
||||
t.Run("should pass if all params are provided directly", func(t *testing.T) {
|
||||
params := map[string]any{
|
||||
"image": "nginx",
|
||||
"replicas": 2,
|
||||
"port": 80,
|
||||
"data": map[string]any{
|
||||
"key": "k",
|
||||
"value": "v",
|
||||
},
|
||||
}
|
||||
app := &Appfile{}
|
||||
err := enforceRequiredParams(root, params, app)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should fail if params are missing", func(t *testing.T) {
|
||||
params := map[string]any{
|
||||
"image": "nginx",
|
||||
}
|
||||
app := &Appfile{}
|
||||
err := enforceRequiredParams(root, params, app)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing parameters: replicas,port,data.key,data.value")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParser_ValidateCUESchematicAppfile(t *testing.T) {
|
||||
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(string(features.EnableCueValidation)+"=true"))
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, utilfeature.DefaultMutableFeatureGate.Set(string(features.EnableCueValidation)+"=false"))
|
||||
})
|
||||
|
||||
t.Run("should validate a valid CUE schematic appfile", func(t *testing.T) {
|
||||
appfile := &Appfile{
|
||||
Name: "test-app",
|
||||
Namespace: "test-ns",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "my-comp",
|
||||
Type: "worker",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Params: map[string]any{
|
||||
"image": "nginx",
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: `
|
||||
parameter: {
|
||||
image: string
|
||||
}
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [{
|
||||
name: "my-container"
|
||||
image: parameter.image
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "my-trait",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: `
|
||||
parameter: {
|
||||
domain: string
|
||||
}
|
||||
patch: {}
|
||||
`,
|
||||
Params: map[string]any{
|
||||
"domain": "example.com",
|
||||
},
|
||||
engine: definition.NewTraitAbstractEngine("my-trait"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := &Parser{}
|
||||
err := p.ValidateCUESchematicAppfile(appfile)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("should return error for invalid trait evaluation", func(t *testing.T) {
|
||||
appfile := &Appfile{
|
||||
Name: "test-app",
|
||||
Namespace: "test-ns",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "my-comp",
|
||||
Type: "worker",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Params: map[string]any{
|
||||
"image": "nginx",
|
||||
},
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: `
|
||||
parameter: {
|
||||
image: string
|
||||
}
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
}
|
||||
`,
|
||||
},
|
||||
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
||||
Traits: []*Trait{
|
||||
{
|
||||
Name: "my-trait",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Template: `
|
||||
// invalid CUE template
|
||||
parameter: {
|
||||
domain: string
|
||||
}
|
||||
patch: {
|
||||
invalid: {
|
||||
}
|
||||
`,
|
||||
Params: map[string]any{
|
||||
"domain": "example.com",
|
||||
},
|
||||
engine: definition.NewTraitAbstractEngine("my-trait"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := &Parser{}
|
||||
err := p.ValidateCUESchematicAppfile(appfile)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot evaluate trait \"my-trait\"")
|
||||
})
|
||||
|
||||
t.Run("should return error for missing parameters", func(t *testing.T) {
|
||||
appfile := &Appfile{
|
||||
Name: "test-app",
|
||||
Namespace: "test-ns",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "my-comp",
|
||||
Type: "worker",
|
||||
CapabilityCategory: types.CUECategory,
|
||||
Params: map[string]any{}, // no params provided
|
||||
FullTemplate: &Template{
|
||||
TemplateStr: `
|
||||
parameter: {
|
||||
image: string
|
||||
}
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
}
|
||||
`,
|
||||
},
|
||||
engine: definition.NewWorkloadAbstractEngine("my-comp"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p := &Parser{}
|
||||
err := p.ValidateCUESchematicAppfile(appfile)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing parameters: image")
|
||||
})
|
||||
|
||||
t.Run("should skip non-CUE components", func(t *testing.T) {
|
||||
appfile := &Appfile{
|
||||
Name: "test-app",
|
||||
Namespace: "test-ns",
|
||||
ParsedComponents: []*Component{
|
||||
{
|
||||
Name: "my-comp",
|
||||
Type: "helm",
|
||||
CapabilityCategory: types.TerraformCategory,
|
||||
},
|
||||
},
|
||||
}
|
||||
p := &Parser{}
|
||||
err := p.ValidateCUESchematicAppfile(appfile)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
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 component
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
|
||||
pkgcommon "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
)
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
testEnv = &envtest.Environment{
|
||||
ControlPlaneStartTimeout: time.Minute * 3,
|
||||
ControlPlaneStopTimeout: time.Minute,
|
||||
UseExistingCluster: ptr.To(false),
|
||||
CRDDirectoryPaths: []string{"./testdata"},
|
||||
}
|
||||
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start test environment: %v", err)
|
||||
}
|
||||
|
||||
cfg.Timeout = time.Minute * 2
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: pkgcommon.Scheme})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create new kube client: %v", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err = testEnv.Stop(); err != nil {
|
||||
log.Printf("Failed to tear down the test environment: %v", err)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
|
@ -1,381 +0,0 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/rest"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/utils/ptr"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
pkgcommon "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
)
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Utils Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
By("bootstrapping test environment for utils test")
|
||||
|
||||
testEnv = &envtest.Environment{
|
||||
ControlPlaneStartTimeout: time.Minute * 3,
|
||||
ControlPlaneStopTimeout: time.Minute,
|
||||
UseExistingCluster: ptr.To(false),
|
||||
CRDDirectoryPaths: []string{"./testdata"},
|
||||
}
|
||||
|
||||
By("start kube test env")
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
By("new kube client")
|
||||
cfg.Timeout = time.Minute * 2
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: pkgcommon.Scheme})
|
||||
Expect(err).Should(Succeed())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
var _ = Describe("Test ref-objects functions", func() {
|
||||
It("Test SelectRefObjectsForDispatch", func() {
|
||||
featuregatetesting.SetFeatureGateDuringTest(GinkgoT(), utilfeature.DefaultFeatureGate, features.LegacyObjectTypeIdentifier, true)
|
||||
featuregatetesting.SetFeatureGateDuringTest(GinkgoT(), utilfeature.DefaultFeatureGate, features.DeprecatedObjectLabelSelector, true)
|
||||
By("Create objects")
|
||||
Expect(k8sClient.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}})).Should(Succeed())
|
||||
for _, obj := range []client.Object{&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "dynamic",
|
||||
Namespace: "test",
|
||||
},
|
||||
}, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "dynamic",
|
||||
Namespace: "test",
|
||||
Generation: int64(5),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.0.0.254",
|
||||
Ports: []corev1.ServicePort{{Port: 80}},
|
||||
},
|
||||
}, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "by-label-1",
|
||||
Namespace: "test",
|
||||
Labels: map[string]string{"key": "value"},
|
||||
},
|
||||
}, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "by-label-2",
|
||||
Namespace: "test",
|
||||
Labels: map[string]string{"key": "value"},
|
||||
},
|
||||
}, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cluster-role",
|
||||
},
|
||||
}} {
|
||||
Expect(k8sClient.Create(context.Background(), obj)).Should(Succeed())
|
||||
}
|
||||
createUnstructured := func(apiVersion string, kind string, name string, namespace string, labels map[string]interface{}) *unstructured.Unstructured {
|
||||
un := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]interface{}{"name": name},
|
||||
},
|
||||
}
|
||||
if namespace != "" {
|
||||
un.SetNamespace(namespace)
|
||||
}
|
||||
if labels != nil {
|
||||
un.Object["metadata"].(map[string]interface{})["labels"] = labels
|
||||
}
|
||||
return un
|
||||
}
|
||||
testcases := map[string]struct {
|
||||
Input v1alpha1.ObjectReferrer
|
||||
compName string
|
||||
appNs string
|
||||
Output []*unstructured.Unstructured
|
||||
Error string
|
||||
Scope string
|
||||
IsService bool
|
||||
IsClusterRole bool
|
||||
}{
|
||||
"normal": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"legacy-type-identifier": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{LegacyObjectTypeIdentifier: v1alpha1.LegacyObjectTypeIdentifier{Kind: "ConfigMap", APIVersion: "v1"}},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"invalid-apiVersion": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{LegacyObjectTypeIdentifier: v1alpha1.LegacyObjectTypeIdentifier{Kind: "ConfigMap", APIVersion: "a/b/v1"}},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "invalid APIVersion",
|
||||
},
|
||||
"invalid-type-identifier": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "neither resource or apiVersion/kind is set",
|
||||
},
|
||||
"name-and-selector-both-set": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", LabelSelector: map[string]string{"key": "value"}}},
|
||||
appNs: "test",
|
||||
Error: "invalid object selector for ref-objects, name and labelSelector cannot be both set",
|
||||
},
|
||||
"empty-ref-object-name": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"}},
|
||||
compName: "dynamic",
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cannot-find-ref-object": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "static"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "failed to load ref object",
|
||||
},
|
||||
"modify-service": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "service"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
IsService: true,
|
||||
},
|
||||
"by-labels": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{LabelSelector: map[string]string{"key": "value"}},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{
|
||||
createUnstructured("v1", "ConfigMap", "by-label-1", "test", map[string]interface{}{"key": "value"}),
|
||||
createUnstructured("v1", "ConfigMap", "by-label-2", "test", map[string]interface{}{"key": "value"}),
|
||||
},
|
||||
},
|
||||
"by-deprecated-labels": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{DeprecatedLabelSelector: map[string]string{"key": "value"}},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{
|
||||
createUnstructured("v1", "ConfigMap", "by-label-1", "test", map[string]interface{}{"key": "value"}),
|
||||
createUnstructured("v1", "ConfigMap", "by-label-2", "test", map[string]interface{}{"key": "value"}),
|
||||
},
|
||||
},
|
||||
"no-kind-for-resource": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "unknown"}},
|
||||
appNs: "test",
|
||||
Error: "no matches",
|
||||
},
|
||||
"cross-namespace": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Namespace: "test"},
|
||||
},
|
||||
appNs: "demo",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cross-namespace-forbidden": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Namespace: "test"},
|
||||
},
|
||||
appNs: "demo",
|
||||
Scope: RefObjectsAvailableScopeNamespace,
|
||||
Error: "cannot refer to objects outside the application's namespace",
|
||||
},
|
||||
"cross-cluster": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Cluster: "demo"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cross-cluster-forbidden": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Cluster: "demo"},
|
||||
},
|
||||
appNs: "test",
|
||||
Scope: RefObjectsAvailableScopeCluster,
|
||||
Error: "cannot refer to objects outside control plane",
|
||||
},
|
||||
"test-cluster-scope-resource": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "clusterrole"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "test-cluster-role"},
|
||||
},
|
||||
appNs: "test",
|
||||
Scope: RefObjectsAvailableScopeCluster,
|
||||
Output: []*unstructured.Unstructured{createUnstructured("rbac.authorization.k8s.io/v1", "ClusterRole", "test-cluster-role", "", nil)},
|
||||
IsClusterRole: true,
|
||||
},
|
||||
}
|
||||
for name, tt := range testcases {
|
||||
By("Test " + name)
|
||||
if tt.Scope == "" {
|
||||
tt.Scope = RefObjectsAvailableScopeGlobal
|
||||
}
|
||||
RefObjectsAvailableScope = tt.Scope
|
||||
output, err := SelectRefObjectsForDispatch(context.Background(), k8sClient, tt.appNs, tt.compName, tt.Input)
|
||||
if tt.Error != "" {
|
||||
Expect(err).ShouldNot(BeNil())
|
||||
Expect(err.Error()).Should(ContainSubstring(tt.Error))
|
||||
} else {
|
||||
Expect(err).Should(Succeed())
|
||||
if tt.IsService {
|
||||
Expect(output[0].Object["kind"]).Should(Equal("Service"))
|
||||
Expect(output[0].Object["spec"].(map[string]interface{})["clusterIP"]).Should(BeNil())
|
||||
} else {
|
||||
if tt.IsClusterRole {
|
||||
delete(output[0].Object, "rules")
|
||||
}
|
||||
Expect(output).Should(Equal(tt.Output))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
It("Test AppendUnstructuredObjects", func() {
|
||||
testCases := map[string]struct {
|
||||
Inputs []*unstructured.Unstructured
|
||||
Input *unstructured.Unstructured
|
||||
Outputs []*unstructured.Unstructured
|
||||
}{
|
||||
"overlap": {
|
||||
Inputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}},
|
||||
Input: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "c",
|
||||
}},
|
||||
Outputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "c",
|
||||
}}},
|
||||
},
|
||||
"append": {
|
||||
Inputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}},
|
||||
Input: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "z", "namespace": "default"},
|
||||
"data": "c",
|
||||
}},
|
||||
Outputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "z", "namespace": "default"},
|
||||
"data": "c",
|
||||
}}},
|
||||
},
|
||||
}
|
||||
for name, tt := range testCases {
|
||||
By("Test " + name)
|
||||
Expect(AppendUnstructuredObjects(tt.Inputs, tt.Input)).Should(Equal(tt.Outputs))
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -0,0 +1,748 @@
|
|||
/*
|
||||
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 component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
)
|
||||
|
||||
func TestGetLabelSelectorFromRefObjectSelector(t *testing.T) {
|
||||
type args struct {
|
||||
selector v1alpha1.ObjectReferrer
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
featureOn bool
|
||||
want map[string]string
|
||||
}{
|
||||
{
|
||||
name: "label selector present",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
LabelSelector: map[string]string{"app": "my-app"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[string]string{"app": "my-app"},
|
||||
},
|
||||
{
|
||||
name: "deprecated label selector present and feature on",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
DeprecatedLabelSelector: map[string]string{"app": "my-app-deprecated"},
|
||||
},
|
||||
},
|
||||
},
|
||||
featureOn: true,
|
||||
want: map[string]string{"app": "my-app-deprecated"},
|
||||
},
|
||||
{
|
||||
name: "deprecated label selector present and feature off",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
DeprecatedLabelSelector: map[string]string{"app": "my-app-deprecated"},
|
||||
},
|
||||
},
|
||||
},
|
||||
featureOn: false,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "both present, label selector takes precedence",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
LabelSelector: map[string]string{"app": "my-app"},
|
||||
DeprecatedLabelSelector: map[string]string{"app": "my-app-deprecated"},
|
||||
},
|
||||
},
|
||||
},
|
||||
featureOn: true,
|
||||
want: map[string]string{"app": "my-app"},
|
||||
},
|
||||
{
|
||||
name: "no selector",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{},
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeprecatedObjectLabelSelector, tt.featureOn)
|
||||
if got := GetLabelSelectorFromRefObjectSelector(tt.args.selector); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetLabelSelectorFromRefObjectSelector() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRefObjectSelector(t *testing.T) {
|
||||
type args struct {
|
||||
selector v1alpha1.ObjectReferrer
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "name only",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
Name: "my-obj",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "label selector only",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
LabelSelector: map[string]string{"app": "my-app"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "both name and label selector",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{
|
||||
ObjectSelector: v1alpha1.ObjectSelector{
|
||||
Name: "my-obj",
|
||||
LabelSelector: map[string]string{"app": "my-app"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty selector",
|
||||
args: args{
|
||||
selector: v1alpha1.ObjectReferrer{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateRefObjectSelector(tt.args.selector); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateRefObjectSelector() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearRefObjectForDispatch(t *testing.T) {
|
||||
un := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-obj",
|
||||
"namespace": "default",
|
||||
"resourceVersion": "12345",
|
||||
"generation": int64(1),
|
||||
"uid": "abc-def",
|
||||
"creationTimestamp": "2021-01-01T00:00:00Z",
|
||||
"managedFields": []interface{}{},
|
||||
"ownerReferences": []interface{}{},
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"phase": "Available",
|
||||
},
|
||||
},
|
||||
}
|
||||
ClearRefObjectForDispatch(un)
|
||||
|
||||
if un.GetResourceVersion() != "" {
|
||||
t.Errorf("resourceVersion should be cleared")
|
||||
}
|
||||
if un.GetGeneration() != 0 {
|
||||
t.Errorf("generation should be cleared")
|
||||
}
|
||||
if len(un.GetOwnerReferences()) != 0 {
|
||||
t.Errorf("ownerReferences should be cleared")
|
||||
}
|
||||
if un.GetDeletionTimestamp() != nil {
|
||||
t.Errorf("deletionTimestamp should be nil")
|
||||
}
|
||||
if len(un.GetManagedFields()) != 0 {
|
||||
t.Errorf("managedFields should be cleared")
|
||||
}
|
||||
if un.GetUID() != "" {
|
||||
t.Errorf("uid should be cleared")
|
||||
}
|
||||
if _, found, _ := unstructured.NestedFieldNoCopy(un.Object, "metadata", "creationTimestamp"); found {
|
||||
t.Errorf("creationTimestamp should be removed")
|
||||
}
|
||||
if _, found, _ := unstructured.NestedFieldNoCopy(un.Object, "status"); found {
|
||||
t.Errorf("status should be removed")
|
||||
}
|
||||
|
||||
// Test for service
|
||||
svc := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-svc",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"clusterIP": "1.2.3.4",
|
||||
"clusterIPs": []interface{}{"1.2.3.4"},
|
||||
},
|
||||
},
|
||||
}
|
||||
ClearRefObjectForDispatch(svc)
|
||||
if _, found, _ := unstructured.NestedString(svc.Object, "spec", "clusterIP"); found {
|
||||
t.Errorf("service clusterIP should be removed")
|
||||
}
|
||||
if _, found, _ := unstructured.NestedStringSlice(svc.Object, "spec", "clusterIPs"); found {
|
||||
t.Errorf("service clusterIPs should be removed")
|
||||
}
|
||||
|
||||
// Test for service with ClusterIPNone
|
||||
svcNone := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-svc-none",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"clusterIP": corev1.ClusterIPNone,
|
||||
},
|
||||
},
|
||||
}
|
||||
ClearRefObjectForDispatch(svcNone)
|
||||
if ip, found, _ := unstructured.NestedString(svcNone.Object, "spec", "clusterIP"); !found || ip != corev1.ClusterIPNone {
|
||||
t.Errorf("service with clusterIP None should not be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRefObjectsForDispatch(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.LegacyObjectTypeIdentifier, true)
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeprecatedObjectLabelSelector, true)
|
||||
t.Log("Create objects")
|
||||
if err := k8sClient.Create(context.Background(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, obj := range []client.Object{&corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "dynamic",
|
||||
Namespace: "test",
|
||||
},
|
||||
}, &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "dynamic",
|
||||
Namespace: "test",
|
||||
Generation: int64(5),
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
ClusterIP: "10.0.0.254",
|
||||
Ports: []corev1.ServicePort{{Port: 80}},
|
||||
},
|
||||
}, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "by-label-1",
|
||||
Namespace: "test",
|
||||
Labels: map[string]string{"key": "value"},
|
||||
},
|
||||
}, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "by-label-2",
|
||||
Namespace: "test",
|
||||
Labels: map[string]string{"key": "value"},
|
||||
},
|
||||
}, &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-cluster-role",
|
||||
},
|
||||
}} {
|
||||
if err := k8sClient.Create(context.Background(), obj); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
createUnstructured := func(apiVersion string, kind string, name string, namespace string, labels map[string]interface{}) *unstructured.Unstructured {
|
||||
un := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{"apiVersion": apiVersion,
|
||||
"kind": kind,
|
||||
"metadata": map[string]interface{}{"name": name},
|
||||
},
|
||||
}
|
||||
if namespace != "" {
|
||||
un.SetNamespace(namespace)
|
||||
}
|
||||
if labels != nil {
|
||||
un.Object["metadata"].(map[string]interface{})["labels"] = labels
|
||||
}
|
||||
return un
|
||||
}
|
||||
testcases := map[string]struct {
|
||||
Input v1alpha1.ObjectReferrer
|
||||
compName string
|
||||
appNs string
|
||||
Output []*unstructured.Unstructured
|
||||
Error string
|
||||
Scope string
|
||||
IsService bool
|
||||
IsClusterRole bool
|
||||
}{
|
||||
"normal": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"legacy-type-identifier": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{LegacyObjectTypeIdentifier: v1alpha1.LegacyObjectTypeIdentifier{Kind: "ConfigMap", APIVersion: "v1"}},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"invalid-apiVersion": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{LegacyObjectTypeIdentifier: v1alpha1.LegacyObjectTypeIdentifier{Kind: "ConfigMap", APIVersion: "a/b/v1"}},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "invalid APIVersion",
|
||||
},
|
||||
"invalid-type-identifier": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "neither resource or apiVersion/kind is set",
|
||||
},
|
||||
"name-and-selector-both-set": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", LabelSelector: map[string]string{"key": "value"}}},
|
||||
appNs: "test",
|
||||
Error: "invalid object selector for ref-objects, name and labelSelector cannot be both set",
|
||||
},
|
||||
"empty-ref-object-name": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"}},
|
||||
compName: "dynamic",
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cannot-find-ref-object": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "static"},
|
||||
},
|
||||
appNs: "test",
|
||||
Error: "failed to load ref object",
|
||||
},
|
||||
"modify-service": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "service"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic"},
|
||||
},
|
||||
appNs: "test",
|
||||
IsService: true,
|
||||
},
|
||||
"by-labels": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{LabelSelector: map[string]string{"key": "value"}},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{
|
||||
createUnstructured("v1", "ConfigMap", "by-label-1", "test", map[string]interface{}{"key": "value"}),
|
||||
createUnstructured("v1", "ConfigMap", "by-label-2", "test", map[string]interface{}{"key": "value"}),
|
||||
},
|
||||
},
|
||||
"by-deprecated-labels": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{DeprecatedLabelSelector: map[string]string{"key": "value"}},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{
|
||||
createUnstructured("v1", "ConfigMap", "by-label-1", "test", map[string]interface{}{"key": "value"}),
|
||||
createUnstructured("v1", "ConfigMap", "by-label-2", "test", map[string]interface{}{"key": "value"}),
|
||||
},
|
||||
},
|
||||
"no-kind-for-resource": {
|
||||
Input: v1alpha1.ObjectReferrer{ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "unknown"}},
|
||||
appNs: "test",
|
||||
Error: "no matches",
|
||||
},
|
||||
"cross-namespace": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Namespace: "test"},
|
||||
},
|
||||
appNs: "demo",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cross-namespace-forbidden": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Namespace: "test"},
|
||||
},
|
||||
appNs: "demo",
|
||||
Scope: RefObjectsAvailableScopeNamespace,
|
||||
Error: "cannot refer to objects outside the application's namespace",
|
||||
},
|
||||
"cross-cluster": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Cluster: "demo"},
|
||||
},
|
||||
appNs: "test",
|
||||
Output: []*unstructured.Unstructured{createUnstructured("v1", "ConfigMap", "dynamic", "test", nil)},
|
||||
},
|
||||
"cross-cluster-forbidden": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "configmap"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "dynamic", Cluster: "demo"},
|
||||
},
|
||||
appNs: "test",
|
||||
Scope: RefObjectsAvailableScopeCluster,
|
||||
Error: "cannot refer to objects outside control plane",
|
||||
},
|
||||
"test-cluster-scope-resource": {
|
||||
Input: v1alpha1.ObjectReferrer{
|
||||
ObjectTypeIdentifier: v1alpha1.ObjectTypeIdentifier{Resource: "clusterrole"},
|
||||
ObjectSelector: v1alpha1.ObjectSelector{Name: "test-cluster-role"},
|
||||
},
|
||||
appNs: "test",
|
||||
Scope: RefObjectsAvailableScopeCluster,
|
||||
Output: []*unstructured.Unstructured{createUnstructured("rbac.authorization.k8s.io/v1", "ClusterRole", "test-cluster-role", "", nil)},
|
||||
IsClusterRole: true,
|
||||
},
|
||||
}
|
||||
for name, tt := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if tt.Scope == "" {
|
||||
tt.Scope = RefObjectsAvailableScopeGlobal
|
||||
}
|
||||
RefObjectsAvailableScope = tt.Scope
|
||||
output, err := SelectRefObjectsForDispatch(context.Background(), k8sClient, tt.appNs, tt.compName, tt.Input)
|
||||
if tt.Error != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.Error) {
|
||||
t.Fatalf("expected error message to contain %q, got %q", tt.Error, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.IsService {
|
||||
if output[0].Object["kind"] != "Service" {
|
||||
t.Fatalf(`expected kind to be "Service", got %q`, output[0].Object["kind"])
|
||||
}
|
||||
if output[0].Object["spec"].(map[string]interface{})["clusterIP"] != nil {
|
||||
t.Fatalf(`expected clusterIP to be nil, got %v`, output[0].Object["spec"].(map[string]interface{})["clusterIP"])
|
||||
}
|
||||
} else {
|
||||
if tt.IsClusterRole {
|
||||
delete(output[0].Object, "rules")
|
||||
}
|
||||
if !reflect.DeepEqual(output, tt.Output) {
|
||||
t.Fatalf("expected output to be %v, got %v", tt.Output, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReferredObjectsDelegatingClient(t *testing.T) {
|
||||
objs := []*unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm-1",
|
||||
"namespace": "ns-1",
|
||||
"labels": map[string]interface{}{"app": "app-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm-2",
|
||||
"namespace": "ns-1",
|
||||
"labels": map[string]interface{}{"app": "app-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "secret-1",
|
||||
"namespace": "ns-1",
|
||||
"labels": map[string]interface{}{"app": "app-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
delegatingClient := ReferredObjectsDelegatingClient(k8sClient, objs)
|
||||
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
t.Run("should get existing object", func(t *testing.T) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"})
|
||||
err := delegatingClient.Get(context.Background(), client.ObjectKey{Name: "cm-1", Namespace: "ns-1"}, obj)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if obj.GetName() != "cm-1" {
|
||||
t.Errorf("expected object name cm-1, got %s", obj.GetName())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return not found for non-existing object", func(t *testing.T) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"})
|
||||
err := delegatingClient.Get(context.Background(), client.ObjectKey{Name: "cm-non-existent", Namespace: "ns-1"}, obj)
|
||||
if !apierrors.IsNotFound(err) {
|
||||
t.Errorf("expected not found error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should return not found for different kind", func(t *testing.T) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"})
|
||||
err := delegatingClient.Get(context.Background(), client.ObjectKey{Name: "cm-1", Namespace: "ns-1"}, obj)
|
||||
if !apierrors.IsNotFound(err) {
|
||||
t.Errorf("expected not found error, got %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("List", func(t *testing.T) {
|
||||
t.Run("should list all objects of a kind", func(t *testing.T) {
|
||||
list := &unstructured.UnstructuredList{}
|
||||
list.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMapList"})
|
||||
err := delegatingClient.List(context.Background(), list)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(list.Items) != 2 {
|
||||
t.Errorf("expected 2 items, got %d", len(list.Items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should list with namespace", func(t *testing.T) {
|
||||
list := &unstructured.UnstructuredList{}
|
||||
list.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMapList"})
|
||||
err := delegatingClient.List(context.Background(), list, client.InNamespace("ns-1"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(list.Items) != 2 {
|
||||
t.Errorf("expected 2 items, got %d", len(list.Items))
|
||||
}
|
||||
|
||||
list.Items = []unstructured.Unstructured{}
|
||||
err = delegatingClient.List(context.Background(), list, client.InNamespace("ns-2"))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(list.Items) != 0 {
|
||||
t.Errorf("expected 0 items, got %d", len(list.Items))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should list with label selector", func(t *testing.T) {
|
||||
list := &unstructured.UnstructuredList{}
|
||||
list.SetGroupVersionKind(schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMapList"})
|
||||
err := delegatingClient.List(context.Background(), list, client.MatchingLabels{"app": "app-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(list.Items) != 1 {
|
||||
t.Errorf("expected 1 item, got %d", len(list.Items))
|
||||
}
|
||||
if list.Items[0].GetName() != "cm-1" {
|
||||
t.Errorf("expected cm-1, got %s", list.Items[0].GetName())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAppendUnstructuredObjects(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Inputs []*unstructured.Unstructured
|
||||
Input *unstructured.Unstructured
|
||||
Outputs []*unstructured.Unstructured
|
||||
}{
|
||||
"overlap": {
|
||||
Inputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}},
|
||||
Input: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "c",
|
||||
}},
|
||||
Outputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "c",
|
||||
}}},
|
||||
},
|
||||
"append": {
|
||||
Inputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}},
|
||||
Input: &unstructured.Unstructured{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "z", "namespace": "default"},
|
||||
"data": "c",
|
||||
}},
|
||||
Outputs: []*unstructured.Unstructured{{Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "x", "namespace": "default"},
|
||||
"data": "a",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "y", "namespace": "default"},
|
||||
"data": "b",
|
||||
}}, {Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{"name": "z", "namespace": "default"},
|
||||
"data": "c",
|
||||
}}},
|
||||
},
|
||||
}
|
||||
for name, tt := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := AppendUnstructuredObjects(tt.Inputs, tt.Input)
|
||||
if !reflect.DeepEqual(got, tt.Outputs) {
|
||||
t.Fatalf("expected output to be %v, got %v", tt.Outputs, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertUnstructuredsToReferredObjects(t *testing.T) {
|
||||
uns := []*unstructured.Unstructured{
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "cm-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "deploy-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
refObjs, err := ConvertUnstructuredsToReferredObjects(uns)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(refObjs) != 2 {
|
||||
t.Fatalf("expected 2 referred objects, got %d", len(refObjs))
|
||||
}
|
||||
|
||||
for i, un := range uns {
|
||||
raw, err := json.Marshal(un)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal unstructured: %v", err)
|
||||
}
|
||||
expected := common.ReferredObject{
|
||||
RawExtension: runtime.RawExtension{Raw: raw},
|
||||
}
|
||||
if !reflect.DeepEqual(refObjs[i], expected) {
|
||||
t.Errorf("expected refObj %v, got %v", expected, refObjs[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue