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

     1  /*
     2  Copyright 2018 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 clonerefs
    18  
    19  import (
    20  	"crypto/rand"
    21  	"crypto/rsa"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"encoding/pem"
    25  	"fmt"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"os"
    29  	"path"
    30  	"path/filepath"
    31  	"reflect"
    32  	"sync"
    33  	"testing"
    34  	"time"
    35  
    36  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    37  	"sigs.k8s.io/prow/pkg/github"
    38  	"sigs.k8s.io/prow/pkg/pod-utils/clone"
    39  )
    40  
    41  func TestRun(t *testing.T) {
    42  	srcRoot := t.TempDir()
    43  
    44  	oauthTokenDir := t.TempDir()
    45  	oauthTokenFilePath := filepath.Join(oauthTokenDir, "oauth-token")
    46  	oauthTokenValue := []byte("12345678")
    47  	if err := os.WriteFile(oauthTokenFilePath, oauthTokenValue, 0644); err != nil {
    48  		t.Fatalf("Error while create oauth token file: %v", err)
    49  	}
    50  
    51  	githubAppDir := t.TempDir()
    52  	githubAppPrivateKeyFilePath := filepath.Join(githubAppDir, "private-key.pem")
    53  	privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
    54  	if err != nil {
    55  		t.Fatalf("Error while create github app private key file: %v", err)
    56  	}
    57  	githubAppPrivateKeyValue := pem.EncodeToMemory(&pem.Block{
    58  		Type:  "RSA PRIVATE KEY",
    59  		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
    60  	})
    61  	if err := os.WriteFile(githubAppPrivateKeyFilePath, githubAppPrivateKeyValue, 0644); err != nil {
    62  		t.Fatalf("Error while create github app private key file: %v", err)
    63  	}
    64  
    65  	githubAppOrg := "kubernetes"
    66  	githubAppToken := "github-app-token"
    67  	mockGitHubAppServer := httptest.NewServer(mockGitHubAppHandler(githubAppOrg, githubAppToken))
    68  	defer mockGitHubAppServer.Close()
    69  
    70  	type cloneRec struct {
    71  		refs        prowapi.Refs
    72  		root        string
    73  		user, email string
    74  		cookiePath  string
    75  		env         []string
    76  		authUser    string
    77  		authToken   string
    78  		authError   error
    79  	}
    80  
    81  	var recordedClones []cloneRec
    82  	var lock sync.Mutex
    83  	cloneFuncOld := cloneFunc
    84  	cloneFunc = func(refs prowapi.Refs, root, user, email, cookiePath string, env []string, userGenerator github.UserGenerator, tokenGenerator github.TokenGenerator) clone.Record {
    85  		lock.Lock()
    86  		defer lock.Unlock()
    87  		var (
    88  			authUser  string
    89  			authToken string
    90  			authError error
    91  		)
    92  		if userGenerator != nil {
    93  			user, err := userGenerator()
    94  			if err != nil {
    95  				authError = err
    96  			}
    97  			authUser = user
    98  		}
    99  		if tokenGenerator != nil {
   100  			token, err := tokenGenerator(refs.Org)
   101  			if err != nil {
   102  				authError = err
   103  			}
   104  			authToken = token
   105  		}
   106  		recordedClones = append(recordedClones, cloneRec{
   107  			refs:       refs,
   108  			root:       root,
   109  			user:       user,
   110  			email:      email,
   111  			cookiePath: cookiePath,
   112  			env:        env,
   113  			authUser:   authUser,
   114  			authToken:  authToken,
   115  			authError:  authError,
   116  		})
   117  		return clone.Record{}
   118  	}
   119  	defer func() { cloneFunc = cloneFuncOld }()
   120  
   121  	testcases := []struct {
   122  		name           string
   123  		opts           Options
   124  		expectedClones []cloneRec
   125  	}{
   126  		{
   127  			name: "single PR clone",
   128  			opts: Options{
   129  				SrcRoot:      srcRoot,
   130  				Log:          path.Join(srcRoot, "log.txt"),
   131  				GitUserName:  "me",
   132  				GitUserEmail: "me@domain.com",
   133  				CookiePath:   "cookies/path",
   134  				GitRefs: []prowapi.Refs{
   135  					{
   136  						Org:       "kubernetes",
   137  						Repo:      "test-infra",
   138  						BaseRef:   "master",
   139  						PathAlias: "sigs.k8s.io/prow",
   140  						Pulls: []prowapi.Pull{
   141  							{
   142  								Number: 5,
   143  								SHA:    "FEEDDAD",
   144  							},
   145  						},
   146  						SkipSubmodules: true,
   147  					},
   148  				},
   149  			},
   150  			expectedClones: []cloneRec{
   151  				{
   152  					refs: prowapi.Refs{
   153  						Org:       "kubernetes",
   154  						Repo:      "test-infra",
   155  						BaseRef:   "master",
   156  						PathAlias: "sigs.k8s.io/prow",
   157  						Pulls: []prowapi.Pull{
   158  							{
   159  								Number: 5,
   160  								SHA:    "FEEDDAD",
   161  							},
   162  						},
   163  						SkipSubmodules: true,
   164  					},
   165  					root:       srcRoot,
   166  					user:       "me",
   167  					email:      "me@domain.com",
   168  					cookiePath: "cookies/path",
   169  				},
   170  			},
   171  		},
   172  		{
   173  			name: "multi repo clone",
   174  			opts: Options{
   175  				Log: path.Join(srcRoot, "log.txt"),
   176  				GitRefs: []prowapi.Refs{
   177  					{
   178  						Org:       "kubernetes",
   179  						Repo:      "test-infra",
   180  						BaseRef:   "master",
   181  						PathAlias: "sigs.k8s.io/prow",
   182  						Pulls: []prowapi.Pull{
   183  							{
   184  								Number: 5,
   185  								SHA:    "FEEDDAD",
   186  							},
   187  						},
   188  					},
   189  					{
   190  						Org:       "kubernetes",
   191  						Repo:      "release",
   192  						BaseRef:   "master",
   193  						PathAlias: "k8s.io/release",
   194  					},
   195  				},
   196  			},
   197  			expectedClones: []cloneRec{
   198  				{
   199  					refs: prowapi.Refs{
   200  						Org:       "kubernetes",
   201  						Repo:      "test-infra",
   202  						BaseRef:   "master",
   203  						PathAlias: "sigs.k8s.io/prow",
   204  						Pulls: []prowapi.Pull{
   205  							{
   206  								Number: 5,
   207  								SHA:    "FEEDDAD",
   208  							},
   209  						},
   210  					},
   211  				},
   212  				{
   213  					refs: prowapi.Refs{
   214  						Org:       "kubernetes",
   215  						Repo:      "release",
   216  						BaseRef:   "master",
   217  						PathAlias: "k8s.io/release",
   218  					},
   219  				},
   220  			},
   221  		},
   222  		{
   223  			name: "single PR clone with oauth token",
   224  			opts: Options{
   225  				OauthTokenFile: oauthTokenFilePath,
   226  				SrcRoot:        srcRoot,
   227  				Log:            path.Join(srcRoot, "log.txt"),
   228  				GitUserName:    "me",
   229  				GitUserEmail:   "me@domain.com",
   230  				CookiePath:     "cookies/path",
   231  				GitRefs: []prowapi.Refs{
   232  					{
   233  						Org:       "kubernetes",
   234  						Repo:      "test-infra",
   235  						BaseRef:   "master",
   236  						PathAlias: "sigs.k8s.io/prow",
   237  						Pulls: []prowapi.Pull{
   238  							{
   239  								Number: 5,
   240  								SHA:    "FEEDDAD",
   241  							},
   242  						},
   243  						SkipSubmodules: true,
   244  					},
   245  				},
   246  			},
   247  			expectedClones: []cloneRec{
   248  				{
   249  					refs: prowapi.Refs{
   250  						Org:       "kubernetes",
   251  						Repo:      "test-infra",
   252  						BaseRef:   "master",
   253  						PathAlias: "sigs.k8s.io/prow",
   254  						Pulls: []prowapi.Pull{
   255  							{
   256  								Number: 5,
   257  								SHA:    "FEEDDAD",
   258  							},
   259  						},
   260  						SkipSubmodules: true,
   261  					},
   262  					root:       srcRoot,
   263  					user:       "me",
   264  					email:      "me@domain.com",
   265  					cookiePath: "cookies/path",
   266  					authToken:  "12345678",
   267  				},
   268  			},
   269  		},
   270  		{
   271  			name: "single PR clone with GitHub App",
   272  			opts: Options{
   273  				GitHubAPIEndpoints: []string{
   274  					mockGitHubAppServer.URL,
   275  				},
   276  				GitHubAppID:             "123456",
   277  				GitHubAppPrivateKeyFile: githubAppPrivateKeyFilePath,
   278  				SrcRoot:                 srcRoot,
   279  				Log:                     path.Join(srcRoot, "log.txt"),
   280  				GitUserName:             "me",
   281  				GitUserEmail:            "me@domain.com",
   282  				CookiePath:              "cookies/path",
   283  				GitRefs: []prowapi.Refs{
   284  					{
   285  						Org:       githubAppOrg,
   286  						Repo:      "test-infra",
   287  						BaseRef:   "master",
   288  						PathAlias: "sigs.k8s.io/prow",
   289  						Pulls: []prowapi.Pull{
   290  							{
   291  								Number: 5,
   292  								SHA:    "FEEDDAD",
   293  							},
   294  						},
   295  						SkipSubmodules: true,
   296  					},
   297  				},
   298  			},
   299  			expectedClones: []cloneRec{
   300  				{
   301  					refs: prowapi.Refs{
   302  						Org:       "kubernetes",
   303  						Repo:      "test-infra",
   304  						BaseRef:   "master",
   305  						PathAlias: "sigs.k8s.io/prow",
   306  						Pulls: []prowapi.Pull{
   307  							{
   308  								Number: 5,
   309  								SHA:    "FEEDDAD",
   310  							},
   311  						},
   312  						SkipSubmodules: true,
   313  					},
   314  					root:       srcRoot,
   315  					user:       "me",
   316  					email:      "me@domain.com",
   317  					cookiePath: "cookies/path",
   318  					authUser:   "x-access-token",
   319  					authToken:  githubAppToken,
   320  				},
   321  			},
   322  		},
   323  	}
   324  	for _, tc := range testcases {
   325  		t.Run(tc.name, func(t *testing.T) {
   326  			defer func() { recordedClones = nil }()
   327  			os.RemoveAll(srcRoot)
   328  			os.MkdirAll(srcRoot, os.ModePerm)
   329  
   330  			if err := tc.opts.Run(); err != nil {
   331  				t.Fatalf("Unexpected error: %v.", err)
   332  			}
   333  
   334  			// Check for set equality (ignore ordering)
   335  			for _, rec := range recordedClones {
   336  				found := false
   337  				var exp cloneRec
   338  				for _, exp = range tc.expectedClones {
   339  					if reflect.DeepEqual(rec, exp) {
   340  						found = true
   341  						break
   342  					}
   343  				}
   344  				if !found {
   345  					t.Errorf("recordedClones %#v is missing expected clone %#v", recordedClones, exp)
   346  				}
   347  			}
   348  			if rec, exp := len(recordedClones), len(tc.expectedClones); rec != exp {
   349  				t.Errorf("recordedClones has length %d and expectedClones has length %d", rec, exp)
   350  			}
   351  		})
   352  	}
   353  }
   354  
   355  func TestNeedsGlobalCookiePath(t *testing.T) {
   356  	cases := []struct {
   357  		name       string
   358  		cookieFile string
   359  		refs       []prowapi.Refs
   360  		expected   string
   361  	}{
   362  		{
   363  			name: "basically works",
   364  		},
   365  		{
   366  			name: "return empty when no cookieFile",
   367  			refs: []prowapi.Refs{
   368  				{},
   369  			},
   370  		},
   371  		{
   372  			name:       "return empty when no refs",
   373  			cookieFile: "foo",
   374  		},
   375  		{
   376  			name:       "return empty when all refs skip submodules",
   377  			cookieFile: "foo",
   378  			refs: []prowapi.Refs{
   379  				{SkipSubmodules: true},
   380  				{SkipSubmodules: true},
   381  			},
   382  		},
   383  		{
   384  			name:       "return cookieFile when all refs use submodules",
   385  			cookieFile: "foo",
   386  			refs: []prowapi.Refs{
   387  				{},
   388  				{},
   389  			},
   390  			expected: "foo",
   391  		},
   392  		{
   393  			name:       "return cookieFile when any refs uses submodules",
   394  			cookieFile: "foo",
   395  			refs: []prowapi.Refs{
   396  				{SkipSubmodules: true},
   397  				{},
   398  			},
   399  			expected: "foo",
   400  		},
   401  	}
   402  
   403  	for _, tc := range cases {
   404  		t.Run(tc.name, func(t *testing.T) {
   405  			if actual := needsGlobalCookiePath(tc.cookieFile, tc.refs...); actual != tc.expected {
   406  				t.Errorf("needsGlobalCookiePath(%q,%v) got %q, want %q", tc.cookieFile, tc.refs, actual, tc.expected)
   407  			}
   408  		})
   409  	}
   410  }
   411  
   412  func mockGitHubAppHandler(org, token string) http.Handler {
   413  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   414  		switch r.URL.Path {
   415  		case "/app":
   416  			json.NewEncoder(w).Encode(github.App{
   417  				Slug: "slug",
   418  			})
   419  		case "/app/installations":
   420  			json.NewEncoder(w).Encode([]github.AppInstallation{
   421  				{
   422  					ID: 1,
   423  					Account: github.User{
   424  						Login: org,
   425  					},
   426  				},
   427  			})
   428  		case "/app/installations/1/access_tokens":
   429  			w.WriteHeader(http.StatusCreated)
   430  			json.NewEncoder(w).Encode(&github.AppInstallationToken{
   431  				Token:     token,
   432  				ExpiresAt: time.Now().Add(time.Minute),
   433  			})
   434  		default:
   435  			fmt.Println(r.URL.Path)
   436  			w.WriteHeader(http.StatusNotFound)
   437  		}
   438  	})
   439  }