use cue template instead of parameter

This commit is contained in:
天元
2020-08-06 11:58:13 +08:00
parent cb06bb710a
commit ae08333efa
38 changed files with 1191 additions and 560 deletions
+31 -13
View File
@@ -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)
}
}
+117
View File
@@ -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"`
}
+162
View File
@@ -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)
}
}
+10
View File
@@ -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{}),
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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
}]
}
+1
View File
@@ -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
+3 -1
View File
@@ -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 {
+19 -59
View File
@@ -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
}
+8 -4
View File
@@ -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)
+10 -37
View File
@@ -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",
},
},
}
+1 -1
View File
@@ -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
}
+56 -35
View File
@@ -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
}
+1 -1
View File
@@ -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")
+9 -23
View File
@@ -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
}
}
+87 -49
View File
@@ -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)
}
+1 -1
View File
@@ -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")
+169
View File
@@ -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
}
+62
View File
@@ -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()))
}
-101
View File
@@ -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
}
-23
View File
@@ -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)
}
-12
View File
@@ -1,12 +0,0 @@
deployment: {
name: "myapp"
port: 8080
image: "nginx:v1"
env: [{
name: "MYDB"
value: "true"
}]
}
route: {
domain: "www.example.com"
}
+31
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
#Template: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: manualscaler.replica
}
}
manualscaler: {
//+short=r
replica: *2 | int
}
+15
View File
@@ -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
}
+11
View File
@@ -0,0 +1,11 @@
#Template: {
apiVersion: "apps/v1"
kind: "Route"
spec: {
domain: route.domain
}
}
route: {
domain: string
}
+26
View File
@@ -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
}
@@ -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
+19
View File
@@ -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
}
+37 -17
View File
@@ -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
}
+50 -30
View File
@@ -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}))
})
})
+15 -33
View File
@@ -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,
},
},
}
+7
View File
@@ -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)
+12 -10
View File
@@ -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
}
+32 -10
View File
@@ -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"
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
}]
}
+90
View File
@@ -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
}