OpenAPI: Application creation based on Application object (#924)

* OpenAPI: Application creation based on Application object

Implemented Application creation OpenAPI based on new Application object
To #913

* fix e2e problems

* fix import issue
This commit is contained in:
Zheng Xi Zhou
2021-01-28 10:15:26 +08:00
committed by GitHub
parent 4d00ad78ae
commit aa03e9c9d4
14 changed files with 988 additions and 286 deletions
@@ -783,6 +783,38 @@
"type" : "integer"
}
}
};
defs["appfile.AppFile"] = {
"type" : "object",
"properties" : {
"createTime" : {
"type" : "string"
},
"name" : {
"type" : "string"
},
"secrets" : {
"type" : "object",
"additionalProperties" : {
"type" : "string"
}
},
"services" : {
"type" : "object",
"additionalProperties" : {
"$ref" : "#/components/schemas/appfile.Service"
}
},
"updateTime" : {
"type" : "string"
}
}
};
defs["appfile.Service"] = {
"type" : "object"
};
defs["config.Store"] = {
"type" : "object"
};
defs["inline_response_200"] = {
"type" : "object",
@@ -1269,6 +1301,9 @@
<li class="nav-fixed nav-header active" data-group="_"><a href="#api-_">API Summary</a></li>
<li class="nav-header" data-group="Applications"><a href="#api-Applications">API Methods - Applications</a></li>
<li data-group="Applications" data-name="createApplication" class="">
<a href="#api-Applications-createApplication">createApplication</a>
</li>
<li data-group="Applications" data-name="listApplications" class="">
<a href="#api-Applications-listApplications">listApplications</a>
</li>
@@ -1316,6 +1351,404 @@
<div id="sections">
<section id="api-Applications">
<h1>Applications</h1>
<div id="api-Applications-createApplication">
<article id="api-Applications-createApplication-0" data-group="User" data-name="createApplication" data-version="0">
<div class="pull-left">
<h1>createApplication</h1>
<p>creates an application</p>
</div>
<div class="pull-right"></div>
<div class="clearfix"></div>
<p></p>
<p class="marked"></p>
<p></p>
<br />
<pre class="prettyprint language-html prettyprinted" data-type="post"><code><span class="pln">/envs/{envName}/apps</span></code></pre>
<p>
<h3>Usage and SDK Samples</h3>
</p>
<ul class="nav nav-tabs nav-tabs-examples">
<li class="active"><a href="#examples-Applications-createApplication-0-curl">Curl</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-java">Java</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-android">Android</a></li>
<!--<li class=""><a href="#examples-Applications-createApplication-0-groovy">Groovy</a></li>-->
<li class=""><a href="#examples-Applications-createApplication-0-objc">Obj-C</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-javascript">JavaScript</a></li>
<!--<li class=""><a href="#examples-Applications-createApplication-0-angular">Angular</a></li>-->
<li class=""><a href="#examples-Applications-createApplication-0-csharp">C#</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-php">PHP</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-perl">Perl</a></li>
<li class=""><a href="#examples-Applications-createApplication-0-python">Python</a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active" id="examples-Applications-createApplication-0-curl">
<pre class="prettyprint"><code class="language-bsh">curl -X POST\
-H "Accept: */*"\
-H "Content-Type: */*"\
"//127.0.0.1:38081/api/envs/{envName}/apps"</code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-java">
<pre class="prettyprint"><code class="language-java">import io.swagger.client.*;
import io.swagger.client.auth.*;
import io.swagger.client.model.*;
import io.swagger.client.api.ApplicationsApi;
import java.io.File;
import java.util.*;
public class ApplicationsApiExample {
public static void main(String[] args) {
ApplicationsApi apiInstance = new ApplicationsApi();
Appfile.AppFile body = ; // Appfile.AppFile | application parameters
String envName = envName_example; // String | environment name
try {
inline_response_200 result = apiInstance.createApplication(body, envName);
System.out.println(result);
} catch (ApiException e) {
System.err.println("Exception when calling ApplicationsApi#createApplication");
e.printStackTrace();
}
}
}</code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-android">
<pre class="prettyprint"><code class="language-java">import io.swagger.client.api.ApplicationsApi;
public class ApplicationsApiExample {
public static void main(String[] args) {
ApplicationsApi apiInstance = new ApplicationsApi();
Appfile.AppFile body = ; // Appfile.AppFile | application parameters
String envName = envName_example; // String | environment name
try {
inline_response_200 result = apiInstance.createApplication(body, envName);
System.out.println(result);
} catch (ApiException e) {
System.err.println("Exception when calling ApplicationsApi#createApplication");
e.printStackTrace();
}
}
}</code></pre>
</div>
<!--
<div class="tab-pane" id="examples-Applications-createApplication-0-groovy">
<pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre>
</div> -->
<div class="tab-pane" id="examples-Applications-createApplication-0-objc">
<pre class="prettyprint"><code class="language-cpp">Appfile.AppFile *body = ; // application parameters
String *envName = envName_example; // environment name
ApplicationsApi *apiInstance = [[ApplicationsApi alloc] init];
// creates an application
[apiInstance createApplicationWith:body
envName:envName
completionHandler: ^(inline_response_200 output, NSError* error) {
if (output) {
NSLog(@"%@", output);
}
if (error) {
NSLog(@"Error: %@", error);
}
}];
</code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-javascript">
<pre class="prettyprint"><code class="language-js">var KubeVelaRestfulApi = require('kube_vela_restful_api');
var api = new KubeVelaRestfulApi.ApplicationsApi()
var body = ; // {{Appfile.AppFile}} application parameters
var envName = envName_example; // {{String}} environment name
var callback = function(error, data, response) {
if (error) {
console.error(error);
} else {
console.log('API called successfully. Returned data: ' + data);
}
};
api.createApplication(bodyenvName, callback);
</code></pre>
</div>
<!--<div class="tab-pane" id="examples-Applications-createApplication-0-angular">
<pre class="prettyprint language-json prettyprinted" data-type="json"><code>Coming Soon!</code></pre>
</div>-->
<div class="tab-pane" id="examples-Applications-createApplication-0-csharp">
<pre class="prettyprint"><code class="language-cs">using System;
using System.Diagnostics;
using IO.Swagger.Api;
using IO.Swagger.Client;
using IO.Swagger.Model;
namespace Example
{
public class createApplicationExample
{
public void main()
{
var apiInstance = new ApplicationsApi();
var body = new Appfile.AppFile(); // Appfile.AppFile | application parameters
var envName = envName_example; // String | environment name
try
{
// creates an application
inline_response_200 result = apiInstance.createApplication(body, envName);
Debug.WriteLine(result);
}
catch (Exception e)
{
Debug.Print("Exception when calling ApplicationsApi.createApplication: " + e.Message );
}
}
}
}
</code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-php">
<pre class="prettyprint"><code class="language-php"><&#63;php
require_once(__DIR__ . '/vendor/autoload.php');
$api_instance = new Swagger\Client\ApiApplicationsApi();
$body = ; // Appfile.AppFile | application parameters
$envName = envName_example; // String | environment name
try {
$result = $api_instance->createApplication($body, $envName);
print_r($result);
} catch (Exception $e) {
echo 'Exception when calling ApplicationsApi->createApplication: ', $e->getMessage(), PHP_EOL;
}
?></code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-perl">
<pre class="prettyprint"><code class="language-perl">use Data::Dumper;
use WWW::SwaggerClient::Configuration;
use WWW::SwaggerClient::ApplicationsApi;
my $api_instance = WWW::SwaggerClient::ApplicationsApi->new();
my $body = WWW::SwaggerClient::Object::Appfile.AppFile->new(); # Appfile.AppFile | application parameters
my $envName = envName_example; # String | environment name
eval {
my $result = $api_instance->createApplication(body => $body, envName => $envName);
print Dumper($result);
};
if ($@) {
warn "Exception when calling ApplicationsApi->createApplication: $@\n";
}</code></pre>
</div>
<div class="tab-pane" id="examples-Applications-createApplication-0-python">
<pre class="prettyprint"><code class="language-python">from __future__ import print_statement
import time
import swagger_client
from swagger_client.rest import ApiException
from pprint import pprint
# create an instance of the API class
api_instance = swagger_client.ApplicationsApi()
body = # Appfile.AppFile | application parameters
envName = envName_example # String | environment name
try:
# creates an application
api_response = api_instance.create_application(body, envName)
pprint(api_response)
except ApiException as e:
print("Exception when calling ApplicationsApi->createApplication: %s\n" % e)</code></pre>
</div>
</div>
<h2>Parameters</h2>
<div class="methodsubtabletitle">Path parameters</div>
<table id="methodsubtable">
<tr>
<th width="150px">Name</th>
<th>Description</th>
</tr>
<tr><td style="width:150px;">envName*</td>
<td>
<div id="d2e199_createApplication_envName">
<div class="json-schema-view">
<div class="primitive">
<span class="type">
String
</span>
<div class="inner description">
environment name
</div>
</div>
<div class="inner required">
Required
</div>
</div>
</div>
</td>
</tr>
</table>
<div class="methodsubtabletitle">Body parameters</div>
<table id="methodsubtable">
<tr>
<th width="150px">Name</th>
<th>Description</th>
</tr>
<tr><td style="width:150px;">body <span style="color:red;">*</span></td>
<td>
<script>
$(document).ready(function() {
var schemaWrapper = {
"description" : "application parameters",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/appfile.AppFile"
}
}
},
"required" : true
};
var schema = schemaWrapper.content["*/*"].schema;
if (schema.$ref != null) {
schema = defsParser.$refs.get(schema.$ref);
} else {
schemaWrapper.components = {};
schemaWrapper.components.schemas = Object.assign({}, defs);
$RefParser.dereference(schemaWrapper).catch(function(err) {
console.log(err);
});
}
var view = new JSONSchemaView(schema,2,{isBodyParam: true});
var result = $('#d2e199_createApplication_body');
result.empty();
result.append(view.render());
});
</script>
<div id="d2e199_createApplication_body"></div>
</td>
</tr>
</table>
<h2>Responses</h2>
<h3> Status: 200 - OK </h3>
<ul class="nav nav-tabs nav-tabs-examples" >
<li class="active">
<a data-toggle="tab" href="#responses-createApplication-200-schema">Schema</a>
</li>
</ul>
<div class="tab-content" style='margin-bottom: 10px;'>
<div class="tab-pane active" id="responses-createApplication-200-schema">
<div id='responses-createApplication-200-schema-200' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
<script>
$(document).ready(function() {
var schemaWrapper = {
"description" : "OK",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/inline_response_200"
}
}
}
};
var schema = schemaWrapper.content["*/*"].schema;
if (schema.$ref != null) {
schema = defsParser.$refs.get(schema.$ref);
} else {
schemaWrapper.components = {};
schemaWrapper.components.schemas = Object.assign({}, defs);
$RefParser.dereference(schemaWrapper).catch(function(err) {
console.log(err);
});
}
//console.log(JSON.stringify(schema));
var view = new JSONSchemaView(schema, 3);
$('#responses-createApplication-200-schema-data').val(stringify(schema));
var result = $('#responses-createApplication-200-schema-200');
result.empty();
result.append(view.render());
});
</script>
</div>
<input id='responses-createApplication-200-schema-data' type='hidden' value=''></input>
</div>
</div>
<h3> Status: 500 - Internal Server Error </h3>
<ul class="nav nav-tabs nav-tabs-examples" >
<li class="active">
<a data-toggle="tab" href="#responses-createApplication-500-schema">Schema</a>
</li>
</ul>
<div class="tab-content" style='margin-bottom: 10px;'>
<div class="tab-pane active" id="responses-createApplication-500-schema">
<div id='responses-createApplication-500-schema-500' style="padding: 30px; border-left: 1px solid #eee; border-right: 1px solid #eee; border-bottom: 1px solid #eee;">
<script>
$(document).ready(function() {
var schemaWrapper = {
"description" : "Internal Server Error",
"content" : {
"*/*" : {
"schema" : {
"$ref" : "#/components/schemas/inline_response_200"
}
}
}
};
var schema = schemaWrapper.content["*/*"].schema;
if (schema.$ref != null) {
schema = defsParser.$refs.get(schema.$ref);
} else {
schemaWrapper.components = {};
schemaWrapper.components.schemas = Object.assign({}, defs);
$RefParser.dereference(schemaWrapper).catch(function(err) {
console.log(err);
});
}
//console.log(JSON.stringify(schema));
var view = new JSONSchemaView(schema, 3);
$('#responses-createApplication-500-schema-data').val(stringify(schema));
var result = $('#responses-createApplication-500-schema-500');
result.empty();
result.append(view.render());
});
</script>
</div>
<input id='responses-createApplication-500-schema-data' type='hidden' value=''></input>
</div>
</div>
</article>
</div>
<hr>
<div id="api-Applications-listApplications">
<article id="api-Applications-listApplications-0" data-group="User" data-name="listApplications" data-version="0">
<div class="pull-left">
+37 -26
View File
@@ -7,12 +7,13 @@ import (
"net/http"
"strings"
"github.com/oam-dev/kubevela/e2e"
"github.com/oam-dev/kubevela/pkg/server/apis"
"github.com/oam-dev/kubevela/pkg/server/util"
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"github.com/oam-dev/kubevela/e2e"
"github.com/oam-dev/kubevela/pkg/appfile/api"
"github.com/oam-dev/kubevela/pkg/server/apis"
"github.com/oam-dev/kubevela/pkg/server/util"
)
var (
@@ -30,20 +31,27 @@ var (
Namespace: "env-e2e-world-modified",
}
workloadType = "webservice"
workloadName = "app-e2e-api-hello"
workloadType = "webservice"
applicationName = "app-e2e-api-hello"
svcName = "svc-e2e-api-hello"
workloadRunBodyWithoutImageFlag = apis.WorkloadRunBody{
EnvName: envHelloMeta.EnvName,
WorkloadName: workloadName,
WorkloadType: workloadType,
Flags: []apis.CommonFlag{{Name: "port", Value: "80"}},
applicationCreationBodyWithoutImageFlag = api.AppFile{
Name: applicationName,
Services: map[string]api.Service{
svcName: map[string]interface{}{},
},
}
workloadRunBody = apis.WorkloadRunBody{
EnvName: envHelloMeta.EnvName,
WorkloadName: workloadName,
WorkloadType: workloadType,
Flags: []apis.CommonFlag{{Name: "image", Value: "nginx:1.9.4"}, {Name: "port", Value: "80"}},
applicationCreationBody = api.AppFile{
Name: applicationName,
Services: map[string]api.Service{
svcName: map[string]interface{}{
"type": workloadType,
"image": "wordpress:php7.4-apache",
"port": "80",
"cpu": "1",
},
},
}
)
@@ -165,10 +173,11 @@ var _ = ginkgo.Describe("API", func() {
})
ginkgo.Context("Workloads", func() {
ginkgo.It("run workload", func() {
data, err := json.Marshal(&workloadRunBody)
ginkgo.It("create an application", func() {
data, err := json.Marshal(&applicationCreationBody)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data)))
url := fmt.Sprintf("/envs/%s/apps/", envHelloMeta.EnvName)
resp, err := http.Post(util.URL(url), "application/json", strings.NewReader(string(data)))
gomega.Expect(err).NotTo(gomega.HaveOccurred())
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
@@ -177,14 +186,16 @@ var _ = ginkgo.Describe("API", func() {
err = json.Unmarshal(result, &r)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code), string(result))
output := fmt.Sprintf("App %s deployed", workloadName)
output := fmt.Sprintf("application %s is successfully created", applicationName)
gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output))
})
ginkgo.It("run workload without compulsory flag", func() {
data, err := json.Marshal(&workloadRunBodyWithoutImageFlag)
ginkgo.It("create an application without compulsory flag of a service", func() {
data, err := json.Marshal(&applicationCreationBodyWithoutImageFlag)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
resp, err := http.Post(util.URL("/workloads/"), "application/json", strings.NewReader(string(data)))
url := fmt.Sprintf("/envs/%s/apps/", envHelloMeta.EnvName)
resp, err := http.Post(util.URL(url), "application/json", strings.NewReader(string(data)))
// TODO(zzxwill) revise the check process if we need to work on https://github.com/oam-dev/kubevela/discussions/933
gomega.Expect(err).NotTo(gomega.HaveOccurred())
defer resp.Body.Close()
result, err := ioutil.ReadAll(resp.Body)
@@ -192,8 +203,8 @@ var _ = ginkgo.Describe("API", func() {
var r apis.Response
err = json.Unmarshal(result, &r)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(http.StatusInternalServerError).Should(gomega.Equal(r.Code))
output := "required flag(s) \"image\" not set"
gomega.Expect(http.StatusOK).Should(gomega.Equal(r.Code), string(result))
output := fmt.Sprintf("application %s is successfully created", applicationName)
gomega.Expect(r.Data.(string)).To(gomega.ContainSubstring(output))
})
@@ -215,7 +226,7 @@ var _ = ginkgo.Describe("API", func() {
})
ginkgo.It("should delete an application", func() {
req, err := http.NewRequest("DELETE", util.URL("/envs/"+envHelloMeta.EnvName+"/apps/"+workloadRunBody.WorkloadName), nil)
req, err := http.NewRequest("DELETE", util.URL("/envs/"+envHelloMeta.EnvName+"/apps/"+applicationName), nil)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
resp, err := http.DefaultClient.Do(req)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
+3 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
)
// NewExportCommand will create command for exporting deploy manifests from an AppFile
@@ -18,7 +19,7 @@ func NewExportCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
types.TagCommandType: types.TypeStart,
},
RunE: func(cmd *cobra.Command, args []string) error {
o := &AppfileOptions{
o := &serverlib.AppfileOptions{
IO: ioStream,
Env: &types.EnvMeta{},
}
@@ -26,7 +27,7 @@ func NewExportCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
if err != nil {
return err
}
_, data, err := o.export(filePath, true)
_, data, err := o.Export(filePath, true)
if err != nil {
return err
}
+2 -190
View File
@@ -1,29 +1,12 @@
package commands
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
k8sjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/api"
"github.com/oam-dev/kubevela/pkg/appfile/template"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/serverlib"
)
var (
@@ -53,7 +36,7 @@ func NewUpCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
return err
}
o := &AppfileOptions{
o := &serverlib.AppfileOptions{
Kubecli: kubecli,
IO: ioStream,
Env: velaEnv,
@@ -70,174 +53,3 @@ func NewUpCommand(c types.Args, ioStream cmdutil.IOStreams) *cobra.Command {
cmd.Flags().StringP(appFilePath, "f", "", "specify file path for appfile")
return cmd
}
// AppfileOptions is some configuration that modify options for an Appfile
type AppfileOptions struct {
Kubecli client.Client
IO cmdutil.IOStreams
Env *types.EnvMeta
}
func saveRemoteAppfile(url string) (string, error) {
body, err := common.HTTPGet(context.Background(), url)
if err != nil {
return "", err
}
ext := filepath.Ext(url)
dest := "Appfile"
if ext == ".json" {
dest = "vela.json"
} else if ext == ".yaml" || ext == ".yml" {
dest = "vela.yaml"
}
//nolint:gosec
return dest, ioutil.WriteFile(dest, body, 0644)
}
type buildResult struct {
appFile *api.AppFile
application *v1alpha2.Application
scopes []oam.Object
}
func (o *AppfileOptions) export(filePath string, quiet bool) (*buildResult, []byte, error) {
var app *api.AppFile
var err error
if !quiet {
o.IO.Info("Parsing vela appfile ...")
}
if filePath != "" {
if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
filePath, err = saveRemoteAppfile(filePath)
if err != nil {
return nil, nil, err
}
}
app, err = api.LoadFromFile(filePath)
} else {
app, err = api.Load()
}
if err != nil {
return nil, nil, err
}
if !quiet {
o.IO.Info("Load Template ...")
}
tm, err := template.Load()
if err != nil {
return nil, nil, err
}
appHandler := appfile.NewApplication(app, tm)
// new
retApplication, scopes, err := appHandler.BuildOAMApplication(o.Env, o.IO, appHandler.Tm, quiet)
if err != nil {
return nil, nil, err
}
var w bytes.Buffer
enc := k8sjson.NewYAMLSerializer(k8sjson.DefaultMetaFactory, nil, nil)
err = enc.Encode(retApplication, &w)
if err != nil {
return nil, nil, fmt.Errorf("yaml encode application failed: %w", err)
}
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
}
// Run starts an application according to Appfile
func (o *AppfileOptions) Run(filePath string) error {
result, data, err := o.export(filePath, false)
if err != nil {
return err
}
deployFilePath := ".vela/deploy.yaml"
o.IO.Infof("Writing deploy config to (%s)\n", deployFilePath)
if err := os.MkdirAll(filepath.Dir(deployFilePath), 0700); err != nil {
return err
}
if err := ioutil.WriteFile(deployFilePath, data, 0600); err != nil {
return errors.Wrap(err, "write deploy config manifests failed")
}
if err := o.saveToAppDir(result.appFile); err != nil {
return errors.Wrap(err, "save to app dir failed")
}
o.IO.Infof("\nApplying application ...\n")
return o.ApplyApp(result.application, result.scopes)
}
func (o *AppfileOptions) saveToAppDir(f *api.AppFile) error {
app := &api.Application{AppFile: f}
return appfile.Save(app, o.Env.Name)
}
// ApplyApp applys config resources for the app.
// It differs by create and update:
// - for create, it displays app status along with information of url, metrics, ssh, logging.
// - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment.
// This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback.
func (o *AppfileOptions) ApplyApp(app *v1alpha2.Application, scopes []oam.Object) error {
key := apitypes.NamespacedName{
Namespace: app.Namespace,
Name: app.Name,
}
o.IO.Infof("Checking if app has been deployed...\n")
var tmpApp v1alpha2.Application
err := o.Kubecli.Get(context.TODO(), key, &tmpApp)
switch {
case apierrors.IsNotFound(err):
o.IO.Infof("App has not been deployed, creating a new deployment...\n")
case err == nil:
o.IO.Infof("App exists, updating existing deployment...\n")
default:
return err
}
if err := o.apply(app, scopes); err != nil {
return err
}
o.IO.Infof(o.Info(app))
return nil
}
func (o *AppfileOptions) apply(app *v1alpha2.Application, scopes []oam.Object) error {
if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil {
return err
}
return nil
}
// Info shows the status of each service in the Appfile
func (o *AppfileOptions) Info(app *v1alpha2.Application) string {
appName := app.Name
var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
fmt.Sprintf(" SSH: vela exec %s\n", appName) +
fmt.Sprintf(" Logging: vela logs %s\n", appName) +
fmt.Sprintf(" App status: vela status %s\n", appName)
for _, comp := range app.Spec.Components {
appUpMessage += fmt.Sprintf(" Service status: vela status %s --svc %s\n", appName, comp.Name)
}
return appUpMessage
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
)
func TestUp(t *testing.T) {
@@ -23,7 +24,7 @@ func TestUp(t *testing.T) {
Namespace: "env-up",
Issuer: "up",
}
o := AppfileOptions{
o := serverlib.AppfileOptions{
Kubecli: client,
IO: ioStream,
Env: &env,
@@ -22,7 +22,7 @@ import (
var _ = Describe("CRD without definition can run in an ApplicationConfiguration", func() {
ctx := context.Background()
It("run workload and trait without CRD", func() {
It("create an application without CRD", func() {
By("Creating CRD foo.crdtest1.com")
// Create a crd for appconfig dependency test
-11
View File
@@ -34,17 +34,6 @@ type CommonFlag struct {
Value string `json:"value"`
}
// WorkloadRunBody used for restful API arguments for run workload for dashboard restful API server
type WorkloadRunBody struct {
EnvName string `json:"envName"`
WorkloadType string `json:"workloadType"`
WorkloadName string `json:"workloadName"`
AppName string `json:"appName,omitempty"`
Flags []CommonFlag `json:"flags"`
Staging bool `json:"staging,omitempty"`
Traits []TraitBody `json:"traits,omitempty"`
}
// WorkloadMeta store workload metadata for dashboard restful API server
type WorkloadMeta struct {
Name string `json:"name"`
+49 -3
View File
@@ -1,11 +1,17 @@
package server
import (
"fmt"
"os"
"github.com/gin-gonic/gin"
"github.com/oam-dev/kubevela/pkg/appfile/api"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/server/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
"github.com/oam-dev/kubevela/pkg/utils/env"
"github.com/gin-gonic/gin"
env2 "github.com/oam-dev/kubevela/pkg/utils/env"
)
// UpdateApps is placeholder for updating applications
@@ -57,7 +63,7 @@ func (s *APIServer) ListApps(c *gin.Context) {
util.AssembleResponse(c, applicationMetaList, nil)
}
// DeleteApps deletes an application by the namespacedname in the gin.Context
// DeleteApps deletes an application by the namespaced name in the gin.Context
func (s *APIServer) DeleteApps(c *gin.Context) {
envName := c.Param("envName")
envMeta, err := env.GetEnvByName(envName)
@@ -75,3 +81,43 @@ func (s *APIServer) DeleteApps(c *gin.Context) {
message, err := o.DeleteApp()
util.AssembleResponse(c, message, err)
}
// CreateApplication creates an application
// @tags applications
// @ID CreateApplication
// @Summary creates an application
// @Param envName path string true "environment name"
// @Param body body appfile.AppFile true "application parameters"
// @Success 200 {object} apis.Response{code=int,data=string}
// @Failure 500 {object} apis.Response{code=int,data=string}
// @Router /envs/{envName}/apps [post]
func (s *APIServer) CreateApplication(c *gin.Context) {
var body api.AppFile
if err := c.ShouldBindJSON(&body); err != nil {
util.HandleError(c, util.InvalidArgument, "the application creation request body is invalid")
return
}
env, err := env2.GetEnvByName(c.Param("envName"))
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
ioStream := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
o := &serverlib.AppfileOptions{
Kubecli: s.KubeClient,
IO: ioStream,
Env: env,
}
buildResult, data, err := o.ExportFromAppFile(&body, false)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
err = o.BaseAppFileRun(buildResult, data)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
msg := fmt.Sprintf("application %s is successfully created", body.Name)
util.AssembleResponse(c, msg, nil)
}
+102
View File
@@ -518,6 +518,75 @@ var doc = `{
}
}
}
},
"post": {
"tags": [
"applications"
],
"summary": "creates an application",
"operationId": "CreateApplication",
"parameters": [
{
"type": "string",
"description": "environment name",
"name": "envName",
"in": "path",
"required": true
},
{
"description": "application parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/appfile.AppFile"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/apis.Response"
},
{
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/apis.Response"
},
{
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {
"type": "string"
}
}
}
]
}
}
}
}
}
},
@@ -622,6 +691,39 @@ var doc = `{
}
}
},
"appfile.AppFile": {
"type": "object",
"properties": {
"createTime": {
"type": "string"
},
"name": {
"type": "string"
},
"secrets": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"services": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/appfile.Service"
}
},
"updateTime": {
"type": "string"
}
}
},
"appfile.Service": {
"type": "object",
"additionalProperties": true
},
"config.Store": {
"type": "object"
},
"v1alpha2.ApplicationConfiguration": {
"type": "object",
"properties": {
+102
View File
@@ -503,6 +503,75 @@
}
}
}
},
"post": {
"tags": [
"applications"
],
"summary": "creates an application",
"operationId": "CreateApplication",
"parameters": [
{
"type": "string",
"description": "environment name",
"name": "envName",
"in": "path",
"required": true
},
{
"description": "application parameters",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/appfile.AppFile"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/apis.Response"
},
{
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/apis.Response"
},
{
"type": "object",
"properties": {
"code": {
"type": "integer"
},
"data": {
"type": "string"
}
}
}
]
}
}
}
}
}
},
@@ -607,6 +676,39 @@
}
}
},
"appfile.AppFile": {
"type": "object",
"properties": {
"createTime": {
"type": "string"
},
"name": {
"type": "string"
},
"secrets": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"services": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/appfile.Service"
}
},
"updateTime": {
"type": "string"
}
}
},
"appfile.Service": {
"type": "object",
"additionalProperties": true
},
"config.Store": {
"type": "object"
},
"v1alpha2.ApplicationConfiguration": {
"type": "object",
"properties": {
+62
View File
@@ -66,6 +66,28 @@ definitions:
code:
type: integer
type: object
appfile.AppFile:
properties:
createTime:
type: string
name:
type: string
secrets:
additionalProperties:
type: string
type: object
services:
additionalProperties:
$ref: '#/definitions/appfile.Service'
type: object
updateTime:
type: string
type: object
appfile.Service:
additionalProperties: true
type: object
config.Store:
type: object
v1alpha2.ApplicationConfiguration:
properties:
spec:
@@ -731,4 +753,44 @@ paths:
summary: list all applications
tags:
- applications
post:
operationId: CreateApplication
parameters:
- description: environment name
in: path
name: envName
required: true
type: string
- description: application parameters
in: body
name: body
required: true
schema:
$ref: '#/definitions/appfile.AppFile'
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/apis.Response'
- properties:
code:
type: integer
data:
type: string
type: object
"500":
description: Internal Server Error
schema:
allOf:
- $ref: '#/definitions/apis.Response'
- properties:
code:
type: integer
data:
type: string
type: object
summary: creates an application
tags:
- applications
swagger: "2.0"
+1 -1
View File
@@ -80,6 +80,7 @@ func (s *APIServer) setupRoute(staticPath string) http.Handler {
apps.GET("/", s.ListApps)
apps.GET("", s.ListApps)
apps.DELETE("/:appName", s.DeleteApps)
apps.POST("/", s.CreateApplication)
// component related operation
components := apps.Group("/:appName/components")
@@ -101,7 +102,6 @@ func (s *APIServer) setupRoute(staticPath string) http.Handler {
// workload related api
workload := api.Group(util.WorkloadDefinitionPath)
{
workload.POST("/", s.CreateWorkload)
workload.GET("/:workloadName", s.GetWorkload)
workload.PUT("/:workloadName", s.UpdateWorkload)
workload.GET("/", s.ListWorkload)
-51
View File
@@ -1,65 +1,14 @@
package server
import (
"os"
"github.com/gin-gonic/gin"
"github.com/spf13/pflag"
"github.com/oam-dev/kubevela/apis/types"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/plugins"
"github.com/oam-dev/kubevela/pkg/server/apis"
"github.com/oam-dev/kubevela/pkg/server/util"
"github.com/oam-dev/kubevela/pkg/serverlib"
env2 "github.com/oam-dev/kubevela/pkg/utils/env"
)
// CreateWorkload creates a workload
func (s *APIServer) CreateWorkload(c *gin.Context) {
var body apis.WorkloadRunBody
if err := c.ShouldBindJSON(&body); err != nil {
util.HandleError(c, util.InvalidArgument, "the workload run request body is invalid")
return
}
fs := pflag.NewFlagSet("workload", pflag.ContinueOnError)
for _, f := range body.Flags {
fs.String(f.Name, f.Value, "")
}
envName := body.EnvName
appObj, err := serverlib.BaseComplete(envName, body.WorkloadName, body.AppName, fs, body.WorkloadType)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
env, err := env2.GetEnvByName(envName)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
io := cmdutil.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
msg, err := serverlib.BaseRun(body.Staging, appObj, s.KubeClient, env, io)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
if len(body.Traits) == 0 {
util.AssembleResponse(c, msg, err)
return
}
for _, t := range body.Traits {
t.AppName = body.AppName
t.ComponentName = body.WorkloadName
msg, err = s.DoAttachTrait(c, t)
if err != nil {
util.HandleError(c, util.StatusInternalServerError, err.Error())
return
}
}
util.AssembleResponse(c, msg, err)
}
// UpdateWorkload updates a workload
func (s *APIServer) UpdateWorkload(c *gin.Context) {
}
+194
View File
@@ -1,23 +1,34 @@
package serverlib
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/pkg/errors"
"github.com/spf13/cobra"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
apitypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
corev1alpha2 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/appfile/api"
"github.com/oam-dev/kubevela/pkg/appfile/template"
cmdutil "github.com/oam-dev/kubevela/pkg/commands/util"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/server/apis"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
// nolint:golint
@@ -31,6 +42,20 @@ const (
type componentMetaList []apis.ComponentMeta
type applicationMetaList []apis.ApplicationMeta
// AppfileOptions is some configuration that modify options for an Appfile
type AppfileOptions struct {
Kubecli client.Client
IO cmdutil.IOStreams
Env *types.EnvMeta
}
// BuildResult is the export struct from AppFile yaml or AppFile object
type BuildResult struct {
appFile *api.AppFile
application *v1alpha2.Application
scopes []oam.Object
}
func (comps componentMetaList) Len() int {
return len(comps)
}
@@ -298,3 +323,172 @@ func GetServicesWhenDescribingApplication(cmd *cobra.Command, app *api.Applicati
}
return targetServices, nil
}
func saveRemoteAppfile(url string) (string, error) {
body, err := common.HTTPGet(context.Background(), url)
if err != nil {
return "", err
}
ext := filepath.Ext(url)
dest := "Appfile"
if ext == ".json" {
dest = "vela.json"
} else if ext == ".yaml" || ext == ".yml" {
dest = "vela.yaml"
}
//nolint:gosec
return dest, ioutil.WriteFile(dest, body, 0644)
}
// ExportFromAppFile exports Application from appfile object
func (o *AppfileOptions) ExportFromAppFile(app *api.AppFile, quiet bool) (*BuildResult, []byte, error) {
tm, err := template.Load()
if err != nil {
return nil, nil, err
}
appHandler := appfile.NewApplication(app, tm)
// new
retApplication, scopes, err := appHandler.BuildOAMApplication(o.Env, o.IO, appHandler.Tm, quiet)
if err != nil {
return nil, nil, err
}
var w bytes.Buffer
options := json.SerializerOptions{Yaml: true, Pretty: false, Strict: false}
enc := json.NewSerializerWithOptions(json.DefaultMetaFactory, nil, nil, options)
err = enc.Encode(retApplication, &w)
if err != nil {
return nil, nil, fmt.Errorf("yaml encode application failed: %w", err)
}
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
}
// Export export Application object from the path of Appfile
func (o *AppfileOptions) Export(filePath string, quiet bool) (*BuildResult, []byte, error) {
var app *api.AppFile
var err error
if !quiet {
o.IO.Info("Parsing vela appfile ...")
}
if filePath != "" {
if strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "http://") {
filePath, err = saveRemoteAppfile(filePath)
if err != nil {
return nil, nil, err
}
}
app, err = api.LoadFromFile(filePath)
} else {
app, err = api.Load()
}
if err != nil {
return nil, nil, err
}
if !quiet {
o.IO.Info("Load Template ...")
}
return o.ExportFromAppFile(app, quiet)
}
// Run starts an application according to Appfile
func (o *AppfileOptions) Run(filePath string) error {
result, data, err := o.Export(filePath, false)
if err != nil {
return err
}
return o.BaseAppFileRun(result, data)
}
// BaseAppFileRun starts an application according to Appfile
func (o *AppfileOptions) BaseAppFileRun(result *BuildResult, data []byte) error {
deployFilePath := ".vela/deploy.yaml"
o.IO.Infof("Writing deploy config to (%s)\n", deployFilePath)
if err := os.MkdirAll(filepath.Dir(deployFilePath), 0700); err != nil {
return err
}
if err := ioutil.WriteFile(deployFilePath, data, 0600); err != nil {
return errors.Wrap(err, "write deploy config manifests failed")
}
if err := o.saveToAppDir(result.appFile); err != nil {
return errors.Wrap(err, "save to app dir failed")
}
o.IO.Infof("\nApplying application ...\n")
return o.ApplyApp(result.application, result.scopes)
}
func (o *AppfileOptions) saveToAppDir(f *api.AppFile) error {
app := &api.Application{AppFile: f}
return appfile.Save(app, o.Env.Name)
}
// ApplyApp applys config resources for the app.
// It differs by create and update:
// - for create, it displays app status along with information of url, metrics, ssh, logging.
// - for update, it rolls out a canary deployment and prints its information. User can verify the canary deployment.
// This will wait for user approval. If approved, it continues upgrading the whole; otherwise, it would rollback.
func (o *AppfileOptions) ApplyApp(app *v1alpha2.Application, scopes []oam.Object) error {
key := apitypes.NamespacedName{
Namespace: app.Namespace,
Name: app.Name,
}
o.IO.Infof("Checking if app has been deployed...\n")
var tmpApp v1alpha2.Application
err := o.Kubecli.Get(context.TODO(), key, &tmpApp)
switch {
case apierrors.IsNotFound(err):
o.IO.Infof("App has not been deployed, creating a new deployment...\n")
case err == nil:
o.IO.Infof("App exists, updating existing deployment...\n")
default:
return err
}
if err := o.apply(app, scopes); err != nil {
return err
}
o.IO.Infof(o.Info(app))
return nil
}
func (o *AppfileOptions) apply(app *v1alpha2.Application, scopes []oam.Object) error {
if err := appfile.Run(context.TODO(), o.Kubecli, app, scopes); err != nil {
return err
}
return nil
}
// Info shows the status of each service in the Appfile
func (o *AppfileOptions) Info(app *v1alpha2.Application) string {
appName := app.Name
var appUpMessage = "✅ App has been deployed 🚀🚀🚀\n" +
fmt.Sprintf(" Port forward: vela port-forward %s\n", appName) +
fmt.Sprintf(" SSH: vela exec %s\n", appName) +
fmt.Sprintf(" Logging: vela logs %s\n", appName) +
fmt.Sprintf(" App status: vela status %s\n", appName)
for _, comp := range app.Spec.Components {
appUpMessage += fmt.Sprintf(" Service status: vela status %s --svc %s\n", appName, comp.Name)
}
return appUpMessage
}