2021-03-26 15:24:19 +08:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-01-28 18:36:52 +08:00
|
|
|
package appfile
|
|
|
|
|
|
|
|
import (
|
2021-03-05 15:14:18 +08:00
|
|
|
"context"
|
2021-03-15 15:54:43 +08:00
|
|
|
"fmt"
|
2021-03-29 17:20:33 +08:00
|
|
|
"strings"
|
2021-03-05 15:14:18 +08:00
|
|
|
|
2021-03-26 23:02:58 +08:00
|
|
|
"cuelang.org/go/cue"
|
2021-01-28 18:36:52 +08:00
|
|
|
"github.com/pkg/errors"
|
2021-03-29 17:20:33 +08:00
|
|
|
v1 "k8s.io/api/core/v1"
|
2021-01-28 18:36:52 +08:00
|
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
2021-05-30 22:57:03 +08:00
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
2021-04-13 12:29:25 +08:00
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
2021-01-28 18:36:52 +08:00
|
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
|
2021-04-13 12:29:25 +08:00
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
|
2021-03-25 08:15:20 +08:00
|
|
|
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
2021-01-28 18:36:52 +08:00
|
|
|
"github.com/oam-dev/kubevela/apis/types"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/appfile/config"
|
2021-04-11 14:10:16 +08:00
|
|
|
velacue "github.com/oam-dev/kubevela/pkg/cue"
|
2021-06-02 15:37:06 +08:00
|
|
|
"github.com/oam-dev/kubevela/pkg/cue/definition"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/cue/packages"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/cue/process"
|
2021-04-13 12:29:25 +08:00
|
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
2021-01-28 18:36:52 +08:00
|
|
|
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/oam/util"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// AppfileBuiltinConfig defines the built-in config variable
|
|
|
|
AppfileBuiltinConfig = "config"
|
|
|
|
)
|
|
|
|
|
2021-04-13 12:29:25 +08:00
|
|
|
// TemplateLoaderFn load template of a capability definition
|
|
|
|
type TemplateLoaderFn func(context.Context, discoverymapper.DiscoveryMapper, client.Reader, string, types.CapType) (*Template, error)
|
|
|
|
|
|
|
|
// LoadTemplate load template of a capability definition
|
|
|
|
func (fn TemplateLoaderFn) LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, c client.Reader, capName string, capType types.CapType) (*Template, error) {
|
|
|
|
return fn(ctx, dm, c, capName, capType)
|
|
|
|
}
|
|
|
|
|
2021-01-28 18:36:52 +08:00
|
|
|
// Parser is an application parser
|
|
|
|
type Parser struct {
|
2021-04-13 12:29:25 +08:00
|
|
|
client client.Client
|
|
|
|
dm discoverymapper.DiscoveryMapper
|
2021-06-02 15:37:06 +08:00
|
|
|
pd *packages.PackageDiscover
|
2021-04-13 12:29:25 +08:00
|
|
|
tmplLoader TemplateLoaderFn
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewApplicationParser create appfile parser
|
2021-06-02 15:37:06 +08:00
|
|
|
func NewApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover) *Parser {
|
2021-01-28 18:36:52 +08:00
|
|
|
return &Parser{
|
2021-04-13 12:29:25 +08:00
|
|
|
client: cli,
|
|
|
|
dm: dm,
|
|
|
|
pd: pd,
|
|
|
|
tmplLoader: LoadTemplate,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewDryRunApplicationParser create an appfile parser for DryRun
|
2021-06-02 15:37:06 +08:00
|
|
|
func NewDryRunApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *packages.PackageDiscover, defs []oam.Object) *Parser {
|
2021-04-13 12:29:25 +08:00
|
|
|
return &Parser{
|
|
|
|
client: cli,
|
|
|
|
dm: dm,
|
|
|
|
pd: pd,
|
|
|
|
tmplLoader: DryRunTemplateLoader(defs),
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenerateAppFile converts an application to an Appfile
|
2021-04-11 14:10:16 +08:00
|
|
|
func (p *Parser) GenerateAppFile(ctx context.Context, app *v1beta1.Application) (*Appfile, error) {
|
|
|
|
ns := app.Namespace
|
|
|
|
appName := app.Name
|
|
|
|
|
2021-01-28 18:36:52 +08:00
|
|
|
appfile := new(Appfile)
|
2021-04-11 14:10:16 +08:00
|
|
|
appfile.Name = appName
|
|
|
|
appfile.Namespace = ns
|
2021-01-28 18:36:52 +08:00
|
|
|
var wds []*Workload
|
|
|
|
for _, comp := range app.Spec.Components {
|
2021-04-11 14:10:16 +08:00
|
|
|
wd, err := p.parseWorkload(ctx, comp, appName, ns)
|
2021-01-28 18:36:52 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
wds = append(wds, wd)
|
|
|
|
}
|
|
|
|
appfile.Workloads = wds
|
|
|
|
return appfile, nil
|
|
|
|
}
|
|
|
|
|
2021-05-30 22:57:03 +08:00
|
|
|
func (p *Parser) makeWorkload(ctx context.Context, appName, ns, name, typ string, capType types.CapType, props runtime.RawExtension) (*Workload, error) {
|
|
|
|
templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, typ, capType)
|
2021-01-28 18:36:52 +08:00
|
|
|
if err != nil && !kerrors.IsNotFound(err) {
|
2021-05-30 22:57:03 +08:00
|
|
|
return nil, errors.WithMessagef(err, "fetch type of %s", name)
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
2021-05-30 22:57:03 +08:00
|
|
|
settings, err := util.RawExtension2Map(&props)
|
2021-01-28 18:36:52 +08:00
|
|
|
if err != nil {
|
2021-05-30 22:57:03 +08:00
|
|
|
return nil, errors.WithMessagef(err, "fail to parse settings for %s", name)
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
2021-05-07 17:52:44 +08:00
|
|
|
|
2021-05-30 22:57:03 +08:00
|
|
|
wlType, err := util.ConvertDefinitionRevName(typ)
|
2021-05-07 17:52:44 +08:00
|
|
|
if err != nil {
|
2021-05-30 22:57:03 +08:00
|
|
|
wlType = typ
|
2021-05-07 17:52:44 +08:00
|
|
|
}
|
2021-03-23 16:30:49 +08:00
|
|
|
workload := &Workload{
|
2021-04-02 10:24:27 +08:00
|
|
|
Traits: []*Trait{},
|
2021-05-30 22:57:03 +08:00
|
|
|
Name: name,
|
2021-05-07 17:52:44 +08:00
|
|
|
Type: wlType,
|
2021-04-02 10:24:27 +08:00
|
|
|
CapabilityCategory: templ.CapabilityCategory,
|
|
|
|
FullTemplate: templ,
|
|
|
|
Params: settings,
|
2021-05-30 22:57:03 +08:00
|
|
|
engine: definition.NewWorkloadAbstractEngine(name, p.pd),
|
2021-03-23 16:30:49 +08:00
|
|
|
}
|
2021-04-11 14:10:16 +08:00
|
|
|
|
|
|
|
if workload.IsCloudResourceConsumer() {
|
|
|
|
requiredSecrets, err := parseWorkloadInsertSecretTo(ctx, p.client, ns, workload)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
workload.RequiredSecrets = requiredSecrets
|
|
|
|
}
|
|
|
|
|
|
|
|
userConfig := workload.GetUserConfigName()
|
|
|
|
if userConfig != "" {
|
|
|
|
cg := config.Configmap{Client: p.client}
|
|
|
|
// TODO(wonderflow): envName should not be namespace when we have serverside env
|
|
|
|
var envName = ns
|
|
|
|
data, err := cg.GetConfigData(config.GenConfigMapName(appName, workload.Name, userConfig), envName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, appName, ns)
|
|
|
|
}
|
|
|
|
workload.UserConfigs = data
|
|
|
|
}
|
2021-05-30 22:57:03 +08:00
|
|
|
return workload, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseWorkload resolve an ApplicationComponent and generate a Workload
|
|
|
|
// containing ALL information required by an Appfile.
|
|
|
|
func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComponent, appName, ns string) (*Workload, error) {
|
|
|
|
workload, err := p.makeWorkload(ctx, appName, ns, comp.Name, comp.Type, types.TypeComponentDefinition, comp.Properties)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-11 14:10:16 +08:00
|
|
|
|
2021-01-28 18:36:52 +08:00
|
|
|
for _, traitValue := range comp.Traits {
|
|
|
|
properties, err := util.RawExtension2Map(&traitValue.Properties)
|
|
|
|
if err != nil {
|
2021-03-25 08:15:20 +08:00
|
|
|
return nil, errors.Errorf("fail to parse properties of %s for %s", traitValue.Type, comp.Name)
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
2021-03-25 08:15:20 +08:00
|
|
|
trait, err := p.parseTrait(ctx, traitValue.Type, properties)
|
2021-01-28 18:36:52 +08:00
|
|
|
if err != nil {
|
2021-03-25 08:15:20 +08:00
|
|
|
return nil, errors.WithMessagef(err, "component(%s) parse trait(%s)", comp.Name, traitValue.Type)
|
2021-01-28 18:36:52 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
workload.Traits = append(workload.Traits, trait)
|
|
|
|
}
|
|
|
|
for scopeType, instanceName := range comp.Scopes {
|
2021-04-13 12:29:25 +08:00
|
|
|
gvk, err := getScopeGVK(ctx, p.client, p.dm, scopeType)
|
2021-01-28 18:36:52 +08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
workload.Scopes = append(workload.Scopes, Scope{
|
|
|
|
Name: instanceName,
|
|
|
|
GVK: gvk,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return workload, nil
|
|
|
|
}
|
2021-03-05 15:14:18 +08:00
|
|
|
func (p *Parser) parseTrait(ctx context.Context, name string, properties map[string]interface{}) (*Trait, error) {
|
2021-04-13 12:29:25 +08:00
|
|
|
templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait)
|
2021-01-28 18:36:52 +08:00
|
|
|
if kerrors.IsNotFound(err) {
|
|
|
|
return nil, errors.Errorf("trait definition of %s not found", name)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &Trait{
|
|
|
|
Name: name,
|
|
|
|
CapabilityCategory: templ.CapabilityCategory,
|
|
|
|
Params: properties,
|
|
|
|
Template: templ.TemplateStr,
|
2021-02-01 10:43:44 +08:00
|
|
|
HealthCheckPolicy: templ.Health,
|
|
|
|
CustomStatusFormat: templ.CustomStatus,
|
2021-03-20 03:30:31 +08:00
|
|
|
FullTemplate: templ,
|
2021-03-23 16:30:49 +08:00
|
|
|
engine: definition.NewTraitAbstractEngine(name, p.pd),
|
2021-01-28 18:36:52 +08:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-03-29 17:20:33 +08:00
|
|
|
// GetOutputSecretNames set all secret names, which are generated by cloud resource, to context
|
|
|
|
func GetOutputSecretNames(workloads *Workload) (string, error) {
|
|
|
|
secretName, err := getComponentSetting(process.OutputSecretName, workloads.Params)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Sprint(secretName), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseWorkloadInsertSecretTo(ctx context.Context, c client.Client, namespace string, wl *Workload) ([]process.RequiredSecrets, error) {
|
|
|
|
var requiredSecret []process.RequiredSecrets
|
2021-04-11 14:10:16 +08:00
|
|
|
cueStr := velacue.BaseTemplate + wl.FullTemplate.TemplateStr
|
|
|
|
r := cue.Runtime{}
|
|
|
|
ins, err := r.Compile("-", cueStr)
|
2021-03-29 17:20:33 +08:00
|
|
|
if err != nil {
|
2021-04-11 14:10:16 +08:00
|
|
|
return nil, errors.Wrap(err, "cannot compile CUE template")
|
2021-03-29 17:20:33 +08:00
|
|
|
}
|
2021-04-11 14:10:16 +08:00
|
|
|
params := ins.Lookup("parameter")
|
|
|
|
if !params.Exists() {
|
|
|
|
return nil, nil
|
2021-03-29 17:20:33 +08:00
|
|
|
}
|
2021-04-11 14:10:16 +08:00
|
|
|
paramsSt, err := params.Struct()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "cannot resolve parameters in CUE template")
|
|
|
|
}
|
|
|
|
for i := 0; i < paramsSt.Len(); i++ {
|
|
|
|
fieldInfo := paramsSt.Field(i)
|
|
|
|
fName := fieldInfo.Name
|
|
|
|
cgs := fieldInfo.Value.Doc()
|
|
|
|
for _, cg := range cgs {
|
|
|
|
for _, comment := range cg.List {
|
|
|
|
if comment == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if strings.Contains(comment.Text, InsertSecretToTag) {
|
|
|
|
contextName := strings.Split(comment.Text, InsertSecretToTag)[1]
|
|
|
|
contextName = strings.TrimSpace(contextName)
|
|
|
|
secretNameInterface, err := getComponentSetting(fName, wl.Params)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
secretName, ok := secretNameInterface.(string)
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("failed to convert secret name %v to string", secretNameInterface)
|
|
|
|
}
|
|
|
|
secretData, err := extractSecret(ctx, c, namespace, secretName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
requiredSecret = append(requiredSecret, process.RequiredSecrets{
|
|
|
|
Name: secretName,
|
|
|
|
ContextName: contextName,
|
|
|
|
Namespace: namespace,
|
|
|
|
Data: secretData,
|
|
|
|
})
|
|
|
|
}
|
2021-03-29 17:20:33 +08:00
|
|
|
}
|
|
|
|
}
|
2021-04-11 14:10:16 +08:00
|
|
|
|
2021-03-29 17:20:33 +08:00
|
|
|
}
|
|
|
|
return requiredSecret, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func extractSecret(ctx context.Context, c client.Client, namespace, name string) (map[string]interface{}, error) {
|
|
|
|
secretData := make(map[string]interface{})
|
|
|
|
var secret v1.Secret
|
|
|
|
if err := c.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &secret); err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to get secret %s from namespace %s which is required by the component: %w",
|
|
|
|
name, namespace, err)
|
|
|
|
}
|
|
|
|
for k, v := range secret.Data {
|
|
|
|
secretData[k] = string(v)
|
|
|
|
}
|
|
|
|
if len(secretData) == 0 {
|
|
|
|
return nil, fmt.Errorf("data in secret %s from namespace %s isn't available", name, namespace)
|
|
|
|
}
|
|
|
|
return secretData, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func getComponentSetting(settingParamName string, params map[string]interface{}) (interface{}, error) {
|
|
|
|
if secretName, ok := params[settingParamName]; ok {
|
|
|
|
return secretName, nil
|
|
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to get the value of component setting %s", settingParamName)
|
|
|
|
}
|
2021-04-13 12:29:25 +08:00
|
|
|
|
|
|
|
func getScopeGVK(ctx context.Context, cli client.Reader, dm discoverymapper.DiscoveryMapper,
|
|
|
|
name string) (schema.GroupVersionKind, error) {
|
|
|
|
var gvk schema.GroupVersionKind
|
|
|
|
sd := new(v1alpha2.ScopeDefinition)
|
|
|
|
err := util.GetDefinition(ctx, cli, sd, name)
|
|
|
|
if err != nil {
|
|
|
|
return gvk, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return util.GetGVKFromDefinition(dm, sd.Spec.Reference)
|
|
|
|
}
|