1
2
3
4 package git
5
6 import (
7 "fmt"
8 "log"
9 "net/url"
10 "path/filepath"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15
16 "sigs.k8s.io/kustomize/kyaml/errors"
17 "sigs.k8s.io/kustomize/kyaml/filesys"
18 )
19
20
21
22
23
24
25 const notCloned = filesys.ConfirmedDir("/notCloned")
26
27
28 type RepoSpec struct {
29
30
31 raw string
32
33
34 Host string
35
36
37
38 RepoPath string
39
40
41 Dir filesys.ConfirmedDir
42
43
44
45 KustRootPath string
46
47
48 Ref string
49
50
51 Submodules bool
52
53
54 Timeout time.Duration
55 }
56
57
58 func (x *RepoSpec) CloneSpec() string {
59 return x.Host + x.RepoPath
60 }
61
62 func (x *RepoSpec) CloneDir() filesys.ConfirmedDir {
63 return x.Dir
64 }
65
66 func (x *RepoSpec) Raw() string {
67 return x.raw
68 }
69
70 func (x *RepoSpec) AbsPath() string {
71 return x.Dir.Join(x.KustRootPath)
72 }
73
74 func (x *RepoSpec) Cleaner(fSys filesys.FileSystem) func() error {
75 return func() error { return fSys.RemoveAll(x.Dir.String()) }
76 }
77
78 const (
79 refQuery = "?ref="
80 gitSuffix = ".git"
81 gitRootDelimiter = "_git/"
82 pathSeparator = "/"
83 )
84
85
86
87
88
89
90
91
92
93
94
95 func NewRepoSpecFromURL(n string) (*RepoSpec, error) {
96 repoSpec := &RepoSpec{raw: n, Dir: notCloned, Timeout: defaultTimeout, Submodules: defaultSubmodules}
97 if filepath.IsAbs(n) {
98 return nil, fmt.Errorf("uri looks like abs path: %s", n)
99 }
100
101
102
103
104 n, query, _ := strings.Cut(n, "?")
105 repoSpec.Ref, repoSpec.Timeout, repoSpec.Submodules = parseQuery(query)
106
107 var err error
108
109
110 repoSpec.Host, n, err = extractHost(n)
111 if err != nil {
112 return nil, err
113 }
114
115
116
117 repoSpec.RepoPath, repoSpec.KustRootPath, err = parsePathParts(n, defaultRepoPathLength(repoSpec.Host))
118 if err != nil {
119 return nil, err
120 }
121
122 return repoSpec, nil
123 }
124
125 const allSegments = -999999
126 const orgRepoSegments = 2
127
128 func defaultRepoPathLength(host string) int {
129 if strings.HasPrefix(host, fileScheme) {
130 return allSegments
131 }
132 return orgRepoSegments
133 }
134
135
136
137
138
139
140
141
142 func parsePathParts(n string, defaultSegmentLength int) (string, string, error) {
143 repoPath, kustRootPath, success := tryExplicitMarkerSplit(n)
144 if !success {
145 repoPath, kustRootPath, success = tryDefaultLengthSplit(n, defaultSegmentLength)
146 }
147
148
149 if !success || len(repoPath) == 0 {
150 return "", "", fmt.Errorf("failed to parse repo path segment")
151 }
152 if kustRootPathExitsRepo(kustRootPath) {
153 return "", "", fmt.Errorf("url path exits repo: %s", n)
154 }
155
156 return repoPath, strings.TrimPrefix(kustRootPath, pathSeparator), nil
157 }
158
159 func tryExplicitMarkerSplit(n string) (string, string, bool) {
160
161
162
163 if gitRootIdx := strings.Index(n, gitRootDelimiter); gitRootIdx >= 0 {
164 gitRootPath := n[:gitRootIdx+len(gitRootDelimiter)]
165 subpathSegments := strings.Split(n[gitRootIdx+len(gitRootDelimiter):], pathSeparator)
166 return gitRootPath + subpathSegments[0], strings.Join(subpathSegments[1:], pathSeparator), true
167
168
169
170
171 } else if repoRootIdx := strings.Index(n, "//"); repoRootIdx >= 0 {
172 return n[:repoRootIdx], n[repoRootIdx+2:], true
173
174
175
176
177 } else if gitSuffixIdx := strings.Index(n, gitSuffix); gitSuffixIdx >= 0 {
178 upToGitSuffix := n[:gitSuffixIdx+len(gitSuffix)]
179 afterGitSuffix := n[gitSuffixIdx+len(gitSuffix):]
180 return upToGitSuffix, afterGitSuffix, true
181 }
182 return "", "", false
183 }
184
185 func tryDefaultLengthSplit(n string, defaultSegmentLength int) (string, string, bool) {
186
187 if defaultSegmentLength == allSegments {
188 return n, "", true
189
190
191
192 } else if segments := strings.Split(n, pathSeparator); len(segments) >= defaultSegmentLength {
193 firstNSegments := strings.Join(segments[:defaultSegmentLength], pathSeparator)
194 rest := strings.Join(segments[defaultSegmentLength:], pathSeparator)
195 return firstNSegments, rest, true
196 }
197 return "", "", false
198 }
199
200 func kustRootPathExitsRepo(kustRootPath string) bool {
201 cleanedPath := filepath.Clean(strings.TrimPrefix(kustRootPath, string(filepath.Separator)))
202 pathElements := strings.Split(cleanedPath, string(filepath.Separator))
203 return len(pathElements) > 0 &&
204 pathElements[0] == filesys.ParentDir
205 }
206
207
208 const defaultSubmodules = true
209
210
211 const defaultTimeout = 27 * time.Second
212
213 func parseQuery(query string) (string, time.Duration, bool) {
214 values, err := url.ParseQuery(query)
215
216 if err != nil {
217 return "", defaultTimeout, defaultSubmodules
218 }
219
220
221
222 ref := values.Get("version")
223 if queryValue := values.Get("ref"); queryValue != "" {
224 ref = queryValue
225 }
226
227
228
229 duration := defaultTimeout
230 if queryValue := values.Get("timeout"); queryValue != "" {
231
232
233 if intValue, err := strconv.Atoi(queryValue); err == nil && intValue > 0 {
234 duration = time.Duration(intValue) * time.Second
235 } else if durationValue, err := time.ParseDuration(queryValue); err == nil && durationValue > 0 {
236 duration = durationValue
237 }
238 }
239
240
241
242 submodules := defaultSubmodules
243 if queryValue := values.Get("submodules"); queryValue != "" {
244 if boolValue, err := strconv.ParseBool(queryValue); err == nil {
245 submodules = boolValue
246 }
247 }
248
249 return ref, duration, submodules
250 }
251
252 func extractHost(n string) (string, string, error) {
253 n = ignoreForcedGitProtocol(n)
254 scheme, n := extractScheme(n)
255 username, n := extractUsername(n)
256 stdGithub := isStandardGithubHost(n)
257 acceptSCP := acceptSCPStyle(scheme, username, stdGithub)
258
259
260
261 if err := validateScheme(scheme, acceptSCP); err != nil {
262 return "", "", err
263 }
264
265
266
267
268
269 if scheme == fileScheme {
270 return scheme, username + n, nil
271 }
272 var host, rest = n, ""
273 if sepIndex := findPathSeparator(n, acceptSCP); sepIndex >= 0 {
274 host, rest = n[:sepIndex+1], n[sepIndex+1:]
275 }
276
277
278 if stdGithub {
279 scheme, username, host = normalizeGithubHostParts(scheme, username)
280 }
281
282
283 if host == "" {
284 return "", "", errors.Errorf("failed to parse host segment")
285 }
286 return scheme + username + host, rest, nil
287 }
288
289
290
291
292
293
294
295 func ignoreForcedGitProtocol(n string) string {
296 n, found := trimPrefixIgnoreCase(n, "git::")
297 if found {
298 log.Println("Warning: Forcing the git protocol using the 'git::' URL prefix is not supported. " +
299 "Kustomize currently strips this invalid prefix, but will stop doing so in a future release. " +
300 "Please remove the 'git::' prefix from your configuration.")
301 }
302 return n
303 }
304
305
306
307
308
309
310 func acceptSCPStyle(scheme, username string, isGithubURL bool) bool {
311 return scheme == "" && (username != "" || isGithubURL)
312 }
313
314 func validateScheme(scheme string, acceptSCPStyle bool) error {
315
316 switch scheme {
317 case "":
318
319 if !acceptSCPStyle {
320 return fmt.Errorf("failed to parse scheme")
321 }
322 case sshScheme, fileScheme, httpsScheme, httpScheme:
323
324 default:
325
326
327 return fmt.Errorf("unsupported scheme %q", scheme)
328 }
329 return nil
330 }
331
332 const fileScheme = "file://"
333 const httpScheme = "http://"
334 const httpsScheme = "https://"
335 const sshScheme = "ssh://"
336
337 func extractScheme(s string) (string, string) {
338 for _, prefix := range []string{sshScheme, httpsScheme, httpScheme, fileScheme} {
339 if rest, found := trimPrefixIgnoreCase(s, prefix); found {
340 return prefix, rest
341 }
342 }
343 return "", s
344 }
345
346 func extractUsername(s string) (string, string) {
347 var userRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9-]*)@`)
348 if m := userRegexp.FindStringSubmatch(s); m != nil {
349 username := m[1] + "@"
350 return username, s[len(username):]
351 }
352 return "", s
353 }
354
355 func isStandardGithubHost(s string) bool {
356 lowerCased := strings.ToLower(s)
357 return strings.HasPrefix(lowerCased, "github.com/") ||
358 strings.HasPrefix(lowerCased, "github.com:")
359 }
360
361
362
363 func trimPrefixIgnoreCase(s, prefix string) (string, bool) {
364 if len(prefix) <= len(s) && strings.ToLower(s[:len(prefix)]) == prefix {
365 return s[len(prefix):], true
366 }
367 return s, false
368 }
369
370 func findPathSeparator(hostPath string, acceptSCP bool) int {
371 sepIndex := strings.Index(hostPath, pathSeparator)
372 if acceptSCP {
373 colonIndex := strings.Index(hostPath, ":")
374
375 if sepIndex == -1 || (colonIndex > 0 && colonIndex < sepIndex) {
376 sepIndex = colonIndex
377 }
378 }
379 return sepIndex
380 }
381
382 func normalizeGithubHostParts(scheme, username string) (string, string, string) {
383 if strings.HasPrefix(scheme, sshScheme) || username != "" {
384 return "", username, "github.com:"
385 }
386 return httpsScheme, "", "github.com/"
387 }
388
View as plain text