diff --git a/docs/en/developers/references/restful-api/index.html b/docs/en/developers/references/restful-api/index.html index 2e4b0cd15..b73910376 100644 --- a/docs/en/developers/references/restful-api/index.html +++ b/docs/en/developers/references/restful-api/index.html @@ -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 @@ +
  • + createApplication +
  • listApplications
  • @@ -1316,6 +1351,404 @@

    Applications

    +
    +
    +
    +

    createApplication

    +

    creates an application

    +
    +
    +
    +

    +

    +

    +
    +
    /envs/{envName}/apps
    +

    +

    Usage and SDK Samples

    +

    + + +
    +
    +
    curl -X POST\
    +-H "Accept: */*"\
    +-H "Content-Type: */*"\
    +"//127.0.0.1:38081/api/envs/{envName}/apps"
    +
    +
    +
    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();
    +        }
    +    }
    +}
    +
    + +
    +
    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();
    +        }
    +    }
    +}
    +
    + +
    +
    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);
    +                            }
    +                        }];
    +
    +
    + +
    +
    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);
    +
    +
    + + +
    +
    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 );
    +            }
    +        }
    +    }
    +}
    +
    +
    + +
    +
    <?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;
    +}
    +?>
    +
    + +
    +
    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";
    +}
    +
    + +
    +
    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)
    +
    +
    + +

    Parameters

    + +
    Path parameters
    + + + + + + + + +
    NameDescription
    envName* + + +
    +
    +
    + + String + + +
    + environment name +
    +
    +
    + Required +
    +
    +
    +
    + + +
    Body parameters
    + + + + + + + + +
    NameDescription
    body * + + + +
    +
    + + + +

    Responses

    +

    Status: 200 - OK

    + + + +
    +
    +
    + +
    + +
    +
    + +

    Status: 500 - Internal Server Error

    + + + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    diff --git a/e2e/apiserver/apiserver_test.go b/e2e/apiserver/apiserver_test.go index 9aedb4047..d4f4abe3c 100644 --- a/e2e/apiserver/apiserver_test.go +++ b/e2e/apiserver/apiserver_test.go @@ -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()) diff --git a/pkg/commands/export.go b/pkg/commands/export.go index 7a67dd188..dfd623af3 100644 --- a/pkg/commands/export.go +++ b/pkg/commands/export.go @@ -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 } diff --git a/pkg/commands/up.go b/pkg/commands/up.go index 342c91fcc..4ed4ca68a 100644 --- a/pkg/commands/up.go +++ b/pkg/commands/up.go @@ -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 -} diff --git a/pkg/commands/up_test.go b/pkg/commands/up_test.go index 518a93819..b7970b34a 100644 --- a/pkg/commands/up_test.go +++ b/pkg/commands/up_test.go @@ -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, diff --git a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go index 5570ef5fc..a3928c0ea 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/applicationconfiguration/appconfig_suit_test.go @@ -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 diff --git a/pkg/server/apis/types.go b/pkg/server/apis/types.go index 0baa1cd92..a9e8e8e6a 100644 --- a/pkg/server/apis/types.go +++ b/pkg/server/apis/types.go @@ -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"` diff --git a/pkg/server/appHandlers.go b/pkg/server/appHandlers.go index 39f4c49ae..cb8e4b7e4 100644 --- a/pkg/server/appHandlers.go +++ b/pkg/server/appHandlers.go @@ -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) +} diff --git a/pkg/server/docs/docs.go b/pkg/server/docs/docs.go index dfbc68182..5918ff17c 100644 --- a/pkg/server/docs/docs.go +++ b/pkg/server/docs/docs.go @@ -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": { diff --git a/pkg/server/docs/swagger.json b/pkg/server/docs/swagger.json index 9394c4a51..1e7b19390 100644 --- a/pkg/server/docs/swagger.json +++ b/pkg/server/docs/swagger.json @@ -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": { diff --git a/pkg/server/docs/swagger.yaml b/pkg/server/docs/swagger.yaml index 350a76760..ed025fc7e 100644 --- a/pkg/server/docs/swagger.yaml +++ b/pkg/server/docs/swagger.yaml @@ -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" diff --git a/pkg/server/route.go b/pkg/server/route.go index 1f9413549..1e93f5e89 100644 --- a/pkg/server/route.go +++ b/pkg/server/route.go @@ -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) diff --git a/pkg/server/workloadHandler.go b/pkg/server/workloadHandler.go index 31e105c2f..e94563bcd 100644 --- a/pkg/server/workloadHandler.go +++ b/pkg/server/workloadHandler.go @@ -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) { } diff --git a/pkg/serverlib/application.go b/pkg/serverlib/application.go index 808f5b328..21f665610 100644 --- a/pkg/serverlib/application.go +++ b/pkg/serverlib/application.go @@ -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 +}