istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/validate/validate_test.go (about)

     1  // Copyright Istio Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package validate
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"path/filepath"
    23  	"regexp"
    24  	"strings"
    25  	"testing"
    26  
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"sigs.k8s.io/yaml"
    29  
    30  	"istio.io/istio/istioctl/pkg/cli"
    31  	"istio.io/istio/pkg/test/util/assert"
    32  )
    33  
    34  const (
    35  	validDeploymentList = `
    36  apiVersion: v1
    37  items:
    38  - apiVersion: apps/v1
    39    kind: Deployment
    40    metadata:
    41      labels:
    42        app: hello
    43        version: v1
    44      name: hello-v1
    45    spec:
    46      replicas: 1
    47      template:
    48        metadata:
    49          labels:
    50            app: hello
    51            version: v1
    52        spec:
    53          containers:
    54          - name: hello
    55            image: istio/examples-hello
    56            imagePullPolicy: IfNotPresent
    57            ports:
    58            - containerPort: 9080
    59  - apiVersion: apps/v1
    60    kind: Deployment
    61    metadata:
    62      labels:
    63        app: details
    64        version: v2
    65      name: details
    66    spec:
    67      replicas: 1
    68      template:
    69        metadata:
    70          labels:
    71            app: details
    72            version: v1
    73        spec:
    74          containers:
    75          - name: details
    76            image: istio/examples-bookinfo-details-v1:1.10.1
    77            imagePullPolicy: IfNotPresent
    78            ports:
    79            - containerPort: 9080
    80  kind: List
    81  metadata:
    82    resourceVersion: ""
    83    selfLink: ""`
    84  	invalidSvcList = `
    85  apiVersion: v1
    86  items:
    87    -
    88      apiVersion: v1
    89      kind: Service
    90      metadata:
    91        name: details
    92      spec:
    93        ports:
    94          -
    95            name: details
    96            port: 9080
    97    -
    98      apiVersion: v1
    99      kind: Service
   100      metadata:
   101        name: hello
   102      spec:
   103        ports:
   104          -
   105            port: 80
   106            protocol: TCP
   107  kind: List
   108  metadata:
   109    resourceVersion: ""`
   110  	udpService = `
   111  kind: Service
   112  metadata:
   113    name: hello
   114  spec:
   115    ports:
   116      -
   117        protocol: udp`
   118  	skippedService = `
   119  kind: Service
   120  metadata:
   121    name: hello
   122    namespace: istio-system
   123  spec:
   124    ports:
   125      -
   126        name: http
   127        port: 9080`
   128  	validPortNamingSvc = `
   129  apiVersion: v1
   130  kind: Service
   131  metadata:
   132    name: hello
   133  spec:
   134    ports:
   135      - name: http
   136        port: 9080`
   137  	validPortNamingWithSuffixSvc = `
   138  apiVersion: v1
   139  kind: Service
   140  metadata:
   141    name: hello
   142  spec:
   143    ports:
   144      - name: http-hello
   145        port: 9080`
   146  	invalidPortNamingSvc = `
   147  apiVersion: v1
   148  kind: Service
   149  metadata:
   150    name: hello
   151  spec:
   152    ports:
   153      - name: hello
   154        port: 9080`
   155  	portNameMissingSvc = `
   156  apiVersion: v1
   157  kind: Service
   158  metadata:
   159    name: hello
   160  spec:
   161    ports:
   162    - protocol: TCP`
   163  	validVirtualServiceYAML = `
   164  apiVersion: networking.istio.io/v1alpha3
   165  kind: VirtualService
   166  metadata:
   167    name: valid-virtual-service
   168  spec:
   169    hosts:
   170      - c
   171    http:
   172      - route:
   173        - destination:
   174            host: c
   175            subset: v1
   176          weight: 75
   177        - destination:
   178            host: c
   179            subset: v2
   180          weight: 25`
   181  	validVirtualServiceJSON = `{
   182  "apiVersion": "networking.istio.io/v1alpha3",
   183  "kind": "VirtualService",
   184  "metadata": {
   185  	"name": "valid-virtual-service"
   186  },
   187  "spec": {
   188  	"hosts": [
   189  	"c"
   190  	],
   191  	"http": [
   192  	{
   193  		"route": [
   194  		{
   195  			"destination": {
   196  			"host": "c",
   197  			"subset": "v1"
   198  			},
   199  			"weight": 75
   200  		},
   201  		{
   202  			"destination": {
   203  			"host": "c",
   204  			"subset": "v2"
   205  			},
   206  			"weight": 25
   207  		}
   208  		]
   209  	}
   210  	]
   211  }
   212  }`
   213  	validVirtualService1YAML = `
   214  apiVersion: networking.istio.io/v1alpha3
   215  kind: VirtualService
   216  metadata:
   217    name: valid-virtual-service1
   218  spec:
   219    hosts:
   220      - d
   221    http:
   222      - route:
   223        - destination:
   224            host: c
   225            subset: v1
   226          weight: 75
   227        - destination:
   228            host: c
   229            subset: v2
   230          weight: 25`
   231  	validVirtualService1JSON = `{
   232  "apiVersion": "networking.istio.io/v1alpha3",
   233  "kind": "VirtualService",
   234  "metadata": {
   235  	"name": "valid-virtual-service1"
   236  },
   237  "spec": {
   238  	"hosts": [
   239  	"d"
   240  	],
   241  	"http": [
   242  	{
   243  		"route": [
   244  		{
   245  			"destination": {
   246  			"host": "c",
   247  			"subset": "v1"
   248  			},
   249  			"weight": 75
   250  		},
   251  		{
   252  			"destination": {
   253  			"host": "c",
   254  			"subset": "v2"
   255  			},
   256  			"weight": 25
   257  		}
   258  		]
   259  	}
   260  	]
   261  }
   262  }`
   263  	validVirtualService2YAML = `
   264  apiVersion: networking.istio.io/v1alpha3
   265  kind: VirtualService
   266  metadata:
   267    name: valid-virtual-service2
   268  spec:
   269    exportTo:
   270    - '.'
   271    hosts:
   272    - d
   273    http:
   274    - route:
   275      - destination:
   276          host: c
   277          subset: v1`
   278  	validVirtualService2JSON = `{
   279  "apiVersion": "networking.istio.io/v1alpha3",
   280  "kind": "VirtualService",
   281  "metadata": {
   282  	"name": "valid-virtual-service2"
   283  },
   284  "spec": {
   285  	"exportTo": [
   286  	"."
   287  	],
   288  	"hosts": [
   289  	"d"
   290  	],
   291  	"http": [
   292  	{
   293  		"route": [
   294  		{
   295  			"destination": {
   296  			"host": "c",
   297  			"subset": "v1"
   298  			}
   299  		}
   300  		]
   301  	}
   302  	]
   303  }
   304  }`
   305  	invalidVirtualServiceYAML = `
   306  apiVersion: networking.istio.io/v1alpha3
   307  kind: VirtualService
   308  metadata:
   309    name: invalid-virtual-service
   310  spec:
   311    http:
   312      - route:
   313        - destination:
   314            host: c
   315            subset: v1
   316          weight: 75
   317        - destination:
   318            host: c
   319            subset: v2
   320          weight: -15`
   321  	invalidVirtualServiceJSON = `{
   322  "apiVersion": "networking.istio.io/v1alpha3",
   323  "kind": "VirtualService",
   324  "metadata": {
   325  	"name": "invalid-virtual-service"
   326  },
   327  "spec": {
   328  	"http": [
   329  	{
   330  		"route": [
   331  		{
   332  			"destination": {
   333  			"host": "c",
   334  			"subset": "v1"
   335  			},
   336  			"weight": 75
   337  		},
   338  		{
   339  			"destination": {
   340  			"host": "c",
   341  			"subset": "v2"
   342  			},
   343  			"weight": -15
   344  		}
   345  		]
   346  	}
   347  	]
   348  }
   349  }
   350  `
   351  	invalidVirtualServiceV1Beta1 = `
   352  apiVersion: networking.istio.io/v1beta1
   353  kind: VirtualService
   354  metadata:
   355    name: invalid-virtual-service
   356  spec:
   357    http:
   358  `
   359  	warnDestinationRule = `apiVersion: networking.istio.io/v1beta1
   360  kind: DestinationRule
   361  metadata:
   362    name: reviews-cb-policy
   363  spec:
   364    host: reviews.prod.svc.cluster.local
   365    trafficPolicy:
   366      outlierDetection:
   367        consecutiveErrors: 7
   368  `
   369  	invalidYAML = `
   370  (...!)`
   371  	validKubernetesYAML = `
   372  apiVersion: v1
   373  kind: Namespace
   374  metadata:
   375    name: istio-system
   376  `
   377  	validKubernetesJSON = `{
   378  "apiVersion": "v1",
   379  "kind": "Namespace",
   380  "metadata": {
   381  	"name": "istio-system"
   382  }
   383  }`
   384  	invalidUnsupportedKey = `
   385  apiVersion: networking.istio.io/v1alpha3
   386  kind: DestinationRule
   387  metadata:
   388    name: productpage
   389  unexpected_junk:
   390     still_more_junk:
   391  spec:
   392    host: productpage`
   393  	versionLabelMissingDeployment = `
   394  apiVersion: apps/v1
   395  kind: Deployment
   396  metadata:
   397    name: hello
   398  spec:`
   399  	skippedDeployment = `
   400  apiVersion: apps/v1
   401  kind: Deployment
   402  metadata:
   403    name: hello
   404    namespace: istio-system
   405  spec: ~`
   406  	invalidIstioConfig = `
   407  apiVersion: install.istio.io/v1alpha1
   408  kind: IstioOperator
   409  metadata:
   410    namespace: istio-system
   411    name: example-istiocontrolplane
   412  spec:
   413    dummy:
   414    traffic_management:
   415      components:
   416      namespace: istio-traffic-management
   417  `
   418  	validIstioConfig = `
   419  apiVersion: install.istio.io/v1alpha1
   420  kind: IstioOperator
   421  metadata:
   422    namespace: istio-system
   423    name: example-istiocontrolplane
   424  spec:
   425    addonComponents:
   426      grafana:
   427        enabled: true
   428  `
   429  	invalidDuplicateKey = `
   430  apiVersion: networking.istio.io/v1alpha3
   431  kind: DestinationRule
   432  metadata:
   433    name: productpage
   434  spec:
   435  trafficPolicy: {}
   436  trafficPolicy:
   437    tls:
   438      mode: ISTIO_MUTUAL
   439  `
   440  	validDeployment = `
   441  apiVersion: apps/v1
   442  kind: Deployment
   443  metadata:
   444    name: helloworld-v1
   445    labels:
   446      app: helloworld
   447      version: v1
   448  spec:
   449    replicas: 1
   450    selector:
   451      matchLabels:
   452        app: helloworld
   453        version: v1
   454    template:
   455      metadata:
   456        annotations:
   457          sidecar.istio.io/bootstrapOverride: "istio-custom-bootstrap-config"
   458        labels:
   459          app: helloworld
   460          version: v1
   461      spec:
   462        containers:
   463          - name: helloworld
   464            image: docker.io/istio/examples-helloworld-v1
   465            resources:
   466              requests:
   467                cpu: "100m"
   468            imagePullPolicy: IfNotPresent
   469            ports:
   470              - containerPort: 5000
   471  `
   472  	validPortNamingSvcWithAppProtocol = `
   473  apiVersion: v1
   474  kind: Service
   475  metadata:
   476    name: hello
   477  spec:
   478    ports:
   479      - appProtocol: http
   480        port: 9080`
   481  	validPortNamingSvcWithAppProtocol2 = `
   482  apiVersion: v1
   483  kind: Service
   484  metadata:
   485    name: hello
   486  spec:
   487    ports:
   488      - appProtocol: http
   489        name: fake # should not have error since the appProtocol field
   490        port: 9080`
   491  	inValidPortNamingSvcWithAppProtocol = `
   492  apiVersion: v1
   493  kind: Service
   494  metadata:
   495    name: hello
   496  spec:
   497    ports:
   498      - appProtocol: fake
   499        port: 9080`
   500  	validK8sRecommendedLabels = `
   501  apiVersion: v1
   502  kind: Deployment
   503  metadata:
   504    name: helloworld-v1
   505    labels:
   506      app.kubernetes.io/name: helloworld
   507      app.kubernetes.io/version: v1
   508  spec:
   509    replicas: 1
   510  `
   511  	validIstioCanonical = `
   512  apiVersion: v1
   513  kind: Deployment
   514  metadata:
   515    name: helloworld-v1
   516    labels:
   517      service.istio.io/canonical-name: helloworld
   518      service.istio.io/canonical-revision: v1
   519  spec:
   520    replicas: 1
   521  `
   522  )
   523  
   524  func fromYAML(in string) *unstructured.Unstructured {
   525  	var un unstructured.Unstructured
   526  	if err := yaml.Unmarshal([]byte(in), &un); err != nil {
   527  		panic(err)
   528  	}
   529  	return &un
   530  }
   531  
   532  func TestValidateResource(t *testing.T) {
   533  	cases := []struct {
   534  		name  string
   535  		in    string
   536  		valid bool
   537  		warn  bool
   538  	}{
   539  		{
   540  			name:  "valid pilot configuration",
   541  			in:    validVirtualServiceYAML,
   542  			valid: true,
   543  		},
   544  		{
   545  			name:  "invalid pilot configuration",
   546  			in:    invalidVirtualServiceYAML,
   547  			valid: false,
   548  		},
   549  		{
   550  			name:  "invalid pilot configuration v1beta1",
   551  			in:    invalidVirtualServiceV1Beta1,
   552  			valid: false,
   553  		},
   554  		{
   555  			name:  "port name missing service",
   556  			in:    portNameMissingSvc,
   557  			valid: false,
   558  		},
   559  		{
   560  			name:  "version label missing deployment",
   561  			in:    versionLabelMissingDeployment,
   562  			valid: true,
   563  		},
   564  		{
   565  			name:  "valid port naming service",
   566  			in:    validPortNamingSvc,
   567  			valid: true,
   568  		},
   569  		{
   570  			name:  "valid port naming with suffix service",
   571  			in:    validPortNamingWithSuffixSvc,
   572  			valid: true,
   573  		},
   574  		{
   575  			name:  "invalid port naming service",
   576  			in:    invalidPortNamingSvc,
   577  			valid: false,
   578  		},
   579  		{
   580  			name:  "invalid service list",
   581  			in:    invalidSvcList,
   582  			valid: false,
   583  		},
   584  		{
   585  			name:  "valid deployment list",
   586  			in:    validDeploymentList,
   587  			valid: true,
   588  		},
   589  		{
   590  			name:  "skip validating deployment",
   591  			in:    skippedDeployment,
   592  			valid: true,
   593  		},
   594  		{
   595  			name:  "skip validating service",
   596  			in:    skippedService,
   597  			valid: true,
   598  		},
   599  		{
   600  			name:  "service with udp port",
   601  			in:    udpService,
   602  			valid: true,
   603  		},
   604  		{
   605  			name:  "invalid Istio Operator config",
   606  			in:    invalidIstioConfig,
   607  			valid: false,
   608  		},
   609  		{
   610  			name:  "valid Istio Operator config",
   611  			in:    validIstioConfig,
   612  			valid: true,
   613  		},
   614  		{
   615  			name:  "warning",
   616  			in:    warnDestinationRule,
   617  			valid: true,
   618  			warn:  true,
   619  		},
   620  		{
   621  			name:  "exportTo=.",
   622  			in:    validVirtualService2YAML,
   623  			valid: true,
   624  		},
   625  		{
   626  			name:  "appProtocol=http",
   627  			in:    validPortNamingSvcWithAppProtocol,
   628  			valid: true,
   629  		},
   630  		{
   631  			name:  "appProtocol=http,name=fake",
   632  			in:    validPortNamingSvcWithAppProtocol2,
   633  			valid: true,
   634  		},
   635  		{
   636  			name:  "appProtocol=fake",
   637  			in:    inValidPortNamingSvcWithAppProtocol,
   638  			valid: false,
   639  		},
   640  		{
   641  			name:  "metric labels k8s recommended",
   642  			in:    validK8sRecommendedLabels,
   643  			valid: true,
   644  		},
   645  		{
   646  			name:  "metric labels istio canonical",
   647  			in:    validIstioCanonical,
   648  			valid: true,
   649  		},
   650  	}
   651  
   652  	for i, c := range cases {
   653  		t.Run(fmt.Sprintf("[%v] %v ", i, c.name), func(tt *testing.T) {
   654  			defer func() { recover() }()
   655  			v := &validator{}
   656  			var writer io.Writer
   657  			warn, err := v.validateResource("istio-system", "", fromYAML(c.in), writer)
   658  			if (err == nil) != c.valid {
   659  				tt.Fatalf("unexpected validation result: got %v want %v: err=%v", err == nil, c.valid, err)
   660  			}
   661  			if (warn != nil) != c.warn {
   662  				tt.Fatalf("unexpected validation warning result: got %v want %v: warn=%v", warn != nil, c.warn, warn)
   663  			}
   664  		})
   665  	}
   666  }
   667  
   668  func buildMultiDocConfig(docs []string) string {
   669  	var b strings.Builder
   670  	for _, r := range docs {
   671  		if r != "" {
   672  			b.WriteString(strings.Trim(r, " \t\n"))
   673  		}
   674  		b.WriteString("\n---\n")
   675  	}
   676  	return b.String()
   677  }
   678  
   679  func createTestFile(t *testing.T, data string) (string, io.Closer) {
   680  	t.Helper()
   681  	validFile, err := os.CreateTemp("", "TestValidateCommand")
   682  	if err != nil {
   683  		t.Fatal(err)
   684  	}
   685  	if _, err := validFile.WriteString(data); err != nil {
   686  		t.Fatal(err)
   687  	}
   688  	return validFile.Name(), validFile
   689  }
   690  
   691  func createTestDirectory(t *testing.T, files map[string]string) string {
   692  	t.Helper()
   693  	tempDir, err := os.MkdirTemp("", "TestValidateCommand")
   694  	if err != nil {
   695  		t.Fatal(err)
   696  	}
   697  
   698  	for name, content := range files {
   699  		filePath := filepath.Join(tempDir, name)
   700  		if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
   701  			t.Fatal(err)
   702  		}
   703  	}
   704  
   705  	return tempDir
   706  }
   707  
   708  func TestValidateCommand(t *testing.T) {
   709  	validYAML := buildMultiDocConfig([]string{validVirtualServiceYAML, validVirtualService1YAML})
   710  	invalidYAML := buildMultiDocConfig([]string{invalidVirtualServiceYAML, validVirtualService1YAML})
   711  	warningsYAML := buildMultiDocConfig([]string{invalidVirtualServiceYAML, validVirtualService1YAML, warnDestinationRule})
   712  
   713  	validJSON := buildMultiDocConfig([]string{validVirtualServiceJSON, validVirtualService1JSON})
   714  	invalidJSON := buildMultiDocConfig([]string{invalidVirtualServiceJSON, validVirtualService1JSON})
   715  	warningsJSON := buildMultiDocConfig([]string{invalidVirtualServiceYAML, validVirtualService1YAML, warnDestinationRule})
   716  
   717  	validFilenameYAML, closeValidYAMLFile := createTestFile(t, validYAML)
   718  	defer closeValidYAMLFile.Close()
   719  
   720  	validFilenameJSON, closeValidJSONFile := createTestFile(t, validJSON)
   721  	defer closeValidJSONFile.Close()
   722  
   723  	invalidFilenameYAML, closeInvalidYAMLFile := createTestFile(t, invalidYAML)
   724  	defer closeInvalidYAMLFile.Close()
   725  
   726  	warningFilename, closeWarningFile := createTestFile(t, warningsYAML)
   727  	defer closeWarningFile.Close()
   728  
   729  	invalidYAMLFile, closeInvalidYAMLFile := createTestFile(t, invalidYAML)
   730  	defer closeInvalidYAMLFile.Close()
   731  
   732  	validKubernetesYAMLFile, closeKubernetesYAMLFile := createTestFile(t, validKubernetesYAML)
   733  	defer closeKubernetesYAMLFile.Close()
   734  
   735  	validKubernetesJSONFile, closeKubernetesJSONFile := createTestFile(t, validKubernetesYAML)
   736  	defer closeKubernetesJSONFile.Close()
   737  
   738  	versionLabelMissingDeploymentFile, closeVersionLabelMissingDeploymentFile := createTestFile(t, versionLabelMissingDeployment)
   739  	defer closeVersionLabelMissingDeploymentFile.Close()
   740  
   741  	portNameMissingSvcFile, closePortNameMissingSvcFile := createTestFile(t, portNameMissingSvc)
   742  	defer closePortNameMissingSvcFile.Close()
   743  
   744  	unsupportedKeyFilename, closeUnsupportedKeyFile := createTestFile(t, invalidUnsupportedKey)
   745  	defer closeUnsupportedKeyFile.Close()
   746  
   747  	duplicateKeyFilename, closeUnsupportedKeyFile := createTestFile(t, invalidDuplicateKey)
   748  	defer closeUnsupportedKeyFile.Close()
   749  
   750  	validPortNamingSvcFile, closeValidPortNamingSvcFile := createTestFile(t, validPortNamingSvc)
   751  	defer closeValidPortNamingSvcFile.Close()
   752  
   753  	validPortNamingWithSuffixSvcFile, closeValidPortNamingWithSuffixSvcFile := createTestFile(t, validPortNamingWithSuffixSvc)
   754  	defer closeValidPortNamingWithSuffixSvcFile.Close()
   755  
   756  	invalidPortNamingSvcFile, closeInvalidPortNamingSvcFile := createTestFile(t, invalidPortNamingSvc)
   757  	defer closeInvalidPortNamingSvcFile.Close()
   758  
   759  	tempDirYAML := createTestDirectory(t, map[string]string{
   760  		"valid.yaml":       validYAML,
   761  		"invalid.yaml":     invalidYAML,
   762  		"warning.yaml":     warningsYAML,
   763  		"invalidYAML.yaml": invalidYAML,
   764  	})
   765  	validTempDirYAML := createTestDirectory(t, map[string]string{
   766  		"valid.yaml": validYAML,
   767  	})
   768  
   769  	tempDirJSON := createTestDirectory(t, map[string]string{
   770  		"valid.json":       validJSON,
   771  		"invalid.json":     invalidJSON,
   772  		"warning.json":     warningsJSON,
   773  		"invalidYAML.json": invalidJSON,
   774  	})
   775  	validTempDirJSON := createTestDirectory(t, map[string]string{
   776  		"valid.json": validJSON,
   777  	})
   778  
   779  	t.Cleanup(func() {
   780  		os.RemoveAll(tempDirYAML)
   781  		os.RemoveAll(validTempDirYAML)
   782  		os.RemoveAll(tempDirJSON)
   783  		os.RemoveAll(validTempDirJSON)
   784  	})
   785  
   786  	cases := []struct {
   787  		name           string
   788  		args           []string
   789  		wantError      bool
   790  		expectedRegexp *regexp.Regexp // Expected regexp output
   791  	}{
   792  		{
   793  			name:      "valid port naming service",
   794  			args:      []string{"--filename", validPortNamingSvcFile},
   795  			wantError: false,
   796  		},
   797  		{
   798  			name:      "valid port naming with suffix service",
   799  			args:      []string{"--filename", validPortNamingWithSuffixSvcFile},
   800  			wantError: false,
   801  		},
   802  		{
   803  			name:      "invalid port naming service",
   804  			args:      []string{"--filename", invalidPortNamingSvcFile},
   805  			wantError: true,
   806  		},
   807  		{
   808  			name:      "filename missing",
   809  			wantError: true,
   810  		},
   811  		{
   812  			name: "valid resources from file",
   813  			args: []string{"--filename", validFilenameYAML},
   814  		},
   815  		{
   816  			name:      "extra args",
   817  			args:      []string{"--filename", validFilenameYAML, "extra-arg"},
   818  			wantError: true,
   819  		},
   820  		{
   821  			name:      "invalid resources from file",
   822  			args:      []string{"--filename", invalidFilenameYAML},
   823  			wantError: true,
   824  		},
   825  		{
   826  			name:      "invalid filename",
   827  			args:      []string{"--filename", "INVALID_FILE_NAME"},
   828  			wantError: true,
   829  		},
   830  		{
   831  			name:      "invalid YAML",
   832  			args:      []string{"--filename", invalidYAMLFile},
   833  			wantError: true,
   834  		},
   835  		{
   836  			name: "valid Kubernetes YAML",
   837  			args: []string{"--filename", validKubernetesYAMLFile},
   838  			expectedRegexp: regexp.MustCompile(`^".*" is valid
   839  $`),
   840  			wantError: false,
   841  		},
   842  		{
   843  			name: "valid Kubernetes JSON",
   844  			args: []string{"--filename", validKubernetesJSONFile},
   845  			expectedRegexp: regexp.MustCompile(`^".*" is valid
   846  $`),
   847  			wantError: false,
   848  		},
   849  		{
   850  			name:           "invalid top-level key",
   851  			args:           []string{"--filename", unsupportedKeyFilename},
   852  			expectedRegexp: regexp.MustCompile(`.*unknown field "unexpected_junk"`),
   853  			wantError:      true,
   854  		},
   855  		{
   856  			name:      "version label missing deployment",
   857  			args:      []string{"--filename", versionLabelMissingDeploymentFile},
   858  			wantError: false,
   859  		},
   860  		{
   861  			name:      "port name missing service",
   862  			args:      []string{"--filename", portNameMissingSvcFile},
   863  			wantError: true,
   864  		},
   865  		{
   866  			name:           "duplicate key",
   867  			args:           []string{"--filename", duplicateKeyFilename},
   868  			expectedRegexp: regexp.MustCompile(`.*key ".*" already set`),
   869  			wantError:      true,
   870  		},
   871  		{
   872  			name: "warning",
   873  			args: []string{"--filename", warningFilename},
   874  			expectedRegexp: regexp.MustCompile(`(?m)".*" has warnings: 
   875  	\* DestinationRule//reviews-cb-policy: outlier detection consecutive errors is deprecated, use consecutiveGatewayErrors or consecutive5xxErrors instead
   876  
   877  Error: 1 error occurred:
   878  	\* VirtualService//invalid-virtual-service: weight -15 < 0`),
   879  			wantError: true,
   880  		},
   881  		{
   882  			name:      "validate all yaml files in a directory",
   883  			args:      []string{"--filename", tempDirYAML},
   884  			wantError: true, // Since the directory has invalid files
   885  		},
   886  		{
   887  			name:      "validate valid yaml files in a directory",
   888  			args:      []string{"--filename", validTempDirYAML},
   889  			wantError: false,
   890  		},
   891  		{
   892  			name:      "validate combination of yaml files and directories with valid files",
   893  			args:      []string{"--filename", validFilenameYAML, "--filename", validTempDirYAML},
   894  			wantError: false,
   895  		},
   896  		{
   897  			name:      "validate combination of yaml files and directories with valid files and invalid files",
   898  			args:      []string{"--filename", validFilenameYAML, "--filename", tempDirYAML, "--filename", validTempDirYAML},
   899  			wantError: true, // Since the directory has invalid files
   900  		},
   901  		{
   902  			name:      "validate all json files in a directory",
   903  			args:      []string{"--filename", tempDirJSON},
   904  			wantError: true, // Since the directory has invalid files
   905  		},
   906  		{
   907  			name:      "validate valid json files in a directory",
   908  			args:      []string{"--filename", validTempDirJSON},
   909  			wantError: false,
   910  		},
   911  		{
   912  			name:      "validate combination of json files and directories with valid files",
   913  			args:      []string{"--filename", validFilenameJSON, "--filename", validTempDirJSON},
   914  			wantError: false,
   915  		},
   916  		{
   917  			name:      "validate combination of json files and directories with valid files and invalid files",
   918  			args:      []string{"--filename", validFilenameJSON, "--filename", tempDirJSON, "--filename", validTempDirJSON},
   919  			wantError: true, // Since the directory has invalid files
   920  		},
   921  		{
   922  			name:      "validate mix of yaml and json directories with valid files",
   923  			args:      []string{"--filename", validTempDirYAML, "--filename", validTempDirJSON},
   924  			wantError: false,
   925  		},
   926  		{
   927  			name:      "validate combination of yaml and json and yaml directories with invalid files",
   928  			args:      []string{"--filename", tempDirYAML, "--filename", tempDirJSON},
   929  			wantError: true, // Since the directory has invalid files
   930  		},
   931  	}
   932  	for i, c := range cases {
   933  		t.Run(fmt.Sprintf("[%v] %v", i, c.name), func(t *testing.T) {
   934  			ctx := cli.NewFakeContext(&cli.NewFakeContextOption{
   935  				IstioNamespace: "istio-system",
   936  			})
   937  			validateCmd := NewValidateCommand(ctx)
   938  			validateCmd.SilenceUsage = true
   939  			validateCmd.SetArgs(c.args)
   940  
   941  			// capture output to keep test logs clean
   942  			var out bytes.Buffer
   943  			validateCmd.SetOut(&out)
   944  			validateCmd.SetErr(&out)
   945  
   946  			err := validateCmd.Execute()
   947  			if (err != nil) != c.wantError {
   948  				t.Errorf("unexpected validate return status: got %v want %v: \nerr=%v",
   949  					err != nil, c.wantError, err)
   950  			}
   951  			output := out.String()
   952  			if c.expectedRegexp != nil && !c.expectedRegexp.MatchString(output) {
   953  				t.Errorf("Output didn't match for 'istioctl %s'\n got %v\nwant: %v",
   954  					strings.Join(c.args, " "), output, c.expectedRegexp)
   955  			}
   956  		})
   957  	}
   958  }
   959  
   960  func TestGetTemplateLabels(t *testing.T) {
   961  	un := fromYAML(validDeployment)
   962  
   963  	labels, err := GetTemplateLabels(un)
   964  	if err != nil {
   965  		t.Fatal(err)
   966  	}
   967  	assert.Equal(t, labels, map[string]string{
   968  		"app":     "helloworld",
   969  		"version": "v1",
   970  	})
   971  }