diff --git a/pkg/apiserver/domain/service/oam_application.go b/pkg/apiserver/domain/service/oam_application.go index 3bf955a1b..f932400f1 100644 --- a/pkg/apiserver/domain/service/oam_application.go +++ b/pkg/apiserver/domain/service/oam_application.go @@ -23,10 +23,12 @@ import ( v1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1" + "github.com/oam-dev/kubevela/pkg/utils/common" ) // OAMApplicationService oam_application service @@ -34,6 +36,7 @@ type OAMApplicationService interface { CreateOrUpdateOAMApplication(context.Context, apisv1.ApplicationRequest, string, string) error GetOAMApplication(context.Context, string, string) (*apisv1.ApplicationResponse, error) DeleteOAMApplication(context.Context, string, string) error + DryRunOAMApplication(context.Context, apisv1.ApplicationRequest, string, string) error } // NewOAMApplicationService new oam_application service @@ -43,6 +46,7 @@ func NewOAMApplicationService() OAMApplicationService { type oamApplicationServiceImpl struct { KubeClient client.Client `inject:"kubeClient"` + KubeConfig *rest.Config `inject:"kubeConfig"` } // CreateOrUpdateOAMApplication create or update application @@ -104,3 +108,53 @@ func (o oamApplicationServiceImpl) DeleteOAMApplication(ctx context.Context, nam }, })) } + +// DryRunOAMApplication dryRun create or update application +func (o oamApplicationServiceImpl) DryRunOAMApplication(ctx context.Context, request apisv1.ApplicationRequest, name, namespace string) error { + ns := new(v1.Namespace) + err := o.KubeClient.Get(ctx, client.ObjectKey{Name: namespace}, ns) + if kerrors.IsNotFound(err) { + ns.Name = namespace + if err = o.KubeClient.Create(ctx, ns); err != nil { + return err + } + } + + app := &v1beta1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "core.oam.dev/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: v1beta1.ApplicationSpec{ + Components: request.Components, + Policies: request.Policies, + Workflow: request.Workflow, + }, + } + + args := common.Args{ + Schema: common.Scheme, + } + _ = args.SetConfig(o.KubeConfig) + args.SetClient(o.KubeClient) + _, err = dryRunApplication(ctx, args, app) + if err != nil { + return err + } + + existApp := new(v1beta1.Application) + err = o.KubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, existApp) + if err != nil { + if kerrors.IsNotFound(err) { + return o.KubeClient.Create(ctx, app, client.DryRunAll) + } + return err + } + + existApp.Spec = app.Spec + return o.KubeClient.Update(ctx, existApp, client.DryRunAll) +} diff --git a/pkg/apiserver/domain/service/oam_application_test.go b/pkg/apiserver/domain/service/oam_application_test.go index 533839c79..91bdec347 100644 --- a/pkg/apiserver/domain/service/oam_application_test.go +++ b/pkg/apiserver/domain/service/oam_application_test.go @@ -47,6 +47,7 @@ var _ = Describe("Test oam application service function", func() { ns = corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} oamAppService = &oamApplicationServiceImpl{ KubeClient: k8sClient, + KubeConfig: cfg, } Expect(common.ReadYamlToObject("./testdata/example-app.yaml", &baseApp)).Should(BeNil()) @@ -117,4 +118,30 @@ var _ = Describe("Test oam application service function", func() { err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: baseApp.Name}, app) Expect(kerrors.IsNotFound(err)).Should(BeTrue()) }) + + It("Test DryRunOAMApplication function", func() { + By("test dryRun create application") + appName := "test-new-app" + appNs := randomNamespaceName("test-new-app") + var newApp v1beta1.Application + Expect(common.ReadYamlToObject("./testdata/dryrun-app.yaml", &newApp)).Should(BeNil()) + newApp.SetNamespace(namespace) + Eventually(func() error { + return k8sClient.Create(ctx, &newApp) + }, time.Second*3, time.Microsecond*300).Should(SatisfyAny(BeNil(), &util.AlreadyExistMatcher{})) + + req := apiv1.ApplicationRequest{ + Components: newApp.Spec.Components, + Policies: newApp.Spec.Policies, + Workflow: newApp.Spec.Workflow, + } + Expect(oamAppService.DryRunOAMApplication(ctx, req, appName, appNs)).Should(BeNil()) + + By("test dryRun update application") + updateReq := apiv1.ApplicationRequest{ + Components: newApp.Spec.Components[1:], + } + Expect(oamAppService.DryRunOAMApplication(ctx, updateReq, appName, appNs)).Should(BeNil()) + + }) }) diff --git a/pkg/apiserver/domain/service/testdata/dryrun-app.yaml b/pkg/apiserver/domain/service/testdata/dryrun-app.yaml new file mode 100644 index 000000000..f03548fe7 --- /dev/null +++ b/pkg/apiserver/domain/service/testdata/dryrun-app.yaml @@ -0,0 +1,16 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: app-dryrun + namespace: default +spec: + components: + - name: hello-world + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1 diff --git a/pkg/apiserver/interfaces/api/oam_application.go b/pkg/apiserver/interfaces/api/oam_application.go index 9bc672a01..53927bb98 100644 --- a/pkg/apiserver/interfaces/api/oam_application.go +++ b/pkg/apiserver/interfaces/api/oam_application.go @@ -60,6 +60,9 @@ func (c *oamApplicationAPIInterface) GetWebServiceRoute() *restful.WebService { Filter(c.RbacService.CheckPerm("application", "deploy")). Param(ws.PathParameter("namespace", "identifier of the namespace").DataType("string")). Param(ws.PathParameter("appname", "identifier of the oam application").DataType("string")). + Param(ws.QueryParameter("dryRun", "When present, indicates that modifications should not be persisted. "+ + "An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. "+ + "Valid values are: - All: all dry run stages will be processed").DataType("string").Required(false)). Reads(apis.ApplicationRequest{})) ws.Route(ws.DELETE("/namespaces/{namespace}/applications/{appname}").To(c.deleteApplication). @@ -101,11 +104,23 @@ func (c *oamApplicationAPIInterface) createOrUpdateApplication(req *restful.Requ return } - err := c.OamApplicationService.CreateOrUpdateOAMApplication(req.Request.Context(), createReq, appName, namespace) - if err != nil { - log.Logger.Errorf("create application failure %s", err.Error()) - bcode.ReturnError(req, res, err) - return + dryRun := req.QueryParameter("dryRun") + if len(dryRun) != 0 { + if dryRun != "All" { + bcode.ReturnError(req, res, bcode.ErrApplicationDryRunFailed.SetMessage("Invalid dryRun parameter. Must be 'All'")) + return + } + if err := c.OamApplicationService.DryRunOAMApplication(req.Request.Context(), createReq, appName, namespace); err != nil { + log.Logger.Errorf("dryrun application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } + } else { + if err := c.OamApplicationService.CreateOrUpdateOAMApplication(req.Request.Context(), createReq, appName, namespace); err != nil { + log.Logger.Errorf("create application failure %s", err.Error()) + bcode.ReturnError(req, res, err) + return + } } if err := res.WriteEntity(apis.EmptyResponse{}); err != nil { diff --git a/test/e2e-apiserver-test/oam_application_test.go b/test/e2e-apiserver-test/oam_application_test.go index e08b2a002..64e32d7b7 100644 --- a/test/e2e-apiserver-test/oam_application_test.go +++ b/test/e2e-apiserver-test/oam_application_test.go @@ -97,4 +97,33 @@ var _ = Describe("Test oam application rest api", func() { Expect(res).ShouldNot(BeNil()) Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) }) + + It("Test dryRun create and update oam app", func() { + defer GinkgoRecover() + By("test dryRun create app") + + Expect(common.ReadYamlToObject("./testdata/dryrun-app.yaml", &app)).Should(BeNil()) + req := apiv1.ApplicationRequest{ + Components: app.Spec.Components, + Policies: app.Spec.Policies, + Workflow: app.Spec.Workflow, + } + res := post(fmt.Sprintf("/v1/namespaces/%s/applications/%s?dryRun=All", namespace, appName), req) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + + By("test dryRun update app") + updateReq := apiv1.ApplicationRequest{ + Components: app.Spec.Components[1:], + } + Eventually(func(g Gomega) { + res = post(fmt.Sprintf("/v1/namespaces/%s/applications/%s?dryRun=All", namespace, appName), updateReq) + g.Expect(res).ShouldNot(BeNil()) + g.Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + g.Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + }, time.Minute).Should(Succeed()) + }) }) diff --git a/test/e2e-apiserver-test/testdata/dryrun-app.yaml b/test/e2e-apiserver-test/testdata/dryrun-app.yaml new file mode 100644 index 000000000..f03548fe7 --- /dev/null +++ b/test/e2e-apiserver-test/testdata/dryrun-app.yaml @@ -0,0 +1,16 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: app-dryrun + namespace: default +spec: + components: + - name: hello-world + type: webservice + properties: + image: crccheck/hello-world + port: 8000 + traits: + - type: scaler + properties: + replicas: 1