diff --git a/Dockerfile b/Dockerfile index 4a05932fc..2d6f91c55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile.apiserver b/Dockerfile.apiserver index f9544b087..eb9a86771 100644 --- a/Dockerfile.apiserver +++ b/Dockerfile.apiserver @@ -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 diff --git a/Dockerfile.e2e b/Dockerfile.e2e index 5b8893767..0e642e08e 100644 --- a/Dockerfile.e2e +++ b/Dockerfile.e2e @@ -18,6 +18,7 @@ COPY cmd/ cmd/ COPY apis/ apis/ COPY pkg/ pkg/ COPY version/ version/ +COPY references/ references/ # Build ARG TARGETARCH diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index b122bf60d..ff055d69f 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -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"` diff --git a/pkg/apiserver/rest/usecase/addon.go b/pkg/apiserver/rest/usecase/addon.go index 232e0187e..07984b435 100644 --- a/pkg/apiserver/rest/usecase/addon.go +++ b/pkg/apiserver/rest/usecase/addon.go @@ -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) diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go index ab0baa779..b1a2d4f1c 100644 --- a/pkg/apiserver/rest/usecase/application.go +++ b/pkg/apiserver/rest/usecase/application.go @@ -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 +} diff --git a/pkg/apiserver/rest/usecase/application_test.go b/pkg/apiserver/rest/usecase/application_test.go index a0cee4934..f108d5952 100644 --- a/pkg/apiserver/rest/usecase/application_test.go +++ b/pkg/apiserver/rest/usecase/application_test.go @@ -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()) diff --git a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml index e861fa779..22cf8f1df 100755 --- a/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml +++ b/pkg/apiserver/rest/usecase/testdata/ui-schema.yaml @@ -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: {} diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index 33c02cc3e..c22be06a7 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -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 + } +} diff --git a/references/appfile/dryrun/diff.go b/references/appfile/dryrun/diff.go index 284c794a4..b52992bfd 100644 --- a/references/appfile/dryrun/diff.go +++ b/references/appfile/dryrun/diff.go @@ -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{}