Repository: istio/istio (go)

Issue:
NPE in reconciler
install istio via below iop:

```
apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
metadata:
  name: istio
spec:
  values:
    global:
      meshID: mesh1
      multiCluster:
        clusterName: "cluster1"
      network: "kube-network"
```
fails with `panic: assignment to entry in nil map`

Pull request description:
init Labels map
fix #30725

Patch:
diff --git a/istioctl/cmd/dashboard.go b/istioctl/cmd/dashboard.go
index 06698d730d..b216b59607 100644
--- a/istioctl/cmd/dashboard.go
+++ b/istioctl/cmd/dashboard.go
@@ -261,7 +261,7 @@ func envoyDashCmd() *cobra.Command {
 
 			var podName, ns string
 			if labelSelector != "" {
-				pl, err := client.PodsForSelector(context.TODO(), handlers.HandleNamespace(namespace, defaultNamespace), labelSelector)
+				pl, err := client.PodsForSelector(context.TODO(), handlers.HandleNamespace(addonNamespace, defaultNamespace), labelSelector)
 				if err != nil {
 					return fmt.Errorf("not able to locate pod with selector %s: %v", labelSelector, err)
 				}
@@ -279,7 +279,7 @@ func envoyDashCmd() *cobra.Command {
 				ns = pl.Items[0].Namespace
 			} else {
 				podName, ns, err = handlers.InferPodInfoFromTypedResource(args[0],
-					handlers.HandleNamespace(namespace, defaultNamespace),
+					handlers.HandleNamespace(addonNamespace, defaultNamespace),
 					client.UtilFactory())
 				if err != nil {
 					return err
@@ -475,6 +475,8 @@ func dashboard() *cobra.Command {
 
 	envoy := envoyDashCmd()
 	envoy.PersistentFlags().StringVarP(&labelSelector, "selector", "l", "", "Label selector")
+	envoy.PersistentFlags().StringVarP(&addonNamespace, "namespace", "n", istioNamespace,
+		"Namespace where the addon is running, if not specified, istio-system would be used")
 	dashboardCmd.AddCommand(envoy)
 
 	controlz := controlZDashCmd()
diff --git a/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.go b/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.go
index 478672b3a7..6ed7ddfc15 100644
--- a/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.go
+++ b/operator/pkg/controller/istiocontrolplane/istiocontrolplane_controller.go
@@ -29,6 +29,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -324,6 +325,15 @@ func (r *ReconcileIstioOperator) Reconcile(_ context.Context, request reconcile.
 		}
 		globalValues["jwtPolicy"] = string(jwtPolicy)
 	}
+	client, err := kubernetes.NewForConfig(r.config)
+	if err != nil {
+		return reconcile.Result{}, err
+	}
+	err = util.ValidateIOPCAConfig(client, iopMerged)
+	if err != nil {
+		scope.Errorf(errdict.OperatorFailedToConfigure, "failed to apply IstioOperator resources. Error %s", err)
+		return reconcile.Result{}, err
+	}
 	reconciler, err := helmreconciler.NewHelmReconciler(r.client, r.config, iopMerged, nil)
 	if err != nil {
 		return reconcile.Result{}, err
diff --git a/operator/pkg/helmreconciler/reconciler.go b/operator/pkg/helmreconciler/reconciler.go
index 6b90075777..2cc2696e0a 100644
--- a/operator/pkg/helmreconciler/reconciler.go
+++ b/operator/pkg/helmreconciler/reconciler.go
@@ -509,7 +509,8 @@ func CreateNamespace(cs kubernetes.Interface, namespace string, network string)
 	if err != nil {
 		if errors.IsNotFound(err) {
 			ns := &v1.Namespace{ObjectMeta: v12.ObjectMeta{
-				Name: namespace,
+				Name:   namespace,
+				Labels: map[string]string{},
 			}}
 			if network != "" {
 				ns.Labels[label.TopologyNetwork.Name] = network
diff --git a/operator/pkg/manifest/shared.go b/operator/pkg/manifest/shared.go
index 6f19b85ea4..9b1d60c0ed 100644
--- a/operator/pkg/manifest/shared.go
+++ b/operator/pkg/manifest/shared.go
@@ -23,6 +23,7 @@ import (
 	"strings"
 
 	"github.com/ghodss/yaml"
+	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
 
 	"istio.io/api/operator/v1alpha1"
@@ -188,6 +189,18 @@ func GenIOPFromProfile(profileOrPath, fileOverlayYAML string, setFlags []string,
 	if err != nil {
 		return "", nil, err
 	}
+
+	// Validate Final IOP config against K8s cluster
+	if kubeConfig != nil {
+		client, err := kubernetes.NewForConfig(kubeConfig)
+		if err != nil {
+			return "", nil, err
+		}
+		err = util.ValidateIOPCAConfig(client, finalIOP)
+		if err != nil {
+			return "", nil, err
+		}
+	}
 	// InstallPackagePath may have been a URL, change to extracted to local file path.
 	finalIOP.Spec.InstallPackagePath = installPackagePath
 	return util.ToYAMLWithJSONPB(finalIOP), finalIOP, nil
@@ -364,7 +377,6 @@ func getClusterSpecificValues(config *rest.Config, force bool, l clog.Logger) (s
 	} else {
 		overlays = append(overlays, jwt)
 	}
-
 	return makeTreeFromSetList(overlays)
 }
 
diff --git a/operator/pkg/util/k8s.go b/operator/pkg/util/k8s.go
index 84eaef2f3c..85bd6cdb04 100644
--- a/operator/pkg/util/k8s.go
+++ b/operator/pkg/util/k8s.go
@@ -16,10 +16,14 @@ package util
 
 import (
 	"fmt"
+	"strconv"
 
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
+
+	iopv1alpha1 "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
 )
 
 type JWTPolicy string
@@ -68,3 +72,32 @@ func DetectSupportedJWTPolicy(config *rest.Config) (JWTPolicy, error) {
 func GKString(gvk schema.GroupKind) string {
 	return fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
 }
+
+// ValidateIOPCAConfig validates if the IstioOperator CA configs are applicable to the K8s cluster
+func ValidateIOPCAConfig(client kubernetes.Interface, iop *iopv1alpha1.IstioOperator) error {
+	globalI := iop.Spec.Values["global"]
+	global, ok := globalI.(map[string]interface{})
+	if !ok {
+		// This means no explicit global configuration. Still okay
+		return nil
+	}
+	ca, ok := global["pilotCertProvider"].(string)
+	if !ok {
+		// This means the default pilotCertProvider is being used
+		return nil
+	}
+	if ca == "kubernetes" {
+		versionInfo, err := client.Discovery().ServerVersion()
+		if err != nil {
+			return fmt.Errorf("failed to determine support for K8s legacy signer. Use the --force flag to ignore this: %v", err)
+		}
+		minor, _ := strconv.Atoi(versionInfo.Minor)
+		major, _ := strconv.Atoi(versionInfo.Major)
+
+		if minor >= 22 || major > 1 {
+			return fmt.Errorf("configuration PILOT_CERT_PROVIDER=%s not supported in k8s minor version %v."+
+				"Please pick another value for PILOT_CERT_PROVIDER", ca, minor)
+		}
+	}
+	return nil
+}
diff --git a/pilot/cmd/pilot-agent/main.go b/pilot/cmd/pilot-agent/main.go
index bfcc4e170e..ed84c70d4d 100644
--- a/pilot/cmd/pilot-agent/main.go
+++ b/pilot/cmd/pilot-agent/main.go
@@ -113,7 +113,7 @@ var (
 	).Get()
 
 	caProviderEnv = env.RegisterStringVar("CA_PROVIDER", "Citadel", "name of authentication provider").Get()
-	caEndpointEnv = env.RegisterStringVar("CA_ADDR", "", "Address of the spiffee certificate provider. Defaults to discoveryAddress").Get()
+	caEndpointEnv = env.RegisterStringVar("CA_ADDR", "", "Address of the spiffe certificate provider. Defaults to discoveryAddress").Get()
 
 	trustDomainEnv = env.RegisterStringVar("TRUST_DOMAIN", "cluster.local",
 		"The trust domain for spiffe certificates").Get()
diff --git a/pkg/errdict/errdict.go b/pkg/errdict/errdict.go
index 8ecba9527a..79004c62f6 100644
--- a/pkg/errdict/errdict.go
+++ b/pkg/errdict/errdict.go
@@ -93,6 +93,14 @@ var (
 		Action: "Check that the IstioOperator resource has the correct syntax. " +
 			ActionIfErrSureCorrectConfigContactSupport,
 	}
+	OperatorFailedToConfigure = &structured.Error{
+		MoreInfo: "the IstioOperator Resource could not be applied on the cluster " +
+			"because of incompatible Kubernetes settings",
+		Impact:      OperatorImpactNoUpdates,
+		LikelyCause: formatCauses(LikelyCauseConfiguration),
+		Action: "Esnure that IstioOperator config is compatible with current Kubernetes version." +
+			ActionIfErrSureCorrectConfigContactSupport,
+	}
 )
 
 func fixFormat(s string) string {
diff --git a/releasenotes/notes/22161.yaml b/releasenotes/notes/22161.yaml
new file mode 100644
index 0000000000..e318bbd240
--- /dev/null
+++ b/releasenotes/notes/22161.yaml
@@ -0,0 +1,9 @@
+apiVersion: release-notes/v2
+kind: bug-fix
+area: security
+issue:
+- 22161
+releaseNotes:
+- |
+  **Updated** Configuration to sign Istiod certificates using Kubernetes CA (PILOT_CERT_PROVIDER=kubernetes) will not be honored in
+  clusters with version 1.22 and greater. 
\ No newline at end of file


Test patch:
diff --git a/istioctl/cmd/dashboard_test.go b/istioctl/cmd/dashboard_test.go
index 5d73328223..256c266f1c 100644
--- a/istioctl/cmd/dashboard_test.go
+++ b/istioctl/cmd/dashboard_test.go
@@ -111,6 +111,11 @@ func TestDashboard(t *testing.T) {
 			expectedRegexp: regexp.MustCompile(".*http://localhost:3456"),
 			wantException:  false,
 		},
+		{ // case 17
+			args:           strings.Split("dashboard envoy --browser=false pod-123456-7890 -n istio-system", " "),
+			expectedRegexp: regexp.MustCompile("http://localhost:3456"),
+			wantException:  false,
+		},
 	}
 
 	for i, c := range cases {
diff --git a/operator/pkg/util/k8s_test.go b/operator/pkg/util/k8s_test.go
new file mode 100644
index 0000000000..a4c4715de8
--- /dev/null
+++ b/operator/pkg/util/k8s_test.go
@@ -0,0 +1,105 @@
+// Copyright Istio 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 util
+
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/version"
+	fakediscovery "k8s.io/client-go/discovery/fake"
+	"k8s.io/client-go/kubernetes/fake"
+
+	pkgAPI "istio.io/istio/operator/pkg/apis/istio/v1alpha1"
+	"istio.io/istio/pkg/util/gogoprotomarshal"
+)
+
+var (
+	o1 = `
+apiVersion: install.istio.io/v1alpha1
+kind: IstioOperator
+spec:
+  values:
+    global:
+      pilotCertProvider: kubernetes
+`
+	o2 = `
+apiVersion: install.istio.io/v1alpha1
+kind: IstioOperator
+spec:
+  values:
+    global:
+      pilotCertProvider: istiod
+`
+	o3 = `
+apiVersion: install.istio.io/v1alpha1
+kind: IstioOperator
+spec:
+  values:
+`
+)
+
+func TestValidateIOPCAConfig(t *testing.T) {
+	var err error
+	k8sClient := fake.NewSimpleClientset()
+
+	tests := []struct {
+		major        string
+		minor        string
+		expErr       bool
+		operatorYaml string
+	}{
+		{
+			major:        "1",
+			minor:        "16",
+			expErr:       false,
+			operatorYaml: o1,
+		},
+		{
+			major:        "1",
+			minor:        "22",
+			expErr:       true,
+			operatorYaml: o1,
+		},
+		{
+			major:        "1",
+			minor:        "23",
+			expErr:       false,
+			operatorYaml: o2,
+		},
+		{
+			major:        "1",
+			minor:        "24",
+			expErr:       false,
+			operatorYaml: o3,
+		},
+	}
+
+	for i, tt := range tests {
+		k8sClient.Discovery().(*fakediscovery.FakeDiscovery).FakedServerVersion = &version.Info{
+			Major: tt.major,
+			Minor: tt.minor,
+		}
+		op := &pkgAPI.IstioOperator{}
+		err = gogoprotomarshal.ApplyYAML(tt.operatorYaml, op)
+		if err != nil {
+			t.Fatalf("Failure in test case %v. Error %s", i, err)
+		}
+		err = ValidateIOPCAConfig(k8sClient, op)
+		if !tt.expErr && err != nil {
+			t.Fatalf("Failure in test case %v. Expected No Error. Got %s", i, err)
+		} else if tt.expErr && err == nil {
+			t.Fatalf("Failure in test case %v. Expected Error. Got No error", i)
+		}
+	}
+}
diff --git a/pilot/pkg/xds/bench_test.go b/pilot/pkg/xds/bench_test.go
index 944c8b52a7..c536457764 100644
--- a/pilot/pkg/xds/bench_test.go
+++ b/pilot/pkg/xds/bench_test.go
@@ -93,6 +93,14 @@ var testCases = []ConfigInput{
 		Name:     "virtualservice",
 		Services: 100,
 	},
+	{
+		Name:     "authorizationpolicy",
+		Services: 100,
+	},
+	{
+		Name:     "peerauthentication",
+		Services: 100,
+	},
 }
 
 func disableLogging() {
diff --git a/pilot/pkg/xds/testdata/benchmarks/authorizationpolicy.yaml b/pilot/pkg/xds/testdata/benchmarks/authorizationpolicy.yaml
new file mode 100644
index 0000000000..d0a2a73ec1
--- /dev/null
+++ b/pilot/pkg/xds/testdata/benchmarks/authorizationpolicy.yaml
@@ -0,0 +1,37 @@
+# Set up a Service associated with our proxy, which will run as 1.1.1.1 IP
+apiVersion: networking.istio.io/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: proxy-service-instance
+spec:
+  hosts:
+    - example.com
+  ports:
+    - number: 80
+      name: http
+      protocol: HTTP
+    - number: 443
+      name: https
+      protocol: HTTPS
+  resolution: STATIC
+  endpoints:
+    - address: 1.1.1.1
+      labels:
+        istio.io/benchmark: "true"
+---
+  {{- range $i := until .Services }}
+apiVersion: security.istio.io/v1beta1
+kind: AuthorizationPolicy
+metadata:
+  name: authn-{{$i}}
+spec:
+  action: DENY
+  rules:
+    - from:
+        - source:
+            namespaces: ["default"]
+      to:
+        - operation:
+            methods: ["POST"]
+---
+  {{- end }}
diff --git a/pilot/pkg/xds/testdata/benchmarks/peerauthentication.yaml b/pilot/pkg/xds/testdata/benchmarks/peerauthentication.yaml
new file mode 100644
index 0000000000..f8a904b690
--- /dev/null
+++ b/pilot/pkg/xds/testdata/benchmarks/peerauthentication.yaml
@@ -0,0 +1,31 @@
+# Set up a Service associated with our proxy, which will run as 1.1.1.1 IP
+apiVersion: networking.istio.io/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: proxy-service-instance
+spec:
+  hosts:
+    - example.com
+  ports:
+    - number: 80
+      name: http
+      protocol: HTTP
+    - number: 443
+      name: https
+      protocol: HTTPS
+  resolution: STATIC
+  endpoints:
+    - address: 1.1.1.1
+      labels:
+        istio.io/benchmark: "true"
+---
+  {{- range $i := until .Services }}
+apiVersion: security.istio.io/v1beta1
+kind: PeerAuthentication
+metadata:
+  name: authz-{{$i}}
+spec:
+  mtls:
+    mode: STRICT
+---
+  {{- end }}
diff --git a/pkg/test/framework/components/cluster/clusterboot/factory.go b/pkg/test/framework/components/cluster/clusterboot/factory.go
index 065069385c..d3748b0abf 100644
--- a/pkg/test/framework/components/cluster/clusterboot/factory.go
+++ b/pkg/test/framework/components/cluster/clusterboot/factory.go
@@ -59,6 +59,7 @@ func (a factory) Build() (cluster.Clusters, error) {
 	}()
 
 	allClusters := make(cluster.Map)
+	var clusters cluster.Clusters
 	for i, config := range a.configs {
 		c, err := buildCluster(config, allClusters)
 		if err != nil {
@@ -70,14 +71,14 @@ func (a factory) Build() (cluster.Clusters, error) {
 			continue
 		}
 		allClusters[c.Name()] = c
+		clusters = append(clusters, c)
 	}
 	if errs != nil {
 		return nil, errs
 	}
 
-	// validate the topology has no open edges and build the return slice
-	var clusters cluster.Clusters
-	for n, c := range allClusters {
+	// validate the topology has no open edges
+	for _, c := range allClusters {
 		if primary, ok := allClusters[c.PrimaryName()]; !ok {
 			errs = multierror.Append(errs, fmt.Errorf("primary %s for %s is not in the topology", c.PrimaryName(), c.Name()))
 			continue
@@ -92,14 +93,15 @@ func (a factory) Build() (cluster.Clusters, error) {
 			errs = multierror.Append(errs, fmt.Errorf("config %s for %s is of kind %s, primaries must be Kubernetes", config.Name(), c.Name(), config.Kind()))
 			continue
 		}
-		scopes.Framework.Infof("Built Cluster: %s", n)
-		scopes.Framework.Infof("\n" + c.String())
-		clusters = append(clusters, c)
 	}
 	if errs != nil {
 		return nil, errs
 	}
 
+	for _, c := range clusters {
+		scopes.Framework.Infof("Built Cluster:\n%s", c.String())
+	}
+
 	scopes.Framework.Infof("=== DONE: Building clusters ===")
 	return clusters, errs
 }
diff --git a/pkg/test/framework/components/cluster/config.go b/pkg/test/framework/components/cluster/config.go
index c8aff93251..3d225fd2cf 100644
--- a/pkg/test/framework/components/cluster/config.go
+++ b/pkg/test/framework/components/cluster/config.go
@@ -86,18 +86,24 @@ func toConfigMeta(orig interface{}) (ConfigMeta, bool) {
 }
 
 func (m ConfigMeta) Bool(key string) *bool {
+	if m[key] == nil {
+		return nil
+	}
 	v, ok := m[key].(bool)
 	if !ok {
-		scopes.Framework.Warnf("failed to parse key %q as bool, defaulting to false", key)
+		scopes.Framework.Warnf("failed to parse key of type %T value %q as bool, defaulting to false", m[key], key)
 		return nil
 	}
 	return &v
 }
 
 func (m ConfigMeta) Int(key string) int {
+	if m[key] == nil {
+		return 0
+	}
 	v, ok := m[key].(int)
 	if !ok {
-		scopes.Framework.Warnf("failed to parse key %q as int, defaulting to 0", key)
+		scopes.Framework.Warnf("failed to parse key of type %T value %q as int, defaulting to 0", m[key], key)
 		return 0
 	}
 	return v
diff --git a/tests/integration/pilot/revisioncmd/main_test.go b/tests/integration/pilot/revisioncmd/main_test.go
new file mode 100644
index 0000000000..46954ea7ad
--- /dev/null
+++ b/tests/integration/pilot/revisioncmd/main_test.go
@@ -0,0 +1,45 @@
+// +build integ
+//  Copyright Istio 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 revisioncmd
+
+import (
+	"testing"
+
+	"istio.io/istio/pkg/test/framework"
+	"istio.io/istio/pkg/test/framework/components/istio"
+	"istio.io/istio/pkg/test/framework/resource"
+)
+
+func TestMain(m *testing.M) {
+	framework.
+		NewSuite(m).
+		RequireSingleCluster().
+		Setup(istio.Setup(nil, func(_ resource.Context, cfg *istio.Config) {
+			cfg.ControlPlaneValues = `
+revision: stable
+`
+		})).
+		Setup(istio.Setup(nil, func(_ resource.Context, cfg *istio.Config) {
+			cfg.ControlPlaneValues = `
+profile: empty
+revision: canary
+components:
+  pilot:
+    enabled: true
+`
+		})).
+		Run()
+}
diff --git a/tests/integration/pilot/revisioncmd/revision_view_test.go b/tests/integration/pilot/revisioncmd/revision_view_test.go
new file mode 100644
index 0000000000..1f1443d3af
--- /dev/null
+++ b/tests/integration/pilot/revisioncmd/revision_view_test.go
@@ -0,0 +1,368 @@
+// +build integ
+//  Copyright Istio 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 revisioncmd
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"path/filepath"
+	"strings"
+	"testing"
+	"time"
+
+	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"istio.io/api/label"
+	"istio.io/istio/istioctl/cmd"
+	"istio.io/istio/pkg/test/env"
+	"istio.io/istio/pkg/test/framework"
+	"istio.io/istio/pkg/test/framework/components/echo"
+	"istio.io/istio/pkg/test/framework/components/echo/echoboot"
+	"istio.io/istio/pkg/test/framework/components/istioctl"
+	"istio.io/istio/pkg/test/framework/components/namespace"
+)
+
+var (
+	manifestPath = filepath.Join(env.IstioSrc, "manifests")
+
+	componentMap = map[string]string{
+		"istiod":          "Pilot",
+		"ingress-gateway": "IngressGateways",
+		"egress-gateway":  "EgressGateways",
+	}
+
+	expectedComponentsPerRevision = map[string]map[string]bool{
+		"stable": {
+			"base":                          true,
+			"istiod":                        true,
+			"ingress:istio-ingressgateway":  true,
+			"ingress:istio-eastwestgateway": true,
+			"egress:istio-egressgateway":    true,
+		},
+		"canary": {
+			"istiod":                        true,
+			"ingress:istio-ingressgateway":  true,
+			"ingress:istio-eastwestgateway": true,
+			"egress:istio-egressgateway":    true,
+		},
+	}
+)
+
+type revisionResource struct {
+	ns   namespace.Instance
+	echo echo.Instance
+}
+
+func TestRevisionCommand(t *testing.T) {
+	framework.
+		NewTest(t).
+		RequiresSingleCluster().
+		Features("installation.istioctl.revision_centric_view").
+		Run(func(ctx framework.TestContext) {
+			skipIfUnsupportedKubernetesVersion(ctx)
+			istioCtl := istioctl.NewOrFail(ctx, ctx, istioctl.Config{})
+
+			revisions := []string{"stable", "canary"}
+			revResourceMap := map[string]*revisionResource{}
+			builder := echoboot.NewBuilder(ctx)
+			for _, rev := range revisions {
+				effectiveRev := rev
+				if rev == "default" {
+					effectiveRev = ""
+				}
+				revResourceMap[rev] = &revisionResource{
+					ns: namespace.NewOrFail(t, ctx, namespace.Config{
+						Prefix:   fmt.Sprintf("istioctl-rev-%s", rev),
+						Inject:   true,
+						Revision: effectiveRev,
+					}),
+				}
+				builder.With(&revResourceMap[rev].echo,
+					echo.Config{Namespace: revResourceMap[rev].ns})
+			}
+			builder.BuildOrFail(ctx)
+
+			// Wait some time for things to settle down. This is very
+			// important for gateway pod related tests.
+			time.Sleep(30 * time.Second)
+
+			testCases := []struct {
+				name     string
+				testFunc func(framework.TestContext, istioctl.Instance, map[string]*revisionResource)
+			}{
+				{
+					name:     "list revisions",
+					testFunc: testRevisionListingVerbose,
+				},
+				{
+					name:     "describe valid revision",
+					testFunc: testRevisionDescription,
+				},
+				{
+					name:     "describe non-existent revision",
+					testFunc: testNonExistentRevisionDescription,
+				},
+				{
+					name:     "describe invalid revision",
+					testFunc: testInvalidRevisionFormat,
+				},
+				{
+					name:     "invalid output format",
+					testFunc: testInvalidOutputFormat,
+				},
+			}
+			for _, testCase := range testCases {
+				ctx.NewSubTest(testCase.name).Run(func(sctx framework.TestContext) {
+					testCase.testFunc(sctx, istioCtl, revResourceMap)
+				})
+			}
+		})
+}
+
+func testRevisionListingVerbose(ctx framework.TestContext, istioCtl istioctl.Instance, _ map[string]*revisionResource) {
+	listVerboseCmd := []string{"x", "revision", "list", "-d", manifestPath, "-v", "-o", "json"}
+	stdout, _, err := istioCtl.Invoke(listVerboseCmd)
+	if err != nil {
+		ctx.Fatalf("unexpected error while invoking istioctl command %v: %v", listVerboseCmd, err)
+	}
+	var revDescriptions map[string]*cmd.RevisionDescription
+	if err = json.Unmarshal([]byte(stdout), &revDescriptions); err != nil {
+		ctx.Fatalf("error while unmarshaling JSON output: %v", err)
+	}
+
+	expectedRevisionSet := map[string]bool{"stable": true, "canary": true}
+	actualRevisionSet := map[string]bool{}
+	for rev := range revDescriptions {
+		actualRevisionSet[rev] = true
+	}
+	if !subsetOf(expectedRevisionSet, actualRevisionSet) {
+		ctx.Fatalf("Some expected revisions are missing from actual. "+
+			"Expected should be subset of actual. Expected: %v, Actual: %v",
+			expectedRevisionSet, actualRevisionSet)
+	}
+
+	for rev, descr := range revDescriptions {
+		if !expectedRevisionSet[rev] {
+			ctx.Logf("revision %s is unexpected. Might be a leftover from some other test. skipping...", rev)
+			continue
+		}
+		verifyRevisionOutput(ctx, descr, rev)
+	}
+}
+
+func testRevisionDescription(ctx framework.TestContext, istioCtl istioctl.Instance, revResources map[string]*revisionResource) {
+	for _, rev := range []string{"stable", "canary"} {
+		descr, err := getDescriptionForRevision(istioCtl, rev)
+		if err != nil || descr == nil {
+			ctx.Fatalf("failed to retrieve description for %s: %v", descr, err)
+		}
+		verifyRevisionOutput(ctx, descr, rev)
+		if resources := revResources[rev]; resources != nil {
+			nsName := resources.ns.Name()
+			podsInNamespace := []*cmd.PodFilteredInfo{}
+			if nsi := descr.NamespaceSummary[nsName]; nsi != nil {
+				podsInNamespace = nsi.Pods
+			}
+			labelSelector, err := meta_v1.LabelSelectorAsSelector(&meta_v1.LabelSelector{
+				MatchLabels: map[string]string{label.IoIstioRev.Name: rev},
+			})
+			if err != nil {
+				ctx.Fatalf("error while creating label selector for pods in namespace: %s, revision: %s",
+					nsName, rev)
+			}
+			podsForRev, err := ctx.Clusters().Default().
+				CoreV1().Pods(nsName).
+				List(context.Background(), meta_v1.ListOptions{LabelSelector: labelSelector.String()})
+			if podsForRev == nil || err != nil {
+				ctx.Fatalf("error while getting pods for revision: %s from namespace: %s: %v", rev, nsName, err)
+			}
+			expectedPodsForRev := map[string]bool{}
+			actualPodsForRev := map[string]bool{}
+			//nolint:staticcheck
+			for _, pod := range podsForRev.Items {
+				expectedPodsForRev[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = true
+			}
+			for _, pod := range podsInNamespace {
+				actualPodsForRev[fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)] = true
+			}
+			if !setsMatch(expectedPodsForRev, actualPodsForRev) {
+				ctx.Fatalf("list of pods pointing to %s don't match in namespace %s. Expected: %v, Actual: %v",
+					rev, nsName, expectedPodsForRev, actualPodsForRev)
+			}
+		}
+	}
+}
+
+func testNonExistentRevisionDescription(ctx framework.TestContext, istioCtl istioctl.Instance, _ map[string]*revisionResource) {
+	describeArgs := []string{"x", "revision", "describe", "ghost", "-o", "json", "-v"}
+	_, _, err := istioCtl.Invoke(describeArgs)
+	if err == nil {
+		ctx.Fatalf("expected error for non-existent revision 'ghost', but didn't get it")
+	}
+}
+
+func testInvalidRevisionFormat(ctx framework.TestContext, istioCtl istioctl.Instance, _ map[string]*revisionResource) {
+	describeArgs := []string{"x", "revision", "describe", "$%#@abc", "-o", "json", "-v"}
+	_, _, err := istioCtl.Invoke(describeArgs)
+	if err == nil || !strings.Contains(err.Error(), "invalid revision format") {
+		ctx.Fatalf("expected error message saying revision format is invalid, but got %v", err)
+	}
+}
+
+func testInvalidOutputFormat(ctx framework.TestContext, istioCtl istioctl.Instance, _ map[string]*revisionResource) {
+	type invalidFormatTest struct {
+		name    string
+		command []string
+	}
+	for _, t := range []invalidFormatTest{
+		{name: "list", command: []string{"x", "revision", "list", "-o", "mystery"}},
+		{name: "describe", command: []string{"x", "revision", "describe", "canary", "-o", "mystery"}},
+	} {
+		ctx.NewSubTest(t.name).Run(func(sctx framework.TestContext) {
+			_, _, fErr := istioCtl.Invoke(t.command)
+			if fErr == nil {
+				ctx.Fatalf("expected error due to invalid format, but got nil")
+			}
+		})
+	}
+}
+
+func getDescriptionForRevision(istioCtl istioctl.Instance, revision string) (*cmd.RevisionDescription, error) {
+	describeCmd := []string{"x", "revision", "describe", revision, "-d", manifestPath, "-o", "json", "-v"}
+	descr, _, err := istioCtl.Invoke(describeCmd)
+	if err != nil {
+		return nil, err
+	}
+	var revDescription cmd.RevisionDescription
+	if err = json.Unmarshal([]byte(descr), &revDescription); err != nil {
+		return nil, fmt.Errorf("error while unmarshaling revision description JSON for"+
+			" revision=%s : %v", revision, err)
+	}
+	return &revDescription, nil
+}
+
+func verifyRevisionOutput(ctx framework.TestContext, descr *cmd.RevisionDescription, rev string) {
+	expectedTagSet := map[string]bool{}
+	actualTagSet := map[string]bool{}
+	for _, mwh := range descr.Webhooks {
+		if mwh.Tag != "" {
+			actualTagSet[mwh.Tag] = true
+		}
+	}
+	if !setsMatch(expectedTagSet, actualTagSet) {
+		ctx.Fatalf("tag sets don't match for %s. Expected: %v, Actual:%v", rev, expectedTagSet, actualTagSet)
+	}
+	expectedComponents, ok := expectedComponentsPerRevision[rev]
+	if !ok {
+		ctx.Fatalf("unexpected error. Could not find expected components for %s", rev)
+	}
+	actualComponents := map[string]bool{}
+	for _, iop := range descr.IstioOperatorCRs {
+		for _, comp := range iop.Components {
+			actualComponents[comp] = true
+		}
+	}
+	if !setsMatch(expectedComponents, actualComponents) {
+		ctx.Fatalf("required component sets don't match for revision %s. Expected: %v, Actual: %v",
+			rev, expectedComponents, actualComponents)
+	}
+	verifyComponentPodsForRevision(ctx, "istiod", rev, descr)
+	verifyComponentPodsForRevision(ctx, "ingress-gateway", rev, descr)
+	verifyComponentPodsForRevision(ctx, "egress-gateway", rev, descr)
+}
+
+func verifyComponentPodsForRevision(ctx framework.TestContext, component, rev string, descr *cmd.RevisionDescription) {
+	opComponent := componentMap[component]
+	if opComponent == "" {
+		ctx.Fatalf("unknown component: %s", component)
+	}
+	labelSelector, err := meta_v1.LabelSelectorAsSelector(&meta_v1.LabelSelector{
+		MatchLabels: map[string]string{
+			label.IoIstioRev.Name:        rev,
+			label.OperatorComponent.Name: opComponent,
+		},
+	})
+	if err != nil {
+		ctx.Fatalf("unexpected error: failed to create label selector: %v", err)
+	}
+	componentPods, err := ctx.Clusters().Default().
+		CoreV1().Pods("").
+		List(context.Background(), meta_v1.ListOptions{LabelSelector: labelSelector.String()})
+	if err != nil {
+		ctx.Fatalf("unexpected error while fetching %s pods for revision %s: %v", component, rev, err)
+	}
+	expectedComponentPodSet := map[string]bool{}
+	for _, pod := range componentPods.Items {
+		podName := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
+		expectedComponentPodSet[podName] = true
+	}
+
+	actualComponentPodSet := map[string]bool{}
+	podList := []*cmd.PodFilteredInfo{}
+	switch component {
+	case "istiod":
+		podList = descr.ControlPlanePods
+	case "ingress-gateway":
+		podList = descr.IngressGatewayPods
+	case "egress-gateway":
+		podList = descr.EgressGatewayPods
+	}
+	for _, pod := range podList {
+		podName := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
+		actualComponentPodSet[podName] = true
+	}
+
+	if !setsMatch(expectedComponentPodSet, actualComponentPodSet) {
+		ctx.Fatalf("%s pods are not listed properly. Expected: %v, Actual: %v",
+			component, expectedComponentPodSet, actualComponentPodSet)
+	}
+}
+
+func setsMatch(expected map[string]bool, actual map[string]bool) bool {
+	if len(expected) != len(actual) {
+		return false
+	}
+	for x := range actual {
+		if !expected[x] {
+			return false
+		}
+	}
+	return true
+}
+
+func subsetOf(a map[string]bool, b map[string]bool) bool {
+	for ax := range a {
+		if !b[ax] {
+			return false
+		}
+	}
+	return true
+}
+
+// MutatingWebhookConfiguration version used in this feature is v1 which is only
+// supported from Kubernetes 1.16. For Kubernetes 1.15, we should be using v1beta1.
+// However, this feature is not available only in Istio versions later than 1.8
+// don't support Kubernetes 1.15. So we are fine. Not having this caused postsubmit
+// test flakes when run against 1.15
+//
+// K8s 1.15 - https://v1-16.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/#mutatingwebhookconfiguration-v1beta1-admissionregistration-k8s-io
+// K8s 1.16 - https://v1-16.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#mutatingwebhookconfiguration-v1-admissionregistration-k8s-io
+func skipIfUnsupportedKubernetesVersion(ctx framework.TestContext) {
+	if !ctx.Clusters().Default().MinKubeVersion(1, 16) {
+		ctx.Skipf("k8s version not supported for %s (<%s)", ctx.Name(), "1.16")
+	}
+}
diff --git a/tests/integration/security/external_ca/basic_reachability.go b/tests/integration/security/external_ca/reachability_test.go
similarity index 100%
rename from tests/integration/security/external_ca/basic_reachability.go
rename to tests/integration/security/external_ca/reachability_test.go
diff --git a/tools/go-ordered-test b/tools/go-ordered-test
new file mode 100755
index 0000000000..ed53f39d34
--- /dev/null
+++ b/tools/go-ordered-test
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+
+# Copyright Istio 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.
+
+# This script runs go tests in a package, but each test is run individually. This helps
+# isolate tests that are improperly depending on global state modification of other tests
+
+# Usage: `go test -p 1 -exec $PWD/tools/go-ordered-test ./...`
+# Supported `go test` flags: -v, -race, -run, -count. All others are ignored (-timeout, etc)
+
+set -u
+
+red='\e[0;31m'
+green='\e[0;32m'
+yellow='\e[0;33m'
+clr='\e[0m'
+
+binary="${1}"
+RUN="."
+COUNT=1
+while [[ "$#" -gt 0 ]]; do
+    case $1 in
+        -test.v=true) VERBOSE=true ;;
+        -test.run=*) RUN="${1#-test.run=}" ;;
+        -test.count=*) COUNT="${1#-test.count=}" ;;
+    esac
+    shift
+done
+
+RESULTS=/tmp/test-results"$(dirname ${binary})"
+mkdir -p "${RESULTS}"
+code=0
+
+for testname in $("${binary}" -test.list "${RUN}"); do
+  "${binary}" -test.run '^'"${testname}"'$' -test.count "${COUNT}" -test.v &> "${RESULTS}/${testname}"
+  # shellcheck disable=SC2181
+  if [[ $? != 0 ]]; then
+    echo -e "--- ${red}FAIL:${clr} ${testname}. See ${RESULTS}/${testname} for full logs"
+    code=1
+  elif [[ "${VERBOSE}" == true ]]; then
+    echo -e "--- ${green}PASS:${clr} ${testname}"
+  fi
+done
+
+exit ${code}
diff --git a/tools/go-ordered-test.sh b/tools/go-ordered-test.sh
deleted file mode 100755
index 05d2089d5b..0000000000
--- a/tools/go-ordered-test.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright Istio 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.
-
-# This script runs go tests in a package, but each test is run individually. This helps
-# isolate tests that are improperly depending on global state modification of other tests
-
-PACKAGE="${1:?test packages}"
-PACKAGES="$(go list "${PACKAGE}")"
-VERBOSE="${VERBOSE:-0}"
-
-red='\e[0;31m'
-green='\e[0;32m'
-yellow='\e[0;33m'
-clr='\e[0m'
-
-mkdir -p /tmp/test-results
-
-for p in $PACKAGES; do
-  echo "Testing package $p"
-  dir=${p#"istio.io/istio/"}
-  if go test -o /tmp/test.test -c "${p}" | grep -q 'no test files'; then
-    echo -e "    ${yellow}SKIP ${dir}${clr}"
-    continue
-  fi
-  pass=1
-  for testname in $(/tmp/test.test -test.list '.*'); do
-    (cd "${dir}" || exit; /tmp/test.test -test.run '^'"${testname}"'$' &> "/tmp/test-results/${testname}")
-    # shellcheck disable=SC2181
-    if [[ $? != 0 ]]; then
-      echo -e "    ${red}${testname} failed, see /tmp/test-results/${testname} for full logs$clr"
-      pass=0
-    elif [[ "${VERBOSE}" != 0 ]]; then
-      echo -e "      ${green}PASS${clr} ${testname}"
-    fi
-  done
-  if [[ $pass == 1 ]]; then
-    echo -e "  ${green}PASS${clr} ${dir}"
-  else
-    echo -e "  ${red}FAIL${clr} ${dir}"
-  fi
-done
diff --git a/tools/go-stress-test b/tools/go-stress-test
new file mode 100755
index 0000000000..5705f216ca
--- /dev/null
+++ b/tools/go-stress-test
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+
+# Copyright Istio 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.
+
+# This script runs go tests in a package, but each test is run individually in a loop. This helps
+# determine which tests are flaky.
+# This requires https://github.com/howardjohn/golang-tools/tree/master/cmd/stress to be installed in $PATH.
+
+# Usage: `go test -p 1 -exec $PWD/tools/go-stress-test ./...`
+# Supported `go test` flags: -v, -race, -run, -count. All others are ignored (-timeout, etc)
+# Addition flags:
+# * -stress.runs: exit successfully after this many runs. Default 1000.
+# * -stress.time: exit successfully after this much time has passed. Default 10s.
+
+set -u
+
+red='\e[0;31m'
+green='\e[0;32m'
+yellow='\e[0;33m'
+clr='\e[0m'
+
+binary="${1}"
+RUN="."
+COUNT=1
+RUNS=1000
+TIME=10s
+while [[ "$#" -gt 0 ]]; do
+    case $1 in
+        -test.v=true) VERBOSE=true ;;
+        -test.run=*) RUN="${1#-test.run=}" ;;
+        -test.count=*) COUNT="${1#-test.count=}" ;;
+        -stress.runs=*) RUNS="${1#-stress.runs=}" ;;
+        -stress.runs) RUNS="${2}"; shift ;;
+        -stress.time=*) echo "abc";TIME="${1#-stress.time=}" ;;
+        -stress.time) TIME="${2}"; shift ;;
+    esac
+    shift
+done
+
+RESULTS=/tmp/test-results"$(dirname ${binary})"
+mkdir -p "${RESULTS}"
+code=0
+
+for testname in $("${binary}" -test.list "${RUN}"); do
+  stress -f --max-time "${MAX_TIME:-10s}" --max-runs "${MAX_RUNS:-1000}" "${binary}" -test.run '^'"${testname}"'$' -test.count "${COUNT}" -test.v &> "${RESULTS}/${testname}"
+  # shellcheck disable=SC2181
+  if [[ $? != 0 ]]; then
+    echo -e "--- ${red}FAIL:${clr} ${testname}. See ${RESULTS}/${testname} for full logs"
+    code=1
+  elif [[ "${VERBOSE:-}" == true ]]; then
+    echo -e "--- ${green}PASS:${clr} ${testname}. $(cat ${RESULTS}/${testname} | tail -n2 | head -n1)"
+  fi
+done
+
+exit ${code}