implement appfile oriented OAM Application run

This commit is contained in:
天元
2020-08-12 21:23:14 +08:00
parent 30516c98b5
commit 9ea02adf2f
22 changed files with 599 additions and 335 deletions

View File

@@ -32,6 +32,11 @@ type Source struct {
ChartName string `json:"chartName,omitempty"`
}
type CrdInfo struct {
ApiVersion string `json:"apiVersion"`
Kind string `json:"kind"`
}
// Capability defines the content of a capability
type Capability struct {
Name string `json:"name"`
@@ -47,6 +52,7 @@ type Capability struct {
// Plugin Source
Source *Source `json:"source,omitempty"`
Install *Installation `json:"install,omitempty"`
CrdInfo *CrdInfo `json:"crdInfo,omitempty"`
}
type Chart struct {

View File

@@ -1,11 +1,5 @@
package types
import (
"errors"
"fmt"
"sort"
)
const (
DefaultOAMNS = "oam-system"
DefaultOAMReleaseName = "core-runtime"
@@ -14,116 +8,22 @@ const (
DefaultOAMRepoUrl = "https://charts.crossplane.io/master"
DefaultOAMVersion = ">0.0.0-0"
DefaultEnvName = "default"
DefaultEnvName = "default"
DefaultAppNamespace = "default"
)
const (
Traits = "traits"
Scopes = "scopes"
AnnApiVersion = "oam.appengine.info/apiVersion"
AnnKind = "oam.appengine.info/kind"
// ComponentWorkloadDefLabel indicate which workloaddefinition generate from
ComponentWorkloadDefLabel = "vela.oam.dev/workloadDef"
TraitDefLabel = "vela.oam.dev/traitDef"
)
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"`
Name string `json:"name"`
}
const (

View File

@@ -1 +1 @@
# RudrX Dashboard
# Vela Dashboard

269
pkg/application/app.go Normal file
View File

@@ -0,0 +1,269 @@
package application
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"k8s.io/apimachinery/pkg/runtime"
mycue "github.com/cloud-native-application/rudrx/pkg/cue"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/cloud-native-application/rudrx/pkg/plugins"
"github.com/cloud-native-application/rudrx/api/types"
"github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
"github.com/cloud-native-application/rudrx/pkg/utils/system"
"github.com/ghodss/yaml"
)
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 Load(envName, appName string) (*Application, error) {
appDir, err := system.GetApplicationDir(envName)
if err != nil {
return nil, fmt.Errorf("get app dir from env %s err %v", envName, err)
}
app := &Application{Name: appName}
data, err := ioutil.ReadFile(filepath.Join(appDir, appName+".yaml"))
if err != nil {
if os.IsNotExist(err) {
return app, nil
}
return nil, err
}
err = yaml.Unmarshal(data, app)
if err != nil {
return nil, err
}
return app, nil
}
func (app *Application) Save(envName, appName string) error {
appDir, err := system.GetApplicationDir(envName)
if err != nil {
return fmt.Errorf("get app dir from env %s err %v", envName, err)
}
out, err := yaml.Marshal(app)
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(appDir, appName+".yaml"), out, 0644)
}
func (app *Application) Validate() 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]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 make(map[string]map[string]interface{}), nil
}
// assume it's valid, use Validate() to check
trs := t.(map[string]interface{})
traits := make(map[string]map[string]interface{})
for k, v := range trs {
traits[k] = v.(map[string]interface{})
}
return traits, nil
}
func (app *Application) GetTraitsByType(componentName, traitType string) (map[string]interface{}, error) {
traits, err := app.GetTraits(componentName)
if err != nil {
return nil, err
}
for t, tt := range traits {
if t == traitType {
return tt, nil
}
}
return make(map[string]interface{}), nil
}
func (app *Application) GetWorkloadObject(componentName string) (*unstructured.Unstructured, error) {
workloadType, workloadData, err := app.GetWorkload(componentName)
if err != nil {
return nil, err
}
return EvalToObject(workloadType, workloadData)
}
func EvalToObject(capName string, data map[string]interface{}) (*unstructured.Unstructured, error) {
cap, err := plugins.LoadCapabilityByName(capName)
if err != nil {
return nil, err
}
jsondata, err := mycue.Eval(cap.DefinitionPath, capName, data)
if err != nil {
return nil, err
}
var obj = make(map[string]interface{})
if err = json.Unmarshal([]byte(jsondata), &obj); err != nil {
return nil, err
}
u := &unstructured.Unstructured{Object: obj}
if cap.CrdInfo != nil {
u.SetAPIVersion(cap.CrdInfo.ApiVersion)
u.SetKind(cap.CrdInfo.Kind)
}
return u, nil
}
func (app *Application) GetComponentTraits(componentName string) ([]v1alpha2.ComponentTrait, error) {
var traits []v1alpha2.ComponentTrait
rawTraits, err := app.GetTraits(componentName)
if err != nil {
return nil, err
}
for traitType, traitData := range rawTraits {
obj, err := EvalToObject(traitType, traitData)
if err != nil {
return nil, err
}
//TODO(wonderflow): handle trait data input/output here
obj.SetAnnotations(map[string]string{types.TraitDefLabel: traitType})
traits = append(traits, v1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: obj}})
}
return traits, nil
}
//TODO(wonderflow) add scope support here
func (app *Application) OAM(env *types.EnvMeta) ([]v1alpha2.Component, v1alpha2.ApplicationConfiguration, error) {
var appConfig v1alpha2.ApplicationConfiguration
if err := app.Validate(); err != nil {
return nil, appConfig, err
}
appConfig.Name = app.Name
appConfig.Namespace = env.Namespace
var components []v1alpha2.Component
for name := range app.Components {
// fulfill component
var component v1alpha2.Component
component.Name = name
component.Namespace = env.Namespace
obj, err := app.GetWorkloadObject(name)
if err != nil {
return nil, v1alpha2.ApplicationConfiguration{}, err
}
labels := component.Labels
if labels == nil {
labels = map[string]string{types.ComponentWorkloadDefLabel: name}
} else {
labels[types.ComponentWorkloadDefLabel] = name
}
component.Labels = labels
component.Spec.Workload.Object = obj
components = append(components, component)
var appConfigComp v1alpha2.ApplicationConfigurationComponent
appConfigComp.ComponentName = name
//TODO(wonderflow): handle component data input/output here
compTraits, err := app.GetComponentTraits(name)
if err != nil {
return nil, v1alpha2.ApplicationConfiguration{}, err
}
appConfigComp.Traits = compTraits
appConfig.Spec.Components = append(appConfig.Spec.Components, appConfigComp)
}
return components, appConfig, nil
}

View File

@@ -1,4 +1,4 @@
package types
package application
import (
"errors"
@@ -73,7 +73,7 @@ components:
WantWorkload string
ExpWorklaod map[string]interface{}
ExpWorkloadType string
ExpTraits map[string]interface{}
ExpTraits map[string]map[string]interface{}
}{
"normal case backend": {
raw: yaml1,
@@ -84,7 +84,7 @@ components:
"image": "back:v1",
},
ExpWorkloadType: "cloneset",
ExpTraits: map[string]interface{}{},
ExpTraits: map[string]map[string]interface{}{},
},
"normal case frontend": {
raw: yaml1,
@@ -94,18 +94,18 @@ components:
ExpWorklaod: map[string]interface{}{
"image": "inanimate/echo-server",
"env": map[string]interface{}{
"PORT": 8080,
"PORT": float64(8080),
},
},
ExpWorkloadType: "deployment",
ExpTraits: map[string]interface{}{
"autoscaling": map[string]interface{}{
"max": 10,
"min": 1,
ExpTraits: map[string]map[string]interface{}{
"autoscaling": {
"max": float64(10),
"min": float64(1),
},
"rollout": map[string]interface{}{
"rollout": {
"strategy": "canary",
"step": 5,
"step": float64(5),
},
},
},
@@ -144,7 +144,7 @@ components:
var app Application
err := yaml.Unmarshal([]byte(c.raw), &app)
assert.NoError(t, err, caseName)
err = app.Valid()
err = app.Validate()
if c.InValid {
assert.Equal(t, c.InvalidReason, err)
continue
@@ -157,6 +157,6 @@ components:
assert.Equal(t, c.ExpWorkloadType, workloadType, caseName)
traits, err := app.GetTraits(c.WantWorkload)
assert.NoError(t, err, caseName)
assert.Equal(t, c.ExpTraits, traits)
assert.Equal(t, c.ExpTraits, traits, caseName)
}
}

69
pkg/application/modify.go Normal file
View File

@@ -0,0 +1,69 @@
package application
import (
"errors"
"strings"
)
func (app *Application) SetWorkload(workloadName, workloadType string, workloadData map[string]interface{}) error {
if app == nil {
return errors.New("app is nil pointer")
}
if workloadData == nil {
workloadData = make(map[string]interface{})
}
workloadData["name"] = strings.ToLower(workloadName)
if app.Components == nil {
app.Components = make(map[string]map[string]interface{})
}
app.Components[workloadName] = map[string]interface{}{
workloadType: workloadData,
}
return app.Validate()
}
func (app *Application) SetTrait(workloadName, traitType string, traitData map[string]interface{}) error {
if app == nil {
return errors.New("app is nil pointer")
}
if traitData == nil {
traitData = make(map[string]interface{})
}
traitData["name"] = strings.ToLower(traitType)
if app.Components == nil {
app.Components = make(map[string]map[string]interface{})
}
comp := app.Components[workloadName]
if comp == nil {
comp = make(map[string]interface{})
}
traits, err := app.GetTraits(workloadName)
if err != nil {
return err
}
traits[traitType] = traitData
comp[Traits] = traits
app.Components[workloadName] = comp
return app.Validate()
}
func (app *Application) RemoveTrait(workloadName, traitType string) error {
if app == nil {
return errors.New("app is nil pointer")
}
if app.Components == nil {
app.Components = make(map[string]map[string]interface{})
}
comp := app.Components[workloadName]
if comp == nil {
comp = make(map[string]interface{})
}
traits, err := app.GetTraits(workloadName)
if err != nil {
return err
}
delete(traits, traitType)
comp[Traits] = traits
app.Components[workloadName] = comp
return app.Validate()
}

58
pkg/application/run.go Normal file
View File

@@ -0,0 +1,58 @@
package application
import (
"context"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
"github.com/cloud-native-application/rudrx/api/types"
ctypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func (app *Application) Run(ctx context.Context, client client.Client, env *types.EnvMeta) error {
components, appconfig, err := app.OAM(env)
if err != nil {
return err
}
for _, cmp := range components {
if err = CreateOrUpdateComponent(ctx, client, cmp); err != nil {
return err
}
}
return CreateOrUpdateAppConfig(ctx, client, appconfig)
}
func CreateOrUpdateComponent(ctx context.Context, client client.Client, comp v1alpha2.Component) error {
var getc v1alpha2.Component
key := ctypes.NamespacedName{Name: comp.Name, Namespace: comp.Namespace}
var exist = true
if err := client.Get(ctx, key, &getc); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
exist = false
}
if !exist {
return client.Create(ctx, &comp)
}
return client.Update(ctx, &comp)
}
func CreateOrUpdateAppConfig(ctx context.Context, client client.Client, appConfig v1alpha2.ApplicationConfiguration) error {
var geta v1alpha2.ApplicationConfiguration
key := ctypes.NamespacedName{Name: appConfig.Name, Namespace: appConfig.Namespace}
var exist = true
if err := client.Get(ctx, key, &geta); err != nil {
if !apierrors.IsAlreadyExists(err) {
return err
}
exist = false
}
if !exist {
return client.Create(ctx, &appConfig)
}
return client.Update(ctx, &appConfig)
}

View File

@@ -325,6 +325,12 @@ func InstallCapability(client client.Client, centerName, capabilityName string,
return err
}
}
if apiVerion, kind := cmdutil.GetApiVersionKindFromWorkload(wd); apiVerion != "" && kind != "" {
tp.CrdInfo = &types.CrdInfo{
ApiVersion: apiVerion,
Kind: kind,
}
}
if err = client.Create(context.Background(), &wd); err != nil && !apierrors.IsAlreadyExists(err) {
return err
}
@@ -345,6 +351,12 @@ func InstallCapability(client client.Client, centerName, capabilityName string,
return err
}
}
if apiVerion, kind := cmdutil.GetApiVersionKindFromTrait(td); apiVerion != "" && kind != "" {
tp.CrdInfo = &types.CrdInfo{
ApiVersion: apiVerion,
Kind: kind,
}
}
if err = client.Create(context.Background(), &td); err != nil && !apierrors.IsAlreadyExists(err) {
return err
}

View File

@@ -27,16 +27,15 @@ func newDeleteOptions(ioStreams cmdutil.IOStreams) *deleteOptions {
func newDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "app:delete [APPLICATION_NAME]",
Use: "app:delete <APPLICATION_NAME>",
Aliases: []string{"delete"},
DisableFlagsInUseLine: true,
Short: "Delete OAM Applications",
Long: "Delete OAM Applications",
Annotations: map[string]string{
types.TagCommandType: types.TypeApp,
},
Example: `
vela delete frontend
`}
Example: "vela delete frontend"}
}
// NewDeleteCommand init new command
@@ -64,7 +63,7 @@ func NewDeleteCommand(c types.Args, ioStreams cmdutil.IOStreams, args []string)
func (o *deleteOptions) Complete(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("must specify name for workload")
return errors.New("must specify name for the app")
}
namespace := o.Env.Namespace

View File

@@ -59,7 +59,7 @@ func NewEnvInitCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command
var envArgs types.EnvMeta
ctx := context.Background()
cmd := &cobra.Command{
Use: "env:init",
Use: "env:init <envName>",
DisableFlagsInUseLine: true,
Short: "Create environments",
Long: "Create environment and switch to it",
@@ -149,10 +149,10 @@ func ListEnvs(ctx context.Context, args []string, ioStreams cmdutil.IOStreams) e
curEnv = types.DefaultEnvName
}
for _, f := range files {
if f.IsDir() {
if !f.IsDir() {
continue
}
data, err := ioutil.ReadFile(filepath.Join(envDir, f.Name()))
data, err := ioutil.ReadFile(filepath.Join(envDir, f.Name(), system.EnvConfigName))
if err != nil {
continue
}
@@ -186,7 +186,7 @@ func DeleteEnv(ctx context.Context, args []string, ioStreams cmdutil.IOStreams)
if err != nil {
return err
}
if err = os.Remove(filepath.Join(envdir, envname)); err != nil {
if err = os.RemoveAll(filepath.Join(envdir, envname)); err != nil {
return err
}
ioStreams.Info(envname + " deleted")
@@ -198,6 +198,7 @@ func CreateOrUpdateEnv(ctx context.Context, c client.Client, envArgs *types.EnvM
return fmt.Errorf("you must specify env name for vela env:init command")
}
envname := args[0]
envArgs.Name = envname
data, err := json.Marshal(envArgs)
if err != nil {
return err
@@ -206,7 +207,9 @@ func CreateOrUpdateEnv(ctx context.Context, c client.Client, envArgs *types.EnvM
if err != nil {
return err
}
if err = ioutil.WriteFile(filepath.Join(envdir, envname), data, 0644); err != nil {
subEnvDir := filepath.Join(envdir, envname)
system.StatAndCreate(subEnvDir)
if err = ioutil.WriteFile(filepath.Join(subEnvDir, system.EnvConfigName), data, 0644); err != nil {
return err
}
curEnvPath, err := system.GetCurrentEnvPath()
@@ -271,11 +274,7 @@ func GetEnv() (*types.EnvMeta, error) {
}
func getEnvByName(name string) (*types.EnvMeta, error) {
envdir, err := system.GetEnvDir()
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(filepath.Join(envdir, name))
data, err := ioutil.ReadFile(filepath.Join(system.GetEnvDirByName(name), system.EnvConfigName))
if err != nil {
return nil, err
}

View File

@@ -37,11 +37,13 @@ func TestENV(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, &types.EnvMeta{
Namespace: "default",
Name: "default",
}, gotEnv)
ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
exp := &types.EnvMeta{
Namespace: "test1",
Name: "default",
}
client := test.NewMockClient()
// Create env1
@@ -81,6 +83,7 @@ func TestENV(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, &types.EnvMeta{
Namespace: "default",
Name: "default",
}, gotEnv)
// delete env

View File

@@ -4,9 +4,10 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
corev1alpha2 "github.com/crossplane/oam-kubernetes-runtime/apis/core/v1alpha2"
"github.com/gosuri/uitable"
"strings"
"github.com/cloud-native-application/rudrx/api/types"
cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util"
@@ -26,6 +27,7 @@ func NewAppsCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
ctx := context.Background()
cmd := &cobra.Command{
Use: "app:ls",
Aliases: []string{"ls"},
DisableFlagsInUseLine: true,
Short: "List applications",
Long: "List applications with workloads, traits, status and created time",

View File

@@ -18,14 +18,15 @@ import (
func NewAppShowCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
ctx := context.Background()
cmd := &cobra.Command{
Use: "app:show",
Use: "app:show <APPLICATION-NAME>",
Aliases: []string{"show"},
Short: "get detail spec of your app",
Long: "get detail spec of your app, including its workload and trait",
Example: `vela app:show <APPLICATION-NAME>`,
RunE: func(cmd *cobra.Command, args []string) error {
argsLength := len(args)
if argsLength == 0 {
ioStreams.Errorf("Hint: please specify an application")
ioStreams.Errorf("Hint: please specify the application name")
os.Exit(1)
}
appName := args[0]
@@ -79,7 +80,7 @@ func showApplication(ctx context.Context, c client.Client, cmd *cobra.Command, e
}
if component.Labels == nil {
return fmt.Errorf("Can't get workloadDef, please check component %s label \"%s\" is correct.",
componentName, ComponentWorkloadDefLabel)
componentName, types.ComponentWorkloadDefLabel)
}
traitDefinitions := cmdutil.ListTraitDefinitionsByApplicationConfiguration(application)

View File

@@ -24,7 +24,8 @@ type ApplicationStatusMeta struct {
func NewAppStatusCommand(c types.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
ctx := context.Background()
cmd := &cobra.Command{
Use: "app:status",
Use: "app:status <APPLICATION-NAME>",
Aliases: []string{"status"},
Short: "get status of an application",
Long: "get status of an application, including its workload and trait",
Example: `vela app:status <APPLICATION-NAME>`,

View File

@@ -2,38 +2,33 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
mycue "github.com/cloud-native-application/rudrx/pkg/cue"
"github.com/cloud-native-application/rudrx/pkg/application"
"cuelang.org/go/cue"
"k8s.io/apimachinery/pkg/runtime"
"github.com/cloud-native-application/rudrx/pkg/plugins"
"github.com/cloud-native-application/rudrx/api/types"
cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util"
"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"
)
type commandOptions struct {
Template types.Capability
Component corev1alpha2.Component
AppConfig corev1alpha2.ApplicationConfiguration
Client client.Client
TraitAlias string
Detach bool
Env *types.EnvMeta
workloadName string
appName string
staging bool
app *application.Application
cmdutil.IOStreams
}
@@ -56,14 +51,14 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.
DisableFlagsInUseLine: true,
Short: "Attach " + name + " trait to an app",
Long: "Attach " + name + " trait to an app",
Example: `vela scale frontend --max=5`,
Example: "vela " + name + " frontend",
RunE: func(cmd *cobra.Command, args []string) error {
newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema})
if err != nil {
return err
}
o.Client = newClient
if err := o.Complete(cmd, args, ctx); err != nil {
if err := o.AddOrUpdateTrait(cmd, args); err != nil {
return err
}
return o.Run(cmd, ctx)
@@ -76,6 +71,8 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.
for _, v := range tmp.Parameters {
types.SetFlagBy(pluginCmd, v)
}
pluginCmd.Flags().StringP(App, "a", "", "create or add into an existing application group")
pluginCmd.Flags().BoolP(Staging, "s", false, "only save changes locally without real update application")
o.Template = tmp
parentCmd.AddCommand(pluginCmd)
@@ -83,35 +80,32 @@ func AddTraitCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.
return nil
}
func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context.Context) error {
argsLength := len(args)
var appName string
c := o.Client
namespace := o.Env.Namespace
if argsLength < 1 {
func (o *commandOptions) Prepare(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return errors.New("please specify the name of the app")
}
o.workloadName = args[0]
if app := cmd.Flag(App).Value.String(); app != "" {
o.appName = app
} else {
o.appName = o.workloadName
}
return nil
}
// Get AppConfig
// TODO(wonderflow): appName is Component Name here, check if it's has appset with a different name
appName = args[0]
if err := c.Get(ctx, client.ObjectKey{Namespace: o.Env.Namespace, Name: appName}, &o.AppConfig); err != nil {
func (o *commandOptions) AddOrUpdateTrait(cmd *cobra.Command, args []string) error {
if err := o.Prepare(cmd, args); err != nil {
return err
}
// Get component
var component corev1alpha2.Component
if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: appName}, &component); err != nil {
app, err := application.Load(o.Env.Name, o.appName)
if err != nil {
return err
}
var traitType = o.Template.Name
traitData, err := app.GetTraitsByType(o.workloadName, traitType)
if err != nil {
return err
}
var traitData = make(map[string]interface{})
var tp = o.Template.Name
for _, v := range o.Template.Parameters {
flagSet := cmd.Flag(v.Name)
switch v.Type {
@@ -128,43 +122,11 @@ func (o *commandOptions) Complete(cmd *cobra.Command, args []string, ctx context
traitData[v.Name] = d
}
}
jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, traitData)
if err != nil {
if err = app.SetTrait(o.workloadName, traitType, traitData); 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.Name)))
curObj := &unstructured.Unstructured{Object: pvd.UnstructuredContent()}
var updated bool
for ic, c := range o.AppConfig.Spec.Components {
if c.ComponentName != appName {
continue
}
for it, t := range c.Traits {
g, v, k := GetGVKFromRawExtension(t.Trait)
// 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
c.Traits[it] = corev1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: curObj}}
break
}
}
if !updated {
c.Traits = append(c.Traits, corev1alpha2.ComponentTrait{Trait: runtime.RawExtension{Object: curObj}})
}
o.AppConfig.Spec.Components[ic] = c
break
}
return nil
o.app = app
return app.Save(o.Env.Name, o.appName)
}
func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.IOStreams) error {
@@ -182,14 +144,14 @@ func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cm
DisableFlagsInUseLine: true,
Short: "Detach " + name + " trait from an app",
Long: "Detach " + name + " trait from an app",
Example: `vela scale:detach frontend`,
Example: "vela " + name + ":detach frontend",
RunE: func(cmd *cobra.Command, args []string) error {
newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema})
if err != nil {
return err
}
o.Client = newClient
if err := o.DetachTrait(cmd, args, ctx); err != nil {
if err := o.DetachTrait(cmd, args); err != nil {
return err
}
return o.Run(cmd, ctx)
@@ -206,48 +168,36 @@ func AddTraitDetachCommands(parentCmd *cobra.Command, c types.Args, ioStreams cm
return nil
}
func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string, ctx context.Context) error {
argsLength := len(args)
if argsLength < 1 {
return errors.New("please specify the name of the app")
}
c := o.Client
namespace := o.Env.Namespace
var appName = args[0]
if err := c.Get(ctx, client.ObjectKey{Namespace: o.Env.Namespace, Name: appName}, &o.AppConfig); err != nil {
func (o *commandOptions) DetachTrait(cmd *cobra.Command, args []string) error {
if err := o.Prepare(cmd, args); err != nil {
return err
}
apiVersion, kind, err := cmdutil.GetTraitApiVersionKind(ctx, c, namespace, o.TraitAlias)
app, err := application.Load(o.Env.Name, o.appName)
if err != nil {
return err
}
for i, com := range o.AppConfig.Spec.Components {
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
var traitType = o.Template.Name
if err = app.RemoveTrait(o.workloadName, traitType); err != nil {
return err
}
return nil
return app.Save(o.Env.Name, o.appName)
}
func (o *commandOptions) Run(cmd *cobra.Command, ctx context.Context) error {
if o.Detach {
o.Info("Detaching trait from app", o.Component.Name)
o.Infof("Detaching %s from app %s\n", o.Template.Name, o.workloadName)
} else {
o.Info("Applying trait for app", o.Component.Name)
o.Infof("Adding %s for app %s \n", o.Template.Name, o.workloadName)
}
c := o.Client
err := c.Update(ctx, &o.AppConfig)
staging, err := strconv.ParseBool(cmd.Flag(Staging).Value.String())
if err != nil {
return err
}
if staging {
o.Info("Staging saved")
return nil
}
err = o.app.Run(ctx, o.Client, o.Env)
if err != nil {
return err
}

View File

@@ -107,11 +107,19 @@ func GetTraitApiVersionKind(ctx context.Context, c client.Client, namespace stri
if err != nil {
return "", "", err
}
apiVersion := t.Annotations["oam.appengine.info/apiVersion"]
kind := t.Annotations["oam.appengine.info/kind"]
apiVersion := t.Annotations[types.AnnApiVersion]
kind := t.Annotations[types.AnnKind]
return apiVersion, kind, nil
}
func GetApiVersionKindFromTrait(td corev1alpha2.TraitDefinition) (string, string) {
return td.Annotations[types.AnnApiVersion], td.Annotations[types.AnnKind]
}
func GetApiVersionKindFromWorkload(td corev1alpha2.WorkloadDefinition) (string, string) {
return td.Annotations[types.AnnApiVersion], td.Annotations[types.AnnKind]
}
func GetWorkloadNameAliasKind(ctx context.Context, c client.Client, namespace string, workloadName string) (string, string, string) {
var name, alias, kind string

View File

@@ -2,52 +2,39 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/ghodss/yaml"
"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/application"
"github.com/cloud-native-application/rudrx/pkg/cmd/util"
"github.com/cloud-native-application/rudrx/pkg/plugins"
"cuelang.org/go/cue"
"github.com/spf13/cobra"
"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 = "vela.oam.dev/workloadDef"
const Staging = "staging"
const App = "app"
type runOptions struct {
Template types.Capability
Env *types.EnvMeta
workloadName string
client client.Client
app *types.Application
cmdutil.IOStreams
app *application.Application
appName string
staging bool
util.IOStreams
}
func newRunOptions(ioStreams cmdutil.IOStreams) *runOptions {
func newRunOptions(ioStreams util.IOStreams) *runOptions {
return &runOptions{IOStreams: ioStreams}
}
func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdutil.IOStreams) error {
func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams util.IOStreams) error {
templates, err := plugins.LoadInstalledCapabilityWithType(types.TypeWorkload)
if err != nil {
return err
@@ -62,7 +49,7 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut
DisableFlagsInUseLine: true,
Short: "Run " + name + " workloads",
Long: "Run " + name + " workloads",
Example: `vela deployment:run frontend -i nginx:latest`,
Example: "vela " + name + ":run frontend",
RunE: func(cmd *cobra.Command, args []string) error {
newClient, err := client.New(c.Config, client.Options{Scheme: c.Schema})
if err != nil {
@@ -82,6 +69,8 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut
for _, v := range tmp.Parameters {
types.SetFlagBy(pluginCmd, v)
}
pluginCmd.Flags().StringP(App, "a", "", "create or add into an existing application group")
pluginCmd.Flags().BoolP(Staging, "s", false, "only save changes locally without real update application")
o.Template = tmp
parentCmd.AddCommand(pluginCmd)
@@ -90,21 +79,25 @@ func AddWorkloadCommands(parentCmd *cobra.Command, c types.Args, ioStreams cmdut
}
func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Context) error {
argsLength := len(args)
if argsLength < 1 {
return errors.New("must specify name for workload")
}
workloadName := args[0]
// TODO(wonderflow): load application from file
var app = &types.Application{Name: workloadName}
o.workloadName = args[0]
if app := cmd.Flag(App).Value.String(); app != "" {
o.appName = app
} else {
o.appName = o.workloadName
}
app, err := application.Load(o.Env.Name, o.appName)
if err != nil {
return err
}
if app.Components == nil {
app.Components = make(map[string]map[string]interface{})
}
tp, workloadData, err := app.GetWorkload(workloadName)
tp, workloadData, err := app.GetWorkload(o.workloadName)
if err != nil {
// Not exist
tp = o.Template.Name
@@ -127,52 +120,25 @@ func (o *runOptions) Complete(cmd *cobra.Command, args []string, ctx context.Con
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 {
if err = app.SetWorkload(o.workloadName, tp, workloadData); err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(appDir, workloadName), out, 0644)
o.app = app
return app.Save(o.Env.Name, o.appName)
}
func (o *runOptions) Run(cmd *cobra.Command) error {
var component corev1alpha2.Component
var appconfig corev1alpha2.ApplicationConfiguration
tp, data, _ := o.app.GetWorkload(o.workloadName)
jsondata, err := mycue.Eval(o.Template.DefinitionPath, tp, data)
staging, err := strconv.ParseBool(cmd.Flag(Staging).Value.String())
if err != nil {
return err
}
var obj = make(map[string]interface{})
if err = json.Unmarshal([]byte(jsondata), &obj); err != nil {
return err
if staging {
o.Info("Staging saved")
return nil
}
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(), &appconfig)
if err != nil {
return fmt.Errorf("create appconfig err %s", err)
o.Infof("Creating App %s\n", o.app.Name)
if err := o.app.Run(context.Background(), o.client, o.Env); err != nil {
return fmt.Errorf("create app err: %s", err)
}
o.Info("SUCCEED")
return nil

View File

@@ -7,6 +7,8 @@ import (
"io/ioutil"
"path/filepath"
cmdutil "github.com/cloud-native-application/rudrx/pkg/cmd/util"
"github.com/cloud-native-application/rudrx/pkg/utils/system"
"k8s.io/apimachinery/pkg/labels"
@@ -47,6 +49,12 @@ func GetWorkloadsFromCluster(ctx context.Context, namespace string, c client.Cli
fmt.Printf("[WARN]handle template %s: %v\n", wd.Name, err)
continue
}
if apiVerion, kind := cmdutil.GetApiVersionKindFromWorkload(wd); apiVerion != "" && kind != "" {
tmp.CrdInfo = &types.CrdInfo{
ApiVersion: apiVerion,
Kind: kind,
}
}
templates = append(templates, tmp)
}
return templates, nil
@@ -66,6 +74,12 @@ func GetTraitsFromCluster(ctx context.Context, namespace string, c client.Client
fmt.Printf("[WARN]handle template %s: %v\n", td.Name, err)
continue
}
if apiVerion, kind := cmdutil.GetApiVersionKindFromTrait(td); apiVerion != "" && kind != "" {
tmp.CrdInfo = &types.CrdInfo{
ApiVersion: apiVerion,
Kind: kind,
}
}
templates = append(templates, tmp)
}
return templates, nil

View File

@@ -13,6 +13,19 @@ import (
"github.com/cloud-native-application/rudrx/pkg/utils/system"
)
func LoadCapabilityByName(name string) (types.Capability, error) {
caps, err := LoadAllInstalledCapability()
if err != nil {
return types.Capability{}, err
}
for _, c := range caps {
if c.Name == name {
return c, nil
}
}
return types.Capability{}, fmt.Errorf("%s not found", name)
}
func LoadAllInstalledCapability() ([]types.Capability, error) {
workloads, err := LoadInstalledCapabilityWithType(types.TypeWorkload)
if err != nil {

View File

@@ -39,14 +39,6 @@ func GetRepoConfig() (string, error) {
return filepath.Join(home, "config.yaml"), nil
}
func GetApplicationDir() (string, error) {
home, err := GetVelaHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, "applications"), nil
}
func GetCapabilityDir() (string, error) {
home, err := GetVelaHomeDir()
if err != nil {
@@ -75,9 +67,6 @@ func InitDirs() error {
if err := InitCapabilityDir(); err != nil {
return err
}
if err := InitApplicationDir(); err != nil {
return err
}
if err := InitCapCenterDir(); err != nil {
return err
}
@@ -100,22 +89,22 @@ func InitCapabilityDir() error {
return StatAndCreate(dir)
}
func InitApplicationDir() error {
dir, err := GetApplicationDir()
if err != nil {
return err
}
return StatAndCreate(dir)
func GetApplicationDir(envName string) (string, error) {
appDir := filepath.Join(GetEnvDirByName(envName), "applications")
return appDir, StatAndCreate(appDir)
}
const EnvConfigName = "config.json"
func InitDefaultEnv() error {
envDir, err := GetEnvDir()
if err != nil {
return err
}
StatAndCreate(envDir)
data, _ := json.Marshal(&types.EnvMeta{Namespace: types.DefaultEnvName})
if err = ioutil.WriteFile(filepath.Join(envDir, types.DefaultEnvName), data, 0644); err != nil {
defaultEnvDir := filepath.Join(envDir, types.DefaultEnvName)
StatAndCreate(defaultEnvDir)
data, _ := json.Marshal(&types.EnvMeta{Namespace: types.DefaultAppNamespace, Name: types.DefaultEnvName})
if err = ioutil.WriteFile(filepath.Join(defaultEnvDir, EnvConfigName), data, 0644); err != nil {
return err
}
curEnvPath, err := GetCurrentEnvPath()
@@ -134,3 +123,8 @@ func StatAndCreate(dir string) error {
}
return nil
}
func GetEnvDirByName(name string) string {
envdir, _ := GetEnvDir()
return filepath.Join(envdir, name)
}