Repository: kubesphere/ks-devops
Pull request #466 (2022-02-14 07:52:13)

Add argo cd application support (controller part)
<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubesphere/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubesphere/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding conventions followed by the KubeSphere community: https://github.com/kubesphere/community/blob/master/developer-guide/development/coding-conventions.md
4. Additional open-source best practice: https://github.com/LinuxSuRen/open-source-best-practice
-->

### What type of PR is this?
<!-- 
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind design
/kind chore

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind feature

### What this PR does / why we need it:
In this PR, I create the CRD (application.devops.kubesphere.io). The CRD responsible to maintain a mapping between the `application.devops.kubesphere.io` and `application.argoproj.io`.

This is part of #63 

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
Please leave it or change # to be None if there is no corresponding issue that exists
-->
Fixes #

### Special notes for reviewers:
<!--
You can use the following command to let the DevOps SIG members help you to review your PR.
/cc @kubesphere/sig-devops 
And please avoid cc any individual.
-->
Feel free to help me to test it with the following image:
```
surenpi/devops-controller:dev-v3.2.1-4279b12
```

### Does this PR introduce a user-facing change??
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended-release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

Please keep the note be same as your PR title if you believe it should be in the release notes.
-->
```release-note
None
```


ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/466#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#466](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (0d2545c) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/b8ae208f0ca0b4a491b69bb34fc61e62a75c9a9a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (b8ae208) will **increase** coverage by `0.03%`.
> The diff coverage is `68.25%`.

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/466/graphs/tree.svg?width=650&height=150&src=pr&token=qSK4RBbyNg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff             @@
##           master     #466      +/-   ##
==========================================
+ Coverage   76.78%   76.81%   +0.03%     
==========================================
  Files         123      125       +2     
  Lines       22385    22439      +54     
==========================================
+ Hits        17188    17237      +49     
- Misses       5011     5016       +5     
  Partials      186      186              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `76.81% <68.25%> (+0.03%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [controllers/argocd/application\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvYXJnb2NkL2FwcGxpY2F0aW9uX2NvbnRyb2xsZXIuZ28=) | `62.26% <62.26%> (ø)` | |
| [controllers/argocd/project\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvYXJnb2NkL3Byb2plY3RfY29udHJvbGxlci5nbw==) | `72.72% <100.00%> (-2.28%)` | :arrow_down: |
| [pkg/api/devops/v1alpha1/application.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTEvYXBwbGljYXRpb24uZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/utils/k8sutil/k8sutil.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL3V0aWxzL2s4c3V0aWwvazhzdXRpbC5nbw==) | `100.00% <100.00%> (ø)` | |
| [.../jenkins/devopsproject/devopsproject\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9kZXZvcHNwcm9qZWN0L2Rldm9wc3Byb2plY3RfY29udHJvbGxlci5nbw==) | `43.22% <0.00%> (+2.60%)` | :arrow_up: |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpcnVuL3MyaXJ1bl9jb250cm9sbGVyLmdv) | `37.22% <0.00%> (+3.64%)` | :arrow_up: |
| [...ontrollers/jenkins/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/466/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZS9waXBlbGluZV9jb250cm9sbGVyLmdv) | `43.18% <0.00%> (+3.78%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [b8ae208...0d2545c](https://codecov.io/gh/kubesphere/ks-devops/pull/466?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).


Patch:
diff --git a/cmd/controller/app/controllers.go b/cmd/controller/app/controllers.go
index 94a4930..aae8fef 100644
--- a/cmd/controller/app/controllers.go
+++ b/cmd/controller/app/controllers.go
@@ -102,6 +102,9 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 	argocdReconciler := &argocd.Reconciler{
 		Client: mgr.GetClient(),
 	}
+	argocdAppReconciler := &argocd.ApplicationReconciler{
+		Client: mgr.GetClient(),
+	}
 
 	return map[string]func(mgr manager.Manager) error{
 		"gitrepository": func(mgr manager.Manager) error {
@@ -173,7 +176,10 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 				kubesphereInformer.Devops().V1alpha1().S2iRuns()))
 		},
 		argocdReconciler.GetGroupName(): func(mgr manager.Manager) error {
-			return argocdReconciler.SetupWithManager(mgr)
+			if err := argocdReconciler.SetupWithManager(mgr); err != nil {
+				return err
+			}
+			return argocdAppReconciler.SetupWithManager(mgr)
 		},
 	}
 }
diff --git a/config/crd/bases/devops.kubesphere.io_applications.yaml b/config/crd/bases/devops.kubesphere.io_applications.yaml
new file mode 100644
index 0000000..41d5513
--- /dev/null
+++ b/config/crd/bases/devops.kubesphere.io_applications.yaml
@@ -0,0 +1,453 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.6.2
+  creationTimestamp: null
+  name: applications.devops.kubesphere.io
+spec:
+  group: devops.kubesphere.io
+  names:
+    kind: Application
+    listKind: ApplicationList
+    plural: applications
+    singular: application
+  scope: Namespaced
+  versions:
+  - name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: Application represents an application the DevOps system
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: ApplicationSpec is the specification of the Application
+            properties:
+              argoApp:
+                description: ArgoApplication represents an ArgoCD Application The
+                  fields simply are copied from the argo-cd project
+                properties:
+                  destination:
+                    description: Destination is a reference to the target Kubernetes
+                      server and namespace
+                    properties:
+                      name:
+                        description: Name is an alternate way of specifying the target
+                          cluster by its symbolic name
+                        type: string
+                      namespace:
+                        description: Namespace specifies the target namespace for
+                          the application's resources. The namespace will only be
+                          set for namespace-scoped resources that have not set a value
+                          for .metadata.namespace
+                        type: string
+                      server:
+                        description: Server specifies the URL of the target cluster
+                          and must be set to the Kubernetes control plane API
+                        type: string
+                    type: object
+                  ignoreDifferences:
+                    description: IgnoreDifferences is a list of resources and their
+                      fields which should be ignored during comparison
+                    items:
+                      description: ResourceIgnoreDifferences contains resource filter
+                        and list of json paths which should be ignored during comparison
+                        with live state.
+                      properties:
+                        group:
+                          type: string
+                        jqPathExpressions:
+                          items:
+                            type: string
+                          type: array
+                        jsonPointers:
+                          items:
+                            type: string
+                          type: array
+                        kind:
+                          type: string
+                        managedFieldsManagers:
+                          description: ManagedFieldsManagers is a list of trusted
+                            managers. Fields mutated by those managers will take precedence
+                            over the desired state defined in the SCM and won't be
+                            displayed in diffs
+                          items:
+                            type: string
+                          type: array
+                        name:
+                          type: string
+                        namespace:
+                          type: string
+                      required:
+                      - kind
+                      type: object
+                    type: array
+                  info:
+                    description: Info contains a list of information (URLs, email
+                      addresses, and plain text) that relates to the application
+                    items:
+                      description: Info represents a name and value
+                      properties:
+                        name:
+                          type: string
+                        value:
+                          type: string
+                      required:
+                      - name
+                      - value
+                      type: object
+                    type: array
+                  project:
+                    description: Project is a reference to the project this application
+                      belongs to. The empty string means that application belongs
+                      to the 'default' project.
+                    type: string
+                  revisionHistoryLimit:
+                    description: RevisionHistoryLimit limits the number of items kept
+                      in the application's revision history, which is used for informational
+                      purposes as well as for rollbacks to previous versions. This
+                      should only be changed in exceptional circumstances. Setting
+                      to zero will store no history. This will reduce storage used.
+                      Increasing will increase the space used to store the history,
+                      so we do not recommend increasing it. Default is 10.
+                    format: int64
+                    type: integer
+                  source:
+                    description: Source is a reference to the location of the application's
+                      manifests or chart
+                    properties:
+                      chart:
+                        description: Chart is a Helm chart name, and must be specified
+                          for applications sourced from a Helm repo.
+                        type: string
+                      directory:
+                        description: Directory holds path/directory specific options
+                        properties:
+                          exclude:
+                            description: Exclude contains a glob pattern to match
+                              paths against that should be explicitly excluded from
+                              being used during manifest generation
+                            type: string
+                          include:
+                            description: Include contains a glob pattern to match
+                              paths against that should be explicitly included during
+                              manifest generation
+                            type: string
+                          jsonnet:
+                            description: Jsonnet holds options specific to Jsonnet
+                            properties:
+                              extVars:
+                                description: ExtVars is a list of Jsonnet External
+                                  Variables
+                                items:
+                                  description: JsonnetVar represents a variable to
+                                    be passed to jsonnet during manifest generation
+                                  properties:
+                                    code:
+                                      type: boolean
+                                    name:
+                                      type: string
+                                    value:
+                                      type: string
+                                  required:
+                                  - name
+                                  - value
+                                  type: object
+                                type: array
+                              libs:
+                                description: Additional library search dirs
+                                items:
+                                  type: string
+                                type: array
+                              tlas:
+                                description: TLAS is a list of Jsonnet Top-level Arguments
+                                items:
+                                  description: JsonnetVar represents a variable to
+                                    be passed to jsonnet during manifest generation
+                                  properties:
+                                    code:
+                                      type: boolean
+                                    name:
+                                      type: string
+                                    value:
+                                      type: string
+                                  required:
+                                  - name
+                                  - value
+                                  type: object
+                                type: array
+                            type: object
+                          recurse:
+                            description: Recurse specifies whether to scan a directory
+                              recursively for manifests
+                            type: boolean
+                        type: object
+                      helm:
+                        description: Helm holds helm specific options
+                        properties:
+                          fileParameters:
+                            description: FileParameters are file parameters to the
+                              helm template
+                            items:
+                              description: HelmFileParameter is a file parameter that's
+                                passed to helm template during manifest generation
+                              properties:
+                                name:
+                                  description: Name is the name of the Helm parameter
+                                  type: string
+                                path:
+                                  description: Path is the path to the file containing
+                                    the values for the Helm parameter
+                                  type: string
+                              type: object
+                            type: array
+                          ignoreMissingValueFiles:
+                            description: IgnoreMissingValueFiles prevents helm template
+                              from failing when valueFiles do not exist locally by
+                              not appending them to helm template --values
+                            type: boolean
+                          parameters:
+                            description: Parameters is a list of Helm parameters which
+                              are passed to the helm template command upon manifest
+                              generation
+                            items:
+                              description: HelmParameter is a parameter that's passed
+                                to helm template during manifest generation
+                              properties:
+                                forceString:
+                                  description: ForceString determines whether to tell
+                                    Helm to interpret booleans and numbers as strings
+                                  type: boolean
+                                name:
+                                  description: Name is the name of the Helm parameter
+                                  type: string
+                                value:
+                                  description: Value is the value for the Helm parameter
+                                  type: string
+                              type: object
+                            type: array
+                          passCredentials:
+                            description: PassCredentials pass credentials to all domains
+                              (Helm's --pass-credentials)
+                            type: boolean
+                          releaseName:
+                            description: ReleaseName is the Helm release name to use.
+                              If omitted it will use the application name
+                            type: string
+                          skipCrds:
+                            description: SkipCrds skips custom resource definition
+                              installation step (Helm's --skip-crds)
+                            type: boolean
+                          valueFiles:
+                            description: ValuesFiles is a list of Helm value files
+                              to use when generating a template
+                            items:
+                              type: string
+                            type: array
+                          values:
+                            description: Values specifies Helm values to be passed
+                              to helm template, typically defined as a block
+                            type: string
+                          version:
+                            description: Version is the Helm version to use for templating
+                              (either "2" or "3")
+                            type: string
+                        type: object
+                      ksonnet:
+                        description: Ksonnet holds ksonnet specific options
+                        properties:
+                          environment:
+                            description: Environment is a ksonnet application environment
+                              name
+                            type: string
+                          parameters:
+                            description: Parameters are a list of ksonnet component
+                              parameter override values
+                            items:
+                              description: KsonnetParameter is a ksonnet component
+                                parameter
+                              properties:
+                                component:
+                                  type: string
+                                name:
+                                  type: string
+                                value:
+                                  type: string
+                              required:
+                              - name
+                              - value
+                              type: object
+                            type: array
+                        type: object
+                      kustomize:
+                        description: Kustomize holds kustomize specific options
+                        properties:
+                          commonAnnotations:
+                            additionalProperties:
+                              type: string
+                            description: CommonAnnotations is a list of additional
+                              annotations to add to rendered manifests
+                            type: object
+                          commonLabels:
+                            additionalProperties:
+                              type: string
+                            description: CommonLabels is a list of additional labels
+                              to add to rendered manifests
+                            type: object
+                          forceCommonAnnotations:
+                            description: ForceCommonAnnotations specifies whether
+                              to force applying common annotations to resources for
+                              Kustomize apps
+                            type: boolean
+                          forceCommonLabels:
+                            description: ForceCommonLabels specifies whether to force
+                              applying common labels to resources for Kustomize apps
+                            type: boolean
+                          images:
+                            description: Images is a list of Kustomize image override
+                              specifications
+                            items:
+                              description: KustomizeImage represents a Kustomize image
+                                definition in the format [old_image_name=]<image_name>:<image_tag>
+                              type: string
+                            type: array
+                          namePrefix:
+                            description: NamePrefix is a prefix appended to resources
+                              for Kustomize apps
+                            type: string
+                          nameSuffix:
+                            description: NameSuffix is a suffix appended to resources
+                              for Kustomize apps
+                            type: string
+                          version:
+                            description: Version controls which version of Kustomize
+                              to use for rendering manifests
+                            type: string
+                        type: object
+                      path:
+                        description: Path is a directory path within the Git repository,
+                          and is only valid for applications sourced from Git.
+                        type: string
+                      plugin:
+                        description: ConfigManagementPlugin holds config management
+                          plugin specific options
+                        properties:
+                          env:
+                            description: Env is a list of environment variable entries
+                            items:
+                              description: EnvEntry represents an entry in the application's
+                                environment
+                              properties:
+                                name:
+                                  description: Name is the name of the variable, usually
+                                    expressed in uppercase
+                                  type: string
+                                value:
+                                  description: Value is the value of the variable
+                                  type: string
+                              required:
+                              - name
+                              - value
+                              type: object
+                            type: array
+                          name:
+                            type: string
+                        type: object
+                      repoURL:
+                        description: RepoURL is the URL to the repository (Git or
+                          Helm) that contains the application manifests
+                        type: string
+                      targetRevision:
+                        description: TargetRevision defines the revision of the source
+                          to sync the application to. In case of Git, this can be
+                          commit, tag, or branch. If omitted, will equal to HEAD.
+                          In case of Helm, this is a semver tag for the Chart's version.
+                        type: string
+                    required:
+                    - repoURL
+                    type: object
+                  syncPolicy:
+                    description: SyncPolicy controls when and how a sync will be performed
+                    properties:
+                      automated:
+                        description: Automated will keep an application synced to
+                          the target revision
+                        properties:
+                          allowEmpty:
+                            description: 'AllowEmpty allows apps have zero live resources
+                              (default: false)'
+                            type: boolean
+                          prune:
+                            description: 'Prune specifies whether to delete resources
+                              from the cluster that are not found in the sources anymore
+                              as part of automated sync (default: false)'
+                            type: boolean
+                          selfHeal:
+                            description: 'SelfHeal specifes whether to revert resources
+                              back to their desired state upon modification in the
+                              cluster (default: false)'
+                            type: boolean
+                        type: object
+                      retry:
+                        description: Retry controls failed sync retry behavior
+                        properties:
+                          backoff:
+                            description: Backoff controls how to backoff on subsequent
+                              retries of failed syncs
+                            properties:
+                              duration:
+                                description: Duration is the amount to back off. Default
+                                  unit is seconds, but could also be a duration (e.g.
+                                  "2m", "1h")
+                                type: string
+                              factor:
+                                description: Factor is a factor to multiply the base
+                                  duration after each failed retry
+                                format: int64
+                                type: integer
+                              maxDuration:
+                                description: MaxDuration is the maximum amount of
+                                  time allowed for the backoff strategy
+                                type: string
+                            type: object
+                          limit:
+                            description: Limit is the maximum number of attempts for
+                              retrying a failed sync. If set to 0, no retries will
+                              be performed.
+                            format: int64
+                            type: integer
+                        type: object
+                      syncOptions:
+                        description: Options allow you to specify whole app sync-options
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                required:
+                - destination
+                - project
+                - source
+                type: object
+            type: object
+        type: object
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index ff5ad3c..ca40e9b 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -23,6 +23,15 @@ rules:
   - get
   - list
   - watch
+- apiGroups:
+  - argoproj.io
+  resources:
+  - applications
+  verbs:
+  - create
+  - get
+  - list
+  - update
 - apiGroups:
   - argoproj.io
   resources:
@@ -50,6 +59,14 @@ rules:
   - delete
   - get
   - update
+- apiGroups:
+  - devops.kubesphere.io
+  resources:
+  - applications
+  verbs:
+  - get
+  - list
+  - update
 - apiGroups:
   - devops.kubesphere.io
   resources:
diff --git a/config/samples/devops_v1alpha1_application.yaml b/config/samples/devops_v1alpha1_application.yaml
new file mode 100644
index 0000000..c762f53
--- /dev/null
+++ b/config/samples/devops_v1alpha1_application.yaml
@@ -0,0 +1,19 @@
+apiVersion: devops.kubesphere.io/v1alpha1
+kind: Application
+metadata:
+  name: app
+spec:
+  argoApp:
+    project: default
+    source:
+      repoURL: https://github.com/kubesphere-sigs/ks-versions
+      targetRevision: HEAD
+      path: .
+      directory:
+        recurse: true
+    destination:
+      server: https://kubernetes.default.svc
+      namespace: ks-version
+    syncPolicy:
+      automated:
+        prune: true
diff --git a/config/samples/devops_v1alpha3_fake.yaml b/config/samples/devops_v1alpha3_fake.yaml
deleted file mode 100644
index fe7bccb..0000000
--- a/config/samples/devops_v1alpha3_fake.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-apiVersion: devops.kubesphere.io/v1alpha3
-kind: Fake
-metadata:
-  name: fake-sample
-spec:
-  # Add fields here
-  foo: bar
diff --git a/controllers/argocd/application_controller.go b/controllers/argocd/application_controller.go
new file mode 100644
index 0000000..281b41c
--- /dev/null
+++ b/controllers/argocd/application_controller.go
@@ -0,0 +1,164 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package argocd
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"github.com/go-logr/logr"
+	"html/template"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/utils/k8sutil"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+//+kubebuilder:rbac:groups=devops.kubesphere.io,resources=applications,verbs=get;list;update
+//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list;create;update
+
+// ApplicationReconciler is the reconciler of the Application
+type ApplicationReconciler struct {
+	client.Client
+	log      logr.Logger
+	recorder record.EventRecorder
+}
+
+// Reconcile makes sure the Application and ArgoCD application works well
+func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
+	ctx := context.Background()
+	r.log.Info(fmt.Sprintf("start to reconcile application: %s", req.String()))
+
+	app := &v1alpha1.Application{}
+	if err = r.Client.Get(ctx, req.NamespacedName, app); err != nil {
+		err = client.IgnoreNotFound(err)
+		return
+	}
+
+	if argo := app.Spec.ArgoApp; argo != nil {
+		err = r.reconcileArgoApplication(app)
+	}
+	return
+}
+
+func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Application) (err error) {
+	ctx := context.Background()
+
+	argoApp := &unstructured.Unstructured{}
+	argoApp.SetKind("Application")
+	argoApp.SetAPIVersion("argoproj.io/v1alpha1")
+
+	if err = r.Client.Get(ctx, types.NamespacedName{
+		Namespace: app.GetNamespace(),
+		Name:      app.GetName(),
+	}, argoApp); err != nil {
+		if !apierrors.IsNotFound(err) {
+			return
+		}
+
+		if argoApp, err = createUnstructuredApplication(app); err != nil {
+			return
+		}
+
+		err = r.Client.Create(ctx, argoApp)
+	} else {
+		var newArgoApp *unstructured.Unstructured
+		if newArgoApp, err = createUnstructuredApplication(app); err == nil {
+			argoApp.Object["spec"] = newArgoApp.Object["spec"]
+			k8sutil.AddOwnerReference(argoApp, app.TypeMeta, app.ObjectMeta)
+			err = r.Client.Update(ctx, argoApp)
+		}
+	}
+	return
+}
+
+func createUnstructuredApplication(app *v1alpha1.Application) (result *unstructured.Unstructured, err error) {
+	app = app.DeepCopy()
+	argoApp := app.Spec.ArgoApp
+	if argoApp == nil {
+		err = fmt.Errorf("no argo found from the spec")
+		return
+	}
+
+	var tpl *template.Template
+	if tpl, err = template.New("argo").Parse(argoApplicationTemplate); err != nil {
+		return
+	}
+
+	// TODO set some default values
+	if argoApp.Project == "" {
+		argoApp.Project = "default"
+	}
+
+	buffer := new(bytes.Buffer)
+	if err = tpl.Execute(buffer, argoApp); err == nil {
+		if result, err = GetObjectFromYaml(buffer.String()); err == nil {
+			result.SetName(app.GetName())
+			result.SetNamespace(app.GetNamespace())
+			k8sutil.AddOwnerReference(result, app.TypeMeta, app.ObjectMeta)
+		}
+	}
+	return
+}
+
+const argoApplicationTemplate = `apiVersion: argoproj.io/v1alpha1
+kind: Application
+spec:
+  project: "{{.Project}}"
+  source:
+    repoURL: "{{.Source.RepoURL}}"
+    targetRevision: {{.Source.TargetRevision}}
+    path: "{{.Source.Path}}"
+{{if .Source.Directory }}
+    directory:
+      recurse: {{.Source.Directory.Recurse}}
+{{end}}
+  destination:
+    server: "{{.Destination.Server}}"
+    namespace: "{{.Destination.Namespace}}"
+{{if .SyncPolicy }}
+{{if .SyncPolicy.Automated }}
+  syncPolicy:
+    automated:
+      prune: {{.SyncPolicy.Automated.Prune}}
+{{end}}
+{{end}}
+`
+
+// GetName returns the name of this reconciler
+func (r *ApplicationReconciler) GetName() string {
+	return "ApplicationController"
+}
+
+// GetGroupName returns the group name of this reconciler
+func (r *ApplicationReconciler) GetGroupName() string {
+	return controllerGroupName
+}
+
+// SetupWithManager setups the reconciler with a manager
+// setup the logger, recorder
+func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	r.log = ctrl.Log.WithName(r.GetName())
+	r.recorder = mgr.GetEventRecorderFor(r.GetName())
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&v1alpha1.Application{}).
+		Complete(r)
+}
diff --git a/controllers/argocd/constants.go b/controllers/argocd/constants.go
new file mode 100644
index 0000000..775082a
--- /dev/null
+++ b/controllers/argocd/constants.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package argocd
+
+const controllerGroupName = "argocd"
diff --git a/controllers/argocd/project_controller.go b/controllers/argocd/project_controller.go
index 30f971c..70a9381 100644
--- a/controllers/argocd/project_controller.go
+++ b/controllers/argocd/project_controller.go
@@ -86,7 +86,7 @@ func (r *Reconciler) reconcileArgoProject(project *v1alpha3.DevOpsProject) (err
 		var newProject *unstructured.Unstructured
 		if newProject, err = createUnstructuredObject(project); err == nil {
 			argoAppProject.Object["spec"] = newProject.Object["spec"]
-			setOwnerRef(argoAppProject, project)
+			k8sutil.AddOwnerReference(argoAppProject, project.TypeMeta, project.ObjectMeta)
 			err = r.Client.Update(ctx, argoAppProject)
 		}
 	}
@@ -128,21 +128,12 @@ func createUnstructuredObject(project *v1alpha3.DevOpsProject) (result *unstruct
 		if result, err = GetObjectFromYaml(buffer.String()); err == nil {
 			result.SetName(project.GetName())
 			result.SetNamespace(project.GetName())
-			setOwnerRef(result, project)
+			k8sutil.AddOwnerReference(result, project.TypeMeta, project.ObjectMeta)
 		}
 	}
 	return
 }
 
-func setOwnerRef(object metav1.Object, project *v1alpha3.DevOpsProject) {
-	k8sutil.SetOwnerReference(object, metav1.OwnerReference{
-		Kind:       project.Kind,
-		Name:       project.Name,
-		APIVersion: project.APIVersion,
-		UID:        project.UID,
-	})
-}
-
 const argoProjectTemplate = `apiVersion: argoproj.io/v1alpha1
 kind: AppProject
 spec:
@@ -177,7 +168,7 @@ func (r *Reconciler) GetName() string {
 
 // GetGroupName returns the group name
 func (r *Reconciler) GetGroupName() string {
-	return "argocd"
+	return controllerGroupName
 }
 
 // SetupWithManager setups the reconciler with a manager
diff --git a/pkg/api/devops/v1alpha1/application.go b/pkg/api/devops/v1alpha1/application.go
new file mode 100644
index 0000000..90d8475
--- /dev/null
+++ b/pkg/api/devops/v1alpha1/application.go
@@ -0,0 +1,320 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+// ApplicationSpec is the specification of the Application
+type ApplicationSpec struct {
+	ArgoApp *ArgoApplication `json:"argoApp,omitempty"`
+}
+
+// ArgoApplication represents an ArgoCD Application
+// The fields simply are copied from the argo-cd project
+type ArgoApplication struct {
+	// Source is a reference to the location of the application's manifests or chart
+	Source ApplicationSource `json:"source"`
+	// Destination is a reference to the target Kubernetes server and namespace
+	Destination ApplicationDestination `json:"destination"`
+	// Project is a reference to the project this application belongs to.
+	// The empty string means that application belongs to the 'default' project.
+	Project string `json:"project"`
+	// SyncPolicy controls when and how a sync will be performed
+	SyncPolicy *SyncPolicy `json:"syncPolicy,omitempty"`
+	// IgnoreDifferences is a list of resources and their fields which should be ignored during comparison
+	IgnoreDifferences []ResourceIgnoreDifferences `json:"ignoreDifferences,omitempty"`
+	// Info contains a list of information (URLs, email addresses, and plain text) that relates to the application
+	Info []Info `json:"info,omitempty"`
+	// RevisionHistoryLimit limits the number of items kept in the application's revision history, which is used for informational purposes as well as for rollbacks to previous versions.
+	// This should only be changed in exceptional circumstances.
+	// Setting to zero will store no history. This will reduce storage used.
+	// Increasing will increase the space used to store the history, so we do not recommend increasing it.
+	// Default is 10.
+	RevisionHistoryLimit *int64 `json:"revisionHistoryLimit,omitempty"`
+}
+
+// Info represents a name and value
+type Info struct {
+	Name  string `json:"name" protobuf:"bytes,1,name=name"`
+	Value string `json:"value" protobuf:"bytes,2,name=value"`
+}
+
+// ResourceIgnoreDifferences contains resource filter and list of json paths which should be ignored during comparison with live state.
+type ResourceIgnoreDifferences struct {
+	Group             string   `json:"group,omitempty"`
+	Kind              string   `json:"kind"`
+	Name              string   `json:"name,omitempty"`
+	Namespace         string   `json:"namespace,omitempty"`
+	JSONPointers      []string `json:"jsonPointers,omitempty"`
+	JQPathExpressions []string `json:"jqPathExpressions,omitempty"`
+	// ManagedFieldsManagers is a list of trusted managers. Fields mutated by those managers will take precedence over the
+	// desired state defined in the SCM and won't be displayed in diffs
+	ManagedFieldsManagers []string `json:"managedFieldsManagers,omitempty"`
+}
+
+// SyncPolicy controls when a sync will be performed in response to updates in git
+type SyncPolicy struct {
+	// Automated will keep an application synced to the target revision
+	Automated *SyncPolicyAutomated `json:"automated,omitempty"`
+	// Options allow you to specify whole app sync-options
+	SyncOptions SyncOptions `json:"syncOptions,omitempty"`
+	// Retry controls failed sync retry behavior
+	Retry *RetryStrategy `json:"retry,omitempty"`
+}
+
+// RetryStrategy contains information about the strategy to apply when a sync failed
+type RetryStrategy struct {
+	// Limit is the maximum number of attempts for retrying a failed sync. If set to 0, no retries will be performed.
+	Limit int64 `json:"limit,omitempty"`
+	// Backoff controls how to backoff on subsequent retries of failed syncs
+	Backoff *Backoff `json:"backoff,omitempty"`
+}
+
+// SyncOptions is type alias of []string
+type SyncOptions []string
+
+// ApplicationDestination holds information about the application's destination
+type ApplicationDestination struct {
+	// Server specifies the URL of the target cluster and must be set to the Kubernetes control plane API
+	Server string `json:"server,omitempty"`
+	// Namespace specifies the target namespace for the application's resources.
+	// The namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace
+	Namespace string `json:"namespace,omitempty"`
+	// Name is an alternate way of specifying the target cluster by its symbolic name
+	Name string `json:"name,omitempty"`
+}
+
+// ApplicationSource contains all required information about the source of an application
+type ApplicationSource struct {
+	// RepoURL is the URL to the repository (Git or Helm) that contains the application manifests
+	RepoURL string `json:"repoURL"`
+	// Path is a directory path within the Git repository, and is only valid for applications sourced from Git.
+	Path string `json:"path,omitempty"`
+	// TargetRevision defines the revision of the source to sync the application to.
+	// In case of Git, this can be commit, tag, or branch. If omitted, will equal to HEAD.
+	// In case of Helm, this is a semver tag for the Chart's version.
+	TargetRevision string `json:"targetRevision,omitempty"`
+	// Helm holds helm specific options
+	Helm *ApplicationSourceHelm `json:"helm,omitempty"`
+	// Kustomize holds kustomize specific options
+	Kustomize *ApplicationSourceKustomize `json:"kustomize,omitempty"`
+	// Ksonnet holds ksonnet specific options
+	Ksonnet *ApplicationSourceKsonnet `json:"ksonnet,omitempty"`
+	// Directory holds path/directory specific options
+	Directory *ApplicationSourceDirectory `json:"directory,omitempty"`
+	// ConfigManagementPlugin holds config management plugin specific options
+	Plugin *ApplicationSourcePlugin `json:"plugin,omitempty"`
+	// Chart is a Helm chart name, and must be specified for applications sourced from a Helm repo.
+	Chart string `json:"chart,omitempty"`
+}
+
+// ApplicationSourcePlugin holds options specific to config management plugins
+type ApplicationSourcePlugin struct {
+	Name string `json:"name,omitempty"`
+	Env  `json:"env,omitempty"`
+}
+
+// Env is a list of environment variable entries
+type Env []*EnvEntry
+
+// EnvEntry represents an entry in the application's environment
+type EnvEntry struct {
+	// Name is the name of the variable, usually expressed in uppercase
+	Name string `json:"name"`
+	// Value is the value of the variable
+	Value string `json:"value"`
+}
+
+// ApplicationSourceDirectory holds options for applications of type plain YAML or Jsonnet
+type ApplicationSourceDirectory struct {
+	// Recurse specifies whether to scan a directory recursively for manifests
+	Recurse bool `json:"recurse,omitempty"`
+	// Jsonnet holds options specific to Jsonnet
+	Jsonnet ApplicationSourceJsonnet `json:"jsonnet,omitempty"`
+	// Exclude contains a glob pattern to match paths against that should be explicitly excluded from being used during manifest generation
+	Exclude string `json:"exclude,omitempty"`
+	// Include contains a glob pattern to match paths against that should be explicitly included during manifest generation
+	Include string `json:"include,omitempty"`
+}
+
+// ApplicationSourceJsonnet holds options specific to applications of type Jsonnet
+type ApplicationSourceJsonnet struct {
+	// ExtVars is a list of Jsonnet External Variables
+	ExtVars []JsonnetVar `json:"extVars,omitempty"`
+	// TLAS is a list of Jsonnet Top-level Arguments
+	TLAs []JsonnetVar `json:"tlas,omitempty"`
+	// Additional library search dirs
+	Libs []string `json:"libs,omitempty"`
+}
+
+// JsonnetVar represents a variable to be passed to jsonnet during manifest generation
+type JsonnetVar struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+	Code  bool   `json:"code,omitempty"`
+}
+
+// ApplicationSourceKsonnet holds ksonnet specific options
+type ApplicationSourceKsonnet struct {
+	// Environment is a ksonnet application environment name
+	Environment string `json:"environment,omitempty"`
+	// Parameters are a list of ksonnet component parameter override values
+	Parameters []KsonnetParameter `json:"parameters,omitempty"`
+}
+
+// KsonnetParameter is a ksonnet component parameter
+type KsonnetParameter struct {
+	Component string `json:"component,omitempty"`
+	Name      string `json:"name"`
+	Value     string `json:"value"`
+}
+
+// ApplicationSourceKustomize holds options specific to an Application source specific to Kustomize
+type ApplicationSourceKustomize struct {
+	// NamePrefix is a prefix appended to resources for Kustomize apps
+	NamePrefix string `json:"namePrefix,omitempty"`
+	// NameSuffix is a suffix appended to resources for Kustomize apps
+	NameSuffix string `json:"nameSuffix,omitempty"`
+	// Images is a list of Kustomize image override specifications
+	Images KustomizeImages `json:"images,omitempty"`
+	// CommonLabels is a list of additional labels to add to rendered manifests
+	CommonLabels map[string]string `json:"commonLabels,omitempty"`
+	// Version controls which version of Kustomize to use for rendering manifests
+	Version string `json:"version,omitempty"`
+	// CommonAnnotations is a list of additional annotations to add to rendered manifests
+	CommonAnnotations map[string]string `json:"commonAnnotations,omitempty"`
+	// ForceCommonLabels specifies whether to force applying common labels to resources for Kustomize apps
+	ForceCommonLabels bool `json:"forceCommonLabels,omitempty"`
+	// ForceCommonAnnotations specifies whether to force applying common annotations to resources for Kustomize apps
+	ForceCommonAnnotations bool `json:"forceCommonAnnotations,omitempty"`
+}
+
+// KustomizeImages is a list of Kustomize images
+type KustomizeImages []KustomizeImage
+
+// KustomizeImage represents a Kustomize image definition in the format [old_image_name=]<image_name>:<image_tag>
+type KustomizeImage string
+
+// ApplicationSourceHelm holds helm specific options
+type ApplicationSourceHelm struct {
+	// ValuesFiles is a list of Helm value files to use when generating a template
+	ValueFiles []string `json:"valueFiles,omitempty"`
+	// Parameters is a list of Helm parameters which are passed to the helm template command upon manifest generation
+	Parameters []HelmParameter `json:"parameters,omitempty"`
+	// ReleaseName is the Helm release name to use. If omitted it will use the application name
+	ReleaseName string `json:"releaseName,omitempty"`
+	// Values specifies Helm values to be passed to helm template, typically defined as a block
+	Values string `json:"values,omitempty"`
+	// FileParameters are file parameters to the helm template
+	FileParameters []HelmFileParameter `json:"fileParameters,omitempty"`
+	// Version is the Helm version to use for templating (either "2" or "3")
+	Version string `json:"version,omitempty"`
+	// PassCredentials pass credentials to all domains (Helm's --pass-credentials)
+	PassCredentials bool `json:"passCredentials,omitempty"`
+	// IgnoreMissingValueFiles prevents helm template from failing when valueFiles do not exist locally by not appending them to helm template --values
+	IgnoreMissingValueFiles bool `json:"ignoreMissingValueFiles,omitempty"`
+	// SkipCrds skips custom resource definition installation step (Helm's --skip-crds)
+	SkipCrds bool `json:"skipCrds,omitempty"`
+}
+
+// HelmParameter is a parameter that's passed to helm template during manifest generation
+type HelmParameter struct {
+	// Name is the name of the Helm parameter
+	Name string `json:"name,omitempty"`
+	// Value is the value for the Helm parameter
+	Value string `json:"value,omitempty"`
+	// ForceString determines whether to tell Helm to interpret booleans and numbers as strings
+	ForceString bool `json:"forceString,omitempty"`
+}
+
+// HelmFileParameter is a file parameter that's passed to helm template during manifest generation
+type HelmFileParameter struct {
+	// Name is the name of the Helm parameter
+	Name string `json:"name,omitempty"`
+	// Path is the path to the file containing the values for the Helm parameter
+	Path string `json:"path,omitempty"`
+}
+
+// Backoff is the backoff strategy to use on subsequent retries for failing syncs
+type Backoff struct {
+	// Duration is the amount to back off. Default unit is seconds, but could also be a duration (e.g. "2m", "1h")
+	Duration string `json:"duration,omitempty"`
+	// Factor is a factor to multiply the base duration after each failed retry
+	Factor *int64 `json:"factor,omitempty"`
+	// MaxDuration is the maximum amount of time allowed for the backoff strategy
+	MaxDuration string `json:"maxDuration,omitempty"`
+}
+
+// SyncPolicyAutomated controls the behavior of an automated sync
+type SyncPolicyAutomated struct {
+	// Prune specifies whether to delete resources from the cluster that are not found in the sources anymore as part of automated sync (default: false)
+	Prune bool `json:"prune,omitempty"`
+	// SelfHeal specifes whether to revert resources back to their desired state upon modification in the cluster (default: false)
+	SelfHeal bool `json:"selfHeal,omitempty"`
+	// AllowEmpty allows apps have zero live resources (default: false)
+	AllowEmpty bool `json:"allowEmpty,omitempty"`
+}
+
+// SyncStrategy controls the manner in which a sync is performed
+type SyncStrategy struct {
+	// Apply will perform a `kubectl apply` to perform the sync.
+	Apply *SyncStrategyApply `json:"apply,omitempty"`
+	// Hook will submit any referenced resources to perform the sync. This is the default strategy
+	Hook *SyncStrategyHook `json:"hook,omitempty"`
+}
+
+// SyncStrategyApply uses `kubectl apply` to perform the apply
+type SyncStrategyApply struct {
+	// Force indicates whether or not to supply the --force flag to `kubectl apply`.
+	// The --force flag deletes and re-create the resource, when PATCH encounters conflict and has
+	// retried for 5 times.
+	Force bool `json:"force,omitempty"`
+}
+
+// SyncStrategyHook will perform a sync using hooks annotations.
+// If no hook annotation is specified falls back to `kubectl apply`.
+type SyncStrategyHook struct {
+	// Embed SyncStrategyApply type to inherit any `apply` options
+	// +optional
+	SyncStrategyApply `json:",inline"`
+}
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:object:root=true
+// +k8s:openapi-gen=true
+
+// Application represents an application the DevOps system
+type Application struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec ApplicationSpec `json:"spec,omitempty"`
+}
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// ApplicationList represents a set of the applications
+type ApplicationList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []Application `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&Application{}, &ApplicationList{})
+}
diff --git a/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go b/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
index d40e3cf..cdd04f4 100644
--- a/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
@@ -205,6 +205,334 @@ func (in *AddonStrategyList) DeepCopyObject() runtime.Object {
 	return nil
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Application) DeepCopyInto(out *Application) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application.
+func (in *Application) DeepCopy() *Application {
+	if in == nil {
+		return nil
+	}
+	out := new(Application)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Application) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationDestination) DeepCopyInto(out *ApplicationDestination) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDestination.
+func (in *ApplicationDestination) DeepCopy() *ApplicationDestination {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationDestination)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationList) DeepCopyInto(out *ApplicationList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Application, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationList.
+func (in *ApplicationList) DeepCopy() *ApplicationList {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ApplicationList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSource) DeepCopyInto(out *ApplicationSource) {
+	*out = *in
+	if in.Helm != nil {
+		in, out := &in.Helm, &out.Helm
+		*out = new(ApplicationSourceHelm)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Kustomize != nil {
+		in, out := &in.Kustomize, &out.Kustomize
+		*out = new(ApplicationSourceKustomize)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Ksonnet != nil {
+		in, out := &in.Ksonnet, &out.Ksonnet
+		*out = new(ApplicationSourceKsonnet)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Directory != nil {
+		in, out := &in.Directory, &out.Directory
+		*out = new(ApplicationSourceDirectory)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Plugin != nil {
+		in, out := &in.Plugin, &out.Plugin
+		*out = new(ApplicationSourcePlugin)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSource.
+func (in *ApplicationSource) DeepCopy() *ApplicationSource {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourceDirectory) DeepCopyInto(out *ApplicationSourceDirectory) {
+	*out = *in
+	in.Jsonnet.DeepCopyInto(&out.Jsonnet)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourceDirectory.
+func (in *ApplicationSourceDirectory) DeepCopy() *ApplicationSourceDirectory {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourceDirectory)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourceHelm) DeepCopyInto(out *ApplicationSourceHelm) {
+	*out = *in
+	if in.ValueFiles != nil {
+		in, out := &in.ValueFiles, &out.ValueFiles
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Parameters != nil {
+		in, out := &in.Parameters, &out.Parameters
+		*out = make([]HelmParameter, len(*in))
+		copy(*out, *in)
+	}
+	if in.FileParameters != nil {
+		in, out := &in.FileParameters, &out.FileParameters
+		*out = make([]HelmFileParameter, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourceHelm.
+func (in *ApplicationSourceHelm) DeepCopy() *ApplicationSourceHelm {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourceHelm)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourceJsonnet) DeepCopyInto(out *ApplicationSourceJsonnet) {
+	*out = *in
+	if in.ExtVars != nil {
+		in, out := &in.ExtVars, &out.ExtVars
+		*out = make([]JsonnetVar, len(*in))
+		copy(*out, *in)
+	}
+	if in.TLAs != nil {
+		in, out := &in.TLAs, &out.TLAs
+		*out = make([]JsonnetVar, len(*in))
+		copy(*out, *in)
+	}
+	if in.Libs != nil {
+		in, out := &in.Libs, &out.Libs
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourceJsonnet.
+func (in *ApplicationSourceJsonnet) DeepCopy() *ApplicationSourceJsonnet {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourceJsonnet)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourceKsonnet) DeepCopyInto(out *ApplicationSourceKsonnet) {
+	*out = *in
+	if in.Parameters != nil {
+		in, out := &in.Parameters, &out.Parameters
+		*out = make([]KsonnetParameter, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourceKsonnet.
+func (in *ApplicationSourceKsonnet) DeepCopy() *ApplicationSourceKsonnet {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourceKsonnet)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourceKustomize) DeepCopyInto(out *ApplicationSourceKustomize) {
+	*out = *in
+	if in.Images != nil {
+		in, out := &in.Images, &out.Images
+		*out = make(KustomizeImages, len(*in))
+		copy(*out, *in)
+	}
+	if in.CommonLabels != nil {
+		in, out := &in.CommonLabels, &out.CommonLabels
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.CommonAnnotations != nil {
+		in, out := &in.CommonAnnotations, &out.CommonAnnotations
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourceKustomize.
+func (in *ApplicationSourceKustomize) DeepCopy() *ApplicationSourceKustomize {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourceKustomize)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSourcePlugin) DeepCopyInto(out *ApplicationSourcePlugin) {
+	*out = *in
+	if in.Env != nil {
+		in, out := &in.Env, &out.Env
+		*out = make(Env, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(EnvEntry)
+				**out = **in
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSourcePlugin.
+func (in *ApplicationSourcePlugin) DeepCopy() *ApplicationSourcePlugin {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSourcePlugin)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) {
+	*out = *in
+	if in.ArgoApp != nil {
+		in, out := &in.ArgoApp, &out.ArgoApp
+		*out = new(ArgoApplication)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSpec.
+func (in *ApplicationSpec) DeepCopy() *ApplicationSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArgoApplication) DeepCopyInto(out *ArgoApplication) {
+	*out = *in
+	in.Source.DeepCopyInto(&out.Source)
+	out.Destination = in.Destination
+	if in.SyncPolicy != nil {
+		in, out := &in.SyncPolicy, &out.SyncPolicy
+		*out = new(SyncPolicy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.IgnoreDifferences != nil {
+		in, out := &in.IgnoreDifferences, &out.IgnoreDifferences
+		*out = make([]ResourceIgnoreDifferences, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.Info != nil {
+		in, out := &in.Info, &out.Info
+		*out = make([]Info, len(*in))
+		copy(*out, *in)
+	}
+	if in.RevisionHistoryLimit != nil {
+		in, out := &in.RevisionHistoryLimit, &out.RevisionHistoryLimit
+		*out = new(int64)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoApplication.
+func (in *ArgoApplication) DeepCopy() *ArgoApplication {
+	if in == nil {
+		return nil
+	}
+	out := new(ArgoApplication)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AuthConfig) DeepCopyInto(out *AuthConfig) {
 	*out = *in
@@ -225,6 +553,26 @@ func (in *AuthConfig) DeepCopy() *AuthConfig {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Backoff) DeepCopyInto(out *Backoff) {
+	*out = *in
+	if in.Factor != nil {
+		in, out := &in.Factor, &out.Factor
+		*out = new(int64)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backoff.
+func (in *Backoff) DeepCopy() *Backoff {
+	if in == nil {
+		return nil
+	}
+	out := new(Backoff)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CGroupLimits) DeepCopyInto(out *CGroupLimits) {
 	*out = *in
@@ -424,6 +772,46 @@ func (in DockerConfigMap) DeepCopy() DockerConfigMap {
 	return *out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Env) DeepCopyInto(out *Env) {
+	{
+		in := &in
+		*out = make(Env, len(*in))
+		for i := range *in {
+			if (*in)[i] != nil {
+				in, out := &(*in)[i], &(*out)[i]
+				*out = new(EnvEntry)
+				**out = **in
+			}
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Env.
+func (in Env) DeepCopy() Env {
+	if in == nil {
+		return nil
+	}
+	out := new(Env)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *EnvEntry) DeepCopyInto(out *EnvEntry) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvEntry.
+func (in *EnvEntry) DeepCopy() *EnvEntry {
+	if in == nil {
+		return nil
+	}
+	out := new(EnvEntry)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *EnvironmentSpec) DeepCopyInto(out *EnvironmentSpec) {
 	*out = *in
@@ -522,6 +910,100 @@ func (in *GitRepositorySpec) DeepCopy() *GitRepositorySpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmFileParameter) DeepCopyInto(out *HelmFileParameter) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmFileParameter.
+func (in *HelmFileParameter) DeepCopy() *HelmFileParameter {
+	if in == nil {
+		return nil
+	}
+	out := new(HelmFileParameter)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HelmParameter) DeepCopyInto(out *HelmParameter) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmParameter.
+func (in *HelmParameter) DeepCopy() *HelmParameter {
+	if in == nil {
+		return nil
+	}
+	out := new(HelmParameter)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Info) DeepCopyInto(out *Info) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Info.
+func (in *Info) DeepCopy() *Info {
+	if in == nil {
+		return nil
+	}
+	out := new(Info)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *JsonnetVar) DeepCopyInto(out *JsonnetVar) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JsonnetVar.
+func (in *JsonnetVar) DeepCopy() *JsonnetVar {
+	if in == nil {
+		return nil
+	}
+	out := new(JsonnetVar)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KsonnetParameter) DeepCopyInto(out *KsonnetParameter) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KsonnetParameter.
+func (in *KsonnetParameter) DeepCopy() *KsonnetParameter {
+	if in == nil {
+		return nil
+	}
+	out := new(KsonnetParameter)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in KustomizeImages) DeepCopyInto(out *KustomizeImages) {
+	{
+		in := &in
+		*out = make(KustomizeImages, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KustomizeImages.
+func (in KustomizeImages) DeepCopy() KustomizeImages {
+	if in == nil {
+		return nil
+	}
+	out := new(KustomizeImages)
+	in.DeepCopyInto(out)
+	return *out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Parameter) DeepCopyInto(out *Parameter) {
 	*out = *in
@@ -572,6 +1054,56 @@ func (in *ProxyConfig) DeepCopy() *ProxyConfig {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResourceIgnoreDifferences) DeepCopyInto(out *ResourceIgnoreDifferences) {
+	*out = *in
+	if in.JSONPointers != nil {
+		in, out := &in.JSONPointers, &out.JSONPointers
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.JQPathExpressions != nil {
+		in, out := &in.JQPathExpressions, &out.JQPathExpressions
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.ManagedFieldsManagers != nil {
+		in, out := &in.ManagedFieldsManagers, &out.ManagedFieldsManagers
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceIgnoreDifferences.
+func (in *ResourceIgnoreDifferences) DeepCopy() *ResourceIgnoreDifferences {
+	if in == nil {
+		return nil
+	}
+	out := new(ResourceIgnoreDifferences)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RetryStrategy) DeepCopyInto(out *RetryStrategy) {
+	*out = *in
+	if in.Backoff != nil {
+		in, out := &in.Backoff, &out.Backoff
+		*out = new(Backoff)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RetryStrategy.
+func (in *RetryStrategy) DeepCopy() *RetryStrategy {
+	if in == nil {
+		return nil
+	}
+	out := new(RetryStrategy)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *S2iAutoScale) DeepCopyInto(out *S2iAutoScale) {
 	*out = *in
@@ -1145,6 +1677,126 @@ func (in *S2iRunStatus) DeepCopy() *S2iRunStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in SyncOptions) DeepCopyInto(out *SyncOptions) {
+	{
+		in := &in
+		*out = make(SyncOptions, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncOptions.
+func (in SyncOptions) DeepCopy() SyncOptions {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncOptions)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SyncPolicy) DeepCopyInto(out *SyncPolicy) {
+	*out = *in
+	if in.Automated != nil {
+		in, out := &in.Automated, &out.Automated
+		*out = new(SyncPolicyAutomated)
+		**out = **in
+	}
+	if in.SyncOptions != nil {
+		in, out := &in.SyncOptions, &out.SyncOptions
+		*out = make(SyncOptions, len(*in))
+		copy(*out, *in)
+	}
+	if in.Retry != nil {
+		in, out := &in.Retry, &out.Retry
+		*out = new(RetryStrategy)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncPolicy.
+func (in *SyncPolicy) DeepCopy() *SyncPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SyncPolicyAutomated) DeepCopyInto(out *SyncPolicyAutomated) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncPolicyAutomated.
+func (in *SyncPolicyAutomated) DeepCopy() *SyncPolicyAutomated {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncPolicyAutomated)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SyncStrategy) DeepCopyInto(out *SyncStrategy) {
+	*out = *in
+	if in.Apply != nil {
+		in, out := &in.Apply, &out.Apply
+		*out = new(SyncStrategyApply)
+		**out = **in
+	}
+	if in.Hook != nil {
+		in, out := &in.Hook, &out.Hook
+		*out = new(SyncStrategyHook)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategy.
+func (in *SyncStrategy) DeepCopy() *SyncStrategy {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncStrategy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SyncStrategyApply) DeepCopyInto(out *SyncStrategyApply) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategyApply.
+func (in *SyncStrategyApply) DeepCopy() *SyncStrategyApply {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncStrategyApply)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SyncStrategyHook) DeepCopyInto(out *SyncStrategyHook) {
+	*out = *in
+	out.SyncStrategyApply = in.SyncStrategyApply
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategyHook.
+func (in *SyncStrategyHook) DeepCopy() *SyncStrategyHook {
+	if in == nil {
+		return nil
+	}
+	out := new(SyncStrategyHook)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Template) DeepCopyInto(out *Template) {
 	*out = *in
diff --git a/pkg/utils/k8sutil/k8sutil.go b/pkg/utils/k8sutil/k8sutil.go
index 82e0595..b8a0ac9 100644
--- a/pkg/utils/k8sutil/k8sutil.go
+++ b/pkg/utils/k8sutil/k8sutil.go
@@ -47,3 +47,13 @@ func SetOwnerReference(object metav1.Object, ownerRef metav1.OwnerReference) {
 		object.SetOwnerReferences(append(object.GetOwnerReferences(), ownerRef))
 	}
 }
+
+// AddOwnerReference add an ownerReference
+func AddOwnerReference(object metav1.Object, typeMeta metav1.TypeMeta, objectMeta metav1.ObjectMeta) {
+	SetOwnerReference(object, metav1.OwnerReference{
+		Kind:       typeMeta.Kind,
+		APIVersion: typeMeta.APIVersion,
+		Name:       objectMeta.Name,
+		UID:        objectMeta.UID,
+	})
+}


Test patch:
diff --git a/controllers/argocd/application_controller_test.go b/controllers/argocd/application_controller_test.go
new file mode 100644
index 0000000..a063422
--- /dev/null
+++ b/controllers/argocd/application_controller_test.go
@@ -0,0 +1,204 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package argocd
+
+import (
+	"context"
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+)
+
+func Test_createUnstructuredApplication(t *testing.T) {
+	type args struct {
+		app *v1alpha1.Application
+	}
+	tests := []struct {
+		name   string
+		args   args
+		verify func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error)
+	}{{
+		name: "without argo",
+		args: args{
+			app: &v1alpha1.Application{},
+		},
+		verify: func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error) {
+			assert.NotNil(t, gotErr)
+		},
+	}, {
+		name: "empty Argo Application with the default value",
+		args: args{
+			app: &v1alpha1.Application{
+				Spec: v1alpha1.ApplicationSpec{
+					ArgoApp: &v1alpha1.ArgoApplication{},
+				},
+			},
+		},
+		verify: func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error) {
+			assert.Nil(t, gotErr)
+
+			// make sure it has the ownerReference
+			refs, _, _ := unstructured.NestedSlice(gotResult.Object, "metadata", "ownerReferences")
+			assert.NotNil(t, refs)
+			assert.Equal(t, 1, len(refs))
+		},
+	}, {
+		name: "with some specific fields, with default values",
+		args: args{
+			app: &v1alpha1.Application{
+				Spec: v1alpha1.ApplicationSpec{
+					ArgoApp: &v1alpha1.ArgoApplication{
+						Destination: v1alpha1.ApplicationDestination{
+							Server:    "server",
+							Namespace: "namespace",
+						},
+					},
+				},
+			},
+		},
+		verify: func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error) {
+			assert.Nil(t, gotErr)
+
+			project, _, _ := unstructured.NestedString(gotResult.Object, "spec", "project")
+			assert.Equal(t, "default", project)
+			destServer, _, _ := unstructured.NestedString(gotResult.Object, "spec", "destination", "server")
+			assert.Equal(t, "server", destServer)
+			destNs, _, _ := unstructured.NestedString(gotResult.Object, "spec", "destination", "namespace")
+			assert.Equal(t, "namespace", destNs)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotResult, err := createUnstructuredApplication(tt.args.app)
+			tt.verify(t, gotResult, err)
+		})
+	}
+}
+
+func TestApplicationReconcilerBasicInterface(t *testing.T) {
+	nameGetter := &ApplicationReconciler{}
+	assert.NotEmpty(t, nameGetter.GetName())
+
+	groupNameGetter := &ApplicationReconciler{}
+	assert.NotEmpty(t, groupNameGetter.GetGroupName())
+}
+
+func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
+	schema, err := v1alpha3.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	app := &v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake",
+			Namespace: "fake",
+		},
+		Spec: v1alpha1.ApplicationSpec{
+			ArgoApp: &v1alpha1.ArgoApplication{
+				Project: "project",
+				SyncPolicy: &v1alpha1.SyncPolicy{
+					Automated: &v1alpha1.SyncPolicyAutomated{
+						Prune: true,
+					},
+				},
+			},
+		},
+	}
+
+	argoApp := &unstructured.Unstructured{}
+	argoApp.SetKind("Application")
+	argoApp.SetAPIVersion("argoproj.io/v1alpha1")
+	argoApp.SetName("fake")
+	argoApp.SetNamespace("fake")
+
+	type fields struct {
+		Client   client.Client
+		log      logr.Logger
+		recorder record.EventRecorder
+	}
+	type args struct {
+		app *v1alpha1.Application
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		verify func(t *testing.T, Client client.Client, err error)
+	}{{
+		name: "without Argo Application",
+		fields: fields{
+			Client: fake.NewFakeClientWithScheme(schema),
+		},
+		args: args{
+			app: app.DeepCopy(),
+		},
+		verify: func(t *testing.T, Client client.Client, err error) {
+			assert.Nil(t, err)
+
+			app := argoApp.DeepCopy()
+
+			err = Client.Get(context.TODO(), types.NamespacedName{
+				Namespace: "fake",
+				Name:      "fake",
+			}, app)
+			assert.Nil(t, err)
+			project, _, _ := unstructured.NestedString(app.Object, "spec", "project")
+			assert.Equal(t, "project", project)
+			prune, _, _ := unstructured.NestedBool(app.Object, "spec", "syncPolicy", "automated", "prune")
+			assert.True(t, prune)
+		},
+	}, {
+		name: "with Argo Application",
+		fields: fields{
+			Client: fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
+		},
+		args: args{
+			app: app.DeepCopy(),
+		},
+		verify: func(t *testing.T, Client client.Client, err error) {
+			assert.Nil(t, err)
+
+			app := argoApp.DeepCopy()
+
+			err = Client.Get(context.TODO(), types.NamespacedName{
+				Namespace: "fake",
+				Name:      "fake",
+			}, app)
+			assert.Nil(t, err)
+			project, _, _ := unstructured.NestedString(app.Object, "spec", "project")
+			assert.Equal(t, "project", project)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &ApplicationReconciler{
+				Client:   tt.fields.Client,
+				log:      tt.fields.log,
+				recorder: tt.fields.recorder,
+			}
+			err := r.reconcileArgoApplication(tt.args.app)
+			tt.verify(t, tt.fields.Client, err)
+		})
+	}
+}
diff --git a/pkg/utils/k8sutil/k8sutil_test.go b/pkg/utils/k8sutil/k8sutil_test.go
index a8cde6d..f845790 100644
--- a/pkg/utils/k8sutil/k8sutil_test.go
+++ b/pkg/utils/k8sutil/k8sutil_test.go
@@ -119,3 +119,50 @@ func TestSetOwnerReference(t *testing.T) {
 		})
 	}
 }
+
+func TestAddOwnerReference(t *testing.T) {
+	type fake struct {
+		metav1.TypeMeta
+		metav1.ObjectMeta
+	}
+
+	type args struct {
+		object     metav1.Object
+		typeMeta   metav1.TypeMeta
+		objectMeta metav1.ObjectMeta
+	}
+	tests := []struct {
+		name   string
+		args   args
+		verify func(t *testing.T, object metav1.Object)
+	}{{
+		name: "normal case",
+		args: args{
+			object: &fake{},
+			typeMeta: metav1.TypeMeta{
+				Kind:       "kind",
+				APIVersion: "version",
+			},
+			objectMeta: metav1.ObjectMeta{
+				Name: "name",
+				UID:  "uid",
+			},
+		},
+		verify: func(t *testing.T, object metav1.Object) {
+			refs := object.GetOwnerReferences()
+			assert.Equal(t, 1, len(refs))
+
+			ref := refs[0]
+			assert.Equal(t, "kind", ref.Kind)
+			assert.Equal(t, "version", ref.APIVersion)
+			assert.Equal(t, "name", ref.Name)
+			assert.Equal(t, "uid", string(ref.UID))
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			AddOwnerReference(tt.args.object, tt.args.typeMeta, tt.args.objectMeta)
+			tt.verify(t, tt.args.object)
+		})
+	}
+}