refactor pkg/appfile (#1447)

Signed-off-by: roy wang <seiwy2010@gmail.com>
This commit is contained in:
Yue Wang
2021-04-11 14:10:16 +08:00
committed by GitHub
parent c619b7b290
commit 2d6f2083db
16 changed files with 1191 additions and 1112 deletions
+461
View File
@@ -0,0 +1,461 @@
/*
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 (
"encoding/json"
"fmt"
"regexp"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/format"
json2cue "cuelang.org/go/encoding/json"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"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/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
// constant error information
const (
errInvalidValueType = "require %q type parameter value"
)
// Workload is component
type Workload struct {
Name string
Type string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
Traits []*Trait
Scopes []Scope
FullTemplate *Template
engine definition.AbstractEngine
// OutputSecretName is the secret name which this workload will generate after it successfully generate a cloud resource
OutputSecretName string
// RequiredSecrets stores secret names which the workload needs from cloud resource component and its context
RequiredSecrets []process.RequiredSecrets
UserConfigs []map[string]string
}
// GetUserConfigName get user config from AppFile, it will contain config file in it.
func (wl *Workload) GetUserConfigName() string {
if wl.Params == nil {
return ""
}
t, ok := wl.Params[AppfileBuiltinConfig]
if !ok {
return ""
}
ts, ok := t.(string)
if !ok {
return ""
}
return ts
}
// EvalContext eval workload template and set result to context
func (wl *Workload) EvalContext(ctx process.Context) error {
return wl.engine.Complete(ctx, wl.FullTemplate.TemplateStr, wl.Params)
}
// EvalStatus eval workload status
func (wl *Workload) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
return wl.engine.Status(ctx, cli, ns, wl.FullTemplate.CustomStatus)
}
// EvalHealth eval workload health check
func (wl *Workload) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
return wl.engine.HealthCheck(ctx, client, namespace, wl.FullTemplate.Health)
}
// IsCloudResourceProducer checks whether a workload is cloud resource producer role
func (wl *Workload) IsCloudResourceProducer() bool {
var existed bool
_, existed = wl.Params[process.OutputSecretName]
return existed
}
// IsCloudResourceConsumer checks whether a workload is cloud resource consumer role
func (wl *Workload) IsCloudResourceConsumer() bool {
requiredSecretTag := strings.TrimRight(InsertSecretToTag, "=")
matched, err := regexp.Match(regexp.QuoteMeta(requiredSecretTag), []byte(wl.FullTemplate.TemplateStr))
if err != nil || !matched {
return false
}
return true
}
// Scope defines the scope of workload
type Scope struct {
Name string
GVK schema.GroupVersionKind
}
// Trait is ComponentTrait
type Trait struct {
// The Name is name of TraitDefinition, actually it's a type of the trait instance
Name string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
Template string
HealthCheckPolicy string
CustomStatusFormat string
FullTemplate *Template
engine definition.AbstractEngine
}
// EvalContext eval trait template and set result to context
func (trait *Trait) EvalContext(ctx process.Context) error {
return trait.engine.Complete(ctx, trait.Template, trait.Params)
}
// EvalStatus eval trait status
func (trait *Trait) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
return trait.engine.Status(ctx, cli, ns, trait.CustomStatusFormat)
}
// EvalHealth eval trait health check
func (trait *Trait) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
return trait.engine.HealthCheck(ctx, client, namespace, trait.HealthCheckPolicy)
}
// Appfile describes application
type Appfile struct {
Name string
Namespace string
RevisionName string
Workloads []*Workload
}
// TemplateValidate validate Template format
func (af *Appfile) TemplateValidate() error {
return nil
}
// GenerateApplicationConfiguration converts an appFile to applicationConfig & Components
func (af *Appfile) GenerateApplicationConfiguration() (*v1alpha2.ApplicationConfiguration,
[]*v1alpha2.Component, error) {
appconfig := &v1alpha2.ApplicationConfiguration{}
appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind)
appconfig.Name = af.Name
appconfig.Namespace = af.Namespace
if appconfig.Labels == nil {
appconfig.Labels = map[string]string{}
}
appconfig.Labels[oam.LabelAppName] = af.Name
var components []*v1alpha2.Component
for _, wl := range af.Workloads {
var (
comp *v1alpha2.Component
acComp *v1alpha2.ApplicationConfigurationComponent
err error
)
switch wl.CapabilityCategory {
case types.HelmCategory:
comp, acComp, err = generateComponentFromHelmModule(wl, af.Name, af.RevisionName, af.Namespace)
if err != nil {
return nil, nil, err
}
case types.KubeCategory:
comp, acComp, err = generateComponentFromKubeModule(wl, af.Name, af.RevisionName, af.Namespace)
if err != nil {
return nil, nil, err
}
default:
comp, acComp, err = generateComponentFromCUEModule(wl, af.Name, af.RevisionName, af.Namespace)
if err != nil {
return nil, nil, err
}
}
components = append(components, comp)
appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp)
}
return appconfig, components, nil
}
// PrepareProcessContext prepares a DSL process Context
func PrepareProcessContext(wl *Workload, applicationName, revision, namespace string) (process.Context, error) {
pCtx := process.NewContext(namespace, wl.Name, applicationName, revision)
pCtx.InsertSecrets(wl.OutputSecretName, wl.RequiredSecrets)
if len(wl.UserConfigs) > 0 {
pCtx.SetConfigs(wl.UserConfigs)
}
if err := wl.EvalContext(pCtx); err != nil {
return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", applicationName, namespace)
}
return pCtx, nil
}
func generateComponentFromCUEModule(wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
var (
outputSecretName string
err error
)
if wl.IsCloudResourceProducer() {
outputSecretName, err = GetOutputSecretNames(wl)
if err != nil {
return nil, nil, err
}
wl.OutputSecretName = outputSecretName
}
pCtx, err := PrepareProcessContext(wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
return nil, nil, errors.Wrapf(err, "evaluate template trait=%s app=%s", tr.Name, wl.Name)
}
}
var comp *v1alpha2.Component
var acComp *v1alpha2.ApplicationConfigurationComponent
comp, acComp, err = evalWorkloadWithContext(pCtx, wl, appName, wl.Name)
if err != nil {
return nil, nil, err
}
comp.Name = wl.Name
acComp.ComponentName = comp.Name
for _, sc := range wl.Scopes {
acComp.Scopes = append(acComp.Scopes, v1alpha2.ComponentScope{ScopeReference: v1alpha1.TypedReference{
APIVersion: sc.GVK.GroupVersion().String(),
Kind: sc.GVK.Kind,
Name: sc.Name,
}})
}
if len(comp.Namespace) == 0 {
comp.Namespace = ns
}
if comp.Labels == nil {
comp.Labels = map[string]string{}
}
comp.Labels[oam.LabelAppName] = appName
comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind)
return comp, acComp, nil
}
// evalWorkloadWithContext evaluate the workload's template to generate component and ACComponent
func evalWorkloadWithContext(pCtx process.Context, wl *Workload, appName, compName string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
base, assists := pCtx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
return nil, nil, errors.Wrapf(err, "evaluate base template component=%s app=%s", compName, appName)
}
var commonLabels = definition.GetCommonLabels(pCtx.BaseContextLabels())
util.AddLabels(componentWorkload, util.MergeMapOverrideWithDst(commonLabels, map[string]string{oam.WorkloadTypeLabel: wl.Type}))
component := &v1alpha2.Component{}
// we need to marshal the workload to byte array before sending them to the k8s
component.Spec.Workload = util.Object2RawExtension(componentWorkload)
acComponent := &v1alpha2.ApplicationConfigurationComponent{}
for _, assist := range assists {
tr, err := assist.Ins.Unstructured()
if err != nil {
return nil, nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, compName, appName)
}
labels := util.MergeMapOverrideWithDst(commonLabels, map[string]string{oam.TraitTypeLabel: assist.Type})
if assist.Name != "" {
labels[oam.TraitResource] = assist.Name
}
util.AddLabels(tr, labels)
acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{
// we need to marshal the trait to byte array before sending them to the k8s
Trait: util.Object2RawExtension(tr),
})
}
return component, acComponent, nil
}
func generateComponentFromKubeModule(wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
kubeObj := &unstructured.Unstructured{}
err := json.Unmarshal(wl.FullTemplate.Kube.Template.Raw, kubeObj)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot decode Kube template into K8s object")
}
paramValues, err := resolveKubeParameters(wl.FullTemplate.Kube.Parameters, wl.Params)
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot resolve parameter settings")
}
if err := setParameterValuesToKubeObj(kubeObj, paramValues); err != nil {
return nil, nil, errors.WithMessage(err, "cannot set parameters value")
}
// convert structured kube obj into CUE (go ==marshal==> json ==decoder==> cue)
objRaw, err := kubeObj.MarshalJSON()
if err != nil {
return nil, nil, errors.Wrap(err, "cannot marshal kube object")
}
ins, err := json2cue.Decode(&cue.Runtime{}, "", objRaw)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot decode object into CUE")
}
cueRaw, err := format.Node(ins.Value().Syntax())
if err != nil {
return nil, nil, errors.Wrap(err, "cannot format CUE")
}
// NOTE a hack way to enable using CUE capabilities on KUBE schematic workload
wl.FullTemplate.TemplateStr = fmt.Sprintf(`
output: {
%s
}`, string(cueRaw))
// re-use the way CUE module generates comp & acComp
comp, acComp, err := generateComponentFromCUEModule(wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
return comp, acComp, nil
}
// a helper map whose key is parameter name
type paramValueSettings map[string]paramValueSetting
type paramValueSetting struct {
Value interface{}
ValueType common.ParameterValueType
FieldPaths []string
}
func resolveKubeParameters(params []common.KubeParameter, settings map[string]interface{}) (paramValueSettings, error) {
supported := map[string]*common.KubeParameter{}
for _, p := range params {
supported[p.Name] = p.DeepCopy()
}
values := make(paramValueSettings)
for name, v := range settings {
// check unsupported parameter setting
if supported[name] == nil {
return nil, errors.Errorf("unsupported parameter %q", name)
}
// construct helper map
values[name] = paramValueSetting{
Value: v,
ValueType: supported[name].ValueType,
FieldPaths: supported[name].FieldPaths,
}
}
// check required parameter
for _, p := range params {
if p.Required != nil && *p.Required {
if _, ok := values[p.Name]; !ok {
return nil, errors.Errorf("require parameter %q", p.Name)
}
}
}
return values, nil
}
func setParameterValuesToKubeObj(obj *unstructured.Unstructured, values paramValueSettings) error {
paved := fieldpath.Pave(obj.Object)
for paramName, v := range values {
for _, f := range v.FieldPaths {
switch v.ValueType {
case common.StringType:
vString, ok := v.Value.(string)
if !ok {
return errors.Errorf(errInvalidValueType, v.ValueType)
}
if err := paved.SetString(f, vString); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
case common.NumberType:
switch v.Value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
if err := paved.SetValue(f, v.Value); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
default:
return errors.Errorf(errInvalidValueType, v.ValueType)
}
case common.BooleanType:
vBoolean, ok := v.Value.(bool)
if !ok {
return errors.Errorf(errInvalidValueType, v.ValueType)
}
if err := paved.SetValue(f, vBoolean); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
}
}
}
return nil
}
func generateComponentFromHelmModule(wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
gv, err := schema.ParseGroupVersion(wl.FullTemplate.Reference.APIVersion)
if err != nil {
return nil, nil, err
}
targetWorkloadGVK := gv.WithKind(wl.FullTemplate.Reference.Kind)
// NOTE this is a hack way to enable using CUE module capabilities on Helm module workload
// construct an empty base workload according to its GVK
wl.FullTemplate.TemplateStr = fmt.Sprintf(`
output: {
apiVersion: "%s"
kind: "%s"
}`, targetWorkloadGVK.GroupVersion().String(), targetWorkloadGVK.Kind)
// re-use the way CUE module generates comp & acComp
comp, acComp, err := generateComponentFromCUEModule(wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
release, repo, err := helm.RenderHelmReleaseAndHelmRepo(wl.FullTemplate.Helm, wl.Name, appName, ns, wl.Params)
if err != nil {
return nil, nil, err
}
rlsBytes, err := json.Marshal(release.Object)
if err != nil {
return nil, nil, err
}
repoBytes, err := json.Marshal(repo.Object)
if err != nil {
return nil, nil, err
}
comp.Spec.Helm = &common.Helm{
Release: runtime.RawExtension{Raw: rlsBytes},
Repository: runtime.RawExtension{Raw: repoBytes},
}
return comp, acComp, nil
}
+575
View File
@@ -0,0 +1,575 @@
/*
Copyright 2020 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 (
"fmt"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/ghodss/yaml"
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
oamtypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
var _ = Describe("Test Helm schematic appfile", func() {
var (
appName = "test-app"
compName = "test-comp"
)
It("Test generate AppConfig resources from Helm schematic", func() {
appFile := &Appfile{
Name: appName,
Namespace: "default",
RevisionName: appName + "-v1",
Workloads: []*Workload{
{
Name: compName,
Type: "webapp-chart",
CapabilityCategory: oamtypes.HelmCategory,
Params: map[string]interface{}{
"image": map[string]interface{}{
"tag": "5.1.2",
},
},
engine: definition.NewWorkloadAbstractEngine(compName, pd),
Traits: []*Trait{
{
Name: "scaler",
Params: map[string]interface{}{
"replicas": float64(10),
},
engine: definition.NewTraitAbstractEngine("scaler", pd),
Template: `
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
replicas: *1 | int
}
`,
},
},
FullTemplate: &Template{
Reference: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
Helm: &common.Helm{
Release: util.Object2RawExtension(map[string]interface{}{
"chart": map[string]interface{}{
"spec": map[string]interface{}{
"chart": "podinfo",
"version": "5.1.4",
},
},
}),
Repository: util.Object2RawExtension(map[string]interface{}{
"url": "http://oam.dev/catalog/",
}),
},
},
},
},
}
By("Generate ApplicationConfiguration and Components")
ac, components, err := appFile.GenerateApplicationConfiguration()
Expect(err).To(BeNil())
manuscaler := util.Object2RawExtension(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1alpha2",
"kind": "ManualScalerTrait",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"trait.oam.dev/type": "scaler",
"trait.oam.dev/resource": "scaler",
"app.oam.dev/appRevision": appName + "-v1",
},
},
"spec": map[string]interface{}{"replicaCount": int64(10)},
},
})
expectAppConfig := &v1alpha2.ApplicationConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ApplicationConfiguration",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: compName,
Traits: []v1alpha2.ComponentTrait{
{
Trait: manuscaler,
},
},
},
},
},
}
expectComponent := &v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ComponentSpec{
Helm: &common.Helm{
Release: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
"kind": "HelmRelease",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
"spec": map[string]interface{}{
"chart": map[string]interface{}{
"spec": map[string]interface{}{
"sourceRef": map[string]interface{}{
"kind": "HelmRepository",
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
},
},
"interval": "5m0s",
"values": map[string]interface{}{
"image": map[string]interface{}{
"tag": "5.1.2",
},
},
},
}),
Repository: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "source.toolkit.fluxcd.io/v1beta1",
"kind": "HelmRepository",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
"spec": map[string]interface{}{
"url": "http://oam.dev/catalog/",
},
}),
},
Workload: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"workload.oam.dev/type": "webapp-chart",
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"app.oam.dev/appRevision": appName + "-v1",
},
},
}),
},
}
By("Verify expected ApplicationConfiguration")
diff := cmp.Diff(ac, expectAppConfig)
Expect(diff).Should(BeEmpty())
By("Verify expected Component")
diff = cmp.Diff(components[0], expectComponent)
Expect(diff).ShouldNot(BeEmpty())
})
})
var _ = Describe("Test Kube schematic appfile", func() {
var (
appName = "test-app"
compName = "test-comp"
)
var testTemplate = func() runtime.RawExtension {
yamlStr := `apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
ports:
- containerPort: 80 `
b, _ := yaml.YAMLToJSON([]byte(yamlStr))
return runtime.RawExtension{Raw: b}
}
var expectWorkload = func() runtime.RawExtension {
yamlStr := `apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.0
ports:
- containerPort: 80 `
b, _ := yaml.YAMLToJSON([]byte(yamlStr))
return runtime.RawExtension{Raw: b}
}
var testAppfile = func() *Appfile {
return &Appfile{
RevisionName: appName + "-v1",
Name: appName,
Namespace: "default",
Workloads: []*Workload{
{
Name: compName,
Type: "kube-worker",
CapabilityCategory: oamtypes.KubeCategory,
Params: map[string]interface{}{
"image": "nginx:1.14.0",
},
engine: definition.NewWorkloadAbstractEngine(compName, pd),
Traits: []*Trait{
{
Name: "scaler",
Params: map[string]interface{}{
"replicas": float64(10),
},
engine: definition.NewTraitAbstractEngine("scaler", pd),
Template: `
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
replicas: *1 | int
}
`,
},
},
FullTemplate: &Template{
Kube: &common.Kube{
Template: testTemplate(),
Parameters: []common.KubeParameter{
{
Name: "image",
ValueType: common.StringType,
Required: pointer.BoolPtr(true),
FieldPaths: []string{"spec.template.spec.containers[0].image"},
},
},
},
Reference: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
}
}
manuscaler := util.Object2RawExtension(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1alpha2",
"kind": "ManualScalerTrait",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"app.oam.dev/appRevision": appName + "-v1",
"trait.oam.dev/type": "scaler",
"trait.oam.dev/resource": "scaler",
},
},
"spec": map[string]interface{}{"replicaCount": int64(10)},
},
})
It("Test generate AppConfig resources from Kube schematic", func() {
By("Generate ApplicationConfiguration and Components")
ac, components, err := testAppfile().GenerateApplicationConfiguration()
Expect(err).To(BeNil())
expectAppConfig := &v1alpha2.ApplicationConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ApplicationConfiguration",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: compName,
Traits: []v1alpha2.ComponentTrait{
{
Trait: manuscaler,
},
},
},
},
},
}
expectComponent := &v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ComponentSpec{
Workload: expectWorkload(),
},
}
By("Verify expected ApplicationConfiguration")
diff := cmp.Diff(ac, expectAppConfig)
Expect(diff).Should(BeEmpty())
By("Verify expected Component")
diff = cmp.Diff(components[0], expectComponent)
Expect(diff).ShouldNot(BeEmpty())
})
It("Test missing set required parameter", func() {
appfile := testAppfile()
// remove parameter settings
appfile.Workloads[0].Params = nil
_, _, err := appfile.GenerateApplicationConfiguration()
expectError := errors.WithMessage(errors.New(`require parameter "image"`), "cannot resolve parameter settings")
diff := cmp.Diff(expectError, err, test.EquateErrors())
Expect(diff).Should(BeEmpty())
})
})
func TestResolveKubeParameters(t *testing.T) {
stringParam := &common.KubeParameter{
Name: "strParam",
ValueType: common.StringType,
FieldPaths: []string{"spec"},
}
requiredParam := &common.KubeParameter{
Name: "reqParam",
Required: pointer.BoolPtr(true),
ValueType: common.StringType,
FieldPaths: []string{"spec"},
}
tests := map[string]struct {
reason string
params []common.KubeParameter
settings map[string]interface{}
want paramValueSettings
wantErr error
}{
"EmptyParam": {
reason: "Empty value settings and no error should be returned",
want: make(paramValueSettings),
},
"UnsupportedParam": {
reason: "An error shoulde be returned because of unsupported param",
params: []common.KubeParameter{*stringParam},
settings: map[string]interface{}{"unsupported": "invalid parameter"},
want: nil,
wantErr: errors.Errorf("unsupported parameter %q", "unsupported"),
},
"MissingRequiredParam": {
reason: "An error should be returned because of missing required param",
params: []common.KubeParameter{*stringParam, *requiredParam},
settings: map[string]interface{}{"strParam": "string"},
want: nil,
wantErr: errors.Errorf("require parameter %q", "reqParam"),
},
"Succeed": {
reason: "No error should be returned",
params: []common.KubeParameter{*stringParam, *requiredParam},
settings: map[string]interface{}{"strParam": "test", "reqParam": "test"},
want: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: stringParam.FieldPaths,
},
"reqParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: requiredParam.FieldPaths,
},
},
wantErr: nil,
},
}
for tcName, tc := range tests {
t.Run(tcName, func(t *testing.T) {
result, err := resolveKubeParameters(tc.params, tc.settings)
if diff := cmp.Diff(tc.want, result); diff != "" {
t.Fatalf("\nresolveKubeParameters(...)(...) -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.wantErr, err, test.EquateErrors()); diff != "" {
t.Fatalf("\nresolveKubeParameters(...)(...) -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
})
}
}
func TestSetParameterValuesToKubeObj(t *testing.T) {
tests := map[string]struct {
reason string
obj unstructured.Unstructured
values paramValueSettings
wantObj unstructured.Unstructured
wantErr error
}{
"InvalidStringType": {
reason: "An error should be returned",
values: paramValueSettings{
"strParam": paramValueSetting{
Value: int32(100),
ValueType: common.StringType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.StringType),
},
"InvalidNumberType": {
reason: "An error should be returned",
values: paramValueSettings{
"intParam": paramValueSetting{
Value: "test",
ValueType: common.NumberType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.NumberType),
},
"InvalidBoolType": {
reason: "An error should be returned",
values: paramValueSettings{
"boolParam": paramValueSetting{
Value: "test",
ValueType: common.BooleanType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.BooleanType),
},
"InvalidFieldPath": {
reason: "An error should be returned",
values: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: []string{"spec[.test"}, // a invalid field path
},
},
wantErr: errors.Wrap(errors.New(`cannot parse path "spec[.test": unterminated '[' at position 4`),
`cannot set parameter "strParam" to field "spec[.test"`),
},
"Succeed": {
reason: "No error should be returned",
obj: unstructured.Unstructured{Object: make(map[string]interface{})},
values: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: []string{"spec.strField"},
},
"intParam": paramValueSetting{
Value: 10,
ValueType: common.NumberType,
FieldPaths: []string{"spec.intField"},
},
"floatParam": paramValueSetting{
Value: float64(10.01),
ValueType: common.NumberType,
FieldPaths: []string{"spec.floatField"},
},
"boolParam": paramValueSetting{
Value: true,
ValueType: common.BooleanType,
FieldPaths: []string{"spec.boolField"},
},
},
wantObj: unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"strField": "test",
"intField": int64(10),
"floatField": float64(10.01),
"boolField": true,
},
}},
},
}
for tcName, tc := range tests {
t.Run(tcName, func(t *testing.T) {
obj := tc.obj.DeepCopy()
err := setParameterValuesToKubeObj(obj, tc.values)
if diff := cmp.Diff(tc.wantObj, *obj); diff != "" {
t.Errorf("\nsetParameterValuesToKubeObj(...)error -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.wantErr, err, test.EquateErrors()); diff != "" {
t.Errorf("\nsetParameterValuesToKubeObj(...)error -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
})
}
}
-204
View File
@@ -1,204 +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 (
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
)
func TestResolveKubeParameters(t *testing.T) {
stringParam := &common.KubeParameter{
Name: "strParam",
ValueType: common.StringType,
FieldPaths: []string{"spec"},
}
requiredParam := &common.KubeParameter{
Name: "reqParam",
Required: pointer.BoolPtr(true),
ValueType: common.StringType,
FieldPaths: []string{"spec"},
}
tests := map[string]struct {
reason string
params []common.KubeParameter
settings map[string]interface{}
want paramValueSettings
wantErr error
}{
"EmptyParam": {
reason: "Empty value settings and no error should be returned",
want: make(paramValueSettings),
},
"UnsupportedParam": {
reason: "An error shoulde be returned because of unsupported param",
params: []common.KubeParameter{*stringParam},
settings: map[string]interface{}{"unsupported": "invalid parameter"},
want: nil,
wantErr: errors.Errorf("unsupported parameter %q", "unsupported"),
},
"MissingRequiredParam": {
reason: "An error should be returned because of missing required param",
params: []common.KubeParameter{*stringParam, *requiredParam},
settings: map[string]interface{}{"strParam": "string"},
want: nil,
wantErr: errors.Errorf("require parameter %q", "reqParam"),
},
"Succeed": {
reason: "No error should be returned",
params: []common.KubeParameter{*stringParam, *requiredParam},
settings: map[string]interface{}{"strParam": "test", "reqParam": "test"},
want: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: stringParam.FieldPaths,
},
"reqParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: requiredParam.FieldPaths,
},
},
wantErr: nil,
},
}
for tcName, tc := range tests {
t.Run(tcName, func(t *testing.T) {
result, err := resolveKubeParameters(tc.params, tc.settings)
if diff := cmp.Diff(tc.want, result); diff != "" {
t.Fatalf("\nresolveKubeParameters(...)(...) -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.wantErr, err, test.EquateErrors()); diff != "" {
t.Fatalf("\nresolveKubeParameters(...)(...) -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
})
}
}
func TestSetParameterValuesToKubeObj(t *testing.T) {
tests := map[string]struct {
reason string
obj unstructured.Unstructured
values paramValueSettings
wantObj unstructured.Unstructured
wantErr error
}{
"InvalidStringType": {
reason: "An error should be returned",
values: paramValueSettings{
"strParam": paramValueSetting{
Value: int32(100),
ValueType: common.StringType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.StringType),
},
"InvalidNumberType": {
reason: "An error should be returned",
values: paramValueSettings{
"intParam": paramValueSetting{
Value: "test",
ValueType: common.NumberType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.NumberType),
},
"InvalidBoolType": {
reason: "An error should be returned",
values: paramValueSettings{
"boolParam": paramValueSetting{
Value: "test",
ValueType: common.BooleanType,
FieldPaths: []string{"spec.test"},
},
},
wantErr: errors.Errorf(errInvalidValueType, common.BooleanType),
},
"InvalidFieldPath": {
reason: "An error should be returned",
values: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: []string{"spec[.test"}, // a invalid field path
},
},
wantErr: errors.Wrap(errors.New(`cannot parse path "spec[.test": unterminated '[' at position 4`),
`cannot set parameter "strParam" to field "spec[.test"`),
},
"Succeed": {
reason: "No error should be returned",
obj: unstructured.Unstructured{Object: make(map[string]interface{})},
values: paramValueSettings{
"strParam": paramValueSetting{
Value: "test",
ValueType: common.StringType,
FieldPaths: []string{"spec.strField"},
},
"intParam": paramValueSetting{
Value: 10,
ValueType: common.NumberType,
FieldPaths: []string{"spec.intField"},
},
"floatParam": paramValueSetting{
Value: float64(10.01),
ValueType: common.NumberType,
FieldPaths: []string{"spec.floatField"},
},
"boolParam": paramValueSetting{
Value: true,
ValueType: common.BooleanType,
FieldPaths: []string{"spec.boolField"},
},
},
wantObj: unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"strField": "test",
"intField": int64(10),
"floatField": float64(10.01),
"boolField": true,
},
}},
},
}
for tcName, tc := range tests {
t.Run(tcName, func(t *testing.T) {
obj := tc.obj.DeepCopy()
err := setParameterValuesToKubeObj(obj, tc.values)
if diff := cmp.Diff(tc.wantObj, *obj); diff != "" {
t.Errorf("\nsetParameterValuesToKubeObj(...)error -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
if diff := cmp.Diff(tc.wantErr, err, test.EquateErrors()); diff != "" {
t.Errorf("\nsetParameterValuesToKubeObj(...)error -want +get \nreason:%s\n%s\n", tc.reason, diff)
}
})
}
}
+84 -486
View File
@@ -18,34 +18,21 @@ package appfile
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"cuelang.org/go/cue"
"cuelang.org/go/cue/format"
json2cue "cuelang.org/go/encoding/json"
"github.com/crossplane/crossplane-runtime/apis/core/v1alpha1"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"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/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile/config"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
"github.com/oam-dev/kubevela/pkg/controller/utils"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -55,106 +42,6 @@ const (
AppfileBuiltinConfig = "config"
)
// constant error information
const (
errInvalidValueType = "require %q type parameter value"
)
// Workload is component
type Workload struct {
Name string
Type string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
Traits []*Trait
Scopes []Scope
FullTemplate *util.Template
engine definition.AbstractEngine
// OutputSecretName is the secret name which this workload will generate after it successfully generate a cloud resource
OutputSecretName string
// RequiredSecrets stores secret names which the workload needs from cloud resource component and its context
RequiredSecrets []process.RequiredSecrets
}
// GetUserConfigName get user config from AppFile, it will contain config file in it.
func (wl *Workload) GetUserConfigName() string {
if wl.Params == nil {
return ""
}
t, ok := wl.Params[AppfileBuiltinConfig]
if !ok {
return ""
}
ts, ok := t.(string)
if !ok {
return ""
}
return ts
}
// EvalContext eval workload template and set result to context
func (wl *Workload) EvalContext(ctx process.Context) error {
return wl.engine.Complete(ctx, wl.FullTemplate.TemplateStr, wl.Params)
}
// EvalStatus eval workload status
func (wl *Workload) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
return wl.engine.Status(ctx, cli, ns, wl.FullTemplate.CustomStatus)
}
// EvalHealth eval workload health check
func (wl *Workload) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
return wl.engine.HealthCheck(ctx, client, namespace, wl.FullTemplate.Health)
}
// Scope defines the scope of workload
type Scope struct {
Name string
GVK schema.GroupVersionKind
}
// Trait is ComponentTrait
type Trait struct {
// The Name is name of TraitDefinition, actually it's a type of the trait instance
Name string
CapabilityCategory types.CapabilityCategory
Params map[string]interface{}
Template string
HealthCheckPolicy string
CustomStatusFormat string
FullTemplate *util.Template
engine definition.AbstractEngine
}
// EvalContext eval trait template and set result to context
func (trait *Trait) EvalContext(ctx process.Context) error {
return trait.engine.Complete(ctx, trait.Template, trait.Params)
}
// EvalStatus eval trait status
func (trait *Trait) EvalStatus(ctx process.Context, cli client.Client, ns string) (string, error) {
return trait.engine.Status(ctx, cli, ns, trait.CustomStatusFormat)
}
// EvalHealth eval trait health check
func (trait *Trait) EvalHealth(ctx process.Context, client client.Client, namespace string) (bool, error) {
return trait.engine.HealthCheck(ctx, client, namespace, trait.HealthCheckPolicy)
}
// Appfile describes application
type Appfile struct {
Name string
RevisionName string
Workloads []*Workload
}
// TemplateValidate validate Template format
func (af *Appfile) TemplateValidate() error {
return nil
}
// Parser is an application parser
type Parser struct {
client client.Client
@@ -172,12 +59,16 @@ func NewApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper,
}
// GenerateAppFile converts an application to an Appfile
func (p *Parser) GenerateAppFile(ctx context.Context, name string, app *v1beta1.Application) (*Appfile, error) {
func (p *Parser) GenerateAppFile(ctx context.Context, app *v1beta1.Application) (*Appfile, error) {
ns := app.Namespace
appName := app.Name
appfile := new(Appfile)
appfile.Name = name
appfile.Name = appName
appfile.Namespace = ns
var wds []*Workload
for _, comp := range app.Spec.Components {
wd, err := p.parseWorkload(ctx, comp)
wd, err := p.parseWorkload(ctx, comp, appName, ns)
if err != nil {
return nil, err
}
@@ -187,9 +78,10 @@ func (p *Parser) GenerateAppFile(ctx context.Context, name string, app *v1beta1.
return appfile, nil
}
func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComponent) (*Workload, error) {
templ, err := util.LoadTemplate(ctx, p.dm, p.client, comp.Type, types.TypeComponentDefinition)
// 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) {
templ, err := LoadTemplate(ctx, p.dm, p.client, comp.Type, types.TypeComponentDefinition)
if err != nil && !kerrors.IsNotFound(err) {
return nil, errors.WithMessagef(err, "fetch type of %s", comp.Name)
}
@@ -206,6 +98,34 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp
Params: settings,
engine: definition.NewWorkloadAbstractEngine(comp.Name, p.pd),
}
if workload.IsCloudResourceConsumer() {
requiredSecrets, err := parseWorkloadInsertSecretTo(ctx, p.client, ns, workload)
if err != nil {
return nil, err
}
workload.RequiredSecrets = requiredSecrets
}
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
}
for _, traitValue := range comp.Traits {
properties, err := util.RawExtension2Map(&traitValue.Properties)
if err != nil {
@@ -219,7 +139,7 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp
workload.Traits = append(workload.Traits, trait)
}
for scopeType, instanceName := range comp.Scopes {
gvk, err := util.GetScopeGVK(ctx, p.client, p.dm, scopeType)
gvk, err := GetScopeGVK(ctx, p.client, p.dm, scopeType)
if err != nil {
return nil, err
}
@@ -232,7 +152,7 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp
}
func (p *Parser) parseTrait(ctx context.Context, name string, properties map[string]interface{}) (*Trait, error) {
templ, err := util.LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait)
templ, err := LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait)
if kerrors.IsNotFound(err) {
return nil, errors.Errorf("trait definition of %s not found", name)
}
@@ -251,322 +171,6 @@ func (p *Parser) parseTrait(ctx context.Context, name string, properties map[str
}, nil
}
// GenerateApplicationConfiguration converts an appFile to applicationConfig & Components
func (p *Parser) GenerateApplicationConfiguration(app *Appfile, ns string) (*v1alpha2.ApplicationConfiguration,
[]*v1alpha2.Component, error) {
appconfig := &v1alpha2.ApplicationConfiguration{}
appconfig.SetGroupVersionKind(v1alpha2.ApplicationConfigurationGroupVersionKind)
appconfig.Name = app.Name
appconfig.Namespace = ns
if appconfig.Labels == nil {
appconfig.Labels = map[string]string{}
}
appconfig.Labels[oam.LabelAppName] = app.Name
var components []*v1alpha2.Component
ctx := context.Background()
for _, wl := range app.Workloads {
var (
comp *v1alpha2.Component
acComp *v1alpha2.ApplicationConfigurationComponent
err error
)
if wl.IsCloudResourceConsumer() {
requiredSecrets, err := parseWorkloadInsertSecretTo(ctx, p.client, ns, wl)
if err != nil {
return nil, nil, err
}
wl.RequiredSecrets = requiredSecrets
}
switch wl.CapabilityCategory {
case types.HelmCategory:
comp, acComp, err = generateComponentFromHelmModule(p.client, wl, app.Name, app.RevisionName, ns)
if err != nil {
return nil, nil, err
}
case types.KubeCategory:
comp, acComp, err = generateComponentFromKubeModule(p.client, wl, app.Name, app.RevisionName, ns)
if err != nil {
return nil, nil, err
}
default:
comp, acComp, err = generateComponentFromCUEModule(p.client, wl, app.Name, app.RevisionName, ns)
if err != nil {
return nil, nil, err
}
}
components = append(components, comp)
appconfig.Spec.Components = append(appconfig.Spec.Components, *acComp)
}
return appconfig, components, nil
}
func generateComponentFromCUEModule(c client.Client, wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
var (
outputSecretName string
err error
)
if wl.IsCloudResourceProducer() {
outputSecretName, err = GetOutputSecretNames(wl)
if err != nil {
return nil, nil, err
}
wl.OutputSecretName = outputSecretName
}
pCtx, err := PrepareProcessContext(c, wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
for _, tr := range wl.Traits {
if err := tr.EvalContext(pCtx); err != nil {
return nil, nil, errors.Wrapf(err, "evaluate template trait=%s app=%s", tr.Name, wl.Name)
}
}
var comp *v1alpha2.Component
var acComp *v1alpha2.ApplicationConfigurationComponent
comp, acComp, err = evalWorkloadWithContext(pCtx, wl, appName, wl.Name)
if err != nil {
return nil, nil, err
}
comp.Name = wl.Name
acComp.ComponentName = comp.Name
for _, sc := range wl.Scopes {
acComp.Scopes = append(acComp.Scopes, v1alpha2.ComponentScope{ScopeReference: v1alpha1.TypedReference{
APIVersion: sc.GVK.GroupVersion().String(),
Kind: sc.GVK.Kind,
Name: sc.Name,
}})
}
if len(comp.Namespace) == 0 {
comp.Namespace = ns
}
if comp.Labels == nil {
comp.Labels = map[string]string{}
}
comp.Labels[oam.LabelAppName] = appName
comp.SetGroupVersionKind(v1alpha2.ComponentGroupVersionKind)
return comp, acComp, nil
}
func generateComponentFromKubeModule(c client.Client, wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
kubeObj := &unstructured.Unstructured{}
err := json.Unmarshal(wl.FullTemplate.Kube.Template.Raw, kubeObj)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot decode Kube template into K8s object")
}
paramValues, err := resolveKubeParameters(wl.FullTemplate.Kube.Parameters, wl.Params)
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot resolve parameter settings")
}
if err := setParameterValuesToKubeObj(kubeObj, paramValues); err != nil {
return nil, nil, errors.WithMessage(err, "cannot set parameters value")
}
// convert structured kube obj into CUE (go ==marshal==> json ==decoder==> cue)
objRaw, err := kubeObj.MarshalJSON()
if err != nil {
return nil, nil, errors.Wrap(err, "cannot marshal kube object")
}
ins, err := json2cue.Decode(&cue.Runtime{}, "", objRaw)
if err != nil {
return nil, nil, errors.Wrap(err, "cannot decode object into CUE")
}
cueRaw, err := format.Node(ins.Value().Syntax())
if err != nil {
return nil, nil, errors.Wrap(err, "cannot format CUE")
}
// NOTE a hack way to enable using CUE capabilities on KUBE schematic workload
wl.FullTemplate.TemplateStr = fmt.Sprintf(`
output: {
%s
}`, string(cueRaw))
// re-use the way CUE module generates comp & acComp
comp, acComp, err := generateComponentFromCUEModule(c, wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
return comp, acComp, nil
}
// a helper map whose key is parameter name
type paramValueSettings map[string]paramValueSetting
type paramValueSetting struct {
Value interface{}
ValueType common.ParameterValueType
FieldPaths []string
}
func resolveKubeParameters(params []common.KubeParameter, settings map[string]interface{}) (paramValueSettings, error) {
supported := map[string]*common.KubeParameter{}
for _, p := range params {
supported[p.Name] = p.DeepCopy()
}
values := make(paramValueSettings)
for name, v := range settings {
// check unsupported parameter setting
if supported[name] == nil {
return nil, errors.Errorf("unsupported parameter %q", name)
}
// construct helper map
values[name] = paramValueSetting{
Value: v,
ValueType: supported[name].ValueType,
FieldPaths: supported[name].FieldPaths,
}
}
// check required parameter
for _, p := range params {
if p.Required != nil && *p.Required {
if _, ok := values[p.Name]; !ok {
return nil, errors.Errorf("require parameter %q", p.Name)
}
}
}
return values, nil
}
func setParameterValuesToKubeObj(obj *unstructured.Unstructured, values paramValueSettings) error {
paved := fieldpath.Pave(obj.Object)
for paramName, v := range values {
for _, f := range v.FieldPaths {
switch v.ValueType {
case common.StringType:
vString, ok := v.Value.(string)
if !ok {
return errors.Errorf(errInvalidValueType, v.ValueType)
}
if err := paved.SetString(f, vString); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
case common.NumberType:
switch v.Value.(type) {
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
if err := paved.SetValue(f, v.Value); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
default:
return errors.Errorf(errInvalidValueType, v.ValueType)
}
case common.BooleanType:
vBoolean, ok := v.Value.(bool)
if !ok {
return errors.Errorf(errInvalidValueType, v.ValueType)
}
if err := paved.SetValue(f, vBoolean); err != nil {
return errors.Wrapf(err, "cannot set parameter %q to field %q", paramName, f)
}
}
}
}
return nil
}
func generateComponentFromHelmModule(c client.Client, wl *Workload, appName, revision, ns string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
gv, err := schema.ParseGroupVersion(wl.FullTemplate.Reference.APIVersion)
if err != nil {
return nil, nil, err
}
targetWorkloadGVK := gv.WithKind(wl.FullTemplate.Reference.Kind)
// NOTE this is a hack way to enable using CUE module capabilities on Helm module workload
// construct an empty base workload according to its GVK
wl.FullTemplate.TemplateStr = fmt.Sprintf(`
output: {
apiVersion: "%s"
kind: "%s"
}`, targetWorkloadGVK.GroupVersion().String(), targetWorkloadGVK.Kind)
// re-use the way CUE module generates comp & acComp
comp, acComp, err := generateComponentFromCUEModule(c, wl, appName, revision, ns)
if err != nil {
return nil, nil, err
}
release, repo, err := helm.RenderHelmReleaseAndHelmRepo(wl.FullTemplate.Helm, wl.Name, appName, ns, wl.Params)
if err != nil {
return nil, nil, err
}
rlsBytes, err := json.Marshal(release.Object)
if err != nil {
return nil, nil, err
}
repoBytes, err := json.Marshal(repo.Object)
if err != nil {
return nil, nil, err
}
comp.Spec.Helm = &common.Helm{
Release: runtime.RawExtension{Raw: rlsBytes},
Repository: runtime.RawExtension{Raw: repoBytes},
}
return comp, acComp, nil
}
// evalWorkloadWithContext evaluate the workload's template to generate component and ACComponent
func evalWorkloadWithContext(pCtx process.Context, wl *Workload, appName, compName string) (*v1alpha2.Component, *v1alpha2.ApplicationConfigurationComponent, error) {
base, assists := pCtx.Output()
componentWorkload, err := base.Unstructured()
if err != nil {
return nil, nil, errors.Wrapf(err, "evaluate base template component=%s app=%s", compName, appName)
}
var commonLabels = definition.GetCommonLabels(pCtx.BaseContextLabels())
util.AddLabels(componentWorkload, util.MergeMapOverrideWithDst(commonLabels, map[string]string{oam.WorkloadTypeLabel: wl.Type}))
component := &v1alpha2.Component{}
// we need to marshal the workload to byte array before sending them to the k8s
component.Spec.Workload = util.Object2RawExtension(componentWorkload)
acComponent := &v1alpha2.ApplicationConfigurationComponent{}
for _, assist := range assists {
tr, err := assist.Ins.Unstructured()
if err != nil {
return nil, nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, compName, appName)
}
labels := util.MergeMapOverrideWithDst(commonLabels, map[string]string{oam.TraitTypeLabel: assist.Type})
if assist.Name != "" {
labels[oam.TraitResource] = assist.Name
}
util.AddLabels(tr, labels)
acComponent.Traits = append(acComponent.Traits, v1alpha2.ComponentTrait{
// we need to marshal the trait to byte array before sending them to the k8s
Trait: util.Object2RawExtension(tr),
})
}
return component, acComponent, nil
}
// PrepareProcessContext prepares a DSL process Context
func PrepareProcessContext(k8sClient client.Client, wl *Workload, applicationName, revision, namespace string) (process.Context, error) {
pCtx := process.NewContext(namespace, wl.Name, applicationName, revision)
pCtx.InsertSecrets(wl.OutputSecretName, wl.RequiredSecrets)
userConfig := wl.GetUserConfigName()
if userConfig != "" {
cg := config.Configmap{Client: k8sClient}
// TODO(wonderflow): envName should not be namespace when we have serverside env
var envName = namespace
data, err := cg.GetConfigData(config.GenConfigMapName(applicationName, wl.Name, userConfig), envName)
if err != nil {
return nil, errors.Wrapf(err, "get config=%s for app=%s in namespace=%s", userConfig, applicationName, namespace)
}
pCtx.SetConfigs(data)
}
if err := wl.EvalContext(pCtx); err != nil {
return nil, errors.Wrapf(err, "evaluate base template app=%s in namespace=%s", applicationName, namespace)
}
return pCtx, nil
}
// 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)
@@ -579,43 +183,54 @@ func GetOutputSecretNames(workloads *Workload) (string, error) {
func parseWorkloadInsertSecretTo(ctx context.Context, c client.Client, namespace string, wl *Workload) ([]process.RequiredSecrets, error) {
var requiredSecret []process.RequiredSecrets
api, err := utils.GenerateOpenAPISchemaFromDefinition(wl.Name, wl.FullTemplate.TemplateStr)
cueStr := velacue.BaseTemplate + wl.FullTemplate.TemplateStr
r := cue.Runtime{}
ins, err := r.Compile("-", cueStr)
if err != nil {
if !errors.Is(err, errors.Errorf(utils.ErrNoSectionParameterInCue, wl.Name)) {
return nil, nil
}
return nil, err
return nil, errors.Wrap(err, "cannot compile CUE template")
}
schema, err := utils.ConvertOpenAPISchema2SwaggerObject(api)
params := ins.Lookup("parameter")
if !params.Exists() {
return nil, nil
}
paramsSt, err := params.Struct()
if err != nil {
return nil, err
return nil, errors.Wrap(err, "cannot resolve parameters in CUE template")
}
for k, v := range schema.Properties {
description := v.Value.Description
if strings.Contains(description, utils.InsertSecretToTag) {
contextName := strings.Split(description, utils.InsertSecretToTag)[1]
contextName = strings.TrimSpace(contextName)
secretNameInterface, err := getComponentSetting(k, wl.Params)
if err != nil {
return nil, err
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,
})
}
}
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,
})
}
}
return requiredSecret, nil
}
@@ -642,20 +257,3 @@ func getComponentSetting(settingParamName string, params map[string]interface{})
}
return nil, fmt.Errorf("failed to get the value of component setting %s", settingParamName)
}
// IsCloudResourceProducer checks whether a workload is cloud resource producer role
func (wl *Workload) IsCloudResourceProducer() bool {
var existed bool
_, existed = wl.Params[process.OutputSecretName]
return existed
}
// IsCloudResourceConsumer checks whether a workload is cloud resource consumer role
func (wl *Workload) IsCloudResourceConsumer() bool {
requiredSecretTag := strings.TrimRight(utils.InsertSecretToTag, "=")
matched, err := regexp.Match(regexp.QuoteMeta(requiredSecretTag), []byte(wl.FullTemplate.TemplateStr))
if err != nil || !matched {
return false
}
return true
}
+16 -371
View File
@@ -27,7 +27,6 @@ import (
"github.com/google/go-cmp/cmp"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,12 +34,9 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
oamtypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
@@ -48,7 +44,7 @@ import (
)
var expectedExceptApp = &Appfile{
Name: "test",
Name: "application-sample",
Workloads: []*Workload{
{
Name: "myweb",
@@ -57,7 +53,7 @@ var expectedExceptApp = &Appfile{
"image": "busybox",
"cmd": []interface{}{"sleep", "1000"},
},
FullTemplate: &util.Template{
FullTemplate: &Template{
TemplateStr: `
output: {
apiVersion: "apps/v1"
@@ -209,6 +205,7 @@ apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: application-sample
namespace: default
spec:
components:
- name: myweb
@@ -251,7 +248,7 @@ var _ = Describe("Test application parser", func() {
},
}
appfile, err := NewApplicationParser(&tclient, dm, pd).GenerateAppFile(context.TODO(), "test", &o)
appfile, err := NewApplicationParser(&tclient, dm, pd).GenerateAppFile(context.TODO(), &o)
Expect(err).ShouldNot(HaveOccurred())
Expect(equal(expectedExceptApp, appfile)).Should(BeTrue())
})
@@ -291,6 +288,7 @@ var _ = Describe("Test appFile parser", func() {
var TestApp = &Appfile{
RevisionName: "test-v1",
Name: "test",
Namespace: "default",
Workloads: []*Workload{
{
Name: "myweb",
@@ -300,6 +298,10 @@ var _ = Describe("Test appFile parser", func() {
"cmd": []interface{}{"sleep", "1000"},
"config": "myconfig",
},
UserConfigs: []map[string]string{
{"name": "c1", "value": "v1"},
{"name": "c2", "value": "v2"},
},
Scopes: []Scope{
{Name: "test-scope", GVK: schema.GroupVersionKind{
Group: "core.oam.dev",
@@ -308,7 +310,7 @@ var _ = Describe("Test appFile parser", func() {
}},
},
engine: definition.NewWorkloadAbstractEngine("myweb", pd),
FullTemplate: &util.Template{
FullTemplate: &Template{
TemplateStr: `
output: {
apiVersion: "apps/v1"
@@ -382,7 +384,7 @@ var _ = Describe("Test appFile parser", func() {
Data: map[string]string{"c1": "v1", "c2": "v2"},
}
Expect(k8sClient.Create(context.Background(), cm.DeepCopy())).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
ac, components, err := NewApplicationParser(k8sClient, dm, pd).GenerateApplicationConfiguration(TestApp, "default")
ac, components, err := TestApp.GenerateApplicationConfiguration()
Expect(err).To(BeNil())
manuscaler := util.Object2RawExtension(&unstructured.Unstructured{
Object: map[string]interface{}{
@@ -499,6 +501,7 @@ var _ = Describe("Test appFile parser", func() {
Expect(len(components)).To(BeEquivalentTo(1))
Expect(components[0].ObjectMeta).To(BeEquivalentTo(expectComponent.ObjectMeta))
Expect(components[0].TypeMeta).To(BeEquivalentTo(expectComponent.TypeMeta))
By(string(components[0].Spec.Workload.Raw))
Expect(components[0].Spec.Workload).Should(SatisfyAny(
BeEquivalentTo(util.Object2RawExtension(expectWorkload)),
BeEquivalentTo(util.Object2RawExtension(expectWorkloadOptional))))
@@ -506,364 +509,6 @@ var _ = Describe("Test appFile parser", func() {
})
var _ = Describe("Test appfile parser to parse helm module", func() {
var (
appName = "test-app"
compName = "test-comp"
)
It("Test application containing helm module", func() {
appFile := &Appfile{
Name: appName,
RevisionName: appName + "-v1",
Workloads: []*Workload{
{
Name: compName,
Type: "webapp-chart",
CapabilityCategory: oamtypes.HelmCategory,
Params: map[string]interface{}{
"image": map[string]interface{}{
"tag": "5.1.2",
},
},
engine: definition.NewWorkloadAbstractEngine(compName, pd),
Traits: []*Trait{
{
Name: "scaler",
Params: map[string]interface{}{
"replicas": float64(10),
},
engine: definition.NewTraitAbstractEngine("scaler", pd),
Template: `
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
replicas: *1 | int
}
`,
},
},
FullTemplate: &util.Template{
Reference: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
Helm: &common.Helm{
Release: util.Object2RawExtension(map[string]interface{}{
"chart": map[string]interface{}{
"spec": map[string]interface{}{
"chart": "podinfo",
"version": "5.1.4",
},
},
}),
Repository: util.Object2RawExtension(map[string]interface{}{
"url": "http://oam.dev/catalog/",
}),
},
},
},
},
}
By("Generate ApplicationConfiguration and Components")
ac, components, err := NewApplicationParser(k8sClient, dm, pd).GenerateApplicationConfiguration(appFile, "default")
Expect(err).To(BeNil())
manuscaler := util.Object2RawExtension(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1alpha2",
"kind": "ManualScalerTrait",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"trait.oam.dev/type": "scaler",
"trait.oam.dev/resource": "scaler",
"app.oam.dev/appRevision": appName + "-v1",
},
},
"spec": map[string]interface{}{"replicaCount": int64(10)},
},
})
expectAppConfig := &v1alpha2.ApplicationConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ApplicationConfiguration",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: compName,
Traits: []v1alpha2.ComponentTrait{
{
Trait: manuscaler,
},
},
},
},
},
}
expectComponent := &v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ComponentSpec{
Helm: &common.Helm{
Release: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "helm.toolkit.fluxcd.io/v2beta1",
"kind": "HelmRelease",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
"spec": map[string]interface{}{
"chart": map[string]interface{}{
"spec": map[string]interface{}{
"sourceRef": map[string]interface{}{
"kind": "HelmRepository",
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
},
},
"interval": "5m0s",
"values": map[string]interface{}{
"image": map[string]interface{}{
"tag": "5.1.2",
},
},
},
}),
Repository: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "source.toolkit.fluxcd.io/v1beta1",
"kind": "HelmRepository",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("%s-%s", appName, compName),
"namespace": "default",
},
"spec": map[string]interface{}{
"url": "http://oam.dev/catalog/",
},
}),
},
Workload: util.Object2RawExtension(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"workload.oam.dev/type": "webapp-chart",
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"app.oam.dev/appRevision": appName + "-v1",
},
},
}),
},
}
By("Verify expected ApplicationConfiguration")
diff := cmp.Diff(ac, expectAppConfig)
Expect(diff).Should(BeEmpty())
By("Verify expected Component")
diff = cmp.Diff(components[0], expectComponent)
Expect(diff).ShouldNot(BeEmpty())
})
})
var _ = Describe("Test appfile parser to parse kube module", func() {
var (
appName = "test-app"
compName = "test-comp"
)
var testTemplate = func() runtime.RawExtension {
yamlStr := `apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
ports:
- containerPort: 80 `
b, _ := yaml.YAMLToJSON([]byte(yamlStr))
return runtime.RawExtension{Raw: b}
}
var expectWorkload = func() runtime.RawExtension {
yamlStr := `apiVersion: apps/v1
kind: Deployment
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.0
ports:
- containerPort: 80 `
b, _ := yaml.YAMLToJSON([]byte(yamlStr))
return runtime.RawExtension{Raw: b}
}
var testAppfile = func() *Appfile {
return &Appfile{
RevisionName: appName + "-v1",
Name: appName,
Workloads: []*Workload{
{
Name: compName,
Type: "kube-worker",
CapabilityCategory: oamtypes.KubeCategory,
Params: map[string]interface{}{
"image": "nginx:1.14.0",
},
engine: definition.NewWorkloadAbstractEngine(compName, pd),
Traits: []*Trait{
{
Name: "scaler",
Params: map[string]interface{}{
"replicas": float64(10),
},
engine: definition.NewTraitAbstractEngine("scaler", pd),
Template: `
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
replicas: *1 | int
}
`,
},
},
FullTemplate: &util.Template{
Kube: &common.Kube{
Template: testTemplate(),
Parameters: []common.KubeParameter{
{
Name: "image",
ValueType: common.StringType,
Required: pointer.BoolPtr(true),
FieldPaths: []string{"spec.template.spec.containers[0].image"},
},
},
},
Reference: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
},
},
}
}
manuscaler := util.Object2RawExtension(&unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "core.oam.dev/v1alpha2",
"kind": "ManualScalerTrait",
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
"app.oam.dev/component": compName,
"app.oam.dev/name": appName,
"app.oam.dev/appRevision": appName + "-v1",
"trait.oam.dev/type": "scaler",
"trait.oam.dev/resource": "scaler",
},
},
"spec": map[string]interface{}{"replicaCount": int64(10)},
},
})
It("Test application containing kube module", func() {
By("Generate ApplicationConfiguration and Components")
ac, components, err := NewApplicationParser(k8sClient, dm, pd).GenerateApplicationConfiguration(testAppfile(), "default")
Expect(err).To(BeNil())
expectAppConfig := &v1alpha2.ApplicationConfiguration{
TypeMeta: metav1.TypeMeta{
Kind: "ApplicationConfiguration",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: appName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ApplicationConfigurationSpec{
Components: []v1alpha2.ApplicationConfigurationComponent{
{
ComponentName: compName,
Traits: []v1alpha2.ComponentTrait{
{
Trait: manuscaler,
},
},
},
},
},
}
expectComponent := &v1alpha2.Component{
TypeMeta: metav1.TypeMeta{
Kind: "Component",
APIVersion: "core.oam.dev/v1alpha2",
}, ObjectMeta: metav1.ObjectMeta{
Name: compName,
Namespace: "default",
Labels: map[string]string{oam.LabelAppName: appName},
},
Spec: v1alpha2.ComponentSpec{
Workload: expectWorkload(),
},
}
By("Verify expected ApplicationConfiguration")
diff := cmp.Diff(ac, expectAppConfig)
Expect(diff).Should(BeEmpty())
By("Verify expected Component")
diff = cmp.Diff(components[0], expectComponent)
Expect(diff).ShouldNot(BeEmpty())
})
It("Test missing set required parameter", func() {
appfile := testAppfile()
// remove parameter settings
appfile.Workloads[0].Params = nil
_, _, err := NewApplicationParser(k8sClient, dm, pd).GenerateApplicationConfiguration(appfile, "default")
expectError := errors.WithMessage(errors.New(`require parameter "image"`), "cannot resolve parameter settings")
diff := cmp.Diff(expectError, err, test.EquateErrors())
Expect(diff).Should(BeEmpty())
})
})
var _ = Describe("Test Get OutputSecretNames", func() {
Context("Workload will generate cloud resource secret", func() {
It("", func() {
@@ -929,7 +574,7 @@ settings: {
wl := &Workload{
Name: "abc",
FullTemplate: &util.Template{TemplateStr: template},
FullTemplate: &Template{TemplateStr: template},
}
By("call target function")
secrets, err := parseWorkloadInsertSecretTo(ctx, k8sClient, ns, wl)
@@ -969,7 +614,7 @@ parameter: {
Params: map[string]interface{}{
"dbSecret": targetSecretName,
},
FullTemplate: &util.Template{TemplateStr: template},
FullTemplate: &Template{TemplateStr: template},
}
By("create secret")
s := &corev1.Secret{
@@ -1027,7 +672,7 @@ var _ = Describe("Test IsCloudResourceConsumer", func() {
Context("Workload is a Cloud Resource consumer", func() {
It("", func() {
wl := &Workload{
FullTemplate: &util.Template{TemplateStr: "// +insertSecretTo=dbConn"},
FullTemplate: &Template{TemplateStr: "// +insertSecretTo=dbConn"},
}
Expect(wl.IsCloudResourceConsumer()).Should(Equal(true))
})
@@ -1036,7 +681,7 @@ var _ = Describe("Test IsCloudResourceConsumer", func() {
Context("Workload is a Cloud Resource consumer", func() {
It("", func() {
wl := &Workload{
FullTemplate: &util.Template{TemplateStr: "// +useage=dbConn"},
FullTemplate: &Template{TemplateStr: "// +useage=dbConn"},
}
Expect(wl.IsCloudResourceProducer()).Should(Equal(false))
})
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util
package appfile
import (
"context"
@@ -32,6 +32,16 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
// UsageTag is usage comment annotation
UsageTag = "+usage="
// ShortTag is the short alias annotation
ShortTag = "+short"
// InsertSecretToTag marks the value should be set as an context
InsertSecretToTag = "+insertSecretTo="
)
// Template includes its string, health and its category
@@ -54,12 +64,12 @@ func GetScopeGVK(ctx context.Context, cli client.Reader, dm discoverymapper.Disc
name string) (schema.GroupVersionKind, error) {
var gvk schema.GroupVersionKind
sd := new(v1alpha2.ScopeDefinition)
err := GetDefinition(ctx, cli, sd, name)
err := oamutil.GetDefinition(ctx, cli, sd, name)
if err != nil {
return gvk, err
}
return GetGVKFromDefinition(dm, sd.Spec.Reference)
return oamutil.GetGVKFromDefinition(dm, sd.Spec.Reference)
}
// LoadTemplate Get template according to key
@@ -72,11 +82,11 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c
var extension *runtime.RawExtension
cd := new(v1beta1.ComponentDefinition)
err := GetDefinition(ctx, cli, cd, key)
err := oamutil.GetDefinition(ctx, cli, cd, key)
if err != nil {
if kerrors.IsNotFound(err) {
wd := new(v1beta1.WorkloadDefinition)
if err := GetDefinition(ctx, cli, wd, key); err != nil {
if err := oamutil.GetDefinition(ctx, cli, wd, key); err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate from workloadDefinition [%s] ", key)
}
schematic, status, extension = wd.Spec.Schematic, wd.Spec.Status, wd.Spec.Extension
@@ -88,7 +98,7 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c
tmpl.CapabilityCategory = types.TerraformCategory
}
tmpl.WorkloadDefinition = wd
gvk, err := GetGVKFromDefinition(dm, wd.Spec.Reference)
gvk, err := oamutil.GetGVKFromDefinition(dm, wd.Spec.Reference)
if err != nil {
return nil, errors.WithMessagef(err, "Get GVK from workload definition [%s]", key)
}
@@ -114,7 +124,7 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c
case types.TypeTrait:
td := new(v1beta1.TraitDefinition)
err := GetDefinition(ctx, cli, td, key)
err := oamutil.GetDefinition(ctx, cli, td, key)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
}
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package util
package appfile
import (
"context"
@@ -31,6 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam/mock"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
func TestLoadComponentTemplate(t *testing.T) {
@@ -100,7 +101,7 @@ spec:
MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj runtime.Object) error {
switch o := obj.(type) {
case *v1beta1.ComponentDefinition:
cd, err := UnMarshalStringToComponentDefinition(componentDefintion)
cd, err := oamutil.UnMarshalStringToComponentDefinition(componentDefintion)
if err != nil {
return err
}
@@ -202,7 +203,7 @@ spec:
MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj runtime.Object) error {
switch o := obj.(type) {
case *v1alpha2.WorkloadDefinition:
cd, err := UnMarshalStringToWorkloadDefinition(workloadDefintion)
cd, err := oamutil.UnMarshalStringToWorkloadDefinition(workloadDefintion)
if err != nil {
return err
}
@@ -324,7 +325,7 @@ spec:
MockGet: func(ctx context.Context, key ktypes.NamespacedName, obj runtime.Object) error {
switch o := obj.(type) {
case *v1beta1.TraitDefinition:
wd, err := UnMarshalStringToTraitDefinition(traitDefintion)
wd, err := oamutil.UnMarshalStringToTraitDefinition(traitDefintion)
if err != nil {
return err
}
@@ -114,7 +114,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
appParser := appfile.NewApplicationParser(r.Client, r.dm, r.pd)
ctx = oamutil.SetNamespaceInCtx(ctx, app.Namespace)
generatedAppfile, err := appParser.GenerateAppFile(ctx, app.Name, app)
generatedAppfile, err := appParser.GenerateAppFile(ctx, app)
if err != nil {
applog.Error(err, "[Handle Parse]")
app.Status.SetConditions(errorCondition("Parsed", err))
@@ -135,7 +135,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
applog.Info("build template")
// build template to applicationconfig & component
ac, comps, err := appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err := generatedAppfile.GenerateApplicationConfiguration()
if err != nil {
applog.Error(err, "[Handle GenerateApplicationConfiguration]")
app.Status.SetConditions(errorCondition("Built", err))
@@ -224,9 +224,9 @@ var _ = Describe("test generate revision ", func() {
ctx = util.SetNamespaceInCtx(ctx, app.Namespace)
annoKey1 := "testKey1"
app.SetAnnotations(map[string]string{annoKey1: "true"})
generatedAppfile, err := appParser.GenerateAppFile(ctx, app.Name, &app)
generatedAppfile, err := appParser.GenerateAppFile(ctx, &app)
Expect(err).Should(Succeed())
ac, comps, err = appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err = generatedAppfile.GenerateApplicationConfiguration()
Expect(err).Should(Succeed())
handler.appfile = generatedAppfile
Expect(ac.Namespace).Should(Equal(app.Namespace))
@@ -320,9 +320,9 @@ var _ = Describe("test generate revision ", func() {
}
// persist the app
Expect(k8sClient.Update(ctx, &app)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
generatedAppfile, err = appParser.GenerateAppFile(ctx, app.Name, &app)
generatedAppfile, err = appParser.GenerateAppFile(ctx, &app)
Expect(err).Should(Succeed())
ac, comps, err = appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err = generatedAppfile.GenerateApplicationConfiguration()
Expect(err).Should(Succeed())
handler.appfile = generatedAppfile
handler.app = &app
@@ -372,9 +372,9 @@ var _ = Describe("test generate revision ", func() {
ctx = util.SetNamespaceInCtx(ctx, app.Namespace)
// mark the app as rollout
app.SetAnnotations(map[string]string{oam.AnnotationAppRollout: strconv.FormatBool(true)})
generatedAppfile, err := appParser.GenerateAppFile(ctx, app.Name, &app)
generatedAppfile, err := appParser.GenerateAppFile(ctx, &app)
Expect(err).Should(Succeed())
ac, comps, err = appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err = generatedAppfile.GenerateApplicationConfiguration()
Expect(err).Should(Succeed())
handler.appfile = generatedAppfile
Expect(ac.Namespace).Should(Equal(app.Namespace))
@@ -467,9 +467,9 @@ var _ = Describe("test generate revision ", func() {
}
// persist the app
Expect(k8sClient.Update(ctx, &app)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{}))
generatedAppfile, err = appParser.GenerateAppFile(ctx, app.Name, &app)
generatedAppfile, err = appParser.GenerateAppFile(ctx, &app)
Expect(err).Should(Succeed())
ac, comps, err = appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err = generatedAppfile.GenerateApplicationConfiguration()
Expect(err).Should(Succeed())
handler.appfile = generatedAppfile
handler.app = &app
@@ -520,9 +520,9 @@ var _ = Describe("test generate revision ", func() {
app.SetLabels(map[string]string{labelKey1: "true"})
annoKey1 := "annoKey1"
app.SetAnnotations(map[string]string{annoKey1: "true"})
generatedAppfile, err := appParser.GenerateAppFile(ctx, app.Name, &app)
generatedAppfile, err := appParser.GenerateAppFile(ctx, &app)
Expect(err).Should(Succeed())
ac, comps, err = appParser.GenerateApplicationConfiguration(generatedAppfile, app.Namespace)
ac, comps, err = generatedAppfile.GenerateApplicationConfiguration()
Expect(err).Should(Succeed())
handler.appfile = generatedAppfile
Expect(ac.Namespace).Should(Equal(app.Namespace))
+8 -16
View File
@@ -37,6 +37,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
mycue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
@@ -44,15 +45,6 @@ import (
"github.com/oam-dev/kubevela/pkg/utils/common"
)
const (
// UsageTag is usage comment annotation
UsageTag = "+usage="
// ShortTag is the short alias annotation
ShortTag = "+short"
// InsertSecretToTag marks the value should be set as an context
InsertSecretToTag = "+insertSecretTo="
)
// ErrNoSectionParameterInCue means there is not parameter section in Cue template of a workload
const ErrNoSectionParameterInCue = "capability %s doesn't contain section `parameter`"
@@ -96,9 +88,9 @@ func (def *CapabilityComponentDefinition) GetCapabilityObject(ctx context.Contex
if err := k8sClient.Get(ctx, objectKey, wd); err != nil {
return nil, fmt.Errorf("failed to get WorkloadDefinition that ComponentDefinition refers to")
}
capability, err = util.ConvertTemplateJSON2Object(name, wd.Spec.Extension, wd.Spec.Schematic)
capability, err = appfile.ConvertTemplateJSON2Object(name, wd.Spec.Extension, wd.Spec.Schematic)
default:
capability, err = util.ConvertTemplateJSON2Object(name, componentDefinition.Spec.Extension, componentDefinition.Spec.Schematic)
capability, err = appfile.ConvertTemplateJSON2Object(name, componentDefinition.Spec.Extension, componentDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert ComponentDefinition to Capability Object")
}
@@ -205,7 +197,7 @@ func (def *CapabilityTraitDefinition) GetCapabilityObject(ctx context.Context, k
return &capability, fmt.Errorf("failed to get WorkloadDefinition %s: %w", def.Name, err)
}
def.TraitDefinition = traitDefinition
capability, err = util.ConvertTemplateJSON2Object(name, traitDefinition.Spec.Extension, traitDefinition.Spec.Schematic)
capability, err = appfile.ConvertTemplateJSON2Object(name, traitDefinition.Spec.Extension, traitDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert WorkloadDefinition to Capability Object")
}
@@ -384,11 +376,11 @@ func fixOpenAPISchema(name string, schema *openapi3.Schema) {
}
description := schema.Description
if strings.Contains(description, UsageTag) {
description = strings.Split(description, UsageTag)[1]
if strings.Contains(description, appfile.UsageTag) {
description = strings.Split(description, appfile.UsageTag)[1]
}
if strings.Contains(description, ShortTag) {
description = strings.Split(description, ShortTag)[0]
if strings.Contains(description, appfile.ShortTag) {
description = strings.Split(description, appfile.ShortTag)[0]
description = strings.TrimSpace(description)
}
schema.Description = description
+2 -2
View File
@@ -31,8 +31,8 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
mycue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/system"
)
@@ -74,7 +74,7 @@ func TestGetOpenAPISchema(t *testing.T) {
Template: string(data),
},
}
capability, _ := util.ConvertTemplateJSON2Object(tc.name, nil, schematic)
capability, _ := appfile.ConvertTemplateJSON2Object(tc.name, nil, schematic)
schema, err := getOpenAPISchema(capability, pd)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\ngetOpenAPISchema(...): -want error, +got error:\n%s", tc.reason, diff)
@@ -30,7 +30,7 @@ func (h *ValidatingHandler) ValidateCreate(ctx context.Context, app *v1beta1.App
var componentErrs field.ErrorList
// try to generate an app file
appParser := appfile.NewApplicationParser(h.Client, h.dm, h.pd)
if _, err := appParser.GenerateAppFile(ctx, app.Name, app); err != nil {
if _, err := appParser.GenerateAppFile(ctx, app); err != nil {
componentErrs = append(componentErrs, field.Invalid(field.NewPath("spec"), app, err.Error()))
}
return componentErrs
@@ -31,9 +31,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
controller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
const (
@@ -134,7 +134,7 @@ func ValidateDefinitionReference(_ context.Context, td v1beta1.TraitDefinition)
if len(td.Spec.Reference.Name) > 0 {
return nil
}
tmp, err := util.NewTemplate(td.Spec.Schematic, td.Spec.Status, td.Spec.Extension)
tmp, err := appfile.NewTemplate(td.Spec.Schematic, td.Spec.Status, td.Spec.Extension)
if err != nil {
return errors.Wrap(err, errValidateDefRef)
}
+4 -4
View File
@@ -63,7 +63,7 @@ func ApplyTerraform(app *v1beta1.Application, k8sClient client.Client, ioStream
appParser := appfile.NewApplicationParser(k8sClient, dm, pd)
ctx := util2.SetNamespaceInCtx(context.Background(), namespace)
appFile, err := appParser.GenerateAppFile(ctx, app.Name, app)
appFile, err := appParser.GenerateAppFile(ctx, app)
if err != nil {
return nil, fmt.Errorf("failed to parse appfile: %w", err)
}
@@ -83,7 +83,7 @@ func ApplyTerraform(app *v1beta1.Application, k8sClient client.Client, ioStream
name := wl.Name
ioStream.Infof("\nApplying cloud resources %s\n", name)
tf, err := getTerraformJSONFiles(k8sClient, wl, appFile.Name, revisionName, namespace)
tf, err := getTerraformJSONFiles(wl, appFile.Name, revisionName, namespace)
if err != nil {
return nil, fmt.Errorf("failed to get Terraform JSON files from workload %s: %w", name, err)
}
@@ -197,8 +197,8 @@ func generateSecretFromTerraformOutput(k8sClient client.Client, outputList []str
}
// getTerraformJSONFiles gets Terraform JSON files or modules from workload
func getTerraformJSONFiles(k8sClient client.Client, wl *appfile.Workload, applicationName, revisionName string, namespace string) ([]byte, error) {
pCtx, err := appfile.PrepareProcessContext(k8sClient, wl, applicationName, revisionName, namespace)
func getTerraformJSONFiles(wl *appfile.Workload, applicationName, revisionName string, namespace string) ([]byte, error) {
pCtx, err := appfile.PrepareProcessContext(wl, applicationName, revisionName, namespace)
if err != nil {
return nil, err
}
+2 -2
View File
@@ -102,12 +102,12 @@ func NewDryRunCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command
ctx := oamutil.SetNamespaceInCtx(context.Background(), velaEnv.Namespace)
appFile, err := parser.GenerateAppFile(ctx, app.Name, app)
appFile, err := parser.GenerateAppFile(ctx, app)
if err != nil {
return errors.WithMessage(err, "generate appFile")
}
ac, comps, err := parser.GenerateApplicationConfiguration(appFile, app.Namespace)
ac, comps, err := appFile.GenerateApplicationConfiguration()
if err != nil {
return errors.WithMessage(err, "generate OAM objects")
}
+2 -1
View File
@@ -30,6 +30,7 @@ import (
commontypes "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"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
@@ -185,7 +186,7 @@ func GetDescription(annotation map[string]string) string {
// HandleTemplate will handle definition template to capability
func HandleTemplate(in *runtime.RawExtension, schematic *commontypes.Schematic, name string) (types.Capability, error) {
tmp, err := util.ConvertTemplateJSON2Object(name, in, schematic)
tmp, err := appfile.ConvertTemplateJSON2Object(name, in, schematic)
if err != nil {
return types.Capability{}, err
}