diff --git a/api/types/template_types.go b/api/types/template_types.go index 458080b45..83cf5cd67 100644 --- a/api/types/template_types.go +++ b/api/types/template_types.go @@ -20,16 +20,20 @@ import ( "encoding/json" "fmt" + "github.com/spf13/cobra" + + "cuelang.org/go/cue" + "k8s.io/apimachinery/pkg/runtime" ) // Template defines the content of a plugin type Template struct { - Name string `json:"name"` - Type DefinitionType `json:"type"` - Alias string `json:"alias,omitempty"` - Object map[string]interface{} `json:"object,omitempty"` - Parameters []Parameter `json:"parameters,omitempty"` + Name string `json:"name"` + Type DefinitionType `json:"type"` + Template string `json:"template,omitempty"` + Parameters []Parameter `json:"parameters,omitempty"` + DefinitionPath string `json:"definition"` } type DefinitionType string @@ -40,13 +44,12 @@ const ( ) type Parameter struct { - Name string `json:"name"` - Short string `json:"short,omitempty"` - Required bool `json:"required,omitempty"` - FieldPaths []string `json:"fieldPaths"` - Default string `json:"default,omitempty"` - Usage string `json:"usage,omitempty"` - Type string `json:"type,omitempty"` + Name string `json:"name"` + Short string `json:"short,omitempty"` + Required bool `json:"required,omitempty"` + Default interface{} `json:"default,omitempty"` + Usage string `json:"usage,omitempty"` + Type cue.Kind `json:"type,omitempty"` } // ConvertTemplateJson2Object convert spec.extension to object @@ -63,6 +66,21 @@ func ConvertTemplateJson2Object(in *runtime.RawExtension) (Template, error) { if err == nil { t = extension } - return t, err } + +func SetFlagBy(cmd *cobra.Command, v Parameter) { + switch v.Type { + case cue.IntKind: + cmd.Flags().Int64P(v.Name, v.Short, v.Default.(int64), v.Usage) + case cue.StringKind: + cmd.Flags().StringP(v.Name, v.Short, v.Default.(string), v.Usage) + case cue.BoolKind: + cmd.Flags().BoolP(v.Name, v.Short, v.Default.(bool), v.Usage) + case cue.NumberKind, cue.FloatKind: + cmd.Flags().Float64P(v.Name, v.Short, v.Default.(float64), v.Usage) + } + if v.Required && v.Name != "name" { + cmd.MarkFlagRequired(v.Name) + } +} diff --git a/api/types/types.go b/api/types/types.go index 6fbfd1c13..c2c8d7c95 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -1,5 +1,11 @@ package types +import ( + "errors" + "fmt" + "sort" +) + const ( DefaultOAMNS = "oam-system" DefaultOAMReleaseName = "core-runtime" @@ -7,4 +13,115 @@ const ( DefaultOAMRepoName = "crossplane-master" DefaultOAMRepoUrl = "https://charts.crossplane.io/master" DefaultOAMVersion = ">0.0.0-0" + + DefaultEnvName = "default" ) + +const ( + Traits = "traits" + Scopes = "scopes" +) + +type Application struct { + Name string `json:"name"` + // key of map is component name + Components map[string]map[string]interface{} `json:"components"` + Secrets map[string]map[string]interface{} `json:"secrets"` + Scopes map[string]map[string]interface{} `json:"appScopes"` +} + +func (app *Application) Valid() error { + if app.Name == "" { + return errors.New("name is required") + } + if len(app.Components) == 0 { + return errors.New("at least one component is required") + } + for name, comp := range app.Components { + lenth := len(comp) + if traits, ok := comp[Traits]; ok { + lenth-- + trs, ok := traits.(map[string]interface{}) + if !ok { + return fmt.Errorf("format of traits in %s must be map", name) + } + for traitName, tr := range trs { + _, ok := tr.(map[string]interface{}) + if !ok { + return fmt.Errorf("trait %s in %s must be map", traitName, name) + } + } + } + if scopes, ok := comp[Scopes]; ok { + lenth-- + _, ok := scopes.([]string) + if !ok { + return fmt.Errorf("format of scopes in %s must be string array", name) + } + } + if lenth != 1 { + return fmt.Errorf("you must have only one workload in component %s", name) + } + for workloadType, workload := range comp { + if NotWorkload(workloadType) { + continue + } + _, ok := workload.(map[string]interface{}) + if !ok { + return fmt.Errorf("format of workload in %s must be map", name) + } + //TODO(wonderflow) check workload type exists + //TODO(wonderflow) check arguments of workload is valid + } + } + //TODO(wonderflow) check scope types + return nil +} + +func NotWorkload(tp string) bool { + if tp == Scopes || tp == Traits { + return true + } + return false +} + +func (app *Application) GetComponents() []string { + var components []string + for name := range app.Components { + components = append(components, name) + } + sort.Strings(components) + return components +} + +func (app *Application) GetWorkload(componentName string) (string, map[string]interface{}, error) { + comp, ok := app.Components[componentName] + if !ok { + return "", nil, fmt.Errorf("%s not exist", componentName) + } + for tp, workload := range comp { + if NotWorkload(tp) { + continue + } + return tp, workload.(map[string]interface{}), nil + } + return "", nil, fmt.Errorf("workload not exist in %s", componentName) +} + +func (app *Application) GetTraits(componentName string) (map[string]interface{}, error) { + comp, ok := app.Components[componentName] + if !ok { + return nil, fmt.Errorf("%s not exist", componentName) + } + t, ok := comp[Traits] + if !ok { + return map[string]interface{}{}, nil + } + // assume it's valid, use Valid() to check + traits := t.(map[string]interface{}) + return traits, nil +} + +type EnvMeta struct { + Namespace string `json:"namespace"` +} diff --git a/api/types/types_test.go b/api/types/types_test.go new file mode 100644 index 000000000..073ddcc77 --- /dev/null +++ b/api/types/types_test.go @@ -0,0 +1,162 @@ +package types + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestApplication(t *testing.T) { + yaml1 := `name: myapp +components: + frontend: + deployment: + image: inanimate/echo-server + env: + PORT: 8080 + traits: + autoscaling: + max: 10 + min: 1 + rollout: + strategy: canary + step: 5 + backend: + cloneset: + image: "back:v1" +` + yaml2 := `name: myapp` + yaml3 := `components: + frontend: + deployment: + image: inanimate/echo-server + env: + PORT: 8080` + yaml4 := `name: myapp +components: + frontend: + deployment: + image: inanimate/echo-server + scopes: + public-scope: true +appScopes: + public-scope: + networkPolicy: public` + yaml5 := `name: myapp +components: + frontend: + traits: + rollout: + strategy: canary + step: 5 + backend: + cloneset: + image: "back:v1" +` + yaml6 := `name: myapp +components: + frontend: + deployment: + image: inanimate/echo-server + traits: + autoscaling: 10` + + cases := map[string]struct { + raw string + InValid bool + InvalidReason error + ExpName string + ExpComponents []string + WantWorkload string + ExpWorklaod map[string]interface{} + ExpWorkloadType string + ExpTraits map[string]interface{} + }{ + "normal case backend": { + raw: yaml1, + ExpName: "myapp", + ExpComponents: []string{"backend", "frontend"}, + WantWorkload: "backend", + ExpWorklaod: map[string]interface{}{ + "image": "back:v1", + }, + ExpWorkloadType: "cloneset", + ExpTraits: map[string]interface{}{}, + }, + "normal case frontend": { + raw: yaml1, + ExpName: "myapp", + ExpComponents: []string{"backend", "frontend"}, + WantWorkload: "frontend", + ExpWorklaod: map[string]interface{}{ + "image": "inanimate/echo-server", + "env": map[string]interface{}{ + "PORT": 8080, + }, + }, + ExpWorkloadType: "deployment", + ExpTraits: map[string]interface{}{ + "autoscaling": map[string]interface{}{ + "max": 10, + "min": 1, + }, + "rollout": map[string]interface{}{ + "strategy": "canary", + "step": 5, + }, + }, + }, + "no component": { + raw: yaml2, + ExpName: "myapp", + InValid: true, + InvalidReason: errors.New("at least one component is required"), + }, + "no name": { + raw: yaml3, + ExpName: "", + InValid: true, + InvalidReason: errors.New("name is required"), + }, + "scopes not array": { + raw: yaml4, + ExpName: "myapp", + InValid: true, + InvalidReason: fmt.Errorf("format of scopes in frontend must be string array"), + }, + "workload not exist": { + raw: yaml5, + ExpName: "myapp", + InValid: true, + InvalidReason: fmt.Errorf("you must have only one workload in component frontend"), + }, + "trait must be map": { + raw: yaml6, + ExpName: "myapp", + InValid: true, + InvalidReason: fmt.Errorf("trait autoscaling in frontend must be map"), + }, + } + for caseName, c := range cases { + var app Application + err := yaml.Unmarshal([]byte(c.raw), &app) + assert.NoError(t, err, caseName) + err = app.Valid() + if c.InValid { + assert.Equal(t, c.InvalidReason, err) + continue + } + assert.Equal(t, c.ExpName, app.Name, caseName) + assert.Equal(t, c.ExpComponents, app.GetComponents(), caseName) + workloadType, workload, err := app.GetWorkload(c.WantWorkload) + assert.NoError(t, err, caseName) + assert.Equal(t, c.ExpWorklaod, workload, caseName) + assert.Equal(t, c.ExpWorkloadType, workloadType, caseName) + traits, err := app.GetTraits(c.WantWorkload) + assert.NoError(t, err, caseName) + assert.Equal(t, c.ExpTraits, traits) + } +} diff --git a/cmd/rudrx/main.go b/cmd/rudrx/main.go index 16561e26a..0a30af4a7 100644 --- a/cmd/rudrx/main.go +++ b/cmd/rudrx/main.go @@ -7,6 +7,8 @@ import ( "runtime" "time" + "github.com/cloud-native-application/rudrx/pkg/utils/system" + "github.com/crossplane/oam-kubernetes-runtime/apis/core" "github.com/spf13/cobra" k8sruntime "k8s.io/apimachinery/pkg/runtime" @@ -77,6 +79,14 @@ func newCommand() *cobra.Command { fmt.Println("create client from kubeconfig err", err) os.Exit(1) } + if err := system.InitApplicationDir(); err != nil { + fmt.Println("InitApplicationDir err", err) + os.Exit(1) + } + if err := system.InitDefinitionDir(); err != nil { + fmt.Println("InitDefinitionDir err", err) + os.Exit(1) + } cmds.AddCommand( cmd.NewTraitsCommand(f, client, ioStream, []string{}), diff --git a/config/samples/traits/manualscaletrait_definition.yaml b/config/samples/traits/manualscaletrait_definition.yaml index 623e4c7e2..62a93d699 100644 --- a/config/samples/traits/manualscaletrait_definition.yaml +++ b/config/samples/traits/manualscaletrait_definition.yaml @@ -8,16 +8,15 @@ spec: definitionRef: name: manualscalertraits.core.oam.dev extension: - alias: ManualScaler - object: - apiVersion: core.oam.dev/v1alpha2 - kind: ManualScalerTrait - spec: - replicaCount: 2 - parameters: - - name: replicaCount - required: true - type: int - default: "5" - fieldPaths: - - "spec.replicaCount" + template: | + #Template: { + apiVersion: "core.oam.dev/v1alpha2" + kind: "ManualScalerTrait" + spec: { + replicaCount: manualscaler.replica + } + } + manualscaler: { + //+short=r + replica: *2 | int + } diff --git a/config/samples/traits/simplerollouttrait_definition.yaml b/config/samples/traits/simplerollouttrait_definition.yaml index 6f3b71ef6..2a1416dc2 100644 --- a/config/samples/traits/simplerollouttrait_definition.yaml +++ b/config/samples/traits/simplerollouttrait_definition.yaml @@ -10,35 +10,18 @@ spec: definitionRef: name: simplerollouttraits.extend.oam.dev extension: - alias: SimpleRollout - object: - apiVersion: extend.oam.dev/v1alpha2 - kind: SimpleRolloutTrait - metadata: - name: example-rollout-trait - spec: - replica: 6 - maxUnavailable: 2 - batch: 2 - parameters: - - name: replica - required: true - type: int - default: "6" - short: r - fieldPaths: - - "spec.replica" - - name: maxUnavailable - required: true - type: int - default: "2" - short: u - fieldPaths: - - "spec.maxUnavailable" - - name: batch - required: true - type: int - default: "2" - short: b - fieldPaths: - - "spec.batch" + template: | + #Template: { + apiVersion: "extend.oam.dev/v1alpha2" + kind: "SimpleRolloutTrait" + spec: { + replica: rollout.replica + maxUnavailable: rollout.maxUnavailable + batch: rollout.batch + } + } + rollout: { + replica: *3 | int + maxUnavailable: *1 | int + batch: *2 | int + } diff --git a/config/samples/workloads/containerizedworkload_definition.yaml b/config/samples/workloads/containerizedworkload_definition.yaml index 974a2f831..3d1f3f147 100644 --- a/config/samples/workloads/containerizedworkload_definition.yaml +++ b/config/samples/workloads/containerizedworkload_definition.yaml @@ -11,30 +11,30 @@ spec: - apiVersion: v1 kind: Service extension: - alias: containerized - object: - apiVersion: core.oam.dev/v1alpha2 - kind: ContainerizedWorkload - metadata: - name: tbd - spec: - containers: - - image: myrepo/myapp:v1 - name: master - ports: - - containerPort: 6379 - protocol: TCP - name: tbd # TODO(zzxwill) A temporary workaround for ContainerizedWorkload - parameters: - - name: image - short: i - required: true - type: string - fieldPaths: - - "spec.containers[0].image" - - name: port - short: p - required: false - type: int - fieldPaths: - - "spec.containers[0].ports[0].containerPort" + template: | + #Template: { + apiVersion: "core.oam.dev/v1alpha2" + kind: "ContainerizedWorkload" + metadata: name: containerized.name + spec: { + containers: [{ + image: containerized.image + name: containerized.name + ports: [{ + containerPort: containerized.port + protocol: "TCP" + name: "default" + }] + }] + } + } + containerized: { + name: string + // +usage=specify app image + // +short=i + image: string + // +usage=specify port for container + // +short=p + port: *6379 | int + } + diff --git a/config/samples/workloads/deployment_definition.yaml b/config/samples/workloads/deployment_definition.yaml index 42f6980dd..cf9699ddc 100644 --- a/config/samples/workloads/deployment_definition.yaml +++ b/config/samples/workloads/deployment_definition.yaml @@ -7,29 +7,31 @@ spec: name: deployments.apps extension: alias: deployment - object: - apiVersion: core.oam.dev/v1alpha2 - kind: deployment - metadata: - name: tbd - spec: - containers: - - image: myrepo/myapp:v1 - name: master - ports: - - containerPort: 6379 - protocol: TCP - name: tbd # TODO(zzxwill) A temporary workaround for ContainerizedWorkload - parameters: - - name: image - short: i - required: true - type: string - fieldPaths: - - "spec.containers[0].image" - - name: port - short: p - required: false - type: int - fieldPaths: - - "spec.containers[0].ports[0].containerPort" + template: | + #Template: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: deployment.name + spec: { + containers: [{ + image: deployment.image + name: deployment.name + env: deployment.env + ports: [{ + containerPort: deployment.port + protocol: "TCP" + name: "default" + }] + }] + } + } + deployment: { + name: string + image: string + port: *8080 | int + env: [...{ + name: string + value: string + }] + } + diff --git a/go.mod b/go.mod index 7a401906f..6d37381ff 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.6.1 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.2.4 k8s.io/api v0.18.6 diff --git a/pkg/cmd/delete.go b/pkg/cmd/delete.go index 61f276834..56a38ee8e 100644 --- a/pkg/cmd/delete.go +++ b/pkg/cmd/delete.go @@ -5,6 +5,8 @@ import ( "errors" "fmt" + "github.com/cloud-native-application/rudrx/api/types" + cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" "github.com/spf13/cobra" @@ -16,7 +18,7 @@ type deleteOptions struct { AppConfig corev1alpha2.ApplicationConfiguration client client.Client cmdutil.IOStreams - Env *EnvMeta + Env *types.EnvMeta } func newDeleteOptions(ioStreams cmdutil.IOStreams) *deleteOptions { diff --git a/pkg/cmd/env.go b/pkg/cmd/env.go index 02ad165d8..ed72bde40 100644 --- a/pkg/cmd/env.go +++ b/pkg/cmd/env.go @@ -8,15 +8,17 @@ import ( "os" "path/filepath" + "github.com/cloud-native-application/rudrx/api/types" + + "github.com/cloud-native-application/rudrx/pkg/utils/system" + cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" "github.com/gosuri/uitable" "github.com/spf13/cobra" ) -const DefaultEnvName = "default" - func NewEnvInitCommand(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command { - var envArgs EnvMeta + var envArgs types.EnvMeta ctx := context.Background() cmd := &cobra.Command{ Use: "env:init", @@ -81,10 +83,6 @@ func NewEnvSwitchCommand(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra. return cmd } -type EnvMeta struct { - Namespace string `json:"namespace"` -} - func ListEnvs(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) error { table := uitable.New() table.MaxColWidth = 60 @@ -99,7 +97,7 @@ func ListEnvs(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) e ioStreams.Infof(table.String()) return nil } - envDir, err := getEnvDir() + envDir, err := system.GetEnvDir() if err != nil { return err } @@ -115,7 +113,7 @@ func ListEnvs(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) e if err != nil { continue } - var envMeta EnvMeta + var envMeta types.EnvMeta if err = json.Unmarshal(data, &envMeta); err != nil { continue } @@ -137,7 +135,7 @@ func DeleteEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) if envname == curEnv { return fmt.Errorf("you can't delete current using env %s", curEnv) } - envdir, err := getEnvDir() + envdir, err := system.GetEnvDir() if err != nil { return err } @@ -148,29 +146,7 @@ func DeleteEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) return nil } -func InitDefaultEnv() error { - envDir, err := getEnvDir() - if err != nil { - return err - } - if err = os.MkdirAll(envDir, 0755); err != nil { - return err - } - data, _ := json.Marshal(&EnvMeta{Namespace: DefaultEnvName}) - if err = ioutil.WriteFile(filepath.Join(envDir, DefaultEnvName), data, 0644); err != nil { - return err - } - curEnvPath, err := getCurrentEnvPath() - if err != nil { - return err - } - if err = ioutil.WriteFile(curEnvPath, []byte(DefaultEnvName), 0644); err != nil { - return err - } - return nil -} - -func CreateOrUpdateEnv(ctx context.Context, envArgs *EnvMeta, args []string, ioStreams cmdutil.IOStreams) error { +func CreateOrUpdateEnv(ctx context.Context, envArgs *types.EnvMeta, args []string, ioStreams cmdutil.IOStreams) error { if len(args) < 1 { return fmt.Errorf("you must specify env name for rudr env:init command") } @@ -179,14 +155,14 @@ func CreateOrUpdateEnv(ctx context.Context, envArgs *EnvMeta, args []string, ioS if err != nil { return err } - envdir, err := getEnvDir() + envdir, err := system.GetEnvDir() if err != nil { return err } if err = ioutil.WriteFile(filepath.Join(envdir, envname), data, 0644); err != nil { return err } - curEnvPath, err := getCurrentEnvPath() + curEnvPath, err := system.GetCurrentEnvPath() if err != nil { return err } @@ -197,28 +173,12 @@ func CreateOrUpdateEnv(ctx context.Context, envArgs *EnvMeta, args []string, ioS return nil } -func getCurrentEnvPath() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, ".rudr", "curenv"), nil -} - -func getEnvDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", err - } - return filepath.Join(home, ".rudr", "envs"), nil -} - func SwitchEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) error { if len(args) < 1 { return fmt.Errorf("you must specify env name for rudr env command") } envname := args[0] - currentEnvPath, err := getCurrentEnvPath() + currentEnvPath, err := system.GetCurrentEnvPath() if err != nil { return err } @@ -234,7 +194,7 @@ func SwitchEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) } func GetCurrentEnvName() (string, error) { - currentEnvPath, err := getCurrentEnvPath() + currentEnvPath, err := system.GetCurrentEnvPath() if err != nil { return "", err } @@ -245,22 +205,22 @@ func GetCurrentEnvName() (string, error) { return string(data), nil } -func GetEnv() (*EnvMeta, error) { +func GetEnv() (*types.EnvMeta, error) { envName, err := GetCurrentEnvName() if err != nil { if !os.IsNotExist(err) { return nil, err } - if err = InitDefaultEnv(); err != nil { + if err = system.InitDefaultEnv(); err != nil { return nil, err } - envName = DefaultEnvName + envName = types.DefaultEnvName } return getEnvByName(envName) } -func getEnvByName(name string) (*EnvMeta, error) { - envdir, err := getEnvDir() +func getEnvByName(name string) (*types.EnvMeta, error) { + envdir, err := system.GetEnvDir() if err != nil { return nil, err } @@ -268,7 +228,7 @@ func getEnvByName(name string) (*EnvMeta, error) { if err != nil { return nil, err } - var meta EnvMeta + var meta types.EnvMeta if err = json.Unmarshal(data, &meta); err != nil { return nil, err } diff --git a/pkg/cmd/env_test.go b/pkg/cmd/env_test.go index 1102e0eca..52a350637 100644 --- a/pkg/cmd/env_test.go +++ b/pkg/cmd/env_test.go @@ -6,6 +6,10 @@ import ( "os" "testing" + "github.com/cloud-native-application/rudrx/api/types" + + "github.com/cloud-native-application/rudrx/pkg/utils/system" + cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" "github.com/stretchr/testify/assert" ) @@ -14,7 +18,7 @@ func TestENV(t *testing.T) { ctx := context.Background() // Create Default Env - err := InitDefaultEnv() + err := system.InitDefaultEnv() assert.NoError(t, err) // check and compare create default env success @@ -23,12 +27,12 @@ func TestENV(t *testing.T) { assert.Equal(t, "default", curEnvName) gotEnv, err := GetEnv() assert.NoError(t, err) - assert.Equal(t, &EnvMeta{ + assert.Equal(t, &types.EnvMeta{ Namespace: "default", }, gotEnv) ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} - exp := &EnvMeta{ + exp := &types.EnvMeta{ Namespace: "test1", } @@ -70,7 +74,7 @@ env1 test1 `, b.String()) // check switch success gotEnv, err = GetEnv() assert.NoError(t, err) - assert.Equal(t, &EnvMeta{ + assert.Equal(t, &types.EnvMeta{ Namespace: "default", }, gotEnv) diff --git a/pkg/cmd/fixtures_test.go b/pkg/cmd/fixtures_test.go index aafe0cdf4..7b90a1a0e 100644 --- a/pkg/cmd/fixtures_test.go +++ b/pkg/cmd/fixtures_test.go @@ -24,55 +24,28 @@ func init() { var ( workloadTemplateExample = &types.Template{ - Object: map[string]interface{}{ - "apiVersion": "core.oam.dev/v1alpha2", - "kind": "ContainerizedWorkload", - "metadata": map[string]interface{}{ - "name": "pod", - }, - "spec": map[string]interface{}{ - "containers": "", - }, - }, Parameters: []types.Parameter{ types.Parameter{ - Name: "image", - Short: "i", - Required: true, - Type: "string", - FieldPaths: []string{"spec.containers[0].image"}, + Name: "image", + Short: "i", + Required: true, }, types.Parameter{ - Name: "port", - Short: "p", - Required: false, - Type: "int", - FieldPaths: []string{"spec.containers[0].ports[0].containerPort"}, + Name: "port", + Short: "p", + Required: false, }, }, } traitTemplateExample = &types.Template{ - Object: map[string]interface{}{ - "apiVersion": "core.oam.dev/v1alpha2", - "kind": "ManualScalerTrait", - "metadata": map[string]interface{}{ - "name": "pod", - }, - "spec": map[string]interface{}{ - "replicaCount": "2", - }, - }, - Parameters: []types.Parameter{ types.Parameter{ - Name: "replicaCount", - Short: "i", - Required: true, - Type: "int", - FieldPaths: []string{"spec.replicaCount"}, - Default: "5", + Name: "replicaCount", + Short: "i", + Required: true, + Default: "5", }, }, } diff --git a/pkg/cmd/ls.go b/pkg/cmd/ls.go index a3ede7e86..2afb6aebd 100644 --- a/pkg/cmd/ls.go +++ b/pkg/cmd/ls.go @@ -41,7 +41,7 @@ func printApplicationList(ctx context.Context, c client.Client, appName string, table.MaxColWidth = 60 if err != nil { - fmt.Printf("listing Trait Definition hit an issue: %s\n", err) + fmt.Printf("listing Trait DefinitionPath hit an issue: %s\n", err) return } diff --git a/pkg/cmd/trait_bind.go b/pkg/cmd/trait_bind.go index 47d7f8a52..761a34e6b 100644 --- a/pkg/cmd/trait_bind.go +++ b/pkg/cmd/trait_bind.go @@ -8,6 +8,12 @@ import ( "strconv" "strings" + mycue "github.com/cloud-native-application/rudrx/pkg/cue" + + "cuelang.org/go/cue" + + "github.com/cloud-native-application/rudrx/pkg/utils/system" + "k8s.io/apimachinery/pkg/runtime" "github.com/cloud-native-application/rudrx/pkg/plugins" @@ -23,13 +29,13 @@ import ( ) type commandOptions struct { - Env *EnvMeta Template types.Template Component corev1alpha2.Component AppConfig corev1alpha2.ApplicationConfiguration Client client.Client TraitAlias string Detach bool + Env *types.EnvMeta cmdutil.IOStreams } @@ -38,13 +44,14 @@ func NewCommandOptions(ioStreams cmdutil.IOStreams) *commandOptions { } func AddTraitPlugins(parentCmd *cobra.Command, c client.Client, ioStreams cmdutil.IOStreams) error { - templates, err := plugins.GetTraitsFromCluster(context.TODO(), types.DefaultOAMNS, c) + dir, _ := system.GetDefinitionDir() + templates, err := plugins.GetTraitsFromCluster(context.TODO(), types.DefaultOAMNS, c, dir) if err != nil { return err } ctx := context.Background() for _, tmp := range templates { - var name = tmp.Alias + var name = tmp.Name o := NewCommandOptions(ioStreams) o.Client = c o.Env, _ = GetEnv() @@ -63,10 +70,7 @@ func AddTraitPlugins(parentCmd *cobra.Command, c client.Client, ioStreams cmduti } pluginCmd.SetOut(o.Out) for _, v := range tmp.Parameters { - pluginCmd.Flags().StringP(v.Name, v.Short, v.Default, v.Usage) - if v.Required { - pluginCmd.MarkFlagRequired(v.Name) - } + types.SetFlagBy(pluginCmd, v) } o.Template = tmp @@ -100,21 +104,39 @@ func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context return err } - pvd := fieldpath.Pave(o.Template.Object) + var traitData = make(map[string]interface{}) + + var tp = o.Template.Name + for _, v := range o.Template.Parameters { flagSet := cmd.Flag(v.Name) - for _, path := range v.FieldPaths { - fValue := flagSet.Value.String() - if v.Type == "int" { - portValue, _ := strconv.ParseFloat(fValue, 64) - pvd.SetNumber(path, portValue) - continue - } - pvd.SetString(path, fValue) + switch v.Type { + case cue.IntKind: + d, _ := strconv.ParseInt(flagSet.Value.String(), 10, 64) + traitData[v.Name] = d + case cue.StringKind: + traitData[v.Name] = flagSet.Value.String() + case cue.BoolKind: + d, _ := strconv.ParseBool(flagSet.Value.String()) + traitData[v.Name] = d + case cue.NumberKind, cue.FloatKind: + d, _ := strconv.ParseFloat(flagSet.Value.String(), 64) + traitData[v.Name] = d } } + + jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, traitData) + if err != nil { + return err + } + var obj = make(map[string]interface{}) + if err = json.Unmarshal([]byte(jsondata), &obj); err != nil { + return err + } + + pvd := fieldpath.Pave(obj) // metadata.name needs to be in lower case. - pvd.SetString("metadata.name", strings.ToLower(fmt.Sprintf("%s-%s-trait", appName, o.Template.Alias))) + pvd.SetString("metadata.name", strings.ToLower(fmt.Sprintf("%s-%s-trait", appName, o.Template.Name))) curObj := &unstructured.Unstructured{Object: pvd.UnstructuredContent()} var updated bool for ic, c := range o.AppConfig.Spec.Components { @@ -124,7 +146,7 @@ func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context for it, t := range c.Traits { g, v, k := GetGVKFromRawExtension(t.Trait) - // TODO(wonderflow): we should get GVK from Definition instead of assuming template object contains + // TODO(wonderflow): we should get GVK from DefinitionPath instead of assuming template object contains gvk := curObj.GroupVersionKind() if gvk.Group == g && gvk.Version == v && gvk.Kind == k { updated = true @@ -142,13 +164,14 @@ func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context } func DetachTraitPlugins(parentCmd *cobra.Command, c client.Client, ioStreams cmdutil.IOStreams) error { - templates, err := plugins.GetTraitsFromCluster(context.TODO(), types.DefaultOAMNS, c) + dir, _ := system.GetDefinitionDir() + templates, err := plugins.GetTraitsFromCluster(context.TODO(), types.DefaultOAMNS, c, dir) if err != nil { return err } ctx := context.Background() for _, tmp := range templates { - var name = tmp.Alias + var name = tmp.Name o := NewCommandOptions(ioStreams) o.Client = c o.Env, _ = GetEnv() @@ -186,26 +209,24 @@ func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string, ctx cont return err } - _, _, tKind := cmdutil.GetTraitNameAliasKind(ctx, c, namespace, o.TraitAlias) - var traitDefinition corev1alpha2.TraitDefinition - + apiVersion, kind, err := cmdutil.GetTraitApiVersionKind(ctx, c, namespace, o.TraitAlias) + if err != nil { + return err + } for i, com := range o.AppConfig.Spec.Components { - traits := com.Traits - if com.ComponentName == appName { - for j := 0; j < len(traits); j++ { - err := json.Unmarshal(traits[j].Trait.Raw, &traitDefinition) - if err != nil { - return err - } - if strings.EqualFold(traitDefinition.Kind, tKind) { - traits = append(traits[:j], traits[j+1:]...) - j-- - } + if com.ComponentName != appName { + continue + } + var traits []corev1alpha2.ComponentTrait + for _, tr := range com.Traits { + a, k := tr.Trait.Object.GetObjectKind().GroupVersionKind().ToAPIVersionAndKind() + if a == apiVersion && k == kind { + continue } + traits = append(traits, tr) } o.AppConfig.Spec.Components[i].Traits = traits } - return nil } diff --git a/pkg/cmd/traits.go b/pkg/cmd/traits.go index e2faf084e..e3b2b4717 100644 --- a/pkg/cmd/traits.go +++ b/pkg/cmd/traits.go @@ -38,7 +38,7 @@ func printTraitList(ctx context.Context, c client.Client, workloadName *string, table.MaxColWidth = 60 if err != nil { - return fmt.Errorf("Listing Trait Definition hit an issue: %s", err) + return fmt.Errorf("Listing Trait DefinitionPath hit an issue: %s", err) } table.AddRow("NAME", "ALIAS", "DEFINITION", "APPLIES TO", "STATUS") diff --git a/pkg/cmd/util/helpers.go b/pkg/cmd/util/helpers.go index 0f40715a1..f45b16d81 100644 --- a/pkg/cmd/util/helpers.go +++ b/pkg/cmd/util/helpers.go @@ -203,7 +203,7 @@ func GetTraitDefinitionByAlias(ctx context.Context, c client.Client, traitAlias if err == nil { for _, t := range traitDefinitionList.Items { template, err := types.ConvertTemplateJson2Object(t.Spec.Extension) - if err == nil && strings.EqualFold(template.Alias, traitAlias) { + if err == nil && strings.EqualFold(template.Name, traitAlias) { traitDefinition = t break } @@ -214,29 +214,15 @@ func GetTraitDefinitionByAlias(ctx context.Context, c client.Client, traitAlias // GetTraitNameAndAlias return the name and alias of a TraitDefinition by a string which might be // the trait name, the trait alias, or invalid name -func GetTraitNameAliasKind(ctx context.Context, c client.Client, namespace string, name string) (string, string, string) { - var tName, tAlias, tKind string +func GetTraitApiVersionKind(ctx context.Context, c client.Client, namespace string, name string) (string, string, error) { t, err := GetTraitDefinitionByName(ctx, c, namespace, name) - - if err == nil { - template, err := types.ConvertTemplateJson2Object(t.Spec.Extension) - if err == nil { - tName, tAlias = t.Spec.Reference.Name, template.Alias - tKind = fmt.Sprintf("%v", template.Object["kind"]) - } - } else { - t, err := GetTraitDefinitionByAlias(ctx, c, name) - if err == nil { - template, err := types.ConvertTemplateJson2Object(t.Spec.Extension) - if err == nil { - tName, tAlias = t.Spec.Reference.Name, template.Alias - tKind = fmt.Sprintf("%v", template.Object["kind"]) - } - } + if err != nil { + return "", "", err } - - return tName, tAlias, tKind + apiVersion := t.Annotations["oam.appengine.info/apiVersion"] + kind := t.Annotations["oam.appengine.info/kind"] + return apiVersion, kind, nil } func GetWorkloadNameAliasKind(ctx context.Context, c client.Client, namespace string, workloadName string) (string, string, string) { @@ -248,14 +234,14 @@ func GetWorkloadNameAliasKind(ctx context.Context, c client.Client, namespace st var workloadTemplate types.Template workloadTemplate, err := types.ConvertTemplateJson2Object(w.Spec.Extension) if err == nil { - name, alias = w.Name, workloadTemplate.Alias + name, alias = w.Name, workloadTemplate.Name } } else { // workloadName is alias or kind w, err := GetWorkloadDefinitionByAlias(ctx, c, name) if err == nil { workloadTemplate, err := types.ConvertTemplateJson2Object(w.Spec.Extension) if err == nil { - name, alias, kind = w.Name, workloadTemplate.Alias, w.Kind + name, alias, kind = w.Name, workloadTemplate.Name, w.Kind } } diff --git a/pkg/cmd/workload_run.go b/pkg/cmd/workload_run.go index 565f25d15..b491b78cb 100644 --- a/pkg/cmd/workload_run.go +++ b/pkg/cmd/workload_run.go @@ -2,33 +2,44 @@ package cmd import ( "context" + "encoding/json" "errors" "fmt" + "io/ioutil" + "path/filepath" "strconv" "strings" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "gopkg.in/yaml.v3" + + "cuelang.org/go/cue" + + "github.com/cloud-native-application/rudrx/pkg/utils/system" + "github.com/cloud-native-application/rudrx/api/types" "github.com/cloud-native-application/rudrx/pkg/plugins" - "github.com/crossplane/crossplane-runtime/pkg/fieldpath" - corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util" + mycue "github.com/cloud-native-application/rudrx/pkg/cue" + + corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2" ) // ComponentWorkloadDefLabel indicate which workloaddefinition generate from const ComponentWorkloadDefLabel = "rudrx.oam.dev/workloadDef" type runOptions struct { - Template types.Template - Env *EnvMeta - Component corev1alpha2.Component - AppConfig corev1alpha2.ApplicationConfiguration - client client.Client + Template types.Template + Env *types.EnvMeta + workloadName string + client client.Client + app *types.Application cmdutil.IOStreams } @@ -37,13 +48,14 @@ func newRunOptions(ioStreams cmdutil.IOStreams) *runOptions { } func AddWorkloadPlugins(parentCmd *cobra.Command, c client.Client, ioStreams cmdutil.IOStreams) error { - templates, err := plugins.GetWorkloadsFromCluster(context.TODO(), types.DefaultOAMNS, c) + dir, _ := system.GetDefinitionDir() + templates, err := plugins.GetWorkloadsFromCluster(context.TODO(), types.DefaultOAMNS, c, dir) if err != nil { return err } for _, tmp := range templates { - var name = tmp.Alias + var name = tmp.Name o := newRunOptions(ioStreams) o.client = c o.Env, _ = GetEnv() @@ -62,10 +74,7 @@ func AddWorkloadPlugins(parentCmd *cobra.Command, c client.Client, ioStreams cmd } pluginCmd.SetOut(o.Out) for _, v := range tmp.Parameters { - pluginCmd.Flags().StringP(v.Name, v.Short, v.Default, v.Usage) - if v.Required { - pluginCmd.MarkFlagRequired(v.Name) - } + types.SetFlagBy(pluginCmd, v) } o.Template = tmp @@ -83,50 +92,79 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Con } workloadName := args[0] + // TODO(wonderflow): load application from file + var app = &types.Application{Name: workloadName} - workloadTemplate := o.Template - pvd := fieldpath.Pave(workloadTemplate.Object) - for _, v := range workloadTemplate.Parameters { - var paraV string - - flagSet := cmd.Flag(v.Name) - paraV = flagSet.Value.String() - - if paraV == "" { - continue - } - - for _, path := range v.FieldPaths { - if v.Type == "int" { - portValue, _ := strconv.ParseFloat(paraV, 64) - pvd.SetNumber(path, portValue) - break - } - pvd.SetString(path, paraV) - } + if app.Components == nil { + app.Components = make(map[string]map[string]interface{}) + } + tp, workloadData, err := app.GetWorkload(workloadName) + if err != nil { + // Not exist + tp = o.Template.Name + workloadData = make(map[string]interface{}) } - pvd.SetString("metadata.name", strings.ToLower(workloadName)) - namespace := o.Env.Namespace - o.Component.Spec.Workload.Object = &unstructured.Unstructured{Object: pvd.UnstructuredContent()} - o.Component.Name = args[0] - o.Component.Namespace = namespace - o.Component.Labels = map[string]string{ComponentWorkloadDefLabel: workloadName} - - o.AppConfig.Name = args[0] - o.AppConfig.Namespace = namespace - o.AppConfig.Spec.Components = append(o.AppConfig.Spec.Components, corev1alpha2.ApplicationConfigurationComponent{ComponentName: args[0]}) - - return nil + for _, v := range o.Template.Parameters { + flagSet := cmd.Flag(v.Name) + switch v.Type { + case cue.IntKind: + d, _ := strconv.ParseInt(flagSet.Value.String(), 10, 64) + workloadData[v.Name] = d + case cue.StringKind: + workloadData[v.Name] = flagSet.Value.String() + case cue.BoolKind: + d, _ := strconv.ParseBool(flagSet.Value.String()) + workloadData[v.Name] = d + case cue.NumberKind, cue.FloatKind: + d, _ := strconv.ParseFloat(flagSet.Value.String(), 64) + workloadData[v.Name] = d + } + } + workloadData["name"] = strings.ToLower(workloadName) + app.Components[workloadName] = map[string]interface{}{ + tp: workloadData, + } + o.workloadName = workloadName + o.app = app + appDir, _ := system.GetApplicationDir() + out, err := yaml.Marshal(app) + if err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(appDir, workloadName), out, 0644) } func (o *runOptions) Run(cmd *cobra.Command) error { - o.Infof("Creating AppConfig %s\n", o.AppConfig.Name) - err := o.client.Create(context.Background(), &o.Component) + var component corev1alpha2.Component + var appconfig corev1alpha2.ApplicationConfiguration + tp, data, _ := o.app.GetWorkload(o.workloadName) + jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, data) + if err != nil { + return err + } + var obj = make(map[string]interface{}) + if err = json.Unmarshal([]byte(jsondata), &obj); err != nil { + return err + } + + component.Spec.Workload.Object = &unstructured.Unstructured{Object: obj} + component.Name = o.workloadName + component.Namespace = o.Env.Namespace + component.Labels = map[string]string{ComponentWorkloadDefLabel: o.workloadName} + + appconfig.Name = o.workloadName + appconfig.Namespace = o.Env.Namespace + appconfig.Spec.Components = append(appconfig.Spec.Components, corev1alpha2.ApplicationConfigurationComponent{ComponentName: o.workloadName}) + + //TODO(wonderflow): we should also support update here + + o.Infof("Creating AppConfig %s\n", appconfig.Name) + err = o.client.Create(context.Background(), &component) if err != nil { return fmt.Errorf("create component err: %s", err) } - err = o.client.Create(context.Background(), &o.AppConfig) + err = o.client.Create(context.Background(), &appconfig) if err != nil { return fmt.Errorf("create appconfig err %s", err) } diff --git a/pkg/cmd/workloads.go b/pkg/cmd/workloads.go index f99ad6505..47604ec9e 100644 --- a/pkg/cmd/workloads.go +++ b/pkg/cmd/workloads.go @@ -35,7 +35,7 @@ func printWorkloadList(ctx context.Context, c client.Client, ioStreams cmdutil.I table.MaxColWidth = 60 if err != nil { - return fmt.Errorf("Listing Trait Definition hit an issue: %s", err) + return fmt.Errorf("Listing Trait DefinitionPath hit an issue: %s", err) } table.AddRow("NAME", "SHORT", "DEFINITION") diff --git a/pkg/cue/convert.go b/pkg/cue/convert.go new file mode 100644 index 000000000..8602bb679 --- /dev/null +++ b/pkg/cue/convert.go @@ -0,0 +1,169 @@ +package cue + +import ( + "errors" + "fmt" + "strings" + + "github.com/cloud-native-application/rudrx/api/types" + + "cuelang.org/go/cue" + "cuelang.org/go/pkg/encoding/json" +) + +const Template = "#Template" + +func Eval(templatePath, workloadType string, value map[string]interface{}) (string, error) { + r := cue.Runtime{} + template, err := r.Compile(templatePath, nil) + if err != nil { + return "", err + } + + tempValue := template.Value() + appValue, err := tempValue.Fill(value, workloadType).Eval().Struct() + if err != nil { + return "", err + } + + final, err := appValue.FieldByName(Template, true) + if err != nil { + return "", err + } + if err := final.Value.Validate(cue.Concrete(true), cue.Final()); err != nil { + return "", err + } + data, err := json.Marshal(final.Value) + if err != nil { + return "", err + } + + return data, nil +} + +func GetParameters(templatePath string) ([]types.Parameter, string, error) { + r := cue.Runtime{} + template, err := r.Compile(templatePath, nil) + if err != nil { + return nil, "", err + } + tempStruct, err := template.Value().Struct() + if err != nil { + return nil, "", err + } + var info cue.FieldInfo + var found bool + for i := 0; i < tempStruct.Len(); i++ { + info = tempStruct.Field(i) + if info.IsDefinition { + continue + } + found = true + break + } + if !found { + return nil, "", errors.New("arguments not exist") + } + str, err := info.Value.Struct() + if err != nil { + return nil, "", fmt.Errorf("arguments not defined as struct %v", err) + } + var workloadType = info.Name + var params []types.Parameter + for i := 0; i < str.Len(); i++ { + fi := str.Field(i) + if fi.IsDefinition { + continue + } + var param = types.Parameter{ + Name: fi.Name, + Required: true, + } + val := fi.Value + param.Type = fi.Value.IncompleteKind() + if def, ok := val.Default(); ok && def.IsConcrete() { + param.Required = false + param.Type = def.Kind() + param.Default = GetDefault(def) + } + if param.Default == nil { + param.Default = getDefaultByKind(param.Type) + } + + short, usage := RetrieveComments(val) + if short != "" { + param.Short = short + } + if usage != "" { + param.Usage = usage + } + params = append(params, param) + } + return params, workloadType, nil +} + +func getDefaultByKind(k cue.Kind) interface{} { + switch k { + case cue.IntKind: + var d int64 + return d + case cue.StringKind: + var d string + return d + case cue.BoolKind: + var d bool + return d + case cue.NumberKind, cue.FloatKind: + var d float64 + return d + } + // assume other cue kind won't be valid parameter + return nil +} + +func GetDefault(val cue.Value) interface{} { + switch val.Kind() { + case cue.IntKind: + if d, err := val.Int64(); err == nil { + return d + } + case cue.StringKind: + if d, err := val.String(); err == nil { + return d + } + case cue.BoolKind: + if d, err := val.Bool(); err == nil { + return d + } + case cue.NumberKind, cue.FloatKind: + if d, err := val.Float64(); err == nil { + return d + } + } + return getDefaultByKind(val.Kind()) +} + +const ( + UsagePrefix = "+usage=" + ShortPrefix = "+short=" +) + +func RetrieveComments(value cue.Value) (string, string) { + var short, usage string + docs := value.Doc() + for _, doc := range docs { + lines := strings.Split(doc.Text(), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + line = strings.TrimPrefix(line, "//") + line = strings.TrimSpace(line) + if strings.HasPrefix(line, ShortPrefix) { + short = strings.TrimPrefix(line, ShortPrefix) + } + if strings.HasPrefix(line, UsagePrefix) { + usage = strings.TrimPrefix(line, UsagePrefix) + } + } + } + return short, usage +} diff --git a/pkg/cue/convert_test.go b/pkg/cue/convert_test.go new file mode 100644 index 000000000..6af549d18 --- /dev/null +++ b/pkg/cue/convert_test.go @@ -0,0 +1,62 @@ +package cue + +import ( + "fmt" + "testing" + + "cuelang.org/go/cue" + "cuelang.org/go/pkg/encoding/json" + + "github.com/cloud-native-application/rudrx/api/types" + + "github.com/stretchr/testify/assert" +) + +func TestEval(t *testing.T) { + _, workloadType, err := GetParameters("testdata/workloads/deployment.cue") + assert.NoError(t, err) + data, err := Eval("testdata/workloads/deployment.cue", workloadType, map[string]interface{}{ + "image": "nginx:v1", + "port": 8080, + "name": "myapp", + "env": []interface{}{ + map[string]interface{}{ + "name": "MYDB", + "value": "true", + }, + }, + }) + assert.NoError(t, err) + assert.Equal(t, `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"myapp"},"spec":{"containers":[{"name":"myapp","env":[{"name":"MYDB","value":"true"}],"image":"nginx:v1","ports":[{"name":"default","containerPort":8080,"protocol":"TCP"}]}]}}`, + data) +} + +func TestGetparam(t *testing.T) { + params, workloadType, err := GetParameters("testdata/workloads/deployment.cue") + assert.NoError(t, err) + assert.Equal(t, "deployment", workloadType) + assert.Equal(t, []types.Parameter{ + {Name: "name", Required: true, Default: "", Type: cue.StringKind}, + {Name: "env", Required: false, Default: nil, Type: cue.ListKind}, + {Name: "image", Short: "i", Required: true, Usage: "specify app image", Default: "", Type: cue.StringKind}, + {Name: "port", Short: "p", Usage: "specify port for container", Default: int64(8080), Type: cue.IntKind}}, params) + + params, workloadType, err = GetParameters("testdata/workloads/test-param.cue") + assert.NoError(t, err) + assert.Equal(t, "deployment", workloadType) + assert.Equal(t, []types.Parameter{ + {Name: "name", Required: true, Default: "", Type: cue.StringKind}, + {Name: "env", Required: false, Default: nil, Type: cue.ListKind}, + {Name: "image", Short: "i", Required: true, Usage: "specify app image", Default: "", Type: cue.StringKind}, + {Name: "port", Short: "p", Usage: "specify port for container", Default: int64(8080), Type: cue.IntKind}, + {Name: "enable", Default: false, Type: cue.BoolKind}, + {Name: "fval", Default: 64.3, Type: cue.FloatKind}, + {Name: "nval", Default: float64(0), Required: true, Type: cue.NumberKind}}, params) +} + +func TestName(t *testing.T) { + var r cue.Runtime + ins, _ := r.Compile("testdata/workloads/deployment.cue", nil) + ins.Value() + fmt.Println(json.Marshal(ins.Value())) +} diff --git a/pkg/cue/generate.go b/pkg/cue/generate.go deleted file mode 100644 index 33c64c301..000000000 --- a/pkg/cue/generate.go +++ /dev/null @@ -1,101 +0,0 @@ -package cue - -import ( - "errors" - "fmt" - - "cuelang.org/go/cue" - "cuelang.org/go/pkg/encoding/json" -) - -const Template = "#Template" - -func Eval(templatePath, appPath, workloadType string) (string, error) { - r := cue.Runtime{} - template, err := r.Compile(templatePath, nil) - if err != nil { - return "", err - } - app, err := r.Compile(appPath, nil) - if err != nil { - return "", err - } - - tempValue := template.Value() - appinfo, err := app.Value().FieldByName(workloadType, false) - if err != nil { - return "", err - } - - appValue, err := tempValue.Fill(appinfo.Value, workloadType).Eval().Struct() - if err != nil { - return "", err - } - - final, err := appValue.FieldByName(Template, true) - if err != nil { - return "", err - } - if err := final.Value.Validate(cue.Concrete(true), cue.Final()); err != nil { - return "", err - } - data, err := json.Marshal(final.Value) - if err != nil { - return "", err - } - - return data, nil -} - -type CueParameter struct { - Name string - Default interface{} -} - -func GetParameters(templatePath string) ([]CueParameter, string, error) { - r := cue.Runtime{} - template, err := r.Compile(templatePath, nil) - if err != nil { - return nil, "", err - } - tempStruct, err := template.Value().Struct() - if err != nil { - return nil, "", err - } - var info cue.FieldInfo - var found bool - for i := 0; i < tempStruct.Len(); i++ { - info = tempStruct.Field(i) - if info.IsDefinition { - continue - } - found = true - break - } - if !found { - return nil, "", errors.New("arguments not exist") - } - str, err := info.Value.Struct() - if err != nil { - return nil, "", fmt.Errorf("arguments not defined as struct %v", err) - } - var workloadType = info.Name - var params []CueParameter - for i := 0; i < str.Len(); i++ { - fi := str.Field(i) - if fi.IsDefinition { - continue - } - var param = CueParameter{ - Name: fi.Name, - } - - if def, ok := fi.Value.Default(); ok && def.IsConcrete() { - if data, err := def.MarshalJSON(); err == nil { - param.Default = string(data) - } - } - params = append(params, param) - } - return params, workloadType, nil -} diff --git a/pkg/cue/generate_test.go b/pkg/cue/generate_test.go deleted file mode 100644 index 6b5eb4b68..000000000 --- a/pkg/cue/generate_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package cue - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestEval(t *testing.T) { - _, workloadType, err := GetParameters("testdata/workloads/deployment/deployment.cue") - assert.NoError(t, err) - data, err := Eval("testdata/workloads/deployment/deployment.cue", "testdata/apps/myapp.cue", workloadType) - assert.NoError(t, err) - assert.Equal(t, `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"myapp"},"spec":{"containers":[{"name":"myapp","env":[{"name":"MYDB","value":"true"}],"image":"nginx:v1","ports":[{"name":"default","containerPort":8080,"protocol":"TCP"}]}]}}`, - data) -} - -func TestGetparam(t *testing.T) { - params, workloadType, err := GetParameters("testdata/workloads/deployment/deployment.cue") - assert.NoError(t, err) - assert.Equal(t, "deployment", workloadType) - assert.Equal(t, []CueParameter{{Name: "name"}, {Name: "env", Default: "[]"}, {Name: "image"}, {Name: "port", Default: "8080"}}, params) -} diff --git a/pkg/cue/testdata/apps/myapp.cue b/pkg/cue/testdata/apps/myapp.cue deleted file mode 100644 index b516e048c..000000000 --- a/pkg/cue/testdata/apps/myapp.cue +++ /dev/null @@ -1,12 +0,0 @@ -deployment: { - name: "myapp" - port: 8080 - image: "nginx:v1" - env: [{ - name: "MYDB" - value: "true" - }] -} -route: { - domain: "www.example.com" -} diff --git a/pkg/cue/testdata/apps/myapp.yaml b/pkg/cue/testdata/apps/myapp.yaml new file mode 100644 index 000000000..ca28438d8 --- /dev/null +++ b/pkg/cue/testdata/apps/myapp.yaml @@ -0,0 +1,31 @@ +name: myapp +components: + frontend: + deployment: + image: inanimate/echo-server + env: + PORT: "8080" + traits: + autoscaling: + max: 10 + min: 1 + rollout: + strategy: canary + step: 5 + expose: + service: + type: LoadBalancer + ports: + http: + service_port: 80 + container_port: 8080 + scopes: + - public-scope +secrets: + secret-foo: + key1: 'pass-word' +appScopes: + public-scope: + networkPolicy: public + private-scope: + networkPolicy: private \ No newline at end of file diff --git a/pkg/cue/testdata/traits/manualscaler.cue b/pkg/cue/testdata/traits/manualscaler.cue new file mode 100644 index 000000000..3a23b24b5 --- /dev/null +++ b/pkg/cue/testdata/traits/manualscaler.cue @@ -0,0 +1,11 @@ +#Template: { + apiVersion: "core.oam.dev/v1alpha2" + kind: "ManualScalerTrait" + spec: { + replicaCount: manualscaler.replica + } +} +manualscaler: { + //+short=r + replica: *2 | int +} diff --git a/pkg/cue/testdata/traits/rollout.cue b/pkg/cue/testdata/traits/rollout.cue new file mode 100644 index 000000000..521252654 --- /dev/null +++ b/pkg/cue/testdata/traits/rollout.cue @@ -0,0 +1,15 @@ +#Template: { + apiVersion: "extend.oam.dev/v1alpha2" + kind: "SimpleRolloutTrait" + spec: { + replica: rollout.replica + maxUnavailable: rollout.maxUnavailable + batch: rollout.batch + } +} + +rollout: { + replica: *3 | int + maxUnavailable: *1 | int + batch: *2 | int +} diff --git a/pkg/cue/testdata/traits/route.cue b/pkg/cue/testdata/traits/route.cue new file mode 100644 index 000000000..0e40bd5bc --- /dev/null +++ b/pkg/cue/testdata/traits/route.cue @@ -0,0 +1,11 @@ +#Template: { + apiVersion: "apps/v1" + kind: "Route" + spec: { + domain: route.domain + } +} + +route: { + domain: string +} diff --git a/pkg/cue/testdata/workloads/containerizedworkload.cue b/pkg/cue/testdata/workloads/containerizedworkload.cue new file mode 100644 index 000000000..da52489c6 --- /dev/null +++ b/pkg/cue/testdata/workloads/containerizedworkload.cue @@ -0,0 +1,26 @@ +#Template: { + apiVersion: "core.oam.dev/v1alpha2" + kind: "ContainerizedWorkload" + metadata: name: containerized.name + spec: { + containers: [{ + image: containerized.image + name: containerized.name + ports: [{ + containerPort: containerized.port + protocol: "TCP" + name: "default" + }] + }] + } +} + +containerized: { + name: string + // +usage=specify app image + // +short=i + image: string + // +usage=specify port for container + // +short=p + port: *6379 | int +} diff --git a/pkg/cue/testdata/workloads/deployment/deployment.cue b/pkg/cue/testdata/workloads/deployment.cue similarity index 75% rename from pkg/cue/testdata/workloads/deployment/deployment.cue rename to pkg/cue/testdata/workloads/deployment.cue index 05bead49c..912731006 100644 --- a/pkg/cue/testdata/workloads/deployment/deployment.cue +++ b/pkg/cue/testdata/workloads/deployment.cue @@ -17,9 +17,13 @@ } deployment: { - name: string + name: string + // +usage=specify app image + // +short=i image: string - port: *8080 | int + // +usage=specify port for container + // +short=p + port: *8080 | int env: [...{ name: string value: string diff --git a/pkg/cue/testdata/workloads/test-param.cue b/pkg/cue/testdata/workloads/test-param.cue new file mode 100644 index 000000000..05c17d28a --- /dev/null +++ b/pkg/cue/testdata/workloads/test-param.cue @@ -0,0 +1,19 @@ +#Template: { +} + +deployment: { + name: string + // +usage=specify app image + // +short=i + image: string + // +usage=specify port for container + // +short=p + port: *8080 | int + env: [...{ + name: string + value: string + }] + enable: *false | bool + fval: *64.3 | number + nval: number +} diff --git a/pkg/plugins/cluster.go b/pkg/plugins/cluster.go index 62f88454a..fbe914bea 100644 --- a/pkg/plugins/cluster.go +++ b/pkg/plugins/cluster.go @@ -2,7 +2,14 @@ package plugins import ( "context" + "errors" "fmt" + "io/ioutil" + "path/filepath" + + "github.com/cloud-native-application/rudrx/pkg/cue" + + "k8s.io/apimachinery/pkg/runtime" "github.com/cloud-native-application/rudrx/api/types" @@ -10,12 +17,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func GetTemplatesFromCluster(ctx context.Context, namespace string, c client.Client) ([]types.Template, error) { - workloads, err := GetWorkloadsFromCluster(ctx, namespace, c) +func GetTemplatesFromCluster(ctx context.Context, namespace string, c client.Client, syncDir string) ([]types.Template, error) { + workloads, err := GetWorkloadsFromCluster(ctx, namespace, c, syncDir) if err != nil { return nil, err } - traits, err := GetTraitsFromCluster(ctx, namespace, c) + traits, err := GetTraitsFromCluster(ctx, namespace, c, syncDir) if err != nil { return nil, err } @@ -23,7 +30,7 @@ func GetTemplatesFromCluster(ctx context.Context, namespace string, c client.Cli return workloads, nil } -func GetWorkloadsFromCluster(ctx context.Context, namespace string, c client.Client) ([]types.Template, error) { +func GetWorkloadsFromCluster(ctx context.Context, namespace string, c client.Client, syncDir string) ([]types.Template, error) { var templates []types.Template var workloadDefs corev1alpha2.WorkloadDefinitionList err := c.List(ctx, &workloadDefs, &client.ListOptions{Namespace: namespace}) @@ -33,22 +40,18 @@ func GetWorkloadsFromCluster(ctx context.Context, namespace string, c client.Cli for _, wd := range workloadDefs.Items { var tmp types.Template - tmp, err := types.ConvertTemplateJson2Object(wd.Spec.Extension) + tmp, err := HandleTemplate(wd.Spec.Extension, wd.Name, syncDir) if err != nil { - fmt.Printf("extract template from workloadDefinition %v err: %v, ignore it\n", wd.Name, err) + fmt.Printf("[WARN]handle template %s: %v\n", wd.Name, err) continue } tmp.Type = types.TypeWorkload - tmp.Name = wd.Name - if tmp.Alias == "" { - tmp.Alias = tmp.Name - } templates = append(templates, tmp) } return templates, nil } -func GetTraitsFromCluster(ctx context.Context, namespace string, c client.Client) ([]types.Template, error) { +func GetTraitsFromCluster(ctx context.Context, namespace string, c client.Client, syncDir string) ([]types.Template, error) { var templates []types.Template var traitDefs corev1alpha2.TraitDefinitionList err := c.List(ctx, &traitDefs, &client.ListOptions{Namespace: namespace}) @@ -58,17 +61,34 @@ func GetTraitsFromCluster(ctx context.Context, namespace string, c client.Client for _, td := range traitDefs.Items { var tmp types.Template - tmp, err := types.ConvertTemplateJson2Object(td.Spec.Extension) + tmp, err := HandleTemplate(td.Spec.Extension, td.Name, syncDir) if err != nil { - fmt.Printf("extract template from workloadDefinition %v err: %v, ignore it\n", td.Name, err) + fmt.Printf("[WARN]handle template %s: %v\n", td.Name, err) continue } tmp.Type = types.TypeTrait - tmp.Name = td.Name - if tmp.Alias == "" { - tmp.Alias = tmp.Name - } templates = append(templates, tmp) } return templates, nil } + +func HandleTemplate(in *runtime.RawExtension, name, syncDir string) (types.Template, error) { + tmp, err := types.ConvertTemplateJson2Object(in) + if err != nil { + return types.Template{}, err + } + if tmp.Template == "" { + return types.Template{}, errors.New("template not exist in definition") + } + filePath := filepath.Join(syncDir, name+".cue") + err = ioutil.WriteFile(filePath, []byte(tmp.Template), 0644) + if err != nil { + return types.Template{}, err + } + tmp.DefinitionPath = filePath + tmp.Parameters, tmp.Name, err = cue.GetParameters(filePath) + if err != nil { + return types.Template{}, err + } + return tmp, nil +} diff --git a/pkg/plugins/cluster_test.go b/pkg/plugins/cluster_test.go index c8865c6db..4225c1b9e 100644 --- a/pkg/plugins/cluster_test.go +++ b/pkg/plugins/cluster_test.go @@ -5,6 +5,8 @@ import ( "fmt" "io/ioutil" + "cuelang.org/go/cue" + "github.com/cloud-native-application/rudrx/api/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,36 +26,45 @@ import ( var _ = Describe("DefinitionFiles", func() { ctx := context.Background() route := types.Template{ - Name: "routes.extend.oam.dev", - Type: types.TypeTrait, - Alias: "route", - Object: map[string]interface{}{ - "apiVersion": "extend.oam.dev/v1alpha2", - "kind": "Route", - }, + Name: "route", + Type: types.TypeTrait, Parameters: []types.Parameter{ { - Name: "domain", - Short: "d", - Required: true, - FieldPaths: []string{"spec.domain"}, + Name: "domain", + Required: true, + Default: "", + Type: cue.StringKind, }, }, } deployment := types.Template{ - Name: "deployments.testapps", - Type: types.TypeWorkload, - Alias: "deployment", - Object: map[string]interface{}{ - "apiVersion": "core.oam.dev/v1alpha2", - "kind": "Deployment", - }, + Name: "deployment", + Type: types.TypeWorkload, Parameters: []types.Parameter{ { - Name: "image", - Short: "i", - Required: true, - FieldPaths: []string{"spec.containers[0].image"}, + Name: "name", + Required: true, + Type: cue.StringKind, + Default: "", + }, + { + Type: cue.ListKind, + Name: "env", + }, + { + Name: "image", + Type: cue.StringKind, + Default: "", + Short: "i", + Required: true, + Usage: "specify app image", + }, + { + Name: "port", + Type: cue.IntKind, + Short: "p", + Default: int64(8080), + Usage: "specify port for container", }, }, } @@ -81,29 +92,38 @@ var _ = Describe("DefinitionFiles", func() { }) - // Notice!! Definition Object is Cluster Scope object + // Notice!! DefinitionPath Object is Cluster Scope object // which means objects created in other DefinitionNamespace will also affect here. It("gettrait", func() { - traitDefs, err := GetTraitsFromCluster(context.Background(), DefinitionNamespace, k8sClient) + traitDefs, err := GetTraitsFromCluster(context.Background(), DefinitionNamespace, k8sClient, definitionDir) Expect(err).Should(BeNil()) logf.Log.Info(fmt.Sprintf("Getting trait definitions %v", traitDefs)) - + for i := range traitDefs { + traitDefs[i].Template = "" + traitDefs[i].DefinitionPath = "" + } Expect(traitDefs).Should(Equal([]types.Template{route})) }) - // Notice!! Definition Object is Cluster Scope object + // Notice!! DefinitionPath Object is Cluster Scope object // which means objects created in other DefinitionNamespace will also affect here. It("getworkload", func() { - workloadDefs, err := GetWorkloadsFromCluster(context.Background(), DefinitionNamespace, k8sClient) + workloadDefs, err := GetWorkloadsFromCluster(context.Background(), DefinitionNamespace, k8sClient, definitionDir) Expect(err).Should(BeNil()) logf.Log.Info(fmt.Sprintf("Getting workload definitions %v", workloadDefs)) - + for i := range workloadDefs { + workloadDefs[i].Template = "" + workloadDefs[i].DefinitionPath = "" + } Expect(workloadDefs).Should(Equal([]types.Template{deployment})) }) It("getall", func() { - alldef, err := GetTemplatesFromCluster(context.Background(), DefinitionNamespace, k8sClient) + alldef, err := GetTemplatesFromCluster(context.Background(), DefinitionNamespace, k8sClient, definitionDir) Expect(err).Should(BeNil()) logf.Log.Info(fmt.Sprintf("Getting all definitions %v", alldef)) - + for i := range alldef { + alldef[i].Template = "" + alldef[i].DefinitionPath = "" + } Expect(alldef).Should(Equal([]types.Template{deployment, route})) }) }) diff --git a/pkg/plugins/local_test.go b/pkg/plugins/local_test.go index d4e9373b9..2a7151b36 100644 --- a/pkg/plugins/local_test.go +++ b/pkg/plugins/local_test.go @@ -11,53 +11,35 @@ import ( func TestLocalSink(t *testing.T) { deployment := types.Template{ - Name: "deployment", - Type: types.TypeWorkload, - Alias: "deployment", - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - }, + Name: "deployment", + Type: types.TypeWorkload, Parameters: []types.Parameter{ { - Name: "image", - Short: "i", - Required: true, - FieldPaths: []string{"spec.containers[0].image"}, + Name: "image", + Short: "i", + Required: true, }, }, } statefulset := types.Template{ - Name: "statefulset", - Type: types.TypeWorkload, - Alias: "stateful", - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Statefulset", - }, + Name: "statefulset", + Type: types.TypeWorkload, Parameters: []types.Parameter{ { - Name: "image", - Short: "i", - Required: true, - FieldPaths: []string{"spec.containers[0].image"}, + Name: "image", + Short: "i", + Required: true, }, }, } route := types.Template{ - Name: "route", - Type: types.TypeTrait, - Alias: "route", - Object: map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Route", - }, + Name: "route", + Type: types.TypeTrait, Parameters: []types.Parameter{ { - Name: "domain", - Short: "d", - Required: true, - FieldPaths: []string{"spec.domain"}, + Name: "domain", + Short: "d", + Required: true, }, }, } diff --git a/pkg/plugins/suit_test.go b/pkg/plugins/suit_test.go index 6943fdb16..503ad9e33 100644 --- a/pkg/plugins/suit_test.go +++ b/pkg/plugins/suit_test.go @@ -2,9 +2,12 @@ package plugins import ( "context" + "os" "path/filepath" "testing" + "github.com/cloud-native-application/rudrx/pkg/utils/system" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -32,6 +35,7 @@ import ( var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment +var definitionDir string func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -108,6 +112,9 @@ var _ = BeforeSuite(func(done Done) { }, }, } + definitionDir, err = system.GetDefinitionDir() + Expect(err).Should(BeNil()) + os.MkdirAll(definitionDir, 0755) Expect(k8sClient.Create(context.Background(), &crd)).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) close(done) }, 60) diff --git a/pkg/plugins/testdata/traitDef.yaml b/pkg/plugins/testdata/traitDef.yaml index febb7f65c..b7ee6678d 100644 --- a/pkg/plugins/testdata/traitDef.yaml +++ b/pkg/plugins/testdata/traitDef.yaml @@ -6,13 +6,15 @@ spec: definitionRef: name: routes.extend.oam.dev extension: - alias: route - object: - apiVersion: extend.oam.dev/v1alpha2 - kind: Route - parameters: - - name: domain - required: true - short: d - fieldPaths: - - "spec.domain" + template: | + #Template: { + apiVersion: "apps/v1" + kind: "Route" + spec: { + domain: route.domain + } + } + + route: { + domain: string + } diff --git a/pkg/plugins/testdata/workloadDef.yaml b/pkg/plugins/testdata/workloadDef.yaml index 839025546..d706348b6 100644 --- a/pkg/plugins/testdata/workloadDef.yaml +++ b/pkg/plugins/testdata/workloadDef.yaml @@ -6,13 +6,35 @@ spec: definitionRef: name: deployments.testapps extension: - alias: deployment - object: - apiVersion: core.oam.dev/v1alpha2 - kind: Deployment - parameters: - - name: image - short: i - required: true - fieldPaths: - - "spec.containers[0].image" \ No newline at end of file + template: | + #Template: { + apiVersion: "apps/v1" + kind: "Deployment" + metadata: name: deployment.name + spec: { + containers: [{ + image: deployment.image + name: deployment.name + env: deployment.env + ports: [{ + containerPort: deployment.port + protocol: "TCP" + name: "default" + }] + }] + } + } + + deployment: { + name: string + // +usage=specify app image + // +short=i + image: string + // +usage=specify port for container + // +short=p + port: *8080 | int + env: [...{ + name: string + value: string + }] + } \ No newline at end of file diff --git a/pkg/utils/system/system.go b/pkg/utils/system/system.go new file mode 100644 index 000000000..7b4b7a7c4 --- /dev/null +++ b/pkg/utils/system/system.go @@ -0,0 +1,90 @@ +package system + +import ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "github.com/cloud-native-application/rudrx/api/types" +) + +const rudrHome = ".rudr" + +func GetRudrHomeDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, rudrHome), nil +} + +func GetApplicationDir() (string, error) { + home, err := GetRudrHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, "applications"), nil +} + +func GetDefinitionDir() (string, error) { + home, err := GetRudrHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, "definitions"), nil +} + +func GetEnvDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, rudrHome, "envs"), nil +} + +func GetCurrentEnvPath() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, rudrHome, "curenv"), nil +} + +func InitDefinitionDir() error { + dir, err := GetDefinitionDir() + if err != nil { + return err + } + return os.MkdirAll(dir, 0755) +} + +func InitApplicationDir() error { + dir, err := GetApplicationDir() + if err != nil { + return err + } + return os.MkdirAll(dir, 0755) +} + +func InitDefaultEnv() error { + envDir, err := GetEnvDir() + if err != nil { + return err + } + if err = os.MkdirAll(envDir, 0755); err != nil { + return err + } + data, _ := json.Marshal(&types.EnvMeta{Namespace: types.DefaultEnvName}) + if err = ioutil.WriteFile(filepath.Join(envDir, types.DefaultEnvName), data, 0644); err != nil { + return err + } + curEnvPath, err := GetCurrentEnvPath() + if err != nil { + return err + } + if err = ioutil.WriteFile(curEnvPath, []byte(types.DefaultEnvName), 0644); err != nil { + return err + } + return nil +}