Repository: argoproj/argo (go)

Issue:
The version command fails due to lack of kubeconfig
## Summary 

What happened/what you expected to happen?

When I run the version command, it exits non-zero
```
▶ ./argo-darwin-amd64 version --short
argo: v2.10.1
FATA[0000] invalid configuration: no configuration has been provided

▶ echo $?
1
```

It would be great to have the ability to just check the client version and not assume the need for a server connection. Perhaps a flag could enable that?

## Diagnostics

What version of Argo Workflows are you running?
v2.10.1


---
<!-- Issue Author: Don't delete this message to encourage other users to support your issue! -->
**Message from the maintainers**:

Impacted by this bug? Give it a 👍. We prioritise the issues with the most 👍.


Pull request:
fix(cli): Allow `argo version` without `KUBECONFIG`. Fixes #3943
Checklist:

* [x] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this is a chore.
* [x] The title of the PR is (a) [conventional](https://www.conventionalcommits.org/en/v1.0.0/), (b) states what changed, and (c) suffixes the related issues number. E.g. `"fix(controller): Updates such and such. Fixes #1234"`.  
* [x] I've signed the CLA.
* [x] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
* [x] My builds are green. Try syncing with master if they are not. 
* [x] My organization is added to [USERS.md](https://github.com/argoproj/argo/blob/master/USERS.md).

Fixes #3943

Patch:
diff --git a/Makefile b/Makefile
index c77260f2c..0dbe29a56 100644
--- a/Makefile
+++ b/Makefile
@@ -314,7 +314,7 @@ endif
 # for local we have a faster target that prints to stdout, does not use json, and can cache because it has no coverage
 .PHONY: test
 test: server/static/files.go
-	go test ./...
+	env KUBECONFIG=/dev/null go test ./...
 
 dist/$(PROFILE).yaml: $(MANIFESTS) $(E2E_MANIFESTS) /usr/local/bin/kustomize
 	mkdir -p dist
diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json
index cdd0706c7..cbf9b009c 100644
--- a/api/openapi-spec/swagger.json
+++ b/api/openapi-spec/swagger.json
@@ -2740,13 +2740,7 @@
       }
     },
     "io.argoproj.workflow.v1alpha1.Item": {
-      "description": "Item expands a single workflow step into multiple parallel steps The value of Item can be a map, string, bool, or number",
-      "type": [
-        "boolean",
-        "number",
-        "string",
-        "object"
-      ]
+      "description": "Item expands a single workflow step into multiple parallel steps The value of Item can be a map, string, bool, or number"
     },
     "io.argoproj.workflow.v1alpha1.Link": {
       "description": "A link to another app.",
diff --git a/cmd/argo/commands/version.go b/cmd/argo/commands/version.go
index 20767e459..b54521c0b 100644
--- a/cmd/argo/commands/version.go
+++ b/cmd/argo/commands/version.go
@@ -1,12 +1,13 @@
 package commands
 
 import (
+	"os"
+
 	"github.com/argoproj/pkg/errors"
 	"github.com/spf13/cobra"
 
 	"github.com/argoproj/argo"
 	"github.com/argoproj/argo/cmd/argo/commands/client"
-	"github.com/argoproj/argo/pkg/apiclient"
 	infopkg "github.com/argoproj/argo/pkg/apiclient/info"
 	cmdutil "github.com/argoproj/argo/util/cmd"
 )
@@ -19,15 +20,14 @@ func NewVersionCommand() *cobra.Command {
 		Short: "Print version information",
 		Run: func(cmd *cobra.Command, args []string) {
 			cmdutil.PrintVersion(CLIName, argo.GetVersion(), short)
-			ctx, apiClient := client.NewAPIClient()
-			serviceClient, err := apiClient.NewInfoServiceClient()
-			if err == apiclient.NoArgoServerErr {
-				return
+			if _, ok := os.LookupEnv("ARGO_SERVER"); ok {
+				ctx, apiClient := client.NewAPIClient()
+				serviceClient, err := apiClient.NewInfoServiceClient()
+				errors.CheckError(err)
+				serverVersion, err := serviceClient.GetVersion(ctx, &infopkg.GetVersionRequest{})
+				errors.CheckError(err)
+				cmdutil.PrintVersion("argo-server", *serverVersion, short)
 			}
-			errors.CheckError(err)
-			serverVersion, err := serviceClient.GetVersion(ctx, &infopkg.GetVersionRequest{})
-			errors.CheckError(err)
-			cmdutil.PrintVersion("argo-server", *serverVersion, short)
 		},
 	}
 	cmd.Flags().BoolVar(&short, "short", false, "print just the version number")
diff --git a/docs/roadmap.md b/docs/roadmap.md
index f0e75a3dc..0d100fdda 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,7 +1,7 @@
 # Roadmap
 ## SDKs
 
-Build on the [Java SDK](https://github.com/argoproj-labs/argo-java-client) by stabilising the code base. Continue to improve the stability of SDKs, for example correcting bugs in OpenAPI specifications.
+Build on the [Java SDK](https://github.com/argoproj-labs/argo-client-java) by stabilising the code base. Continue to improve the stability of SDKs, for example correcting bugs in OpenAPI specifications.
 
 ## More Ways to Trigger Workflows
 
diff --git a/docs/swagger.md b/docs/swagger.md
index dd8d0afd8..6dddb2b01 100644
--- a/docs/swagger.md
+++ b/docs/swagger.md
@@ -1128,7 +1128,7 @@ Item expands a single workflow step into multiple parallel steps The value of It
 
 | Name | Type | Description | Required |
 | ---- | ---- | ----------- | -------- |
-| io.argoproj.workflow.v1alpha1.Item | boolean,number,string,object | Item expands a single workflow step into multiple parallel steps The value of Item can be a map, string, bool, or number |  |
+| io.argoproj.workflow.v1alpha1.Item |  | Item expands a single workflow step into multiple parallel steps The value of Item can be a map, string, bool, or number |  |
 
 #### io.argoproj.workflow.v1alpha1.Link
 
diff --git a/manifests/cluster-install/argo-server-rbac/argo-server-clusterole.yaml b/manifests/cluster-install/argo-server-rbac/argo-server-clusterole.yaml
index f3550aaf3..ef532d9f0 100644
--- a/manifests/cluster-install/argo-server-rbac/argo-server-clusterole.yaml
+++ b/manifests/cluster-install/argo-server-rbac/argo-server-clusterole.yaml
@@ -34,6 +34,15 @@ rules:
       - events
     verbs:
       - watch
+      - create
+      - patch
+  - apiGroups:
+      - ""
+    resources:
+      - secrets
+      - serviceaccounts
+    verbs:
+      - get
   - apiGroups:
       - argoproj.io
     resources:
diff --git a/manifests/install.yaml b/manifests/install.yaml
index a0925ee12..9d8cc47d9 100644
--- a/manifests/install.yaml
+++ b/manifests/install.yaml
@@ -341,6 +341,15 @@ rules:
   - events
   verbs:
   - watch
+  - create
+  - patch
+- apiGroups:
+  - ""
+  resources:
+  - secrets
+  - serviceaccounts
+  verbs:
+  - get
 - apiGroups:
   - argoproj.io
   resources:
diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml
index 19a44063f..504af23ef 100644
--- a/manifests/namespace-install.yaml
+++ b/manifests/namespace-install.yaml
@@ -254,10 +254,13 @@ rules:
   - events
   verbs:
   - watch
+  - create
+  - patch
 - apiGroups:
   - ""
   resources:
   - secrets
+  - serviceaccounts
   verbs:
   - get
 - apiGroups:
diff --git a/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml b/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml
index 7c927b584..271a6e0c6 100644
--- a/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml
+++ b/manifests/namespace-install/argo-server-rbac/argo-server-role.yaml
@@ -34,10 +34,13 @@ rules:
       - events
     verbs:
       - watch
+      - create
+      - patch
   - apiGroups:
       - ""
     resources:
       - secrets
+      - serviceaccounts
     verbs:
       - get
   - apiGroups:
diff --git a/manifests/quick-start-minimal.yaml b/manifests/quick-start-minimal.yaml
index 9ec982740..04c868838 100644
--- a/manifests/quick-start-minimal.yaml
+++ b/manifests/quick-start-minimal.yaml
@@ -259,10 +259,13 @@ rules:
   - events
   verbs:
   - watch
+  - create
+  - patch
 - apiGroups:
   - ""
   resources:
   - secrets
+  - serviceaccounts
   verbs:
   - get
 - apiGroups:
diff --git a/manifests/quick-start-mysql.yaml b/manifests/quick-start-mysql.yaml
index 1422b2752..cb1327e09 100644
--- a/manifests/quick-start-mysql.yaml
+++ b/manifests/quick-start-mysql.yaml
@@ -259,10 +259,13 @@ rules:
   - events
   verbs:
   - watch
+  - create
+  - patch
 - apiGroups:
   - ""
   resources:
   - secrets
+  - serviceaccounts
   verbs:
   - get
 - apiGroups:
diff --git a/manifests/quick-start-postgres.yaml b/manifests/quick-start-postgres.yaml
index 0e39ad391..7c3ee5467 100644
--- a/manifests/quick-start-postgres.yaml
+++ b/manifests/quick-start-postgres.yaml
@@ -259,10 +259,13 @@ rules:
   - events
   verbs:
   - watch
+  - create
+  - patch
 - apiGroups:
   - ""
   resources:
   - secrets
+  - serviceaccounts
   verbs:
   - get
 - apiGroups:
diff --git a/pkg/apis/workflow/v1alpha1/item.go b/pkg/apis/workflow/v1alpha1/item.go
index 8405cd847..288cc57e6 100644
--- a/pkg/apis/workflow/v1alpha1/item.go
+++ b/pkg/apis/workflow/v1alpha1/item.go
@@ -88,9 +88,9 @@ func (i *Item) DeepCopyInto(out *Item) {
 	}
 }
 
+// See: https://github.com/kubernetes/kube-openapi/tree/master/pkg/generators
 func (i Item) OpenAPISchemaType() []string {
-	// this list should also contain `array`, but that requires `items` and that did not appear to be easy
-	return []string{"boolean", "number", "string", "object"}
+	return nil
 }
 
 func (i Item) OpenAPISchemaFormat() string { return "" }
diff --git a/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx b/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx
index 4ab22db8c..116ee1ffa 100644
--- a/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx
+++ b/ui/src/app/workflows/components/workflow-dag/workflow-dag.tsx
@@ -296,6 +296,12 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
         }
 
         const allNodes = this.props.nodes;
+        const getChildren = (nodeId: string): string[] => {
+            if (!allNodes[nodeId] || !allNodes[nodeId].children) {
+                return [];
+            }
+            return allNodes[nodeId].children;
+        };
         const pushChildren = (nodeId: string, children: string[], isExpanded: boolean): void => {
             if (!children) {
                 return;
@@ -306,7 +312,7 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
                 queue.push({
                     nodeName: children[0],
                     parent: nodeId,
-                    children: allNodes[children[0]].children
+                    children: getChildren(children[0])
                 });
                 const newChildren: string[] = children
                     .slice(1, children.length - 1)
@@ -320,7 +326,7 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
                 queue.push({
                     nodeName: children[children.length - 1],
                     parent: nodeId,
-                    children: allNodes[children[children.length - 1]].children
+                    children: getChildren(children[children.length - 1])
                 });
             } else {
                 // Node will not be collapsed
@@ -328,7 +334,7 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
                     queue.push({
                         nodeName: child,
                         parent: nodeId,
-                        children: allNodes[child].children
+                        children: getChildren(child)
                     })
                 );
             }
@@ -337,7 +343,7 @@ export class WorkflowDag extends React.Component<WorkflowDagProps, WorkflowDagRe
         const root: PrepareNode = {
             nodeName: this.props.workflowName,
             parent: '',
-            children: this.props.nodes[this.props.workflowName].children
+            children: getChildren(this.props.workflowName)
         };
 
         const queue: PrepareNode[] = [root];
diff --git a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
index af4cfb219..b3278d3cf 100644
--- a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
+++ b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx
@@ -339,15 +339,13 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
                                     if (!wfUID) {
                                         return;
                                     }
-                                    const currentlySelected = this.state.selectedWorkflows;
-                                    if (!(wfUID in currentlySelected)) {
-                                        this.updateBatchActionsDisabled(subWf, false);
+                                    const currentlySelected: Map<string, Workflow> = this.state.selectedWorkflows;
+                                    if (!currentlySelected.has(wfUID)) {
                                         currentlySelected.set(wfUID, subWf);
                                     } else {
-                                        this.updateBatchActionsDisabled(subWf, true);
                                         currentlySelected.delete(wfUID);
                                     }
-                                    this.setState({selectedWorkflows: new Map<string, models.Workflow>(currentlySelected)});
+                                    this.updateCurrentlySelectedAndBatchActions(currentlySelected);
                                 }}
                             />
                         );
@@ -361,23 +359,15 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
         );
     }
 
-    private updateBatchActionsDisabled(wf: Workflow, deselect: boolean): void {
-        const currentlyDisabled: any = this.state.batchActionDisabled;
+    private updateCurrentlySelectedAndBatchActions(newSelectedWorkflows: Map<string, Workflow>): void {
         const actions: any = Actions.WorkflowOperationsMap;
         const nowDisabled: any = {...allBatchActionsEnabled};
-        for (const action of Object.keys(currentlyDisabled)) {
-            if (deselect) {
-                for (const wfUID of Object.keys(this.state.selectedWorkflows)) {
-                    if (wfUID === wf.metadata.uid) {
-                        continue;
-                    }
-                    nowDisabled[action] = actions[action].disabled(this.state.selectedWorkflows.get(wfUID)) || nowDisabled[action];
-                }
-            } else {
-                nowDisabled[action] = actions[action].disabled(wf) || currentlyDisabled[action];
+        for (const action of Object.keys(nowDisabled)) {
+            for (const wf of Array.from(newSelectedWorkflows.values())) {
+                nowDisabled[action] = nowDisabled[action] || actions[action].disabled(wf);
             }
         }
-        this.setState({batchActionDisabled: nowDisabled});
+        this.setState({batchActionDisabled: nowDisabled, selectedWorkflows: new Map<string, models.Workflow>(newSelectedWorkflows)});
     }
 
     private renderQuery(ctx: any) {


Test patch:
diff --git a/api/openapi-spec/swagger_test.go b/api/openapi-spec/swagger_test.go
index 1d6b19319..927c4a973 100644
--- a/api/openapi-spec/swagger_test.go
+++ b/api/openapi-spec/swagger_test.go
@@ -67,7 +67,7 @@ func TestSwagger(t *testing.T) {
 	})
 	t.Run("io.argoproj.workflow.v1alpha1.Item", func(t *testing.T) {
 		definition := definitions["io.argoproj.workflow.v1alpha1.Item"].(obj)
-		assert.ElementsMatch(t, []string{"boolean", "number", "object", "string"}, definition["type"])
+		assert.Empty(t, definition["type"])
 	})
 	t.Run("io.argoproj.workflow.v1alpha1.ParallelSteps", func(t *testing.T) {
 		definition := definitions["io.argoproj.workflow.v1alpha1.ParallelSteps"].(obj)
diff --git a/test/e2e/argo_server_test.go b/test/e2e/argo_server_test.go
index 5ef070677..c2b697fd0 100644
--- a/test/e2e/argo_server_test.go
+++ b/test/e2e/argo_server_test.go
@@ -793,8 +793,10 @@ func (s *ArgoServerSuite) TestWorkflowService() {
 			Expect().
 			Status(200)
 
-		// sleep in a test is bad practice
-		time.Sleep(3 * time.Second)
+		s.Given().
+			WorkflowName(name).
+			When().
+			WaitForWorkflow()
 
 		s.e().GET("/api/v1/workflows/argo/" + name).
 			Expect().
diff --git a/test/e2e/cli_test.go b/test/e2e/cli_test.go
index e832e6de7..42c93fe9a 100644
--- a/test/e2e/cli_test.go
+++ b/test/e2e/cli_test.go
@@ -22,12 +22,19 @@ import (
 
 type CLISuite struct {
 	fixtures.E2ESuite
+	kubeConfig string
 }
 
 func (s *CLISuite) BeforeTest(suiteName, testName string) {
 	s.E2ESuite.BeforeTest(suiteName, testName)
 	_ = os.Unsetenv("ARGO_SERVER")
 	_ = os.Unsetenv("ARGO_TOKEN")
+	s.kubeConfig = os.Getenv("KUBECONFIG")
+}
+
+func (s *CLISuite) AfterTest(suiteName, testName string) {
+	_ = os.Setenv("KUBECONFIG", s.kubeConfig)
+	s.E2ESuite.AfterTest(suiteName, testName)
 }
 
 func (s *CLISuite) testNeedsOffloading() {
@@ -48,6 +55,7 @@ func (s *CLISuite) TestCompletion() {
 }
 
 func (s *CLISuite) TestVersion() {
+	_ = os.Setenv("KUBECONFIG", "/dev/null")
 	// check we can run this without error
 	s.Given().
 		RunCli([]string{"version"}, func(t *testing.T, output string, err error) {
diff --git a/test/e2e/cli_with_server_test.go b/test/e2e/cli_with_server_test.go
index 1c4d91053..14a1f715d 100644
--- a/test/e2e/cli_with_server_test.go
+++ b/test/e2e/cli_with_server_test.go
@@ -18,7 +18,6 @@ import (
 
 type CLIWithServerSuite struct {
 	CLISuite
-	kubeConfig string
 }
 
 func (s *CLIWithServerSuite) BeforeTest(suiteName, testName string) {
@@ -29,12 +28,10 @@ func (s *CLIWithServerSuite) BeforeTest(suiteName, testName string) {
 	_ = os.Setenv("ARGO_SECURE", "false")
 	_ = os.Setenv("ARGO_TOKEN", "Bearer "+token)
 	// we should not need this to run any tests
-	s.kubeConfig = os.Getenv("KUBECONFIG")
-
+	_ = os.Setenv("KUBECONFIG", "/dev/null")
 }
 
 func (s *CLIWithServerSuite) AfterTest(suiteName, testName string) {
-	_ = os.Setenv("KUBECONFIG", s.kubeConfig)
 	_ = os.Unsetenv("ARGO_SERVER")
 	_ = os.Unsetenv("ARGO_SECURE")
 	_ = os.Unsetenv("ARGO_TOKEN")