mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
Feat: add api for app compare、reset、dryrun (#3136)
* Feat: add api for app compare or reset Signed-off-by: zxbyoyoyo <596908030@qq.com> * Fix: fix lint Signed-off-by: zxbyoyoyo <596908030@qq.com> * Fix: fix lint Signed-off-by: zxbyoyoyo <596908030@qq.com> * Fix: fix imports Signed-off-by: zxbyoyoyo <596908030@qq.com>
This commit is contained in:
@@ -21,6 +21,7 @@ COPY cmd/apiserver/main.go cmd/apiserver/main.go
|
||||
COPY apis/ apis/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY references/ references/
|
||||
|
||||
# Build
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -17,6 +17,7 @@ COPY cmd/apiserver/main.go cmd/apiserver/main.go
|
||||
COPY apis/ apis/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY references/ references/
|
||||
|
||||
# Build
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -18,6 +18,7 @@ COPY cmd/ cmd/
|
||||
COPY apis/ apis/
|
||||
COPY pkg/ pkg/
|
||||
COPY version/ version/
|
||||
COPY references/ references/
|
||||
|
||||
# Build
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -305,6 +305,37 @@ type ApplicationBase struct {
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// AppCompareResponse application compare result
|
||||
type AppCompareResponse struct {
|
||||
IsDiff bool `json:"isDiff"`
|
||||
DiffReport string `json:"diffReport"`
|
||||
NewAppYAML string `json:"newAppYAML"`
|
||||
OldAppYAML string `json:"oldAppYAML"`
|
||||
}
|
||||
|
||||
// AppResetResponse application reset result
|
||||
type AppResetResponse struct {
|
||||
IsReset bool `json:"isReset"`
|
||||
}
|
||||
|
||||
// AppCompareReq application compare req
|
||||
type AppCompareReq struct {
|
||||
Env string `json:"env"`
|
||||
}
|
||||
|
||||
// AppDryRunReq application dry-run req
|
||||
type AppDryRunReq struct {
|
||||
AppName string `json:"appName"`
|
||||
DryRunType string `json:"dryRunType"`
|
||||
Env string `json:"env"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// AppDryRunResponse application dry-run result
|
||||
type AppDryRunResponse struct {
|
||||
YAML string `json:"yaml"`
|
||||
}
|
||||
|
||||
// ApplicationStatusResponse application status response body
|
||||
type ApplicationStatusResponse struct {
|
||||
EnvName string `json:"envName"`
|
||||
|
||||
@@ -398,7 +398,7 @@ func (u *defaultAddonHandler) UpdateAddon(ctx context.Context, name string, args
|
||||
|
||||
var app v1beta1.Application
|
||||
// check addon application whether exist
|
||||
err := u.kubeClient.Get(context.Background(), client.ObjectKey{
|
||||
err := u.kubeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: types.DefaultKubeVelaNS,
|
||||
Name: pkgaddon.Convert2AppName(name),
|
||||
}, &app)
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -41,12 +44,16 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/log"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
|
||||
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
utils2 "github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/references/appfile/dryrun"
|
||||
)
|
||||
|
||||
// PolicyType build-in policy type
|
||||
@@ -91,6 +98,9 @@ type ApplicationUsecase interface {
|
||||
DetailRevision(ctx context.Context, appName, revisionName string) (*apisv1.DetailRevisionResponse, error)
|
||||
Statistics(ctx context.Context, app *model.Application) (*apisv1.ApplicationStatisticsResponse, error)
|
||||
ListRecords(ctx context.Context, appName string) (*apisv1.ListWorkflowRecordsResponse, error)
|
||||
CompareAppWithLatestRevision(ctx context.Context, app *model.Application, compareReq apisv1.AppCompareReq) (*apisv1.AppCompareResponse, error)
|
||||
ResetAppToLatestRevision(ctx context.Context, appName string) (*apisv1.AppResetResponse, error)
|
||||
DryRunAppOrRevision(ctx context.Context, app *model.Application, dryRunReq apisv1.AppDryRunReq) (*apisv1.AppDryRunResponse, error)
|
||||
CreateApplicationTrigger(ctx context.Context, app *model.Application, req apisv1.CreateApplicationTriggerRequest) (*apisv1.ApplicationTriggerBase, error)
|
||||
ListApplicationTriggers(ctx context.Context, app *model.Application) ([]*apisv1.ApplicationTriggerBase, error)
|
||||
DeleteApplicationTrigger(ctx context.Context, app *model.Application, triggerName string) error
|
||||
@@ -1348,6 +1358,78 @@ func (c *applicationUsecaseImpl) Statistics(ctx context.Context, app *model.Appl
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CompareAppWithLatestRevision compare application with last revision
|
||||
func (c *applicationUsecaseImpl) CompareAppWithLatestRevision(ctx context.Context, appModel *model.Application, compareReq apisv1.AppCompareReq) (*apisv1.AppCompareResponse, error) {
|
||||
var reqWorkflowName string
|
||||
if compareReq.Env != "" {
|
||||
reqWorkflowName = convertWorkflowName(compareReq.Env)
|
||||
}
|
||||
newApp, err := c.renderOAMApplication(ctx, appModel, reqWorkflowName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ignoreSomeParams(newApp)
|
||||
newAppBytes, err := yaml.Marshal(newApp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldApp, err := c.getAppFromLatestRevision(ctx, appModel.Name, compareReq.Env, "")
|
||||
if err != nil {
|
||||
if errors.Is(err, bcode.ErrApplicationRevisionNotExist) {
|
||||
return &apisv1.AppCompareResponse{IsDiff: false, NewAppYAML: string(newAppBytes)}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ignoreSomeParams(oldApp)
|
||||
oldAppBytes, err := yaml.Marshal(oldApp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
diffResult, buff, err := compare(ctx, newApp, oldApp)
|
||||
if err != nil {
|
||||
return &apisv1.AppCompareResponse{IsDiff: false, NewAppYAML: string(newAppBytes), OldAppYAML: string(oldAppBytes)}, err
|
||||
}
|
||||
return &apisv1.AppCompareResponse{IsDiff: diffResult.DiffType != "", DiffReport: buff.String(), NewAppYAML: string(newAppBytes), OldAppYAML: string(oldAppBytes)}, nil
|
||||
}
|
||||
|
||||
// ResetAppToLatestRevision reset app's component to last revision
|
||||
func (c *applicationUsecaseImpl) ResetAppToLatestRevision(ctx context.Context, appName string) (*apisv1.AppResetResponse, error) {
|
||||
targetApp, err := c.getAppFromLatestRevision(ctx, appName, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.resetApp(ctx, targetApp)
|
||||
}
|
||||
|
||||
// DryRunAppOrRevision dry-run application or revision
|
||||
func (c *applicationUsecaseImpl) DryRunAppOrRevision(ctx context.Context, appModel *model.Application, dryRunReq apisv1.AppDryRunReq) (*apisv1.AppDryRunResponse, error) {
|
||||
var app *v1beta1.Application
|
||||
var err error
|
||||
if dryRunReq.DryRunType == "APP" {
|
||||
var reqWorkflowName string
|
||||
if dryRunReq.Env != "" {
|
||||
reqWorkflowName = convertWorkflowName(dryRunReq.Env)
|
||||
}
|
||||
app, err = c.renderOAMApplication(ctx, appModel, reqWorkflowName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
app, err = c.getAppFromLatestRevision(ctx, dryRunReq.AppName, dryRunReq.Env, dryRunReq.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dryRunResult, err := dryRunApplication(ctx, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apisv1.AppDryRunResponse{YAML: dryRunResult.String()}, nil
|
||||
}
|
||||
|
||||
func (c *applicationUsecaseImpl) createTargetClusterEnv(ctx context.Context, envBind *model.EnvBinding, env *model.Env, target *model.Target, components []*model.ApplicationComponent) v1alpha1.EnvConfig {
|
||||
placement := v1alpha1.EnvPlacement{}
|
||||
if target.Cluster != nil {
|
||||
@@ -1419,3 +1501,234 @@ func genWebhookToken() string {
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func convertToModelComponent(appPrimaryKey string, component common.ApplicationComponent) (model.ApplicationComponent, error) {
|
||||
bc := model.ApplicationComponent{
|
||||
AppPrimaryKey: appPrimaryKey,
|
||||
Name: component.Name,
|
||||
Type: component.Type,
|
||||
ExternalRevision: component.ExternalRevision,
|
||||
DependsOn: component.DependsOn,
|
||||
Inputs: component.Inputs,
|
||||
Outputs: component.Outputs,
|
||||
Scopes: component.Scopes,
|
||||
}
|
||||
if component.Properties != nil {
|
||||
properties, err := model.NewJSONStruct(component.Properties)
|
||||
if err != nil {
|
||||
return bc, err
|
||||
}
|
||||
bc.Properties = properties
|
||||
}
|
||||
for _, trait := range component.Traits {
|
||||
properties, err := model.NewJSONStruct(trait.Properties)
|
||||
if err != nil {
|
||||
return bc, err
|
||||
}
|
||||
bc.Traits = append(bc.Traits, model.ApplicationTrait{CreateTime: time.Now(), UpdateTime: time.Now(), Properties: properties, Type: trait.Type, Alias: trait.Type, Description: "auto gen"})
|
||||
}
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func (c *applicationUsecaseImpl) getAppFromLatestRevision(ctx context.Context, appName string, envName string, version string) (*v1beta1.Application, error) {
|
||||
|
||||
ar := &model.ApplicationRevision{AppPrimaryKey: appName}
|
||||
if envName != "" {
|
||||
ar.EnvName = envName
|
||||
}
|
||||
if version != "" {
|
||||
ar.Version = version
|
||||
}
|
||||
revisions, err := c.ds.List(ctx, ar, &datastore.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
SortBy: []datastore.SortOption{{Key: "createTime", Order: datastore.SortOrderDescending}},
|
||||
})
|
||||
if err != nil || len(revisions) == 0 {
|
||||
return nil, bcode.ErrApplicationRevisionNotExist
|
||||
}
|
||||
latestRevisionRaw := revisions[0]
|
||||
latestRevision, ok := latestRevisionRaw.(*model.ApplicationRevision)
|
||||
if !ok {
|
||||
return nil, errors.New("convert application revision error")
|
||||
}
|
||||
oldApp := &v1beta1.Application{}
|
||||
if err := yaml.Unmarshal([]byte(latestRevision.ApplyAppConfig), oldApp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return oldApp, nil
|
||||
}
|
||||
|
||||
func (c *applicationUsecaseImpl) resetApp(ctx context.Context, targetApp *v1beta1.Application) (*apisv1.AppResetResponse, error) {
|
||||
appPrimaryKey := targetApp.Name
|
||||
|
||||
originComps, err := c.ds.List(ctx, &model.ApplicationComponent{AppPrimaryKey: appPrimaryKey}, &datastore.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, bcode.ErrApplicationComponetNotExist
|
||||
}
|
||||
|
||||
var originCompNames []string
|
||||
for _, entity := range originComps {
|
||||
comp := entity.(*model.ApplicationComponent)
|
||||
originCompNames = append(originCompNames, comp.Name)
|
||||
}
|
||||
|
||||
var targetCompNames []string
|
||||
targetComps := targetApp.Spec.Components
|
||||
for _, comp := range targetComps {
|
||||
targetCompNames = append(targetCompNames, comp.Name)
|
||||
}
|
||||
|
||||
readyToUpdate, readyToDelete, readyToAdd := compareSlices(originCompNames, targetCompNames)
|
||||
|
||||
// delete new app's components
|
||||
for _, compName := range readyToDelete {
|
||||
var component = model.ApplicationComponent{
|
||||
AppPrimaryKey: appPrimaryKey,
|
||||
Name: compName,
|
||||
}
|
||||
if err := c.ds.Delete(ctx, &component); err != nil {
|
||||
if errors.Is(err, datastore.ErrRecordNotExist) {
|
||||
continue
|
||||
}
|
||||
log.Logger.Warnf("delete app %s comp %s failure %s", appPrimaryKey, compName, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for _, comp := range targetComps {
|
||||
// add or update new app's components from old app
|
||||
if utils.StringsContain(readyToAdd, comp.Name) || utils.StringsContain(readyToUpdate, comp.Name) {
|
||||
compModel, err := convertToModelComponent(appPrimaryKey, comp)
|
||||
if err != nil {
|
||||
return &apisv1.AppResetResponse{}, bcode.ErrInvalidProperties
|
||||
}
|
||||
properties, err := model.NewJSONStruct(comp.Properties)
|
||||
if err != nil {
|
||||
return &apisv1.AppResetResponse{}, bcode.ErrInvalidProperties
|
||||
}
|
||||
compModel.Properties = properties
|
||||
if err := c.ds.Add(ctx, &compModel); err != nil {
|
||||
if errors.Is(err, datastore.ErrRecordExist) {
|
||||
err := c.ds.Put(ctx, &compModel)
|
||||
if err != nil {
|
||||
log.Logger.Warnf("update comp %s for app %s failure %s", comp.Name, utils2.Sanitize(appPrimaryKey), err.Error())
|
||||
}
|
||||
return &apisv1.AppResetResponse{IsReset: true}, err
|
||||
}
|
||||
log.Logger.Warnf("add comp %s for app %s failure %s", comp.Name, utils2.Sanitize(appPrimaryKey), err.Error())
|
||||
return &apisv1.AppResetResponse{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return &apisv1.AppResetResponse{IsReset: true}, nil
|
||||
}
|
||||
|
||||
func dryRunApplication(ctx context.Context, app *v1beta1.Application) (bytes.Buffer, error) {
|
||||
c := common2.Args{
|
||||
Schema: common2.Scheme,
|
||||
}
|
||||
var buff = bytes.Buffer{}
|
||||
newClient, err := c.GetClient()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
var objs []oam.Object
|
||||
pd, err := c.GetPackageDiscover()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
dm, err := discoverymapper.New(config)
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
dryRunOpt := dryrun.NewDryRunOption(newClient, dm, pd, objs)
|
||||
comps, err := dryRunOpt.ExecuteDryRun(ctx, app)
|
||||
if err != nil {
|
||||
return buff, errors.New("generate OAM objects")
|
||||
}
|
||||
var components = make(map[string]*unstructured.Unstructured)
|
||||
for _, comp := range comps {
|
||||
components[comp.Name] = comp.StandardWorkload
|
||||
}
|
||||
buff.Write([]byte(fmt.Sprintf("---\n# Application(%s) \n---\n\n", app.Name)))
|
||||
result, err := yaml.Marshal(app)
|
||||
if err != nil {
|
||||
return buff, errors.New("marshal app error")
|
||||
}
|
||||
buff.Write(result)
|
||||
buff.Write([]byte("\n---\n"))
|
||||
for _, c := range comps {
|
||||
buff.Write([]byte(fmt.Sprintf("---\n# Application(%s) -- Component(%s) \n---\n\n", app.Name, c.Name)))
|
||||
result, err := yaml.Marshal(components[c.Name])
|
||||
if err != nil {
|
||||
return buff, errors.New("marshal result for component " + c.Name + " object in yaml format")
|
||||
}
|
||||
buff.Write(result)
|
||||
buff.Write([]byte("\n---\n"))
|
||||
for _, t := range c.Traits {
|
||||
result, err := yaml.Marshal(t)
|
||||
if err != nil {
|
||||
return buff, errors.New("marshal result for component " + c.Name + " object in yaml format")
|
||||
}
|
||||
buff.Write(result)
|
||||
buff.Write([]byte("\n---\n"))
|
||||
}
|
||||
buff.Write([]byte("\n"))
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func ignoreSomeParams(o *v1beta1.Application) {
|
||||
// set default
|
||||
o.ResourceVersion = ""
|
||||
o.Spec.Workflow = nil
|
||||
newAnnotations := map[string]string{}
|
||||
annotations := o.GetAnnotations()
|
||||
for k, v := range annotations {
|
||||
if k == oam.AnnotationDeployVersion || k == oam.AnnotationPublishVersion || k == "kubectl.kubernetes.io/last-applied-configuration" {
|
||||
continue
|
||||
}
|
||||
newAnnotations[k] = v
|
||||
}
|
||||
o.SetAnnotations(newAnnotations)
|
||||
}
|
||||
|
||||
func compare(ctx context.Context, newApp *v1beta1.Application, oldApp *v1beta1.Application) (*dryrun.DiffEntry, bytes.Buffer, error) {
|
||||
var buff = bytes.Buffer{}
|
||||
c := common2.Args{
|
||||
Schema: common2.Scheme,
|
||||
}
|
||||
_, err := c.GetClient()
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
pd, err := c.GetPackageDiscover()
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
config, err := c.GetConfig()
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
dm, err := discoverymapper.New(config)
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
var objs []oam.Object
|
||||
client, err := c.GetClient()
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(client, dm, pd, objs)
|
||||
diffResult, err := liveDiffOption.DiffApps(ctx, newApp, oldApp)
|
||||
if err != nil {
|
||||
return nil, buff, err
|
||||
}
|
||||
reportDiffOpt := dryrun.NewReportDiffOption(10, &buff)
|
||||
reportDiffOpt.PrintDiffReport(diffResult)
|
||||
return diffResult, buff, nil
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/datastore"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/model"
|
||||
v1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
@@ -56,7 +57,7 @@ var _ = Describe("Test application usecase function", func() {
|
||||
testProject = "app-project"
|
||||
testApp = "test-app"
|
||||
defaultTarget = "default"
|
||||
namespace1 = "app-test1"
|
||||
namespace1 = "app-test2"
|
||||
envnsdev = "envnsdev"
|
||||
envnstest = "envnstest"
|
||||
)
|
||||
@@ -67,7 +68,7 @@ var _ = Describe("Test application usecase function", func() {
|
||||
Expect(err).Should(BeNil())
|
||||
envUsecase = &envUsecaseImpl{ds: ds, kubeClient: k8sClient}
|
||||
workflowUsecase = &workflowUsecaseImpl{ds: ds, envUsecase: envUsecase}
|
||||
definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient}
|
||||
definitionUsecase = &definitionUsecaseImpl{kubeClient: k8sClient, caches: make(map[string]*utils.MemoryCache)}
|
||||
envBindingUsecase = &envBindingUsecaseImpl{ds: ds, envUsecase: envUsecase, workflowUsecase: workflowUsecase, kubeClient: k8sClient, definitionUsecase: definitionUsecase}
|
||||
targetUsecase = &targetUsecaseImpl{ds: ds, k8sClient: k8sClient}
|
||||
projectUsecase = &projectUsecaseImpl{ds: ds, k8sClient: k8sClient}
|
||||
@@ -82,7 +83,6 @@ var _ = Describe("Test application usecase function", func() {
|
||||
targetUsecase: targetUsecase,
|
||||
projectUsecase: projectUsecase,
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
It("Test CreateApplication function", func() {
|
||||
@@ -506,6 +506,119 @@ var _ = Describe("Test application usecase function", func() {
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test CompareAppWithLatestRevision function", func() {
|
||||
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = appUsecase.Deploy(context.TODO(), appModel, v1.ApplicationDeployRequest{WorkflowName: convertWorkflowName("app-dev")})
|
||||
Expect(err).Should(BeNil())
|
||||
component, err := appUsecase.GetApplicationComponent(context.TODO(), appModel, "component-name")
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
By("compare when app not change, should return false")
|
||||
compareResponse, err := appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, false)).Should(BeEmpty())
|
||||
|
||||
By("compare when app not change and env not empty, should return false")
|
||||
compareResponse, err = appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{Env: "app-dev"})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, false)).Should(BeEmpty())
|
||||
|
||||
By("compare when app add env, not change, should return false")
|
||||
_, err = envUsecase.CreateEnv(context.TODO(), v1.CreateEnvRequest{Name: "app-prod", Namespace: "envnsprod", Targets: []string{defaultTarget}, Project: "app-prod"})
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = envBindingUsecase.CreateEnvBinding(context.TODO(), appModel, v1.CreateApplicationEnvbindingRequest{EnvBinding: v1.EnvBinding{Name: "app-prod"}})
|
||||
Expect(err).Should(BeNil())
|
||||
compareResponse, err = appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, false)).Should(BeEmpty())
|
||||
|
||||
By("compare when app's env add target, should return true")
|
||||
_, err = targetUsecase.CreateTarget(context.TODO(), v1.CreateTargetRequest{Name: "dev-target1", Cluster: &v1.ClusterTarget{ClusterName: "local", Namespace: "dev-target1"}})
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = envUsecase.UpdateEnv(context.TODO(), "app-dev",
|
||||
v1.UpdateEnvRequest{
|
||||
Description: "this is a env description update",
|
||||
Targets: []string{defaultTarget, "dev-target1"},
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
compareResponse, err = appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, true)).Should(BeEmpty())
|
||||
|
||||
By("compare when update app's trait, should return true")
|
||||
// reset app config
|
||||
_, err = appUsecase.ResetAppToLatestRevision(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
_, err = appUsecase.UpdateApplicationTrait(context.TODO(), appModel, &model.ApplicationComponent{Name: "component-name"}, "scaler", v1.UpdateApplicationTraitRequest{
|
||||
Properties: `{"replicas":2}`,
|
||||
Alias: "alias",
|
||||
Description: "description",
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
compareResponse, err = appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, true)).Should(BeEmpty())
|
||||
|
||||
By("compare when update component's target after app deployed ,should return ture")
|
||||
// reset app config
|
||||
_, err = appUsecase.ResetAppToLatestRevision(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
newProperties := "{\"exposeType\":\"NodePort\",\"image\":\"nginx\",\"imagePullPolicy\":\"Always\"}"
|
||||
_, err = appUsecase.UpdateComponent(context.TODO(),
|
||||
appModel,
|
||||
component,
|
||||
v1.UpdateApplicationComponentRequest{
|
||||
Properties: &newProperties,
|
||||
})
|
||||
Expect(err).Should(BeNil())
|
||||
compareResponse, err = appUsecase.CompareAppWithLatestRevision(context.TODO(), appModel, v1.AppCompareReq{})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(compareResponse.IsDiff, true)).Should(BeEmpty())
|
||||
err = envBindingUsecase.ApplicationEnvRecycle(context.TODO(), &model.Application{Name: testApp}, &model.EnvBinding{Name: "app-dev"})
|
||||
Expect(err).Should(BeNil())
|
||||
})
|
||||
|
||||
It("Test ResetAppToLatestRevision function", func() {
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
resetResponse, err := appUsecase.ResetAppToLatestRevision(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(cmp.Diff(resetResponse.IsReset, true)).Should(BeEmpty())
|
||||
component, err := appUsecase.GetApplicationComponent(context.TODO(), appModel, "component-name")
|
||||
Expect(err).Should(BeNil())
|
||||
expectProperties := "{\"image\":\"nginx\"}"
|
||||
Expect(cmp.Diff(component.Properties.JSON(), expectProperties)).Should(BeEmpty())
|
||||
})
|
||||
|
||||
It("Test DryRun with app function", func() {
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
resetResponse, err := appUsecase.DryRunAppOrRevision(context.TODO(), appModel, v1.AppDryRunReq{DryRunType: "APP"})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app)")).Should(BeTrue())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app) -- Component(component-name)")).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Test DryRun with env revision function", func() {
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
resetResponse, err := appUsecase.DryRunAppOrRevision(context.TODO(), appModel, v1.AppDryRunReq{DryRunType: "Revision", Env: "app-dev"})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app)")).Should(BeTrue())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app) -- Component(component-name)")).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Test DryRun with last revision function", func() {
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
resetResponse, err := appUsecase.DryRunAppOrRevision(context.TODO(), appModel, v1.AppDryRunReq{DryRunType: "Revision"})
|
||||
Expect(err).Should(BeNil())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app)")).Should(BeTrue())
|
||||
Expect(strings.Contains(resetResponse.YAML, "# Application(test-app) -- Component(component-name)")).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Test DeleteApplication function", func() {
|
||||
appModel, err := appUsecase.GetApplication(context.TODO(), testApp)
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
+75
-75
@@ -102,6 +102,15 @@
|
||||
label: ReadinessProbe
|
||||
sort: 13
|
||||
subParameters:
|
||||
- description: Minimum consecutive successes for the probe to be considered successful
|
||||
after having failed.
|
||||
jsonKey: successThreshold
|
||||
label: SuccessThreshold
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 1
|
||||
required: true
|
||||
- description: Instructions for assessing container health by probing a TCP socket.
|
||||
Either this attribute or the exec attribute or the httpGet attribute MUST be
|
||||
specified. This attribute is mutually exclusive with both the exec attribute
|
||||
@@ -221,15 +230,6 @@
|
||||
validate:
|
||||
defaultValue: 10
|
||||
required: true
|
||||
- description: Minimum consecutive successes for the probe to be considered successful
|
||||
after having failed.
|
||||
jsonKey: successThreshold
|
||||
label: SuccessThreshold
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 1
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Instructions for assessing whether the container is alive.
|
||||
@@ -237,43 +237,6 @@
|
||||
label: LivenessProbe
|
||||
sort: 15
|
||||
subParameters:
|
||||
- description: Number of seconds after which the probe times out.
|
||||
jsonKey: timeoutSeconds
|
||||
label: TimeoutSeconds
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 1
|
||||
required: true
|
||||
- description: Instructions for assessing container health by executing a command.
|
||||
Either this attribute or the httpGet attribute or the tcpSocket attribute MUST
|
||||
be specified. This attribute is mutually exclusive with both the httpGet attribute
|
||||
and the tcpSocket attribute.
|
||||
jsonKey: exec
|
||||
label: Exec
|
||||
sort: 100
|
||||
subParameters:
|
||||
- description: A command to be executed inside the container to assess its health.
|
||||
Each space delimited token of the command is a separate array element. Commands
|
||||
exiting 0 are considered to be successful probes, whilst all other exit codes
|
||||
are considered failures.
|
||||
jsonKey: command
|
||||
label: Command
|
||||
sort: 100
|
||||
uiType: Strings
|
||||
validate:
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Number of consecutive failures required to determine the container
|
||||
is not alive (liveness probe) or not ready (readiness probe).
|
||||
jsonKey: failureThreshold
|
||||
label: FailureThreshold
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 3
|
||||
required: true
|
||||
- description: Instructions for assessing container health by executing an HTTP
|
||||
GET request. Either this attribute or the exec attribute or the tcpSocket attribute
|
||||
MUST be specified. This attribute is mutually exclusive with both the exec attribute
|
||||
@@ -282,22 +245,6 @@
|
||||
label: HttpGet
|
||||
sort: 100
|
||||
subParameters:
|
||||
- description: The endpoint, relative to the port, to which the HTTP GET request
|
||||
should be directed.
|
||||
jsonKey: path
|
||||
label: Path
|
||||
sort: 100
|
||||
uiType: Input
|
||||
validate:
|
||||
required: true
|
||||
- description: The TCP socket within the container to which the HTTP GET request
|
||||
should be directed.
|
||||
jsonKey: port
|
||||
label: Port
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
required: true
|
||||
- description: ""
|
||||
jsonKey: httpHeaders
|
||||
label: HttpHeaders
|
||||
@@ -319,6 +266,22 @@
|
||||
required: true
|
||||
uiType: Structs
|
||||
validate: {}
|
||||
- description: The endpoint, relative to the port, to which the HTTP GET request
|
||||
should be directed.
|
||||
jsonKey: path
|
||||
label: Path
|
||||
sort: 100
|
||||
uiType: Input
|
||||
validate:
|
||||
required: true
|
||||
- description: The TCP socket within the container to which the HTTP GET request
|
||||
should be directed.
|
||||
jsonKey: port
|
||||
label: Port
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Number of seconds after the container is started before the first
|
||||
@@ -365,14 +328,45 @@
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Number of seconds after which the probe times out.
|
||||
jsonKey: timeoutSeconds
|
||||
label: TimeoutSeconds
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 1
|
||||
required: true
|
||||
- description: Instructions for assessing container health by executing a command.
|
||||
Either this attribute or the httpGet attribute or the tcpSocket attribute MUST
|
||||
be specified. This attribute is mutually exclusive with both the httpGet attribute
|
||||
and the tcpSocket attribute.
|
||||
jsonKey: exec
|
||||
label: Exec
|
||||
sort: 100
|
||||
subParameters:
|
||||
- description: A command to be executed inside the container to assess its health.
|
||||
Each space delimited token of the command is a separate array element. Commands
|
||||
exiting 0 are considered to be successful probes, whilst all other exit codes
|
||||
are considered failures.
|
||||
jsonKey: command
|
||||
label: Command
|
||||
sort: 100
|
||||
uiType: Strings
|
||||
validate:
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Number of consecutive failures required to determine the container
|
||||
is not alive (liveness probe) or not ready (readiness probe).
|
||||
jsonKey: failureThreshold
|
||||
label: FailureThreshold
|
||||
sort: 100
|
||||
uiType: Number
|
||||
validate:
|
||||
defaultValue: 3
|
||||
required: true
|
||||
uiType: Group
|
||||
validate: {}
|
||||
- description: Specify image pull secrets for your service
|
||||
jsonKey: imagePullSecrets
|
||||
label: ImagePullSecrets
|
||||
sort: 100
|
||||
uiType: Strings
|
||||
validate: {}
|
||||
- description: Which port do you want customer traffic sent to
|
||||
disable: true
|
||||
jsonKey: port
|
||||
@@ -388,13 +382,6 @@
|
||||
label: Volumes
|
||||
sort: 100
|
||||
subParameters:
|
||||
- description: ""
|
||||
jsonKey: mountPath
|
||||
label: MountPath
|
||||
sort: 100
|
||||
uiType: Input
|
||||
validate:
|
||||
required: true
|
||||
- description: ""
|
||||
jsonKey: name
|
||||
label: Name
|
||||
@@ -418,6 +405,13 @@
|
||||
- label: EmptyDir
|
||||
value: emptyDir
|
||||
required: true
|
||||
- description: ""
|
||||
jsonKey: mountPath
|
||||
label: MountPath
|
||||
sort: 100
|
||||
uiType: Input
|
||||
validate:
|
||||
required: true
|
||||
uiType: Structs
|
||||
validate: {}
|
||||
- description: If addRevisionLabel is true, the appRevision label will be added to
|
||||
@@ -430,3 +424,9 @@
|
||||
validate:
|
||||
defaultValue: false
|
||||
required: true
|
||||
- description: Specify image pull secrets for your service
|
||||
jsonKey: imagePullSecrets
|
||||
label: ImagePullSecrets
|
||||
sort: 100
|
||||
uiType: Strings
|
||||
validate: {}
|
||||
|
||||
@@ -495,6 +495,33 @@ func (c *applicationWebService) GetWebService() *restful.WebService {
|
||||
Returns(400, "", bcode.Bcode{}).
|
||||
Writes(apis.ListWorkflowRecordsResponse{}))
|
||||
|
||||
ws.Route(ws.POST("/{name}/compare").To(c.compareAppWithLatestRevision).
|
||||
Doc("compare application with env latest revision").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.appCheckFilter).
|
||||
Param(ws.PathParameter("name", "identifier of the application ").DataType("string")).
|
||||
Returns(200, "", apis.ApplicationBase{}).
|
||||
Returns(400, "", bcode.Bcode{}).
|
||||
Writes(apis.AppCompareResponse{}))
|
||||
|
||||
ws.Route(ws.POST("/{name}/reset").To(c.resetAppToLatestRevision).
|
||||
Doc("reset application to latest revision").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.appCheckFilter).
|
||||
Param(ws.PathParameter("name", "identifier of the application ").DataType("string")).
|
||||
Returns(200, "", apis.AppResetResponse{}).
|
||||
Returns(400, "", bcode.Bcode{}).
|
||||
Writes(apis.AppResetResponse{}))
|
||||
|
||||
ws.Route(ws.POST("/{name}/dry-run").To(c.dryRunAppOrRevision).
|
||||
Doc("dry-run application to latest revision").
|
||||
Metadata(restfulspec.KeyOpenAPITags, tags).
|
||||
Filter(c.appCheckFilter).
|
||||
Param(ws.PathParameter("name", "identifier of the application ").DataType("string")).
|
||||
Returns(200, "", apis.AppDryRunResponse{}).
|
||||
Returns(400, "", bcode.Bcode{}).
|
||||
Writes(apis.AppDryRunResponse{}))
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
@@ -1076,3 +1103,68 @@ func (c *applicationWebService) listApplicationRecords(req *restful.Request, res
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *applicationWebService) compareAppWithLatestRevision(req *restful.Request, res *restful.Response) {
|
||||
app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application)
|
||||
// Verify the validity of parameters
|
||||
var compareReq apis.AppCompareReq
|
||||
if err := req.ReadEntity(&compareReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&compareReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
|
||||
base, err := c.applicationUsecase.CompareAppWithLatestRevision(req.Request.Context(), app, compareReq)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(base); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *applicationWebService) resetAppToLatestRevision(req *restful.Request, res *restful.Response) {
|
||||
app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application)
|
||||
|
||||
base, err := c.applicationUsecase.ResetAppToLatestRevision(req.Request.Context(), app.Name)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(base); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (c *applicationWebService) dryRunAppOrRevision(req *restful.Request, res *restful.Response) {
|
||||
app := req.Request.Context().Value(&apis.CtxKeyApplication).(*model.Application)
|
||||
// Verify the validity of parameters
|
||||
var dryRunReq apis.AppDryRunReq
|
||||
if err := req.ReadEntity(&dryRunReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := validate.Struct(&dryRunReq); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if dryRunReq.AppName == "" {
|
||||
dryRunReq.AppName = app.Name
|
||||
}
|
||||
|
||||
base, err := c.applicationUsecase.DryRunAppOrRevision(req.Request.Context(), app, dryRunReq)
|
||||
if err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
if err := res.WriteEntity(base); err != nil {
|
||||
bcode.ReturnError(req, res, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,33 @@ func (l *LiveDiffOption) Diff(ctx context.Context, app *v1beta1.Application, app
|
||||
return diffResult, nil
|
||||
}
|
||||
|
||||
// DiffApps does three phases, dry-run on input app, preparing manifest for diff, and
|
||||
// calculating diff on manifests.
|
||||
func (l *LiveDiffOption) DiffApps(ctx context.Context, app *v1beta1.Application, oldApp *v1beta1.Application) (*DiffEntry, error) {
|
||||
comps, err := l.ExecuteDryRun(ctx, app)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name)
|
||||
}
|
||||
// new refers to the app as input to dry-run
|
||||
newManifest, err := generateManifest(app, comps)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name)
|
||||
}
|
||||
|
||||
oldComps, err := l.ExecuteDryRun(ctx, oldApp)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", oldApp.Name)
|
||||
}
|
||||
// new refers to the app as input to dry-run
|
||||
oldManifest, err := generateManifest(oldApp, oldComps)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", oldApp.Name)
|
||||
}
|
||||
|
||||
diffResult := l.calculateDiff(oldManifest, newManifest)
|
||||
return diffResult, nil
|
||||
}
|
||||
|
||||
// calculateDiff calculate diff between two application and their sub-resources
|
||||
func (l *LiveDiffOption) calculateDiff(oldApp, newApp *manifest) *DiffEntry {
|
||||
emptyManifest := &manifest{}
|
||||
|
||||
Reference in New Issue
Block a user