diff --git a/pkg/config/factory.go b/pkg/config/factory.go index e73774fb9..734e0eed0 100644 --- a/pkg/config/factory.go +++ b/pkg/config/factory.go @@ -24,11 +24,7 @@ import ( "strings" "time" - velacuex "github.com/oam-dev/kubevela/pkg/cue/cuex" - "cuelang.org/go/cue" - "github.com/kubevela/pkg/cue/cuex" - "github.com/getkin/kin-openapi/openapi3" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -49,6 +45,7 @@ import ( "github.com/oam-dev/kubevela/apis/types" icontext "github.com/oam-dev/kubevela/pkg/config/context" "github.com/oam-dev/kubevela/pkg/config/writer" + velacue "github.com/oam-dev/kubevela/pkg/cue" "github.com/oam-dev/kubevela/pkg/cue/script" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam/util" @@ -474,11 +471,9 @@ func (k *kubeConfigFactory) ParseConfig(ctx context.Context, Namespace: meta.Namespace, } // Compile the config template - contextOption := cuex.WithExtraData("context", contextValue) - parameterOption := cuex.WithExtraData(TemplateParameter, meta.Properties) - val, err := velacuex.KubeVelaDefaultCompiler.Get().CompileStringWithOptions(ctx, string(template.Template), contextOption, parameterOption) - if err != nil { - return nil, fmt.Errorf("failed to compile config template: %w", err) + val, err := template.Template.RunAndOutputWithCueX(ctx, contextValue, meta.Properties) + if err != nil && !velacue.IsFieldNotExist(err) { + return nil, err } // Render the validation response and check validation result valid := val.LookupPath(cue.ParsePath(TemplateValidationReturns)) diff --git a/pkg/cue/script/template.go b/pkg/cue/script/template.go index 14fab3225..9b0a3a4e9 100644 --- a/pkg/cue/script/template.go +++ b/pkg/cue/script/template.go @@ -22,12 +22,14 @@ import ( "fmt" "strings" + "github.com/kubevela/pkg/cue/cuex" + cuelang "cuelang.org/go/cue" "cuelang.org/go/cue/errors" "github.com/kubevela/workflow/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/cue" - "github.com/oam-dev/kubevela/pkg/cue/cuex" + velacuex "github.com/oam-dev/kubevela/pkg/cue/cuex" ) // CUE the cue script with the template format @@ -63,7 +65,7 @@ func (c CUE) ParseToValue() (*value.Value, error) { func (c CUE) ParseToValueWithCueX() (cuelang.Value, error) { // the cue script must be first, it could include the imports template := string(c) + "\n" + cue.BaseTemplate - val, err := cuex.KubeVelaDefaultCompiler.Get().CompileString(context.Background(), template) + val, err := velacuex.KubeVelaDefaultCompiler.Get().CompileString(context.Background(), template) if err != nil { return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", err) } @@ -168,6 +170,35 @@ func (c CUE) RunAndOutput(context interface{}, properties map[string]interface{} return render.LookupValue(outputField...) } +// RunAndOutputWithCueX run the cue script and return the values of the specified field. +// The output field must be under the template field. +func (c CUE) RunAndOutputWithCueX(ctx context.Context, context interface{}, properties map[string]interface{}, outputField ...string) (cuelang.Value, error) { + // Validate the properties + if err := c.ValidatePropertiesWithCueX(properties); err != nil { + return cuelang.Value{}, err + } + contextOption := cuex.WithExtraData("context", context) + parameterOption := cuex.WithExtraData("template.parameter", properties) + val, err := velacuex.KubeVelaDefaultCompiler.Get().CompileStringWithOptions(ctx, string(c), contextOption, parameterOption) + if !val.Exists() { + return cuelang.Value{}, fmt.Errorf("failed to compile config template") + } + if err != nil { + return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", err) + } + if Error(val) != nil { + return cuelang.Value{}, fmt.Errorf("failed to compile config template: %w", Error(val)) + } + if len(outputField) == 0 { + return val, nil + } + outputFieldVal := val.LookupPath(cuelang.ParsePath(strings.Join(outputField, "."))) + if !outputFieldVal.Exists() { + return cuelang.Value{}, fmt.Errorf("failed to lookup value: var(path=%s) not exist", strings.Join(outputField, ".")) + } + return outputFieldVal, nil +} + // ValidateProperties validate the input properties by the template func (c CUE) ValidateProperties(properties map[string]interface{}) error { template, err := c.ParseToTemplateValue() @@ -203,6 +234,31 @@ func (c CUE) ValidateProperties(properties map[string]interface{}) error { return nil } +// ValidatePropertiesWithCueX validate the input properties by the template +func (c CUE) ValidatePropertiesWithCueX(properties map[string]interface{}) error { + template, err := c.ParseToTemplateValueWithCueX() + if err != nil { + return err + } + paramPath := cuelang.ParsePath("template.parameter") + parameter := template.LookupPath(paramPath) + if !parameter.Exists() { + return fmt.Errorf("failed to lookup value: var(path=template.parameter) not exist") + } + props := parameter.FillPath(cuelang.ParsePath(""), properties) + if props.Err() != nil { + return ConvertFieldError(props.Err()) + } + if err := props.Validate(); err != nil { + return ConvertFieldError(err) + } + _, err = props.MarshalJSON() + if err != nil { + return ConvertFieldError(err) + } + return nil +} + // ParameterError the error report of the parameter field validation type ParameterError struct { Name string @@ -232,3 +288,22 @@ func ConvertFieldError(err error) error { } return err } + +// Error return value's error information. +func Error(val cuelang.Value) error { + if !val.Exists() { + return errors.New("empty value") + } + if err := val.Err(); err != nil { + return err + } + var gerr error + val.Walk(func(value cuelang.Value) bool { + if err := value.Eval().Err(); err != nil { + gerr = err + return false + } + return true + }, nil) + return gerr +} diff --git a/pkg/cue/script/template_test.go b/pkg/cue/script/template_test.go index c3a70a1bb..6a55a03f8 100644 --- a/pkg/cue/script/template_test.go +++ b/pkg/cue/script/template_test.go @@ -17,6 +17,7 @@ limitations under the License. package script import ( + "context" "fmt" "strings" "testing" @@ -196,6 +197,26 @@ func TestRunAndOutput(t *testing.T) { assert.Equal(t, data["url"], "hub.docker.com") } +func TestRunAndOutputWithCueX(t *testing.T) { + var cueScript = BuildCUEScriptWithDefaultContext([]byte("context:{namespace:string \n name:string}"), []byte(templateWithContextScript)) + output, err := cueScript.RunAndOutputWithCueX(context.Background(), map[string]interface{}{ + "name": "nnn", + "namespace": "ns", + }, map[string]interface{}{ + "url": "hub.docker.com", + "username": "test", + "password": "test", + "caFile": "test ca", + }, "template", "output") + assert.Equal(t, err, nil) + var data = map[string]interface{}{} + err = output.Decode(&data) + assert.Equal(t, err, nil) + assert.Equal(t, data["name"], "nnn") + assert.Equal(t, data["namespace"], "ns") + assert.Equal(t, data["url"], "hub.docker.com") +} + func TestValidateProperties(t *testing.T) { var cueScript = CUE(templateScript) // miss the required parameter @@ -231,6 +252,41 @@ func TestValidateProperties(t *testing.T) { assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "2 errors in empty disjunction"), true) } +func TestValidatePropertiesWithCueX(t *testing.T) { + var cueScript = CUE(templateScript) + // miss the required parameter + err := cueScript.ValidatePropertiesWithCueX(map[string]interface{}{ + "url": "hub.docker.com", + }) + assert.Equal(t, err.(*ParameterError).Message, "This parameter is required") + + // wrong the parameter value type + err = cueScript.ValidatePropertiesWithCueX(map[string]interface{}{ + "url": 1, + "username": "ddd", + }) + assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "conflicting values"), true) + assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "url"), true) + + // wrong the parameter value + err = cueScript.ValidatePropertiesWithCueX(map[string]interface{}{ + "url": "ddd", + "username": "ddd", + }) + assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "This parameter is required"), true) + assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "options"), true) + + // wrong the parameter value and no required value + err = cueScript.ValidatePropertiesWithCueX(map[string]interface{}{ + "url": "ddd", + "username": "ddd", + "options": "o3", + }) + fmt.Println(err.(*ParameterError).Message) + assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "options"), true) + assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "2 errors in empty disjunction"), true) +} + func TestParsePropertiesToSchema(t *testing.T) { cue := CUE([]byte(withPackage)) schema, err := cue.ParsePropertiesToSchema()