Feat: support vela up from remote url file (#3075)

Signed-off-by: Jianbo Sun <jianbo.sjb@alibaba-inc.com>
This commit is contained in:
Jianbo Sun
2022-01-11 17:28:45 +08:00
committed by GitHub
parent d64c78db47
commit 19a542ff11
6 changed files with 105 additions and 143 deletions

View File

@@ -24,15 +24,11 @@ import (
"strings"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/builtin"
"github.com/oam-dev/kubevela/pkg/oam"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/appfile/template"
)
@@ -103,20 +99,17 @@ func LoadFromFile(filename string) (*AppFile, error) {
if err != nil {
return nil, err
}
return LoadFromBytes(b)
}
// LoadFromBytes will load AppFile from bytes
func LoadFromBytes(b []byte) (*AppFile, error) {
af := NewAppFile()
// Add JSON format appfile support
ext := filepath.Ext(filename)
switch ext {
case ".yaml", ".yml":
err = yaml.Unmarshal(b, af)
case ".json":
var err error
if json.Valid(b) {
af, err = JSONToYaml(b, af)
default:
if json.Valid(b) {
af, err = JSONToYaml(b, af)
} else {
err = yaml.Unmarshal(b, af)
}
} else {
err = yaml.Unmarshal(b, af)
}
if err != nil {
return nil, err
@@ -140,16 +133,15 @@ func (app *AppFile) ExecuteAppfileTasks(io cmdutil.IOStreams) error {
return nil
}
// BuildOAMApplication renders Appfile into Application, Scopes and other K8s Resources.
func (app *AppFile) BuildOAMApplication(namespace string, io cmdutil.IOStreams, tm template.Manager, silence bool) (*v1beta1.Application, []oam.Object, error) {
// ConvertToApplication renders Appfile into Application, Scopes and other K8s Resources.
func (app *AppFile) ConvertToApplication(namespace string, io cmdutil.IOStreams, tm template.Manager, silence bool) (*v1beta1.Application, error) {
if err := app.ExecuteAppfileTasks(io); err != nil {
if strings.Contains(err.Error(), "'image' : not found") {
return nil, nil, ErrImageNotDefined
return nil, ErrImageNotDefined
}
return nil, nil, err
return nil, err
}
// auxiliaryObjects currently include OAM Scope Custom Resources and ConfigMaps
var auxiliaryObjects []oam.Object
servApp := new(v1beta1.Application)
servApp.SetNamespace(namespace)
servApp.SetName(app.Name)
@@ -160,33 +152,10 @@ func (app *AppFile) BuildOAMApplication(namespace string, io cmdutil.IOStreams,
for serviceName, svc := range app.GetServices() {
comp, err := svc.RenderServiceToApplicationComponent(tm, serviceName)
if err != nil {
return nil, nil, err
return nil, err
}
servApp.Spec.Components = append(servApp.Spec.Components, comp)
}
servApp.SetGroupVersionKind(v1beta1.SchemeGroupVersion.WithKind("Application"))
auxiliaryObjects = append(auxiliaryObjects, addDefaultHealthScopeToApplication(servApp))
return servApp, auxiliaryObjects, nil
}
func addDefaultHealthScopeToApplication(app *v1beta1.Application) *v1alpha2.HealthScope {
health := &v1alpha2.HealthScope{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha2.HealthScopeGroupVersionKind.GroupVersion().String(),
Kind: v1alpha2.HealthScopeKind,
},
}
health.Name = FormatDefaultHealthScopeName(app.Name)
health.Namespace = app.Namespace
health.Spec.WorkloadReferences = make([]corev1.ObjectReference, 0)
for i := range app.Spec.Components {
// FIXME(wonderflow): the hardcode health scope should be fixed.
app.Spec.Components[i].Scopes = map[string]string{DefaultHealthScopeKey: health.Name}
}
return health
}
// FormatDefaultHealthScopeName will create a default health scope name.
func FormatDefaultHealthScopeName(appName string) string {
return appName + "-default-health"
return servApp, nil
}

View File

@@ -21,13 +21,11 @@ import (
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
@@ -80,7 +78,6 @@ func TestBuildOAMApplication2(t *testing.T) {
Properties: &runtime.RawExtension{
Raw: []byte("{\"image\":\"busybox\"}"),
},
Scopes: map[string]string{"healthscopes.core.oam.dev": "test-default-health"},
},
},
},
@@ -114,7 +111,6 @@ func TestBuildOAMApplication2(t *testing.T) {
Properties: &runtime.RawExtension{
Raw: []byte("{\"image\":\"busybox\"}"),
},
Scopes: map[string]string{"healthscopes.core.oam.dev": "test-default-health"},
Traits: []common.ApplicationTrait{
{
Type: "scaler",
@@ -132,7 +128,7 @@ func TestBuildOAMApplication2(t *testing.T) {
for _, tcase := range testCases {
tcase.expectApp.Namespace = expectNs
o, _, err := tcase.appFile.BuildOAMApplication(expectNs, cmdutil.IOStreams{
o, err := tcase.appFile.ConvertToApplication(expectNs, cmdutil.IOStreams{
In: os.Stdin,
Out: os.Stdout,
}, tm, false)
@@ -258,9 +254,8 @@ outputs: ingress: {
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Type: "webservice",
Name: "express-server",
Scopes: map[string]string{"healthscopes.core.oam.dev": "myapp-default-health"},
Type: "webservice",
Name: "express-server",
Properties: &runtime.RawExtension{
Raw: []byte(`{"image": "oamdev/testapp:v1", "cmd": ["node", "server.js"]}`),
},
@@ -282,7 +277,6 @@ outputs: ingress: {
Raw: []byte(`{"image":"bitnami/mongodb:3.6.20","cmd": ["mongodb"]}`),
},
Traits: []common.ApplicationTrait{},
Scopes: map[string]string{"healthscopes.core.oam.dev": "myapp-default-health"},
})
ac3 := ac1.DeepCopy()
@@ -291,15 +285,6 @@ outputs: ingress: {
// TODO application 那边补测试:
// 2. 1对多的情况多对1 的情况
health := &v1alpha2.HealthScope{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha2.HealthScopeGroupVersionKind.GroupVersion().String(),
Kind: v1alpha2.HealthScopeKind,
},
}
health.Name = FormatDefaultHealthScopeName("myapp")
health.Namespace = "default"
health.Spec.WorkloadReferences = make([]corev1.ObjectReference, 0)
type args struct {
appfileData string
workloadTemplates map[string]string
@@ -326,7 +311,7 @@ outputs: ingress: {
},
want: want{
app: ac1,
objs: []oam.Object{health},
objs: []oam.Object{},
},
},
"two services should generate two components and one appconfig": {
@@ -342,7 +327,7 @@ outputs: ingress: {
},
want: want{
app: ac2,
objs: []oam.Object{health},
objs: []oam.Object{},
},
},
"no image should fail": {
@@ -378,7 +363,7 @@ outputs: ingress: {
}
}
application, objects, err := app.BuildOAMApplication("default", io, tm, false)
application, err := app.ConvertToApplication("default", io, tm, false)
if c.want.err != nil {
assert.Equal(t, c.want.err, err)
return
@@ -412,9 +397,6 @@ outputs: ingress: {
}
assert.Equal(t, true, found, "no component found for %s", comp.Name)
}
for idx, v := range objects {
assert.Equal(t, c.want.objs[idx], v)
}
})
}

View File

@@ -17,9 +17,6 @@ limitations under the License.
package cli
import (
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
@@ -38,7 +35,7 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
Use: "up",
DisableFlagsInUseLine: true,
Short: "Apply an appfile or application from file",
Long: "Create or update vela application from file, both appfile or application object format are supported.",
Long: "Create or update vela application from file or URL, both appfile or application object format are supported.",
Annotations: map[string]string{
types.TagCommandOrder: order,
types.TagCommandType: types.TypeStart,
@@ -52,21 +49,13 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
if err != nil {
return err
}
fileContent, err := os.ReadFile(filepath.Clean(*appFilePath))
body, err := common.ReadRemoteOrLocalPath(*appFilePath)
if err != nil {
return err
}
var app corev1beta1.Application
err = yaml.Unmarshal(fileContent, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
if app.APIVersion != "" && app.Kind != "" {
err = common.ApplyApplication(app, ioStream, kubecli)
if err != nil {
return err
}
} else {
if common.IsAppfile(body) {
o := &common.AppfileOptions{
Kubecli: kubecli,
IO: ioStream,
@@ -74,11 +63,20 @@ func NewUpCommand(c common2.Args, order string, ioStream cmdutil.IOStreams) *cob
}
return o.Run(*appFilePath, o.Namespace, c)
}
var app corev1beta1.Application
err = yaml.Unmarshal(body, &app)
if err != nil {
return errors.Wrap(err, "File format is illegal, only support vela appfile format or OAM Application object yaml")
}
err = common.ApplyApplication(app, ioStream, kubecli)
if err != nil {
return err
}
return nil
},
}
cmd.SetOut(ioStream.Out)
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application")
cmd.Flags().StringVarP(appFilePath, "file", "f", "", "specify file path for appfile or application, it could be a remote url.")
addNamespaceAndEnvArg(cmd)
return cmd

View File

@@ -18,26 +18,19 @@ package cli
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/common"
)
func TestUp(t *testing.T) {
ioStream := util.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
namespace := "up-ns"
o := common.AppfileOptions{
IO: ioStream,
Namespace: namespace,
}
app := &v1beta1.Application{}
app.Name = "app-up"
msg := o.Info(app)
msg := common.Info(app)
assert.Contains(t, msg, "App has been deployed")
assert.Contains(t, msg, fmt.Sprintf("App status: vela status %s", app.Name))
}

View File

@@ -26,9 +26,8 @@ import (
"strings"
"time"
"github.com/fatih/color"
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/fatih/color"
"github.com/gosuri/uilive"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -240,33 +239,71 @@ func (o *DeleteOptions) DeleteComponent(io cmdutil.IOStreams) error {
return nil
}
func saveAndLoadRemoteAppfile(url string) (*api.AppFile, error) {
body, err := common.HTTPGet(context.Background(), url)
// LoadAppFile will load vela appfile from remote URL or local file system.
func LoadAppFile(pathOrURL string) (*api.AppFile, error) {
body, err := ReadRemoteOrLocalPath(pathOrURL)
if err != nil {
return nil, err
}
af := api.NewAppFile()
ext := filepath.Ext(url)
dest := "Appfile"
switch ext {
case ".json":
dest = "vela.json"
af, err = api.JSONToYaml(body, af)
case ".yaml", ".yml":
dest = "vela.yaml"
err = yaml.Unmarshal(body, af)
default:
if j.Valid(body) {
af, err = api.JSONToYaml(body, af)
} else {
err = yaml.Unmarshal(body, af)
return api.LoadFromBytes(body)
}
// ReadRemoteOrLocalPath will read a path remote or locally
func ReadRemoteOrLocalPath(pathOrURL string) ([]byte, error) {
var body []byte
var err error
if strings.HasPrefix(pathOrURL, "https://") || strings.HasPrefix(pathOrURL, "http://") {
body, err = common.HTTPGet(context.Background(), pathOrURL)
if err != nil {
return nil, err
}
if err = localSave(pathOrURL, body); err != nil {
return nil, err
}
} else {
body, err = os.ReadFile(filepath.Clean(pathOrURL))
if err != nil {
return nil, err
}
}
return body, nil
}
// IsAppfile check if a file is Appfile format or application format, return true if it's appfile, false means application object
func IsAppfile(body []byte) bool {
if j.Valid(body) {
// we only support json format for appfile
return true
}
res := map[string]interface{}{}
err := yaml.Unmarshal(body, &res)
if err != nil {
return nil, err
return false
}
// appfile didn't have apiVersion
if _, ok := res["apiVersion"]; ok {
return false
}
return true
}
func localSave(url string, body []byte) error {
var name string
ext := filepath.Ext(url)
switch ext {
case ".json":
name = "vela.json"
case ".yaml", ".yml":
name = "vela.yaml"
default:
if j.Valid(body) {
name = "vela.json"
} else {
name = "vela.yaml"
}
}
//nolint:gosec
return af, os.WriteFile(dest, body, 0644)
return os.WriteFile(name, body, 0644)
}
// ExportFromAppFile exports Application from appfile object
@@ -279,7 +316,7 @@ func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, q
appHandler := appfile.NewApplication(app, tm)
// new
retApplication, scopes, err := appHandler.BuildOAMApplication(o.Namespace, o.IO, appHandler.Tm, quiet)
retApplication, err := appHandler.ConvertToApplication(o.Namespace, o.IO, appHandler.Tm, quiet)
if err != nil {
return nil, nil, err
}
@@ -294,19 +331,9 @@ func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, namespace string, q
}
w.WriteByte('\n')
for _, scope := range scopes {
w.WriteString("---\n")
err = enc.Encode(scope, &w)
if err != nil {
return nil, nil, fmt.Errorf("yaml encode scope (%s) failed: %w", scope.GetName(), err)
}
w.WriteByte('\n')
}
result := &BuildResult{
appFile: app,
application: retApplication,
scopes: scopes,
}
return result, w.Bytes(), nil
}
@@ -316,14 +343,10 @@ func (o *AppfileOptions) Export(filePath, namespace string, quiet bool, c common
var app *api.AppFile
var err error
if !quiet {
o.IO.Info("Parsing vela appfile ...")
o.IO.Info("Parsing vela application file ...")
}
if filePath != "" {
if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
app, err = saveAndLoadRemoteAppfile(filePath)
} else {
app, err = api.LoadFromFile(filePath)
}
app, err = LoadAppFile(filePath)
} else {
app, err = api.Load()
}
@@ -383,7 +406,7 @@ func (o *AppfileOptions) ApplyApp(app *corev1beta1.Application, scopes []oam.Obj
if err := o.apply(app, scopes); err != nil {
return err
}
o.IO.Infof(o.Info(app))
o.IO.Infof(Info(app))
return nil
}
@@ -395,7 +418,7 @@ func (o *AppfileOptions) apply(app *corev1beta1.Application, scopes []oam.Object
}
// Info shows the status of each service in the Appfile
func (o *AppfileOptions) Info(app *corev1beta1.Application) string {
func Info(app *corev1beta1.Application) string {
appName := app.Name
var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
@@ -413,7 +436,7 @@ func ApplyApplication(app corev1beta1.Application, ioStream cmdutil.IOStreams, c
if app.Namespace == "" {
app.Namespace = types.DefaultAppNamespace
}
_, err := ioStream.Out.Write([]byte("Applying an application in K8S format...\n"))
_, err := ioStream.Out.Write([]byte("Applying an application in vela K8s object format...\n"))
if err != nil {
return err
}
@@ -422,9 +445,6 @@ func ApplyApplication(app corev1beta1.Application, ioStream cmdutil.IOStreams, c
if err != nil {
return err
}
_, err = ioStream.Out.Write([]byte("Successfully apply application"))
if err != nil {
return err
}
ioStream.Infof(Info(&app))
return nil
}

View File

@@ -28,10 +28,10 @@ import (
// BuildRun will build application and deploy from Appfile
func BuildRun(ctx context.Context, app *api.Application, client client.Client, namespace string, io util.IOStreams) error {
o, scopes, err := app.BuildOAMApplication(namespace, io, app.Tm, true)
o, err := app.ConvertToApplication(namespace, io, app.Tm, true)
if err != nil {
return err
}
return appfile.Run(ctx, client, o, scopes)
return appfile.Run(ctx, client, o, nil)
}