Files
kubevela/pkg/addon/addon.go
wyike 4ad27e9bcd Fix: (#2761)
1. load component in arrary, so apply them in order addon's needNamespace will be apply firstly
2. apply application  in controle plane will be first workflowStep
3. bigger application reconcile timeout context get avoid of time out
2021-11-21 21:03:41 +08:00

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