Repository: argoproj/argo (go)

Issue:
If a step outputs no logs, the Logs display in the UI will show "Waiting for data..." forever
**What happened**:
Have a step with no output. Click "Logs" in UI. Says "Waiting for data..." forever instead of telling you the logs are blank.

**What you expected to happen**:
Message saying "This container did not output any logs" or similar.

**Environment**:
- Argo version:
Using `argoproj/argoexec:latest`

```
$ argo version
argo: v2.4.3
  BuildDate: 2019-12-06T03:36:01Z
  GitCommit: cfe5f377bc3552fba90afe6db7a76edd92c753cd
  GitTreeState: clean
  GitTag: v2.4.3
  GoVersion: go1.11.5
  Compiler: gc
  Platform: linux/amd64
```
- Kubernetes version :
```
$ kubectl version -o yaml
clientVersion:
  buildDate: "2019-11-13T11:23:11Z"
  compiler: gc
  gitCommit: b3cbbae08ec52a7fc73d334838e18d17e8512749
  gitTreeState: clean
  gitVersion: v1.16.3
  goVersion: go1.12.12
  major: "1"
  minor: "16"
  platform: linux/amd64
serverVersion:
  buildDate: "2020-01-18T23:22:30Z"
  compiler: gc
  gitCommit: 59603c6e503c87169aea6106f57b9f242f64df89
  gitTreeState: clean
  gitVersion: v1.17.2
  goVersion: go1.13.5
  major: "1"
  minor: "17"
  platform: linux/amd64
```


Pull request description:
fix: Say no logs were outputted when pod is done
Fixes #2372. Thanks @foobarbecue!

Patch:
diff --git a/cmd/argoexec/commands/root.go b/cmd/argoexec/commands/root.go
index 5d0b37bf4..5649df95c 100644
--- a/cmd/argoexec/commands/root.go
+++ b/cmd/argoexec/commands/root.go
@@ -67,6 +67,7 @@ func NewRootCommand() *cobra.Command {
 }
 
 func initExecutor() *executor.WorkflowExecutor {
+	log.WithField("version", argo.GetVersion()).Info("Starting Workflow Executor")
 	config, err := clientConfig.ClientConfig()
 	checkErr(err)
 
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 78ab3fdeb..5fdb80b26 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -11,56 +11,16 @@ the workflows. Here are the requirements and steps to run the workflows.
 
 ## 1. Download the Argo CLI
 
-### Mac
+Download the latest Argo CLI from our [releases page](https://github.com/argoproj/argo/releases).
 
-Available via `brew`
-```sh
-brew install argoproj/tap/argo
-```
-And via `curl`
-```sh
-# Download the binary
-curl -sLO https://github.com/argoproj/argo/releases/download/v2.6.0/argo-darwin-amd64
-
-# Make binary executable
-chmod +x argo-darwin-amd64
-
-# Move binary to path
-mv ./argo-darwin-amd64 /usr/local/bin/argo
-
-# Test installation
-argo version
-```
-
-### Linux
-
-Available via `curl`
-```sh
-# Download the binary
-curl -sLO https://github.com/argoproj/argo/releases/download/v2.6.0/argo-linux-amd64
-
-# Make binary executable
-chmod +x argo-linux-amd64
-
-# Move binary to path
-mv ./argo-linux-amd64 /usr/local/bin/argo
-
-# Test installation
-argo version
-```
-
-### Binaries
-
-You can download the latest and previous Argo binaries from our [releases page](https://github.com/argoproj/argo/releases/).
-
-## 2. Install the Controller and UI
+## 2. Install the Controller
 
 ```sh
 kubectl create namespace argo
-kubectl apply -n argo -f https://raw.githubusercontent.com/argoproj/argo/v2.6.0/manifests/install.yaml
+kubectl apply -n argo -f https://raw.githubusercontent.com/argoproj/argo/stable/manifests/install.yaml
 ```
 
-Namespaced installs as well as installs with MinIO and/or a database built in [are also available](https://github.com/argoproj/argo/tree/v2.6.0/manifests). 
+Namespaced installs as well as installs with MinIO and/or a database built in [are also available](https://github.com/argoproj/argo/tree/stable/manifests). 
 
 Examples below will assume you've installed argo in the `argo` namespace. If you have not, adjust 
 the commands accordingly.
diff --git a/server/apiserver/argoserver.go b/server/apiserver/argoserver.go
index 20b1041c7..4a668005c 100644
--- a/server/apiserver/argoserver.go
+++ b/server/apiserver/argoserver.go
@@ -22,6 +22,7 @@ import (
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/yaml"
 
+	"github.com/argoproj/argo"
 	"github.com/argoproj/argo/errors"
 	"github.com/argoproj/argo/persist/sqldb"
 	cronworkflowpkg "github.com/argoproj/argo/pkg/apiclient/cronworkflow"
@@ -108,7 +109,7 @@ func (ao ArgoServerOpts) ValidateOpts() error {
 }
 
 func (as *argoServer) Run(ctx context.Context, port int, browserOpenFunc func(string)) {
-
+	log.WithField("version", argo.GetVersion()).Info("Starting Argo Server")
 	configMap, err := as.rsyncConfig()
 	if err != nil {
 		log.Fatal(err)
diff --git a/ui/README.md b/ui/README.md
index 6987e1188..9bdaf80a7 100644
--- a/ui/README.md
+++ b/ui/README.md
@@ -1,10 +1,12 @@
 # Argo UI
 
-![Argo Image](https://github.com/argoproj/argo/blob/master/argo.png?raw=true)
+![Argo Image](../docs/assets/argo.png)
 
 A web-based UI for the Argo Workflow engine. 
 
 The UI has the following features:
 * View live Argo Workflows running in the cluster
 * View completed Argo Workflows
+* Create new Argo Workflow templates
+* View and create Argo Cron Workflows
 * View container logs
diff --git a/ui/src/app/shared/services/workflows-service.ts b/ui/src/app/shared/services/workflows-service.ts
index 01613ead5..9429c6c75 100644
--- a/ui/src/app/shared/services/workflows-service.ts
+++ b/ui/src/app/shared/services/workflows-service.ts
@@ -68,7 +68,7 @@ export class WorkflowsService {
         return requests
             .loadEventSource(
                 `api/v1/workflows/${workflow.metadata.namespace}/${workflow.metadata.name}/${nodeId}/log` +
-                    `?logOptions.container=${container}&logOptions.tailLines=20&logOptions.follow=true`
+                    `?logOptions.container=${container}&logOptions.follow=true`
             )
             .pipe(
                 map(line => JSON.parse(line).result.content),
diff --git a/ui/src/app/workflows/components/workflow-logs-viewer/workflow-logs-viewer.tsx b/ui/src/app/workflows/components/workflow-logs-viewer/workflow-logs-viewer.tsx
index c4c261ebf..9e61040d5 100644
--- a/ui/src/app/workflows/components/workflow-logs-viewer/workflow-logs-viewer.tsx
+++ b/ui/src/app/workflows/components/workflow-logs-viewer/workflow-logs-viewer.tsx
@@ -68,11 +68,12 @@ export class WorkflowLogsViewer extends React.Component<WorkflowLogsViewerProps,
                             <i className='fa fa-exclamation-triangle status-icon--failed' /> Failed to load logs: {this.state.error.message}
                         </p>
                     )}
-                    {!this.state.error && this.state.lines.length === 0 && (
+                    {!this.state.error && this.state.lines.length === 0 && this.isCurrentNodeRunningOrPending() && (
                         <p>
                             <i className='fa fa-circle-notch fa-spin' /> Waiting for data...
                         </p>
                     )}
+                    {!this.state.error && this.state.lines.length === 0 && !this.isCurrentNodeRunningOrPending() && <p>Pod did not output any logs.</p>}
                     {this.state.lines.length > 0 && (
                         <div className='log-box'>
                             <i className='fa fa-chevron-down' />
@@ -100,4 +101,8 @@ export class WorkflowLogsViewer extends React.Component<WorkflowLogsViewerProps,
             </div>
         );
     }
+
+    private isCurrentNodeRunningOrPending(): boolean {
+        return this.props.workflow.status.nodes[this.props.nodeId].phase === 'Running' || this.props.workflow.status.nodes[this.props.nodeId].phase === 'Pending';
+    }
 }
diff --git a/workflow/config/config.go b/workflow/config/config.go
index a22fa2fbd..f6424862d 100644
--- a/workflow/config/config.go
+++ b/workflow/config/config.go
@@ -64,6 +64,8 @@ type WorkflowControllerConfig struct {
 
 	// Config customized Docker Sock path
 	DockerSockPath string `json:"dockerSockPath,omitempty"`
+
+	DefautWorkflowSpec *wfv1.WorkflowSpec `json:"workflowDefaults,omitempty"`
 }
 
 // KubeConfig is used for wait & init sidecar containers to communicate with a k8s apiserver by a outofcluster method,
diff --git a/workflow/controller/controller.go b/workflow/controller/controller.go
index 38fc0131b..a35b23fcc 100644
--- a/workflow/controller/controller.go
+++ b/workflow/controller/controller.go
@@ -2,6 +2,7 @@ package controller
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"os"
 	"strings"
@@ -17,6 +18,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/selection"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/strategicpatch"
 	"k8s.io/apimachinery/pkg/util/wait"
 	"k8s.io/apimachinery/pkg/watch"
 	"k8s.io/client-go/kubernetes"
@@ -45,8 +47,10 @@ type WorkflowController struct {
 	// namespace of the workflow controller
 	namespace        string
 	managedNamespace string
+
 	// configMap is the name of the config map in which to derive configuration of the controller from
 	configMap string
+
 	// Config is the workflow controller's configuration
 	Config config.WorkflowControllerConfig
 
@@ -158,7 +162,7 @@ func (wfc *WorkflowController) Run(ctx context.Context, wfWorkers, podWorkers in
 	defer wfc.wfQueue.ShutDown()
 	defer wfc.podQueue.ShutDown()
 
-	log.Infof("Workflow Controller (version: %s) starting", argo.GetVersion())
+	log.WithField("version", argo.GetVersion()).Info("Starting Workflow Controller")
 	log.Infof("Workers: workflow: %d, pod: %d", wfWorkers, podWorkers)
 	log.Info("Watch Workflow controller config map updates")
 	_, err := wfc.watchControllerConfigMap(ctx)
@@ -355,6 +359,14 @@ func (wfc *WorkflowController) processNextItem() bool {
 		wfc.throttler.Remove(key)
 		return true
 	}
+	err = wfc.addingWorkflowDefaultValueIfValueNotExist(wf)
+	if err != nil {
+		log.Warnf("Failed to unmarshal key '%s' to workflow object: %v", key, err)
+		woc := newWorkflowOperationCtx(wf, wfc)
+		woc.markWorkflowFailed(fmt.Sprintf("invalid spec: %s", err.Error()))
+		woc.persistUpdates()
+		wfc.throttler.Remove(key)
+	}
 
 	if wf.ObjectMeta.Labels[common.LabelKeyCompleted] == "true" {
 		wfc.throttler.Remove(key)
@@ -362,9 +374,7 @@ func (wfc *WorkflowController) processNextItem() bool {
 		// but we are still draining the controller's workflow workqueue
 		return true
 	}
-
 	woc := newWorkflowOperationCtx(wf, wfc)
-
 	// Loading running workflow from persistence storage if nodeStatusOffload enabled
 	if wf.Status.IsOffloadNodeStatus() {
 		nodes, err := wfc.offloadNodeStatusRepo.Get(string(wf.UID), wf.GetOffloadNodeStatusVersion())
@@ -417,6 +427,33 @@ func (wfc *WorkflowController) processNextItem() bool {
 	return true
 }
 
+// addingWorkflowDefaultValueIfValueNotExist sets values in the workflow.Spec with defaults from the
+// workflowController. Values in the workflow will be given the upper hand over the defaults.
+// The defaults for the workflow controller is set in the WorkflowController.Config.DefautWorkflowSpec
+func (wfc *WorkflowController) addingWorkflowDefaultValueIfValueNotExist(wf *wfv1.Workflow) error {
+	//var workflowSpec *wfv1.WorkflowSpec = &wf.Spec
+	if wfc.Config.DefautWorkflowSpec != nil {
+		defaultsSpec, err := json.Marshal(*wfc.Config.DefautWorkflowSpec)
+		if err != nil {
+			return err
+		}
+		workflowSpec, err := json.Marshal(wf.Spec)
+		if err != nil {
+			return err
+		}
+		// https://github.com/kubernetes/apimachinery/blob/2373d029717c4d169463414a6127cd1d0d12680e/pkg/util/strategicpatch/patch.go#L94
+		new, err := strategicpatch.StrategicMergePatch(defaultsSpec, workflowSpec, wfv1.WorkflowSpec{})
+		if err != nil {
+			return err
+		}
+		err = json.Unmarshal(new, &wf.Spec)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (wfc *WorkflowController) podWorker() {
 	for wfc.processNextPodItem() {
 	}


Test patch:
diff --git a/workflow/controller/controller_test.go b/workflow/controller/controller_test.go
index d981f60b4..dbc363965 100644
--- a/workflow/controller/controller_test.go
+++ b/workflow/controller/controller_test.go
@@ -43,6 +43,55 @@ spec:
       args: ["hello world"]
 `
 
+var testDefaultWf = `
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  name: hello-world
+spec:
+  entrypoint: whalesay
+  serviceAccountName: whalesay
+  templates:
+  - name: whalesay
+    metadata:
+      annotations:
+        annotationKey1: "annotationValue1"
+        annotationKey2: "annotationValue2"
+      labels:
+        labelKey1: "labelValue1"
+        labelKey2: "labelValue2"
+    container:
+      image: docker/whalesay:latest
+      command: [cowsay]
+      args: ["hello world"]
+`
+
+var testDefaultWfTTL = `
+apiVersion: argoproj.io/v1alpha1
+kind: Workflow
+metadata:
+  name: hello-world
+spec:
+  entrypoint: whalesay
+  serviceAccountName: whalesay
+  ttlSecondsAfterFinished: 7
+  ttlStrategy:
+    secondsAfterCompletion: 5
+  templates:
+  - name: whalesay
+    metadata:
+      annotations:
+        annotationKey1: "annotationValue1"
+        annotationKey2: "annotationValue2"
+      labels:
+        labelKey1: "labelValue1"
+        labelKey2: "labelValue2"
+    container:
+      image: docker/whalesay:latest
+      command: [cowsay]
+      args: ["hello world"]
+`
+
 func newController() *WorkflowController {
 	wfclientset := fakewfclientset.NewSimpleClientset()
 	informerFactory := wfextv.NewSharedInformerFactory(wfclientset, 10*time.Minute)
@@ -65,6 +114,68 @@ func newController() *WorkflowController {
 	}
 }
 
+func newControllerWithDefaults() *WorkflowController {
+	wfclientset := fakewfclientset.NewSimpleClientset()
+	informerFactory := wfextv.NewSharedInformerFactory(wfclientset, 10*time.Minute)
+	wftmplInformer := informerFactory.Argoproj().V1alpha1().WorkflowTemplates()
+	ctx := context.Background()
+	go wftmplInformer.Informer().Run(ctx.Done())
+	if !cache.WaitForCacheSync(ctx.Done(), wftmplInformer.Informer().HasSynced) {
+		panic("Timed out waiting for caches to sync")
+	}
+	myBool := true
+	return &WorkflowController{
+		Config: config.WorkflowControllerConfig{
+			ExecutorImage: "executor:latest",
+			DefautWorkflowSpec: &wfv1.WorkflowSpec{
+				HostNetwork: &myBool,
+			},
+		},
+		kubeclientset:  fake.NewSimpleClientset(),
+		wfclientset:    wfclientset,
+		completedPods:  make(chan string, 512),
+		wftmplInformer: wftmplInformer,
+		wfQueue:        workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		wfArchive:      sqldb.NullWorkflowArchive,
+	}
+}
+
+func newControllerWithComplexDefaults() *WorkflowController {
+	wfclientset := fakewfclientset.NewSimpleClientset()
+	informerFactory := wfextv.NewSharedInformerFactory(wfclientset, 10*time.Minute)
+	wftmplInformer := informerFactory.Argoproj().V1alpha1().WorkflowTemplates()
+	ctx := context.Background()
+	go wftmplInformer.Informer().Run(ctx.Done())
+	if !cache.WaitForCacheSync(ctx.Done(), wftmplInformer.Informer().HasSynced) {
+		panic("Timed out waiting for caches to sync")
+	}
+	myBool := true
+	var ten int32 = 10
+	var seven int32 = 10
+	return &WorkflowController{
+		Config: config.WorkflowControllerConfig{
+			ExecutorImage: "executor:latest",
+			DefautWorkflowSpec: &wfv1.WorkflowSpec{
+				HostNetwork:        &myBool,
+				Entrypoint:         "good_entrypoint",
+				ServiceAccountName: "my_service_account",
+				TTLStrategy: &wfv1.TTLStrategy{
+					SecondsAfterCompletion: &ten,
+					SecondsAfterSuccess:    &ten,
+					SecondsAfterFailure:    &ten,
+				},
+				TTLSecondsAfterFinished: &seven,
+			},
+		},
+		kubeclientset:  fake.NewSimpleClientset(),
+		wfclientset:    wfclientset,
+		completedPods:  make(chan string, 512),
+		wftmplInformer: wftmplInformer,
+		wfQueue:        workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
+		wfArchive:      sqldb.NullWorkflowArchive,
+	}
+}
+
 func unmarshalWF(yamlStr string) *wfv1.Workflow {
 	var wf wfv1.Workflow
 	err := yaml.Unmarshal([]byte(yamlStr), &wf)
@@ -98,3 +209,52 @@ func makePodsPhase(t *testing.T, phase apiv1.PodPhase, kubeclientset kubernetes.
 		}
 	}
 }
+
+func TestAddingWorkflowDefaultValueIfValueNotExist(t *testing.T) {
+	assert.Equal(t, "hello", "hello")
+	ans := true
+	controller := newController()
+	workflow := unmarshalWF(helloWorldWf)
+	err := controller.addingWorkflowDefaultValueIfValueNotExist(workflow)
+	assert.NoError(t, err)
+	assert.Equal(t, workflow, unmarshalWF(helloWorldWf))
+	controllerDefaults := newControllerWithDefaults()
+	defautWorkflowSpec := unmarshalWF(helloWorldWf)
+	err = controllerDefaults.addingWorkflowDefaultValueIfValueNotExist(defautWorkflowSpec)
+	assert.NoError(t, err)
+	assert.Equal(t, defautWorkflowSpec.Spec.HostNetwork, &ans)
+	assert.NotEqual(t, defautWorkflowSpec, unmarshalWF(helloWorldWf))
+	assert.Equal(t, *defautWorkflowSpec.Spec.HostNetwork, true)
+}
+
+func TestAddingWorkflowDefaultComplex(t *testing.T) {
+	assert.Equal(t, "hello", "hello")
+	controller := newControllerWithComplexDefaults()
+	workflow := unmarshalWF(testDefaultWf)
+	var ten int32 = 10
+	assert.Equal(t, workflow.Spec.Entrypoint, "whalesay")
+	assert.Nil(t, workflow.Spec.TTLStrategy)
+	err := controller.addingWorkflowDefaultValueIfValueNotExist(workflow)
+	assert.NoError(t, err)
+	assert.NotEqual(t, workflow, unmarshalWF(testDefaultWf))
+	assert.Equal(t, workflow.Spec.Entrypoint, "whalesay")
+	assert.Equal(t, workflow.Spec.ServiceAccountName, "whalesay")
+	assert.Equal(t, *workflow.Spec.TTLStrategy.SecondsAfterFailure, ten)
+}
+
+func TestAddingWorkflowDefaultComplexTwo(t *testing.T) {
+	assert.Equal(t, "hello", "hello")
+	controller := newControllerWithComplexDefaults()
+	workflow := unmarshalWF(testDefaultWfTTL)
+	var ten int32 = 10
+	var seven int32 = 7
+	var five int32 = 5
+	err := controller.addingWorkflowDefaultValueIfValueNotExist(workflow)
+	assert.NoError(t, err)
+	assert.NotEqual(t, workflow, unmarshalWF(testDefaultWfTTL))
+	assert.Equal(t, workflow.Spec.Entrypoint, "whalesay")
+	assert.Equal(t, workflow.Spec.ServiceAccountName, "whalesay")
+	assert.Equal(t, *workflow.Spec.TTLStrategy.SecondsAfterCompletion, five)
+	assert.Equal(t, *workflow.Spec.TTLStrategy.SecondsAfterFailure, ten)
+	assert.Equal(t, *workflow.Spec.TTLSecondsAfterFinished, seven)
+}