mirror of
https://github.com/kubevela/kubevela.git
synced 2026-03-06 03:31:12 +00:00
599 lines
15 KiB
Go
599 lines
15 KiB
Go
package addon
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
v1 "k8s.io/api/core/v1"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"cuelang.org/go/cue"
|
|
cueyaml "cuelang.org/go/encoding/yaml"
|
|
"github.com/google/go-github/v32/github"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/oauth2"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
|
|
|
common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
utils2 "github.com/oam-dev/kubevela/pkg/controller/utils"
|
|
cuemodel "github.com/oam-dev/kubevela/pkg/cue/model"
|
|
"github.com/oam-dev/kubevela/pkg/cue/model/value"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
"github.com/oam-dev/kubevela/pkg/utils"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
)
|
|
|
|
const (
|
|
// ReadmeFileName is the addon readme file name
|
|
ReadmeFileName string = "readme.md"
|
|
|
|
// MetadataFileName is the addon meatadata.yaml file name
|
|
MetadataFileName string = "metadata.yaml"
|
|
|
|
// TemplateFileName is the addon template.yaml dir name
|
|
TemplateFileName string = "template.yaml"
|
|
|
|
// ResourcesDirName is the addon resources/ dir name
|
|
ResourcesDirName string = "resources"
|
|
|
|
// DefinitionsDirName is the addon definitions/ dir name
|
|
DefinitionsDirName string = "definitions"
|
|
)
|
|
|
|
type ListOptions struct {
|
|
GetDetail bool
|
|
GetDefinition bool
|
|
GetResource bool
|
|
GetParameter bool
|
|
GetTemplate bool
|
|
}
|
|
|
|
var (
|
|
ListLevelOptions = ListOptions{}
|
|
GetLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true}
|
|
EnableLevelOptions = ListOptions{GetDetail: true, GetDefinition: true, GetResource: true, GetTemplate: true, GetParameter: true}
|
|
)
|
|
|
|
type AddonErr error
|
|
|
|
var (
|
|
AddonNotExist AddonErr = errors.New("addon not exist")
|
|
)
|
|
|
|
type gitHelper struct {
|
|
Client *github.Client
|
|
Meta *utils.Content
|
|
}
|
|
|
|
// GitAddonSource defines the information about the Git as addon source
|
|
type GitAddonSource struct {
|
|
URL string `json:"url,omitempty" validate:"required"`
|
|
Path string `json:"path,omitempty"`
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
type AddonReader struct {
|
|
addon *types.Addon
|
|
h *gitHelper
|
|
item *github.RepositoryContent
|
|
errChan chan error
|
|
}
|
|
|
|
func (r *AddonReader) SetReadContent(content *github.RepositoryContent) {
|
|
r.item = content
|
|
}
|
|
|
|
// GetAddon get a addon info from GitAddonSource, can be used for get or enable
|
|
func GetAddon(name string, git *GitAddonSource, opt ListOptions) (*types.Addon, error) {
|
|
addon, err := getSingleAddonFromGit(git.URL, git.Path, name, git.Token, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return addon, nil
|
|
}
|
|
|
|
// ListAddons list addons' info from GitAddonSource
|
|
func ListAddons(git *GitAddonSource, opt ListOptions) ([]*types.Addon, error) {
|
|
gitAddons, err := getAddonsFromGit(git.URL, git.Path, git.Token, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gitAddons, nil
|
|
}
|
|
|
|
func getAddonsFromGit(baseURL, dir, token string, opt ListOptions) ([]*types.Addon, error) {
|
|
var addons []*types.Addon
|
|
var err error
|
|
var wg sync.WaitGroup
|
|
errChan := make(chan error, 1)
|
|
|
|
gith, err := createGitHelper(baseURL, dir, token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, items, err := gith.readRepo(gith.Meta.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, subItems := range items {
|
|
if subItems.GetType() != "dir" {
|
|
continue
|
|
}
|
|
wg.Add(1)
|
|
go func(item *github.RepositoryContent) {
|
|
defer wg.Done()
|
|
addonRes, err := getSingleAddonFromGit(baseURL, dir, item.GetName(), token, opt)
|
|
if err != nil {
|
|
errChan <- err
|
|
return
|
|
}
|
|
addons = append(addons, addonRes)
|
|
}(subItems)
|
|
}
|
|
wg.Wait()
|
|
if len(errChan) != 0 {
|
|
return nil, <-errChan
|
|
}
|
|
return addons, nil
|
|
}
|
|
|
|
func getSingleAddonFromGit(baseURL, dir, addonName, token string, opt ListOptions) (*types.Addon, error) {
|
|
var wg sync.WaitGroup
|
|
|
|
gith, err := createGitHelper(baseURL, path.Join(dir, addonName), token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, items, err := gith.readRepo(gith.Meta.Path)
|
|
|
|
reader := AddonReader{
|
|
addon: &types.Addon{},
|
|
h: gith,
|
|
errChan: make(chan error, 1),
|
|
}
|
|
for _, item := range items {
|
|
switch strings.ToLower(item.GetName()) {
|
|
case ReadmeFileName:
|
|
if !opt.GetDetail {
|
|
break
|
|
}
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readReadme(&wg, reader)
|
|
case MetadataFileName:
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readMetadata(&wg, reader)
|
|
case DefinitionsDirName:
|
|
if !opt.GetDefinition {
|
|
break
|
|
}
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readDefinitions(&wg, reader)
|
|
case ResourcesDirName:
|
|
if !opt.GetResource {
|
|
break
|
|
}
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readResources(&wg, reader)
|
|
case TemplateFileName:
|
|
if !opt.GetTemplate {
|
|
break
|
|
}
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readTemplate(&wg, reader)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
|
|
if opt.GetParameter && reader.addon.Parameters != "" {
|
|
err = genAddonAPISchema(reader.addon)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return reader.addon, nil
|
|
|
|
}
|
|
|
|
func readTemplate(wg *sync.WaitGroup, reader AddonReader) {
|
|
defer wg.Done()
|
|
content, _, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
data, err := content.GetContent()
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
|
reader.addon.AppTemplate = &v1beta1.Application{}
|
|
_, _, err = dec.Decode([]byte(data), nil, reader.addon.AppTemplate)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
}
|
|
|
|
func readResources(wg *sync.WaitGroup, reader AddonReader) {
|
|
defer wg.Done()
|
|
dirPath := strings.Split(reader.item.GetPath(), "/")
|
|
dirPath, err := cutPathUntil(dirPath, ResourcesDirName)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
}
|
|
|
|
_, items, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
for _, item := range items {
|
|
switch item.GetType() {
|
|
case "file":
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readResFile(wg, reader, dirPath)
|
|
case "dir":
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readResources(wg, reader)
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
// readResFile read single resource file
|
|
func readResFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) {
|
|
defer wg.Done()
|
|
content, _, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
b, err := content.GetContent()
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
|
|
if reader.item.GetName() == "parameter.cue" {
|
|
reader.addon.Parameters = b
|
|
return
|
|
}
|
|
switch filepath.Ext(reader.item.GetName()) {
|
|
case ".cue":
|
|
reader.addon.CUETemplates = append(reader.addon.CUETemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath})
|
|
default:
|
|
reader.addon.YAMLTemplates = append(reader.addon.YAMLTemplates, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath})
|
|
}
|
|
}
|
|
|
|
func readDefinitions(wg *sync.WaitGroup, reader AddonReader) {
|
|
defer wg.Done()
|
|
dirPath := strings.Split(reader.item.GetPath(), "/")
|
|
dirPath, err := cutPathUntil(dirPath, DefinitionsDirName)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
_, items, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
for _, item := range items {
|
|
switch item.GetType() {
|
|
case "file":
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readDefFile(wg, reader, dirPath)
|
|
case "dir":
|
|
reader.SetReadContent(item)
|
|
wg.Add(1)
|
|
go readDefinitions(wg, reader)
|
|
}
|
|
}
|
|
}
|
|
|
|
// readDefFile read single definition file
|
|
func readDefFile(wg *sync.WaitGroup, reader AddonReader, dirPath []string) {
|
|
defer wg.Done()
|
|
content, _, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
b, err := content.GetContent()
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
reader.addon.Definitions = append(reader.addon.Definitions, types.AddonElementFile{Data: b, Name: reader.item.GetName(), Path: dirPath})
|
|
}
|
|
|
|
func readMetadata(wg *sync.WaitGroup, reader AddonReader) {
|
|
defer wg.Done()
|
|
content, _, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
b, err := content.GetContent()
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
err = yaml.Unmarshal([]byte(b), &reader.addon.AddonMeta)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func readReadme(wg *sync.WaitGroup, reader AddonReader) {
|
|
defer wg.Done()
|
|
content, _, err := reader.h.readRepo(*reader.item.Path)
|
|
if err != nil {
|
|
reader.errChan <- err
|
|
return
|
|
}
|
|
reader.addon.Detail, err = content.GetContent()
|
|
return
|
|
}
|
|
|
|
func createGitHelper(baseURL, dir, token string) (*gitHelper, error) {
|
|
var ts oauth2.TokenSource
|
|
if token != "" {
|
|
ts = oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
|
|
}
|
|
tc := oauth2.NewClient(context.Background(), ts)
|
|
tc.Timeout = time.Second * 10
|
|
cli := github.NewClient(tc)
|
|
|
|
baseURL = strings.TrimSuffix(baseURL, ".git")
|
|
u, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, errors.New("addon registry invalid")
|
|
}
|
|
u.Path = path.Join(u.Path, dir)
|
|
_, gitmeta, err := utils.Parse(u.String())
|
|
if err != nil {
|
|
return nil, errors.New("addon registry invalid")
|
|
}
|
|
|
|
return &gitHelper{
|
|
Client: cli,
|
|
Meta: gitmeta,
|
|
}, nil
|
|
}
|
|
|
|
func (h *gitHelper) readRepo(path string) (*github.RepositoryContent, []*github.RepositoryContent, error) {
|
|
file, items, _, err := h.Client.Repositories.GetContents(context.Background(), h.Meta.Owner, h.Meta.Repo, path, nil)
|
|
if err != nil {
|
|
return nil, nil, WrapErrRateLimit(err)
|
|
}
|
|
return file, items, nil
|
|
}
|
|
|
|
func genAddonAPISchema(addonRes *types.Addon) error {
|
|
param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var r cue.Runtime
|
|
cueInst, err := r.Compile("-", param)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data, err := common.GenOpenAPI(cueInst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
schema, err := utils2.ConvertOpenAPISchema2SwaggerObject(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
utils2.FixOpenAPISchema("", schema)
|
|
addonRes.APISchema = schema
|
|
return nil
|
|
}
|
|
|
|
func cutPathUntil(path []string, end string) ([]string, error) {
|
|
for i, d := range path {
|
|
if d == end {
|
|
return path[i:], nil
|
|
}
|
|
}
|
|
return nil, errors.New("cut path fail, target directory name not found")
|
|
}
|
|
|
|
// RenderApplication render a K8s application
|
|
func RenderApplication(addon *types.Addon, args map[string]string) (*v1beta1.Application, []*unstructured.Unstructured, error) {
|
|
if args == nil {
|
|
args = map[string]string{}
|
|
}
|
|
app := addon.AppTemplate
|
|
if app == nil {
|
|
app = &v1beta1.Application{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "core.oam.dev/v1beta1", Kind: "Application"},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: Convert2AppName(addon.Name),
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
Labels: map[string]string{
|
|
oam.LabelAddonName: addon.Name,
|
|
},
|
|
},
|
|
Spec: v1beta1.ApplicationSpec{
|
|
Components: []common2.ApplicationComponent{},
|
|
},
|
|
}
|
|
}
|
|
app.Name = Convert2AppName(addon.Name)
|
|
app.Labels = util.MergeMapOverrideWithDst(app.Labels, map[string]string{oam.LabelAddonName: addon.Name})
|
|
|
|
for _, tmpl := range addon.YAMLTemplates {
|
|
comp, err := renderRawComponent(tmpl)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
app.Spec.Components = append(app.Spec.Components, *comp)
|
|
}
|
|
for _, tmpl := range addon.CUETemplates {
|
|
comp, err := renderCUETemplate(tmpl, addon.Parameters, args)
|
|
if err != nil {
|
|
return nil, nil, ErrRenderCueTmpl
|
|
}
|
|
app.Spec.Components = append(app.Spec.Components, *comp)
|
|
}
|
|
|
|
var defObjs []*unstructured.Unstructured
|
|
|
|
if isDeployToRuntimeOnly(addon) {
|
|
// Runtime cluster mode needs to deploy definitions to control plane k8s.
|
|
for _, def := range addon.Definitions {
|
|
obj, err := renderObject(def)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defObjs = append(defObjs, obj)
|
|
}
|
|
app.Spec.Workflow.Steps = append(app.Spec.Workflow.Steps,
|
|
v1beta1.WorkflowStep{
|
|
Name: "deploy-all",
|
|
Type: "deploy2runtime",
|
|
})
|
|
} else {
|
|
for _, def := range addon.Definitions {
|
|
comp, err := renderRawComponent(def)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
app.Spec.Components = append(app.Spec.Components, *comp)
|
|
}
|
|
}
|
|
|
|
return app, defObjs, nil
|
|
}
|
|
|
|
func isDeployToRuntimeOnly(addon *types.Addon) bool {
|
|
if addon.DeployTo == nil {
|
|
return false
|
|
}
|
|
return addon.DeployTo.RuntimeCluster
|
|
}
|
|
|
|
func renderObject(elem types.AddonElementFile) (*unstructured.Unstructured, error) {
|
|
obj := &unstructured.Unstructured{}
|
|
dec := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
|
_, _, err := dec.Decode([]byte(elem.Data), nil, obj)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return obj, nil
|
|
}
|
|
|
|
// renderRawComponent will return a component in raw type from string
|
|
func renderRawComponent(elem types.AddonElementFile) (*common2.ApplicationComponent, error) {
|
|
baseRawComponent := common2.ApplicationComponent{
|
|
Type: "raw",
|
|
Name: strings.Join(append(elem.Path, elem.Name), "-"),
|
|
}
|
|
obj, err := renderObject(elem)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
baseRawComponent.Properties = util.Object2RawExtension(obj)
|
|
return &baseRawComponent, nil
|
|
}
|
|
|
|
// renderCUETemplate will return a component from cue template
|
|
func renderCUETemplate(elem types.AddonElementFile, parameters string, args map[string]string) (*common2.ApplicationComponent, error) {
|
|
bt, err := json.Marshal(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var paramFile = cuemodel.ParameterFieldName + ": {}"
|
|
if string(bt) != "null" {
|
|
paramFile = fmt.Sprintf("%s: %s", cuemodel.ParameterFieldName, string(bt))
|
|
}
|
|
param := fmt.Sprintf("%s\n%s", paramFile, parameters)
|
|
v, err := value.NewValue(param, nil, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out, err := v.LookupByScript(fmt.Sprintf("{%s}", elem.Data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
compContent, err := out.LookupValue("output")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := cueyaml.Encode(compContent.CueValue())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
comp := common2.ApplicationComponent{
|
|
Name: strings.Join(append(elem.Path, elem.Name), "-"),
|
|
}
|
|
err = yaml.Unmarshal(b, &comp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &comp, err
|
|
}
|
|
|
|
const addonAppPrefix = "addon-"
|
|
const addonSecPrefix = "addon-secret-"
|
|
|
|
// Convert2AppName -
|
|
func Convert2AppName(name string) string {
|
|
return addonAppPrefix + name
|
|
}
|
|
|
|
// Convert2AddonName -
|
|
func Convert2AddonName(name string) string {
|
|
return strings.TrimPrefix(name, addonAppPrefix)
|
|
}
|
|
|
|
// RenderArgsSecret TODO add desc
|
|
func RenderArgsSecret(addon *types.Addon, args map[string]string) *v1.Secret {
|
|
sec := v1.Secret{
|
|
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: Convert2SecName(addon.Name),
|
|
Namespace: types.DefaultKubeVelaNS,
|
|
},
|
|
StringData: args,
|
|
Type: v1.SecretTypeOpaque,
|
|
}
|
|
return &sec
|
|
}
|
|
|
|
// Convert2SecName TODO add desc
|
|
func Convert2SecName(name string) string {
|
|
return addonSecPrefix + name
|
|
}
|