sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/config_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package config
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"reflect"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"testing"
    30  	"text/template"
    31  	"time"
    32  
    33  	"github.com/google/go-cmp/cmp"
    34  	"github.com/google/go-cmp/cmp/cmpopts"
    35  	fuzz "github.com/google/gofuzz"
    36  	pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
    37  	v1 "k8s.io/api/core/v1"
    38  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/labels"
    41  	"k8s.io/apimachinery/pkg/util/diff"
    42  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    43  	"k8s.io/apimachinery/pkg/util/sets"
    44  	utilpointer "k8s.io/utils/pointer"
    45  	"sigs.k8s.io/yaml"
    46  
    47  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    48  	"sigs.k8s.io/prow/pkg/config/secret"
    49  	"sigs.k8s.io/prow/pkg/git/types"
    50  	"sigs.k8s.io/prow/pkg/github"
    51  	"sigs.k8s.io/prow/pkg/github/fakegithub"
    52  	"sigs.k8s.io/prow/pkg/kube"
    53  	"sigs.k8s.io/prow/pkg/pod-utils/decorate"
    54  	"sigs.k8s.io/prow/pkg/pod-utils/downwardapi"
    55  )
    56  
    57  func pStr(str string) *string {
    58  	return &str
    59  }
    60  
    61  func TestKeysForIdentifier(t *testing.T) {
    62  	tests := []struct {
    63  		name       string
    64  		identifier string
    65  		want       []string
    66  	}{
    67  		{
    68  			name:       "base",
    69  			identifier: "foo",
    70  			want:       []string{"foo", "*"},
    71  		},
    72  		{
    73  			name:       "with-http",
    74  			identifier: "http://foo",
    75  			want:       []string{"http://foo", "foo", "*"},
    76  		},
    77  		{
    78  			name:       "with-https",
    79  			identifier: "https://foo",
    80  			want:       []string{"https://foo", "foo", "*"},
    81  		},
    82  		{
    83  			name:       "org-name-contains-https",
    84  			identifier: "https-foo",
    85  			want:       []string{"https-foo", "*"},
    86  		},
    87  	}
    88  
    89  	for _, tc := range tests {
    90  		tc := tc
    91  		t.Run(tc.name, func(t *testing.T) {
    92  			if diff := cmp.Diff(tc.want, keysForIdentifier(tc.identifier)); diff != "" {
    93  				t.Errorf("Keys mismatch. Want(-), got(+):\n%s", diff)
    94  			}
    95  		})
    96  	}
    97  }
    98  
    99  func TestDefaultJobBase(t *testing.T) {
   100  	bar := "bar"
   101  	filled := JobBase{
   102  		Agent:     "foo",
   103  		Namespace: &bar,
   104  		Cluster:   "build",
   105  	}
   106  	cases := []struct {
   107  		name     string
   108  		config   ProwConfig
   109  		base     func(j *JobBase)
   110  		expected func(j *JobBase)
   111  	}{
   112  		{
   113  			name: "no changes when fields are already set",
   114  		},
   115  		{
   116  			name: "empty agent results in kubernetes",
   117  			base: func(j *JobBase) {
   118  				j.Agent = ""
   119  			},
   120  			expected: func(j *JobBase) {
   121  				j.Agent = string(prowapi.KubernetesAgent)
   122  			},
   123  		},
   124  		{
   125  			name: "nil namespace becomes PodNamespace",
   126  			config: ProwConfig{
   127  				PodNamespace:     "pod-namespace",
   128  				ProwJobNamespace: "wrong",
   129  			},
   130  			base: func(j *JobBase) {
   131  				j.Namespace = nil
   132  			},
   133  			expected: func(j *JobBase) {
   134  				p := "pod-namespace"
   135  				j.Namespace = &p
   136  			},
   137  		},
   138  		{
   139  			name: "empty namespace becomes PodNamespace",
   140  			config: ProwConfig{
   141  				PodNamespace:     "new-pod-namespace",
   142  				ProwJobNamespace: "still-wrong",
   143  			},
   144  			base: func(j *JobBase) {
   145  				var empty string
   146  				j.Namespace = &empty
   147  			},
   148  			expected: func(j *JobBase) {
   149  				p := "new-pod-namespace"
   150  				j.Namespace = &p
   151  			},
   152  		},
   153  		{
   154  			name: "empty cluster becomes DefaultClusterAlias",
   155  			base: func(j *JobBase) {
   156  				j.Cluster = ""
   157  			},
   158  			expected: func(j *JobBase) {
   159  				j.Cluster = kube.DefaultClusterAlias
   160  			},
   161  		},
   162  	}
   163  
   164  	for _, tc := range cases {
   165  		t.Run(tc.name, func(t *testing.T) {
   166  			actual := filled
   167  			if tc.base != nil {
   168  				tc.base(&actual)
   169  			}
   170  			expected := actual
   171  			if tc.expected != nil {
   172  				tc.expected(&expected)
   173  			}
   174  			tc.config.defaultJobBase(&actual)
   175  			if !reflect.DeepEqual(actual, expected) {
   176  				t.Errorf("expected %#v\n!=\nactual %#v", expected, actual)
   177  			}
   178  		})
   179  	}
   180  }
   181  
   182  func TestSpyglassConfig(t *testing.T) {
   183  	testCases := []struct {
   184  		name                 string
   185  		spyglassConfig       string
   186  		expectedViewers      map[string][]string
   187  		expectedRegexMatches map[string][]string
   188  		expectedSizeLimit    int64
   189  		expectError          bool
   190  	}{
   191  		{
   192  			name: "Default: build log, metadata, junit",
   193  			spyglassConfig: `
   194  deck:
   195    spyglass:
   196      size_limit: 500e+6
   197      viewers:
   198        "started.json|finished.json":
   199        - "metadata"
   200        "build-log.txt":
   201        - "buildlog"
   202        "artifacts/junit.*\\.xml":
   203        - "junit"
   204  `,
   205  			expectedViewers: map[string][]string{
   206  				"started.json|finished.json": {"metadata"},
   207  				"build-log.txt":              {"buildlog"},
   208  				"artifacts/junit.*\\.xml":    {"junit"},
   209  			},
   210  			expectedRegexMatches: map[string][]string{
   211  				"started.json|finished.json": {"started.json", "finished.json"},
   212  				"build-log.txt":              {"build-log.txt"},
   213  				"artifacts/junit.*\\.xml":    {"artifacts/junit01.xml", "artifacts/junit_runner.xml"},
   214  			},
   215  			expectedSizeLimit: 500e6,
   216  			expectError:       false,
   217  		},
   218  		{
   219  			name: "Backwards compatibility",
   220  			spyglassConfig: `
   221  deck:
   222    spyglass:
   223      size_limit: 500e+6
   224      viewers:
   225        "started.json|finished.json":
   226        - "metadata-viewer"
   227        "build-log.txt":
   228        - "build-log-viewer"
   229        "artifacts/junit.*\\.xml":
   230        - "junit-viewer"
   231  `,
   232  			expectedViewers: map[string][]string{
   233  				"started.json|finished.json": {"metadata"},
   234  				"build-log.txt":              {"buildlog"},
   235  				"artifacts/junit.*\\.xml":    {"junit"},
   236  			},
   237  			expectedSizeLimit: 500e6,
   238  			expectError:       false,
   239  		},
   240  		{
   241  			name: "Invalid spyglass size limit",
   242  			spyglassConfig: `
   243  deck:
   244    spyglass:
   245      size_limit: -4
   246      viewers:
   247        "started.json|finished.json":
   248        - "metadata-viewer"
   249        "build-log.txt":
   250        - "build-log-viewer"
   251        "artifacts/junit.*\\.xml":
   252        - "junit-viewer"
   253  `,
   254  			expectError: true,
   255  		},
   256  		{
   257  			name: "Invalid Spyglass regexp",
   258  			spyglassConfig: `
   259  deck:
   260    spyglass:
   261      size_limit: 5
   262      viewers:
   263        "started.json\|]finished.json":
   264        - "metadata-viewer"
   265  `,
   266  			expectError: true,
   267  		},
   268  		{
   269  			name: "Invalid Spyglass gcs browser web prefix",
   270  			spyglassConfig: `
   271  deck:
   272    spyglass:
   273      gcs_browser_prefix: https://gcsweb.k8s.io/gcs/
   274      gcs_browser_prefixes:
   275        '*': https://gcsweb.k8s.io/gcs/
   276  `,
   277  			expectError: true,
   278  		},
   279  		{
   280  			name: "Invalid Spyglass gcs browser web prefix by bucket",
   281  			spyglassConfig: `
   282  deck:
   283    spyglass:
   284      gcs_browser_prefix: https://gcsweb.k8s.io/gcs/
   285      gcs_browser_prefixes_by_bucket:
   286        '*': https://gcsweb.k8s.io/gcs/
   287  `,
   288  			expectError: true,
   289  		},
   290  	}
   291  	for _, tc := range testCases {
   292  		// save the config
   293  		spyglassConfigDir := t.TempDir()
   294  
   295  		spyglassConfig := filepath.Join(spyglassConfigDir, "config.yaml")
   296  		if err := os.WriteFile(spyglassConfig, []byte(tc.spyglassConfig), 0666); err != nil {
   297  			t.Fatalf("fail to write spyglass config: %v", err)
   298  		}
   299  
   300  		cfg, err := Load(spyglassConfig, "", nil, "")
   301  		if (err != nil) != tc.expectError {
   302  			t.Fatalf("tc %s: expected error: %v, got: %v, error: %v", tc.name, tc.expectError, (err != nil), err)
   303  		}
   304  
   305  		if err != nil {
   306  			continue
   307  		}
   308  		got := cfg.Deck.Spyglass.Viewers
   309  		for re, viewNames := range got {
   310  			expected, ok := tc.expectedViewers[re]
   311  			if !ok {
   312  				t.Errorf("With re %s, got %s, was not found in expected.", re, viewNames)
   313  				continue
   314  			}
   315  			if !reflect.DeepEqual(expected, viewNames) {
   316  				t.Errorf("With re %s, got %s, expected view name %s", re, viewNames, expected)
   317  			}
   318  
   319  		}
   320  		for re, viewNames := range tc.expectedViewers {
   321  			gotNames, ok := got[re]
   322  			if !ok {
   323  				t.Errorf("With re %s, expected %s, was not found in got.", re, viewNames)
   324  				continue
   325  			}
   326  			if !reflect.DeepEqual(gotNames, viewNames) {
   327  				t.Errorf("With re %s, got %s, expected view name %s", re, gotNames, viewNames)
   328  			}
   329  		}
   330  
   331  		for expectedRegex, matches := range tc.expectedRegexMatches {
   332  			compiledRegex, ok := cfg.Deck.Spyglass.RegexCache[expectedRegex]
   333  			if !ok {
   334  				t.Errorf("tc %s, regex %s was not found in the spyglass regex cache", tc.name, expectedRegex)
   335  				continue
   336  			}
   337  			for _, match := range matches {
   338  				if !compiledRegex.MatchString(match) {
   339  					t.Errorf("tc %s expected compiled regex %s to match %s, did not match.", tc.name, expectedRegex, match)
   340  				}
   341  			}
   342  
   343  		}
   344  		if cfg.Deck.Spyglass.SizeLimit != tc.expectedSizeLimit {
   345  			t.Errorf("%s expected SizeLimit %d, got %d", tc.name, tc.expectedSizeLimit, cfg.Deck.Spyglass.SizeLimit)
   346  		}
   347  	}
   348  
   349  }
   350  
   351  func TestGetGCSBrowserPrefix(t *testing.T) {
   352  	testCases := []struct {
   353  		id       string
   354  		config   Spyglass
   355  		expected string
   356  	}{
   357  		{
   358  			id: "only default",
   359  			config: Spyglass{
   360  				GCSBrowserPrefixesByRepo: map[string]string{
   361  					"*": "https://default.com/gcs/",
   362  				},
   363  			},
   364  			expected: "https://default.com/gcs/",
   365  		},
   366  		{
   367  			id: "org exists",
   368  			config: Spyglass{
   369  				GCSBrowserPrefixesByRepo: map[string]string{
   370  					"*":   "https://default.com/gcs/",
   371  					"org": "https://org.com/gcs/",
   372  				},
   373  			},
   374  			expected: "https://org.com/gcs/",
   375  		},
   376  		{
   377  			id: "repo exists",
   378  			config: Spyglass{
   379  				GCSBrowserPrefixesByRepo: map[string]string{
   380  					"*":        "https://default.com/gcs/",
   381  					"org":      "https://org.com/gcs/",
   382  					"org/repo": "https://repo.com/gcs/",
   383  				},
   384  			},
   385  			expected: "https://repo.com/gcs/",
   386  		},
   387  		{
   388  			id: "repo overrides bucket",
   389  			config: Spyglass{
   390  				GCSBrowserPrefixesByRepo: map[string]string{
   391  					"*":        "https://default.com/gcs/",
   392  					"org":      "https://org.com/gcs/",
   393  					"org/repo": "https://repo.com/gcs/",
   394  				},
   395  				GCSBrowserPrefixesByBucket: map[string]string{
   396  					"*":      "https://default.com/gcs/",
   397  					"bucket": "https://bucket.com/gcs/",
   398  				},
   399  			},
   400  			expected: "https://repo.com/gcs/",
   401  		},
   402  		{
   403  			id: "bucket exists",
   404  			config: Spyglass{
   405  				GCSBrowserPrefixesByRepo: map[string]string{
   406  					"*": "https://default.com/gcs/",
   407  				},
   408  				GCSBrowserPrefixesByBucket: map[string]string{
   409  					"*":      "https://default.com/gcs/",
   410  					"bucket": "https://bucket.com/gcs/",
   411  				},
   412  			},
   413  			expected: "https://bucket.com/gcs/",
   414  		},
   415  	}
   416  
   417  	for _, tc := range testCases {
   418  		actual := tc.config.GetGCSBrowserPrefix("org", "repo", "bucket")
   419  		if !reflect.DeepEqual(actual, tc.expected) {
   420  			t.Fatalf("%s", cmp.Diff(tc.expected, actual))
   421  		}
   422  	}
   423  }
   424  
   425  func TestDefaultMatches(t *testing.T) {
   426  	for _, tc := range []struct {
   427  		desc         string
   428  		givenOrgRepo string
   429  		givenCluster string
   430  		orgRepo      string
   431  		cluster      string
   432  		want         bool
   433  	}{
   434  		{
   435  			desc:    "empty",
   436  			orgRepo: "org/repo",
   437  			cluster: "cluster",
   438  			want:    true,
   439  		},
   440  		{
   441  			desc:         "givenOrgRepo empty",
   442  			givenOrgRepo: "",
   443  			orgRepo:      "org/repo",
   444  			want:         true,
   445  		},
   446  		{
   447  			desc:         "givenOrgRepo star",
   448  			givenOrgRepo: "*",
   449  			orgRepo:      "org/repo",
   450  			want:         true,
   451  		},
   452  		{
   453  			desc:         "givenOrgRepo org match",
   454  			givenOrgRepo: "org",
   455  			orgRepo:      "org/repo",
   456  			want:         true,
   457  		},
   458  		{
   459  			desc:         "givenOrgRepo full match",
   460  			givenOrgRepo: "org/repo",
   461  			orgRepo:      "org/repo",
   462  			want:         true,
   463  		},
   464  		{
   465  			desc:         "givenOrgRepo gerrit review org match",
   466  			givenOrgRepo: "some.org",
   467  			orgRepo:      "some-review.org/repo",
   468  			want:         true,
   469  		},
   470  		{
   471  			desc:         "givenOrgRepo gerrit review full match",
   472  			givenOrgRepo: "some.org/repo",
   473  			orgRepo:      "some-review.org/repo",
   474  			want:         true,
   475  		},
   476  		// The following two cases cover an unexpected existing configuration
   477  		// that matches a literal http/s prefix. Though unadvised, it should
   478  		// not be broken by fuzz matching http prefixes.
   479  		{
   480  			desc:         "givenOrgRepo http full match",
   481  			givenOrgRepo: "http://org/repo",
   482  			orgRepo:      "http://org/repo",
   483  			want:         true,
   484  		},
   485  		{
   486  			desc:         "givenOrgRepo https full match",
   487  			givenOrgRepo: "https://org/repo",
   488  			orgRepo:      "https://org/repo",
   489  			want:         true,
   490  		},
   491  		{
   492  			desc:         "givenOrgRepo http fuzz org match",
   493  			givenOrgRepo: "org",
   494  			orgRepo:      "http://org/repo",
   495  			want:         true,
   496  		},
   497  		{
   498  			desc:         "givenOrgRepo https fuzz org match",
   499  			givenOrgRepo: "org",
   500  			orgRepo:      "https://org/repo",
   501  			want:         true,
   502  		},
   503  		{
   504  			desc:         "givenOrgRepo https fuzz gerrit review org match",
   505  			givenOrgRepo: "some.org",
   506  			orgRepo:      "https://some-review.org/repo",
   507  			want:         true,
   508  		},
   509  		{
   510  			desc:         "givenOrgRepo http fuzz full match",
   511  			givenOrgRepo: "org/repo",
   512  			orgRepo:      "http://org/repo",
   513  			want:         true,
   514  		},
   515  		{
   516  			desc:         "givenOrgRepo https fuzz full match",
   517  			givenOrgRepo: "org/repo",
   518  			orgRepo:      "https://org/repo",
   519  			want:         true,
   520  		},
   521  		{
   522  			desc:         "givenOrgRepo org mismatch",
   523  			givenOrgRepo: "org2",
   524  			orgRepo:      "org/repo",
   525  			want:         false,
   526  		},
   527  		{
   528  			desc:         "givenOrgRepo repo mismatch",
   529  			givenOrgRepo: "org/repo2",
   530  			orgRepo:      "org/repo",
   531  			want:         false,
   532  		},
   533  		{
   534  			desc:         "givenOrgRepo gerrit review org mismatch",
   535  			givenOrgRepo: "some.other.org",
   536  			orgRepo:      "some.other-review.org",
   537  			want:         false,
   538  		},
   539  		{
   540  			desc:         "givenCluster empty",
   541  			givenCluster: "",
   542  			cluster:      "cluster",
   543  			want:         true,
   544  		},
   545  		{
   546  			desc:         "givenCluster star",
   547  			givenCluster: "*",
   548  			cluster:      "cluster",
   549  			want:         true,
   550  		},
   551  		{
   552  			desc:         "givenCluster match",
   553  			givenCluster: "cluster",
   554  			cluster:      "cluster",
   555  			want:         true,
   556  		},
   557  		{
   558  			desc:         "givenCluster mismatch",
   559  			givenCluster: "cluster2",
   560  			cluster:      "cluster",
   561  			want:         false,
   562  		},
   563  	} {
   564  		t.Run(tc.desc, func(t *testing.T) {
   565  			if got := matches(tc.givenOrgRepo, tc.givenCluster, tc.orgRepo, tc.cluster); got != tc.want {
   566  				t.Errorf("matches() got %v, want %v", got, tc.want)
   567  			}
   568  		})
   569  	}
   570  }
   571  
   572  func TestDecorationRawYaml(t *testing.T) {
   573  	t.Parallel()
   574  	var testCases = []struct {
   575  		name              string
   576  		expectError       bool
   577  		expectStrictError bool
   578  		rawConfig         string
   579  		expected          *prowapi.DecorationConfig
   580  	}{
   581  		{
   582  			name:        "no default",
   583  			expectError: true,
   584  			rawConfig: `
   585  periodics:
   586  - name: kubernetes-defaulted-decoration
   587    interval: 1h
   588    decorate: true
   589    spec:
   590      containers:
   591      - image: golang:latest
   592        args:
   593        - "test"
   594        - "./..."`,
   595  		},
   596  		{
   597  			name: "with bad default",
   598  			rawConfig: `
   599  plank:
   600    default_decoration_configs:
   601      '*':
   602        timeout: 2h
   603        grace_period: 15s
   604        utility_images:
   605        # clonerefs: "clonerefs:default"
   606          initupload: "initupload:default"
   607          entrypoint: "entrypoint:default"
   608          sidecar: "sidecar:default"
   609        gcs_configuration:
   610          bucket: "default-bucket"
   611          path_strategy: "legacy"
   612          default_org: "kubernetes"
   613          default_repo: "kubernetes"
   614        gcs_credentials_secret: "default-service-account"
   615  
   616  periodics:
   617  - name: kubernetes-defaulted-decoration
   618    interval: 1h
   619    decorate: true
   620    spec:
   621      containers:
   622      - image: golang:latest
   623        args:
   624        - "test"
   625        - "./..."`,
   626  			expectError: true,
   627  		},
   628  		{
   629  			name: "repo should inherit from default config",
   630  			rawConfig: `
   631  plank:
   632    default_decoration_configs:
   633      '*':
   634        timeout: 2h
   635        grace_period: 15s
   636        utility_images:
   637          clonerefs: "clonerefs:default"
   638          initupload: "initupload:default"
   639          entrypoint: "entrypoint:default"
   640          sidecar: "sidecar:default"
   641        gcs_configuration:
   642          bucket: "default-bucket"
   643          path_strategy: "legacy"
   644          default_org: "kubernetes"
   645          default_repo: "kubernetes"
   646        gcs_credentials_secret: "default-service-account"
   647      'org/inherit':
   648        timeout: 2h
   649        grace_period: 15s
   650        utility_images: {}
   651        gcs_configuration:
   652          bucket: "default-bucket"
   653          path_strategy: "legacy"
   654          default_org: "kubernetes"
   655          default_repo: "kubernetes"
   656        gcs_credentials_secret: "default-service-account"
   657  periodics:
   658  - name: kubernetes-defaulted-decoration
   659    interval: 1h
   660    decorate: true
   661    spec:
   662      containers:
   663      - image: golang:latest
   664        args:
   665        - "test"
   666        - "./..."`,
   667  		},
   668  		{
   669  			name: "with default and repo, use default",
   670  			rawConfig: `
   671  plank:
   672    default_decoration_configs:
   673      '*':
   674        timeout: 2h
   675        grace_period: 15s
   676        utility_images:
   677          clonerefs: "clonerefs:default"
   678          initupload: "initupload:default"
   679          entrypoint: "entrypoint:default"
   680          sidecar: "sidecar:default"
   681        gcs_configuration:
   682          bucket: "default-bucket"
   683          path_strategy: "legacy"
   684          default_org: "kubernetes"
   685          default_repo: "kubernetes"
   686        gcs_credentials_secret: "default-service-account"
   687      'random/repo':
   688        timeout: 2h
   689        grace_period: 15s
   690        utility_images:
   691          clonerefs: "clonerefs:random"
   692          initupload: "initupload:random"
   693          entrypoint: "entrypoint:random"
   694          sidecar: "sidecar:org"
   695        gcs_configuration:
   696          bucket: "ignore"
   697          path_strategy: "legacy"
   698          default_org: "random"
   699          default_repo: "repo"
   700        gcs_credentials_secret: "random-service-account"
   701  
   702  periodics:
   703  - name: kubernetes-defaulted-decoration
   704    interval: 1h
   705    decorate: true
   706    spec:
   707      containers:
   708      - image: golang:latest
   709        args:
   710        - "test"
   711        - "./..."`,
   712  			expected: &prowapi.DecorationConfig{
   713  				Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
   714  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
   715  				UtilityImages: &prowapi.UtilityImages{
   716  					CloneRefs:  "clonerefs:default",
   717  					InitUpload: "initupload:default",
   718  					Entrypoint: "entrypoint:default",
   719  					Sidecar:    "sidecar:default",
   720  				},
   721  				GCSConfiguration: &prowapi.GCSConfiguration{
   722  					Bucket:       "default-bucket",
   723  					PathStrategy: prowapi.PathStrategyLegacy,
   724  					DefaultOrg:   "kubernetes",
   725  					DefaultRepo:  "kubernetes",
   726  				},
   727  				GCSCredentialsSecret: pStr("default-service-account"),
   728  			},
   729  		},
   730  		{
   731  			name:              "with non-existent additional field",
   732  			expectStrictError: true,
   733  			rawConfig: `
   734  lolNotARealField: bogus
   735  plank:
   736    default_decoration_configs:
   737      '*':
   738        timeout: 2h
   739        grace_period: 15s
   740        utility_images:
   741          clonerefs: "clonerefs:default"
   742          initupload: "initupload:default"
   743          entrypoint: "entrypoint:default"
   744          sidecar: "sidecar:default"
   745        gcs_configuration:
   746          bucket: "default-bucket"
   747          path_strategy: "legacy"
   748          default_org: "kubernetes"
   749          default_repo: "kubernetes"
   750        gcs_credentials_secret: "default-service-account"
   751      'random/repo':
   752        timeout: 2h
   753        grace_period: 15s
   754        utility_images:
   755          clonerefs: "clonerefs:random"
   756          initupload: "initupload:random"
   757          entrypoint: "entrypoint:random"
   758          sidecar: "sidecar:org"
   759        gcs_configuration:
   760          bucket: "ignore"
   761          path_strategy: "legacy"
   762          default_org: "random"
   763          default_repo: "repo"
   764        gcs_credentials_secret: "random-service-account"
   765  
   766  periodics:
   767  - name: kubernetes-defaulted-decoration
   768    interval: 1h
   769    decorate: true
   770    spec:
   771      containers:
   772      - image: golang:latest
   773        args:
   774        - "test"
   775        - "./..."`,
   776  			expected: &prowapi.DecorationConfig{
   777  				Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
   778  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
   779  				UtilityImages: &prowapi.UtilityImages{
   780  					CloneRefs:  "clonerefs:default",
   781  					InitUpload: "initupload:default",
   782  					Entrypoint: "entrypoint:default",
   783  					Sidecar:    "sidecar:default",
   784  				},
   785  				GCSConfiguration: &prowapi.GCSConfiguration{
   786  					Bucket:       "default-bucket",
   787  					PathStrategy: prowapi.PathStrategyLegacy,
   788  					DefaultOrg:   "kubernetes",
   789  					DefaultRepo:  "kubernetes",
   790  				},
   791  				GCSCredentialsSecret: pStr("default-service-account"),
   792  			},
   793  		},
   794  		{
   795  			name: "with non-existent additional field allowed under 'prow_ignored'",
   796  			rawConfig: `
   797  prow_ignored:
   798    lolNotARealField: bogus
   799  plank:
   800    default_decoration_configs:
   801      '*':
   802        timeout: 2h
   803        grace_period: 15s
   804        utility_images:
   805          clonerefs: "clonerefs:default"
   806          initupload: "initupload:default"
   807          entrypoint: "entrypoint:default"
   808          sidecar: "sidecar:default"
   809        gcs_configuration:
   810          bucket: "default-bucket"
   811          path_strategy: "legacy"
   812          default_org: "kubernetes"
   813          default_repo: "kubernetes"
   814        gcs_credentials_secret: "default-service-account"
   815      'random/repo':
   816        timeout: 2h
   817        grace_period: 15s
   818        utility_images:
   819          clonerefs: "clonerefs:random"
   820          initupload: "initupload:random"
   821          entrypoint: "entrypoint:random"
   822          sidecar: "sidecar:org"
   823        gcs_configuration:
   824          bucket: "ignore"
   825          path_strategy: "legacy"
   826          default_org: "random"
   827          default_repo: "repo"
   828        gcs_credentials_secret: "random-service-account"
   829  
   830  periodics:
   831  - name: kubernetes-defaulted-decoration
   832    interval: 1h
   833    decorate: true
   834    spec:
   835      containers:
   836      - image: golang:latest
   837        args:
   838        - "test"
   839        - "./..."`,
   840  			expected: &prowapi.DecorationConfig{
   841  				Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
   842  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
   843  				UtilityImages: &prowapi.UtilityImages{
   844  					CloneRefs:  "clonerefs:default",
   845  					InitUpload: "initupload:default",
   846  					Entrypoint: "entrypoint:default",
   847  					Sidecar:    "sidecar:default",
   848  				},
   849  				GCSConfiguration: &prowapi.GCSConfiguration{
   850  					Bucket:       "default-bucket",
   851  					PathStrategy: prowapi.PathStrategyLegacy,
   852  					DefaultOrg:   "kubernetes",
   853  					DefaultRepo:  "kubernetes",
   854  				},
   855  				GCSCredentialsSecret: pStr("default-service-account"),
   856  			},
   857  		},
   858  		{
   859  			name: "with default, no explicit decorate",
   860  			rawConfig: `
   861  plank:
   862    default_decoration_configs:
   863      '*':
   864        timeout: 2h
   865        grace_period: 15s
   866        utility_images:
   867          clonerefs: "clonerefs:default"
   868          initupload: "initupload:default"
   869          entrypoint: "entrypoint:default"
   870          sidecar: "sidecar:default"
   871        gcs_configuration:
   872          bucket: "default-bucket"
   873          path_strategy: "legacy"
   874          default_org: "kubernetes"
   875          default_repo: "kubernetes"
   876        gcs_credentials_secret: "default-service-account"
   877  
   878  periodics:
   879  - name: kubernetes-defaulted-decoration
   880    interval: 1h
   881    decorate: true
   882    spec:
   883      containers:
   884      - image: golang:latest
   885        args:
   886        - "test"
   887        - "./..."`,
   888  			expected: &prowapi.DecorationConfig{
   889  				Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
   890  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
   891  				UtilityImages: &prowapi.UtilityImages{
   892  					CloneRefs:  "clonerefs:default",
   893  					InitUpload: "initupload:default",
   894  					Entrypoint: "entrypoint:default",
   895  					Sidecar:    "sidecar:default",
   896  				},
   897  				GCSConfiguration: &prowapi.GCSConfiguration{
   898  					Bucket:       "default-bucket",
   899  					PathStrategy: prowapi.PathStrategyLegacy,
   900  					DefaultOrg:   "kubernetes",
   901  					DefaultRepo:  "kubernetes",
   902  				},
   903  				GCSCredentialsSecret: pStr("default-service-account"),
   904  			},
   905  		},
   906  		{
   907  			name: "with default, has explicit decorate",
   908  			rawConfig: `
   909  plank:
   910    default_decoration_configs:
   911      '*':
   912        timeout: 2h
   913        grace_period: 15s
   914        utility_images:
   915          clonerefs: "clonerefs:default"
   916          initupload: "initupload:default"
   917          entrypoint: "entrypoint:default"
   918          sidecar: "sidecar:default"
   919        gcs_configuration:
   920          bucket: "default-bucket"
   921          path_strategy: "legacy"
   922          default_org: "kubernetes"
   923          default_repo: "kubernetes"
   924        gcs_credentials_secret: "default-service-account"
   925  
   926  periodics:
   927  - name: kubernetes-defaulted-decoration
   928    interval: 1h
   929    decorate: true
   930    decoration_config:
   931      timeout: 1
   932      grace_period: 1
   933      utility_images:
   934        clonerefs: "clonerefs:explicit"
   935        initupload: "initupload:explicit"
   936        entrypoint: "entrypoint:explicit"
   937        sidecar: "sidecar:explicit"
   938      gcs_configuration:
   939        bucket: "explicit-bucket"
   940        path_strategy: "explicit"
   941      gcs_credentials_secret: "explicit-service-account"
   942    spec:
   943      containers:
   944      - image: golang:latest
   945        args:
   946        - "test"
   947        - "./..."`,
   948  			expected: &prowapi.DecorationConfig{
   949  				Timeout:     &prowapi.Duration{Duration: 1 * time.Nanosecond},
   950  				GracePeriod: &prowapi.Duration{Duration: 1 * time.Nanosecond},
   951  				UtilityImages: &prowapi.UtilityImages{
   952  					CloneRefs:  "clonerefs:explicit",
   953  					InitUpload: "initupload:explicit",
   954  					Entrypoint: "entrypoint:explicit",
   955  					Sidecar:    "sidecar:explicit",
   956  				},
   957  				GCSConfiguration: &prowapi.GCSConfiguration{
   958  					Bucket:       "explicit-bucket",
   959  					PathStrategy: prowapi.PathStrategyExplicit,
   960  					DefaultOrg:   "kubernetes",
   961  					DefaultRepo:  "kubernetes",
   962  				},
   963  				GCSCredentialsSecret: pStr("explicit-service-account"),
   964  			},
   965  		},
   966  		{
   967  			name: "with default, configures bucket explicitly",
   968  			rawConfig: `
   969  plank:
   970    default_decoration_configs:
   971      '*':
   972        timeout: 2h
   973        grace_period: 15s
   974        utility_images:
   975          clonerefs: "clonerefs:default"
   976          initupload: "initupload:default"
   977          entrypoint: "entrypoint:default"
   978          sidecar: "sidecar:default"
   979        gcs_configuration:
   980          bucket: "default-bucket"
   981          path_strategy: "legacy"
   982          default_org: "kubernetes"
   983          default_repo: "kubernetes"
   984          mediaTypes:
   985            log: text/plain
   986        gcs_credentials_secret: "default-service-account"
   987  
   988  periodics:
   989  - name: kubernetes-defaulted-decoration
   990    interval: 1h
   991    decorate: true
   992    decoration_config:
   993      gcs_configuration:
   994        bucket: "explicit-bucket"
   995      gcs_credentials_secret: "explicit-service-account"
   996    spec:
   997      containers:
   998      - image: golang:latest
   999        args:
  1000        - "test"
  1001        - "./..."`,
  1002  			expected: &prowapi.DecorationConfig{
  1003  				Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
  1004  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  1005  				UtilityImages: &prowapi.UtilityImages{
  1006  					CloneRefs:  "clonerefs:default",
  1007  					InitUpload: "initupload:default",
  1008  					Entrypoint: "entrypoint:default",
  1009  					Sidecar:    "sidecar:default",
  1010  				},
  1011  				GCSConfiguration: &prowapi.GCSConfiguration{
  1012  					Bucket:       "explicit-bucket",
  1013  					PathStrategy: prowapi.PathStrategyLegacy,
  1014  					DefaultOrg:   "kubernetes",
  1015  					DefaultRepo:  "kubernetes",
  1016  					MediaTypes:   map[string]string{"log": "text/plain"},
  1017  				},
  1018  				GCSCredentialsSecret: pStr("explicit-service-account"),
  1019  			},
  1020  		},
  1021  		{
  1022  			name: "Just the timeout is overwritten via more specific default_decoration_config ",
  1023  			rawConfig: `
  1024  plank:
  1025    default_decoration_configs:
  1026      '*':
  1027        timeout: 2h
  1028        grace_period: 15s
  1029        utility_images:
  1030          clonerefs: "clonerefs:default"
  1031          initupload: "initupload:default"
  1032          entrypoint: "entrypoint:default"
  1033          sidecar: "sidecar:default"
  1034        gcs_configuration:
  1035          bucket: "default-bucket"
  1036          path_strategy: "legacy"
  1037          default_org: "kubernetes"
  1038          default_repo: "kubernetes"
  1039          mediaTypes:
  1040            log: text/plain
  1041        gcs_credentials_secret: "default-service-account"
  1042      'org/repo':
  1043        timeout: 4h
  1044  
  1045  periodics:
  1046  - name: kubernetes-defaulted-decoration
  1047    interval: 1h
  1048    decorate: true
  1049    extra_refs:
  1050    - org: org
  1051      repo: repo
  1052    spec:
  1053      containers:
  1054      - image: golang:latest
  1055        args:
  1056        - "test"
  1057        - "./..."`,
  1058  			expected: &prowapi.DecorationConfig{
  1059  				Timeout:     &prowapi.Duration{Duration: 4 * time.Hour},
  1060  				GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  1061  				UtilityImages: &prowapi.UtilityImages{
  1062  					CloneRefs:  "clonerefs:default",
  1063  					InitUpload: "initupload:default",
  1064  					Entrypoint: "entrypoint:default",
  1065  					Sidecar:    "sidecar:default",
  1066  				},
  1067  				GCSConfiguration: &prowapi.GCSConfiguration{
  1068  					Bucket:       "default-bucket",
  1069  					PathStrategy: prowapi.PathStrategyLegacy,
  1070  					DefaultOrg:   "kubernetes",
  1071  					DefaultRepo:  "kubernetes",
  1072  					MediaTypes:   map[string]string{"log": "text/plain"},
  1073  				},
  1074  				GCSCredentialsSecret: pStr("default-service-account"),
  1075  			},
  1076  		},
  1077  		{
  1078  			name: "new format; global, org, repo, cluster, org+cluster",
  1079  			rawConfig: `
  1080  plank:
  1081    default_decoration_config_entries:
  1082    - config:
  1083        timeout: 2h
  1084        grace_period: 15s
  1085        utility_images:
  1086          clonerefs: "clonerefs:default"
  1087          initupload: "initupload:default"
  1088          entrypoint: "entrypoint:default"
  1089          sidecar: "sidecar:default"
  1090        gcs_configuration:
  1091          bucket: "default-bucket"
  1092          path_strategy: "legacy"
  1093          default_org: "kubernetes"
  1094          default_repo: "kubernetes"
  1095        gcs_credentials_secret: "default-service-account"
  1096    - repo: "org"
  1097      config:
  1098        timeout: 1h
  1099    - repo: "org/repo"
  1100      config:
  1101        timeout: 3h
  1102    - cluster: "trusted"
  1103      config:
  1104        grace_period: 30s
  1105    - repo: "org/foo"
  1106      cluster: "trusted"
  1107      config:
  1108        grace_period: 1m
  1109  
  1110  periodics:
  1111  - name: kubernetes-defaulted-decoration
  1112    interval: 1h
  1113    decorate: true
  1114    cluster: trusted
  1115    extra_refs:
  1116    - org: org
  1117      repo: foo
  1118      base_ref: master
  1119    spec:
  1120      containers:
  1121      - image: golang:latest
  1122        args:
  1123        - "test"
  1124        - "./..."
  1125  `,
  1126  			expected: &prowapi.DecorationConfig{
  1127  				Timeout:     &prowapi.Duration{Duration: 1 * time.Hour},
  1128  				GracePeriod: &prowapi.Duration{Duration: 1 * time.Minute},
  1129  				UtilityImages: &prowapi.UtilityImages{
  1130  					CloneRefs:  "clonerefs:default",
  1131  					InitUpload: "initupload:default",
  1132  					Entrypoint: "entrypoint:default",
  1133  					Sidecar:    "sidecar:default",
  1134  				},
  1135  				GCSConfiguration: &prowapi.GCSConfiguration{
  1136  					Bucket:       "default-bucket",
  1137  					PathStrategy: prowapi.PathStrategyLegacy,
  1138  					DefaultOrg:   "kubernetes",
  1139  					DefaultRepo:  "kubernetes",
  1140  				},
  1141  				GCSCredentialsSecret: pStr("default-service-account"),
  1142  			},
  1143  		},
  1144  	}
  1145  
  1146  	for _, tc := range testCases {
  1147  		t.Run(tc.name, func(t *testing.T) {
  1148  			// save the config
  1149  			prowConfigDir := t.TempDir()
  1150  
  1151  			prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  1152  			if err := os.WriteFile(prowConfig, []byte(tc.rawConfig), 0666); err != nil {
  1153  				t.Fatalf("fail to write prow config: %v", err)
  1154  			}
  1155  
  1156  			// all errors in Load should also apply in LoadStrict
  1157  			// some errors in LoadStrict will not apply in Load
  1158  			tc.expectStrictError = tc.expectStrictError || tc.expectError
  1159  			_, err := LoadStrict(prowConfig, "", nil, "")
  1160  			if tc.expectStrictError && err == nil {
  1161  				t.Errorf("tc %s: Expect error for LoadStrict, but got nil", tc.name)
  1162  			} else if !tc.expectStrictError && err != nil {
  1163  				t.Fatalf("tc %s: Expect no error for LoadStrict, but got error %v", tc.name, err)
  1164  			}
  1165  
  1166  			cfg, err := Load(prowConfig, "", nil, "")
  1167  			if tc.expectError && err == nil {
  1168  				t.Errorf("tc %s: Expect error for Load, but got nil", tc.name)
  1169  			} else if !tc.expectError && err != nil {
  1170  				t.Fatalf("tc %s: Expect no error for Load, but got error %v", tc.name, err)
  1171  			}
  1172  
  1173  			if tc.expected != nil {
  1174  				if len(cfg.Periodics) != 1 {
  1175  					t.Fatalf("tc %s: Expect to have one periodic job, got none", tc.name)
  1176  				}
  1177  
  1178  				if diff := cmp.Diff(cfg.Periodics[0].DecorationConfig, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  1179  					t.Errorf("got diff: %s", diff)
  1180  				}
  1181  			}
  1182  		})
  1183  	}
  1184  }
  1185  
  1186  func TestGerritRawYaml(t *testing.T) {
  1187  	t.Parallel()
  1188  	var testCases = []struct {
  1189  		name        string
  1190  		expectError bool
  1191  		rawConfig   string
  1192  		expected    Gerrit
  1193  	}{
  1194  		{
  1195  			name:        "no-default",
  1196  			expectError: false,
  1197  			rawConfig: `
  1198  gerrit:
  1199  `,
  1200  			expected: Gerrit{
  1201  				TickInterval: &metav1.Duration{Duration: time.Minute},
  1202  				RateLimit:    5,
  1203  			},
  1204  		},
  1205  		{
  1206  			name:        "override-default",
  1207  			expectError: false,
  1208  			rawConfig: `
  1209  gerrit:
  1210    tick_interval: 2s
  1211    ratelimit: 10
  1212    allowed_presubmit_trigger_re: "/test units"
  1213  `,
  1214  			expected: Gerrit{
  1215  				AllowedPresubmitTriggerReRawString: "/test units",
  1216  				TickInterval:                       &metav1.Duration{Duration: time.Second * 2},
  1217  				RateLimit:                          10,
  1218  			},
  1219  		},
  1220  		{
  1221  			name:        "simple-org-repo",
  1222  			expectError: false,
  1223  			rawConfig: `
  1224  gerrit:
  1225    org_repos_config:
  1226    - org: org-a
  1227      repos:
  1228      - repo-b
  1229  `,
  1230  			expected: Gerrit{
  1231  				TickInterval: &metav1.Duration{Duration: time.Minute},
  1232  				RateLimit:    5,
  1233  				OrgReposConfig: &GerritOrgRepoConfigs{
  1234  					{
  1235  						Org:   "org-a",
  1236  						Repos: []string{"repo-b"},
  1237  					},
  1238  				},
  1239  			},
  1240  		},
  1241  		{
  1242  			name:        "multiple-org-repo",
  1243  			expectError: false,
  1244  			rawConfig: `
  1245  gerrit:
  1246    org_repos_config:
  1247    - org: org-a
  1248      repos:
  1249      - repo-b
  1250    - org: org-c
  1251      repos:
  1252      - repo-d
  1253  `,
  1254  			expected: Gerrit{
  1255  				TickInterval: &metav1.Duration{Duration: time.Minute},
  1256  				RateLimit:    5,
  1257  				OrgReposConfig: &GerritOrgRepoConfigs{
  1258  					{
  1259  						Org:   "org-a",
  1260  						Repos: []string{"repo-b"},
  1261  					},
  1262  					{
  1263  						Org:   "org-c",
  1264  						Repos: []string{"repo-d"},
  1265  					},
  1266  				},
  1267  			},
  1268  		},
  1269  	}
  1270  
  1271  	for _, tc := range testCases {
  1272  		t.Run(tc.name, func(t *testing.T) {
  1273  			// save the config
  1274  			prowConfigDir := t.TempDir()
  1275  
  1276  			prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  1277  			if err := os.WriteFile(prowConfig, []byte(tc.rawConfig), 0666); err != nil {
  1278  				t.Fatalf("fail to write prow config: %v", err)
  1279  			}
  1280  
  1281  			cfg, err := Load(prowConfig, "", nil, "")
  1282  			if tc.expectError && err == nil {
  1283  				t.Errorf("tc %s: Expect error, but got nil", tc.name)
  1284  			} else if !tc.expectError && err != nil {
  1285  				t.Fatalf("tc %s: Expect no error, but got error %v", tc.name, err)
  1286  			}
  1287  
  1288  			if d := cmp.Diff(tc.expected, cfg.Gerrit, cmpopts.EquateEmpty(), cmpopts.IgnoreFields(Gerrit{}, "AllowedPresubmitTriggerRe")); d != "" {
  1289  				t.Errorf("got d: %s", d)
  1290  			}
  1291  		})
  1292  	}
  1293  }
  1294  
  1295  func TestDisabledClustersRawYaml(t *testing.T) {
  1296  	t.Parallel()
  1297  	var testCases = []struct {
  1298  		name        string
  1299  		expectError bool
  1300  		rawConfig   string
  1301  		expected    []string
  1302  	}{
  1303  		{
  1304  			name:        "default value",
  1305  			expectError: false,
  1306  			rawConfig:   `a: b`,
  1307  		},
  1308  		{
  1309  			name:        "basic case",
  1310  			expectError: false,
  1311  			rawConfig: `disabled_clusters:
  1312  - build01
  1313  - build08
  1314  `,
  1315  			expected: []string{"build01", "build08"},
  1316  		},
  1317  		{
  1318  			name:        "duplicates without ordering",
  1319  			expectError: false,
  1320  			rawConfig: `disabled_clusters:
  1321  - build08
  1322  - build08
  1323  - build01
  1324  `,
  1325  			expected: []string{"build08", "build08", "build01"},
  1326  		},
  1327  	}
  1328  
  1329  	for _, tc := range testCases {
  1330  		t.Run(tc.name, func(t *testing.T) {
  1331  			// save the config
  1332  			prowConfigDir := t.TempDir()
  1333  
  1334  			prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  1335  			if err := os.WriteFile(prowConfig, []byte(tc.rawConfig), 0666); err != nil {
  1336  				t.Fatalf("fail to write prow config: %v", err)
  1337  			}
  1338  
  1339  			cfg, err := Load(prowConfig, "", nil, "")
  1340  			if tc.expectError && err == nil {
  1341  				t.Errorf("tc %s: Expect error, but got nil", tc.name)
  1342  			} else if !tc.expectError && err != nil {
  1343  				t.Fatalf("tc %s: Expect no error, but got error %v", tc.name, err)
  1344  			}
  1345  
  1346  			if d := cmp.Diff(tc.expected, cfg.DisabledClusters); d != "" {
  1347  				t.Errorf("got d: %s", d)
  1348  			}
  1349  		})
  1350  	}
  1351  }
  1352  
  1353  func TestValidateAgent(t *testing.T) {
  1354  	jenk := string(prowapi.JenkinsAgent)
  1355  	k := string(prowapi.KubernetesAgent)
  1356  	ns := "default"
  1357  	base := JobBase{
  1358  		Agent:     k,
  1359  		Namespace: &ns,
  1360  		Spec:      &v1.PodSpec{},
  1361  		UtilityConfig: UtilityConfig{
  1362  			DecorationConfig: &prowapi.DecorationConfig{},
  1363  		},
  1364  	}
  1365  
  1366  	cases := []struct {
  1367  		name string
  1368  		base func(j *JobBase)
  1369  		pass bool
  1370  	}{
  1371  		{
  1372  			name: "accept unknown agent",
  1373  			base: func(j *JobBase) {
  1374  				j.Agent = "random-agent"
  1375  			},
  1376  			pass: true,
  1377  		},
  1378  		{
  1379  			name: "kubernetes agent requires spec",
  1380  			base: func(j *JobBase) {
  1381  				j.Spec = nil
  1382  			},
  1383  		},
  1384  		{
  1385  			name: "non-nil namespace required",
  1386  			base: func(j *JobBase) {
  1387  				j.Namespace = nil
  1388  			},
  1389  		},
  1390  		{
  1391  			name: "filled namespace required",
  1392  			base: func(j *JobBase) {
  1393  				var s string
  1394  				j.Namespace = &s
  1395  			},
  1396  		},
  1397  		{
  1398  			name: "custom namespace requires knative-build agent",
  1399  			base: func(j *JobBase) {
  1400  				s := "custom-namespace"
  1401  				j.Namespace = &s
  1402  			},
  1403  		},
  1404  		{
  1405  			name: "accept kubernetes agent",
  1406  			pass: true,
  1407  		},
  1408  		{
  1409  			name: "accept kubernetes agent without decoration",
  1410  			base: func(j *JobBase) {
  1411  				j.DecorationConfig = nil
  1412  			},
  1413  			pass: true,
  1414  		},
  1415  		{
  1416  			name: "accept jenkins agent",
  1417  			base: func(j *JobBase) {
  1418  				j.Agent = jenk
  1419  				j.Spec = nil
  1420  				j.DecorationConfig = nil
  1421  			},
  1422  			pass: true,
  1423  		},
  1424  		{
  1425  			name: "error_on_eviction allowed for kubernetes agent",
  1426  			base: func(j *JobBase) {
  1427  				j.ErrorOnEviction = true
  1428  			},
  1429  			pass: true,
  1430  		},
  1431  	}
  1432  
  1433  	for _, tc := range cases {
  1434  		t.Run(tc.name, func(t *testing.T) {
  1435  			jb := base
  1436  			if tc.base != nil {
  1437  				tc.base(&jb)
  1438  			}
  1439  			switch err := validateAgent(jb, ns); {
  1440  			case err == nil && !tc.pass:
  1441  				t.Error("validation failed to raise an error")
  1442  			case err != nil && tc.pass:
  1443  				t.Errorf("validation should have passed, got: %v", err)
  1444  			}
  1445  		})
  1446  	}
  1447  }
  1448  
  1449  func TestValidatePodSpec(t *testing.T) {
  1450  	periodEnv := sets.New[string](downwardapi.EnvForType(prowapi.PeriodicJob)...)
  1451  	postEnv := sets.New[string](downwardapi.EnvForType(prowapi.PostsubmitJob)...)
  1452  	preEnv := sets.New[string](downwardapi.EnvForType(prowapi.PresubmitJob)...)
  1453  	cases := []struct {
  1454  		name             string
  1455  		jobType          prowapi.ProwJobType
  1456  		spec             func(s *v1.PodSpec)
  1457  		decorationConfig *prowapi.DecorationConfig
  1458  		noSpec           bool
  1459  		pass             bool
  1460  	}{
  1461  		{
  1462  			name:   "allow nil spec",
  1463  			noSpec: true,
  1464  			pass:   true,
  1465  		},
  1466  		{
  1467  			name: "happy case",
  1468  			pass: true,
  1469  		},
  1470  		{
  1471  			name: "reject init containers",
  1472  			spec: func(s *v1.PodSpec) {
  1473  				s.InitContainers = []v1.Container{
  1474  					{},
  1475  				}
  1476  			},
  1477  		},
  1478  		{
  1479  			name: "reject 0 containers",
  1480  			spec: func(s *v1.PodSpec) {
  1481  				s.Containers = nil
  1482  			},
  1483  		},
  1484  		{
  1485  			name: "reject 2 containers",
  1486  			spec: func(s *v1.PodSpec) {
  1487  				s.Containers = append(s.Containers, v1.Container{})
  1488  			},
  1489  		},
  1490  		{
  1491  			name:    "reject reserved presubmit env",
  1492  			jobType: prowapi.PresubmitJob,
  1493  			spec: func(s *v1.PodSpec) {
  1494  				// find a presubmit value
  1495  				for n := range preEnv.Difference(postEnv).Difference(periodEnv) {
  1496  
  1497  					s.Containers[0].Env = append(s.Containers[0].Env, v1.EnvVar{Name: n, Value: "whatever"})
  1498  				}
  1499  				if len(s.Containers[0].Env) == 0 {
  1500  					t.Fatal("empty env")
  1501  				}
  1502  			},
  1503  		},
  1504  		{
  1505  			name:    "reject reserved postsubmit env",
  1506  			jobType: prowapi.PostsubmitJob,
  1507  			spec: func(s *v1.PodSpec) {
  1508  				// find a postsubmit value
  1509  				for n := range postEnv.Difference(periodEnv) {
  1510  
  1511  					s.Containers[0].Env = append(s.Containers[0].Env, v1.EnvVar{Name: n, Value: "whatever"})
  1512  				}
  1513  				if len(s.Containers[0].Env) == 0 {
  1514  					t.Fatal("empty env")
  1515  				}
  1516  			},
  1517  		},
  1518  		{
  1519  			name:    "reject reserved periodic env",
  1520  			jobType: prowapi.PeriodicJob,
  1521  			spec: func(s *v1.PodSpec) {
  1522  				// find a postsubmit value
  1523  				for n := range periodEnv {
  1524  
  1525  					s.Containers[0].Env = append(s.Containers[0].Env, v1.EnvVar{Name: n, Value: "whatever"})
  1526  				}
  1527  				if len(s.Containers[0].Env) == 0 {
  1528  					t.Fatal("empty env")
  1529  				}
  1530  			},
  1531  		},
  1532  		{
  1533  			name: "reject reserved mount name",
  1534  			spec: func(s *v1.PodSpec) {
  1535  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1536  					Name:      sets.List(decorate.VolumeMounts(nil))[0],
  1537  					MountPath: "/whatever",
  1538  				})
  1539  			},
  1540  		},
  1541  		{
  1542  			name: "reject reserved mount path",
  1543  			spec: func(s *v1.PodSpec) {
  1544  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1545  					Name:      "fun",
  1546  					MountPath: sets.List(decorate.VolumeMountPathsOnTestContainer())[0],
  1547  				})
  1548  			},
  1549  		},
  1550  		{
  1551  			name: "accept conflicting mount path parent",
  1552  			spec: func(s *v1.PodSpec) {
  1553  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1554  					Name:      "foo",
  1555  					MountPath: filepath.Dir(sets.List(decorate.VolumeMountPathsOnTestContainer())[0]),
  1556  				})
  1557  				s.Volumes = append(s.Volumes, v1.Volume{
  1558  					Name: "foo",
  1559  				})
  1560  			},
  1561  			pass: true,
  1562  		},
  1563  		{
  1564  			name: "accept conflicting mount path child",
  1565  			spec: func(s *v1.PodSpec) {
  1566  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1567  					Name:      "foo",
  1568  					MountPath: filepath.Join(sets.List(decorate.VolumeMountPathsOnTestContainer())[0], "extra"),
  1569  				})
  1570  				s.Volumes = append(s.Volumes, v1.Volume{
  1571  					Name: "foo",
  1572  				})
  1573  			},
  1574  			pass: true,
  1575  		},
  1576  		{
  1577  			name: "accept mount path that works only through decoration volume",
  1578  			spec: func(s *v1.PodSpec) {
  1579  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1580  					Name:      "gcs-credentials",
  1581  					MountPath: "/secrets/gcs",
  1582  				})
  1583  			},
  1584  			pass: true,
  1585  		},
  1586  		{
  1587  			name:             "accept mount path that works only through decoration volume specified by user",
  1588  			decorationConfig: &prowapi.DecorationConfig{OauthTokenSecret: &prowapi.OauthTokenSecret{Name: "my-oauth-secret-name", Key: "oauth"}},
  1589  			spec: func(s *v1.PodSpec) {
  1590  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{
  1591  					Name:      "my-oauth-secret-name",
  1592  					MountPath: "/secrets/oauth",
  1593  				})
  1594  			},
  1595  			pass: true,
  1596  		},
  1597  		{
  1598  			name: "accept multiple mount paths that works only through decoration volume specified by user",
  1599  			decorationConfig: &prowapi.DecorationConfig{
  1600  				OauthTokenSecret: &prowapi.OauthTokenSecret{Name: "my-oauth-secret-name", Key: "oauth"},
  1601  				SSHKeySecrets:    []string{"ssh-private-1", "ssh-private-2"},
  1602  			},
  1603  			spec: func(s *v1.PodSpec) {
  1604  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, []v1.VolumeMount{
  1605  					{
  1606  						Name:      "my-oauth-secret-name",
  1607  						MountPath: "/secrets/oauth",
  1608  					},
  1609  					{
  1610  						Name:      "ssh-private-1",
  1611  						MountPath: "/secrets/ssh-private-1",
  1612  					},
  1613  					{
  1614  						Name:      "ssh-private-2",
  1615  						MountPath: "/secrets/ssh-private-2",
  1616  					},
  1617  				}...)
  1618  			},
  1619  			pass: true,
  1620  		},
  1621  		{
  1622  			name: "reject reserved volume",
  1623  			spec: func(s *v1.PodSpec) {
  1624  				s.Volumes = append(s.Volumes, v1.Volume{Name: sets.List(decorate.VolumeMounts(nil))[0]})
  1625  			},
  1626  		},
  1627  		{
  1628  			name: "reject duplicate env",
  1629  			spec: func(s *v1.PodSpec) {
  1630  				s.Containers[0].Env = append(s.Containers[0].Env, v1.EnvVar{Name: "foo", Value: "bar"})
  1631  				s.Containers[0].Env = append(s.Containers[0].Env, v1.EnvVar{Name: "foo", Value: "baz"})
  1632  			},
  1633  		},
  1634  		{
  1635  			name: "reject duplicate volume",
  1636  			spec: func(s *v1.PodSpec) {
  1637  				s.Volumes = append(s.Volumes, v1.Volume{Name: "foo"})
  1638  				s.Volumes = append(s.Volumes, v1.Volume{Name: "foo"})
  1639  			},
  1640  		},
  1641  		{
  1642  			name: "reject undefined volume reference",
  1643  			spec: func(s *v1.PodSpec) {
  1644  				s.Containers[0].VolumeMounts = append(s.Containers[0].VolumeMounts, v1.VolumeMount{Name: "foo", MountPath: "/not-used-by-decoration-utils"})
  1645  			},
  1646  		},
  1647  	}
  1648  
  1649  	spec := v1.PodSpec{
  1650  		Containers: []v1.Container{
  1651  			{},
  1652  		},
  1653  	}
  1654  
  1655  	for _, tc := range cases {
  1656  		t.Run(tc.name, func(t *testing.T) {
  1657  			jt := prowapi.PresubmitJob
  1658  			if tc.jobType != "" {
  1659  				jt = tc.jobType
  1660  			}
  1661  			current := spec.DeepCopy()
  1662  			if tc.noSpec {
  1663  				current = nil
  1664  			} else if tc.spec != nil {
  1665  				tc.spec(current)
  1666  			}
  1667  			switch err := validatePodSpec(jt, current, tc.decorationConfig); {
  1668  			case err == nil && !tc.pass:
  1669  				t.Error("validation failed to raise an error")
  1670  			case err != nil && tc.pass:
  1671  				t.Errorf("validation should have passed, got: %v", err)
  1672  			}
  1673  		})
  1674  	}
  1675  }
  1676  
  1677  func TestValidatePipelineRunSpec(t *testing.T) {
  1678  	cases := []struct {
  1679  		name      string
  1680  		jobType   prowapi.ProwJobType
  1681  		spec      func(s *pipelinev1beta1.PipelineRunSpec)
  1682  		extraRefs []prowapi.Refs
  1683  		noSpec    bool
  1684  		pass      bool
  1685  	}{
  1686  		{
  1687  			name:   "allow nil spec",
  1688  			noSpec: true,
  1689  			pass:   true,
  1690  		},
  1691  		{
  1692  			name: "happy case",
  1693  			pass: true,
  1694  		},
  1695  		{
  1696  			name:    "reject implicit ref for periodic",
  1697  			jobType: prowapi.PeriodicJob,
  1698  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1699  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1700  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_IMPLICIT_GIT_REF"}}}}
  1701  			},
  1702  			pass: false,
  1703  		},
  1704  		{
  1705  			name:    "allow implicit ref for presubmit",
  1706  			jobType: prowapi.PresubmitJob,
  1707  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1708  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1709  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_IMPLICIT_GIT_REF"}}}}
  1710  			},
  1711  			pass: true,
  1712  		},
  1713  		{
  1714  			name:    "allow implicit ref for postsubmit",
  1715  			jobType: prowapi.PostsubmitJob,
  1716  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1717  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1718  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_IMPLICIT_GIT_REF"}}}}
  1719  			},
  1720  			pass: true,
  1721  		},
  1722  		{
  1723  			name: "reject extra refs usage with no extra refs",
  1724  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1725  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1726  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_0"}}}}
  1727  			},
  1728  			pass: false,
  1729  		},
  1730  		{
  1731  			name: "allow extra refs usage with extra refs",
  1732  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1733  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1734  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_0"}}}}
  1735  			},
  1736  			extraRefs: []prowapi.Refs{{Org: "o", Repo: "r"}},
  1737  			pass:      true,
  1738  		},
  1739  		{
  1740  			name: "reject wrong extra refs index usage",
  1741  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1742  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1743  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_1"}}}}
  1744  			},
  1745  			extraRefs: []prowapi.Refs{{Org: "o", Repo: "r"}},
  1746  			pass:      false,
  1747  		},
  1748  		{
  1749  			name:      "reject extra refs without usage",
  1750  			extraRefs: []prowapi.Refs{{Org: "o", Repo: "r"}},
  1751  			pass:      false,
  1752  		},
  1753  		{
  1754  			name: "allow unrelated resource refs",
  1755  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1756  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1757  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "some-other-ref"}}}}
  1758  			},
  1759  			pass: true,
  1760  		},
  1761  		{
  1762  			name: "reject leading zeros when extra ref usage is otherwise valid",
  1763  			spec: func(s *pipelinev1beta1.PipelineRunSpec) {
  1764  				s.PipelineSpec = &pipelinev1beta1.PipelineSpec{
  1765  					Tasks: []pipelinev1beta1.PipelineTask{{Name: "git ref", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_000"}}}}
  1766  			},
  1767  			extraRefs: []prowapi.Refs{{Org: "o", Repo: "r"}},
  1768  			pass:      false,
  1769  		},
  1770  	}
  1771  
  1772  	spec := pipelinev1beta1.PipelineRunSpec{}
  1773  
  1774  	for _, tc := range cases {
  1775  		t.Run(tc.name, func(t *testing.T) {
  1776  			jt := prowapi.PresubmitJob
  1777  			if tc.jobType != "" {
  1778  				jt = tc.jobType
  1779  			}
  1780  			current := spec.DeepCopy()
  1781  			if tc.noSpec {
  1782  				current = nil
  1783  			} else if tc.spec != nil {
  1784  				tc.spec(current)
  1785  			}
  1786  			switch err := ValidatePipelineRunSpec(jt, tc.extraRefs, current); {
  1787  			case err == nil && !tc.pass:
  1788  				t.Error("validation failed to raise an error")
  1789  			case err != nil && tc.pass:
  1790  				t.Errorf("validation should have passed, got: %v", err)
  1791  			}
  1792  		})
  1793  	}
  1794  }
  1795  
  1796  func TestValidateDecoration(t *testing.T) {
  1797  	defCfg := prowapi.DecorationConfig{
  1798  		UtilityImages: &prowapi.UtilityImages{
  1799  			CloneRefs:  "clone-me",
  1800  			InitUpload: "upload-me",
  1801  			Entrypoint: "enter-me",
  1802  			Sidecar:    "official-drink-of-the-org",
  1803  		},
  1804  		GCSCredentialsSecret: pStr("upload-secret"),
  1805  		GCSConfiguration: &prowapi.GCSConfiguration{
  1806  			PathStrategy: prowapi.PathStrategyExplicit,
  1807  			DefaultOrg:   "so-org",
  1808  			DefaultRepo:  "very-repo",
  1809  		},
  1810  	}
  1811  	cases := []struct {
  1812  		name      string
  1813  		container v1.Container
  1814  		config    *prowapi.DecorationConfig
  1815  		pass      bool
  1816  	}{
  1817  		{
  1818  			name: "allow no decoration",
  1819  			pass: true,
  1820  		},
  1821  		{
  1822  			name:   "happy case with cmd",
  1823  			config: &defCfg,
  1824  			container: v1.Container{
  1825  				Command: []string{"hello", "world"},
  1826  			},
  1827  			pass: true,
  1828  		},
  1829  		{
  1830  			name:   "happy case with args",
  1831  			config: &defCfg,
  1832  			container: v1.Container{
  1833  				Args: []string{"hello", "world"},
  1834  			},
  1835  			pass: true,
  1836  		},
  1837  		{
  1838  			name:   "reject invalid decoration config",
  1839  			config: &prowapi.DecorationConfig{},
  1840  			container: v1.Container{
  1841  				Command: []string{"hello", "world"},
  1842  			},
  1843  		},
  1844  		{
  1845  			name:   "reject container that has no cmd, no args",
  1846  			config: &defCfg,
  1847  		},
  1848  	}
  1849  	for _, tc := range cases {
  1850  		t.Run(tc.name, func(t *testing.T) {
  1851  			switch err := validateDecoration(tc.container, tc.config); {
  1852  			case err == nil && !tc.pass:
  1853  				t.Error("validation failed to raise an error")
  1854  			case err != nil && tc.pass:
  1855  				t.Errorf("validation should have passed, got: %v", err)
  1856  			}
  1857  		})
  1858  	}
  1859  }
  1860  
  1861  func TestValidateLabels(t *testing.T) {
  1862  	cases := []struct {
  1863  		name   string
  1864  		labels map[string]string
  1865  		pass   bool
  1866  	}{
  1867  		{
  1868  			name: "happy case",
  1869  			pass: true,
  1870  		},
  1871  		{
  1872  			name: "reject reserved label",
  1873  			labels: map[string]string{
  1874  				decorate.Labels()[0]: "anything",
  1875  			},
  1876  		},
  1877  		{
  1878  			name: "reject bad label key",
  1879  			labels: map[string]string{
  1880  				"_underscore-prefix": "annoying",
  1881  			},
  1882  		},
  1883  		{
  1884  			name: "reject bad label value",
  1885  			labels: map[string]string{
  1886  				"whatever": "_private-is-rejected",
  1887  			},
  1888  		},
  1889  	}
  1890  
  1891  	for _, tc := range cases {
  1892  		t.Run(tc.name, func(t *testing.T) {
  1893  			switch err := validateLabels(tc.labels); {
  1894  			case err == nil && !tc.pass:
  1895  				t.Error("validation failed to raise an error")
  1896  			case err != nil && tc.pass:
  1897  				t.Errorf("validation should have passed, got: %v", err)
  1898  			}
  1899  		})
  1900  	}
  1901  }
  1902  
  1903  func TestValidateMultipleContainers(t *testing.T) {
  1904  	ka := string(prowapi.KubernetesAgent)
  1905  	yes := true
  1906  	defCfg := prowapi.DecorationConfig{
  1907  		UtilityImages: &prowapi.UtilityImages{
  1908  			CloneRefs:  "clone-me",
  1909  			InitUpload: "upload-me",
  1910  			Entrypoint: "enter-me",
  1911  			Sidecar:    "official-drink-of-the-org",
  1912  		},
  1913  		GCSCredentialsSecret: pStr("upload-secret"),
  1914  		GCSConfiguration: &prowapi.GCSConfiguration{
  1915  			PathStrategy: prowapi.PathStrategyExplicit,
  1916  			DefaultOrg:   "so-org",
  1917  			DefaultRepo:  "very-repo",
  1918  		},
  1919  	}
  1920  	goodSpec := v1.PodSpec{
  1921  		Containers: []v1.Container{
  1922  			{
  1923  				Name:    "test1",
  1924  				Command: []string{"hello", "world"},
  1925  			},
  1926  			{
  1927  				Name: "test2",
  1928  				Args: []string{"hello", "world"},
  1929  			},
  1930  		},
  1931  	}
  1932  	cfg := Config{
  1933  		ProwConfig: ProwConfig{PodNamespace: "target-namespace"},
  1934  	}
  1935  	cases := []struct {
  1936  		name string
  1937  		base JobBase
  1938  		pass bool
  1939  	}{
  1940  		{
  1941  			name: "valid kubernetes job with multiple containers",
  1942  			base: JobBase{
  1943  				Name:          "name",
  1944  				Agent:         ka,
  1945  				UtilityConfig: UtilityConfig{Decorate: &yes, DecorationConfig: &defCfg},
  1946  				Spec:          &goodSpec,
  1947  				Namespace:     &cfg.PodNamespace,
  1948  			},
  1949  			pass: true,
  1950  		},
  1951  		{
  1952  			name: "invalid: containers with no cmd or args",
  1953  			base: JobBase{
  1954  				Name:          "name",
  1955  				Agent:         ka,
  1956  				UtilityConfig: UtilityConfig{Decorate: &yes, DecorationConfig: &defCfg},
  1957  				Spec: &v1.PodSpec{
  1958  					Containers: []v1.Container{
  1959  						{
  1960  							Name: "test1",
  1961  						},
  1962  						{
  1963  							Name: "test2",
  1964  						},
  1965  					},
  1966  				},
  1967  				Namespace: &cfg.PodNamespace,
  1968  			},
  1969  		},
  1970  		{
  1971  			name: "invalid: containers with no names",
  1972  			base: JobBase{
  1973  				Name:          "name",
  1974  				Agent:         ka,
  1975  				UtilityConfig: UtilityConfig{Decorate: &yes, DecorationConfig: &defCfg},
  1976  				Spec: &v1.PodSpec{
  1977  					Containers: []v1.Container{
  1978  						{
  1979  							Command: []string{"hello", "world"},
  1980  						},
  1981  						{
  1982  							Args: []string{"hello", "world"},
  1983  						},
  1984  					},
  1985  				},
  1986  				Namespace: &cfg.PodNamespace,
  1987  			},
  1988  		},
  1989  		{
  1990  			name: "invalid: no decoration enabled",
  1991  			base: JobBase{
  1992  				Name:      "name",
  1993  				Agent:     ka,
  1994  				Spec:      &goodSpec,
  1995  				Namespace: &cfg.PodNamespace,
  1996  			},
  1997  		},
  1998  		{
  1999  			name: "invalid: container names reserved for decoration",
  2000  			base: JobBase{
  2001  				Name:          "name",
  2002  				Agent:         ka,
  2003  				UtilityConfig: UtilityConfig{Decorate: &yes, DecorationConfig: &defCfg},
  2004  				Spec: &v1.PodSpec{
  2005  					Containers: []v1.Container{
  2006  						{
  2007  							Name:    "place-entrypoint",
  2008  							Command: []string{"hello", "world"},
  2009  						},
  2010  						{
  2011  							Name: "sidecar",
  2012  							Args: []string{"hello", "world"},
  2013  						},
  2014  					},
  2015  				}, Namespace: &cfg.PodNamespace,
  2016  			},
  2017  		},
  2018  	}
  2019  
  2020  	for _, tc := range cases {
  2021  		t.Run(tc.name, func(t *testing.T) {
  2022  			switch err := cfg.validateJobBase(tc.base, prowapi.PresubmitJob); {
  2023  			case err == nil && !tc.pass:
  2024  				t.Error("validation failed to raise an error")
  2025  			case err != nil && tc.pass:
  2026  				t.Errorf("validation should have passed, got: %v", err)
  2027  			}
  2028  		})
  2029  	}
  2030  }
  2031  
  2032  func TestValidateJobBase(t *testing.T) {
  2033  	ka := string(prowapi.KubernetesAgent)
  2034  	ja := string(prowapi.JenkinsAgent)
  2035  	goodSpec := v1.PodSpec{
  2036  		Containers: []v1.Container{
  2037  			{},
  2038  		},
  2039  	}
  2040  	cfg := Config{
  2041  		ProwConfig: ProwConfig{
  2042  			Plank:        Plank{JobQueueCapacities: map[string]int{"queue": 0}},
  2043  			PodNamespace: "target-namespace",
  2044  		},
  2045  	}
  2046  	cases := []struct {
  2047  		name string
  2048  		base JobBase
  2049  		pass bool
  2050  	}{
  2051  		{
  2052  			name: "valid kubernetes job",
  2053  			base: JobBase{
  2054  				Name:      "name",
  2055  				Agent:     ka,
  2056  				Spec:      &goodSpec,
  2057  				Namespace: &cfg.PodNamespace,
  2058  			},
  2059  			pass: true,
  2060  		},
  2061  		{
  2062  			name: "valid jenkins job",
  2063  			base: JobBase{
  2064  				Name:      "name",
  2065  				Agent:     ja,
  2066  				Namespace: &cfg.PodNamespace,
  2067  			},
  2068  			pass: true,
  2069  		},
  2070  		{
  2071  			name: "valid jenkins job - nested job",
  2072  			base: JobBase{
  2073  				Name:      "folder/job",
  2074  				Agent:     ja,
  2075  				Namespace: &cfg.PodNamespace,
  2076  			},
  2077  			pass: true,
  2078  		},
  2079  		{
  2080  			name: "invalid jenkins job",
  2081  			base: JobBase{
  2082  				Name:      "job.",
  2083  				Agent:     ja,
  2084  				Namespace: &cfg.PodNamespace,
  2085  			},
  2086  			pass: false,
  2087  		},
  2088  		{
  2089  			name: "invalid concurrency",
  2090  			base: JobBase{
  2091  				Name:           "name",
  2092  				MaxConcurrency: -1,
  2093  				Agent:          ka,
  2094  				Spec:           &goodSpec,
  2095  				Namespace:      &cfg.PodNamespace,
  2096  			},
  2097  		},
  2098  		{
  2099  			name: "invalid pod spec",
  2100  			base: JobBase{
  2101  				Name:      "name",
  2102  				Agent:     ka,
  2103  				Namespace: &cfg.PodNamespace,
  2104  				Spec:      &v1.PodSpec{}, // no containers
  2105  			},
  2106  		},
  2107  		{
  2108  			name: "invalid decoration",
  2109  			base: JobBase{
  2110  				Name:  "name",
  2111  				Agent: ka,
  2112  				Spec:  &goodSpec,
  2113  				UtilityConfig: UtilityConfig{
  2114  					DecorationConfig: &prowapi.DecorationConfig{}, // missing many fields
  2115  				},
  2116  				Namespace: &cfg.PodNamespace,
  2117  			},
  2118  		},
  2119  		{
  2120  			name: "invalid labels",
  2121  			base: JobBase{
  2122  				Name:  "name",
  2123  				Agent: ka,
  2124  				Spec:  &goodSpec,
  2125  				Labels: map[string]string{
  2126  					"_leading_underscore": "_rejected",
  2127  				},
  2128  				Namespace: &cfg.PodNamespace,
  2129  			},
  2130  		},
  2131  		{
  2132  			name: "invalid name",
  2133  			base: JobBase{
  2134  				Name:      "a/b",
  2135  				Agent:     ka,
  2136  				Spec:      &goodSpec,
  2137  				Namespace: &cfg.PodNamespace,
  2138  			},
  2139  			pass: false,
  2140  		},
  2141  		{
  2142  			name: "valid complex name",
  2143  			base: JobBase{
  2144  				Name:      "a-b.c",
  2145  				Agent:     ka,
  2146  				Spec:      &goodSpec,
  2147  				Namespace: &cfg.PodNamespace,
  2148  			},
  2149  			pass: true,
  2150  		},
  2151  		{
  2152  			name: "invalid rerun_permissions",
  2153  			base: JobBase{
  2154  				RerunAuthConfig: &prowapi.RerunAuthConfig{
  2155  					AllowAnyone: true,
  2156  					GitHubUsers: []string{"user"},
  2157  				},
  2158  			},
  2159  			pass: false,
  2160  		},
  2161  		{
  2162  			name: "valid job queue name",
  2163  			base: JobBase{
  2164  				Name:         "name",
  2165  				JobQueueName: "queue",
  2166  			},
  2167  			pass: true,
  2168  		},
  2169  		{
  2170  			name: "invalid job queue name",
  2171  			base: JobBase{
  2172  				Name:         "name",
  2173  				JobQueueName: "invalid-queue",
  2174  			},
  2175  			pass: false,
  2176  		},
  2177  	}
  2178  
  2179  	for _, tc := range cases {
  2180  		t.Run(tc.name, func(t *testing.T) {
  2181  			switch err := cfg.validateJobBase(tc.base, prowapi.PresubmitJob); {
  2182  			case err == nil && !tc.pass:
  2183  				t.Error("validation failed to raise an error")
  2184  			case err != nil && tc.pass:
  2185  				t.Errorf("validation should have passed, got: %v", err)
  2186  			}
  2187  		})
  2188  	}
  2189  }
  2190  
  2191  func TestValidateDeck(t *testing.T) {
  2192  	boolTrue := true
  2193  	boolFalse := false
  2194  	cases := []struct {
  2195  		name        string
  2196  		deck        Deck
  2197  		expectedErr string
  2198  	}{
  2199  		{
  2200  			name:        "empty Deck is valid",
  2201  			deck:        Deck{},
  2202  			expectedErr: "",
  2203  		},
  2204  		{
  2205  			name:        "AdditionalAllowedBuckets has items, SkipStoragePathValidation is false => no errors",
  2206  			deck:        Deck{SkipStoragePathValidation: &boolFalse, AdditionalAllowedBuckets: []string{"foo", "bar", "batz"}},
  2207  			expectedErr: "",
  2208  		},
  2209  		{
  2210  			name:        "AdditionalAllowedBuckets has items, SkipStoragePathValidation is default value => error",
  2211  			deck:        Deck{AdditionalAllowedBuckets: []string{"hello", "world"}},
  2212  			expectedErr: "skip_storage_path_validation is enabled",
  2213  		},
  2214  		{
  2215  			name:        "AdditionalAllowedBuckets has items, SkipStoragePathValidation is true => error",
  2216  			deck:        Deck{SkipStoragePathValidation: &boolTrue, AdditionalAllowedBuckets: []string{"hello", "world"}},
  2217  			expectedErr: "skip_storage_path_validation is enabled",
  2218  		},
  2219  	}
  2220  
  2221  	for _, tc := range cases {
  2222  		t.Run(tc.name, func(t *testing.T) {
  2223  			expectingErr := len(tc.expectedErr) > 0
  2224  			err := tc.deck.Validate()
  2225  			if expectingErr && err == nil {
  2226  				t.Fatalf("expecting error (%v), but did not get an error", tc.expectedErr)
  2227  			}
  2228  			if !expectingErr && err != nil {
  2229  				t.Fatalf("not expecting error, but got an error: %v", err)
  2230  			}
  2231  			if expectingErr && err != nil && !strings.Contains(err.Error(), tc.expectedErr) {
  2232  				t.Fatalf("expected error (%v), but got unknown error, instead: %v", tc.expectedErr, err)
  2233  			}
  2234  		})
  2235  	}
  2236  }
  2237  
  2238  func TestValidateRefs(t *testing.T) {
  2239  	cases := []struct {
  2240  		name      string
  2241  		extraRefs []prowapi.Refs
  2242  		expected  error
  2243  	}{
  2244  		{
  2245  			name: "validation error for extra ref specifying the same repo for which the job is configured",
  2246  			extraRefs: []prowapi.Refs{
  2247  				{
  2248  					Org:  "org",
  2249  					Repo: "repo",
  2250  				},
  2251  			},
  2252  			expected: fmt.Errorf("invalid job test on repo org/repo: the following refs specified more than once: %s",
  2253  				"org/repo"),
  2254  		},
  2255  		{
  2256  			name: "validation error lists all duplications",
  2257  			extraRefs: []prowapi.Refs{
  2258  				{
  2259  					Org:  "org",
  2260  					Repo: "repo",
  2261  				},
  2262  				{
  2263  					Org:  "org",
  2264  					Repo: "foo",
  2265  				},
  2266  				{
  2267  					Org:  "org",
  2268  					Repo: "bar",
  2269  				},
  2270  				{
  2271  					Org:  "org",
  2272  					Repo: "foo",
  2273  				},
  2274  			},
  2275  			expected: fmt.Errorf("invalid job test on repo org/repo: the following refs specified more than once: %s",
  2276  				"org/foo,org/repo"),
  2277  		},
  2278  		{
  2279  			name: "no errors if there are no duplications",
  2280  			extraRefs: []prowapi.Refs{
  2281  				{
  2282  					Org:  "org",
  2283  					Repo: "foo",
  2284  				},
  2285  			},
  2286  		},
  2287  	}
  2288  
  2289  	for _, tc := range cases {
  2290  		t.Run(tc.name, func(t *testing.T) {
  2291  			job := JobBase{
  2292  				Name: "test",
  2293  				UtilityConfig: UtilityConfig{
  2294  					ExtraRefs: tc.extraRefs,
  2295  				},
  2296  			}
  2297  			if err := ValidateRefs("org/repo", job); !reflect.DeepEqual(err, tc.expected) {
  2298  				t.Errorf("expected %#v\n!=\nactual %#v", tc.expected, err)
  2299  			}
  2300  		})
  2301  	}
  2302  }
  2303  
  2304  func TestValidateReportingWithGerritLabel(t *testing.T) {
  2305  	cases := []struct {
  2306  		name     string
  2307  		labels   map[string]string
  2308  		reporter Reporter
  2309  		expected error
  2310  	}{
  2311  		{
  2312  			name: "no errors if job is set to report",
  2313  			reporter: Reporter{
  2314  				Context: "context",
  2315  			},
  2316  			labels: map[string]string{
  2317  				kube.GerritReportLabel: "label",
  2318  			},
  2319  		},
  2320  		{
  2321  			name:     "no errors if Gerrit report label is not defined",
  2322  			reporter: Reporter{SkipReport: true},
  2323  			labels: map[string]string{
  2324  				"label": "value",
  2325  			},
  2326  		},
  2327  		{
  2328  			name:     "no errors if job is set to skip report and Gerrit report label is empty",
  2329  			reporter: Reporter{SkipReport: true},
  2330  			labels: map[string]string{
  2331  				kube.GerritReportLabel: "",
  2332  			},
  2333  		},
  2334  		{
  2335  			name:     "error if job is set to skip report and Gerrit report label is set to non-empty",
  2336  			reporter: Reporter{SkipReport: true},
  2337  			labels: map[string]string{
  2338  				kube.GerritReportLabel: "label",
  2339  			},
  2340  			expected: fmt.Errorf("gerrit report label %s set to non-empty string but job is configured to skip reporting.", kube.GerritReportLabel),
  2341  		},
  2342  	}
  2343  
  2344  	for _, tc := range cases {
  2345  		t.Run(tc.name, func(t *testing.T) {
  2346  			cfg := Config{
  2347  				ProwConfig: ProwConfig{
  2348  					PodNamespace: "default-namespace",
  2349  				},
  2350  			}
  2351  			base := JobBase{
  2352  				Name:   "test-job",
  2353  				Labels: tc.labels,
  2354  			}
  2355  			presubmits := []Presubmit{
  2356  				{
  2357  					JobBase:  base,
  2358  					Reporter: tc.reporter,
  2359  				},
  2360  			}
  2361  			var expected error
  2362  			if tc.expected != nil {
  2363  				expected = fmt.Errorf("invalid presubmit job %s: %w", "test-job", tc.expected)
  2364  			}
  2365  			if err := cfg.validatePresubmits(presubmits); !reflect.DeepEqual(err, utilerrors.NewAggregate([]error{expected})) {
  2366  				t.Errorf("did not get expected validation result:\n%v", cmp.Diff(expected, err))
  2367  			}
  2368  
  2369  			postsubmits := []Postsubmit{
  2370  				{
  2371  					JobBase:  base,
  2372  					Reporter: tc.reporter,
  2373  				},
  2374  			}
  2375  			if tc.expected != nil {
  2376  				expected = fmt.Errorf("invalid postsubmit job %s: %w", "test-job", tc.expected)
  2377  			}
  2378  			if err := cfg.validatePostsubmits(postsubmits); !reflect.DeepEqual(err, utilerrors.NewAggregate([]error{expected})) {
  2379  				t.Errorf("did not get expected validation result:\n%v", cmp.Diff(expected, err))
  2380  			}
  2381  		})
  2382  	}
  2383  }
  2384  
  2385  func TestGerritAllRepos(t *testing.T) {
  2386  	tests := []struct {
  2387  		name string
  2388  		in   *GerritOrgRepoConfigs
  2389  		want map[string]map[string]*GerritQueryFilter
  2390  	}{
  2391  		{
  2392  			name: "multiple-org",
  2393  			in: &GerritOrgRepoConfigs{
  2394  				{
  2395  					Org:   "org-1",
  2396  					Repos: []string{"repo-1"},
  2397  				},
  2398  				{
  2399  					Org:   "org-2",
  2400  					Repos: []string{"repo-2"},
  2401  				},
  2402  			},
  2403  			want: map[string]map[string]*GerritQueryFilter{"org-1": {"repo-1": nil}, "org-2": {"repo-2": nil}},
  2404  		},
  2405  		{
  2406  			name: "org-union",
  2407  			in: &GerritOrgRepoConfigs{
  2408  				{
  2409  					Org:   "org-1",
  2410  					Repos: []string{"repo-1"},
  2411  				},
  2412  				{
  2413  					Org:   "org-1",
  2414  					Repos: []string{"repo-2"},
  2415  				},
  2416  			},
  2417  			want: map[string]map[string]*GerritQueryFilter{"org-1": {"repo-1": nil, "repo-2": nil}},
  2418  		},
  2419  		{
  2420  			name: "empty",
  2421  			in:   &GerritOrgRepoConfigs{},
  2422  			want: nil,
  2423  		},
  2424  	}
  2425  
  2426  	for _, tc := range tests {
  2427  		t.Run(tc.name, func(t *testing.T) {
  2428  			got := tc.in.AllRepos()
  2429  			if diff := cmp.Diff(tc.want, got); diff != "" {
  2430  				t.Errorf("output mismatch. got(+), want(-):\n%s", diff)
  2431  			}
  2432  		})
  2433  	}
  2434  }
  2435  
  2436  func TestGerritOptOutHelpRepos(t *testing.T) {
  2437  	tests := []struct {
  2438  		name string
  2439  		in   *GerritOrgRepoConfigs
  2440  		want map[string]sets.Set[string]
  2441  	}{
  2442  		{
  2443  			name: "multiple-org",
  2444  			in: &GerritOrgRepoConfigs{
  2445  				{
  2446  					Org:        "org-1",
  2447  					Repos:      []string{"repo-1"},
  2448  					OptOutHelp: true,
  2449  				},
  2450  				{
  2451  					Org:        "org-2",
  2452  					Repos:      []string{"repo-2"},
  2453  					OptOutHelp: true,
  2454  				},
  2455  			},
  2456  			want: map[string]sets.Set[string]{
  2457  				"org-1": sets.New[string]("repo-1"),
  2458  				"org-2": sets.New[string]("repo-2"),
  2459  			},
  2460  		},
  2461  		{
  2462  			name: "org-union",
  2463  			in: &GerritOrgRepoConfigs{
  2464  				{
  2465  					Org:        "org-1",
  2466  					Repos:      []string{"repo-1"},
  2467  					OptOutHelp: true,
  2468  				},
  2469  				{
  2470  					Org:        "org-1",
  2471  					Repos:      []string{"repo-2"},
  2472  					OptOutHelp: true,
  2473  				},
  2474  			},
  2475  			want: map[string]sets.Set[string]{
  2476  				"org-1": sets.New[string]("repo-1", "repo-2"),
  2477  			},
  2478  		},
  2479  		{
  2480  			name: "skip-non-optout",
  2481  			in: &GerritOrgRepoConfigs{
  2482  				{
  2483  					Org:   "org-1",
  2484  					Repos: []string{"repo-1"},
  2485  				},
  2486  				{
  2487  					Org:        "org-1",
  2488  					Repos:      []string{"repo-2"},
  2489  					OptOutHelp: true,
  2490  				},
  2491  			},
  2492  			want: map[string]sets.Set[string]{
  2493  				"org-1": sets.New[string]("repo-2"),
  2494  			},
  2495  		},
  2496  		{
  2497  			name: "empty",
  2498  			in:   &GerritOrgRepoConfigs{},
  2499  			want: nil,
  2500  		},
  2501  	}
  2502  
  2503  	for _, tc := range tests {
  2504  		t.Run(tc.name, func(t *testing.T) {
  2505  			got := tc.in.OptOutHelpRepos()
  2506  			if diff := cmp.Diff(tc.want, got); diff != "" {
  2507  				t.Errorf("output mismatch. got(+), want(-):\n%s", diff)
  2508  			}
  2509  		})
  2510  	}
  2511  }
  2512  
  2513  // integration test for fake config loading
  2514  func TestValidConfigLoading(t *testing.T) {
  2515  	ptrOrBool := func(p *bool) string {
  2516  		if p != nil {
  2517  			return fmt.Sprintf("%t", *p)
  2518  		}
  2519  
  2520  		return "nil"
  2521  	}
  2522  	var testCases = []struct {
  2523  		name               string
  2524  		prowConfig         string
  2525  		versionFileContent string
  2526  		jobConfigs         []string
  2527  		expectError        bool
  2528  		expectPodNameSpace string
  2529  		expectEnv          map[string][]v1.EnvVar
  2530  		verify             func(*Config) error
  2531  	}{
  2532  		{
  2533  			name:       "one config",
  2534  			prowConfig: ``,
  2535  		},
  2536  		{
  2537  			name:       "reject invalid kubernetes periodic",
  2538  			prowConfig: ``,
  2539  			jobConfigs: []string{
  2540  				`
  2541  periodics:
  2542  - interval: 10m
  2543    agent: kubernetes
  2544    build_spec:
  2545    name: foo`,
  2546  			},
  2547  			expectError: true,
  2548  		},
  2549  		{
  2550  			name:       "one periodic",
  2551  			prowConfig: ``,
  2552  			jobConfigs: []string{
  2553  				`
  2554  periodics:
  2555  - interval: 10m
  2556    agent: kubernetes
  2557    name: foo
  2558    spec:
  2559      containers:
  2560      - image: alpine`,
  2561  			},
  2562  		},
  2563  		{
  2564  			name:       "one periodic no agent, should default",
  2565  			prowConfig: ``,
  2566  			jobConfigs: []string{
  2567  				`
  2568  periodics:
  2569  - interval: 10m
  2570    name: foo
  2571    spec:
  2572      containers:
  2573      - image: alpine`,
  2574  			},
  2575  		},
  2576  		{
  2577  			name:       "two periodics",
  2578  			prowConfig: ``,
  2579  			jobConfigs: []string{
  2580  				`
  2581  periodics:
  2582  - interval: 10m
  2583    agent: kubernetes
  2584    name: foo
  2585    spec:
  2586      containers:
  2587      - image: alpine`,
  2588  				`
  2589  periodics:
  2590  - interval: 10m
  2591    agent: kubernetes
  2592    name: bar
  2593    spec:
  2594      containers:
  2595      - image: alpine`,
  2596  			},
  2597  		},
  2598  		{
  2599  			name:       "duplicated periodics",
  2600  			prowConfig: ``,
  2601  			jobConfigs: []string{
  2602  				`
  2603  periodics:
  2604  - interval: 10m
  2605    agent: kubernetes
  2606    name: foo
  2607    spec:
  2608      containers:
  2609      - image: alpine`,
  2610  				`
  2611  periodics:
  2612  - interval: 10m
  2613    agent: kubernetes
  2614    name: foo
  2615    spec:
  2616      containers:
  2617      - image: alpine`,
  2618  			},
  2619  			expectError: true,
  2620  		},
  2621  		{
  2622  			name:       "one presubmit no context should default",
  2623  			prowConfig: ``,
  2624  			jobConfigs: []string{
  2625  				`
  2626  presubmits:
  2627    foo/bar:
  2628    - agent: kubernetes
  2629      name: presubmit-bar
  2630      spec:
  2631        containers:
  2632        - image: alpine`,
  2633  			},
  2634  		},
  2635  		{
  2636  			name:       "one presubmit no agent should default",
  2637  			prowConfig: ``,
  2638  			jobConfigs: []string{
  2639  				`
  2640  presubmits:
  2641    foo/bar:
  2642    - context: bar
  2643      name: presubmit-bar
  2644      spec:
  2645        containers:
  2646        - image: alpine`,
  2647  			},
  2648  		},
  2649  		{
  2650  			name:       "one presubmit, ok",
  2651  			prowConfig: ``,
  2652  			jobConfigs: []string{
  2653  				`
  2654  presubmits:
  2655    foo/bar:
  2656    - agent: kubernetes
  2657      name: presubmit-bar
  2658      context: bar
  2659      spec:
  2660        containers:
  2661        - image: alpine`,
  2662  			},
  2663  		},
  2664  		{
  2665  			name:       "two presubmits",
  2666  			prowConfig: ``,
  2667  			jobConfigs: []string{
  2668  				`
  2669  presubmits:
  2670    foo/bar:
  2671    - agent: kubernetes
  2672      name: presubmit-bar
  2673      context: bar
  2674      spec:
  2675        containers:
  2676        - image: alpine`,
  2677  				`
  2678  presubmits:
  2679    foo/baz:
  2680    - agent: kubernetes
  2681      name: presubmit-baz
  2682      context: baz
  2683      spec:
  2684        containers:
  2685        - image: alpine`,
  2686  			},
  2687  		},
  2688  		{
  2689  			name:       "dup presubmits, one file",
  2690  			prowConfig: ``,
  2691  			jobConfigs: []string{
  2692  				`
  2693  presubmits:
  2694    foo/bar:
  2695    - agent: kubernetes
  2696      name: presubmit-bar
  2697      context: bar
  2698      spec:
  2699        containers:
  2700        - image: alpine
  2701    - agent: kubernetes
  2702      name: presubmit-bar
  2703      context: bar
  2704      spec:
  2705        containers:
  2706        - image: alpine`,
  2707  			},
  2708  			expectError: true,
  2709  		},
  2710  		{
  2711  			name:       "dup presubmits, two files",
  2712  			prowConfig: ``,
  2713  			jobConfigs: []string{
  2714  				`
  2715  presubmits:
  2716    foo/bar:
  2717    - agent: kubernetes
  2718      name: presubmit-bar
  2719      context: bar
  2720      spec:
  2721        containers:
  2722        - image: alpine`,
  2723  				`
  2724  presubmits:
  2725    foo/bar:
  2726    - agent: kubernetes
  2727      context: bar
  2728      name: presubmit-bar
  2729      spec:
  2730        containers:
  2731        - image: alpine`,
  2732  			},
  2733  			expectError: true,
  2734  		},
  2735  		{
  2736  			name:       "dup presubmits not the same branch, two files",
  2737  			prowConfig: ``,
  2738  			jobConfigs: []string{
  2739  				`
  2740  presubmits:
  2741    foo/bar:
  2742    - agent: kubernetes
  2743      name: presubmit-bar
  2744      context: bar
  2745      branches:
  2746      - master
  2747      spec:
  2748        containers:
  2749        - image: alpine`,
  2750  				`
  2751  presubmits:
  2752    foo/bar:
  2753    - agent: kubernetes
  2754      context: bar
  2755      branches:
  2756      - other
  2757      name: presubmit-bar
  2758      spec:
  2759        containers:
  2760        - image: alpine`,
  2761  			},
  2762  			expectError: false,
  2763  		},
  2764  		{
  2765  			name: "dup presubmits main file",
  2766  			prowConfig: `
  2767  presubmits:
  2768    foo/bar:
  2769    - agent: kubernetes
  2770      name: presubmit-bar
  2771      context: bar
  2772      spec:
  2773        containers:
  2774        - image: alpine
  2775    - agent: kubernetes
  2776      context: bar
  2777      name: presubmit-bar
  2778      spec:
  2779        containers:
  2780        - image: alpine`,
  2781  			expectError: true,
  2782  		},
  2783  		{
  2784  			name: "dup presubmits main file not on the same branch",
  2785  			prowConfig: `
  2786  presubmits:
  2787    foo/bar:
  2788    - agent: kubernetes
  2789      name: presubmit-bar
  2790      context: bar
  2791      branches:
  2792      - other
  2793      spec:
  2794        containers:
  2795        - image: alpine
  2796    - agent: kubernetes
  2797      context: bar
  2798      branches:
  2799      - master
  2800      name: presubmit-bar
  2801      spec:
  2802        containers:
  2803        - image: alpine`,
  2804  			expectError: false,
  2805  		},
  2806  		{
  2807  			name:       "one postsubmit, ok",
  2808  			prowConfig: ``,
  2809  			jobConfigs: []string{
  2810  				`
  2811  postsubmits:
  2812    foo/bar:
  2813    - agent: kubernetes
  2814      name: postsubmit-bar
  2815      spec:
  2816        containers:
  2817        - image: alpine`,
  2818  			},
  2819  		},
  2820  		{
  2821  			name:       "one postsubmit no agent, should default",
  2822  			prowConfig: ``,
  2823  			jobConfigs: []string{
  2824  				`
  2825  postsubmits:
  2826    foo/bar:
  2827    - name: postsubmit-bar
  2828      spec:
  2829        containers:
  2830        - image: alpine`,
  2831  			},
  2832  		},
  2833  		{
  2834  			name:       "two postsubmits",
  2835  			prowConfig: ``,
  2836  			jobConfigs: []string{
  2837  				`
  2838  postsubmits:
  2839    foo/bar:
  2840    - agent: kubernetes
  2841      name: postsubmit-bar
  2842      spec:
  2843        containers:
  2844        - image: alpine`,
  2845  				`
  2846  postsubmits:
  2847    foo/baz:
  2848    - agent: kubernetes
  2849      name: postsubmit-baz
  2850      spec:
  2851        containers:
  2852        - image: alpine`,
  2853  			},
  2854  		},
  2855  		{
  2856  			name:       "dup postsubmits, one file",
  2857  			prowConfig: ``,
  2858  			jobConfigs: []string{
  2859  				`
  2860  postsubmits:
  2861    foo/bar:
  2862    - agent: kubernetes
  2863      name: postsubmit-bar
  2864      spec:
  2865        containers:
  2866        - image: alpine
  2867    - agent: kubernetes
  2868      name: postsubmit-bar
  2869      spec:
  2870        containers:
  2871        - image: alpine`,
  2872  			},
  2873  			expectError: true,
  2874  		},
  2875  		{
  2876  			name:       "dup postsubmits, two files",
  2877  			prowConfig: ``,
  2878  			jobConfigs: []string{
  2879  				`
  2880  postsubmits:
  2881    foo/bar:
  2882    - agent: kubernetes
  2883      name: postsubmit-bar
  2884      spec:
  2885        containers:
  2886        - image: alpine`,
  2887  				`
  2888  postsubmits:
  2889    foo/bar:
  2890    - agent: kubernetes
  2891      name: postsubmit-bar
  2892      spec:
  2893        containers:
  2894        - image: alpine`,
  2895  			},
  2896  			expectError: true,
  2897  		},
  2898  		{
  2899  			name: "test valid presets in main config",
  2900  			prowConfig: `
  2901  presets:
  2902  - labels:
  2903      preset-baz: "true"
  2904    env:
  2905    - name: baz
  2906      value: fejtaverse`,
  2907  			jobConfigs: []string{
  2908  				`periodics:
  2909  - interval: 10m
  2910    agent: kubernetes
  2911    name: foo
  2912    labels:
  2913      preset-baz: "true"
  2914    spec:
  2915      containers:
  2916      - image: alpine`,
  2917  				`
  2918  periodics:
  2919  - interval: 10m
  2920    agent: kubernetes
  2921    name: bar
  2922    labels:
  2923      preset-baz: "true"
  2924    spec:
  2925      containers:
  2926      - image: alpine`,
  2927  			},
  2928  			expectEnv: map[string][]v1.EnvVar{
  2929  				"foo": {
  2930  					{
  2931  						Name:  "baz",
  2932  						Value: "fejtaverse",
  2933  					},
  2934  				},
  2935  				"bar": {
  2936  					{
  2937  						Name:  "baz",
  2938  						Value: "fejtaverse",
  2939  					},
  2940  				},
  2941  			},
  2942  		},
  2943  		{
  2944  			name:       "test valid presets in job configs",
  2945  			prowConfig: ``,
  2946  			jobConfigs: []string{
  2947  				`
  2948  presets:
  2949  - labels:
  2950      preset-baz: "true"
  2951    env:
  2952    - name: baz
  2953      value: fejtaverse
  2954  periodics:
  2955  - interval: 10m
  2956    agent: kubernetes
  2957    name: foo
  2958    labels:
  2959      preset-baz: "true"
  2960    spec:
  2961      containers:
  2962      - image: alpine`,
  2963  				`
  2964  periodics:
  2965  - interval: 10m
  2966    agent: kubernetes
  2967    name: bar
  2968    labels:
  2969      preset-baz: "true"
  2970    spec:
  2971      containers:
  2972      - image: alpine`,
  2973  			},
  2974  			expectEnv: map[string][]v1.EnvVar{
  2975  				"foo": {
  2976  					{
  2977  						Name:  "baz",
  2978  						Value: "fejtaverse",
  2979  					},
  2980  				},
  2981  				"bar": {
  2982  					{
  2983  						Name:  "baz",
  2984  						Value: "fejtaverse",
  2985  					},
  2986  				},
  2987  			},
  2988  		},
  2989  		{
  2990  			name: "test valid presets in both main & job configs",
  2991  			prowConfig: `
  2992  presets:
  2993  - labels:
  2994      preset-baz: "true"
  2995    env:
  2996    - name: baz
  2997      value: fejtaverse`,
  2998  			jobConfigs: []string{
  2999  				`
  3000  presets:
  3001  - labels:
  3002      preset-k8s: "true"
  3003    env:
  3004    - name: k8s
  3005      value: kubernetes
  3006  periodics:
  3007  - interval: 10m
  3008    agent: kubernetes
  3009    name: foo
  3010    labels:
  3011      preset-baz: "true"
  3012      preset-k8s: "true"
  3013    spec:
  3014      containers:
  3015      - image: alpine`,
  3016  				`
  3017  periodics:
  3018  - interval: 10m
  3019    agent: kubernetes
  3020    name: bar
  3021    labels:
  3022      preset-baz: "true"
  3023    spec:
  3024      containers:
  3025      - image: alpine`,
  3026  			},
  3027  			expectEnv: map[string][]v1.EnvVar{
  3028  				"foo": {
  3029  					{
  3030  						Name:  "baz",
  3031  						Value: "fejtaverse",
  3032  					},
  3033  					{
  3034  						Name:  "k8s",
  3035  						Value: "kubernetes",
  3036  					},
  3037  				},
  3038  				"bar": {
  3039  					{
  3040  						Name:  "baz",
  3041  						Value: "fejtaverse",
  3042  					},
  3043  				},
  3044  			},
  3045  		},
  3046  		{
  3047  			name:       "decorated periodic missing `command`",
  3048  			prowConfig: ``,
  3049  			jobConfigs: []string{
  3050  				`
  3051  periodics:
  3052  - interval: 10m
  3053    agent: kubernetes
  3054    name: foo
  3055    decorate: true
  3056    spec:
  3057      containers:
  3058      - image: alpine`,
  3059  			},
  3060  			expectError: true,
  3061  		},
  3062  		{
  3063  			name: "all repos contains repos from tide, presubmits and postsubmits",
  3064  			prowConfig: `
  3065  tide:
  3066    queries:
  3067    - repos:
  3068      - stranded/fish`,
  3069  			jobConfigs: []string{`
  3070  presubmits:
  3071    k/k:
  3072    - name: my-job
  3073      spec:
  3074        containers:
  3075        - name: lost-vessel
  3076          image: vessel:latest
  3077          command: ["ride"]`,
  3078  				`
  3079  postsubmits:
  3080    k/test-infra:
  3081    - name: my-job
  3082      spec:
  3083        containers:
  3084        - name: lost-vessel
  3085          image: vessel:latest
  3086          command: ["ride"]`,
  3087  			},
  3088  			verify: func(c *Config) error {
  3089  				if diff := c.AllRepos.Difference(sets.New[string]("k/k", "k/test-infra", "stranded/fish")); len(diff) != 0 {
  3090  					return fmt.Errorf("expected no diff, got %q", diff)
  3091  				}
  3092  				return nil
  3093  			},
  3094  		},
  3095  		{
  3096  			name: "no jobs doesn't make AllRepos a nilpointer",
  3097  			verify: func(c *Config) error {
  3098  				if c.AllRepos == nil {
  3099  					return errors.New("config.AllRepos is nil")
  3100  				}
  3101  				return nil
  3102  			},
  3103  		},
  3104  		{
  3105  			name: "ProwYAMLGetterWithDefaults gets set",
  3106  			verify: func(c *Config) error {
  3107  				if c.ProwYAMLGetterWithDefaults == nil {
  3108  					return errors.New("config.ProwYAMLGetterWithDefaults is nil")
  3109  				}
  3110  				return nil
  3111  			},
  3112  		},
  3113  		{
  3114  			name: "InRepoConfigAllowedClusters gets defaulted if unset",
  3115  			verify: func(c *Config) error {
  3116  				if len(c.InRepoConfig.AllowedClusters) != 1 ||
  3117  					len(c.InRepoConfig.AllowedClusters["*"]) != 1 ||
  3118  					c.InRepoConfig.AllowedClusters["*"][0] != kube.DefaultClusterAlias {
  3119  					return fmt.Errorf("expected c.InRepoConfig.AllowedClusters to contain exactly one global entry to allow the buildcluster, was %v", c.InRepoConfig.AllowedClusters)
  3120  				}
  3121  				return nil
  3122  			},
  3123  		},
  3124  		{
  3125  			name: "InRepoConfigAllowedClusters gets defaulted if no global setting",
  3126  			prowConfig: `
  3127  in_repo_config:
  3128    allowed_clusters:
  3129      foo/bar: ["my-cluster"]
  3130  `,
  3131  			verify: func(c *Config) error {
  3132  				if len(c.InRepoConfig.AllowedClusters) != 2 ||
  3133  					len(c.InRepoConfig.AllowedClusters["*"]) != 1 ||
  3134  					c.InRepoConfig.AllowedClusters["*"][0] != kube.DefaultClusterAlias {
  3135  					return fmt.Errorf("expected c.InRepoConfig.AllowedClusters to contain exactly one global entry to allow the buildcluster, was %v", c.InRepoConfig.AllowedClusters)
  3136  				}
  3137  				return nil
  3138  			},
  3139  		},
  3140  		{
  3141  			name: "InRepoConfigAllowedClusters respects explicit empty default",
  3142  			prowConfig: `
  3143  in_repo_config:
  3144    allowed_clusters:
  3145      "*": []
  3146  `,
  3147  			verify: func(c *Config) error {
  3148  				if len(c.InRepoConfig.AllowedClusters) != 1 ||
  3149  					len(c.InRepoConfig.AllowedClusters["*"]) != 0 {
  3150  					return fmt.Errorf("expected c.InRepoConfig.AllowedClusters to contain no global entry, was %v", c.InRepoConfig.AllowedClusters)
  3151  				}
  3152  				return nil
  3153  			},
  3154  		},
  3155  		{
  3156  			name: "InRepoConfigAllowedClusters doesn't get overwritten",
  3157  			prowConfig: `
  3158  in_repo_config:
  3159    allowed_clusters:
  3160      foo/bar: ["my-cluster"]
  3161  `,
  3162  			verify: func(c *Config) error {
  3163  				if len(c.InRepoConfig.AllowedClusters) != 2 ||
  3164  					len(c.InRepoConfig.AllowedClusters["foo/bar"]) != 1 ||
  3165  					c.InRepoConfig.AllowedClusters["foo/bar"][0] != "my-cluster" {
  3166  					return fmt.Errorf("expected c.InRepoConfig.AllowedClusters to contain exactly one entry for foo/bar, was %v", c.InRepoConfig.AllowedClusters)
  3167  				}
  3168  				return nil
  3169  			},
  3170  		},
  3171  		{
  3172  			name: "PubSubSubscriptions set but not PubSubTriggers",
  3173  			prowConfig: `
  3174  pubsub_subscriptions:
  3175    projA:
  3176    - topicB
  3177    - topicC
  3178  `,
  3179  			verify: func(c *Config) error {
  3180  				if diff := cmp.Diff(c.PubSubTriggers, PubSubTriggers([]PubSubTrigger{
  3181  					{
  3182  						Project:                "projA",
  3183  						Topics:                 []string{"topicB", "topicC"},
  3184  						AllowedClusters:        []string{"*"},
  3185  						MaxOutstandingMessages: 10,
  3186  					},
  3187  				})); diff != "" {
  3188  					return fmt.Errorf("want(-), got(+): \n%s", diff)
  3189  				}
  3190  				return nil
  3191  			},
  3192  		},
  3193  		{
  3194  			name:               "Version file sets the version",
  3195  			versionFileContent: "some-git-sha",
  3196  			verify: func(c *Config) error {
  3197  				if c.ConfigVersionSHA != "some-git-sha" {
  3198  					return fmt.Errorf("expected value of ConfigVersionSH field to be 'some-git-sha', was %q", c.ConfigVersionSHA)
  3199  				}
  3200  				return nil
  3201  			},
  3202  		},
  3203  		{
  3204  			name: "tide global target_url respected",
  3205  			prowConfig: `
  3206  tide:
  3207    target_url: https://global.tide.com
  3208  `,
  3209  			verify: func(c *Config) error {
  3210  				orgRepo := OrgRepo{Org: "org", Repo: "repo"}
  3211  				if got, expected := c.Tide.GetTargetURL(orgRepo), "https://global.tide.com"; got != expected {
  3212  					return fmt.Errorf("expected target URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3213  				}
  3214  				return nil
  3215  			},
  3216  		},
  3217  		{
  3218  			name: "tide target_url and target_urls conflict",
  3219  			prowConfig: `
  3220  tide:
  3221    target_url: https://global.tide.com
  3222    target_urls:
  3223      "org": https://org.tide.com
  3224  `,
  3225  			expectError: true,
  3226  		},
  3227  		{
  3228  			name: "tide specific target_urls respected",
  3229  			prowConfig: `
  3230  tide:
  3231    target_urls:
  3232      "*": https://star.tide.com
  3233      "org": https://org.tide.com
  3234      "org/repo": https://repo.tide.com
  3235  `,
  3236  			verify: func(c *Config) error {
  3237  				orgRepo := OrgRepo{Org: "other-org", Repo: "other-repo"}
  3238  				if got, expected := c.Tide.GetTargetURL(orgRepo), "https://star.tide.com"; got != expected {
  3239  					return fmt.Errorf("expected target URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3240  				}
  3241  				orgRepo = OrgRepo{Org: "org", Repo: "other-repo"}
  3242  				if got, expected := c.Tide.GetTargetURL(orgRepo), "https://org.tide.com"; got != expected {
  3243  					return fmt.Errorf("expected target URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3244  				}
  3245  				orgRepo = OrgRepo{Org: "org", Repo: "repo"}
  3246  				if got, expected := c.Tide.GetTargetURL(orgRepo), "https://repo.tide.com"; got != expected {
  3247  					return fmt.Errorf("expected target URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3248  				}
  3249  				return nil
  3250  			},
  3251  		},
  3252  		{
  3253  			name: "tide no target_url specified returns empty string",
  3254  			verify: func(c *Config) error {
  3255  				orgRepo := OrgRepo{Org: "org", Repo: "repo"}
  3256  				if got, expected := c.Tide.GetTargetURL(orgRepo), ""; got != expected {
  3257  					return fmt.Errorf("expected target URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3258  				}
  3259  				return nil
  3260  			},
  3261  		},
  3262  		{
  3263  			name: "tide specific pr_status_base_urls respected",
  3264  			prowConfig: `
  3265  tide:
  3266    pr_status_base_urls:
  3267      "*": https://star.tide.com/pr
  3268      "org": https://org.tide.com/pr
  3269      "org/repo": https://repo.tide.com/pr
  3270  `,
  3271  			verify: func(c *Config) error {
  3272  				orgRepo := OrgRepo{Org: "other-org", Repo: "other-repo"}
  3273  				if got, expected := c.Tide.GetPRStatusBaseURL(orgRepo), "https://star.tide.com/pr"; got != expected {
  3274  					return fmt.Errorf("expected PR Status Base URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3275  				}
  3276  				orgRepo = OrgRepo{Org: "org", Repo: "other-repo"}
  3277  				if got, expected := c.Tide.GetPRStatusBaseURL(orgRepo), "https://org.tide.com/pr"; got != expected {
  3278  					return fmt.Errorf("expected PR Status Base URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3279  				}
  3280  				orgRepo = OrgRepo{Org: "org", Repo: "repo"}
  3281  				if got, expected := c.Tide.GetPRStatusBaseURL(orgRepo), "https://repo.tide.com/pr"; got != expected {
  3282  					return fmt.Errorf("expected PR Status Base URL for %q to be %q, but got %q", orgRepo.String(), expected, got)
  3283  				}
  3284  				return nil
  3285  			},
  3286  		},
  3287  		{
  3288  			name: "postsubmit without explicit 'always_run' sets this field to nil by default",
  3289  			jobConfigs: []string{
  3290  				`
  3291  postsubmits:
  3292    k/test-infra:
  3293    - name: my-job
  3294      spec:
  3295        containers:
  3296        - name: lost-vessel
  3297          image: vessel:latest
  3298          command: ["ride"]`,
  3299  			},
  3300  			verify: func(c *Config) error {
  3301  				jobs := c.PostsubmitsStatic["k/test-infra"]
  3302  				if jobs[0].AlwaysRun != nil {
  3303  					return fmt.Errorf("expected job to have 'always_run' set to nil, but got %q", ptrOrBool(jobs[0].AlwaysRun))
  3304  				}
  3305  				return nil
  3306  			},
  3307  		},
  3308  		{
  3309  			name: "postsubmit with explicit 'always_run: true' sets this field to true",
  3310  			jobConfigs: []string{
  3311  				`
  3312  postsubmits:
  3313    k/test-infra:
  3314    - name: my-job
  3315      always_run: true
  3316      spec:
  3317        containers:
  3318        - name: lost-vessel
  3319          image: vessel:latest
  3320          command: ["ride"]`,
  3321  			},
  3322  			verify: func(c *Config) error {
  3323  				jobs := c.PostsubmitsStatic["k/test-infra"]
  3324  				if jobs[0].AlwaysRun == nil || *jobs[0].AlwaysRun == false {
  3325  					return fmt.Errorf("expected job to have 'always_run' set to true, but got %q", ptrOrBool(jobs[0].AlwaysRun))
  3326  				}
  3327  				return nil
  3328  			},
  3329  		},
  3330  		{
  3331  			name: "postsubmit with explicit 'always_run: false' sets this field to false",
  3332  			jobConfigs: []string{
  3333  				`
  3334  postsubmits:
  3335    k/test-infra:
  3336    - name: my-job
  3337      always_run: false
  3338      spec:
  3339        containers:
  3340        - name: lost-vessel
  3341          image: vessel:latest
  3342          command: ["ride"]`,
  3343  			},
  3344  			verify: func(c *Config) error {
  3345  				jobs := c.PostsubmitsStatic["k/test-infra"]
  3346  				if jobs[0].AlwaysRun == nil || *jobs[0].AlwaysRun == true {
  3347  					return fmt.Errorf("expected job to have 'always_run' set to false, but got %q", ptrOrBool(jobs[0].AlwaysRun))
  3348  				}
  3349  				return nil
  3350  			},
  3351  		},
  3352  		{
  3353  			name: "postsubmit with explicit 'always_run: true' and 'run_if_changed' set, err",
  3354  			jobConfigs: []string{
  3355  				`
  3356  postsubmits:
  3357    k/test-infra:
  3358    - name: my-job
  3359      always_run: true
  3360      run_if_changed: "foo"
  3361      spec:
  3362        containers:
  3363        - name: lost-vessel
  3364          image: vessel:latest
  3365          command: ["ride"]`,
  3366  			},
  3367  			expectError: true,
  3368  		},
  3369  		{
  3370  			name: "postsubmit with explicit 'always_run: true' and 'skip_if_only_changed' set, err",
  3371  			jobConfigs: []string{
  3372  				`
  3373  postsubmits:
  3374    k/test-infra:
  3375    - name: my-job
  3376      always_run: true
  3377  	skip_if_only_changed: "foo"
  3378      spec:
  3379        containers:
  3380        - name: lost-vessel
  3381          image: vessel:latest
  3382          command: ["ride"]`,
  3383  			},
  3384  			expectError: true,
  3385  		},
  3386  		{
  3387  			name: "postsubmit with explicit 'always_run: false' and 'run_if_changed' set, OK",
  3388  			jobConfigs: []string{
  3389  				`
  3390  postsubmits:
  3391    k/test-infra:
  3392    - name: my-job
  3393      always_run: false
  3394      run_if_changed: "foo"
  3395      spec:
  3396        containers:
  3397        - name: lost-vessel
  3398          image: vessel:latest
  3399          command: ["ride"]`,
  3400  			},
  3401  		},
  3402  		{
  3403  			name: "postsubmit with explicit 'always_run: false' and 'skip_if_only_changed' set, OK",
  3404  			jobConfigs: []string{
  3405  				`
  3406  postsubmits:
  3407    k/test-infra:
  3408    - name: my-job
  3409      always_run: false
  3410      skip_if_only_changed: "foo"
  3411      spec:
  3412        containers:
  3413        - name: lost-vessel
  3414          image: vessel:latest
  3415          command: ["ride"]`,
  3416  			},
  3417  		},
  3418  		{
  3419  			name: "postsubmit with 'run_if_changed' set, then 'always_run' is 'nil'",
  3420  			jobConfigs: []string{
  3421  				`
  3422  postsubmits:
  3423    k/test-infra:
  3424    - name: my-job
  3425      run_if_changed: "foo"
  3426      spec:
  3427        containers:
  3428        - name: lost-vessel
  3429          image: vessel:latest
  3430          command: ["ride"]`,
  3431  			},
  3432  			verify: func(c *Config) error {
  3433  				jobs := c.PostsubmitsStatic["k/test-infra"]
  3434  				if jobs[0].AlwaysRun != nil {
  3435  					return fmt.Errorf("expected job to have 'always_run' set to nil, but got %q", ptrOrBool(jobs[0].AlwaysRun))
  3436  				}
  3437  				return nil
  3438  			},
  3439  		},
  3440  		{
  3441  			name: "postsubmit with 'skip_if_only_changed' set, then 'always_run' is 'nil'",
  3442  			jobConfigs: []string{
  3443  				`
  3444  postsubmits:
  3445    k/test-infra:
  3446    - name: my-job
  3447      skip_if_only_changed: "foo"
  3448      spec:
  3449        containers:
  3450        - name: lost-vessel
  3451          image: vessel:latest
  3452          command: ["ride"]`,
  3453  			},
  3454  			verify: func(c *Config) error {
  3455  				jobs := c.PostsubmitsStatic["k/test-infra"]
  3456  				if jobs[0].AlwaysRun != nil {
  3457  					return fmt.Errorf("expected job to have 'always_run' set to nil, but got %q", ptrOrBool(jobs[0].AlwaysRun))
  3458  				}
  3459  				return nil
  3460  			},
  3461  		},
  3462  	}
  3463  
  3464  	for _, tc := range testCases {
  3465  		t.Run(tc.name, func(t *testing.T) {
  3466  
  3467  			// save the config
  3468  			prowConfigDir := t.TempDir()
  3469  
  3470  			prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  3471  			if err := os.WriteFile(prowConfig, []byte(tc.prowConfig), 0666); err != nil {
  3472  				t.Fatalf("fail to write prow config: %v", err)
  3473  			}
  3474  
  3475  			if tc.versionFileContent != "" {
  3476  				versionFile := filepath.Join(prowConfigDir, "VERSION")
  3477  				if err := os.WriteFile(versionFile, []byte(tc.versionFileContent), 0600); err != nil {
  3478  					t.Fatalf("failed to write prow version file: %v", err)
  3479  				}
  3480  			}
  3481  
  3482  			jobConfig := ""
  3483  			if len(tc.jobConfigs) > 0 {
  3484  				jobConfigDir := t.TempDir()
  3485  
  3486  				// cover both job config as a file & a dir
  3487  				if len(tc.jobConfigs) == 1 {
  3488  					// a single file
  3489  					jobConfig = filepath.Join(jobConfigDir, "config.yaml")
  3490  					if err := os.WriteFile(jobConfig, []byte(tc.jobConfigs[0]), 0666); err != nil {
  3491  						t.Fatalf("fail to write job config: %v", err)
  3492  					}
  3493  				} else {
  3494  					// a dir
  3495  					jobConfig = jobConfigDir
  3496  					for idx, config := range tc.jobConfigs {
  3497  						subConfig := filepath.Join(jobConfigDir, fmt.Sprintf("config_%d.yaml", idx))
  3498  						if err := os.WriteFile(subConfig, []byte(config), 0666); err != nil {
  3499  							t.Fatalf("fail to write job config: %v", err)
  3500  						}
  3501  					}
  3502  				}
  3503  			}
  3504  
  3505  			cfg, err := Load(prowConfig, jobConfig, nil, "")
  3506  			if tc.expectError && err == nil {
  3507  				t.Errorf("tc %s: Expect error, but got nil", tc.name)
  3508  			} else if !tc.expectError && err != nil {
  3509  				t.Errorf("tc %s: Expect no error, but got error %v", tc.name, err)
  3510  			}
  3511  
  3512  			if err == nil {
  3513  				if tc.expectPodNameSpace == "" {
  3514  					tc.expectPodNameSpace = "default"
  3515  				}
  3516  
  3517  				if cfg.PodNamespace != tc.expectPodNameSpace {
  3518  					t.Errorf("tc %s: Expect PodNamespace %s, but got %v", tc.name, tc.expectPodNameSpace, cfg.PodNamespace)
  3519  				}
  3520  
  3521  				if len(tc.expectEnv) > 0 {
  3522  					for _, j := range cfg.AllStaticPresubmits(nil) {
  3523  						if envs, ok := tc.expectEnv[j.Name]; ok {
  3524  							if !reflect.DeepEqual(envs, j.Spec.Containers[0].Env) {
  3525  								t.Errorf("tc %s: expect env %v for job %s, got %+v", tc.name, envs, j.Name, j.Spec.Containers[0].Env)
  3526  							}
  3527  						}
  3528  					}
  3529  
  3530  					for _, j := range cfg.AllStaticPostsubmits(nil) {
  3531  						if envs, ok := tc.expectEnv[j.Name]; ok {
  3532  							if !reflect.DeepEqual(envs, j.Spec.Containers[0].Env) {
  3533  								t.Errorf("tc %s: expect env %v for job %s, got %+v", tc.name, envs, j.Name, j.Spec.Containers[0].Env)
  3534  							}
  3535  						}
  3536  					}
  3537  
  3538  					for _, j := range cfg.AllPeriodics() {
  3539  						if envs, ok := tc.expectEnv[j.Name]; ok {
  3540  							if !reflect.DeepEqual(envs, j.Spec.Containers[0].Env) {
  3541  								t.Errorf("tc %s: expect env %v for job %s, got %+v", tc.name, envs, j.Name, j.Spec.Containers[0].Env)
  3542  							}
  3543  						}
  3544  					}
  3545  				}
  3546  			}
  3547  
  3548  			if tc.verify != nil {
  3549  				if err := tc.verify(cfg); err != nil {
  3550  					t.Fatalf("verify failed:  %v", err)
  3551  				}
  3552  			}
  3553  		})
  3554  	}
  3555  }
  3556  
  3557  func TestReadJobConfigProwIgnore(t *testing.T) {
  3558  	expectExactly := func(expected ...string) func(c *JobConfig) error {
  3559  		return func(c *JobConfig) error {
  3560  			expected := sets.New[string](expected...)
  3561  			actual := sets.New[string]()
  3562  			for _, pres := range c.PresubmitsStatic {
  3563  				for _, pre := range pres {
  3564  					actual.Insert(pre.Name)
  3565  				}
  3566  			}
  3567  			if diff := expected.Difference(actual); diff.Len() > 0 {
  3568  				return fmt.Errorf("missing expected job(s): %q", sets.List(diff))
  3569  			}
  3570  			if diff := actual.Difference(expected); diff.Len() > 0 {
  3571  				return fmt.Errorf("found unexpected job(s): %q", sets.List(diff))
  3572  			}
  3573  			return nil
  3574  		}
  3575  	}
  3576  	commonFiles := map[string]string{
  3577  		"foo_jobs.yaml": `presubmits:
  3578    org/foo:
  3579    - name: foo_1
  3580      spec:
  3581        containers:
  3582        - image: my-image:latest
  3583          command: ["do-the-thing"]`,
  3584  		"bar_jobs.yaml": `presubmits:
  3585    org/bar:
  3586    - name: bar_1
  3587      spec:
  3588        containers:
  3589        - image: my-image:latest
  3590          command: ["do-the-thing"]`,
  3591  		"subdir/baz_jobs.yaml": `presubmits:
  3592    org/baz:
  3593    - name: baz_1
  3594      spec:
  3595        containers:
  3596        - image: my-image:latest
  3597          command: ["do-the-thing"]`,
  3598  		"extraneous.md": `I am unrelated.`,
  3599  	}
  3600  
  3601  	var testCases = []struct {
  3602  		name   string
  3603  		files  map[string]string
  3604  		verify func(*JobConfig) error
  3605  	}{
  3606  		{
  3607  			name:   "no ignore files",
  3608  			verify: expectExactly("foo_1", "bar_1", "baz_1"),
  3609  		},
  3610  		{
  3611  			name: "ignore file present, all ignored",
  3612  			files: map[string]string{
  3613  				ProwIgnoreFileName: `*.yaml`,
  3614  			},
  3615  			verify: expectExactly(),
  3616  		},
  3617  		{
  3618  			name: "ignore file present, no match",
  3619  			files: map[string]string{
  3620  				ProwIgnoreFileName: `*_ignored.yaml`,
  3621  			},
  3622  			verify: expectExactly("foo_1", "bar_1", "baz_1"),
  3623  		},
  3624  		{
  3625  			name: "ignore file present, matches bar file",
  3626  			files: map[string]string{
  3627  				ProwIgnoreFileName: `bar_*.yaml`,
  3628  			},
  3629  			verify: expectExactly("foo_1", "baz_1"),
  3630  		},
  3631  		{
  3632  			name: "ignore file present, matches subdir",
  3633  			files: map[string]string{
  3634  				ProwIgnoreFileName: `subdir/`,
  3635  			},
  3636  			verify: expectExactly("foo_1", "bar_1"),
  3637  		},
  3638  		{
  3639  			name: "ignore file present, matches bar and subdir",
  3640  			files: map[string]string{
  3641  				ProwIgnoreFileName: `subdir/
  3642  bar_jobs.yaml`,
  3643  			},
  3644  			verify: expectExactly("foo_1"),
  3645  		},
  3646  		{
  3647  			name: "ignore file in subdir, matches only subdir files",
  3648  			files: map[string]string{
  3649  				"subdir/" + ProwIgnoreFileName: `*.yaml`,
  3650  			},
  3651  			verify: expectExactly("foo_1", "bar_1"),
  3652  		},
  3653  		{
  3654  			name: "ignore file in root and subdir, matches bar and subdir",
  3655  			files: map[string]string{
  3656  				"subdir/" + ProwIgnoreFileName: `*.yaml`,
  3657  				ProwIgnoreFileName:             `bar_jobs.yaml`,
  3658  			},
  3659  			verify: expectExactly("foo_1"),
  3660  		},
  3661  	}
  3662  
  3663  	for _, tc := range testCases {
  3664  		tc := tc
  3665  		t.Run(tc.name, func(t *testing.T) {
  3666  			jobConfigDir := t.TempDir()
  3667  			err := os.Mkdir(filepath.Join(jobConfigDir, "subdir"), 0777)
  3668  			if err != nil {
  3669  				t.Fatalf("fail to make subdir: %v", err)
  3670  			}
  3671  
  3672  			for _, fileMap := range []map[string]string{commonFiles, tc.files} {
  3673  				for name, content := range fileMap {
  3674  					fullName := filepath.Join(jobConfigDir, name)
  3675  					if err := os.WriteFile(fullName, []byte(content), 0666); err != nil {
  3676  						t.Fatalf("fail to write file %s: %v", fullName, err)
  3677  					}
  3678  				}
  3679  			}
  3680  
  3681  			cfg, err := ReadJobConfig(jobConfigDir)
  3682  			if err != nil {
  3683  				t.Fatalf("Unexpected error reading job config: %v.", err)
  3684  			}
  3685  
  3686  			if tc.verify != nil {
  3687  				if err := tc.verify(&cfg); err != nil {
  3688  					t.Errorf("Verify failed:  %v", err)
  3689  				}
  3690  			}
  3691  		})
  3692  	}
  3693  }
  3694  
  3695  func TestBrancher_Intersects(t *testing.T) {
  3696  	testCases := []struct {
  3697  		name   string
  3698  		a, b   Brancher
  3699  		result bool
  3700  	}{
  3701  		{
  3702  			name: "TwodifferentBranches",
  3703  			a: Brancher{
  3704  				Branches: []string{"a"},
  3705  			},
  3706  			b: Brancher{
  3707  				Branches: []string{"b"},
  3708  			},
  3709  		},
  3710  		{
  3711  			name: "Opposite",
  3712  			a: Brancher{
  3713  				SkipBranches: []string{"b"},
  3714  			},
  3715  			b: Brancher{
  3716  				Branches: []string{"b"},
  3717  			},
  3718  		},
  3719  		{
  3720  			name:   "BothRunOnAllBranches",
  3721  			a:      Brancher{},
  3722  			b:      Brancher{},
  3723  			result: true,
  3724  		},
  3725  		{
  3726  			name: "RunsOnAllBranchesAndSpecified",
  3727  			a:    Brancher{},
  3728  			b: Brancher{
  3729  				Branches: []string{"b"},
  3730  			},
  3731  			result: true,
  3732  		},
  3733  		{
  3734  			name: "SkipBranchesAndSet",
  3735  			a: Brancher{
  3736  				SkipBranches: []string{"a", "b", "c"},
  3737  			},
  3738  			b: Brancher{
  3739  				Branches: []string{"a"},
  3740  			},
  3741  		},
  3742  		{
  3743  			name: "SkipBranchesAndSet",
  3744  			a: Brancher{
  3745  				Branches: []string{"c"},
  3746  			},
  3747  			b: Brancher{
  3748  				Branches: []string{"a"},
  3749  			},
  3750  		},
  3751  		{
  3752  			name: "BothSkipBranches",
  3753  			a: Brancher{
  3754  				SkipBranches: []string{"a", "b", "c"},
  3755  			},
  3756  			b: Brancher{
  3757  				SkipBranches: []string{"d", "e", "f"},
  3758  			},
  3759  			result: true,
  3760  		},
  3761  		{
  3762  			name: "BothSkipCommonBranches",
  3763  			a: Brancher{
  3764  				SkipBranches: []string{"a", "b", "c"},
  3765  			},
  3766  			b: Brancher{
  3767  				SkipBranches: []string{"b", "e", "f"},
  3768  			},
  3769  			result: true,
  3770  		},
  3771  		{
  3772  			name: "NoIntersectionBecauseRegexSkip",
  3773  			a: Brancher{
  3774  				SkipBranches: []string{`release-\d+\.\d+`},
  3775  			},
  3776  			b: Brancher{
  3777  				Branches: []string{`release-1.14`, `release-1.13`},
  3778  			},
  3779  			result: false,
  3780  		},
  3781  		{
  3782  			name: "IntersectionDespiteRegexSkip",
  3783  			a: Brancher{
  3784  				SkipBranches: []string{`release-\d+\.\d+`},
  3785  			},
  3786  			b: Brancher{
  3787  				Branches: []string{`release-1.14`, `master`},
  3788  			},
  3789  			result: true,
  3790  		},
  3791  	}
  3792  
  3793  	for _, tc := range testCases {
  3794  		t.Run(tc.name, func(st *testing.T) {
  3795  			a, err := setBrancherRegexes(tc.a)
  3796  			if err != nil {
  3797  				st.Fatalf("Failed to set brancher A regexes: %v", err)
  3798  			}
  3799  			b, err := setBrancherRegexes(tc.b)
  3800  			if err != nil {
  3801  				st.Fatalf("Failed to set brancher B regexes: %v", err)
  3802  			}
  3803  			r1 := a.Intersects(b)
  3804  			r2 := b.Intersects(a)
  3805  			for _, result := range []bool{r1, r2} {
  3806  				if result != tc.result {
  3807  					st.Errorf("Expected %v got %v", tc.result, result)
  3808  				}
  3809  			}
  3810  		})
  3811  	}
  3812  }
  3813  
  3814  // Integration test for fake secrets loading in a secret agent.
  3815  // Checking also if the agent changes the secret's values as expected.
  3816  func TestSecretAgentLoading(t *testing.T) {
  3817  	tempTokenValue := "121f3cb3e7f70feeb35f9204f5a988d7292c7ba1"
  3818  	changedTokenValue := "121f3cb3e7f70feeb35f9204f5a988d7292c7ba0"
  3819  
  3820  	// Creating a temporary directory.
  3821  	secretDir := t.TempDir()
  3822  
  3823  	// Create the first temporary secret.
  3824  	firstTempSecret := filepath.Join(secretDir, "firstTempSecret")
  3825  	if err := os.WriteFile(firstTempSecret, []byte(tempTokenValue), 0666); err != nil {
  3826  		t.Fatalf("fail to write secret: %v", err)
  3827  	}
  3828  
  3829  	// Create the second temporary secret.
  3830  	secondTempSecret := filepath.Join(secretDir, "secondTempSecret")
  3831  	if err := os.WriteFile(secondTempSecret, []byte(tempTokenValue), 0666); err != nil {
  3832  		t.Fatalf("fail to write secret: %v", err)
  3833  	}
  3834  
  3835  	tempSecrets := []string{firstTempSecret, secondTempSecret}
  3836  	// Starting the agent and add the two temporary secrets.
  3837  	if err := secret.Add(tempSecrets...); err != nil {
  3838  		t.Fatalf("Error starting secrets agent. %v", err)
  3839  	}
  3840  
  3841  	// Check if the values are as expected.
  3842  	for _, tempSecret := range tempSecrets {
  3843  		tempSecretValue := secret.GetSecret(tempSecret)
  3844  		if string(tempSecretValue) != tempTokenValue {
  3845  			t.Fatalf("In secret %s it was expected %s but found %s",
  3846  				tempSecret, tempTokenValue, tempSecretValue)
  3847  		}
  3848  	}
  3849  
  3850  	// Change the values of the files.
  3851  	if err := os.WriteFile(firstTempSecret, []byte(changedTokenValue), 0666); err != nil {
  3852  		t.Fatalf("fail to write secret: %v", err)
  3853  	}
  3854  	if err := os.WriteFile(secondTempSecret, []byte(changedTokenValue), 0666); err != nil {
  3855  		t.Fatalf("fail to write secret: %v", err)
  3856  	}
  3857  
  3858  	retries := 10
  3859  	var errors []string
  3860  
  3861  	// Check if the values changed as expected.
  3862  	for _, tempSecret := range tempSecrets {
  3863  		// Reset counter
  3864  		counter := 0
  3865  		for counter <= retries {
  3866  			tempSecretValue := secret.GetSecret(tempSecret)
  3867  			if string(tempSecretValue) != changedTokenValue {
  3868  				if counter == retries {
  3869  					errors = append(errors, fmt.Sprintf("In secret %s it was expected %s but found %s\n",
  3870  						tempSecret, changedTokenValue, tempSecretValue))
  3871  				} else {
  3872  					// Secret agent needs some time to update the values. So wait and retry.
  3873  					time.Sleep(400 * time.Millisecond)
  3874  				}
  3875  			} else {
  3876  				break
  3877  			}
  3878  			counter++
  3879  		}
  3880  	}
  3881  
  3882  	if len(errors) > 0 {
  3883  		t.Fatal(errors)
  3884  	}
  3885  
  3886  }
  3887  
  3888  func TestValidGitHubReportType(t *testing.T) {
  3889  	var testCases = []struct {
  3890  		name        string
  3891  		prowConfig  string
  3892  		expectError bool
  3893  		expectTypes []prowapi.ProwJobType
  3894  	}{
  3895  		{
  3896  			name:        "empty config should default to report for both presubmit and postsubmit",
  3897  			prowConfig:  ``,
  3898  			expectTypes: []prowapi.ProwJobType{prowapi.PresubmitJob, prowapi.PostsubmitJob},
  3899  		},
  3900  		{
  3901  			name: "reject unsupported job types",
  3902  			prowConfig: `
  3903  github_reporter:
  3904    job_types_to_report:
  3905    - presubmit
  3906    - batch
  3907  `,
  3908  			expectError: true,
  3909  		},
  3910  		{
  3911  			name: "accept valid job types",
  3912  			prowConfig: `
  3913  github_reporter:
  3914    job_types_to_report:
  3915    - presubmit
  3916    - postsubmit
  3917  `,
  3918  			expectTypes: []prowapi.ProwJobType{prowapi.PresubmitJob, prowapi.PostsubmitJob},
  3919  		},
  3920  	}
  3921  
  3922  	for _, tc := range testCases {
  3923  		// save the config
  3924  		prowConfigDir := t.TempDir()
  3925  
  3926  		prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  3927  		if err := os.WriteFile(prowConfig, []byte(tc.prowConfig), 0666); err != nil {
  3928  			t.Fatalf("fail to write prow config: %v", err)
  3929  		}
  3930  
  3931  		cfg, err := Load(prowConfig, "", nil, "")
  3932  		if tc.expectError && err == nil {
  3933  			t.Errorf("tc %s: Expect error, but got nil", tc.name)
  3934  		} else if !tc.expectError && err != nil {
  3935  			t.Errorf("tc %s: Expect no error, but got error %v", tc.name, err)
  3936  		}
  3937  
  3938  		if err == nil {
  3939  			if !reflect.DeepEqual(cfg.GitHubReporter.JobTypesToReport, tc.expectTypes) {
  3940  				t.Errorf("tc %s: expected %#v\n!=\nactual %#v", tc.name, tc.expectTypes, cfg.GitHubReporter.JobTypesToReport)
  3941  			}
  3942  		}
  3943  	}
  3944  }
  3945  
  3946  func TestRerunAuthConfigsGetRerunAuthConfig(t *testing.T) {
  3947  	var testCases = []struct {
  3948  		name     string
  3949  		configs  RerunAuthConfigs
  3950  		jobSpec  *prowapi.ProwJobSpec
  3951  		expected *prowapi.RerunAuthConfig
  3952  	}{
  3953  		{
  3954  			name:    "default to an empty config",
  3955  			configs: RerunAuthConfigs{},
  3956  			jobSpec: &prowapi.ProwJobSpec{
  3957  				Refs: &prowapi.Refs{Org: "my-default-org", Repo: "my-default-repo"},
  3958  			},
  3959  			expected: nil,
  3960  		},
  3961  		{
  3962  			name:    "unknown org or org/repo return wildcard",
  3963  			configs: RerunAuthConfigs{"*": prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}}},
  3964  			jobSpec: &prowapi.ProwJobSpec{
  3965  				Refs: &prowapi.Refs{Org: "my-default-org", Repo: "my-default-repo"},
  3966  			},
  3967  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  3968  		},
  3969  		{
  3970  			name:     "no refs return wildcard empty string match",
  3971  			configs:  RerunAuthConfigs{"": prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}}},
  3972  			jobSpec:  &prowapi.ProwJobSpec{},
  3973  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}},
  3974  		},
  3975  		{
  3976  			name: "no refs return wildcard override to star match",
  3977  			configs: RerunAuthConfigs{
  3978  				"":      prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  3979  				"*":     prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  3980  				"istio": prowapi.RerunAuthConfig{GitHubUsers: []string{"billybob"}},
  3981  			},
  3982  			jobSpec:  &prowapi.ProwJobSpec{},
  3983  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  3984  		},
  3985  		{
  3986  			name: "no refs return wildcard but there is no match",
  3987  			configs: RerunAuthConfigs{
  3988  				"istio": prowapi.RerunAuthConfig{GitHubUsers: []string{"billybob"}},
  3989  			},
  3990  			jobSpec:  &prowapi.ProwJobSpec{},
  3991  			expected: nil,
  3992  		},
  3993  		{
  3994  			name: "use org if defined",
  3995  			configs: RerunAuthConfigs{
  3996  				"*":                prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  3997  				"istio":            prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  3998  				"istio/test-infra": prowapi.RerunAuthConfig{GitHubUsers: []string{"billybob"}},
  3999  			},
  4000  			jobSpec: &prowapi.ProwJobSpec{
  4001  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4002  			},
  4003  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  4004  		},
  4005  		{
  4006  			name: "use org/repo if defined",
  4007  			configs: RerunAuthConfigs{
  4008  				"*":           prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4009  				"istio/istio": prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4010  			},
  4011  			jobSpec: &prowapi.ProwJobSpec{
  4012  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4013  			},
  4014  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4015  		},
  4016  		{
  4017  			name: "org/repo takes precedence over org",
  4018  			configs: RerunAuthConfigs{
  4019  				"*":           prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4020  				"istio":       prowapi.RerunAuthConfig{GitHubUsers: []string{"scrappydoo"}},
  4021  				"istio/istio": prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4022  			},
  4023  			jobSpec: &prowapi.ProwJobSpec{
  4024  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4025  			},
  4026  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4027  		},
  4028  		{
  4029  			name: "use org/repo from extra refs",
  4030  			configs: RerunAuthConfigs{
  4031  				"*":           prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4032  				"istio/istio": prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4033  			},
  4034  			jobSpec: &prowapi.ProwJobSpec{
  4035  				ExtraRefs: []prowapi.Refs{{Org: "istio", Repo: "istio"}},
  4036  			},
  4037  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4038  		},
  4039  		{
  4040  			name: "use only org/repo from first extra refs entry",
  4041  			configs: RerunAuthConfigs{
  4042  				"*":                    prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4043  				"istio/istio":          prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4044  				"other-org/other-repo": prowapi.RerunAuthConfig{GitHubUsers: []string{"anakin"}},
  4045  			},
  4046  			jobSpec: &prowapi.ProwJobSpec{
  4047  				ExtraRefs: []prowapi.Refs{{Org: "istio", Repo: "istio"}, {Org: "other-org", Repo: "other-repo"}},
  4048  			},
  4049  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4050  		},
  4051  	}
  4052  
  4053  	for _, tc := range testCases {
  4054  		t.Run(tc.name, func(t *testing.T) {
  4055  			d := Deck{}
  4056  			d.RerunAuthConfigs = tc.configs
  4057  			err := d.FinalizeDefaultRerunAuthConfigs()
  4058  			if err != nil {
  4059  				t.Fatal("Failed to finalize default rerun auth config.")
  4060  			}
  4061  
  4062  			if diff := cmp.Diff(tc.expected, d.GetRerunAuthConfig(tc.jobSpec)); diff != "" {
  4063  				t.Errorf("GetRerunAuthConfig returned unexpected value (-want +got):\n%s", diff)
  4064  			}
  4065  		})
  4066  	}
  4067  }
  4068  
  4069  func TestDefaultRerunAuthConfigsGetRerunAuthConfig(t *testing.T) {
  4070  	var testCases = []struct {
  4071  		name     string
  4072  		configs  []*DefaultRerunAuthConfigEntry
  4073  		jobSpec  *prowapi.ProwJobSpec
  4074  		expected *prowapi.RerunAuthConfig
  4075  	}{
  4076  		{
  4077  			name:    "default to an empty config",
  4078  			configs: []*DefaultRerunAuthConfigEntry{},
  4079  			jobSpec: &prowapi.ProwJobSpec{
  4080  				Refs: &prowapi.Refs{Org: "my-default-org", Repo: "my-default-repo"},
  4081  			},
  4082  			expected: nil,
  4083  		},
  4084  		{
  4085  			name: "unknown org or org/repo return wildcard",
  4086  			configs: []*DefaultRerunAuthConfigEntry{
  4087  				{
  4088  					OrgRepo: "*",
  4089  					Cluster: "",
  4090  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4091  				},
  4092  			},
  4093  			jobSpec: &prowapi.ProwJobSpec{
  4094  				Refs: &prowapi.Refs{Org: "my-default-org", Repo: "my-default-repo"},
  4095  			},
  4096  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4097  		},
  4098  		{
  4099  			name: "no refs return wildcard",
  4100  			configs: []*DefaultRerunAuthConfigEntry{
  4101  				{
  4102  					OrgRepo: "*",
  4103  					Cluster: "",
  4104  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}},
  4105  				},
  4106  			},
  4107  			jobSpec:  &prowapi.ProwJobSpec{},
  4108  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}},
  4109  		},
  4110  		{
  4111  			name: "no refs return wildcard empty string match",
  4112  			configs: []*DefaultRerunAuthConfigEntry{
  4113  				{
  4114  					OrgRepo: "",
  4115  					Cluster: "",
  4116  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}},
  4117  				},
  4118  			},
  4119  			jobSpec:  &prowapi.ProwJobSpec{},
  4120  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"leonardo"}},
  4121  		},
  4122  		{
  4123  			name: "no refs return wildcard override to star match",
  4124  			configs: []*DefaultRerunAuthConfigEntry{
  4125  				{
  4126  					OrgRepo: "",
  4127  					Cluster: "",
  4128  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4129  				},
  4130  				{
  4131  					OrgRepo: "*",
  4132  					Cluster: "",
  4133  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  4134  				},
  4135  				{
  4136  					OrgRepo: "istio",
  4137  					Cluster: "",
  4138  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4139  				},
  4140  			},
  4141  			jobSpec:  &prowapi.ProwJobSpec{},
  4142  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  4143  		},
  4144  		{
  4145  			name: "no refs return wildcard but there is no match",
  4146  			configs: []*DefaultRerunAuthConfigEntry{
  4147  				{
  4148  					OrgRepo: "istio",
  4149  					Cluster: "",
  4150  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"billybob"}},
  4151  				},
  4152  			},
  4153  			jobSpec:  &prowapi.ProwJobSpec{},
  4154  			expected: nil,
  4155  		},
  4156  		{
  4157  			name: "use org if defined",
  4158  			configs: []*DefaultRerunAuthConfigEntry{
  4159  				{
  4160  					OrgRepo: "*",
  4161  					Cluster: "",
  4162  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4163  				},
  4164  				{
  4165  					OrgRepo: "istio",
  4166  					Cluster: "",
  4167  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  4168  				},
  4169  				{
  4170  					OrgRepo: "istio/test-infra",
  4171  					Cluster: "",
  4172  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"billybob"}},
  4173  				},
  4174  			},
  4175  			jobSpec: &prowapi.ProwJobSpec{
  4176  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4177  			},
  4178  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"scoobydoo"}},
  4179  		},
  4180  		{
  4181  			name: "use org/repo if defined",
  4182  			configs: []*DefaultRerunAuthConfigEntry{
  4183  				{
  4184  					OrgRepo: "*",
  4185  					Cluster: "",
  4186  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4187  				},
  4188  				{
  4189  					OrgRepo: "istio/istio",
  4190  					Cluster: "",
  4191  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4192  				},
  4193  			},
  4194  			jobSpec: &prowapi.ProwJobSpec{
  4195  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4196  			},
  4197  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4198  		},
  4199  		{
  4200  			name: "org/repo takes precedence over org",
  4201  			configs: []*DefaultRerunAuthConfigEntry{
  4202  				{
  4203  					OrgRepo: "*",
  4204  					Cluster: "",
  4205  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4206  				},
  4207  				{
  4208  					OrgRepo: "istio",
  4209  					Cluster: "",
  4210  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"scrappydoo"}},
  4211  				},
  4212  				{
  4213  					OrgRepo: "istio/istio",
  4214  					Cluster: "",
  4215  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4216  				},
  4217  			},
  4218  			jobSpec: &prowapi.ProwJobSpec{
  4219  				Refs: &prowapi.Refs{Org: "istio", Repo: "istio"},
  4220  			},
  4221  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4222  		},
  4223  		{
  4224  			name: "cluster returns matching cluster",
  4225  			configs: []*DefaultRerunAuthConfigEntry{
  4226  				{
  4227  					OrgRepo: "istio",
  4228  					Cluster: "",
  4229  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"scrappydoo"}},
  4230  				},
  4231  				{
  4232  					OrgRepo: "istio",
  4233  					Cluster: "trusted",
  4234  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4235  				},
  4236  			},
  4237  			jobSpec: &prowapi.ProwJobSpec{
  4238  				Refs:    &prowapi.Refs{Org: "istio", Repo: "istio"},
  4239  				Cluster: "trusted",
  4240  			},
  4241  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4242  		},
  4243  		{
  4244  			name: "cluster returns wild card",
  4245  			configs: []*DefaultRerunAuthConfigEntry{
  4246  				{
  4247  					OrgRepo: "istio",
  4248  					Cluster: "*",
  4249  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"scrappydoo"}},
  4250  				},
  4251  				{
  4252  					OrgRepo: "istio",
  4253  					Cluster: "cluster",
  4254  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4255  				},
  4256  			},
  4257  			jobSpec: &prowapi.ProwJobSpec{
  4258  				Refs:    &prowapi.Refs{Org: "istio", Repo: "istio"},
  4259  				Cluster: "trusted",
  4260  			},
  4261  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"scrappydoo"}},
  4262  		},
  4263  		{
  4264  			name: "no refs with cluster returns overriding matching cluster",
  4265  			configs: []*DefaultRerunAuthConfigEntry{
  4266  				{
  4267  					OrgRepo: "",
  4268  					Cluster: "*",
  4269  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4270  				},
  4271  				{
  4272  					OrgRepo: "",
  4273  					Cluster: "trusted",
  4274  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4275  				},
  4276  			},
  4277  			jobSpec: &prowapi.ProwJobSpec{
  4278  				Cluster: "trusted",
  4279  			},
  4280  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"airbender"}},
  4281  		},
  4282  		{
  4283  			name: "no matching orgrepo or cluster",
  4284  			configs: []*DefaultRerunAuthConfigEntry{
  4285  				{
  4286  					OrgRepo: "notIstio",
  4287  					Cluster: "notTrusted",
  4288  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4289  				},
  4290  			},
  4291  			jobSpec: &prowapi.ProwJobSpec{
  4292  				Refs:    &prowapi.Refs{Org: "istio", Repo: "istio"},
  4293  				Cluster: "trusted",
  4294  			},
  4295  			expected: nil,
  4296  		},
  4297  		{
  4298  			name: "use org/repo from extra refs",
  4299  			configs: []*DefaultRerunAuthConfigEntry{
  4300  				{
  4301  					OrgRepo: "*",
  4302  					Cluster: "",
  4303  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4304  				},
  4305  				{
  4306  					OrgRepo: "istio/istio",
  4307  					Cluster: "",
  4308  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4309  				},
  4310  			},
  4311  			jobSpec: &prowapi.ProwJobSpec{
  4312  				ExtraRefs: []prowapi.Refs{{Org: "istio", Repo: "istio"}},
  4313  			},
  4314  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4315  		},
  4316  		{
  4317  			name: "use only org/repo from first extra refs entry",
  4318  			configs: []*DefaultRerunAuthConfigEntry{
  4319  				{
  4320  					OrgRepo: "*",
  4321  					Cluster: "",
  4322  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"clarketm"}},
  4323  				},
  4324  				{
  4325  					OrgRepo: "istio/istio",
  4326  					Cluster: "",
  4327  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4328  				},
  4329  				{
  4330  					OrgRepo: "other-org/other-repo",
  4331  					Cluster: "",
  4332  					Config:  &prowapi.RerunAuthConfig{GitHubUsers: []string{"anakin"}},
  4333  				},
  4334  			},
  4335  			jobSpec: &prowapi.ProwJobSpec{
  4336  				ExtraRefs: []prowapi.Refs{{Org: "istio", Repo: "istio"}, {Org: "other-org", Repo: "other-repo"}},
  4337  			},
  4338  			expected: &prowapi.RerunAuthConfig{GitHubUsers: []string{"skywalker"}},
  4339  		},
  4340  	}
  4341  
  4342  	for _, tc := range testCases {
  4343  		t.Run(tc.name, func(t *testing.T) {
  4344  			d := Deck{}
  4345  			d.DefaultRerunAuthConfigs = tc.configs
  4346  			err := d.FinalizeDefaultRerunAuthConfigs()
  4347  			if err != nil {
  4348  				t.Fatal("Failed to finalize default rerun auth config.")
  4349  			}
  4350  
  4351  			if diff := cmp.Diff(tc.expected, d.GetRerunAuthConfig(tc.jobSpec)); diff != "" {
  4352  				t.Errorf("GetRerunAuthConfig returned unexpected value (-want +got):\n%s", diff)
  4353  			}
  4354  		})
  4355  	}
  4356  }
  4357  
  4358  func TestMergeCommitTemplateLoading(t *testing.T) {
  4359  	var testCases = []struct {
  4360  		name        string
  4361  		prowConfig  string
  4362  		expectError bool
  4363  		expect      map[string]TideMergeCommitTemplate
  4364  	}{
  4365  		{
  4366  			name: "no template",
  4367  			prowConfig: `
  4368  tide:
  4369    merge_commit_template:
  4370  `,
  4371  			expect: nil,
  4372  		},
  4373  		{
  4374  			name: "empty template",
  4375  			prowConfig: `
  4376  tide:
  4377    merge_commit_template:
  4378      kubernetes/ingress:
  4379  `,
  4380  			expect: map[string]TideMergeCommitTemplate{
  4381  				"kubernetes/ingress": {},
  4382  			},
  4383  		},
  4384  		{
  4385  			name: "two proper templates",
  4386  			prowConfig: `
  4387  tide:
  4388    merge_commit_template:
  4389      kubernetes/ingress:
  4390        title: "{{ .Title }}"
  4391        body: "{{ .Body }}"
  4392  `,
  4393  			expect: map[string]TideMergeCommitTemplate{
  4394  				"kubernetes/ingress": {
  4395  					TitleTemplate: "{{ .Title }}",
  4396  					BodyTemplate:  "{{ .Body }}",
  4397  					Title:         template.Must(template.New("CommitTitle").Parse("{{ .Title }}")),
  4398  					Body:          template.Must(template.New("CommitBody").Parse("{{ .Body }}")),
  4399  				},
  4400  			},
  4401  		},
  4402  		{
  4403  			name: "only title template",
  4404  			prowConfig: `
  4405  tide:
  4406    merge_commit_template:
  4407      kubernetes/ingress:
  4408        title: "{{ .Title }}"
  4409  `,
  4410  			expect: map[string]TideMergeCommitTemplate{
  4411  				"kubernetes/ingress": {
  4412  					TitleTemplate: "{{ .Title }}",
  4413  					BodyTemplate:  "",
  4414  					Title:         template.Must(template.New("CommitTitle").Parse("{{ .Title }}")),
  4415  					Body:          nil,
  4416  				},
  4417  			},
  4418  		},
  4419  		{
  4420  			name: "only body template",
  4421  			prowConfig: `
  4422  tide:
  4423    merge_commit_template:
  4424      kubernetes/ingress:
  4425        body: "{{ .Body }}"
  4426  `,
  4427  			expect: map[string]TideMergeCommitTemplate{
  4428  				"kubernetes/ingress": {
  4429  					TitleTemplate: "",
  4430  					BodyTemplate:  "{{ .Body }}",
  4431  					Title:         nil,
  4432  					Body:          template.Must(template.New("CommitBody").Parse("{{ .Body }}")),
  4433  				},
  4434  			},
  4435  		},
  4436  		{
  4437  			name: "malformed title template",
  4438  			prowConfig: `
  4439  tide:
  4440    merge_commit_template:
  4441      kubernetes/ingress:
  4442        title: "{{ .Title"
  4443  `,
  4444  			expectError: true,
  4445  		},
  4446  		{
  4447  			name: "malformed body template",
  4448  			prowConfig: `
  4449  tide:
  4450    merge_commit_template:
  4451      kubernetes/ingress:
  4452        body: "{{ .Body"
  4453  `,
  4454  			expectError: true,
  4455  		},
  4456  	}
  4457  
  4458  	for _, tc := range testCases {
  4459  		// save the config
  4460  		prowConfigDir := t.TempDir()
  4461  
  4462  		prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  4463  		if err := os.WriteFile(prowConfig, []byte(tc.prowConfig), 0666); err != nil {
  4464  			t.Fatalf("fail to write prow config: %v", err)
  4465  		}
  4466  
  4467  		cfg, err := Load(prowConfig, "", nil, "")
  4468  		if tc.expectError && err == nil {
  4469  			t.Errorf("tc %s: Expect error, but got nil", tc.name)
  4470  		} else if !tc.expectError && err != nil {
  4471  			t.Errorf("tc %s: Expect no error, but got error %v", tc.name, err)
  4472  		}
  4473  
  4474  		if err == nil {
  4475  			if !reflect.DeepEqual(cfg.Tide.MergeTemplate, tc.expect) {
  4476  				t.Errorf("tc %s: expected %#v\n!=\nactual %#v", tc.name, tc.expect, cfg.Tide.MergeTemplate)
  4477  			}
  4478  		}
  4479  	}
  4480  }
  4481  
  4482  func TestPlankJobURLPrefix(t *testing.T) {
  4483  	testCases := []struct {
  4484  		name                 string
  4485  		plank                Plank
  4486  		prowjob              *prowapi.ProwJob
  4487  		expectedJobURLPrefix string
  4488  	}{
  4489  		{
  4490  			name:                 "Nil refs returns default JobURLPrefix",
  4491  			plank:                Plank{JobURLPrefixConfig: map[string]string{"*": "https://my-prow"}},
  4492  			expectedJobURLPrefix: "https://my-prow",
  4493  		},
  4494  		{
  4495  			name: "No matching refs returns default JobURLPrefx",
  4496  			plank: Plank{
  4497  				JobURLPrefixConfig: map[string]string{
  4498  					"*":      "https://my-prow",
  4499  					"my-org": "https://my-alternate-prow",
  4500  				},
  4501  			},
  4502  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Org: "my-default-org", Repo: "my-default-repo"}}},
  4503  			expectedJobURLPrefix: "https://my-prow",
  4504  		},
  4505  		{
  4506  			name: "Matching repo returns JobURLPrefix from repo",
  4507  			plank: Plank{
  4508  				JobURLPrefixConfig: map[string]string{
  4509  					"*":                        "https://my-prow",
  4510  					"my-alternate-org":         "https://my-third-prow",
  4511  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4512  				},
  4513  			},
  4514  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Org: "my-alternate-org", Repo: "my-repo"}}},
  4515  			expectedJobURLPrefix: "https://my-alternate-prow",
  4516  		},
  4517  		{
  4518  			name: "Matching repo in extraRefs returns JobURLPrefix from repo",
  4519  			plank: Plank{
  4520  				JobURLPrefixConfig: map[string]string{
  4521  					"*":                        "https://my-prow",
  4522  					"my-alternate-org":         "https://my-third-prow",
  4523  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4524  				},
  4525  			},
  4526  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{ExtraRefs: []prowapi.Refs{{Org: "my-alternate-org", Repo: "my-repo"}}}},
  4527  			expectedJobURLPrefix: "https://my-alternate-prow",
  4528  		},
  4529  		{
  4530  			name: "JobURLPrefix in decoration config overrides job_url_prefix_config",
  4531  			plank: Plank{
  4532  				JobURLPrefixConfig: map[string]string{
  4533  					"*":                        "https://my-prow",
  4534  					"my-alternate-org":         "https://my-third-prow",
  4535  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4536  				},
  4537  			},
  4538  			prowjob: &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{
  4539  				DecorationConfig: &prowapi.DecorationConfig{GCSConfiguration: &prowapi.GCSConfiguration{JobURLPrefix: "https://overriden"}},
  4540  				Refs:             &prowapi.Refs{Org: "my-alternate-org", Repo: "my-repo"},
  4541  			}},
  4542  			expectedJobURLPrefix: "https://overriden",
  4543  		},
  4544  		{
  4545  			name: "Matching org and not matching repo returns JobURLPrefix from org",
  4546  			plank: Plank{
  4547  				JobURLPrefixConfig: map[string]string{
  4548  					"*":                        "https://my-prow",
  4549  					"my-alternate-org":         "https://my-third-prow",
  4550  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4551  				},
  4552  			},
  4553  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Org: "my-alternate-org", Repo: "my-second-repo"}}},
  4554  			expectedJobURLPrefix: "https://my-third-prow",
  4555  		},
  4556  		{
  4557  			name: "Matching org in extraRefs and not matching repo returns JobURLPrefix from org",
  4558  			plank: Plank{
  4559  				JobURLPrefixConfig: map[string]string{
  4560  					"*":                        "https://my-prow",
  4561  					"my-alternate-org":         "https://my-third-prow",
  4562  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4563  				},
  4564  			},
  4565  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{ExtraRefs: []prowapi.Refs{{Org: "my-alternate-org", Repo: "my-second-repo"}}}},
  4566  			expectedJobURLPrefix: "https://my-third-prow",
  4567  		},
  4568  		{
  4569  			name: "Matching org without url returns default JobURLPrefix",
  4570  			plank: Plank{
  4571  				JobURLPrefixConfig: map[string]string{
  4572  					"*":                        "https://my-prow",
  4573  					"my-alternate-org/my-repo": "https://my-alternate-prow",
  4574  				},
  4575  			},
  4576  			prowjob:              &prowapi.ProwJob{Spec: prowapi.ProwJobSpec{Refs: &prowapi.Refs{Org: "my-alternate-org", Repo: "my-second-repo"}}},
  4577  			expectedJobURLPrefix: "https://my-prow",
  4578  		},
  4579  	}
  4580  
  4581  	for _, tc := range testCases {
  4582  		t.Run(tc.name, func(t *testing.T) {
  4583  			if tc.prowjob == nil {
  4584  				tc.prowjob = &prowapi.ProwJob{}
  4585  			}
  4586  			if prefix := tc.plank.GetJobURLPrefix(tc.prowjob); prefix != tc.expectedJobURLPrefix {
  4587  				t.Errorf("expected JobURLPrefix to be %q but was %q", tc.expectedJobURLPrefix, prefix)
  4588  			}
  4589  		})
  4590  	}
  4591  }
  4592  
  4593  func TestValidateComponentConfig(t *testing.T) {
  4594  	boolTrue := true
  4595  	boolFalse := false
  4596  	testCases := []struct {
  4597  		name        string
  4598  		config      *Config
  4599  		errExpected bool
  4600  	}{
  4601  		{
  4602  			name: "Valid default URL, no err",
  4603  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4604  				JobURLPrefixConfig: map[string]string{"*": "https://my-prow"}}}},
  4605  			errExpected: false,
  4606  		},
  4607  		{
  4608  			name: "Invalid default URL, err",
  4609  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4610  				JobURLPrefixConfig: map[string]string{"*": "https:// my-prow"}}}},
  4611  			errExpected: true,
  4612  		},
  4613  		{
  4614  			name: "Org config, valid URLs, no err",
  4615  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4616  				JobURLPrefixConfig: map[string]string{
  4617  					"*":      "https://my-prow",
  4618  					"my-org": "https://my-alternate-prow",
  4619  				},
  4620  			}}},
  4621  			errExpected: false,
  4622  		},
  4623  		{
  4624  			name: "Org override, invalid default jobURLPrefix URL, err",
  4625  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4626  				JobURLPrefixConfig: map[string]string{
  4627  					"*":      "https:// my-prow",
  4628  					"my-org": "https://my-alternate-prow",
  4629  				},
  4630  			}}},
  4631  			errExpected: true,
  4632  		},
  4633  		{
  4634  			name: "Org override, invalid org URL, err",
  4635  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4636  				JobURLPrefixConfig: map[string]string{
  4637  					"*":      "https://my-prow",
  4638  					"my-org": "https:// my-alternate-prow",
  4639  				},
  4640  			}}},
  4641  			errExpected: true,
  4642  		},
  4643  		{
  4644  			name: "Org override, invalid URLs, err",
  4645  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4646  				JobURLPrefixConfig: map[string]string{
  4647  					"*":      "https:// my-prow",
  4648  					"my-org": "https:// my-alternate-prow",
  4649  				},
  4650  			}}},
  4651  			errExpected: true,
  4652  		},
  4653  		{
  4654  			name: "Repo override, valid URLs, no err",
  4655  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4656  				JobURLPrefixConfig: map[string]string{
  4657  					"*":              "https://my-prow",
  4658  					"my-org":         "https://my-alternate-prow",
  4659  					"my-org/my-repo": "https://my-third-prow",
  4660  				}}}},
  4661  			errExpected: false,
  4662  		},
  4663  		{
  4664  			name: "Repo override, invalid repo URL, err",
  4665  			config: &Config{ProwConfig: ProwConfig{Plank: Plank{
  4666  				JobURLPrefixConfig: map[string]string{
  4667  					"*":              "https://my-prow",
  4668  					"my-org":         "https://my-alternate-prow",
  4669  					"my-org/my-repo": "https:// my-third-prow",
  4670  				}}}},
  4671  			errExpected: true,
  4672  		},
  4673  		{
  4674  			name: "RerunAuthConfigs and not RerunAuthConfig is valid, no err",
  4675  			config: &Config{ProwConfig: ProwConfig{Deck: Deck{
  4676  				RerunAuthConfigs: RerunAuthConfigs{
  4677  					"*":                     prowapi.RerunAuthConfig{AllowAnyone: true},
  4678  					"kubernetes":            prowapi.RerunAuthConfig{GitHubUsers: []string{"easterbunny"}},
  4679  					"kubernetes/kubernetes": prowapi.RerunAuthConfig{GitHubOrgs: []string{"kubernetes", "kubernetes-sigs"}},
  4680  				},
  4681  			}}},
  4682  			errExpected: false,
  4683  		},
  4684  		{
  4685  			name: "RerunAuthConfigs only and validation fails, err",
  4686  			config: &Config{ProwConfig: ProwConfig{Deck: Deck{
  4687  				RerunAuthConfigs: RerunAuthConfigs{
  4688  					"*":                     prowapi.RerunAuthConfig{AllowAnyone: true},
  4689  					"kubernetes":            prowapi.RerunAuthConfig{GitHubUsers: []string{"easterbunny"}},
  4690  					"kubernetes/kubernetes": prowapi.RerunAuthConfig{AllowAnyone: true, GitHubOrgs: []string{"kubernetes", "kubernetes-sigs"}},
  4691  				},
  4692  			}}},
  4693  			errExpected: true,
  4694  		},
  4695  		{
  4696  			name: "SkipStoragePathValidation true and AdditionalAllowedBuckets empty, no err",
  4697  			config: &Config{ProwConfig: ProwConfig{Deck: Deck{
  4698  				SkipStoragePathValidation: &boolTrue,
  4699  				AdditionalAllowedBuckets:  []string{},
  4700  			}}},
  4701  			errExpected: false,
  4702  		},
  4703  		{
  4704  			name: "SkipStoragePathValidation true and AdditionalAllowedBuckets non-empty, err",
  4705  			config: &Config{ProwConfig: ProwConfig{Deck: Deck{
  4706  				SkipStoragePathValidation: &boolTrue,
  4707  				AdditionalAllowedBuckets: []string{
  4708  					"foo",
  4709  					"bar",
  4710  				},
  4711  			}}},
  4712  			errExpected: true,
  4713  		},
  4714  		{
  4715  			name: "SkipStoragePathValidation false and AdditionalAllowedBuckets non-empty, no err",
  4716  			config: &Config{ProwConfig: ProwConfig{Deck: Deck{
  4717  				SkipStoragePathValidation: &boolFalse,
  4718  				AdditionalAllowedBuckets: []string{
  4719  					"foo",
  4720  					"bar",
  4721  				},
  4722  			}}},
  4723  			errExpected: false,
  4724  		},
  4725  	}
  4726  
  4727  	for _, tc := range testCases {
  4728  		t.Run(tc.name, func(t *testing.T) {
  4729  			if hasErr := tc.config.validateComponentConfig() != nil; hasErr != tc.errExpected {
  4730  				t.Errorf("expected err: %t but was %t", tc.errExpected, hasErr)
  4731  			}
  4732  		})
  4733  	}
  4734  }
  4735  
  4736  func TestSlackReporterValidation(t *testing.T) {
  4737  	testCases := []struct {
  4738  		name            string
  4739  		config          func() Config
  4740  		successExpected bool
  4741  	}{
  4742  		{
  4743  			name: "Valid config w/ wildcard slack_reporter_configs - no error",
  4744  			config: func() Config {
  4745  				slackCfg := map[string]SlackReporter{
  4746  					"*": {
  4747  						SlackReporterConfig: prowapi.SlackReporterConfig{
  4748  							Channel: "my-channel",
  4749  						},
  4750  					},
  4751  				}
  4752  				return Config{
  4753  					ProwConfig: ProwConfig{
  4754  						SlackReporterConfigs: slackCfg,
  4755  					},
  4756  				}
  4757  			},
  4758  			successExpected: true,
  4759  		},
  4760  		{
  4761  			name: "Valid config w/ org/repo slack_reporter_configs - no error",
  4762  			config: func() Config {
  4763  				slackCfg := map[string]SlackReporter{
  4764  					"istio/proxy": {
  4765  						SlackReporterConfig: prowapi.SlackReporterConfig{
  4766  							Channel: "my-channel",
  4767  						},
  4768  					},
  4769  				}
  4770  				return Config{
  4771  					ProwConfig: ProwConfig{
  4772  						SlackReporterConfigs: slackCfg,
  4773  					},
  4774  				}
  4775  			},
  4776  			successExpected: true,
  4777  		},
  4778  		{
  4779  			name: "Valid config w/ repo slack_reporter_configs - no error",
  4780  			config: func() Config {
  4781  				slackCfg := map[string]SlackReporter{
  4782  					"proxy": {
  4783  						SlackReporterConfig: prowapi.SlackReporterConfig{
  4784  							Channel: "my-channel",
  4785  						},
  4786  					},
  4787  				}
  4788  				return Config{
  4789  					ProwConfig: ProwConfig{
  4790  						SlackReporterConfigs: slackCfg,
  4791  					},
  4792  				}
  4793  			},
  4794  			successExpected: true,
  4795  		},
  4796  		{
  4797  			name: "No channel w/ slack_reporter_configs - error",
  4798  			config: func() Config {
  4799  				slackCfg := map[string]SlackReporter{
  4800  					"*": {
  4801  						JobTypesToReport: []prowapi.ProwJobType{"presubmit"},
  4802  					},
  4803  				}
  4804  				return Config{
  4805  					ProwConfig: ProwConfig{
  4806  						SlackReporterConfigs: slackCfg,
  4807  					},
  4808  				}
  4809  			},
  4810  			successExpected: false,
  4811  		},
  4812  		{
  4813  			name: "Empty config - no error",
  4814  			config: func() Config {
  4815  				slackCfg := map[string]SlackReporter{}
  4816  				return Config{
  4817  					ProwConfig: ProwConfig{
  4818  						SlackReporterConfigs: slackCfg,
  4819  					},
  4820  				}
  4821  			},
  4822  			successExpected: true,
  4823  		},
  4824  		{
  4825  			name: "Invalid template - error",
  4826  			config: func() Config {
  4827  				slackCfg := map[string]SlackReporter{
  4828  					"*": {
  4829  						SlackReporterConfig: prowapi.SlackReporterConfig{
  4830  							Channel:        "my-channel",
  4831  							ReportTemplate: "{{ if .Spec.Name}}",
  4832  						},
  4833  					},
  4834  				}
  4835  				return Config{
  4836  					ProwConfig: ProwConfig{
  4837  						SlackReporterConfigs: slackCfg,
  4838  					},
  4839  				}
  4840  			},
  4841  			successExpected: false,
  4842  		},
  4843  		{
  4844  			name: "Template accessed invalid property - error",
  4845  			config: func() Config {
  4846  				slackCfg := map[string]SlackReporter{
  4847  					"*": {
  4848  						SlackReporterConfig: prowapi.SlackReporterConfig{
  4849  							Channel:        "my-channel",
  4850  							ReportTemplate: "{{ .Undef}}",
  4851  						},
  4852  					},
  4853  				}
  4854  				return Config{
  4855  					ProwConfig: ProwConfig{
  4856  						SlackReporterConfigs: slackCfg,
  4857  					},
  4858  				}
  4859  			},
  4860  			successExpected: false,
  4861  		},
  4862  	}
  4863  
  4864  	for _, tc := range testCases {
  4865  		t.Run(tc.name, func(t *testing.T) {
  4866  			cfg := tc.config()
  4867  			if err := cfg.validateComponentConfig(); (err == nil) != tc.successExpected {
  4868  				t.Errorf("Expected success=%t but got err=%v", tc.successExpected, err)
  4869  			}
  4870  			if tc.successExpected {
  4871  				for _, config := range cfg.SlackReporterConfigs {
  4872  					if config.ReportTemplate == "" {
  4873  						t.Errorf("expected default ReportTemplate to be set")
  4874  					}
  4875  					if config.Channel == "" {
  4876  						t.Errorf("expected Channel to be required")
  4877  					}
  4878  				}
  4879  			}
  4880  		})
  4881  	}
  4882  }
  4883  func TestManagedHmacEntityValidation(t *testing.T) {
  4884  	testCases := []struct {
  4885  		name       string
  4886  		prowConfig Config
  4887  		shouldFail bool
  4888  	}{
  4889  		{
  4890  			name:       "Missing managed HmacEntities",
  4891  			prowConfig: Config{ProwConfig: ProwConfig{ManagedWebhooks: ManagedWebhooks{}}},
  4892  			shouldFail: false,
  4893  		},
  4894  		{
  4895  			name: "Config with all valid dates",
  4896  			prowConfig: Config{ProwConfig: ProwConfig{
  4897  				ManagedWebhooks: ManagedWebhooks{
  4898  					OrgRepoConfig: map[string]ManagedWebhookInfo{
  4899  						"foo/bar": {TokenCreatedAfter: time.Now()},
  4900  						"foo/baz": {TokenCreatedAfter: time.Now()},
  4901  					},
  4902  				},
  4903  			}},
  4904  			shouldFail: false,
  4905  		},
  4906  		{
  4907  			name: "Config with one invalid dates",
  4908  			prowConfig: Config{ProwConfig: ProwConfig{
  4909  				ManagedWebhooks: ManagedWebhooks{
  4910  					OrgRepoConfig: map[string]ManagedWebhookInfo{
  4911  						"foo/bar": {TokenCreatedAfter: time.Now()},
  4912  						"foo/baz": {TokenCreatedAfter: time.Now().Add(time.Hour)},
  4913  					},
  4914  				},
  4915  			}},
  4916  			shouldFail: true,
  4917  		},
  4918  	}
  4919  	for _, tc := range testCases {
  4920  		t.Run(tc.name, func(t *testing.T) {
  4921  
  4922  			err := tc.prowConfig.validateComponentConfig()
  4923  			if tc.shouldFail != (err != nil) {
  4924  				t.Errorf("%s: Unexpected outcome. Error expected %v, Error found %s", tc.name, tc.shouldFail, err)
  4925  			}
  4926  
  4927  		})
  4928  	}
  4929  }
  4930  func TestValidateTriggering(t *testing.T) {
  4931  	testCases := []struct {
  4932  		name        string
  4933  		presubmit   Presubmit
  4934  		errExpected bool
  4935  	}{
  4936  		{
  4937  			name: "Trigger set, rerun command unset, err",
  4938  			presubmit: Presubmit{
  4939  				Trigger: "my-trigger",
  4940  				Reporter: Reporter{
  4941  					Context: "my-context",
  4942  				},
  4943  			},
  4944  			errExpected: true,
  4945  		},
  4946  		{
  4947  			name: "Triger unset, rerun command set, err",
  4948  			presubmit: Presubmit{
  4949  				RerunCommand: "my-rerun-command",
  4950  				Reporter: Reporter{
  4951  					Context: "my-context",
  4952  				},
  4953  			},
  4954  			errExpected: true,
  4955  		},
  4956  		{
  4957  			name: "Both trigger and rerun command set, no err",
  4958  			presubmit: Presubmit{
  4959  				Trigger:      "my-trigger",
  4960  				RerunCommand: "my-rerun-command",
  4961  				Reporter: Reporter{
  4962  					Context: "my-context",
  4963  				},
  4964  			},
  4965  			errExpected: false,
  4966  		},
  4967  	}
  4968  
  4969  	for _, tc := range testCases {
  4970  		t.Run(tc.name, func(t *testing.T) {
  4971  			err := validateTriggering(tc.presubmit)
  4972  			if err != nil != tc.errExpected {
  4973  				t.Errorf("Expected err: %t but got err %v", tc.errExpected, err)
  4974  			}
  4975  		})
  4976  	}
  4977  }
  4978  
  4979  func TestValidateAlwaysRunPostsubmit(t *testing.T) {
  4980  	true_ := true
  4981  	testCases := []struct {
  4982  		name        string
  4983  		postsubmit  Postsubmit
  4984  		errExpected bool
  4985  	}{
  4986  		{
  4987  			name: "both always_run and run_if_changed set, err",
  4988  			postsubmit: Postsubmit{
  4989  				AlwaysRun: &true_,
  4990  				RegexpChangeMatcher: RegexpChangeMatcher{
  4991  					RunIfChanged: `foo`,
  4992  				},
  4993  			},
  4994  			errExpected: true,
  4995  		},
  4996  		{
  4997  			name: "both always_run and skip_if_only_changed set, err",
  4998  			postsubmit: Postsubmit{
  4999  				AlwaysRun: &true_,
  5000  				RegexpChangeMatcher: RegexpChangeMatcher{
  5001  					SkipIfOnlyChanged: `foo`,
  5002  				},
  5003  			},
  5004  			errExpected: true,
  5005  		},
  5006  		{
  5007  			name: "both run_if_changed and skip_if_only_changed set, err",
  5008  			postsubmit: Postsubmit{
  5009  				RegexpChangeMatcher: RegexpChangeMatcher{
  5010  					RunIfChanged:      `foo`,
  5011  					SkipIfOnlyChanged: `foo`,
  5012  				},
  5013  			},
  5014  			errExpected: true,
  5015  		},
  5016  	}
  5017  
  5018  	for _, tc := range testCases {
  5019  		t.Run(tc.name, func(t *testing.T) {
  5020  			err := validateAlwaysRun(tc.postsubmit)
  5021  			if err != nil != tc.errExpected {
  5022  				t.Errorf("Expected err: %t but got err %v", tc.errExpected, err)
  5023  			}
  5024  		})
  5025  	}
  5026  }
  5027  
  5028  func TestRefGetterForGitHubPullRequest(t *testing.T) {
  5029  	testCases := []struct {
  5030  		name   string
  5031  		rg     *RefGetterForGitHubPullRequest
  5032  		verify func(*RefGetterForGitHubPullRequest) error
  5033  	}{
  5034  		{
  5035  			name: "Existing PullRequest is returned",
  5036  			rg:   &RefGetterForGitHubPullRequest{pr: &github.PullRequest{ID: 123456}},
  5037  			verify: func(rg *RefGetterForGitHubPullRequest) error {
  5038  				if rg.pr == nil || rg.pr.ID != 123456 {
  5039  					return fmt.Errorf("Expected refGetter to contain pr with id 123456, pr was %v", rg.pr)
  5040  				}
  5041  				return nil
  5042  			},
  5043  		},
  5044  		{
  5045  			name: "PullRequest is fetched, stored and returned",
  5046  			rg: &RefGetterForGitHubPullRequest{
  5047  				ghc: &fakegithub.FakeClient{
  5048  					PullRequests: map[int]*github.PullRequest{0: {ID: 123456}}},
  5049  			},
  5050  			verify: func(rg *RefGetterForGitHubPullRequest) error {
  5051  				pr, err := rg.PullRequest()
  5052  				if err != nil {
  5053  					return fmt.Errorf("failed to fetch PullRequest: %w", err)
  5054  				}
  5055  				if rg.pr == nil || rg.pr.ID != 123456 {
  5056  					return fmt.Errorf("expected agent to contain pr with id 123456, pr was %v", rg.pr)
  5057  				}
  5058  				if pr.ID != 123456 {
  5059  					return fmt.Errorf("expected returned pr.ID to be 123456, was %d", pr.ID)
  5060  				}
  5061  				return nil
  5062  			},
  5063  		},
  5064  		{
  5065  			name: "Existing baseSHA is returned",
  5066  			rg:   &RefGetterForGitHubPullRequest{baseSHA: "12345", pr: &github.PullRequest{}},
  5067  			verify: func(rg *RefGetterForGitHubPullRequest) error {
  5068  				baseSHA, err := rg.BaseSHA()
  5069  				if err != nil {
  5070  					return fmt.Errorf("error calling baseSHA: %w", err)
  5071  				}
  5072  				if rg.baseSHA != "12345" {
  5073  					return fmt.Errorf("expected agent baseSHA to be 12345, was %q", rg.baseSHA)
  5074  				}
  5075  				if baseSHA != "12345" {
  5076  					return fmt.Errorf("expected returned baseSHA to be 12345, was %q", baseSHA)
  5077  				}
  5078  				return nil
  5079  			},
  5080  		},
  5081  		{
  5082  			name: "BaseSHA is fetched, stored and returned",
  5083  			rg: &RefGetterForGitHubPullRequest{
  5084  				ghc: &fakegithub.FakeClient{
  5085  					PullRequests: map[int]*github.PullRequest{0: {}},
  5086  				},
  5087  			},
  5088  			verify: func(rg *RefGetterForGitHubPullRequest) error {
  5089  				baseSHA, err := rg.BaseSHA()
  5090  				if err != nil {
  5091  					return fmt.Errorf("expected err to be nil, was %w", err)
  5092  				}
  5093  				if rg.baseSHA != fakegithub.TestRef {
  5094  					return fmt.Errorf("expected baseSHA on agent to be %q, was %q", fakegithub.TestRef, rg.baseSHA)
  5095  				}
  5096  				if baseSHA != fakegithub.TestRef {
  5097  					return fmt.Errorf("expected returned baseSHA to be %q, was %q", fakegithub.TestRef, baseSHA)
  5098  				}
  5099  				return nil
  5100  			},
  5101  		},
  5102  	}
  5103  
  5104  	for _, tc := range testCases {
  5105  		t.Run(tc.name, func(t *testing.T) {
  5106  			tc.rg.lock = &sync.Mutex{}
  5107  			if err := tc.verify(tc.rg); err != nil {
  5108  				t.Fatal(err)
  5109  			}
  5110  		})
  5111  	}
  5112  }
  5113  
  5114  func TestFinalizeDefaultDecorationConfigs(t *testing.T) {
  5115  	tcs := []struct {
  5116  		name      string
  5117  		raw       string
  5118  		expected  []*DefaultDecorationConfigEntry
  5119  		expectErr bool
  5120  	}{
  5121  		{
  5122  			name:     "omitted config",
  5123  			raw:      "deck:",
  5124  			expected: nil,
  5125  		},
  5126  		{
  5127  			name: "old format; global only",
  5128  			raw: `
  5129  default_decoration_configs:
  5130    '*':
  5131      timeout: 2h
  5132      grace_period: 15s
  5133      utility_images:
  5134        clonerefs: "clonerefs:default"
  5135        initupload: "initupload:default"
  5136        entrypoint: "entrypoint:default"
  5137        sidecar: "sidecar:default"
  5138      gcs_configuration:
  5139        bucket: "default-bucket"
  5140        path_strategy: "legacy"
  5141        default_org: "kubernetes"
  5142        default_repo: "kubernetes"
  5143      gcs_credentials_secret: "default-service-account"
  5144  `,
  5145  			expected: []*DefaultDecorationConfigEntry{
  5146  				{
  5147  					OrgRepo: "*",
  5148  					Cluster: "",
  5149  					Config: &prowapi.DecorationConfig{
  5150  						Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
  5151  						GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  5152  						UtilityImages: &prowapi.UtilityImages{
  5153  							CloneRefs:  "clonerefs:default",
  5154  							InitUpload: "initupload:default",
  5155  							Entrypoint: "entrypoint:default",
  5156  							Sidecar:    "sidecar:default",
  5157  						},
  5158  						GCSConfiguration: &prowapi.GCSConfiguration{
  5159  							Bucket:       "default-bucket",
  5160  							PathStrategy: prowapi.PathStrategyLegacy,
  5161  							DefaultOrg:   "kubernetes",
  5162  							DefaultRepo:  "kubernetes",
  5163  						},
  5164  						GCSCredentialsSecret: pStr("default-service-account"),
  5165  					},
  5166  				},
  5167  			},
  5168  		},
  5169  		{
  5170  			name: "old format; org repo ordered",
  5171  			raw: `
  5172  default_decoration_configs:
  5173    '*':
  5174      timeout: 2h
  5175      grace_period: 15s
  5176      utility_images:
  5177        clonerefs: "clonerefs:default"
  5178        initupload: "initupload:default"
  5179        entrypoint: "entrypoint:default"
  5180        sidecar: "sidecar:default"
  5181      gcs_configuration:
  5182        bucket: "default-bucket"
  5183        path_strategy: "legacy"
  5184        default_org: "kubernetes"
  5185        default_repo: "kubernetes"
  5186      gcs_credentials_secret: "default-service-account"
  5187    'org/repo':
  5188      timeout: 1h
  5189    'org':
  5190      timeout: 3h
  5191  `,
  5192  			expected: []*DefaultDecorationConfigEntry{
  5193  				{
  5194  					OrgRepo: "*",
  5195  					Cluster: "",
  5196  					Config: &prowapi.DecorationConfig{
  5197  						Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
  5198  						GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  5199  						UtilityImages: &prowapi.UtilityImages{
  5200  							CloneRefs:  "clonerefs:default",
  5201  							InitUpload: "initupload:default",
  5202  							Entrypoint: "entrypoint:default",
  5203  							Sidecar:    "sidecar:default",
  5204  						},
  5205  						GCSConfiguration: &prowapi.GCSConfiguration{
  5206  							Bucket:       "default-bucket",
  5207  							PathStrategy: prowapi.PathStrategyLegacy,
  5208  							DefaultOrg:   "kubernetes",
  5209  							DefaultRepo:  "kubernetes",
  5210  						},
  5211  						GCSCredentialsSecret: pStr("default-service-account"),
  5212  					},
  5213  				},
  5214  				{
  5215  					OrgRepo: "org",
  5216  					Cluster: "",
  5217  					Config: &prowapi.DecorationConfig{
  5218  						Timeout: &prowapi.Duration{Duration: 3 * time.Hour},
  5219  					},
  5220  				},
  5221  				{
  5222  					OrgRepo: "org/repo",
  5223  					Cluster: "",
  5224  					Config: &prowapi.DecorationConfig{
  5225  						Timeout: &prowapi.Duration{Duration: 1 * time.Hour},
  5226  					},
  5227  				},
  5228  			},
  5229  		},
  5230  		{
  5231  			name: "new format; global only",
  5232  			raw: `
  5233  default_decoration_config_entries:
  5234    - config:
  5235        timeout: 2h
  5236        grace_period: 15s
  5237        utility_images:
  5238          clonerefs: "clonerefs:default"
  5239          initupload: "initupload:default"
  5240          entrypoint: "entrypoint:default"
  5241          sidecar: "sidecar:default"
  5242        gcs_configuration:
  5243          bucket: "default-bucket"
  5244          path_strategy: "legacy"
  5245          default_org: "kubernetes"
  5246          default_repo: "kubernetes"
  5247        gcs_credentials_secret: "default-service-account"
  5248  `,
  5249  			expected: []*DefaultDecorationConfigEntry{
  5250  				{
  5251  					OrgRepo: "",
  5252  					Cluster: "",
  5253  					Config: &prowapi.DecorationConfig{
  5254  						Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
  5255  						GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  5256  						UtilityImages: &prowapi.UtilityImages{
  5257  							CloneRefs:  "clonerefs:default",
  5258  							InitUpload: "initupload:default",
  5259  							Entrypoint: "entrypoint:default",
  5260  							Sidecar:    "sidecar:default",
  5261  						},
  5262  						GCSConfiguration: &prowapi.GCSConfiguration{
  5263  							Bucket:       "default-bucket",
  5264  							PathStrategy: prowapi.PathStrategyLegacy,
  5265  							DefaultOrg:   "kubernetes",
  5266  							DefaultRepo:  "kubernetes",
  5267  						},
  5268  						GCSCredentialsSecret: pStr("default-service-account"),
  5269  					},
  5270  				},
  5271  			},
  5272  		},
  5273  		{
  5274  			name: "new format; global, org, repo, cluster, org+cluster",
  5275  			raw: `
  5276  default_decoration_config_entries:
  5277    - config:
  5278        timeout: 2h
  5279        grace_period: 15s
  5280        utility_images:
  5281          clonerefs: "clonerefs:default"
  5282          initupload: "initupload:default"
  5283          entrypoint: "entrypoint:default"
  5284          sidecar: "sidecar:default"
  5285        gcs_configuration:
  5286          bucket: "default-bucket"
  5287          path_strategy: "legacy"
  5288          default_org: "kubernetes"
  5289          default_repo: "kubernetes"
  5290        gcs_credentials_secret: "default-service-account"
  5291    - repo: "org"
  5292      cluster: "*"
  5293      config:
  5294        timeout: 1h
  5295    - repo: "org/repo"
  5296      config:
  5297        timeout: 3h
  5298    - cluster: "trusted"
  5299      config:
  5300        grace_period: 30s
  5301    - repo: "org/foo"
  5302      cluster: "trusted"
  5303      config:
  5304        grace_period: 1m
  5305  `,
  5306  			expected: []*DefaultDecorationConfigEntry{
  5307  				{
  5308  					OrgRepo: "",
  5309  					Cluster: "",
  5310  					Config: &prowapi.DecorationConfig{
  5311  						Timeout:     &prowapi.Duration{Duration: 2 * time.Hour},
  5312  						GracePeriod: &prowapi.Duration{Duration: 15 * time.Second},
  5313  						UtilityImages: &prowapi.UtilityImages{
  5314  							CloneRefs:  "clonerefs:default",
  5315  							InitUpload: "initupload:default",
  5316  							Entrypoint: "entrypoint:default",
  5317  							Sidecar:    "sidecar:default",
  5318  						},
  5319  						GCSConfiguration: &prowapi.GCSConfiguration{
  5320  							Bucket:       "default-bucket",
  5321  							PathStrategy: prowapi.PathStrategyLegacy,
  5322  							DefaultOrg:   "kubernetes",
  5323  							DefaultRepo:  "kubernetes",
  5324  						},
  5325  						GCSCredentialsSecret: pStr("default-service-account"),
  5326  					},
  5327  				},
  5328  				{
  5329  					OrgRepo: "org",
  5330  					Cluster: "*",
  5331  					Config: &prowapi.DecorationConfig{
  5332  						Timeout: &prowapi.Duration{Duration: 1 * time.Hour},
  5333  					},
  5334  				},
  5335  				{
  5336  					OrgRepo: "org/repo",
  5337  					Cluster: "",
  5338  					Config: &prowapi.DecorationConfig{
  5339  						Timeout: &prowapi.Duration{Duration: 3 * time.Hour},
  5340  					},
  5341  				},
  5342  				{
  5343  					OrgRepo: "",
  5344  					Cluster: "trusted",
  5345  					Config: &prowapi.DecorationConfig{
  5346  						GracePeriod: &prowapi.Duration{Duration: 30 * time.Second},
  5347  					},
  5348  				},
  5349  				{
  5350  					OrgRepo: "org/foo",
  5351  					Cluster: "trusted",
  5352  					Config: &prowapi.DecorationConfig{
  5353  						GracePeriod: &prowapi.Duration{Duration: 1 * time.Minute},
  5354  					},
  5355  				},
  5356  			},
  5357  		},
  5358  		{
  5359  			name: "org, repo, cluster specific timeouts",
  5360  			raw: `
  5361  default_decoration_config_entries:
  5362    - repo: "org"
  5363      config:
  5364        pod_running_timeout: 3h
  5365        pod_pending_timeout: 2h
  5366        pod_unscheduled_timeout: 1h
  5367    - repo: "org/repo"
  5368      config:
  5369        pod_running_timeout: 2h
  5370        pod_pending_timeout: 1h
  5371        pod_unscheduled_timeout: 3h
  5372    - repo: "org/foo"
  5373      config:
  5374        pod_running_timeout: 1h
  5375        pod_pending_timeout: 2h
  5376        pod_unscheduled_timeout: 3h
  5377    - cluster: "trusted"
  5378      config:
  5379        pod_running_timeout: 30m
  5380        pod_pending_timeout: 45m
  5381        pod_unscheduled_timeout: 15m
  5382  `,
  5383  			expected: []*DefaultDecorationConfigEntry{
  5384  				{
  5385  					OrgRepo: "org",
  5386  					Cluster: "",
  5387  					Config: &prowapi.DecorationConfig{
  5388  						PodRunningTimeout:     &metav1.Duration{Duration: 3 * time.Hour},
  5389  						PodPendingTimeout:     &metav1.Duration{Duration: 2 * time.Hour},
  5390  						PodUnscheduledTimeout: &metav1.Duration{Duration: 1 * time.Hour},
  5391  					},
  5392  				},
  5393  				{
  5394  					OrgRepo: "org/repo",
  5395  					Cluster: "",
  5396  					Config: &prowapi.DecorationConfig{
  5397  						PodRunningTimeout:     &metav1.Duration{Duration: 2 * time.Hour},
  5398  						PodPendingTimeout:     &metav1.Duration{Duration: 1 * time.Hour},
  5399  						PodUnscheduledTimeout: &metav1.Duration{Duration: 3 * time.Hour},
  5400  					},
  5401  				},
  5402  				{
  5403  					OrgRepo: "org/foo",
  5404  					Cluster: "",
  5405  					Config: &prowapi.DecorationConfig{
  5406  						PodRunningTimeout:     &metav1.Duration{Duration: 1 * time.Hour},
  5407  						PodPendingTimeout:     &metav1.Duration{Duration: 2 * time.Hour},
  5408  						PodUnscheduledTimeout: &metav1.Duration{Duration: 3 * time.Hour},
  5409  					},
  5410  				},
  5411  				{
  5412  					OrgRepo: "",
  5413  					Cluster: "trusted",
  5414  					Config: &prowapi.DecorationConfig{
  5415  						PodRunningTimeout:     &metav1.Duration{Duration: 30 * time.Minute},
  5416  						PodPendingTimeout:     &metav1.Duration{Duration: 45 * time.Minute},
  5417  						PodUnscheduledTimeout: &metav1.Duration{Duration: 15 * time.Minute},
  5418  					},
  5419  				},
  5420  			},
  5421  		},
  5422  		{
  5423  			name: "both formats, expect error",
  5424  			raw: `
  5425  default_decoration_configs:
  5426    "*":
  5427      timeout: 1h
  5428      grace_period: 15s
  5429      utility_images:
  5430        clonerefs: "clonerefs:default"
  5431        initupload: "initupload:default"
  5432        entrypoint: "entrypoint:default"
  5433        sidecar: "sidecar:default"
  5434      gcs_configuration:
  5435        bucket: "default-bucket"
  5436        path_strategy: "legacy"
  5437        default_org: "kubernetes"
  5438        default_repo: "kubernetes"
  5439      gcs_credentials_secret: "default-service-account"
  5440  
  5441  default_decoration_config_entries:
  5442    - config:
  5443        timeout: 2h
  5444        grace_period: 15s
  5445        utility_images:
  5446          clonerefs: "clonerefs:default"
  5447          initupload: "initupload:default"
  5448          entrypoint: "entrypoint:default"
  5449          sidecar: "sidecar:default"
  5450        gcs_configuration:
  5451          bucket: "default-bucket"
  5452          path_strategy: "legacy"
  5453          default_org: "kubernetes"
  5454          default_repo: "kubernetes"
  5455        gcs_credentials_secret: "default-service-account"
  5456  `,
  5457  			expectErr: true,
  5458  		},
  5459  	}
  5460  
  5461  	for i := range tcs {
  5462  		tc := tcs[i]
  5463  		t.Run(tc.name, func(t *testing.T) {
  5464  			t.Parallel()
  5465  			p := Plank{}
  5466  			if err := yaml.Unmarshal([]byte(tc.raw), &p); err != nil {
  5467  				t.Errorf("error unmarshaling: %v", err)
  5468  			}
  5469  			if err := p.FinalizeDefaultDecorationConfigs(); err != nil && !tc.expectErr {
  5470  				t.Errorf("unexpected error finalizing DefaultDecorationConfigs: %v", err)
  5471  			} else if err == nil && tc.expectErr {
  5472  				t.Error("expected error, but did not receive one")
  5473  			}
  5474  			if diff := cmp.Diff(tc.expected, p.DefaultDecorationConfigs, cmpopts.IgnoreUnexported(regexp.Regexp{})); diff != "" {
  5475  				t.Errorf("expected result diff: %s", diff)
  5476  			}
  5477  		})
  5478  	}
  5479  }
  5480  
  5481  // complexConfig is shared by multiple test cases that test DefaultDecorationConfig
  5482  // merging logic. It configures the upload bucket based on the org/repo and
  5483  // uses either a GCS secret or k8s SA depending on the cluster.
  5484  // A specific 'override' org overrides some fields in the trusted cluster only.
  5485  func complexConfig() *Config {
  5486  	return &Config{
  5487  		JobConfig: JobConfig{
  5488  			DecorateAllJobs: true,
  5489  		},
  5490  		ProwConfig: ProwConfig{
  5491  			Plank: Plank{
  5492  				DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  5493  					{
  5494  						OrgRepo: "*",
  5495  						Cluster: "*",
  5496  						Config: &prowapi.DecorationConfig{
  5497  							UtilityImages: &prowapi.UtilityImages{
  5498  								CloneRefs:  "clonerefs:global",
  5499  								InitUpload: "initupload:global",
  5500  								Entrypoint: "entrypoint:global",
  5501  								Sidecar:    "sidecar:global",
  5502  							},
  5503  							GCSConfiguration: &prowapi.GCSConfiguration{
  5504  								Bucket:       "global",
  5505  								PathStrategy: "explicit",
  5506  							},
  5507  						},
  5508  					},
  5509  					{
  5510  						OrgRepo: "org",
  5511  						Cluster: "*",
  5512  						Config: &prowapi.DecorationConfig{
  5513  							GCSConfiguration: &prowapi.GCSConfiguration{
  5514  								Bucket:       "org-specific",
  5515  								PathStrategy: "explicit",
  5516  							},
  5517  						},
  5518  					},
  5519  					{
  5520  						OrgRepo: "org/repo",
  5521  						Cluster: "*",
  5522  						Config: &prowapi.DecorationConfig{
  5523  							GCSConfiguration: &prowapi.GCSConfiguration{
  5524  								Bucket:       "repo-specific",
  5525  								PathStrategy: "explicit",
  5526  							},
  5527  						},
  5528  					},
  5529  					{
  5530  						OrgRepo: "*",
  5531  						Cluster: "default",
  5532  						Config: &prowapi.DecorationConfig{
  5533  							GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  5534  						},
  5535  					},
  5536  					{
  5537  						OrgRepo: "*",
  5538  						Cluster: "trusted",
  5539  						Config: &prowapi.DecorationConfig{
  5540  							DefaultServiceAccountName: pStr("trusted-cluster-uses-SA"),
  5541  						},
  5542  					},
  5543  					{
  5544  						OrgRepo: "override",
  5545  						Cluster: "trusted",
  5546  						Config: &prowapi.DecorationConfig{
  5547  							UtilityImages: &prowapi.UtilityImages{
  5548  								CloneRefs: "clonerefs:override",
  5549  							},
  5550  							DefaultServiceAccountName: pStr(""),
  5551  							GCSCredentialsSecret:      pStr("trusted-cluster-override-uses-secret"),
  5552  						},
  5553  					},
  5554  				},
  5555  			},
  5556  		},
  5557  	}
  5558  }
  5559  
  5560  // TODO(mpherman): Add more detailed unit test when there is more than 1 field in ProwJobDefaults
  5561  // Need unit tests for merging more complicated defaults.
  5562  func TestSetPeriodicProwJobDefaults(t *testing.T) {
  5563  	testCases := []struct {
  5564  		id            string
  5565  		utilityConfig UtilityConfig
  5566  		cluster       string
  5567  		config        *Config
  5568  		givenDefault  *prowapi.ProwJobDefault
  5569  		expected      *prowapi.ProwJobDefault
  5570  	}{
  5571  		{
  5572  			id:       "No ProwJobDefault in job or in config, expect DefaultTenantID",
  5573  			config:   &Config{ProwConfig: ProwConfig{}},
  5574  			expected: &prowapi.ProwJobDefault{TenantID: DefaultTenantID},
  5575  		},
  5576  		{
  5577  			id: "no default in job or in config's by repo config, expect default entry",
  5578  			config: &Config{
  5579  				ProwConfig: ProwConfig{
  5580  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5581  						{
  5582  							OrgRepo: "*",
  5583  							Cluster: "",
  5584  							Config: &prowapi.ProwJobDefault{
  5585  								TenantID: "configDefault",
  5586  							},
  5587  						},
  5588  					},
  5589  				},
  5590  			},
  5591  			expected: &prowapi.ProwJobDefault{
  5592  				TenantID: "configDefault",
  5593  			},
  5594  		},
  5595  		{
  5596  			id: "no default in presubmit, matching by repo config, expect merged by repo config",
  5597  			utilityConfig: UtilityConfig{
  5598  				ExtraRefs: []prowapi.Refs{
  5599  					{
  5600  						Org:  "org",
  5601  						Repo: "repo",
  5602  					},
  5603  				},
  5604  			},
  5605  			config: &Config{
  5606  				ProwConfig: ProwConfig{
  5607  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5608  						{
  5609  							OrgRepo: "*",
  5610  							Cluster: "",
  5611  							Config: &prowapi.ProwJobDefault{
  5612  								TenantID: "configDefault",
  5613  							},
  5614  						},
  5615  						{
  5616  							OrgRepo: "org/repo",
  5617  							Cluster: "",
  5618  							Config: &prowapi.ProwJobDefault{
  5619  								TenantID: "org/repo default",
  5620  							},
  5621  						},
  5622  					},
  5623  				},
  5624  			},
  5625  			expected: &prowapi.ProwJobDefault{
  5626  				TenantID: "org/repo default",
  5627  			},
  5628  		},
  5629  		{
  5630  			id: "default in job and config's defaults, expect job's default",
  5631  			utilityConfig: UtilityConfig{
  5632  				ExtraRefs: []prowapi.Refs{
  5633  					{
  5634  						Org:  "org",
  5635  						Repo: "repo",
  5636  					},
  5637  				},
  5638  			},
  5639  			givenDefault: &prowapi.ProwJobDefault{
  5640  				TenantID: "given default",
  5641  			},
  5642  			config: &Config{
  5643  				ProwConfig: ProwConfig{
  5644  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5645  						{
  5646  							OrgRepo: "*",
  5647  							Cluster: "",
  5648  							Config: &prowapi.ProwJobDefault{
  5649  								TenantID: "config Default",
  5650  							},
  5651  						},
  5652  						{
  5653  							OrgRepo: "org/repo",
  5654  							Cluster: "",
  5655  							Config: &prowapi.ProwJobDefault{
  5656  								TenantID: "org/repo default",
  5657  							},
  5658  						},
  5659  					},
  5660  				},
  5661  			},
  5662  			expected: &prowapi.ProwJobDefault{
  5663  				TenantID: "given default",
  5664  			},
  5665  		},
  5666  		{
  5667  			id: "no default in job. config's default by org, expect org's default",
  5668  			utilityConfig: UtilityConfig{
  5669  				ExtraRefs: []prowapi.Refs{
  5670  					{
  5671  						Org:  "org",
  5672  						Repo: "repo",
  5673  					},
  5674  				},
  5675  			},
  5676  			config: &Config{
  5677  				ProwConfig: ProwConfig{
  5678  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5679  						{
  5680  							OrgRepo: "*",
  5681  							Cluster: "",
  5682  							Config: &prowapi.ProwJobDefault{
  5683  								TenantID: "configDefault",
  5684  							},
  5685  						},
  5686  						{
  5687  							OrgRepo: "org",
  5688  							Cluster: "",
  5689  							Config: &prowapi.ProwJobDefault{
  5690  								TenantID: "org default",
  5691  							},
  5692  						},
  5693  					},
  5694  				},
  5695  			},
  5696  			expected: &prowapi.ProwJobDefault{
  5697  				TenantID: "org default",
  5698  			},
  5699  		},
  5700  		{
  5701  			id: "no default in job or in config's by repo config, expect default entry with repo provided",
  5702  			utilityConfig: UtilityConfig{
  5703  				ExtraRefs: []prowapi.Refs{
  5704  					{
  5705  						Org:  "org",
  5706  						Repo: "repo",
  5707  					},
  5708  				},
  5709  			},
  5710  			config: &Config{
  5711  				ProwConfig: ProwConfig{
  5712  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5713  						{
  5714  							OrgRepo: "*",
  5715  							Cluster: "",
  5716  							Config: &prowapi.ProwJobDefault{
  5717  								TenantID: "configDefault",
  5718  							},
  5719  						},
  5720  					},
  5721  				},
  5722  			},
  5723  			expected: &prowapi.ProwJobDefault{
  5724  				TenantID: "configDefault",
  5725  			},
  5726  		},
  5727  		{
  5728  			id: "no default in job. config's default by org and org/repo, expect org/repo default",
  5729  			utilityConfig: UtilityConfig{
  5730  				ExtraRefs: []prowapi.Refs{
  5731  					{
  5732  						Org:  "org",
  5733  						Repo: "repo",
  5734  					},
  5735  				},
  5736  			},
  5737  			config: &Config{
  5738  				ProwConfig: ProwConfig{
  5739  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5740  						{
  5741  							OrgRepo: "*",
  5742  							Cluster: "",
  5743  							Config: &prowapi.ProwJobDefault{
  5744  								TenantID: "configDefault",
  5745  							},
  5746  						},
  5747  						{
  5748  							OrgRepo: "org",
  5749  							Cluster: "",
  5750  							Config: &prowapi.ProwJobDefault{
  5751  								TenantID: "org default",
  5752  							},
  5753  						},
  5754  						{
  5755  							OrgRepo: "org/repo",
  5756  							Cluster: "",
  5757  							Config: &prowapi.ProwJobDefault{
  5758  								TenantID: "org/repo default",
  5759  							},
  5760  						},
  5761  					},
  5762  				},
  5763  			},
  5764  			expected: &prowapi.ProwJobDefault{
  5765  				TenantID: "org/repo default",
  5766  			},
  5767  		},
  5768  		{
  5769  			id: "no default in job. config's default by org and org/repo, unknown repo uses org",
  5770  			utilityConfig: UtilityConfig{
  5771  				ExtraRefs: []prowapi.Refs{
  5772  					{
  5773  						Org:  "org",
  5774  						Repo: "foo",
  5775  					},
  5776  				},
  5777  			},
  5778  			config: &Config{
  5779  				ProwConfig: ProwConfig{
  5780  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5781  						{
  5782  							OrgRepo: "*",
  5783  							Cluster: "",
  5784  							Config: &prowapi.ProwJobDefault{
  5785  								TenantID: "configDefault",
  5786  							},
  5787  						},
  5788  						{
  5789  							OrgRepo: "org",
  5790  							Cluster: "",
  5791  							Config: &prowapi.ProwJobDefault{
  5792  								TenantID: "org default",
  5793  							},
  5794  						},
  5795  						{
  5796  							OrgRepo: "org/repo",
  5797  							Cluster: "",
  5798  							Config: &prowapi.ProwJobDefault{
  5799  								TenantID: "org/repo default",
  5800  							},
  5801  						},
  5802  					},
  5803  				},
  5804  			},
  5805  			expected: &prowapi.ProwJobDefault{
  5806  				TenantID: "org default",
  5807  			},
  5808  		},
  5809  		{
  5810  			id: "no default in job. * cluster provided config's default by org and org/repo, unknown repo uses org",
  5811  			utilityConfig: UtilityConfig{
  5812  				ExtraRefs: []prowapi.Refs{
  5813  					{
  5814  						Org:  "org",
  5815  						Repo: "foo",
  5816  					},
  5817  				},
  5818  			},
  5819  			cluster: "default",
  5820  			config: &Config{
  5821  				ProwConfig: ProwConfig{
  5822  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5823  						{
  5824  							OrgRepo: "*",
  5825  							Cluster: "*",
  5826  							Config: &prowapi.ProwJobDefault{
  5827  								TenantID: "configDefault",
  5828  							},
  5829  						},
  5830  						{
  5831  							OrgRepo: "org",
  5832  							Cluster: "",
  5833  							Config: &prowapi.ProwJobDefault{
  5834  								TenantID: "org default",
  5835  							},
  5836  						},
  5837  						{
  5838  							OrgRepo: "org/repo",
  5839  							Cluster: "",
  5840  							Config: &prowapi.ProwJobDefault{
  5841  								TenantID: "org/repo default",
  5842  							},
  5843  						},
  5844  					},
  5845  				},
  5846  			},
  5847  			expected: &prowapi.ProwJobDefault{
  5848  				TenantID: "org default",
  5849  			},
  5850  		},
  5851  		{
  5852  			id: "override cluster",
  5853  			utilityConfig: UtilityConfig{
  5854  				ExtraRefs: []prowapi.Refs{
  5855  					{
  5856  						Org:  "org",
  5857  						Repo: "foo",
  5858  					},
  5859  				},
  5860  			},
  5861  			cluster: "override",
  5862  			config: &Config{
  5863  				ProwConfig: ProwConfig{
  5864  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5865  						{
  5866  							OrgRepo: "*",
  5867  							Cluster: "*",
  5868  							Config: &prowapi.ProwJobDefault{
  5869  								TenantID: "configDefault",
  5870  							},
  5871  						},
  5872  						{
  5873  							OrgRepo: "org",
  5874  							Cluster: "",
  5875  							Config: &prowapi.ProwJobDefault{
  5876  								TenantID: "org default",
  5877  							},
  5878  						},
  5879  						{
  5880  							OrgRepo: "*",
  5881  							Cluster: "override",
  5882  							Config: &prowapi.ProwJobDefault{
  5883  								TenantID: "override default",
  5884  							},
  5885  						},
  5886  					},
  5887  				},
  5888  			},
  5889  			expected: &prowapi.ProwJobDefault{
  5890  				TenantID: "override default",
  5891  			},
  5892  		},
  5893  		{
  5894  			id: "complicated config, but use provided config",
  5895  			utilityConfig: UtilityConfig{
  5896  				ExtraRefs: []prowapi.Refs{
  5897  					{
  5898  						Org:  "org",
  5899  						Repo: "foo",
  5900  					},
  5901  				},
  5902  			},
  5903  			cluster: "override",
  5904  			config: &Config{
  5905  				ProwConfig: ProwConfig{
  5906  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5907  						{
  5908  							OrgRepo: "*",
  5909  							Cluster: "*",
  5910  							Config: &prowapi.ProwJobDefault{
  5911  								TenantID: "configDefault",
  5912  							},
  5913  						},
  5914  						{
  5915  							OrgRepo: "org",
  5916  							Cluster: "",
  5917  							Config: &prowapi.ProwJobDefault{
  5918  								TenantID: "org default",
  5919  							},
  5920  						},
  5921  						{
  5922  							OrgRepo: "*",
  5923  							Cluster: "override",
  5924  							Config: &prowapi.ProwJobDefault{
  5925  								TenantID: "override default",
  5926  							},
  5927  						},
  5928  					},
  5929  				},
  5930  			},
  5931  			givenDefault: &prowapi.ProwJobDefault{
  5932  				TenantID: "given default",
  5933  			},
  5934  			expected: &prowapi.ProwJobDefault{
  5935  				TenantID: "given default",
  5936  			},
  5937  		},
  5938  	}
  5939  	for _, tc := range testCases {
  5940  		t.Run(tc.id, func(t *testing.T) {
  5941  			c := &Config{}
  5942  			periodic := &Periodic{JobBase: JobBase{Cluster: tc.cluster, UtilityConfig: tc.utilityConfig, ProwJobDefault: tc.givenDefault}}
  5943  			c.defaultJobBase(&periodic.JobBase)
  5944  			setPeriodicProwJobDefaults(tc.config, periodic)
  5945  			if diff := cmp.Diff(periodic.ProwJobDefault, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  5946  				t.Error(diff)
  5947  			}
  5948  		})
  5949  	}
  5950  }
  5951  
  5952  // TODO(mpherman): Add more detailed unit test when there is more than 1 field in ProwJobDefaults
  5953  // Need unit tests for merging more complicated defaults.
  5954  func TestSetProwJobDefaults(t *testing.T) {
  5955  	testCases := []struct {
  5956  		id           string
  5957  		repo         string
  5958  		cluster      string
  5959  		config       *Config
  5960  		givenDefault *prowapi.ProwJobDefault
  5961  		expected     *prowapi.ProwJobDefault
  5962  	}{
  5963  		{
  5964  			id:       "No ProwJobDefault in job or in config, expect DefaultTenantID",
  5965  			config:   &Config{ProwConfig: ProwConfig{}},
  5966  			expected: &prowapi.ProwJobDefault{TenantID: DefaultTenantID},
  5967  		},
  5968  		{
  5969  			id: "no default in job or in config's by repo config, expect default entry",
  5970  			config: &Config{
  5971  				ProwConfig: ProwConfig{
  5972  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5973  						{
  5974  							OrgRepo: "*",
  5975  							Cluster: "",
  5976  							Config: &prowapi.ProwJobDefault{
  5977  								TenantID: "configDefault",
  5978  							},
  5979  						},
  5980  					},
  5981  				},
  5982  			},
  5983  			expected: &prowapi.ProwJobDefault{
  5984  				TenantID: "configDefault",
  5985  			},
  5986  		},
  5987  		{
  5988  			id:   "no default in presubmit, matching by repo config, expect merged by repo config",
  5989  			repo: "org/repo",
  5990  			config: &Config{
  5991  				ProwConfig: ProwConfig{
  5992  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  5993  						{
  5994  							OrgRepo: "*",
  5995  							Cluster: "",
  5996  							Config: &prowapi.ProwJobDefault{
  5997  								TenantID: "configDefault",
  5998  							},
  5999  						},
  6000  						{
  6001  							OrgRepo: "org/repo",
  6002  							Cluster: "",
  6003  							Config: &prowapi.ProwJobDefault{
  6004  								TenantID: "org/repo default",
  6005  							},
  6006  						},
  6007  					},
  6008  				},
  6009  			},
  6010  			expected: &prowapi.ProwJobDefault{
  6011  				TenantID: "org/repo default",
  6012  			},
  6013  		},
  6014  		{
  6015  			id:   "default in job and config's defaults, expect job's default",
  6016  			repo: "org/repo",
  6017  			givenDefault: &prowapi.ProwJobDefault{
  6018  				TenantID: "given default",
  6019  			},
  6020  			config: &Config{
  6021  				ProwConfig: ProwConfig{
  6022  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6023  						{
  6024  							OrgRepo: "*",
  6025  							Cluster: "",
  6026  							Config: &prowapi.ProwJobDefault{
  6027  								TenantID: "configDefault",
  6028  							},
  6029  						},
  6030  						{
  6031  							OrgRepo: "org/repo",
  6032  							Cluster: "",
  6033  							Config: &prowapi.ProwJobDefault{
  6034  								TenantID: "org/repo default",
  6035  							},
  6036  						},
  6037  					},
  6038  				},
  6039  			},
  6040  			expected: &prowapi.ProwJobDefault{
  6041  				TenantID: "given default",
  6042  			},
  6043  		},
  6044  		{
  6045  			id:   "no default in job. config's default by org, expect org's default",
  6046  			repo: "org/repo",
  6047  			config: &Config{
  6048  				ProwConfig: ProwConfig{
  6049  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6050  						{
  6051  							OrgRepo: "*",
  6052  							Cluster: "",
  6053  							Config: &prowapi.ProwJobDefault{
  6054  								TenantID: "configDefault",
  6055  							},
  6056  						},
  6057  						{
  6058  							OrgRepo: "org",
  6059  							Cluster: "",
  6060  							Config: &prowapi.ProwJobDefault{
  6061  								TenantID: "org default",
  6062  							},
  6063  						},
  6064  					},
  6065  				},
  6066  			},
  6067  			expected: &prowapi.ProwJobDefault{
  6068  				TenantID: "org default",
  6069  			},
  6070  		},
  6071  		{
  6072  			id:   "no default in job or in config's by repo config, expect default entry with repo provided",
  6073  			repo: "org/repo",
  6074  			config: &Config{
  6075  				ProwConfig: ProwConfig{
  6076  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6077  						{
  6078  							OrgRepo: "*",
  6079  							Cluster: "",
  6080  							Config: &prowapi.ProwJobDefault{
  6081  								TenantID: "configDefault",
  6082  							},
  6083  						},
  6084  					},
  6085  				},
  6086  			},
  6087  			expected: &prowapi.ProwJobDefault{
  6088  				TenantID: "configDefault",
  6089  			},
  6090  		},
  6091  		{
  6092  			id:   "no default in job. config's default by org and org/repo, expect org/repo default",
  6093  			repo: "org/repo",
  6094  			config: &Config{
  6095  				ProwConfig: ProwConfig{
  6096  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6097  						{
  6098  							OrgRepo: "*",
  6099  							Cluster: "",
  6100  							Config: &prowapi.ProwJobDefault{
  6101  								TenantID: "configDefault",
  6102  							},
  6103  						},
  6104  						{
  6105  							OrgRepo: "org",
  6106  							Cluster: "",
  6107  							Config: &prowapi.ProwJobDefault{
  6108  								TenantID: "org default",
  6109  							},
  6110  						},
  6111  						{
  6112  							OrgRepo: "org/repo",
  6113  							Cluster: "",
  6114  							Config: &prowapi.ProwJobDefault{
  6115  								TenantID: "org/repo default",
  6116  							},
  6117  						},
  6118  					},
  6119  				},
  6120  			},
  6121  			expected: &prowapi.ProwJobDefault{
  6122  				TenantID: "org/repo default",
  6123  			},
  6124  		},
  6125  		{
  6126  			id:   "no default in job. config's default by org and org/repo, unknown repo uses org",
  6127  			repo: "org/foo",
  6128  			config: &Config{
  6129  				ProwConfig: ProwConfig{
  6130  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6131  						{
  6132  							OrgRepo: "*",
  6133  							Cluster: "",
  6134  							Config: &prowapi.ProwJobDefault{
  6135  								TenantID: "configDefault",
  6136  							},
  6137  						},
  6138  						{
  6139  							OrgRepo: "org",
  6140  							Cluster: "",
  6141  							Config: &prowapi.ProwJobDefault{
  6142  								TenantID: "org default",
  6143  							},
  6144  						},
  6145  						{
  6146  							OrgRepo: "org/repo",
  6147  							Cluster: "",
  6148  							Config: &prowapi.ProwJobDefault{
  6149  								TenantID: "org/repo default",
  6150  							},
  6151  						},
  6152  					},
  6153  				},
  6154  			},
  6155  			expected: &prowapi.ProwJobDefault{
  6156  				TenantID: "org default",
  6157  			},
  6158  		},
  6159  		{
  6160  			id:      "no default in job. * cluster provided config's default by org and org/repo, unknown repo uses org",
  6161  			repo:    "org/foo",
  6162  			cluster: "default",
  6163  			config: &Config{
  6164  				ProwConfig: ProwConfig{
  6165  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6166  						{
  6167  							OrgRepo: "*",
  6168  							Cluster: "*",
  6169  							Config: &prowapi.ProwJobDefault{
  6170  								TenantID: "configDefault",
  6171  							},
  6172  						},
  6173  						{
  6174  							OrgRepo: "org",
  6175  							Cluster: "",
  6176  							Config: &prowapi.ProwJobDefault{
  6177  								TenantID: "org default",
  6178  							},
  6179  						},
  6180  						{
  6181  							OrgRepo: "org/repo",
  6182  							Cluster: "",
  6183  							Config: &prowapi.ProwJobDefault{
  6184  								TenantID: "org/repo default",
  6185  							},
  6186  						},
  6187  					},
  6188  				},
  6189  			},
  6190  			expected: &prowapi.ProwJobDefault{
  6191  				TenantID: "org default",
  6192  			},
  6193  		},
  6194  		{
  6195  			id:      "override cluster",
  6196  			repo:    "org/foo",
  6197  			cluster: "override",
  6198  			config: &Config{
  6199  				ProwConfig: ProwConfig{
  6200  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6201  						{
  6202  							OrgRepo: "*",
  6203  							Cluster: "*",
  6204  							Config: &prowapi.ProwJobDefault{
  6205  								TenantID: "configDefault",
  6206  							},
  6207  						},
  6208  						{
  6209  							OrgRepo: "org",
  6210  							Cluster: "",
  6211  							Config: &prowapi.ProwJobDefault{
  6212  								TenantID: "org default",
  6213  							},
  6214  						},
  6215  						{
  6216  							OrgRepo: "*",
  6217  							Cluster: "override",
  6218  							Config: &prowapi.ProwJobDefault{
  6219  								TenantID: "override default",
  6220  							},
  6221  						},
  6222  					},
  6223  				},
  6224  			},
  6225  			expected: &prowapi.ProwJobDefault{
  6226  				TenantID: "override default",
  6227  			},
  6228  		},
  6229  		{
  6230  			id:      "complicated config, but use provided config",
  6231  			repo:    "org/foo",
  6232  			cluster: "override",
  6233  			config: &Config{
  6234  				ProwConfig: ProwConfig{
  6235  					ProwJobDefaultEntries: []*ProwJobDefaultEntry{
  6236  						{
  6237  							OrgRepo: "*",
  6238  							Cluster: "*",
  6239  							Config: &prowapi.ProwJobDefault{
  6240  								TenantID: "configDefault",
  6241  							},
  6242  						},
  6243  						{
  6244  							OrgRepo: "org",
  6245  							Cluster: "",
  6246  							Config: &prowapi.ProwJobDefault{
  6247  								TenantID: "org default",
  6248  							},
  6249  						},
  6250  						{
  6251  							OrgRepo: "*",
  6252  							Cluster: "override",
  6253  							Config: &prowapi.ProwJobDefault{
  6254  								TenantID: "override default",
  6255  							},
  6256  						},
  6257  					},
  6258  				},
  6259  			},
  6260  			givenDefault: &prowapi.ProwJobDefault{
  6261  				TenantID: "given default",
  6262  			},
  6263  			expected: &prowapi.ProwJobDefault{
  6264  				TenantID: "given default",
  6265  			},
  6266  		},
  6267  	}
  6268  	for _, tc := range testCases {
  6269  		t.Run(tc.id, func(t *testing.T) {
  6270  			c := &Config{}
  6271  			jb := &JobBase{Cluster: tc.cluster, ProwJobDefault: tc.givenDefault}
  6272  			c.defaultJobBase(jb)
  6273  			presubmit := &Presubmit{JobBase: *jb}
  6274  			postsubmit := &Postsubmit{JobBase: *jb}
  6275  
  6276  			setPresubmitProwJobDefaults(tc.config, presubmit, tc.repo)
  6277  			if diff := cmp.Diff(presubmit.ProwJobDefault, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  6278  				t.Errorf("presubmit: %s", diff)
  6279  			}
  6280  
  6281  			setPostsubmitProwJobDefaults(tc.config, postsubmit, tc.repo)
  6282  			if diff := cmp.Diff(postsubmit.ProwJobDefault, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  6283  				t.Errorf("postsubmit: %s", diff)
  6284  			}
  6285  		})
  6286  	}
  6287  }
  6288  
  6289  func TestSetDecorationDefaults(t *testing.T) {
  6290  	yes := true
  6291  	no := false
  6292  
  6293  	testCases := []struct {
  6294  		id            string
  6295  		repo          string
  6296  		cluster       string
  6297  		config        *Config
  6298  		utilityConfig UtilityConfig
  6299  		expected      *prowapi.DecorationConfig
  6300  	}{
  6301  		{
  6302  			id:            "no dc in presubmit or in plank's config, expect no changes",
  6303  			utilityConfig: UtilityConfig{Decorate: &yes},
  6304  			config:        &Config{ProwConfig: ProwConfig{}},
  6305  			expected:      &prowapi.DecorationConfig{},
  6306  		},
  6307  		{
  6308  			id:            "no dc in presubmit or in plank's by repo config, expect plank's defaults",
  6309  			utilityConfig: UtilityConfig{Decorate: &yes},
  6310  			config: &Config{
  6311  				ProwConfig: ProwConfig{
  6312  					Plank: Plank{
  6313  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6314  							{
  6315  								OrgRepo: "*",
  6316  								Cluster: "",
  6317  								Config: &prowapi.DecorationConfig{
  6318  									UtilityImages: &prowapi.UtilityImages{
  6319  										CloneRefs:  "clonerefs:test",
  6320  										InitUpload: "initupload:test",
  6321  										Entrypoint: "entrypoint:test",
  6322  										Sidecar:    "sidecar:test",
  6323  									},
  6324  									GCSConfiguration: &prowapi.GCSConfiguration{
  6325  										Bucket:       "test-bucket",
  6326  										PathStrategy: "single",
  6327  										DefaultOrg:   "org",
  6328  										DefaultRepo:  "repo",
  6329  									},
  6330  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6331  								},
  6332  							},
  6333  						},
  6334  					},
  6335  				},
  6336  			},
  6337  			expected: &prowapi.DecorationConfig{
  6338  				UtilityImages: &prowapi.UtilityImages{
  6339  					CloneRefs:  "clonerefs:test",
  6340  					InitUpload: "initupload:test",
  6341  					Entrypoint: "entrypoint:test",
  6342  					Sidecar:    "sidecar:test",
  6343  				},
  6344  				GCSConfiguration: &prowapi.GCSConfiguration{
  6345  					Bucket:       "test-bucket",
  6346  					PathStrategy: "single",
  6347  					DefaultOrg:   "org",
  6348  					DefaultRepo:  "repo",
  6349  				},
  6350  				GCSCredentialsSecret: pStr("credentials-gcs"),
  6351  			},
  6352  		},
  6353  		{
  6354  			id:            "no dc in presubmit, part of plank's by repo config, expect merged by repo config and defaults",
  6355  			utilityConfig: UtilityConfig{Decorate: &yes},
  6356  			repo:          "org/repo",
  6357  			config: &Config{
  6358  				ProwConfig: ProwConfig{
  6359  					Plank: Plank{
  6360  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6361  							{
  6362  								OrgRepo: "*",
  6363  								Cluster: "",
  6364  								Config: &prowapi.DecorationConfig{
  6365  									UtilityImages: &prowapi.UtilityImages{
  6366  										CloneRefs:  "clonerefs:test",
  6367  										InitUpload: "initupload:test",
  6368  										Entrypoint: "entrypoint:test",
  6369  										Sidecar:    "sidecar:test",
  6370  									},
  6371  									GCSConfiguration: &prowapi.GCSConfiguration{
  6372  										Bucket:       "test-bucket",
  6373  										PathStrategy: "single",
  6374  										DefaultOrg:   "org",
  6375  										DefaultRepo:  "repo",
  6376  									},
  6377  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6378  								},
  6379  							},
  6380  							{
  6381  								OrgRepo: "org/repo",
  6382  								Cluster: "",
  6383  								Config: &prowapi.DecorationConfig{
  6384  									GCSConfiguration: &prowapi.GCSConfiguration{
  6385  										Bucket:       "test-bucket-by-repo",
  6386  										PathStrategy: "single-by-repo",
  6387  										DefaultOrg:   "org-by-repo",
  6388  										DefaultRepo:  "repo-by-repo",
  6389  									},
  6390  								},
  6391  							},
  6392  						},
  6393  					},
  6394  				},
  6395  			},
  6396  			expected: &prowapi.DecorationConfig{
  6397  				UtilityImages: &prowapi.UtilityImages{
  6398  					CloneRefs:  "clonerefs:test",
  6399  					InitUpload: "initupload:test",
  6400  					Entrypoint: "entrypoint:test",
  6401  					Sidecar:    "sidecar:test",
  6402  				},
  6403  				GCSConfiguration: &prowapi.GCSConfiguration{
  6404  					Bucket:       "test-bucket-by-repo",
  6405  					PathStrategy: "single-by-repo",
  6406  					DefaultOrg:   "org-by-repo",
  6407  					DefaultRepo:  "repo-by-repo",
  6408  				},
  6409  				GCSCredentialsSecret: pStr("credentials-gcs"),
  6410  			},
  6411  		},
  6412  		{
  6413  			id:   "dc in presubmit and plank's defaults, expect presubmit's dc",
  6414  			repo: "org/repo",
  6415  			utilityConfig: UtilityConfig{
  6416  				Decorate: &yes,
  6417  				DecorationConfig: &prowapi.DecorationConfig{
  6418  					UtilityImages: &prowapi.UtilityImages{
  6419  						CloneRefs:  "clonerefs:test-from-ps",
  6420  						InitUpload: "initupload:test-from-ps",
  6421  						Entrypoint: "entrypoint:test-from-ps",
  6422  						Sidecar:    "sidecar:test-from-ps",
  6423  					},
  6424  					GCSConfiguration: &prowapi.GCSConfiguration{
  6425  						Bucket:       "test-bucket-from-ps",
  6426  						PathStrategy: "single-from-ps",
  6427  						DefaultOrg:   "org-from-ps",
  6428  						DefaultRepo:  "repo-from-ps",
  6429  					},
  6430  					GCSCredentialsSecret: pStr("credentials-gcs-from-ps"),
  6431  				},
  6432  			},
  6433  			config: &Config{
  6434  				ProwConfig: ProwConfig{
  6435  					Plank: Plank{
  6436  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6437  							{
  6438  								OrgRepo: "*",
  6439  								Cluster: "",
  6440  								Config: &prowapi.DecorationConfig{
  6441  									UtilityImages: &prowapi.UtilityImages{
  6442  										CloneRefs:  "clonerefs:test",
  6443  										InitUpload: "initupload:test",
  6444  										Entrypoint: "entrypoint:test",
  6445  										Sidecar:    "sidecar:test",
  6446  									},
  6447  									GCSConfiguration: &prowapi.GCSConfiguration{
  6448  										Bucket:       "test-bucket",
  6449  										PathStrategy: "single",
  6450  										DefaultOrg:   "org",
  6451  										DefaultRepo:  "repo",
  6452  									},
  6453  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6454  								},
  6455  							},
  6456  						},
  6457  					},
  6458  				},
  6459  			},
  6460  			expected: &prowapi.DecorationConfig{
  6461  				UtilityImages: &prowapi.UtilityImages{
  6462  					CloneRefs:  "clonerefs:test-from-ps",
  6463  					InitUpload: "initupload:test-from-ps",
  6464  					Entrypoint: "entrypoint:test-from-ps",
  6465  					Sidecar:    "sidecar:test-from-ps",
  6466  				},
  6467  				GCSConfiguration: &prowapi.GCSConfiguration{
  6468  					Bucket:       "test-bucket-from-ps",
  6469  					PathStrategy: "single-from-ps",
  6470  					DefaultOrg:   "org-from-ps",
  6471  					DefaultRepo:  "repo-from-ps",
  6472  				},
  6473  				GCSCredentialsSecret: pStr("credentials-gcs-from-ps"),
  6474  			},
  6475  		},
  6476  		{
  6477  			id:   "dc in presubmit, plank's by repo config and defaults, expected presubmit's dc",
  6478  			repo: "org/repo",
  6479  			utilityConfig: UtilityConfig{
  6480  				Decorate: &yes,
  6481  				DecorationConfig: &prowapi.DecorationConfig{
  6482  					UtilityImages: &prowapi.UtilityImages{
  6483  						CloneRefs:  "clonerefs:test-from-ps",
  6484  						InitUpload: "initupload:test-from-ps",
  6485  						Entrypoint: "entrypoint:test-from-ps",
  6486  						Sidecar:    "sidecar:test-from-ps",
  6487  					},
  6488  					GCSConfiguration: &prowapi.GCSConfiguration{
  6489  						Bucket:       "test-bucket-from-ps",
  6490  						PathStrategy: "single-from-ps",
  6491  						DefaultOrg:   "org-from-ps",
  6492  						DefaultRepo:  "repo-from-ps",
  6493  					},
  6494  					GCSCredentialsSecret: pStr("credentials-gcs-from-ps"),
  6495  				},
  6496  			},
  6497  			config: &Config{
  6498  				ProwConfig: ProwConfig{
  6499  					Plank: Plank{
  6500  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6501  							{
  6502  								OrgRepo: "*",
  6503  								Cluster: "",
  6504  								Config: &prowapi.DecorationConfig{
  6505  									UtilityImages: &prowapi.UtilityImages{
  6506  										CloneRefs:  "clonerefs:test",
  6507  										InitUpload: "initupload:test",
  6508  										Entrypoint: "entrypoint:test",
  6509  										Sidecar:    "sidecar:test",
  6510  									},
  6511  									GCSConfiguration: &prowapi.GCSConfiguration{
  6512  										Bucket:       "test-bucket",
  6513  										PathStrategy: "single",
  6514  										DefaultOrg:   "org",
  6515  										DefaultRepo:  "repo",
  6516  									},
  6517  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6518  								},
  6519  							},
  6520  							{
  6521  								OrgRepo: "org/repo",
  6522  								Cluster: "",
  6523  								Config: &prowapi.DecorationConfig{
  6524  									UtilityImages: &prowapi.UtilityImages{
  6525  										CloneRefs:  "clonerefs:test-by-repo",
  6526  										InitUpload: "initupload:test-by-repo",
  6527  										Entrypoint: "entrypoint:test-by-repo",
  6528  										Sidecar:    "sidecar:test-by-repo",
  6529  									},
  6530  									GCSConfiguration: &prowapi.GCSConfiguration{
  6531  										Bucket:       "test-bucket-by-repo",
  6532  										PathStrategy: "single",
  6533  										DefaultOrg:   "org-test",
  6534  										DefaultRepo:  "repo-test",
  6535  									},
  6536  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6537  								},
  6538  							},
  6539  						},
  6540  					},
  6541  				},
  6542  			},
  6543  			expected: &prowapi.DecorationConfig{
  6544  				UtilityImages: &prowapi.UtilityImages{
  6545  					CloneRefs:  "clonerefs:test-from-ps",
  6546  					InitUpload: "initupload:test-from-ps",
  6547  					Entrypoint: "entrypoint:test-from-ps",
  6548  					Sidecar:    "sidecar:test-from-ps",
  6549  				},
  6550  				GCSConfiguration: &prowapi.GCSConfiguration{
  6551  					Bucket:       "test-bucket-from-ps",
  6552  					PathStrategy: "single-from-ps",
  6553  					DefaultOrg:   "org-from-ps",
  6554  					DefaultRepo:  "repo-from-ps",
  6555  				},
  6556  				GCSCredentialsSecret: pStr("credentials-gcs-from-ps"),
  6557  			},
  6558  		},
  6559  		{
  6560  			id:            "no dc in presubmit, dc in plank's by repo config and defaults, expect by repo config's dc",
  6561  			repo:          "org/repo",
  6562  			utilityConfig: UtilityConfig{Decorate: &yes},
  6563  			config: &Config{
  6564  				ProwConfig: ProwConfig{
  6565  					Plank: Plank{
  6566  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6567  							{
  6568  								OrgRepo: "*",
  6569  								Cluster: "",
  6570  								Config: &prowapi.DecorationConfig{
  6571  									UtilityImages: &prowapi.UtilityImages{
  6572  										CloneRefs:  "clonerefs:test",
  6573  										InitUpload: "initupload:test",
  6574  										Entrypoint: "entrypoint:test",
  6575  										Sidecar:    "sidecar:test",
  6576  									},
  6577  									GCSConfiguration: &prowapi.GCSConfiguration{
  6578  										Bucket:       "test-bucket",
  6579  										PathStrategy: "single",
  6580  										DefaultOrg:   "org",
  6581  										DefaultRepo:  "repo",
  6582  									},
  6583  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6584  								},
  6585  							},
  6586  							{
  6587  								OrgRepo: "org/repo",
  6588  								Cluster: "",
  6589  								Config: &prowapi.DecorationConfig{
  6590  									UtilityImages: &prowapi.UtilityImages{
  6591  										CloneRefs:  "clonerefs:test-by-repo",
  6592  										InitUpload: "initupload:test-by-repo",
  6593  										Entrypoint: "entrypoint:test-by-repo",
  6594  										Sidecar:    "sidecar:test-by-repo",
  6595  									},
  6596  									GCSConfiguration: &prowapi.GCSConfiguration{
  6597  										Bucket:       "test-bucket-by-repo",
  6598  										PathStrategy: "single-by-repo",
  6599  										DefaultOrg:   "org-by-repo",
  6600  										DefaultRepo:  "repo-by-repo",
  6601  									},
  6602  									GCSCredentialsSecret: pStr("credentials-gcs-by-repo"),
  6603  								},
  6604  							},
  6605  						},
  6606  					},
  6607  				},
  6608  			},
  6609  			expected: &prowapi.DecorationConfig{
  6610  				UtilityImages: &prowapi.UtilityImages{
  6611  					CloneRefs:  "clonerefs:test-by-repo",
  6612  					InitUpload: "initupload:test-by-repo",
  6613  					Entrypoint: "entrypoint:test-by-repo",
  6614  					Sidecar:    "sidecar:test-by-repo",
  6615  				},
  6616  				GCSConfiguration: &prowapi.GCSConfiguration{
  6617  					Bucket:       "test-bucket-by-repo",
  6618  					PathStrategy: "single-by-repo",
  6619  					DefaultOrg:   "org-by-repo",
  6620  					DefaultRepo:  "repo-by-repo",
  6621  				},
  6622  				GCSCredentialsSecret: pStr("credentials-gcs-by-repo"),
  6623  			},
  6624  		},
  6625  		{
  6626  			id:            "no dc in presubmit, dc in plank's by repo config and defaults, expect by org config's dc",
  6627  			repo:          "org/repo",
  6628  			utilityConfig: UtilityConfig{Decorate: &yes},
  6629  			config: &Config{
  6630  				ProwConfig: ProwConfig{
  6631  					Plank: Plank{
  6632  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6633  							{
  6634  								OrgRepo: "*",
  6635  								Cluster: "",
  6636  								Config: &prowapi.DecorationConfig{
  6637  									UtilityImages: &prowapi.UtilityImages{
  6638  										CloneRefs:  "clonerefs:test",
  6639  										InitUpload: "initupload:test",
  6640  										Entrypoint: "entrypoint:test",
  6641  										Sidecar:    "sidecar:test",
  6642  									},
  6643  									GCSConfiguration: &prowapi.GCSConfiguration{
  6644  										Bucket:       "test-bucket",
  6645  										PathStrategy: "single",
  6646  										DefaultOrg:   "org",
  6647  										DefaultRepo:  "repo",
  6648  									},
  6649  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6650  								},
  6651  							},
  6652  							{
  6653  								OrgRepo: "org",
  6654  								Cluster: "",
  6655  								Config: &prowapi.DecorationConfig{
  6656  									UtilityImages: &prowapi.UtilityImages{
  6657  										CloneRefs:  "clonerefs:test-by-org",
  6658  										InitUpload: "initupload:test-by-org",
  6659  										Entrypoint: "entrypoint:test-by-org",
  6660  										Sidecar:    "sidecar:test-by-org",
  6661  									},
  6662  									GCSConfiguration: &prowapi.GCSConfiguration{
  6663  										Bucket:       "test-bucket-by-org",
  6664  										PathStrategy: "single-by-org",
  6665  										DefaultOrg:   "org-by-org",
  6666  										DefaultRepo:  "repo-by-org",
  6667  									},
  6668  									GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  6669  								},
  6670  							},
  6671  						},
  6672  					},
  6673  				},
  6674  			},
  6675  			expected: &prowapi.DecorationConfig{
  6676  				UtilityImages: &prowapi.UtilityImages{
  6677  					CloneRefs:  "clonerefs:test-by-org",
  6678  					InitUpload: "initupload:test-by-org",
  6679  					Entrypoint: "entrypoint:test-by-org",
  6680  					Sidecar:    "sidecar:test-by-org",
  6681  				},
  6682  				GCSConfiguration: &prowapi.GCSConfiguration{
  6683  					Bucket:       "test-bucket-by-org",
  6684  					PathStrategy: "single-by-org",
  6685  					DefaultOrg:   "org-by-org",
  6686  					DefaultRepo:  "repo-by-org",
  6687  				},
  6688  				GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  6689  			},
  6690  		},
  6691  		{
  6692  			id:            "no dc in presubmit, dc in plank's by repo config and defaults, expect by * config's dc",
  6693  			repo:          "org/repo",
  6694  			utilityConfig: UtilityConfig{Decorate: &yes},
  6695  			config: &Config{
  6696  				ProwConfig: ProwConfig{
  6697  					Plank: Plank{
  6698  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6699  							{
  6700  								OrgRepo: "*",
  6701  								Cluster: "",
  6702  								Config: &prowapi.DecorationConfig{
  6703  									UtilityImages: &prowapi.UtilityImages{
  6704  										CloneRefs:  "clonerefs:test-by-*",
  6705  										InitUpload: "initupload:test-by-*",
  6706  										Entrypoint: "entrypoint:test-by-*",
  6707  										Sidecar:    "sidecar:test-by-*",
  6708  									},
  6709  									GCSConfiguration: &prowapi.GCSConfiguration{
  6710  										Bucket:       "test-bucket-by-*",
  6711  										PathStrategy: "single-by-*",
  6712  										DefaultOrg:   "org-by-*",
  6713  										DefaultRepo:  "repo-by-*",
  6714  									},
  6715  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  6716  								},
  6717  							},
  6718  						},
  6719  					},
  6720  				},
  6721  			},
  6722  			expected: &prowapi.DecorationConfig{
  6723  				UtilityImages: &prowapi.UtilityImages{
  6724  					CloneRefs:  "clonerefs:test-by-*",
  6725  					InitUpload: "initupload:test-by-*",
  6726  					Entrypoint: "entrypoint:test-by-*",
  6727  					Sidecar:    "sidecar:test-by-*",
  6728  				},
  6729  				GCSConfiguration: &prowapi.GCSConfiguration{
  6730  					Bucket:       "test-bucket-by-*",
  6731  					PathStrategy: "single-by-*",
  6732  					DefaultOrg:   "org-by-*",
  6733  					DefaultRepo:  "repo-by-*",
  6734  				},
  6735  				GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  6736  			},
  6737  		},
  6738  
  6739  		{
  6740  			id:            "no dc in presubmit, dc in plank's by repo config org and org/repo co-exists, expect by org/repo config's dc",
  6741  			repo:          "org/repo",
  6742  			utilityConfig: UtilityConfig{Decorate: &yes},
  6743  			config: &Config{
  6744  				ProwConfig: ProwConfig{
  6745  					Plank: Plank{
  6746  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6747  							{
  6748  								OrgRepo: "*",
  6749  								Cluster: "",
  6750  								Config: &prowapi.DecorationConfig{
  6751  									UtilityImages: &prowapi.UtilityImages{
  6752  										CloneRefs:  "clonerefs:test-by-*",
  6753  										InitUpload: "initupload:test-by-*",
  6754  										Entrypoint: "entrypoint:test-by-*",
  6755  										Sidecar:    "sidecar:test-by-*",
  6756  									},
  6757  									GCSConfiguration: &prowapi.GCSConfiguration{
  6758  										Bucket:       "test-bucket-by-*",
  6759  										PathStrategy: "single-by-*",
  6760  										DefaultOrg:   "org-by-*",
  6761  										DefaultRepo:  "repo-by-*",
  6762  									},
  6763  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  6764  								},
  6765  							},
  6766  							{
  6767  								OrgRepo: "org",
  6768  								Cluster: "",
  6769  								Config: &prowapi.DecorationConfig{
  6770  									UtilityImages: &prowapi.UtilityImages{
  6771  										CloneRefs:  "clonerefs:test-by-org",
  6772  										InitUpload: "initupload:test-by-org",
  6773  										Entrypoint: "entrypoint:test-by-org",
  6774  										Sidecar:    "sidecar:test-by-org",
  6775  									},
  6776  									GCSConfiguration: &prowapi.GCSConfiguration{
  6777  										Bucket:       "test-bucket-by-org",
  6778  										PathStrategy: "single-by-org",
  6779  										DefaultOrg:   "org-by-org",
  6780  										DefaultRepo:  "repo-by-org",
  6781  									},
  6782  									GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  6783  								},
  6784  							},
  6785  							{
  6786  								OrgRepo: "org/repo",
  6787  								Cluster: "",
  6788  								Config: &prowapi.DecorationConfig{
  6789  									UtilityImages: &prowapi.UtilityImages{
  6790  										CloneRefs:  "clonerefs:test-by-org-repo",
  6791  										InitUpload: "initupload:test-by-org-repo",
  6792  										Entrypoint: "entrypoint:test-by-org-repo",
  6793  										Sidecar:    "sidecar:test-by-org-repo",
  6794  									},
  6795  									GCSConfiguration: &prowapi.GCSConfiguration{
  6796  										Bucket:       "test-bucket-by-org-repo",
  6797  										PathStrategy: "single-by-org-repo",
  6798  										DefaultOrg:   "org-by-org-repo",
  6799  										DefaultRepo:  "repo-by-org-repo",
  6800  									},
  6801  									GCSCredentialsSecret: pStr("credentials-gcs-by-org-repo"),
  6802  								},
  6803  							},
  6804  						},
  6805  					},
  6806  				},
  6807  			},
  6808  			expected: &prowapi.DecorationConfig{
  6809  				UtilityImages: &prowapi.UtilityImages{
  6810  					CloneRefs:  "clonerefs:test-by-org-repo",
  6811  					InitUpload: "initupload:test-by-org-repo",
  6812  					Entrypoint: "entrypoint:test-by-org-repo",
  6813  					Sidecar:    "sidecar:test-by-org-repo",
  6814  				},
  6815  				GCSConfiguration: &prowapi.GCSConfiguration{
  6816  					Bucket:       "test-bucket-by-org-repo",
  6817  					PathStrategy: "single-by-org-repo",
  6818  					DefaultOrg:   "org-by-org-repo",
  6819  					DefaultRepo:  "repo-by-org-repo",
  6820  				},
  6821  				GCSCredentialsSecret: pStr("credentials-gcs-by-org-repo"),
  6822  			},
  6823  		},
  6824  
  6825  		{
  6826  			id:            "no dc in presubmit, dc in plank's by repo config with org and * to co-exists, expect by 'org' config's dc",
  6827  			repo:          "org/repo",
  6828  			utilityConfig: UtilityConfig{Decorate: &yes},
  6829  			config: &Config{
  6830  				ProwConfig: ProwConfig{
  6831  					Plank: Plank{
  6832  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6833  							{
  6834  								OrgRepo: "*",
  6835  								Cluster: "",
  6836  								Config: &prowapi.DecorationConfig{
  6837  									UtilityImages: &prowapi.UtilityImages{
  6838  										CloneRefs:  "clonerefs:test-by-*",
  6839  										InitUpload: "initupload:test-by-*",
  6840  										Entrypoint: "entrypoint:test-by-*",
  6841  										Sidecar:    "sidecar:test-by-*",
  6842  									},
  6843  									GCSConfiguration: &prowapi.GCSConfiguration{
  6844  										Bucket:       "test-bucket-by-*",
  6845  										PathStrategy: "single-by-*",
  6846  										DefaultOrg:   "org-by-*",
  6847  										DefaultRepo:  "repo-by-*",
  6848  									},
  6849  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  6850  								},
  6851  							},
  6852  							{
  6853  								OrgRepo: "org",
  6854  								Cluster: "",
  6855  								Config: &prowapi.DecorationConfig{
  6856  									UtilityImages: &prowapi.UtilityImages{
  6857  										CloneRefs:  "clonerefs:test-by-org",
  6858  										InitUpload: "initupload:test-by-org",
  6859  										Entrypoint: "entrypoint:test-by-org",
  6860  										Sidecar:    "sidecar:test-by-org",
  6861  									},
  6862  									GCSConfiguration: &prowapi.GCSConfiguration{
  6863  										Bucket:       "test-bucket-by-org",
  6864  										PathStrategy: "single-by-org",
  6865  										DefaultOrg:   "org-by-org",
  6866  										DefaultRepo:  "repo-by-org",
  6867  									},
  6868  									GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  6869  								},
  6870  							},
  6871  						},
  6872  					},
  6873  				},
  6874  			},
  6875  			expected: &prowapi.DecorationConfig{
  6876  				UtilityImages: &prowapi.UtilityImages{
  6877  					CloneRefs:  "clonerefs:test-by-org",
  6878  					InitUpload: "initupload:test-by-org",
  6879  					Entrypoint: "entrypoint:test-by-org",
  6880  					Sidecar:    "sidecar:test-by-org",
  6881  				},
  6882  				GCSConfiguration: &prowapi.GCSConfiguration{
  6883  					Bucket:       "test-bucket-by-org",
  6884  					PathStrategy: "single-by-org",
  6885  					DefaultOrg:   "org-by-org",
  6886  					DefaultRepo:  "repo-by-org",
  6887  				},
  6888  				GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  6889  			},
  6890  		},
  6891  		{
  6892  			id: "decorate_all_jobs set, no dc in presubmit or in plank's by repo config, expect plank's defaults",
  6893  			config: &Config{
  6894  				JobConfig: JobConfig{
  6895  					DecorateAllJobs: true,
  6896  				},
  6897  				ProwConfig: ProwConfig{
  6898  					Plank: Plank{
  6899  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6900  							{
  6901  								OrgRepo: "*",
  6902  								Cluster: "",
  6903  								Config: &prowapi.DecorationConfig{
  6904  									UtilityImages: &prowapi.UtilityImages{
  6905  										CloneRefs:  "clonerefs:test",
  6906  										InitUpload: "initupload:test",
  6907  										Entrypoint: "entrypoint:test",
  6908  										Sidecar:    "sidecar:test",
  6909  									},
  6910  									GCSConfiguration: &prowapi.GCSConfiguration{
  6911  										Bucket:       "test-bucket",
  6912  										PathStrategy: "single",
  6913  										DefaultOrg:   "org",
  6914  										DefaultRepo:  "repo",
  6915  									},
  6916  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6917  								},
  6918  							},
  6919  						},
  6920  					},
  6921  				},
  6922  			},
  6923  			expected: &prowapi.DecorationConfig{
  6924  				UtilityImages: &prowapi.UtilityImages{
  6925  					CloneRefs:  "clonerefs:test",
  6926  					InitUpload: "initupload:test",
  6927  					Entrypoint: "entrypoint:test",
  6928  					Sidecar:    "sidecar:test",
  6929  				},
  6930  				GCSConfiguration: &prowapi.GCSConfiguration{
  6931  					Bucket:       "test-bucket",
  6932  					PathStrategy: "single",
  6933  					DefaultOrg:   "org",
  6934  					DefaultRepo:  "repo",
  6935  				},
  6936  				GCSCredentialsSecret: pStr("credentials-gcs"),
  6937  			},
  6938  		},
  6939  		{
  6940  			id:            "opt out of decorate_all_jobs by setting decorated to false",
  6941  			utilityConfig: UtilityConfig{Decorate: &no},
  6942  			config: &Config{
  6943  				JobConfig: JobConfig{
  6944  					DecorateAllJobs: true,
  6945  				},
  6946  				ProwConfig: ProwConfig{
  6947  					Plank: Plank{
  6948  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  6949  							{
  6950  								OrgRepo: "*",
  6951  								Cluster: "",
  6952  								Config: &prowapi.DecorationConfig{
  6953  									UtilityImages: &prowapi.UtilityImages{
  6954  										CloneRefs:  "clonerefs:test",
  6955  										InitUpload: "initupload:test",
  6956  										Entrypoint: "entrypoint:test",
  6957  										Sidecar:    "sidecar:test",
  6958  									},
  6959  									GCSConfiguration: &prowapi.GCSConfiguration{
  6960  										Bucket:       "test-bucket",
  6961  										PathStrategy: "single",
  6962  										DefaultOrg:   "org",
  6963  										DefaultRepo:  "repo",
  6964  									},
  6965  									GCSCredentialsSecret: pStr("credentials-gcs"),
  6966  								},
  6967  							},
  6968  						},
  6969  					},
  6970  				},
  6971  			},
  6972  		},
  6973  		{
  6974  			id:     "unrecognized org, no cluster => use global + default cluster configs",
  6975  			config: complexConfig(),
  6976  			expected: &prowapi.DecorationConfig{
  6977  				UtilityImages: &prowapi.UtilityImages{
  6978  					CloneRefs:  "clonerefs:global",
  6979  					InitUpload: "initupload:global",
  6980  					Entrypoint: "entrypoint:global",
  6981  					Sidecar:    "sidecar:global",
  6982  				},
  6983  				GCSConfiguration: &prowapi.GCSConfiguration{
  6984  					Bucket:       "global",
  6985  					PathStrategy: "explicit",
  6986  				},
  6987  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  6988  			},
  6989  		},
  6990  		{
  6991  			id:      "unrecognized repo and explicit 'default' cluster => use global + org + default cluster configs",
  6992  			config:  complexConfig(),
  6993  			cluster: "default",
  6994  			repo:    "org/foo",
  6995  			expected: &prowapi.DecorationConfig{
  6996  				UtilityImages: &prowapi.UtilityImages{
  6997  					CloneRefs:  "clonerefs:global",
  6998  					InitUpload: "initupload:global",
  6999  					Entrypoint: "entrypoint:global",
  7000  					Sidecar:    "sidecar:global",
  7001  				},
  7002  				GCSConfiguration: &prowapi.GCSConfiguration{
  7003  					Bucket:       "org-specific",
  7004  					PathStrategy: "explicit",
  7005  				},
  7006  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  7007  			},
  7008  		},
  7009  		{
  7010  			id:      "recognized repo and explicit 'trusted' cluster => use global + org + repo + trusted cluster configs",
  7011  			config:  complexConfig(),
  7012  			cluster: "trusted",
  7013  			repo:    "org/repo",
  7014  			expected: &prowapi.DecorationConfig{
  7015  				UtilityImages: &prowapi.UtilityImages{
  7016  					CloneRefs:  "clonerefs:global",
  7017  					InitUpload: "initupload:global",
  7018  					Entrypoint: "entrypoint:global",
  7019  					Sidecar:    "sidecar:global",
  7020  				},
  7021  				GCSConfiguration: &prowapi.GCSConfiguration{
  7022  					Bucket:       "repo-specific",
  7023  					PathStrategy: "explicit",
  7024  				},
  7025  				DefaultServiceAccountName: pStr("trusted-cluster-uses-SA"),
  7026  			},
  7027  		},
  7028  		{
  7029  			id:      "override org and in trusted cluster => use global + trusted cluster + override configs",
  7030  			config:  complexConfig(),
  7031  			cluster: "trusted",
  7032  			repo:    "override/foo",
  7033  			expected: &prowapi.DecorationConfig{
  7034  				UtilityImages: &prowapi.UtilityImages{
  7035  					CloneRefs:  "clonerefs:override",
  7036  					InitUpload: "initupload:global",
  7037  					Entrypoint: "entrypoint:global",
  7038  					Sidecar:    "sidecar:global",
  7039  				},
  7040  				GCSConfiguration: &prowapi.GCSConfiguration{
  7041  					Bucket:       "global",
  7042  					PathStrategy: "explicit",
  7043  				},
  7044  				DefaultServiceAccountName: pStr(""),
  7045  				GCSCredentialsSecret:      pStr("trusted-cluster-override-uses-secret"),
  7046  			},
  7047  		},
  7048  		{
  7049  			id:      "override org and in default cluster => use global + default cluster configs",
  7050  			config:  complexConfig(),
  7051  			cluster: "default",
  7052  			repo:    "override/foo",
  7053  			expected: &prowapi.DecorationConfig{
  7054  				UtilityImages: &prowapi.UtilityImages{
  7055  					CloneRefs:  "clonerefs:global",
  7056  					InitUpload: "initupload:global",
  7057  					Entrypoint: "entrypoint:global",
  7058  					Sidecar:    "sidecar:global",
  7059  				},
  7060  				GCSConfiguration: &prowapi.GCSConfiguration{
  7061  					Bucket:       "global",
  7062  					PathStrategy: "explicit",
  7063  				},
  7064  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  7065  			},
  7066  		},
  7067  	}
  7068  
  7069  	for _, tc := range testCases {
  7070  		t.Run(tc.id, func(t *testing.T) {
  7071  			c := &Config{}
  7072  			jb := &JobBase{Cluster: tc.cluster, UtilityConfig: tc.utilityConfig}
  7073  			c.defaultJobBase(jb)
  7074  			presubmit := &Presubmit{JobBase: *jb}
  7075  			postsubmit := &Postsubmit{JobBase: *jb}
  7076  
  7077  			setPresubmitDecorationDefaults(tc.config, presubmit, tc.repo)
  7078  			if diff := cmp.Diff(presubmit.DecorationConfig, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  7079  				t.Errorf("presubmit: %s", diff)
  7080  			}
  7081  
  7082  			setPostsubmitDecorationDefaults(tc.config, postsubmit, tc.repo)
  7083  			if diff := cmp.Diff(postsubmit.DecorationConfig, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  7084  				t.Errorf("postsubmit: %s", diff)
  7085  			}
  7086  		})
  7087  	}
  7088  }
  7089  
  7090  func TestSetPeriodicDecorationDefaults(t *testing.T) {
  7091  	yes := true
  7092  	no := false
  7093  	testCases := []struct {
  7094  		id            string
  7095  		cluster       string
  7096  		config        *Config
  7097  		utilityConfig UtilityConfig
  7098  		expected      *prowapi.DecorationConfig
  7099  	}{
  7100  		{
  7101  			id: "extraRefs[0] not defined, changes from defaultDecorationConfigs[*] expected",
  7102  			config: &Config{
  7103  				ProwConfig: ProwConfig{
  7104  					Plank: Plank{
  7105  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7106  							{
  7107  								OrgRepo: "*",
  7108  								Cluster: "*",
  7109  								Config: &prowapi.DecorationConfig{
  7110  									UtilityImages: &prowapi.UtilityImages{
  7111  										CloneRefs:  "clonerefs:test-by-*",
  7112  										InitUpload: "initupload:test-by-*",
  7113  										Entrypoint: "entrypoint:test-by-*",
  7114  										Sidecar:    "sidecar:test-by-*",
  7115  									},
  7116  									GCSConfiguration: &prowapi.GCSConfiguration{
  7117  										Bucket:       "test-bucket-by-*",
  7118  										PathStrategy: "single-by-*",
  7119  										DefaultOrg:   "org-by-*",
  7120  										DefaultRepo:  "repo-by-*",
  7121  									},
  7122  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7123  								},
  7124  							},
  7125  						},
  7126  					},
  7127  				},
  7128  			},
  7129  			utilityConfig: UtilityConfig{Decorate: &yes},
  7130  			expected: &prowapi.DecorationConfig{
  7131  				UtilityImages: &prowapi.UtilityImages{
  7132  					CloneRefs:  "clonerefs:test-by-*",
  7133  					InitUpload: "initupload:test-by-*",
  7134  					Entrypoint: "entrypoint:test-by-*",
  7135  					Sidecar:    "sidecar:test-by-*",
  7136  				},
  7137  				GCSConfiguration: &prowapi.GCSConfiguration{
  7138  					Bucket:       "test-bucket-by-*",
  7139  					PathStrategy: "single-by-*",
  7140  					DefaultOrg:   "org-by-*",
  7141  					DefaultRepo:  "repo-by-*",
  7142  				},
  7143  				GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7144  			},
  7145  		},
  7146  		{
  7147  			id: "extraRefs[0] defined, only 'org` exists in config, changes from defaultDecorationConfigs[org] expected",
  7148  			config: &Config{
  7149  				ProwConfig: ProwConfig{
  7150  					Plank: Plank{
  7151  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7152  							{
  7153  								OrgRepo: "*",
  7154  								Cluster: "",
  7155  								Config: &prowapi.DecorationConfig{
  7156  									UtilityImages: &prowapi.UtilityImages{
  7157  										CloneRefs:  "clonerefs:test-by-*",
  7158  										InitUpload: "initupload:test-by-*",
  7159  										Entrypoint: "entrypoint:test-by-*",
  7160  										Sidecar:    "sidecar:test-by-*",
  7161  									},
  7162  									GCSConfiguration: &prowapi.GCSConfiguration{
  7163  										Bucket:       "test-bucket-by-*",
  7164  										PathStrategy: "single-by-*",
  7165  										DefaultOrg:   "org-by-*",
  7166  										DefaultRepo:  "repo-by-*",
  7167  									},
  7168  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7169  								},
  7170  							},
  7171  							{
  7172  								OrgRepo: "org",
  7173  								Cluster: "",
  7174  								Config: &prowapi.DecorationConfig{
  7175  									UtilityImages: &prowapi.UtilityImages{
  7176  										CloneRefs:  "clonerefs:test-by-org",
  7177  										InitUpload: "initupload:test-by-org",
  7178  										Entrypoint: "entrypoint:test-by-org",
  7179  										Sidecar:    "sidecar:test-by-org",
  7180  									},
  7181  									GCSConfiguration: &prowapi.GCSConfiguration{
  7182  										Bucket:       "test-bucket-by-org",
  7183  										PathStrategy: "single-by-org",
  7184  										DefaultOrg:   "org-by-org",
  7185  										DefaultRepo:  "repo-by-org",
  7186  									},
  7187  									GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  7188  								},
  7189  							},
  7190  						},
  7191  					},
  7192  				},
  7193  			},
  7194  			utilityConfig: UtilityConfig{
  7195  				Decorate: &yes,
  7196  				ExtraRefs: []prowapi.Refs{
  7197  					{
  7198  						Org:  "org",
  7199  						Repo: "repo",
  7200  					},
  7201  				},
  7202  			},
  7203  			expected: &prowapi.DecorationConfig{
  7204  				UtilityImages: &prowapi.UtilityImages{
  7205  					CloneRefs:  "clonerefs:test-by-org",
  7206  					InitUpload: "initupload:test-by-org",
  7207  					Entrypoint: "entrypoint:test-by-org",
  7208  					Sidecar:    "sidecar:test-by-org",
  7209  				},
  7210  				GCSConfiguration: &prowapi.GCSConfiguration{
  7211  					Bucket:       "test-bucket-by-org",
  7212  					PathStrategy: "single-by-org",
  7213  					DefaultOrg:   "org-by-org",
  7214  					DefaultRepo:  "repo-by-org",
  7215  				},
  7216  				GCSCredentialsSecret: pStr("credentials-gcs-by-org"),
  7217  			},
  7218  		},
  7219  		{
  7220  			id: "extraRefs[0] defined and org/repo of defaultDecorationConfigs exists, changes from defaultDecorationConfigs[org/repo] expected",
  7221  			config: &Config{
  7222  				ProwConfig: ProwConfig{
  7223  					Plank: Plank{
  7224  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7225  							{
  7226  								OrgRepo: "*",
  7227  								Cluster: "",
  7228  								Config: &prowapi.DecorationConfig{
  7229  									UtilityImages: &prowapi.UtilityImages{
  7230  										CloneRefs:  "clonerefs:test-by-*",
  7231  										InitUpload: "initupload:test-by-*",
  7232  										Entrypoint: "entrypoint:test-by-*",
  7233  										Sidecar:    "sidecar:test-by-*",
  7234  									},
  7235  									GCSConfiguration: &prowapi.GCSConfiguration{
  7236  										Bucket:       "test-bucket-by-*",
  7237  										PathStrategy: "single-by-*",
  7238  										DefaultOrg:   "org-by-*",
  7239  										DefaultRepo:  "repo-by-*",
  7240  									},
  7241  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7242  								},
  7243  							},
  7244  							{
  7245  								OrgRepo: "org/repo",
  7246  								Cluster: "",
  7247  								Config: &prowapi.DecorationConfig{
  7248  									UtilityImages: &prowapi.UtilityImages{
  7249  										CloneRefs:  "clonerefs:test-by-org-repo",
  7250  										InitUpload: "initupload:test-by-org-repo",
  7251  										Entrypoint: "entrypoint:test-by-org-repo",
  7252  										Sidecar:    "sidecar:test-by-org-repo",
  7253  									},
  7254  									GCSConfiguration: &prowapi.GCSConfiguration{
  7255  										Bucket:       "test-bucket-by-org-repo",
  7256  										PathStrategy: "single-by-org-repo",
  7257  										DefaultOrg:   "org-by-org-repo",
  7258  										DefaultRepo:  "repo-by-org-repo",
  7259  									},
  7260  									GCSCredentialsSecret: pStr("credentials-gcs-by-org-repo"),
  7261  								},
  7262  							},
  7263  						},
  7264  					},
  7265  				},
  7266  			},
  7267  			utilityConfig: UtilityConfig{
  7268  				Decorate: &yes,
  7269  				ExtraRefs: []prowapi.Refs{
  7270  					{
  7271  						Org:  "org",
  7272  						Repo: "repo",
  7273  					},
  7274  				},
  7275  			},
  7276  			expected: &prowapi.DecorationConfig{
  7277  				UtilityImages: &prowapi.UtilityImages{
  7278  					CloneRefs:  "clonerefs:test-by-org-repo",
  7279  					InitUpload: "initupload:test-by-org-repo",
  7280  					Entrypoint: "entrypoint:test-by-org-repo",
  7281  					Sidecar:    "sidecar:test-by-org-repo",
  7282  				},
  7283  				GCSConfiguration: &prowapi.GCSConfiguration{
  7284  					Bucket:       "test-bucket-by-org-repo",
  7285  					PathStrategy: "single-by-org-repo",
  7286  					DefaultOrg:   "org-by-org-repo",
  7287  					DefaultRepo:  "repo-by-org-repo",
  7288  				},
  7289  				GCSCredentialsSecret: pStr("credentials-gcs-by-org-repo"),
  7290  			},
  7291  		},
  7292  		{
  7293  			id: "decorate_all_jobs set, plank's default decoration config expected",
  7294  			config: &Config{
  7295  				JobConfig: JobConfig{
  7296  					DecorateAllJobs: true,
  7297  				},
  7298  				ProwConfig: ProwConfig{
  7299  					Plank: Plank{
  7300  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7301  							{
  7302  								OrgRepo: "*",
  7303  								Cluster: "",
  7304  								Config: &prowapi.DecorationConfig{
  7305  									UtilityImages: &prowapi.UtilityImages{
  7306  										CloneRefs:  "clonerefs:test-by-*",
  7307  										InitUpload: "initupload:test-by-*",
  7308  										Entrypoint: "entrypoint:test-by-*",
  7309  										Sidecar:    "sidecar:test-by-*",
  7310  									},
  7311  									GCSConfiguration: &prowapi.GCSConfiguration{
  7312  										Bucket:       "test-bucket-by-*",
  7313  										PathStrategy: "single-by-*",
  7314  										DefaultOrg:   "org-by-*",
  7315  										DefaultRepo:  "repo-by-*",
  7316  									},
  7317  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7318  								},
  7319  							},
  7320  						},
  7321  					},
  7322  				},
  7323  			},
  7324  			expected: &prowapi.DecorationConfig{
  7325  				UtilityImages: &prowapi.UtilityImages{
  7326  					CloneRefs:  "clonerefs:test-by-*",
  7327  					InitUpload: "initupload:test-by-*",
  7328  					Entrypoint: "entrypoint:test-by-*",
  7329  					Sidecar:    "sidecar:test-by-*",
  7330  				},
  7331  				GCSConfiguration: &prowapi.GCSConfiguration{
  7332  					Bucket:       "test-bucket-by-*",
  7333  					PathStrategy: "single-by-*",
  7334  					DefaultOrg:   "org-by-*",
  7335  					DefaultRepo:  "repo-by-*",
  7336  				},
  7337  				GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7338  			},
  7339  		},
  7340  		{
  7341  			id:            "opt out of decorate_all_jobs by specifying undecorated",
  7342  			utilityConfig: UtilityConfig{Decorate: &no},
  7343  			config: &Config{
  7344  				JobConfig: JobConfig{
  7345  					DecorateAllJobs: true,
  7346  				},
  7347  				ProwConfig: ProwConfig{
  7348  					Plank: Plank{
  7349  						DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7350  							{
  7351  								OrgRepo: "*",
  7352  								Cluster: "",
  7353  								Config: &prowapi.DecorationConfig{
  7354  									UtilityImages: &prowapi.UtilityImages{
  7355  										CloneRefs:  "clonerefs:test-by-*",
  7356  										InitUpload: "initupload:test-by-*",
  7357  										Entrypoint: "entrypoint:test-by-*",
  7358  										Sidecar:    "sidecar:test-by-*",
  7359  									},
  7360  									GCSConfiguration: &prowapi.GCSConfiguration{
  7361  										Bucket:       "test-bucket-by-*",
  7362  										PathStrategy: "single-by-*",
  7363  										DefaultOrg:   "org-by-*",
  7364  										DefaultRepo:  "repo-by-*",
  7365  									},
  7366  									GCSCredentialsSecret: pStr("credentials-gcs-by-*"),
  7367  								},
  7368  							},
  7369  						},
  7370  					},
  7371  				},
  7372  			},
  7373  		},
  7374  		{
  7375  			id:     "no extraRefs[0] or cluster => use global + default cluster configs",
  7376  			config: complexConfig(),
  7377  			expected: &prowapi.DecorationConfig{
  7378  				UtilityImages: &prowapi.UtilityImages{
  7379  					CloneRefs:  "clonerefs:global",
  7380  					InitUpload: "initupload:global",
  7381  					Entrypoint: "entrypoint:global",
  7382  					Sidecar:    "sidecar:global",
  7383  				},
  7384  				GCSConfiguration: &prowapi.GCSConfiguration{
  7385  					Bucket:       "global",
  7386  					PathStrategy: "explicit",
  7387  				},
  7388  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  7389  			},
  7390  		},
  7391  		{
  7392  			id:      "extraRefs[0] has org and explicit 'default' cluster => use global + org + default cluster configs",
  7393  			config:  complexConfig(),
  7394  			cluster: "default",
  7395  			utilityConfig: UtilityConfig{
  7396  				ExtraRefs: []prowapi.Refs{
  7397  					{
  7398  						Org:  "org",
  7399  						Repo: "foo",
  7400  					},
  7401  				},
  7402  			},
  7403  			expected: &prowapi.DecorationConfig{
  7404  				UtilityImages: &prowapi.UtilityImages{
  7405  					CloneRefs:  "clonerefs:global",
  7406  					InitUpload: "initupload:global",
  7407  					Entrypoint: "entrypoint:global",
  7408  					Sidecar:    "sidecar:global",
  7409  				},
  7410  				GCSConfiguration: &prowapi.GCSConfiguration{
  7411  					Bucket:       "org-specific",
  7412  					PathStrategy: "explicit",
  7413  				},
  7414  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  7415  			},
  7416  		},
  7417  		{
  7418  			id:      "extraRefs[0] has repo and explicit 'trusted' cluster => use global + org + repo + trusted cluster configs",
  7419  			config:  complexConfig(),
  7420  			cluster: "trusted",
  7421  			utilityConfig: UtilityConfig{
  7422  				ExtraRefs: []prowapi.Refs{
  7423  					{
  7424  						Org:  "org",
  7425  						Repo: "repo",
  7426  					},
  7427  				},
  7428  			},
  7429  			expected: &prowapi.DecorationConfig{
  7430  				UtilityImages: &prowapi.UtilityImages{
  7431  					CloneRefs:  "clonerefs:global",
  7432  					InitUpload: "initupload:global",
  7433  					Entrypoint: "entrypoint:global",
  7434  					Sidecar:    "sidecar:global",
  7435  				},
  7436  				GCSConfiguration: &prowapi.GCSConfiguration{
  7437  					Bucket:       "repo-specific",
  7438  					PathStrategy: "explicit",
  7439  				},
  7440  				DefaultServiceAccountName: pStr("trusted-cluster-uses-SA"),
  7441  			},
  7442  		},
  7443  		{
  7444  			id:      "extraRefs[0] has override org and explicit 'trusted' cluster => use global + trusted cluster + override configs",
  7445  			config:  complexConfig(),
  7446  			cluster: "trusted",
  7447  			utilityConfig: UtilityConfig{
  7448  				ExtraRefs: []prowapi.Refs{
  7449  					{
  7450  						Org:  "override",
  7451  						Repo: "foo",
  7452  					},
  7453  				},
  7454  			},
  7455  			expected: &prowapi.DecorationConfig{
  7456  				UtilityImages: &prowapi.UtilityImages{
  7457  					CloneRefs:  "clonerefs:override",
  7458  					InitUpload: "initupload:global",
  7459  					Entrypoint: "entrypoint:global",
  7460  					Sidecar:    "sidecar:global",
  7461  				},
  7462  				GCSConfiguration: &prowapi.GCSConfiguration{
  7463  					Bucket:       "global",
  7464  					PathStrategy: "explicit",
  7465  				},
  7466  				DefaultServiceAccountName: pStr(""),
  7467  				GCSCredentialsSecret:      pStr("trusted-cluster-override-uses-secret"),
  7468  			},
  7469  		},
  7470  		{
  7471  			id:     "extraRefs[0] has override org and no cluster => use global + default cluster configs",
  7472  			config: complexConfig(),
  7473  			utilityConfig: UtilityConfig{
  7474  				ExtraRefs: []prowapi.Refs{
  7475  					{
  7476  						Org:  "override",
  7477  						Repo: "foo",
  7478  					},
  7479  				},
  7480  			},
  7481  			expected: &prowapi.DecorationConfig{
  7482  				UtilityImages: &prowapi.UtilityImages{
  7483  					CloneRefs:  "clonerefs:global",
  7484  					InitUpload: "initupload:global",
  7485  					Entrypoint: "entrypoint:global",
  7486  					Sidecar:    "sidecar:global",
  7487  				},
  7488  				GCSConfiguration: &prowapi.GCSConfiguration{
  7489  					Bucket:       "global",
  7490  					PathStrategy: "explicit",
  7491  				},
  7492  				GCSCredentialsSecret: pStr("default-cluster-uses-secret"),
  7493  			},
  7494  		},
  7495  	}
  7496  
  7497  	for _, tc := range testCases {
  7498  		t.Run(tc.id, func(t *testing.T) {
  7499  			c := &Config{}
  7500  			periodic := &Periodic{JobBase: JobBase{Cluster: tc.cluster, UtilityConfig: tc.utilityConfig}}
  7501  			c.defaultJobBase(&periodic.JobBase)
  7502  			setPeriodicDecorationDefaults(tc.config, periodic)
  7503  			if diff := cmp.Diff(periodic.DecorationConfig, tc.expected, cmpopts.EquateEmpty()); diff != "" {
  7504  				t.Error(diff)
  7505  			}
  7506  		})
  7507  	}
  7508  }
  7509  
  7510  func TestInRepoConfigEnabled(t *testing.T) {
  7511  	testCases := []struct {
  7512  		name     string
  7513  		config   Config
  7514  		expected bool
  7515  		testing  string
  7516  	}{
  7517  		{
  7518  			name: "Exact match",
  7519  			config: Config{
  7520  				ProwConfig: ProwConfig{
  7521  					InRepoConfig: InRepoConfig{
  7522  						Enabled: map[string]*bool{
  7523  							"org/repo": utilpointer.Bool(true),
  7524  						},
  7525  					},
  7526  				},
  7527  			},
  7528  			expected: true,
  7529  			testing:  "org/repo",
  7530  		},
  7531  		{
  7532  			name: "Orgname matches",
  7533  			config: Config{
  7534  				ProwConfig: ProwConfig{
  7535  					InRepoConfig: InRepoConfig{
  7536  						Enabled: map[string]*bool{
  7537  							"org": utilpointer.Bool(true),
  7538  						},
  7539  					},
  7540  				},
  7541  			},
  7542  			expected: true,
  7543  			testing:  "org/repo",
  7544  		},
  7545  		{
  7546  			name: "Globally enabled",
  7547  			config: Config{
  7548  				ProwConfig: ProwConfig{
  7549  					InRepoConfig: InRepoConfig{
  7550  						Enabled: map[string]*bool{
  7551  							"*": utilpointer.Bool(true),
  7552  						},
  7553  					},
  7554  				},
  7555  			},
  7556  			expected: true,
  7557  			testing:  "org/repo",
  7558  		},
  7559  		{
  7560  			name:     "Disabled by default",
  7561  			expected: false,
  7562  			testing:  "org/repo",
  7563  		},
  7564  		{
  7565  			name: "Gerrit format org Hostname matches",
  7566  			config: Config{
  7567  				ProwConfig: ProwConfig{
  7568  					InRepoConfig: InRepoConfig{
  7569  						Enabled: map[string]*bool{
  7570  							"host-name": utilpointer.Bool(true),
  7571  						},
  7572  					},
  7573  				},
  7574  			},
  7575  			expected: true,
  7576  			testing:  "host-name/extra/repo",
  7577  		},
  7578  		{
  7579  			name: "Gerrit format org Hostname matches with http",
  7580  			config: Config{
  7581  				ProwConfig: ProwConfig{
  7582  					InRepoConfig: InRepoConfig{
  7583  						Enabled: map[string]*bool{
  7584  							"host-name": utilpointer.Bool(true),
  7585  						},
  7586  					},
  7587  				},
  7588  			},
  7589  			expected: true,
  7590  			testing:  "http://host-name/extra/repo",
  7591  		},
  7592  		{
  7593  			name: "Gerrit format Just org Hostname matches",
  7594  			config: Config{
  7595  				ProwConfig: ProwConfig{
  7596  					InRepoConfig: InRepoConfig{
  7597  						Enabled: map[string]*bool{
  7598  							"host-name": utilpointer.Bool(true),
  7599  						},
  7600  					},
  7601  				},
  7602  			},
  7603  			expected: true,
  7604  			testing:  "host-name",
  7605  		},
  7606  		{
  7607  			name: "Gerrit format Just org Hostname matches with http",
  7608  			config: Config{
  7609  				ProwConfig: ProwConfig{
  7610  					InRepoConfig: InRepoConfig{
  7611  						Enabled: map[string]*bool{
  7612  							"host-name": utilpointer.Bool(true),
  7613  						},
  7614  					},
  7615  				},
  7616  			},
  7617  			expected: true,
  7618  			testing:  "http://host-name",
  7619  		},
  7620  		{
  7621  			name: "Gerrit format Just repo Hostname matches",
  7622  			config: Config{
  7623  				ProwConfig: ProwConfig{
  7624  					InRepoConfig: InRepoConfig{
  7625  						Enabled: map[string]*bool{
  7626  							"host-name/repo/name": utilpointer.Bool(true),
  7627  						},
  7628  					},
  7629  				},
  7630  			},
  7631  			expected: true,
  7632  			testing:  "host-name/repo/name",
  7633  		},
  7634  		{
  7635  			name: "Gerrit format Just org Hostname matches with http",
  7636  			config: Config{
  7637  				ProwConfig: ProwConfig{
  7638  					InRepoConfig: InRepoConfig{
  7639  						Enabled: map[string]*bool{
  7640  							"host-name/repo/name": utilpointer.Bool(true),
  7641  						},
  7642  					},
  7643  				},
  7644  			},
  7645  			expected: true,
  7646  			testing:  "http://host-name/repo/name",
  7647  		},
  7648  	}
  7649  
  7650  	for idx := range testCases {
  7651  		tc := testCases[idx]
  7652  		t.Run(tc.name, func(t *testing.T) {
  7653  			t.Parallel()
  7654  
  7655  			if result := tc.config.InRepoConfigEnabled(tc.testing); result != tc.expected {
  7656  				t.Errorf("Expected %t, got %t", tc.expected, result)
  7657  			}
  7658  		})
  7659  	}
  7660  }
  7661  
  7662  func TestGetProwYAMLDoesNotCallRefGettersWhenInrepoconfigIsDisabled(t *testing.T) {
  7663  	t.Parallel()
  7664  
  7665  	var baseSHAGetterCalled, headSHAGetterCalled bool
  7666  	baseSHAGetter := func() (string, error) {
  7667  		baseSHAGetterCalled = true
  7668  		return "", nil
  7669  	}
  7670  	headSHAGetter := func() (string, error) {
  7671  		headSHAGetterCalled = true
  7672  		return "", nil
  7673  	}
  7674  
  7675  	c := &Config{}
  7676  	if _, err := c.getProwYAMLWithDefaults(nil, "test", "main", baseSHAGetter, headSHAGetter); err != nil {
  7677  		t.Fatalf("error calling GetProwYAML: %v", err)
  7678  	}
  7679  	if baseSHAGetterCalled {
  7680  		t.Error("baseSHAGetter got called")
  7681  	}
  7682  	if headSHAGetterCalled {
  7683  		t.Error("headSHAGetter got called")
  7684  	}
  7685  }
  7686  
  7687  func TestGetPresubmitsReturnsStaticAndInrepoconfigPresubmits(t *testing.T) {
  7688  	t.Parallel()
  7689  
  7690  	org, repo := "org", "repo"
  7691  	c := &Config{
  7692  		ProwConfig: ProwConfig{
  7693  			InRepoConfig: InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}},
  7694  		},
  7695  		JobConfig: JobConfig{
  7696  			PresubmitsStatic: map[string][]Presubmit{
  7697  				org + "/" + repo: {{
  7698  					JobBase:  JobBase{Name: "my-static-presubmit"},
  7699  					Reporter: Reporter{Context: "my-static-presubmit"},
  7700  				}},
  7701  			},
  7702  			ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory(
  7703  				[]Presubmit{
  7704  					{
  7705  						JobBase: JobBase{Name: "hans"},
  7706  					},
  7707  				},
  7708  				nil,
  7709  			),
  7710  		},
  7711  	}
  7712  
  7713  	presubmits, err := c.GetPresubmits(nil, org+"/"+repo, "main", func() (string, error) { return "", nil })
  7714  	if err != nil {
  7715  		t.Fatalf("Error calling GetPresubmits: %v", err)
  7716  	}
  7717  
  7718  	if n := len(presubmits); n != 2 ||
  7719  		presubmits[0].Name != "my-static-presubmit" ||
  7720  		presubmits[1].Name != "hans" {
  7721  		t.Errorf(`expected exactly two presubmits named "my-static-presubmit" and "hans", got %d (%v)`, n, presubmits)
  7722  	}
  7723  }
  7724  
  7725  func TestGetPostsubmitsReturnsStaticAndInrepoconfigPostsubmits(t *testing.T) {
  7726  	t.Parallel()
  7727  
  7728  	org, repo := "org", "repo"
  7729  	c := &Config{
  7730  		ProwConfig: ProwConfig{
  7731  			InRepoConfig: InRepoConfig{Enabled: map[string]*bool{"*": utilpointer.Bool(true)}},
  7732  		},
  7733  		JobConfig: JobConfig{
  7734  			PostsubmitsStatic: map[string][]Postsubmit{
  7735  				org + "/" + repo: {{
  7736  					JobBase:  JobBase{Name: "my-static-postsubmits"},
  7737  					Reporter: Reporter{Context: "my-static-postsubmits"},
  7738  				}},
  7739  			},
  7740  			ProwYAMLGetterWithDefaults: fakeProwYAMLGetterFactory(
  7741  				nil,
  7742  				[]Postsubmit{
  7743  					{
  7744  						JobBase: JobBase{Name: "hans"},
  7745  					},
  7746  				},
  7747  			),
  7748  		},
  7749  	}
  7750  
  7751  	postsubmits, err := c.GetPostsubmits(nil, org+"/"+repo, "main", func() (string, error) { return "", nil })
  7752  	if err != nil {
  7753  		t.Fatalf("Error calling GetPostsubmits: %v", err)
  7754  	}
  7755  
  7756  	if n := len(postsubmits); n != 2 ||
  7757  		postsubmits[0].Name != "my-static-postsubmits" ||
  7758  		postsubmits[1].Name != "hans" {
  7759  		t.Errorf(`expected exactly two postsubmits named "my-static-postsubmits" and "hans", got %d (%v)`, n, postsubmits)
  7760  	}
  7761  }
  7762  
  7763  func TestInRepoConfigAllowsCluster(t *testing.T) {
  7764  	const clusterName = "that-cluster"
  7765  
  7766  	testCases := []struct {
  7767  		name            string
  7768  		repoIdentifier  string
  7769  		allowedClusters map[string][]string
  7770  
  7771  		expectedResult bool
  7772  	}{
  7773  		{
  7774  			name:           "Nothing configured, nothing allowed",
  7775  			repoIdentifier: "foo",
  7776  			expectedResult: false,
  7777  		},
  7778  		{
  7779  			name:            "Allowed on repolevel",
  7780  			repoIdentifier:  "foo/repo",
  7781  			allowedClusters: map[string][]string{"foo/repo": {clusterName}},
  7782  			expectedResult:  true,
  7783  		},
  7784  		{
  7785  			name:            "Not allowed on repolevel",
  7786  			repoIdentifier:  "foo/repo",
  7787  			allowedClusters: map[string][]string{"foo/repo": {"different-cluster"}},
  7788  			expectedResult:  false,
  7789  		},
  7790  		{
  7791  			name:            "Allowed for different repo",
  7792  			repoIdentifier:  "foo/repo",
  7793  			allowedClusters: map[string][]string{"bar/repo": {clusterName}},
  7794  			expectedResult:  false,
  7795  		},
  7796  		{
  7797  			name:            "Allowed on orglevel",
  7798  			repoIdentifier:  "foo/repo",
  7799  			allowedClusters: map[string][]string{"foo": {clusterName}},
  7800  			expectedResult:  true,
  7801  		},
  7802  		{
  7803  			name:            "Not allowed on orglevel",
  7804  			repoIdentifier:  "foo/repo",
  7805  			allowedClusters: map[string][]string{"foo": {"different-cluster"}},
  7806  			expectedResult:  false,
  7807  		},
  7808  		{
  7809  			name:            "Allowed on for different org",
  7810  			repoIdentifier:  "foo/repo",
  7811  			allowedClusters: map[string][]string{"bar": {clusterName}},
  7812  			expectedResult:  false,
  7813  		},
  7814  		{
  7815  			name:            "Allowed globally",
  7816  			repoIdentifier:  "foo/repo",
  7817  			allowedClusters: map[string][]string{"*": {clusterName}},
  7818  			expectedResult:  true,
  7819  		},
  7820  		{
  7821  			name:            "Allowed for gerrit host",
  7822  			repoIdentifier:  "https://host/repo/name",
  7823  			allowedClusters: map[string][]string{"host": {clusterName}},
  7824  			expectedResult:  true,
  7825  		},
  7826  		{
  7827  			name:            "Allowed for gerrit repo",
  7828  			repoIdentifier:  "https://host/repo/name",
  7829  			allowedClusters: map[string][]string{"host/repo/name": {clusterName}},
  7830  			expectedResult:  true,
  7831  		},
  7832  		{
  7833  			name:            "Allowed for gerrit repo",
  7834  			repoIdentifier:  "host",
  7835  			allowedClusters: map[string][]string{"host": {clusterName}},
  7836  			expectedResult:  true,
  7837  		},
  7838  		{
  7839  			name:            "Allowed for gerrit repo",
  7840  			repoIdentifier:  "host/repo/name",
  7841  			allowedClusters: map[string][]string{"host": {clusterName}},
  7842  			expectedResult:  true,
  7843  		},
  7844  	}
  7845  
  7846  	for idx := range testCases {
  7847  		tc := testCases[idx]
  7848  		t.Run(tc.name, func(t *testing.T) {
  7849  			t.Parallel()
  7850  
  7851  			cfg := &Config{
  7852  				ProwConfig: ProwConfig{InRepoConfig: InRepoConfig{AllowedClusters: tc.allowedClusters}},
  7853  			}
  7854  
  7855  			if actual := cfg.InRepoConfigAllowsCluster(clusterName, tc.repoIdentifier); actual != tc.expectedResult {
  7856  				t.Errorf("expected result %t, got result %t", tc.expectedResult, actual)
  7857  			}
  7858  		})
  7859  	}
  7860  }
  7861  
  7862  func TestMergeDefaultDecorationConfigThreadSafety(t *testing.T) {
  7863  	const repo = "org/repo"
  7864  	const cluster = "default"
  7865  	p := Plank{DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{
  7866  		{
  7867  			OrgRepo: "*",
  7868  			Cluster: "*",
  7869  			Config: &prowapi.DecorationConfig{
  7870  				GCSConfiguration: &prowapi.GCSConfiguration{
  7871  					MediaTypes: map[string]string{"text": "text"},
  7872  				},
  7873  				GCSCredentialsSecret: pStr("service-account-secret"),
  7874  			},
  7875  		},
  7876  		{
  7877  			OrgRepo: repo,
  7878  			Cluster: "*",
  7879  			Config: &prowapi.DecorationConfig{
  7880  				GCSConfiguration: &prowapi.GCSConfiguration{
  7881  					MediaTypes: map[string]string{"text": "text2"},
  7882  				},
  7883  			},
  7884  		},
  7885  		{
  7886  			OrgRepo: "*",
  7887  			Cluster: cluster,
  7888  			Config: &prowapi.DecorationConfig{
  7889  				DefaultServiceAccountName: pStr("service-account-name"),
  7890  				GCSCredentialsSecret:      pStr(""),
  7891  			},
  7892  		},
  7893  	}}
  7894  	jobDC := &prowapi.DecorationConfig{
  7895  		GCSConfiguration: &prowapi.GCSConfiguration{
  7896  			Bucket: "special-bucket",
  7897  		},
  7898  	}
  7899  
  7900  	s1 := make(chan struct{})
  7901  	s2 := make(chan struct{})
  7902  
  7903  	go func() {
  7904  		_ = p.mergeDefaultDecorationConfig(repo, cluster, jobDC)
  7905  		close(s1)
  7906  	}()
  7907  	go func() {
  7908  		_ = p.mergeDefaultDecorationConfig(repo, cluster, jobDC)
  7909  		close(s2)
  7910  	}()
  7911  
  7912  	<-s1
  7913  	<-s2
  7914  }
  7915  
  7916  func TestDefaultAndValidateReportTemplate(t *testing.T) {
  7917  	testCases := []struct {
  7918  		id          string
  7919  		controller  *Controller
  7920  		expected    *Controller
  7921  		expectedErr bool
  7922  	}{
  7923  
  7924  		{
  7925  			id:         "no report_template or report_templates specified, no changes expected",
  7926  			controller: &Controller{},
  7927  			expected:   &Controller{},
  7928  		},
  7929  
  7930  		{
  7931  			id:         "only report_template specified, expected report_template[*]=report_template",
  7932  			controller: &Controller{ReportTemplateString: "test template"},
  7933  			expected: &Controller{
  7934  				ReportTemplateString:  "test template",
  7935  				ReportTemplateStrings: map[string]string{"*": "test template"},
  7936  				ReportTemplates: map[string]*template.Template{
  7937  					"*": func() *template.Template {
  7938  						reportTmpl, _ := template.New("Report").Parse("test template")
  7939  						return reportTmpl
  7940  					}(),
  7941  				},
  7942  			},
  7943  		},
  7944  
  7945  		{
  7946  			id:         "only report_templates specified, expected direct conversion",
  7947  			controller: &Controller{ReportTemplateStrings: map[string]string{"*": "test template"}},
  7948  			expected: &Controller{
  7949  				ReportTemplateStrings: map[string]string{"*": "test template"},
  7950  				ReportTemplates: map[string]*template.Template{
  7951  					"*": func() *template.Template {
  7952  						reportTmpl, _ := template.New("Report").Parse("test template")
  7953  						return reportTmpl
  7954  					}(),
  7955  				},
  7956  			},
  7957  		},
  7958  
  7959  		{
  7960  			id:          "no '*' in report_templates specified, expected error",
  7961  			controller:  &Controller{ReportTemplateStrings: map[string]string{"org": "test template"}},
  7962  			expectedErr: true,
  7963  		},
  7964  	}
  7965  
  7966  	for _, tc := range testCases {
  7967  		t.Run(tc.id, func(t *testing.T) {
  7968  			if err := defaultAndValidateReportTemplate(tc.controller); err != nil && !tc.expectedErr {
  7969  				t.Fatalf("error not expected: %v", err)
  7970  			}
  7971  
  7972  			if !reflect.DeepEqual(tc.controller, tc.expected) && !tc.expectedErr {
  7973  				t.Fatalf("\nGot: %#v\nExpected: %#v", tc.controller, tc.expected)
  7974  			}
  7975  		})
  7976  	}
  7977  }
  7978  
  7979  func TestValidatePresubmits(t *testing.T) {
  7980  	t.Parallel()
  7981  	testCases := []struct {
  7982  		name          string
  7983  		presubmits    []Presubmit
  7984  		expectedError string
  7985  	}{
  7986  		{
  7987  			name: "Duplicate context causes error",
  7988  			presubmits: []Presubmit{
  7989  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "repeated"}},
  7990  				{JobBase: JobBase{Name: "b"}, Reporter: Reporter{Context: "repeated"}},
  7991  			},
  7992  			expectedError: `[jobs b and a report to the same GitHub context "repeated", jobs a and b report to the same GitHub context "repeated"]`,
  7993  		},
  7994  		{
  7995  			name: "Duplicate context on different branch doesn't cause error",
  7996  			presubmits: []Presubmit{
  7997  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "repeated"}, Brancher: Brancher{Branches: []string{"master"}}},
  7998  				{JobBase: JobBase{Name: "b"}, Reporter: Reporter{Context: "repeated"}, Brancher: Brancher{Branches: []string{"next"}}},
  7999  			},
  8000  		},
  8001  		{
  8002  			name: "Duplicate jobname causes error",
  8003  			presubmits: []Presubmit{
  8004  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "foo"}},
  8005  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "bar"}},
  8006  			},
  8007  			expectedError: "duplicated presubmit jobs (consider both inrepo and central config): [a]",
  8008  		},
  8009  		{
  8010  			name: "Duplicate jobname on different branches doesn't cause error",
  8011  			presubmits: []Presubmit{
  8012  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "foo"}, Brancher: Brancher{Branches: []string{"master"}}},
  8013  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "foo"}, Brancher: Brancher{Branches: []string{"next"}}},
  8014  			},
  8015  		},
  8016  		{
  8017  			name:          "Invalid JobBase causes error",
  8018  			presubmits:    []Presubmit{{Reporter: Reporter{Context: "foo"}}},
  8019  			expectedError: `invalid presubmit job : name: must match regex "^[A-Za-z0-9-._]+$"`,
  8020  		},
  8021  		{
  8022  			name:          "Invalid triggering config causes error",
  8023  			presubmits:    []Presubmit{{Trigger: "some-trigger", JobBase: JobBase{Name: "my-job"}, Reporter: Reporter{Context: "foo"}}},
  8024  			expectedError: `either both of job.Trigger and job.RerunCommand must be set, wasnt the case for job "my-job"`,
  8025  		},
  8026  		{
  8027  			name:          "Invalid reporting config causes error",
  8028  			presubmits:    []Presubmit{{JobBase: JobBase{Name: "my-job"}}},
  8029  			expectedError: "invalid presubmit job my-job: job is set to report but has no context configured",
  8030  		},
  8031  		{
  8032  			name: "Mutually exclusive settings: always_run and run_if_changed",
  8033  			presubmits: []Presubmit{{
  8034  				JobBase:             JobBase{Name: "a"},
  8035  				Reporter:            Reporter{Context: "foo"},
  8036  				AlwaysRun:           true,
  8037  				RegexpChangeMatcher: RegexpChangeMatcher{RunIfChanged: `\.go$`},
  8038  			}},
  8039  			expectedError: "job a is set to always run but also declares run_if_changed targets, which are mutually exclusive",
  8040  		},
  8041  		{
  8042  			name: "Mutually exclusive settings: always_run and skip_if_only_changed",
  8043  			presubmits: []Presubmit{{
  8044  				JobBase:             JobBase{Name: "a"},
  8045  				Reporter:            Reporter{Context: "foo"},
  8046  				AlwaysRun:           true,
  8047  				RegexpChangeMatcher: RegexpChangeMatcher{SkipIfOnlyChanged: `\.go$`},
  8048  			}},
  8049  			expectedError: "job a is set to always run but also declares skip_if_only_changed targets, which are mutually exclusive",
  8050  		},
  8051  		{
  8052  			name: "Mutually exclusive settings: run_if_changed and skip_if_only_changed",
  8053  			presubmits: []Presubmit{{
  8054  				JobBase:  JobBase{Name: "a"},
  8055  				Reporter: Reporter{Context: "foo"},
  8056  				RegexpChangeMatcher: RegexpChangeMatcher{
  8057  					RunIfChanged:      `\.go$`,
  8058  					SkipIfOnlyChanged: `\.md`,
  8059  				},
  8060  			}},
  8061  			expectedError: "job a declares run_if_changed and skip_if_only_changed, which are mutually exclusive",
  8062  		},
  8063  	}
  8064  
  8065  	for _, tc := range testCases {
  8066  		var errMsg string
  8067  		err := Config{}.validatePresubmits(tc.presubmits)
  8068  		if err != nil {
  8069  			errMsg = err.Error()
  8070  		}
  8071  		if errMsg != tc.expectedError {
  8072  			t.Errorf("expected error '%s', got error '%s'", tc.expectedError, errMsg)
  8073  		}
  8074  	}
  8075  }
  8076  
  8077  func TestValidatePostsubmits(t *testing.T) {
  8078  	t.Parallel()
  8079  	true_ := true
  8080  	testCases := []struct {
  8081  		name          string
  8082  		postsubmits   []Postsubmit
  8083  		expectedError string
  8084  	}{
  8085  		{
  8086  			name: "Duplicate context causes error",
  8087  			postsubmits: []Postsubmit{
  8088  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "repeated"}},
  8089  				{JobBase: JobBase{Name: "b"}, Reporter: Reporter{Context: "repeated"}},
  8090  			},
  8091  			expectedError: `[jobs b and a report to the same GitHub context "repeated", jobs a and b report to the same GitHub context "repeated"]`,
  8092  		},
  8093  		{
  8094  			name: "Duplicate context on different branch doesn't cause error",
  8095  			postsubmits: []Postsubmit{
  8096  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "repeated"}, Brancher: Brancher{Branches: []string{"master"}}},
  8097  				{JobBase: JobBase{Name: "b"}, Reporter: Reporter{Context: "repeated"}, Brancher: Brancher{Branches: []string{"next"}}},
  8098  			},
  8099  		},
  8100  		{
  8101  			name: "Duplicate jobname causes error",
  8102  			postsubmits: []Postsubmit{
  8103  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "foo"}},
  8104  				{JobBase: JobBase{Name: "a"}, Reporter: Reporter{Context: "bar"}},
  8105  			},
  8106  			expectedError: "duplicated postsubmit jobs (consider both inrepo and central config): [a]",
  8107  		},
  8108  		{
  8109  			name:          "Invalid JobBase causes error",
  8110  			postsubmits:   []Postsubmit{{Reporter: Reporter{Context: "foo"}}},
  8111  			expectedError: `invalid postsubmit job : name: must match regex "^[A-Za-z0-9-._]+$"`,
  8112  		},
  8113  		{
  8114  			name:          "Invalid reporting config causes error",
  8115  			postsubmits:   []Postsubmit{{JobBase: JobBase{Name: "my-job"}}},
  8116  			expectedError: "invalid postsubmit job my-job: job is set to report but has no context configured",
  8117  		},
  8118  		{
  8119  			name: "Mutually exclusive settings: always_run and run_if_changed",
  8120  			postsubmits: []Postsubmit{{
  8121  				JobBase:             JobBase{Name: "a"},
  8122  				Reporter:            Reporter{Context: "foo"},
  8123  				AlwaysRun:           &true_,
  8124  				RegexpChangeMatcher: RegexpChangeMatcher{RunIfChanged: `\.go$`},
  8125  			}},
  8126  			expectedError: "job a is set to always run but also declares run_if_changed targets, which are mutually exclusive",
  8127  		},
  8128  		{
  8129  			name: "Mutually exclusive settings: always_run and skip_if_only_changed",
  8130  			postsubmits: []Postsubmit{{
  8131  				JobBase:             JobBase{Name: "a"},
  8132  				Reporter:            Reporter{Context: "foo"},
  8133  				AlwaysRun:           &true_,
  8134  				RegexpChangeMatcher: RegexpChangeMatcher{SkipIfOnlyChanged: `\.go$`},
  8135  			}},
  8136  			expectedError: "job a is set to always run but also declares skip_if_only_changed targets, which are mutually exclusive",
  8137  		},
  8138  		{
  8139  			name: "Mutually exclusive settings: run_if_changed and skip_if_only_changed",
  8140  			postsubmits: []Postsubmit{{
  8141  				JobBase:  JobBase{Name: "a"},
  8142  				Reporter: Reporter{Context: "foo"},
  8143  				RegexpChangeMatcher: RegexpChangeMatcher{
  8144  					RunIfChanged:      `\.go$`,
  8145  					SkipIfOnlyChanged: `\.md`,
  8146  				},
  8147  			}},
  8148  			expectedError: "job a declares run_if_changed and skip_if_only_changed, which are mutually exclusive",
  8149  		},
  8150  	}
  8151  
  8152  	for _, tc := range testCases {
  8153  		var errMsg string
  8154  		err := Config{}.validatePostsubmits(tc.postsubmits)
  8155  		if err != nil {
  8156  			errMsg = err.Error()
  8157  		}
  8158  		if errMsg != tc.expectedError {
  8159  			t.Errorf("expected error '%s', got error '%s'", tc.expectedError, errMsg)
  8160  		}
  8161  	}
  8162  }
  8163  
  8164  func TestValidatePeriodics(t *testing.T) {
  8165  	t.Parallel()
  8166  	testCases := []struct {
  8167  		name          string
  8168  		periodics     []Periodic
  8169  		expected      []Periodic
  8170  		expectedError string
  8171  	}{
  8172  		{
  8173  			name: "Duplicate jobname causes error",
  8174  			periodics: []Periodic{
  8175  				{JobBase: JobBase{Name: "a"}, Interval: "6h"},
  8176  				{JobBase: JobBase{Name: "a"}, Interval: "12h"},
  8177  			},
  8178  			expectedError: "duplicated periodic job: a",
  8179  		},
  8180  		{
  8181  			name: "Mutually exclusive settings: cron, interval, and minimal_interval",
  8182  			periodics: []Periodic{
  8183  				{JobBase: JobBase{Name: "a"}, Interval: "6h", MinimumInterval: "6h"},
  8184  			},
  8185  			expectedError: "cron, interval, and minimum_interval are mutually exclusive in periodic a",
  8186  		},
  8187  		{
  8188  			name: "Required settings: cron, interval, or minimal_interval",
  8189  			periodics: []Periodic{
  8190  				{JobBase: JobBase{Name: "a"}},
  8191  			},
  8192  			expectedError: "at least one of cron, interval, or minimum_interval must be set in periodic a",
  8193  		},
  8194  		{
  8195  			name: "Invalid cron string",
  8196  			periodics: []Periodic{
  8197  				{JobBase: JobBase{Name: "a"}, Cron: "hello"},
  8198  			},
  8199  			expectedError: "invalid cron string hello in periodic a: Expected 5 or 6 fields, found 1: hello",
  8200  		},
  8201  		{
  8202  			name: "Invalid interval",
  8203  			periodics: []Periodic{
  8204  				{JobBase: JobBase{Name: "a"}, Interval: "hello"},
  8205  			},
  8206  			expectedError: "cannot parse duration for a: time: invalid duration \"hello\"",
  8207  		},
  8208  		{
  8209  			name: "Invalid minimum_interval",
  8210  			periodics: []Periodic{
  8211  				{JobBase: JobBase{Name: "a"}, MinimumInterval: "hello"},
  8212  			},
  8213  			expectedError: "cannot parse duration for a: time: invalid duration \"hello\"",
  8214  		},
  8215  		{
  8216  			name: "Sets interval",
  8217  			periodics: []Periodic{
  8218  				{JobBase: JobBase{Name: "a"}, Interval: "10ns"},
  8219  			},
  8220  			expected: []Periodic{
  8221  				{JobBase: JobBase{Name: "a"}, Interval: "10ns", interval: time.Duration(10)},
  8222  			},
  8223  		},
  8224  		{
  8225  			name: "Sets minimum_interval",
  8226  			periodics: []Periodic{
  8227  				{JobBase: JobBase{Name: "a"}, MinimumInterval: "10ns"},
  8228  			},
  8229  			expected: []Periodic{
  8230  				{JobBase: JobBase{Name: "a"}, MinimumInterval: "10ns", minimum_interval: time.Duration(10)},
  8231  			},
  8232  		},
  8233  	}
  8234  
  8235  	for _, tc := range testCases {
  8236  		var errMsg string
  8237  		err := Config{}.validatePeriodics(tc.periodics)
  8238  		if err != nil {
  8239  			errMsg = err.Error()
  8240  		}
  8241  		if len(tc.expected) > 0 && !reflect.DeepEqual(tc.periodics, tc.expected) {
  8242  			t.Errorf("expected '%v', got '%v'", tc.expected, tc.periodics)
  8243  		}
  8244  		if tc.expectedError != "" && errMsg != tc.expectedError {
  8245  			t.Errorf("expected error '%s', got error '%s'", tc.expectedError, errMsg)
  8246  		}
  8247  	}
  8248  }
  8249  
  8250  func TestValidateStorageBucket(t *testing.T) {
  8251  	testCases := []struct {
  8252  		name        string
  8253  		yaml        string
  8254  		bucket      string
  8255  		expectedErr string
  8256  	}{
  8257  		{
  8258  			name:        "unspecified config means no validation",
  8259  			yaml:        ``,
  8260  			bucket:      "who-knows",
  8261  			expectedErr: "",
  8262  		},
  8263  		{
  8264  			name: "validation disabled",
  8265  			yaml: `
  8266  deck:
  8267      skip_storage_path_validation: true`,
  8268  			bucket:      "random-unknown-bucket",
  8269  			expectedErr: "",
  8270  		},
  8271  		{
  8272  			name: "validation enabled",
  8273  			yaml: `
  8274  deck:
  8275      skip_storage_path_validation: false`,
  8276  			bucket:      "random-unknown-bucket",
  8277  			expectedErr: "bucket \"random-unknown-bucket\" not in allowed list",
  8278  		},
  8279  		{
  8280  			name: "DecorationConfig allowed bucket",
  8281  			yaml: `
  8282  deck:
  8283      skip_storage_path_validation: false
  8284  plank:
  8285      default_decoration_configs:
  8286          '*':
  8287              gcs_configuration:
  8288                  bucket: "kubernetes-jenkins"`,
  8289  			bucket:      "kubernetes-jenkins",
  8290  			expectedErr: "",
  8291  		},
  8292  		{
  8293  			name: "custom allowed bucket",
  8294  			yaml: `
  8295  deck:
  8296      skip_storage_path_validation: false
  8297      additional_allowed_buckets:
  8298      - "kubernetes-prow"`,
  8299  			bucket:      "kubernetes-prow",
  8300  			expectedErr: "",
  8301  		},
  8302  		{
  8303  			name: "unknown bucket path",
  8304  			yaml: `
  8305  deck:
  8306      skip_storage_path_validation: false`,
  8307  			bucket:      "istio-prow",
  8308  			expectedErr: "bucket \"istio-prow\" not in allowed list",
  8309  		},
  8310  	}
  8311  
  8312  	for _, tc := range testCases {
  8313  		t.Run(tc.name, func(nested *testing.T) {
  8314  			cfg, err := loadConfigYaml(tc.yaml, nested)
  8315  			if err != nil {
  8316  				nested.Fatalf("failed to load prow config: err=%v\nYAML=%v", err, tc.yaml)
  8317  			}
  8318  			expectingErr := len(tc.expectedErr) > 0
  8319  
  8320  			err = cfg.ValidateStorageBucket(tc.bucket)
  8321  
  8322  			if expectingErr && err == nil {
  8323  				nested.Fatalf("no errors, but was expecting error: %v", tc.expectedErr)
  8324  			}
  8325  			if err != nil && !expectingErr {
  8326  				nested.Fatalf("expecting no errors, but got: %v", err)
  8327  			}
  8328  			if expectingErr && err != nil && !strings.Contains(err.Error(), tc.expectedErr) {
  8329  				nested.Fatalf("expecting error substring \"%v\", but got error: %v", tc.expectedErr, err)
  8330  			}
  8331  		})
  8332  	}
  8333  }
  8334  
  8335  func loadConfigYaml(prowConfigYaml string, t *testing.T, supplementalProwConfigs ...string) (*Config, error) {
  8336  	prowConfigDir := t.TempDir()
  8337  
  8338  	prowConfig := filepath.Join(prowConfigDir, "config.yaml")
  8339  	if err := os.WriteFile(prowConfig, []byte(prowConfigYaml), 0666); err != nil {
  8340  		t.Fatalf("fail to write prow config: %v", err)
  8341  	}
  8342  
  8343  	var supplementalProwConfigDirs []string
  8344  	for idx, cfg := range supplementalProwConfigs {
  8345  		dir := filepath.Join(prowConfigDir, strconv.Itoa(idx))
  8346  		supplementalProwConfigDirs = append(supplementalProwConfigDirs, dir)
  8347  		if err := os.Mkdir(dir, 0755); err != nil {
  8348  			t.Fatalf("failed to create dir %s for supplemental prow config: %v", dir, err)
  8349  		}
  8350  
  8351  		// use a random prefix for the file to make sure that the loading correctly loads all supplemental configs with the
  8352  		// right suffix.
  8353  		if err := os.WriteFile(filepath.Join(dir, strconv.Itoa(time.Now().Nanosecond())+"_prowconfig.yaml"), []byte(cfg), 0644); err != nil {
  8354  			t.Fatalf("failed to write supplemental prow config: %v", err)
  8355  		}
  8356  	}
  8357  
  8358  	return Load(prowConfig, "", supplementalProwConfigDirs, "_prowconfig.yaml")
  8359  }
  8360  
  8361  func TestProwConfigMerging(t *testing.T) {
  8362  	t.Parallel()
  8363  	testCases := []struct {
  8364  		name                    string
  8365  		prowConfig              string
  8366  		supplementalProwConfigs []string
  8367  		expectedErrorSubstr     string
  8368  		expectedProwConfig      string
  8369  	}{
  8370  		{
  8371  			name:       "Additional branch protection config gets merged in",
  8372  			prowConfig: "config_version_sha: abc",
  8373  			supplementalProwConfigs: []string{
  8374  				`
  8375  branch-protection:
  8376    allow_disabled_job_policies: true`,
  8377  			},
  8378  			expectedProwConfig: `branch-protection:
  8379    allow_disabled_job_policies: true
  8380  config_version_sha: abc
  8381  deck:
  8382    spyglass:
  8383      gcs_browser_prefixes:
  8384        '*': ""
  8385      gcs_browser_prefixes_by_bucket:
  8386        '*': ""
  8387      size_limit: 100000000
  8388    tide_update_period: 10s
  8389  default_job_timeout: 24h0m0s
  8390  gangway: {}
  8391  gerrit:
  8392    ratelimit: 5
  8393    tick_interval: 1m0s
  8394  github:
  8395    link_url: https://github.com
  8396  github_reporter:
  8397    job_types_to_report:
  8398    - presubmit
  8399    - postsubmit
  8400  horologium: {}
  8401  in_repo_config:
  8402    allowed_clusters:
  8403      '*':
  8404      - default
  8405  log_level: info
  8406  managed_webhooks:
  8407    auto_accept_invitation: false
  8408    respect_legacy_global_token: false
  8409  moonraker:
  8410    client_timeout: 10m0s
  8411  plank:
  8412    max_goroutines: 20
  8413    pod_pending_timeout: 10m0s
  8414    pod_running_timeout: 48h0m0s
  8415    pod_unscheduled_timeout: 5m0s
  8416  pod_namespace: default
  8417  prowjob_namespace: default
  8418  push_gateway:
  8419    interval: 1m0s
  8420    serve_metrics: false
  8421  scheduler: {}
  8422  sinker:
  8423    max_pod_age: 24h0m0s
  8424    max_prowjob_age: 168h0m0s
  8425    resync_period: 1h0m0s
  8426    terminated_pod_ttl: 24h0m0s
  8427  status_error_link: https://github.com/kubernetes/test-infra/issues
  8428  tide:
  8429    context_options: {}
  8430    max_goroutines: 20
  8431    status_update_period: 1m0s
  8432    sync_period: 1m0s
  8433  `,
  8434  		},
  8435  		{
  8436  			name:       "Additional branch protection config with duplication errors",
  8437  			prowConfig: "config_version_sha: abc",
  8438  			supplementalProwConfigs: []string{
  8439  				`
  8440  branch-protection:
  8441    allow_disabled_job_policies: true`,
  8442  				`
  8443  branch-protection:
  8444    allow_disabled_job_policies: true`,
  8445  			},
  8446  			expectedErrorSubstr: "both branchprotection configs set allow_disabled_job_policies",
  8447  		},
  8448  		{
  8449  			name:       "Config not supported by merge logic errors",
  8450  			prowConfig: "config_version_sha: abc",
  8451  			supplementalProwConfigs: []string{
  8452  				`
  8453  plank:
  8454    JobURLPrefixDisableAppendStorageProvider: true`,
  8455  			},
  8456  			expectedErrorSubstr: "may be set via additional config, all other fields have no merging logic yet. Diff:",
  8457  		},
  8458  		{
  8459  			name: "Additional merge method config gets merged in",
  8460  			supplementalProwConfigs: []string{`
  8461  tide:
  8462    merge_method:
  8463      foo/bar: squash`},
  8464  			expectedProwConfig: `branch-protection: {}
  8465  deck:
  8466    spyglass:
  8467      gcs_browser_prefixes:
  8468        '*': ""
  8469      gcs_browser_prefixes_by_bucket:
  8470        '*': ""
  8471      size_limit: 100000000
  8472    tide_update_period: 10s
  8473  default_job_timeout: 24h0m0s
  8474  gangway: {}
  8475  gerrit:
  8476    ratelimit: 5
  8477    tick_interval: 1m0s
  8478  github:
  8479    link_url: https://github.com
  8480  github_reporter:
  8481    job_types_to_report:
  8482    - presubmit
  8483    - postsubmit
  8484  horologium: {}
  8485  in_repo_config:
  8486    allowed_clusters:
  8487      '*':
  8488      - default
  8489  log_level: info
  8490  managed_webhooks:
  8491    auto_accept_invitation: false
  8492    respect_legacy_global_token: false
  8493  moonraker:
  8494    client_timeout: 10m0s
  8495  plank:
  8496    max_goroutines: 20
  8497    pod_pending_timeout: 10m0s
  8498    pod_running_timeout: 48h0m0s
  8499    pod_unscheduled_timeout: 5m0s
  8500  pod_namespace: default
  8501  prowjob_namespace: default
  8502  push_gateway:
  8503    interval: 1m0s
  8504    serve_metrics: false
  8505  scheduler: {}
  8506  sinker:
  8507    max_pod_age: 24h0m0s
  8508    max_prowjob_age: 168h0m0s
  8509    resync_period: 1h0m0s
  8510    terminated_pod_ttl: 24h0m0s
  8511  status_error_link: https://github.com/kubernetes/test-infra/issues
  8512  tide:
  8513    context_options: {}
  8514    max_goroutines: 20
  8515    merge_method:
  8516      foo/bar: squash
  8517    status_update_period: 1m0s
  8518    sync_period: 1m0s
  8519  `,
  8520  		},
  8521  		{
  8522  			name: "Additional tide queries get merged in and de-duplicated",
  8523  			prowConfig: `
  8524  tide:
  8525    queries:
  8526    - labels:
  8527      - lgtm
  8528      - approved
  8529      repos:
  8530      - a/repo
  8531  `,
  8532  			supplementalProwConfigs: []string{`
  8533  tide:
  8534    queries:
  8535    - labels:
  8536      - lgtm
  8537      - approved
  8538      repos:
  8539      - another/repo
  8540  `},
  8541  			expectedProwConfig: `branch-protection: {}
  8542  deck:
  8543    spyglass:
  8544      gcs_browser_prefixes:
  8545        '*': ""
  8546      gcs_browser_prefixes_by_bucket:
  8547        '*': ""
  8548      size_limit: 100000000
  8549    tide_update_period: 10s
  8550  default_job_timeout: 24h0m0s
  8551  gangway: {}
  8552  gerrit:
  8553    ratelimit: 5
  8554    tick_interval: 1m0s
  8555  github:
  8556    link_url: https://github.com
  8557  github_reporter:
  8558    job_types_to_report:
  8559    - presubmit
  8560    - postsubmit
  8561  horologium: {}
  8562  in_repo_config:
  8563    allowed_clusters:
  8564      '*':
  8565      - default
  8566  log_level: info
  8567  managed_webhooks:
  8568    auto_accept_invitation: false
  8569    respect_legacy_global_token: false
  8570  moonraker:
  8571    client_timeout: 10m0s
  8572  plank:
  8573    max_goroutines: 20
  8574    pod_pending_timeout: 10m0s
  8575    pod_running_timeout: 48h0m0s
  8576    pod_unscheduled_timeout: 5m0s
  8577  pod_namespace: default
  8578  prowjob_namespace: default
  8579  push_gateway:
  8580    interval: 1m0s
  8581    serve_metrics: false
  8582  scheduler: {}
  8583  sinker:
  8584    max_pod_age: 24h0m0s
  8585    max_prowjob_age: 168h0m0s
  8586    resync_period: 1h0m0s
  8587    terminated_pod_ttl: 24h0m0s
  8588  status_error_link: https://github.com/kubernetes/test-infra/issues
  8589  tide:
  8590    context_options: {}
  8591    max_goroutines: 20
  8592    queries:
  8593    - labels:
  8594      - approved
  8595      - lgtm
  8596      repos:
  8597      - a/repo
  8598      - another/repo
  8599    status_update_period: 1m0s
  8600    sync_period: 1m0s
  8601  `,
  8602  		},
  8603  		{
  8604  			name:       "Additional slack reporter config gets merged in",
  8605  			prowConfig: "config_version_sha: abc",
  8606  			supplementalProwConfigs: []string{
  8607  				`
  8608  slack_reporter_configs:
  8609    my-org:
  8610      channel: '#channel'
  8611      job_states_to_report:
  8612      - failure
  8613      - error
  8614      job_types_to_report:
  8615      - periodic
  8616      report_template: Job {{.Spec.Job}} ended with state {{.Status.State}}.
  8617    my-org/my-repo:
  8618      channel: '#other-channel'
  8619      report_template: Job {{.Spec.Job}} ended with state {{.Status.State}}.
  8620  `,
  8621  			},
  8622  			expectedProwConfig: `branch-protection: {}
  8623  config_version_sha: abc
  8624  deck:
  8625    spyglass:
  8626      gcs_browser_prefixes:
  8627        '*': ""
  8628      gcs_browser_prefixes_by_bucket:
  8629        '*': ""
  8630      size_limit: 100000000
  8631    tide_update_period: 10s
  8632  default_job_timeout: 24h0m0s
  8633  gangway: {}
  8634  gerrit:
  8635    ratelimit: 5
  8636    tick_interval: 1m0s
  8637  github:
  8638    link_url: https://github.com
  8639  github_reporter:
  8640    job_types_to_report:
  8641    - presubmit
  8642    - postsubmit
  8643  horologium: {}
  8644  in_repo_config:
  8645    allowed_clusters:
  8646      '*':
  8647      - default
  8648  log_level: info
  8649  managed_webhooks:
  8650    auto_accept_invitation: false
  8651    respect_legacy_global_token: false
  8652  moonraker:
  8653    client_timeout: 10m0s
  8654  plank:
  8655    max_goroutines: 20
  8656    pod_pending_timeout: 10m0s
  8657    pod_running_timeout: 48h0m0s
  8658    pod_unscheduled_timeout: 5m0s
  8659  pod_namespace: default
  8660  prowjob_namespace: default
  8661  push_gateway:
  8662    interval: 1m0s
  8663    serve_metrics: false
  8664  scheduler: {}
  8665  sinker:
  8666    max_pod_age: 24h0m0s
  8667    max_prowjob_age: 168h0m0s
  8668    resync_period: 1h0m0s
  8669    terminated_pod_ttl: 24h0m0s
  8670  slack_reporter_configs:
  8671    my-org:
  8672      channel: '#channel'
  8673      job_states_to_report:
  8674      - failure
  8675      - error
  8676      job_types_to_report:
  8677      - periodic
  8678      report_template: Job {{.Spec.Job}} ended with state {{.Status.State}}.
  8679    my-org/my-repo:
  8680      channel: '#other-channel'
  8681      report_template: Job {{.Spec.Job}} ended with state {{.Status.State}}.
  8682  status_error_link: https://github.com/kubernetes/test-infra/issues
  8683  tide:
  8684    context_options: {}
  8685    max_goroutines: 20
  8686    status_update_period: 1m0s
  8687    sync_period: 1m0s
  8688  `,
  8689  		},
  8690  		{
  8691  			name:       "Additional slack reporter config with duplication errors",
  8692  			prowConfig: "config_version_sha: abc",
  8693  			supplementalProwConfigs: []string{
  8694  				`
  8695  slack_reporter_configs:
  8696    my-org:
  8697      channel: '#channel'`,
  8698  				`
  8699  slack_reporter_configs:
  8700    my-org:
  8701      channel: '#other-channel'`,
  8702  			},
  8703  			expectedErrorSubstr: "config for org or repo my-org passed more than once",
  8704  		},
  8705  	}
  8706  
  8707  	for _, tc := range testCases {
  8708  		t.Run(tc.name, func(t *testing.T) {
  8709  			config, err := loadConfigYaml(tc.prowConfig, t, tc.supplementalProwConfigs...)
  8710  			if !strings.Contains(fmt.Sprintf("%v", err), tc.expectedErrorSubstr) {
  8711  				t.Fatalf("expected error %v to contain string %s", err, tc.expectedErrorSubstr)
  8712  			} else if err != nil && tc.expectedErrorSubstr == "" {
  8713  				t.Fatalf("config loading errored: %v", err)
  8714  			}
  8715  			if config == nil && tc.expectedProwConfig == "" {
  8716  				return
  8717  			}
  8718  
  8719  			serialized, err := yaml.Marshal(config)
  8720  			if err != nil {
  8721  				t.Fatalf("failed to serialize prow config: %v", err)
  8722  			}
  8723  			if diff := cmp.Diff(tc.expectedProwConfig, string(serialized)); diff != "" {
  8724  				t.Errorf("expected prow config differs from actual: %s", diff)
  8725  			}
  8726  		})
  8727  	}
  8728  }
  8729  
  8730  func TestContextDescriptionWithBaseShaRoundTripping(t *testing.T) {
  8731  	t.Parallel()
  8732  	testCases := []struct {
  8733  		name        string
  8734  		shaIn       string
  8735  		expectedSha string
  8736  	}{
  8737  		{
  8738  			name:        "Valid SHA is returned",
  8739  			shaIn:       "8d287a3aeae90fd0aef4a70009c715712ff302cd",
  8740  			expectedSha: "8d287a3aeae90fd0aef4a70009c715712ff302cd",
  8741  		},
  8742  		{
  8743  			name:        "Invalid sha is not returned",
  8744  			shaIn:       "abc",
  8745  			expectedSha: "",
  8746  		},
  8747  	}
  8748  
  8749  	for _, tc := range testCases {
  8750  		t.Run(tc.name, func(t *testing.T) {
  8751  			for i := 0; i < 100; i++ {
  8752  				var humanReadable string
  8753  				fuzz.New().Fuzz(&humanReadable)
  8754  				contextDescription := ContextDescriptionWithBaseSha(humanReadable, tc.shaIn)
  8755  				if l := len(contextDescription); l > contextDescriptionMaxLen {
  8756  					t.Errorf("Context description %q generated from humanReadable %q and baseSHa %q is longer than %d (%d)", contextDescription, humanReadable, tc.shaIn, contextDescriptionMaxLen, l)
  8757  				}
  8758  
  8759  				if expected, actual := tc.expectedSha, BaseSHAFromContextDescription(contextDescription); expected != actual {
  8760  					t.Errorf("expected to get sha %q back, got %q", expected, actual)
  8761  				}
  8762  			}
  8763  		})
  8764  	}
  8765  }
  8766  
  8767  func shout(i int) string {
  8768  	if i == 0 {
  8769  		return "start"
  8770  	}
  8771  	return fmt.Sprintf("%s part%d", shout(i-1), i)
  8772  }
  8773  
  8774  func TestTruncate(t *testing.T) {
  8775  	if el := len(elide) * 2; contextDescriptionMaxLen < el {
  8776  		t.Fatalf("maxLen must be at least %d (twice %s), got %d", el, elide, contextDescriptionMaxLen)
  8777  	}
  8778  	if s := shout(contextDescriptionMaxLen); len(s) <= contextDescriptionMaxLen {
  8779  		t.Fatalf("%s should be at least %d, got %d", s, contextDescriptionMaxLen, len(s))
  8780  	}
  8781  	big := shout(contextDescriptionMaxLen)
  8782  	outLen := contextDescriptionMaxLen
  8783  	if (contextDescriptionMaxLen-len(elide))%2 == 1 {
  8784  		outLen--
  8785  	}
  8786  	cases := []struct {
  8787  		name   string
  8788  		in     string
  8789  		out    string
  8790  		outLen int
  8791  		front  string
  8792  		back   string
  8793  		middle string
  8794  	}{
  8795  		{
  8796  			name: "do not change short strings",
  8797  			in:   "foo",
  8798  			out:  "foo",
  8799  		},
  8800  		{
  8801  			name: "do not change at boundary",
  8802  			in:   big[:contextDescriptionMaxLen],
  8803  			out:  big[:contextDescriptionMaxLen],
  8804  		},
  8805  		{
  8806  			name: "do not change boundary-1",
  8807  			in:   big[:contextDescriptionMaxLen-1],
  8808  			out:  big[:contextDescriptionMaxLen-1],
  8809  		},
  8810  		{
  8811  			name:   "truncated messages have the right length",
  8812  			in:     big,
  8813  			outLen: outLen,
  8814  		},
  8815  		{
  8816  			name:  "truncated message include beginning",
  8817  			in:    big,
  8818  			front: big[:contextDescriptionMaxLen/4], // include a lot of the start
  8819  		},
  8820  		{
  8821  			name: "truncated messages include ending",
  8822  			in:   big,
  8823  			back: big[len(big)-contextDescriptionMaxLen/4:],
  8824  		},
  8825  		{
  8826  			name:   "truncated messages include a ...",
  8827  			in:     big,
  8828  			middle: elide,
  8829  		},
  8830  	}
  8831  
  8832  	for _, tc := range cases {
  8833  		t.Run(tc.name, func(t *testing.T) {
  8834  			out := truncate(tc.in, contextDescriptionMaxLen)
  8835  			exact := true
  8836  			if tc.front != "" {
  8837  				exact = false
  8838  				if !strings.HasPrefix(out, tc.front) {
  8839  					t.Errorf("%s does not start with %s", out, tc.front)
  8840  				}
  8841  			}
  8842  			if tc.middle != "" {
  8843  				exact = false
  8844  				if !strings.Contains(out, tc.middle) {
  8845  					t.Errorf("%s does not contain %s", out, tc.middle)
  8846  				}
  8847  			}
  8848  			if tc.back != "" {
  8849  				exact = false
  8850  				if !strings.HasSuffix(out, tc.back) {
  8851  					t.Errorf("%s does not end with %s", out, tc.back)
  8852  				}
  8853  			}
  8854  			if tc.outLen > 0 {
  8855  				exact = false
  8856  				if len(out) != tc.outLen {
  8857  					t.Errorf("%s len %d != expected %d", out, len(out), tc.outLen)
  8858  				}
  8859  			}
  8860  			if exact && out != tc.out {
  8861  				t.Errorf("%s != expected %s", out, tc.out)
  8862  			}
  8863  		})
  8864  	}
  8865  }
  8866  
  8867  func TestHasConfigFor(t *testing.T) {
  8868  	t.Parallel()
  8869  	testCases := []struct {
  8870  		name            string
  8871  		resultGenerator func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string])
  8872  	}{
  8873  		{
  8874  			name: "Any non-empty config with empty branchprotection and Tide properties is considered global",
  8875  			resultGenerator: func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string]) {
  8876  				fuzzedConfig.BranchProtection = BranchProtection{}
  8877  				fuzzedConfig.SlackReporterConfigs = SlackReporterConfigs{}
  8878  				fuzzedConfig.Tide.MergeType = nil
  8879  				fuzzedConfig.Tide.Queries = nil
  8880  				return fuzzedConfig, true, nil, nil
  8881  			},
  8882  		},
  8883  		{
  8884  			name: "Any config that is empty except for branchprotection.orgs with empty repo is considered to be for those orgs",
  8885  			resultGenerator: func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string]) {
  8886  				expectOrgs = sets.Set[string]{}
  8887  				result := &ProwConfig{BranchProtection: BranchProtection{Orgs: map[string]Org{}}}
  8888  				for org, orgVal := range fuzzedConfig.BranchProtection.Orgs {
  8889  					orgVal.Repos = nil
  8890  					expectOrgs.Insert(org)
  8891  					result.BranchProtection.Orgs[org] = orgVal
  8892  				}
  8893  				return result, false, expectOrgs, nil
  8894  			},
  8895  		},
  8896  		{
  8897  			name: "Any config that is empty except for repos in branchprotection config is considered to be for those repos",
  8898  			resultGenerator: func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string]) {
  8899  				expectRepos = sets.Set[string]{}
  8900  				result := &ProwConfig{BranchProtection: BranchProtection{Orgs: map[string]Org{}}}
  8901  				for org, orgVal := range fuzzedConfig.BranchProtection.Orgs {
  8902  					result.BranchProtection.Orgs[org] = Org{Repos: map[string]Repo{}}
  8903  					for repo, repoVal := range orgVal.Repos {
  8904  						expectRepos.Insert(org + "/" + repo)
  8905  						result.BranchProtection.Orgs[org].Repos[repo] = repoVal
  8906  					}
  8907  				}
  8908  				return result, false, nil, expectRepos
  8909  			},
  8910  		},
  8911  		{
  8912  			name: "Any config that is empty except for tide.merge_method is considered to be for those orgs or repos",
  8913  			resultGenerator: func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string]) {
  8914  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  8915  				result := &ProwConfig{Tide: Tide{TideGitHubConfig: TideGitHubConfig{MergeType: fuzzedConfig.Tide.MergeType}}}
  8916  				for orgOrRepo := range result.Tide.MergeType {
  8917  					if strings.Contains(orgOrRepo, "/") {
  8918  						expectRepos.Insert(orgOrRepo)
  8919  					} else {
  8920  						expectOrgs.Insert(orgOrRepo)
  8921  					}
  8922  				}
  8923  
  8924  				return result, false, expectOrgs, expectRepos
  8925  			},
  8926  		},
  8927  		{
  8928  			name: "Any config that is empty except for tide.queries is considered to be for those orgs or repos",
  8929  			resultGenerator: func(fuzzedConfig *ProwConfig) (toCheck *ProwConfig, exceptGlobal bool, expectOrgs, expectRepos sets.Set[string]) {
  8930  				expectOrgs, expectRepos = sets.Set[string]{}, sets.Set[string]{}
  8931  				result := &ProwConfig{Tide: Tide{TideGitHubConfig: TideGitHubConfig{Queries: fuzzedConfig.Tide.Queries}}}
  8932  				for _, query := range result.Tide.Queries {
  8933  					expectOrgs.Insert(query.Orgs...)
  8934  					expectRepos.Insert(query.Repos...)
  8935  				}
  8936  
  8937  				return result, false, expectOrgs, expectRepos
  8938  			},
  8939  		},
  8940  	}
  8941  
  8942  	seed := time.Now().UnixNano()
  8943  	// Print the seed so failures can easily be reproduced
  8944  	t.Logf("Seed: %d", seed)
  8945  	fuzzer := fuzz.NewWithSeed(seed).
  8946  		// The fuzzer doesn't know what to put into interface fields, so we have to custom handle them.
  8947  		Funcs(
  8948  			// This is not an interface, but it contains an interface type. Handling the interface type
  8949  			// itself makes the bazel-built tests panic with a nullpointer deref but works fine with
  8950  			// go test.
  8951  			func(t *template.Template, _ fuzz.Continue) {
  8952  				*t = *template.New("whatever")
  8953  			},
  8954  			func(*labels.Selector, fuzz.Continue) {},
  8955  		)
  8956  
  8957  	for _, tc := range testCases {
  8958  		t.Run(tc.name, func(t *testing.T) {
  8959  			for i := 0; i < 100; i++ {
  8960  				fuzzedConfig := &ProwConfig{}
  8961  				fuzzedConfig.SlackReporterConfigs = SlackReporterConfigs{}
  8962  				fuzzer.Fuzz(fuzzedConfig)
  8963  
  8964  				fuzzedAndManipulatedConfig, expectIsGlobal, expectOrgs, expectRepos := tc.resultGenerator(fuzzedConfig)
  8965  				actualIsGlobal, actualOrgs, actualRepos := fuzzedAndManipulatedConfig.HasConfigFor()
  8966  
  8967  				if expectIsGlobal != actualIsGlobal {
  8968  					t.Errorf("exepcted isGlobal: %t, got: %t", expectIsGlobal, actualIsGlobal)
  8969  				}
  8970  
  8971  				if diff := cmp.Diff(expectOrgs, actualOrgs); diff != "" {
  8972  					t.Errorf("expected orgs differ from actual: %s", diff)
  8973  				}
  8974  
  8975  				if diff := cmp.Diff(expectRepos, actualRepos); diff != "" {
  8976  					t.Errorf("expected repos differ from actual: %s", diff)
  8977  				}
  8978  			}
  8979  
  8980  		})
  8981  	}
  8982  }
  8983  
  8984  func TestCalculateStorageBuckets(t *testing.T) {
  8985  	t.Parallel()
  8986  	testCases := []struct {
  8987  		name     string
  8988  		in       *Config
  8989  		expected sets.Set[string]
  8990  	}{
  8991  		{
  8992  			name: "S3 provider prefix gets removed from Plank config",
  8993  			in: &Config{ProwConfig: ProwConfig{Plank: Plank{DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{{Config: &prowapi.DecorationConfig{
  8994  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "s3://prow-logs"},
  8995  			}}}}}},
  8996  			expected: sets.New[string]("prow-logs"),
  8997  		},
  8998  		{
  8999  			name: "GS provider prefix gets removed from Plank config",
  9000  			in: &Config{ProwConfig: ProwConfig{Plank: Plank{DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{{Config: &prowapi.DecorationConfig{
  9001  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "gs://prow-logs"},
  9002  			}}}}}},
  9003  			expected: sets.New[string]("prow-logs"),
  9004  		},
  9005  		{
  9006  			name: "No provider prefix, nothing to do",
  9007  			in: &Config{ProwConfig: ProwConfig{Plank: Plank{DefaultDecorationConfigs: []*DefaultDecorationConfigEntry{{Config: &prowapi.DecorationConfig{
  9008  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "kubernetes-jenkins"},
  9009  			}}}}}},
  9010  			expected: sets.New[string]("kubernetes-jenkins"),
  9011  		},
  9012  		{
  9013  			name: "S3 provider prefix gets removed from periodic config",
  9014  			in: &Config{JobConfig: JobConfig{Periodics: []Periodic{{JobBase: JobBase{UtilityConfig: UtilityConfig{DecorationConfig: &prowapi.DecorationConfig{
  9015  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "s3://prow-logs"},
  9016  			}}}}}}},
  9017  			expected: sets.New[string]("prow-logs"),
  9018  		},
  9019  		{
  9020  			name: "S3 provider prefix gets removed from presubmit config",
  9021  			in: &Config{JobConfig: JobConfig{PresubmitsStatic: map[string][]Presubmit{"": {{JobBase: JobBase{UtilityConfig: UtilityConfig{DecorationConfig: &prowapi.DecorationConfig{
  9022  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "s3://prow-logs"},
  9023  			}}}}}}}},
  9024  			expected: sets.New[string]("prow-logs"),
  9025  		},
  9026  		{
  9027  			name: "S3 provider prefix gets removed from postsubmit config",
  9028  			in: &Config{JobConfig: JobConfig{PostsubmitsStatic: map[string][]Postsubmit{"": {{JobBase: JobBase{UtilityConfig: UtilityConfig{DecorationConfig: &prowapi.DecorationConfig{
  9029  				GCSConfiguration: &prowapi.GCSConfiguration{Bucket: "s3://prow-logs"},
  9030  			}}}}}}}},
  9031  			expected: sets.New[string]("prow-logs"),
  9032  		},
  9033  	}
  9034  
  9035  	for _, tc := range testCases {
  9036  		t.Run(tc.name, func(t *testing.T) {
  9037  			actual := calculateStorageBuckets(tc.in)
  9038  			if diff := cmp.Diff(tc.expected, actual); diff != "" {
  9039  				t.Errorf("actual differs from expected")
  9040  			}
  9041  		})
  9042  	}
  9043  }
  9044  
  9045  func TestProwConfigMergingProperties(t *testing.T) {
  9046  	t.Parallel()
  9047  	testCases := []struct {
  9048  		name          string
  9049  		makeMergeable func(*ProwConfig)
  9050  	}{
  9051  		{
  9052  			name: "Branchprotection config",
  9053  			makeMergeable: func(pc *ProwConfig) {
  9054  				*pc = ProwConfig{BranchProtection: pc.BranchProtection}
  9055  			},
  9056  		},
  9057  		{
  9058  			name: "Tide merge method",
  9059  			makeMergeable: func(pc *ProwConfig) {
  9060  				*pc = ProwConfig{Tide: Tide{TideGitHubConfig: TideGitHubConfig{MergeType: pc.Tide.MergeType}}}
  9061  			},
  9062  		},
  9063  		{
  9064  			name: "Tide queries",
  9065  			makeMergeable: func(pc *ProwConfig) {
  9066  				*pc = ProwConfig{Tide: Tide{TideGitHubConfig: TideGitHubConfig{Queries: pc.Tide.Queries}}}
  9067  			},
  9068  		},
  9069  		{
  9070  			name: "SlackReporter configurations",
  9071  			makeMergeable: func(pc *ProwConfig) {
  9072  				*pc = ProwConfig{SlackReporterConfigs: pc.SlackReporterConfigs}
  9073  			},
  9074  		},
  9075  	}
  9076  
  9077  	expectedProperties := []struct {
  9078  		name         string
  9079  		verification func(t *testing.T, fuzzedConfig *ProwConfig)
  9080  	}{
  9081  		{
  9082  			name: "Merging into empty config always succeeds and makes the empty config equal to the one that was merged in",
  9083  			verification: func(t *testing.T, fuzzedMergeableConfig *ProwConfig) {
  9084  				newConfig := &ProwConfig{}
  9085  				if err := newConfig.mergeFrom(fuzzedMergeableConfig); err != nil {
  9086  					t.Fatalf("merging fuzzed mergeable config into empty config failed: %v", err)
  9087  				}
  9088  				if diff := cmp.Diff(newConfig, fuzzedMergeableConfig, DefaultDiffOpts...); diff != "" {
  9089  					t.Errorf("after merging config into an empty config, the config that was merged into differs from the one we merged from:\n%s\n", diff)
  9090  				}
  9091  			},
  9092  		},
  9093  		{
  9094  			name: "Merging empty config in always succeeds",
  9095  			verification: func(t *testing.T, fuzzedMergeableConfig *ProwConfig) {
  9096  				if err := fuzzedMergeableConfig.mergeFrom(&ProwConfig{}); err != nil {
  9097  					t.Errorf("merging empty config in failed: %v", err)
  9098  				}
  9099  			},
  9100  		},
  9101  		{
  9102  			name: "Merging a config into itself always fails",
  9103  			verification: func(t *testing.T, fuzzedMergeableConfig *ProwConfig) {
  9104  				if apiequality.Semantic.DeepEqual(fuzzedMergeableConfig, &ProwConfig{}) {
  9105  					return
  9106  				}
  9107  
  9108  				// One exception: Tide queries can be merged into themselves, as we just de-duplicate them
  9109  				// later on.
  9110  				if len(fuzzedMergeableConfig.Tide.Queries) > 0 {
  9111  					return
  9112  				}
  9113  
  9114  				// Another exception: A non-nil branchprotection config with only empty policies
  9115  				// can be merged into itself so make sure this can't happen.
  9116  				if !apiequality.Semantic.DeepEqual(fuzzedMergeableConfig.BranchProtection, BranchProtection{}) {
  9117  					fuzzedMergeableConfig.BranchProtection.Exclude = []string{"foo"}
  9118  				}
  9119  
  9120  				if err := fuzzedMergeableConfig.mergeFrom(fuzzedMergeableConfig); err == nil {
  9121  					serialized, serializeErr := yaml.Marshal(fuzzedMergeableConfig)
  9122  					if serializeErr != nil {
  9123  						t.Fatalf("merging non-empty config into itself did not yield an error and serializing it afterwards failed: %v. Raw object: %+v", serializeErr, fuzzedMergeableConfig)
  9124  					}
  9125  					t.Errorf("merging a config into itself did not produce an error. Serialized config:\n%s", string(serialized))
  9126  				}
  9127  			},
  9128  		},
  9129  	}
  9130  
  9131  	seed := time.Now().UnixNano()
  9132  	// Print the seed so failures can easily be reproduced
  9133  	t.Logf("Seed: %d", seed)
  9134  	var i int
  9135  	fuzzer := fuzz.NewWithSeed(seed).
  9136  		// The fuzzer doesn't know what to put into interface fields, so we have to custom handle them.
  9137  		Funcs(
  9138  			// This is not an interface, but it contains an interface type. Handling the interface type
  9139  			// itself makes the bazel-built tests panic with a nullpointer deref but works fine with
  9140  			// go test.
  9141  			func(t *template.Template, _ fuzz.Continue) {
  9142  				*t = *template.New("whatever")
  9143  			},
  9144  			func(*labels.Selector, fuzz.Continue) {},
  9145  			func(p *Policy, c fuzz.Continue) {
  9146  				// Make sure we always have a good sample of non-nil but empty Policies so
  9147  				// we check that they get copied over. Today, the meaning of an empty and
  9148  				// an unset Policy is identical because all the fields are pointers that
  9149  				// will get ignored if unset. However, this might change in the future and
  9150  				// caused flakes when we didn't copy over map entries with an empty Policy,
  9151  				// as an entry with no value and no entry are different things for cmp.Diff.
  9152  				if i%2 == 0 {
  9153  					c.Fuzz(p)
  9154  				}
  9155  				i++
  9156  			},
  9157  			func(config *SlackReporterConfigs, c fuzz.Continue) {
  9158  				for _, reporter := range *config {
  9159  					c.Fuzz(reporter)
  9160  				}
  9161  			},
  9162  		)
  9163  
  9164  	// Do not parallelize, the PRNG used by the fuzzer is not threadsafe
  9165  	for _, tc := range testCases {
  9166  		t.Run(tc.name, func(t *testing.T) {
  9167  
  9168  			for _, propertyTest := range expectedProperties {
  9169  				t.Run(propertyTest.name, func(t *testing.T) {
  9170  
  9171  					for i := 0; i < 100; i++ {
  9172  						fuzzedConfig := &ProwConfig{}
  9173  						fuzzedConfig.SlackReporterConfigs = map[string]SlackReporter{}
  9174  						fuzzer.Fuzz(fuzzedConfig)
  9175  
  9176  						tc.makeMergeable(fuzzedConfig)
  9177  
  9178  						propertyTest.verification(t, fuzzedConfig)
  9179  					}
  9180  				})
  9181  			}
  9182  		})
  9183  	}
  9184  }
  9185  
  9186  func TestEnsureConfigIsDiffable(t *testing.T) {
  9187  	// This will panic in case it is not able to diff 'Config'.
  9188  	_ = cmp.Diff(Config{}, Config{}, DefaultDiffOpts...)
  9189  }
  9190  
  9191  // TestDeduplicateTideQueriesDoesntLoseData simply uses deduplicateTideQueries
  9192  // on a single fuzzed tidequery, which should never result in any change as
  9193  // there is nothing that could be deduplicated. This is mostly to ensure we
  9194  // don't forget to change our code when new fields get added to the type.
  9195  func TestDeduplicateTideQueriesDoesntLoseData(t *testing.T) {
  9196  	config := &Config{}
  9197  	for i := 0; i < 100; i++ {
  9198  		t.Run(strconv.Itoa(i), func(t *testing.T) {
  9199  			query := TideQuery{}
  9200  			fuzz.New().Fuzz(&query)
  9201  			result, err := config.deduplicateTideQueries(TideQueries{query})
  9202  			if err != nil {
  9203  				t.Fatalf("error: %v", err)
  9204  			}
  9205  
  9206  			if diff := cmp.Diff(result[0], query); diff != "" {
  9207  				t.Errorf("result differs from initial query: %s", diff)
  9208  			}
  9209  		})
  9210  	}
  9211  }
  9212  
  9213  func TestDeduplicateTideQueries(t *testing.T) {
  9214  	testCases := []struct {
  9215  		name                  string
  9216  		in                    TideQueries
  9217  		prowJobDefaultEntries []*ProwJobDefaultEntry
  9218  		expected              TideQueries
  9219  	}{
  9220  		{
  9221  			name: "No overlap",
  9222  			in: TideQueries{
  9223  				{Orgs: []string{"kubernetes"}, Labels: []string{"merge-me"}},
  9224  				{Orgs: []string{"kubernetes-priv"}, Labels: []string{"merge-me-differently"}},
  9225  			},
  9226  			expected: TideQueries{
  9227  				{Orgs: []string{"kubernetes"}, Labels: []string{"merge-me"}},
  9228  				{Orgs: []string{"kubernetes-priv"}, Labels: []string{"merge-me-differently"}},
  9229  			},
  9230  		},
  9231  		{
  9232  			name: "Queries get deduplicated",
  9233  			in: TideQueries{
  9234  				{Orgs: []string{"kubernetes"}, Labels: []string{"merge-me"}},
  9235  				{Orgs: []string{"kubernetes-priv"}, Labels: []string{"merge-me"}},
  9236  			},
  9237  			expected: TideQueries{{Orgs: []string{"kubernetes", "kubernetes-priv"}, Labels: []string{"merge-me"}}},
  9238  		},
  9239  		{
  9240  			name: "Queries get deduplicated regardless of element order",
  9241  			in: TideQueries{
  9242  				{Orgs: []string{"kubernetes"}, Labels: []string{"lgtm", "merge-me"}},
  9243  				{Orgs: []string{"kubernetes-priv"}, Labels: []string{"merge-me", "lgtm"}},
  9244  			},
  9245  			expected: TideQueries{{Orgs: []string{"kubernetes", "kubernetes-priv"}, Labels: []string{"lgtm", "merge-me"}}},
  9246  		},
  9247  		{
  9248  			name: "Queries with different tenantIds don't get deduplicated",
  9249  			in: TideQueries{
  9250  				{Repos: []string{"kubernetes/test-infra"}, Labels: []string{"merge-me"}},
  9251  				{Repos: []string{"kubernetes/tenanted"}, Labels: []string{"merge-me"}},
  9252  				{Repos: []string{"kubernetes/other"}, Labels: []string{"merge-me"}},
  9253  			},
  9254  			prowJobDefaultEntries: []*ProwJobDefaultEntry{
  9255  				{OrgRepo: "kubernetes/tenanted", Config: &prowapi.ProwJobDefault{TenantID: "tenanted"}},
  9256  			},
  9257  			expected: TideQueries{
  9258  				{Repos: []string{"kubernetes/tenanted"}, Labels: []string{"merge-me"}},
  9259  				{Repos: []string{"kubernetes/test-infra", "kubernetes/other"}, Labels: []string{"merge-me"}},
  9260  			},
  9261  		},
  9262  	}
  9263  
  9264  	for _, tc := range testCases {
  9265  		t.Run(tc.name, func(t *testing.T) {
  9266  			config := &Config{}
  9267  			config.ProwConfig.ProwJobDefaultEntries = append(config.ProwConfig.ProwJobDefaultEntries, tc.prowJobDefaultEntries...)
  9268  			result, err := config.deduplicateTideQueries(tc.in)
  9269  			if err != nil {
  9270  				t.Fatalf("failed: %v", err)
  9271  			}
  9272  
  9273  			if diff := cmp.Diff(result, tc.expected); diff != "" {
  9274  				t.Errorf("Result differs from expected: %v", diff)
  9275  			}
  9276  		})
  9277  	}
  9278  }
  9279  
  9280  func TestParseTideMergeType(t *testing.T) {
  9281  	exportRegexp := cmp.AllowUnexported(TideBranchMergeType{})
  9282  	regexpComparer := cmp.Comparer(func(a, b *regexp.Regexp) bool {
  9283  		return (a == nil && b == nil) || (a != nil && b != nil) && (a.String() == b.String())
  9284  	})
  9285  	errComparer := cmp.Comparer(func(a, b error) bool {
  9286  		return (a == nil && b == nil) || (a != nil && b != nil) && (a.Error() == b.Error())
  9287  	})
  9288  	sortSlices := cmpopts.SortSlices(func(a, b error) bool {
  9289  		return a.Error() < b.Error()
  9290  	})
  9291  	for _, tc := range []struct {
  9292  		name       string
  9293  		mergeTypes map[string]TideOrgMergeType
  9294  		wantTypes  map[string]TideOrgMergeType
  9295  		wantErrs   []error
  9296  	}{
  9297  		{
  9298  			name: "No errors",
  9299  			mergeTypes: map[string]TideOrgMergeType{
  9300  				"k8s": {
  9301  					MergeType: types.MergeRebase,
  9302  				},
  9303  				"k8s/test": {
  9304  					MergeType: types.MergeSquash,
  9305  				},
  9306  				"k8s/test@test": {
  9307  					MergeType: types.MergeSquash,
  9308  				},
  9309  				"kubernetes": {
  9310  					Repos: map[string]TideRepoMergeType{
  9311  						"test-infra": {
  9312  							MergeType: types.MergeSquash,
  9313  						},
  9314  					},
  9315  				},
  9316  				"golang": {
  9317  					Repos: map[string]TideRepoMergeType{
  9318  						"go": {
  9319  							Branches: map[string]TideBranchMergeType{
  9320  								"master": {
  9321  									MergeType: types.MergeMerge,
  9322  								},
  9323  								"main.+": {
  9324  									MergeType: types.MergeRebase,
  9325  								},
  9326  							},
  9327  						},
  9328  					},
  9329  				},
  9330  			},
  9331  			wantTypes: map[string]TideOrgMergeType{
  9332  				"k8s": {
  9333  					MergeType: types.MergeRebase,
  9334  				},
  9335  				"k8s/test": {
  9336  					MergeType: types.MergeSquash,
  9337  				},
  9338  				"k8s/test@test": {
  9339  					MergeType: types.MergeSquash,
  9340  				},
  9341  				"kubernetes": {
  9342  					Repos: map[string]TideRepoMergeType{
  9343  						"test-infra": {
  9344  							MergeType: types.MergeSquash,
  9345  						},
  9346  					},
  9347  				},
  9348  				"golang": {
  9349  					Repos: map[string]TideRepoMergeType{
  9350  						"go": {
  9351  							Branches: map[string]TideBranchMergeType{
  9352  								"master": {
  9353  									MergeType: types.MergeMerge,
  9354  									Regexpr:   regexp.MustCompile("master"),
  9355  								},
  9356  								"main.+": {
  9357  									MergeType: types.MergeRebase,
  9358  									Regexpr:   regexp.MustCompile("main.+"),
  9359  								},
  9360  							},
  9361  						},
  9362  					},
  9363  				},
  9364  			},
  9365  		},
  9366  		{
  9367  			name: "Errors on every config level",
  9368  			mergeTypes: map[string]TideOrgMergeType{
  9369  				"k8s": {
  9370  					MergeType: "fake-org-mm",
  9371  				},
  9372  				"kubernetes": {
  9373  					Repos: map[string]TideRepoMergeType{
  9374  						"test-infra": {
  9375  							MergeType: "fake-repo-mm",
  9376  						},
  9377  						"kubernetes": {
  9378  							Branches: map[string]TideBranchMergeType{
  9379  								"main": {
  9380  									MergeType: "fake-br-mm",
  9381  								},
  9382  								"invalid-regex[": {
  9383  									MergeType: types.MergeMerge,
  9384  								},
  9385  							},
  9386  						},
  9387  					},
  9388  				},
  9389  			},
  9390  			wantErrs: []error{
  9391  				errors.New(`merge type "fake-org-mm" for k8s is not a valid type`),
  9392  				errors.New(`merge type "fake-repo-mm" for kubernetes/test-infra is not a valid type`),
  9393  				errors.New(`merge type "fake-br-mm" for kubernetes/kubernetes@main is not a valid type`),
  9394  				errors.New(`regex "invalid-regex[" is not valid`),
  9395  			},
  9396  		},
  9397  	} {
  9398  		t.Run(tc.name, func(t *testing.T) {
  9399  			err := parseTideMergeType(tc.mergeTypes)
  9400  			if len(tc.wantErrs) > 0 {
  9401  				if err == nil {
  9402  					t.Errorf("expected err '%v', got nil", tc.wantErrs)
  9403  				}
  9404  				// Resulting errors are not guaranteed to be always in the same order, due to how
  9405  				// hashmap is implemented in Go. We need to sort first and then compare.
  9406  				if diff := cmp.Diff(tc.wantErrs, err.Errors(), errComparer, sortSlices); diff != "" {
  9407  					t.Errorf("errors don't match: %v", diff)
  9408  				}
  9409  			} else {
  9410  				if err != nil {
  9411  					t.Errorf("expected err nil, got '%v'", err)
  9412  				}
  9413  
  9414  				if diff := cmp.Diff(tc.wantTypes, tc.mergeTypes, regexpComparer, exportRegexp); diff != "" {
  9415  					t.Errorf("tide configs doesn't match: %v", diff)
  9416  				}
  9417  			}
  9418  		})
  9419  	}
  9420  }
  9421  
  9422  func TestGetAndCheckRefs(t *testing.T) {
  9423  	type expected struct {
  9424  		baseSHA  string
  9425  		headSHAs []string
  9426  		err      string
  9427  	}
  9428  
  9429  	for _, tc := range []struct {
  9430  		name           string
  9431  		baseSHAGetter  RefGetter
  9432  		headSHAGetters []RefGetter
  9433  		expected       expected
  9434  	}{
  9435  		{
  9436  			name:          "Basic",
  9437  			baseSHAGetter: goodSHAGetter("ba5e"),
  9438  			headSHAGetters: []RefGetter{
  9439  				goodSHAGetter("abcd"),
  9440  				goodSHAGetter("ef01")},
  9441  			expected: expected{
  9442  				baseSHA:  "ba5e",
  9443  				headSHAs: []string{"abcd", "ef01"},
  9444  				err:      "",
  9445  			},
  9446  		},
  9447  		{
  9448  			name:           "NoHeadSHAGetters",
  9449  			baseSHAGetter:  goodSHAGetter("ba5e"),
  9450  			headSHAGetters: []RefGetter{},
  9451  			expected: expected{
  9452  				baseSHA:  "ba5e",
  9453  				headSHAs: nil,
  9454  				err:      "",
  9455  			},
  9456  		},
  9457  		{
  9458  			name:          "BaseSHAGetterFailure",
  9459  			baseSHAGetter: badSHAGetter,
  9460  			headSHAGetters: []RefGetter{
  9461  				goodSHAGetter("abcd"),
  9462  				goodSHAGetter("ef01")},
  9463  			expected: expected{
  9464  				baseSHA:  "",
  9465  				headSHAs: nil,
  9466  				err:      "failed to get baseSHA: badSHAGetter",
  9467  			},
  9468  		},
  9469  		{
  9470  			name:          "HeadSHAGetterFailure",
  9471  			baseSHAGetter: goodSHAGetter("ba5e"),
  9472  			headSHAGetters: []RefGetter{
  9473  				goodSHAGetter("abcd"),
  9474  				badSHAGetter},
  9475  			expected: expected{
  9476  				baseSHA:  "",
  9477  				headSHAs: nil,
  9478  				err:      "failed to get headRef: badSHAGetter",
  9479  			},
  9480  		},
  9481  	} {
  9482  		t.Run(tc.name, func(t1 *testing.T) {
  9483  			baseSHA, headSHAs, err := GetAndCheckRefs(tc.baseSHAGetter, tc.headSHAGetters...)
  9484  
  9485  			if tc.expected.err == "" {
  9486  				if err != nil {
  9487  					t.Errorf("Expected error 'nil' got '%v'", err.Error())
  9488  				}
  9489  				if tc.expected.baseSHA != baseSHA {
  9490  					t.Errorf("Expected baseSHA '%v', got '%v'", tc.expected.baseSHA, baseSHA)
  9491  				}
  9492  				if !reflect.DeepEqual(tc.expected.headSHAs, headSHAs) {
  9493  					t.Errorf("headSHAs do not match:\n%s", diff.ObjectReflectDiff(tc.expected.headSHAs, headSHAs))
  9494  				}
  9495  			} else {
  9496  				if err == nil {
  9497  					t.Fatal("Expected non-nil error, got nil")
  9498  				}
  9499  
  9500  				if tc.expected.err != err.Error() {
  9501  					t.Errorf("Expected error '%v', got '%v'", tc.expected.err, err.Error())
  9502  				}
  9503  			}
  9504  		})
  9505  	}
  9506  }
  9507  
  9508  func TestSplitRepoName(t *testing.T) {
  9509  	tests := []struct {
  9510  		name     string
  9511  		full     string
  9512  		wantOrg  string
  9513  		wantRepo string
  9514  		wantErr  bool
  9515  	}{
  9516  		{
  9517  			name:     "github repo",
  9518  			full:     "orgA/repoB",
  9519  			wantOrg:  "orgA",
  9520  			wantRepo: "repoB",
  9521  			wantErr:  false,
  9522  		},
  9523  		{
  9524  			name:     "ref name with http://",
  9525  			full:     "http://orgA/repoB",
  9526  			wantOrg:  "http://orgA",
  9527  			wantRepo: "repoB",
  9528  			wantErr:  false,
  9529  		},
  9530  		{
  9531  			name:     "ref name with https://",
  9532  			full:     "https://orgA/repoB",
  9533  			wantOrg:  "https://orgA",
  9534  			wantRepo: "repoB",
  9535  			wantErr:  false,
  9536  		},
  9537  		{
  9538  			name:     "repo name contains /",
  9539  			full:     "orgA/repoB/subC",
  9540  			wantOrg:  "orgA",
  9541  			wantRepo: "repoB/subC",
  9542  			wantErr:  false,
  9543  		},
  9544  		{
  9545  			name:     "invalid",
  9546  			full:     "repoB",
  9547  			wantOrg:  "",
  9548  			wantRepo: "",
  9549  			wantErr:  true,
  9550  		},
  9551  	}
  9552  
  9553  	for _, tt := range tests {
  9554  		tt := tt
  9555  		t.Run(tt.name, func(t *testing.T) {
  9556  			gotOrg, gotRepo, err := SplitRepoName(tt.full)
  9557  			if gotOrg != tt.wantOrg {
  9558  				t.Errorf("org mismatch. Want: %v, got: %v", tt.wantOrg, gotOrg)
  9559  			}
  9560  			if gotRepo != tt.wantRepo {
  9561  				t.Errorf("repo mismatch. Want: %v, got: %v", tt.wantRepo, gotRepo)
  9562  			}
  9563  			gotErr := (err != nil)
  9564  			if gotErr != (tt.wantErr && gotErr) {
  9565  				t.Errorf("err mismatch. Want: %v, got: %v", tt.wantErr, gotErr)
  9566  			}
  9567  		})
  9568  	}
  9569  }