Repository: argoproj/argo-cd
Pull request #3976 (2020-07-21 21:52:34)

feat: auto create namespace
Checklist:

* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
* [ ] Optional. My organization is added to USERS.md.
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)). 


codecov-commenter: # [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=h1) Report
> Merging [#3976](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=desc) into [master](https://codecov.io/gh/argoproj/argo-cd/commit/ececbed9997a6fd0d47970ea163a0587baf3ed92&el=desc) will **increase** coverage by `0.00%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/argoproj/argo-cd/pull/3976/graphs/tree.svg?width=650&height=150&src=pr&token=kf5kwuegwE)](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #3976   +/-   ##
=======================================
  Coverage   42.08%   42.09%           
=======================================
  Files         123      123           
  Lines       17996    17999    +3     
=======================================
+ Hits         7573     7576    +3     
  Misses       9453     9453           
  Partials      970      970           
```


| [Impacted Files](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [controller/sync.go](https://codecov.io/gh/argoproj/argo-cd/pull/3976/diff?src=pr&el=tree#diff-Y29udHJvbGxlci9zeW5jLmdv) | `40.56% <ø> (ø)` | |
| [cmd/argocd-util/commands/apps.go](https://codecov.io/gh/argoproj/argo-cd/pull/3976/diff?src=pr&el=tree#diff-Y21kL2FyZ29jZC11dGlsL2NvbW1hbmRzL2FwcHMuZ28=) | `41.14% <0.00%> (+0.93%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=footer). Last update [ececbed...e798d6e](https://codecov.io/gh/argoproj/argo-cd/pull/3976?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/controller/appcontroller.go b/controller/appcontroller.go
index 5ba663e7c..f84b42574 100644
--- a/controller/appcontroller.go
+++ b/controller/appcontroller.go
@@ -856,7 +856,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
 }
 
 func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
-	kube.RetryUntilSucceed(func() error {
+	kube.RetryUntilSucceed(context.Background(), updateOperationStateTimeout, "Update application operation state", func() error {
 		if state.Phase == "" {
 			// expose any bugs where we neglect to set phase
 			panic("no phase was set")
@@ -915,7 +915,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
 			ctrl.metricsServer.IncSync(app, state)
 		}
 		return nil
-	}, "Update application operation state", context.Background(), updateOperationStateTimeout)
+	})
 }
 
 func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) {
diff --git a/controller/cache/cache.go b/controller/cache/cache.go
index 23cec8a04..e73f688a0 100644
--- a/controller/cache/cache.go
+++ b/controller/cache/cache.go
@@ -416,9 +416,9 @@ func (c *liveStateCache) Init() error {
 func (c *liveStateCache) Run(ctx context.Context) error {
 	go c.watchSettings(ctx)
 
-	kube.RetryUntilSucceed(func() error {
+	kube.RetryUntilSucceed(ctx, clustercache.ClusterRetryTimeout, "watch clusters", func() error {
 		return c.db.WatchClusters(ctx, c.handleAddEvent, c.handleModEvent, c.handleDeleteEvent)
-	}, "watch clusters", ctx, clustercache.ClusterRetryTimeout)
+	})
 
 	<-ctx.Done()
 	c.invalidate(c.cacheSettings)
diff --git a/controller/sync.go b/controller/sync.go
index 4dccf24e2..09d031160 100644
--- a/controller/sync.go
+++ b/controller/sync.go
@@ -142,6 +142,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
 			return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)
 		}),
 		sync.WithManifestValidation(!syncOp.SyncOptions.HasOption("Validate=false")),
+		sync.WithNamespaceCreation(syncOp.SyncOptions.HasOption("CreateNamespace=true")),
 	)
 
 	if err != nil {
diff --git a/docs/user-guide/jsonnet.md b/docs/user-guide/jsonnet.md
index 0c740707c..973510f61 100644
--- a/docs/user-guide/jsonnet.md
+++ b/docs/user-guide/jsonnet.md
@@ -14,7 +14,7 @@ E.g. via the CLI:
 ```bash
 argocd app create APPNAME \
   --jsonnet-ext-str 'app=${ARGOCD_APP_NAME}' \
-  --jsonnet-tla-str 'ns=${ARGOCD_APP_NAMESPACE}'
+  --jsonnet-tla-str 'ns=${ARGOCD_APP_NAMESPACE}' \
   --jsonnet-libs 'vendor'
 ```
 
diff --git a/go.mod b/go.mod
index 9dbbfaa3d..f7044453d 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
 	github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6 // indirect
 	github.com/alicebob/miniredis v2.5.0+incompatible
-	github.com/argoproj/gitops-engine v0.1.3-0.20200720164507-3c545080c935
+	github.com/argoproj/gitops-engine v0.1.3-0.20200727173650-dcb86f7d6b64
 	github.com/argoproj/pkg v0.0.0-20200624215116-23e74cb168fe
 	github.com/bsm/redislock v0.4.3
 	github.com/casbin/casbin v1.9.1
diff --git a/go.sum b/go.sum
index 229fda740..fceecabea 100644
--- a/go.sum
+++ b/go.sum
@@ -56,8 +56,8 @@ github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+Dx
 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
-github.com/argoproj/gitops-engine v0.1.3-0.20200720164507-3c545080c935 h1:Nn1hPY+VUmvturbbpo3JjJCZqGyfFzCcQaBU9t8fS8U=
-github.com/argoproj/gitops-engine v0.1.3-0.20200720164507-3c545080c935/go.mod h1:JkoSXhq63nmmf/3pKe8fFGrZC0/cyEasF7aFXCB8B24=
+github.com/argoproj/gitops-engine v0.1.3-0.20200727173650-dcb86f7d6b64 h1:3BGkjx4qBuEzKixkNAJG/PZi5Du1TnJB1HaFqLsPuUA=
+github.com/argoproj/gitops-engine v0.1.3-0.20200727173650-dcb86f7d6b64/go.mod h1:JkoSXhq63nmmf/3pKe8fFGrZC0/cyEasF7aFXCB8B24=
 github.com/argoproj/pkg v0.0.0-20200102163130-2dd1f3f6b4de/go.mod h1:2EZ44RG/CcgtPTwrRR0apOc7oU6UIw8GjCUJWZ8X3bM=
 github.com/argoproj/pkg v0.0.0-20200624215116-23e74cb168fe h1:HjTM7H8Z+J1xt340LNpH9q4jc8pXeqzkC8QKIjQphp4=
 github.com/argoproj/pkg v0.0.0-20200624215116-23e74cb168fe/go.mod h1:2EZ44RG/CcgtPTwrRR0apOc7oU6UIw8GjCUJWZ8X3bM=
diff --git a/manifests/base/redis/argocd-redis-deployment.yaml b/manifests/base/redis/argocd-redis-deployment.yaml
index b2cefa367..fa5168a2b 100644
--- a/manifests/base/redis/argocd-redis-deployment.yaml
+++ b/manifests/base/redis/argocd-redis-deployment.yaml
@@ -15,6 +15,11 @@ spec:
       labels:
         app.kubernetes.io/name: argocd-redis
     spec:
+      securityContext:
+        runAsUser: 1000
+        runAsGroup: 1000
+        fsGroup: 1000
+        runAsNonRoot: true
       containers:
       - name: redis
         image: redis:5.0.8
diff --git a/manifests/install.yaml b/manifests/install.yaml
index b10386275..57189d2a7 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -2602,6 +2602,11 @@ spec:
         name: redis
         ports:
         - containerPort: 6379
+      securityContext:
+        fsGroup: 1000
+        runAsGroup: 1000
+        runAsNonRoot: true
+        runAsUser: 1000
 ---
 apiVersion: apps/v1
 kind: Deployment
diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml
index 2705f26ca..9febc1d70 100644
--- a/manifests/namespace-install.yaml
+++ b/manifests/namespace-install.yaml
@@ -2517,6 +2517,11 @@ spec:
         name: redis
         ports:
         - containerPort: 6379
+      securityContext:
+        fsGroup: 1000
+        runAsGroup: 1000
+        runAsNonRoot: true
+        runAsUser: 1000
 ---
 apiVersion: apps/v1
 kind: Deployment
diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go
index 900ebe1a0..89c02b641 100644
--- a/pkg/apis/application/v1alpha1/types.go
+++ b/pkg/apis/application/v1alpha1/types.go
@@ -358,7 +358,7 @@ type ApplicationSourceJsonnet struct {
 }
 
 func (j *ApplicationSourceJsonnet) IsZero() bool {
-	return j == nil || len(j.ExtVars) == 0 && len(j.TLAs) == 0
+	return j == nil || len(j.ExtVars) == 0 && len(j.TLAs) == 0 && len(j.Libs) == 0
 }
 
 // ApplicationSourceKsonnet holds ksonnet specific options
diff --git a/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx b/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx
index 42672480b..16c51c033 100644
--- a/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx
+++ b/ui/src/app/applications/components/application-create-panel/application-create-panel.tsx
@@ -6,7 +6,7 @@ import {clusterTitle, RevisionHelpIcon, YamlEditor} from '../../../shared/compon
 import * as models from '../../../shared/models';
 import {services} from '../../../shared/services';
 import {ApplicationParameters} from '../application-parameters/application-parameters';
-import {ApplicationSyncOptionsField} from '../application-sync-options';
+import {ApplicationSyncOptionsCreateNamespaceField, ApplicationSyncOptionsField} from '../application-sync-options';
 
 const jsonMergePatch = require('json-merge-patch');
 
@@ -191,6 +191,7 @@ export const ApplicationCreatePanel = (props: {
                                                 <div className='argo-form-row'>
                                                     <label>Sync Options</label>
                                                     <FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
+                                                    <FormField formApi={api} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsCreateNamespaceField} />
                                                 </div>
                                             </div>
                                         );
diff --git a/ui/src/app/applications/components/application-summary/application-summary.tsx b/ui/src/app/applications/components/application-summary/application-summary.tsx
index b380adc9a..cd956fe43 100644
--- a/ui/src/app/applications/components/application-summary/application-summary.tsx
+++ b/ui/src/app/applications/components/application-summary/application-summary.tsx
@@ -6,7 +6,7 @@ import {Consumer} from '../../../shared/context';
 import * as models from '../../../shared/models';
 import {services} from '../../../shared/services';
 
-import {ApplicationSyncOptionsField} from '../application-sync-options';
+import {ApplicationSyncOptionsCreateNamespaceField, ApplicationSyncOptionsField} from '../application-sync-options';
 import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage} from '../utils';
 
 require('./application-summary.scss');
@@ -176,7 +176,12 @@ Default is 10.
         {
             title: 'SYNC OPTIONS',
             view: ((app.spec.syncPolicy || {}).syncOptions || []).join(', '),
-            edit: (formApi: FormApi) => <FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
+            edit: (formApi: FormApi) => (
+                <div>
+                    <FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsField} />
+                    <FormField formApi={formApi} field='spec.syncPolicy.syncOptions' component={ApplicationSyncOptionsCreateNamespaceField} />
+                </div>
+            )
         },
         {
             title: 'STATUS',
diff --git a/ui/src/app/applications/components/application-sync-options.tsx b/ui/src/app/applications/components/application-sync-options.tsx
index a3ef5bffc..7aedac164 100644
--- a/ui/src/app/applications/components/application-sync-options.tsx
+++ b/ui/src/app/applications/components/application-sync-options.tsx
@@ -50,3 +50,23 @@ export const ApplicationSyncOptionsField = ReactForm.FormField((props: {fieldApi
         </div>
     );
 });
+
+export const ApplicationSyncOptionsCreateNamespace = (props: ApplicationSyncOptionProps) => <div>{booleanOption('CreateNamespace', 'Auto-create namespace', false, props)}</div>;
+
+export const ApplicationSyncOptionsCreateNamespaceField = ReactForm.FormField((props: {fieldApi: ReactForm.FieldApi}) => {
+    const {
+        fieldApi: {getValue, setValue, setTouched}
+    } = props;
+    const val = getValue() || [];
+    return (
+        <div className='argo-field'>
+            <ApplicationSyncOptionsCreateNamespace
+                options={val}
+                onChanged={opts => {
+                    setTouched(true);
+                    setValue(opts);
+                }}
+            />
+        </div>
+    );
+});
diff --git a/util/helm/client.go b/util/helm/client.go
index 8f39bd160..ad8e0ca18 100644
--- a/util/helm/client.go
+++ b/util/helm/client.go
@@ -153,7 +153,7 @@ func (c *nativeHelmChart) ExtractChart(chart string, version *semver.Version) (s
 		_ = os.RemoveAll(tempDir)
 		return "", nil, err
 	}
-	return path.Join(tempDir, chart), io.NewCloser(func() error {
+	return path.Join(tempDir, normalizeChartName(chart)), io.NewCloser(func() error {
 		return os.RemoveAll(tempDir)
 	}), nil
 }
@@ -241,6 +241,17 @@ func newTLSConfig(creds Creds) (*tls.Config, error) {
 	return tlsConfig, nil
 }
 
+// Normalize a chart name for file system use, that is, if chart name is foo/bar/baz, returns the last component as chart name.
+func normalizeChartName(chart string) string {
+	_, nc := path.Split(chart)
+	// We do not want to return the empty string or something else related to filesystem access
+	// Instead, return original string
+	if nc == "" || nc == "." || nc == ".." {
+		return chart
+	}
+	return nc
+}
+
 func (c *nativeHelmChart) getChartPath(chart string, version *semver.Version) string {
-	return path.Join(c.repoPath, fmt.Sprintf("%s-%v.tgz", chart, version))
+	return path.Join(c.repoPath, fmt.Sprintf("%s-%v.tgz", normalizeChartName(chart), version))
 }


Test patch:
diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go
index c88474453..442e22180 100644
--- a/test/e2e/app_management_test.go
+++ b/test/e2e/app_management_test.go
@@ -1309,3 +1309,47 @@ func TestListResource(t *testing.T) {
 		Expect(SyncStatusIs(SyncStatusCodeSynced)).
 		Expect(NoConditions())
 }
+
+// Given application is set with --sync-option CreateNamespace=true
+//       application --dest-namespace does not exist
+// Verity application --dest-namespace is created
+//        application sync successful
+//        when application is deleted, --dest-namespace is not deleted
+func TestNamespaceAutoCreation(t *testing.T) {
+	updatedNamespace := getNewNamespace(t)
+	defer func() {
+		_, err := Run("", "kubectl", "delete", "namespace", updatedNamespace)
+		assert.NoError(t, err)
+	}()
+	Given(t).
+		Timeout(30).
+		Path("guestbook").
+		When().
+		Create("--sync-option", "CreateNamespace=true").
+		Then().
+		And(func(app *Application) {
+			//Make sure the namespace we are about to update to does not exist
+			_, err := Run("", "kubectl", "get", "namespace", updatedNamespace)
+			assert.Error(t, err)
+			assert.Contains(t, err.Error(), "not found")
+		}).
+		When().
+		AppSet("--dest-namespace", updatedNamespace).
+		Sync().
+		Then().
+		Expect(Success("")).
+		Expect(OperationPhaseIs(OperationSucceeded)).Expect(ResourceHealthWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, health.HealthStatusHealthy)).
+		Expect(ResourceHealthWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, health.HealthStatusHealthy)).
+		Expect(ResourceSyncStatusWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, SyncStatusCodeSynced)).
+		Expect(ResourceSyncStatusWithNamespaceIs("Deployment", "guestbook-ui", updatedNamespace, SyncStatusCodeSynced)).
+		When().
+		Delete(true).
+		Then().
+		Expect(Success("")).
+		And(func(app *Application) {
+			//Verify delete app does not delete the namespace auto created
+			output, err := Run("", "kubectl", "get", "namespace", updatedNamespace)
+			assert.NoError(t, err)
+			assert.Contains(t, output, updatedNamespace)
+		})
+}
diff --git a/util/helm/client_test.go b/util/helm/client_test.go
index 32cb1622e..aacb3f1aa 100644
--- a/util/helm/client_test.go
+++ b/util/helm/client_test.go
@@ -41,3 +41,34 @@ func Test_nativeHelmChart_ExtractChart(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, info.IsDir())
 }
+
+func Test_normalizeChartName(t *testing.T) {
+	t.Run("Test non-slashed name", func(t *testing.T) {
+		n := normalizeChartName("mychart")
+		assert.Equal(t, n, "mychart")
+	})
+	t.Run("Test single-slashed name", func(t *testing.T) {
+		n := normalizeChartName("myorg/mychart")
+		assert.Equal(t, n, "mychart")
+	})
+	t.Run("Test chart name with suborg", func(t *testing.T) {
+		n := normalizeChartName("myorg/mysuborg/mychart")
+		assert.Equal(t, n, "mychart")
+	})
+	t.Run("Test double-slashed name", func(t *testing.T) {
+		n := normalizeChartName("myorg//mychart")
+		assert.Equal(t, n, "mychart")
+	})
+	t.Run("Test invalid chart name - ends with slash", func(t *testing.T) {
+		n := normalizeChartName("myorg/")
+		assert.Equal(t, n, "myorg/")
+	})
+	t.Run("Test invalid chart name - is dot", func(t *testing.T) {
+		n := normalizeChartName("myorg/.")
+		assert.Equal(t, n, "myorg/.")
+	})
+	t.Run("Test invalid chart name - is two dots", func(t *testing.T) {
+		n := normalizeChartName("myorg/..")
+		assert.Equal(t, n, "myorg/..")
+	})
+}