blob: b456f0dcff261c4e98f70452d9f94cd1e7aa0473 [file] [log] [blame]
Serge Bazanskibe538db2020-11-12 00:22:42 +01001package utils
2
3import (
4 "errors"
5 "fmt"
6 "io/ioutil"
7 "net"
8 "net/http"
9 "net/url"
10 "os"
11 "regexp"
12 "strings"
13 "time"
14
15 assetfs "github.com/elazarl/go-bindata-assetfs"
16 jsonnet "github.com/google/go-jsonnet"
17 log "github.com/sirupsen/logrus"
18)
19
20var errNotFound = errors.New("Not found")
21
22var extVarKindRE = regexp.MustCompile("^<(?:extvar|top-level-arg):.+>$")
23
24//go:generate go-bindata -nometadata -ignore .*_test\.|~$DOLLAR -pkg $GOPACKAGE -o bindata.go -prefix ../ ../lib/...
25func newInternalFS(prefix string) http.FileSystem {
26 // Asset/AssetDir returns `fmt.Errorf("Asset %s not found")`,
27 // which does _not_ get mapped to 404 by `http.FileSystem`.
28 // Need to convert to `os.ErrNotExist` explicitly ourselves.
29 mapNotFound := func(err error) error {
30 if err != nil && strings.Contains(err.Error(), "not found") {
31 err = os.ErrNotExist
32 }
33 return err
34 }
35 return &assetfs.AssetFS{
36 Asset: func(path string) ([]byte, error) {
37 ret, err := Asset(path)
38 return ret, mapNotFound(err)
39 },
40 AssetDir: func(path string) ([]string, error) {
41 ret, err := AssetDir(path)
42 return ret, mapNotFound(err)
43 },
44 Prefix: prefix,
45 }
46}
47
48/*
49MakeUniversalImporter creates an importer that handles resolving imports from the filesystem and HTTP/S.
50
51In addition to the standard importer, supports:
52 - URLs in import statements
53 - URLs in library search paths
54
55A real-world example:
56 - You have https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master in your search URLs.
57 - You evaluate a local file which calls `import "ksonnet.beta.2/k.libsonnet"`.
58 - If the `ksonnet.beta.2/k.libsonnet`` is not located in the current working directory, an attempt
59 will be made to follow the search path, i.e. to download
60 https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master/ksonnet.beta.2/k.libsonnet.
61 - Since the downloaded `k.libsonnet`` file turn in contains `import "k8s.libsonnet"`, the import
62 will be resolved as https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master/ksonnet.beta.2/k8s.libsonnet
63 and downloaded from that location.
64*/
65func MakeUniversalImporter(searchURLs []*url.URL) jsonnet.Importer {
66 // Reconstructed copy of http.DefaultTransport (to avoid
67 // modifying the default)
68 t := &http.Transport{
69 Proxy: http.ProxyFromEnvironment,
70 DialContext: (&net.Dialer{
71 Timeout: 30 * time.Second,
72 KeepAlive: 30 * time.Second,
73 DualStack: true,
74 }).DialContext,
75 MaxIdleConns: 100,
76 IdleConnTimeout: 90 * time.Second,
77 TLSHandshakeTimeout: 10 * time.Second,
78 ExpectContinueTimeout: 1 * time.Second,
79 }
80
81 t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
82 t.RegisterProtocol("internal", http.NewFileTransport(newInternalFS("lib")))
83
84 return &universalImporter{
85 BaseSearchURLs: searchURLs,
86 HTTPClient: &http.Client{Transport: t},
87 cache: map[string]jsonnet.Contents{},
88 }
89}
90
91type universalImporter struct {
92 BaseSearchURLs []*url.URL
93 HTTPClient *http.Client
94 cache map[string]jsonnet.Contents
95}
96
97func (importer *universalImporter) Import(importedFrom, importedPath string) (jsonnet.Contents, string, error) {
98 log.Debugf("Importing %q from %q", importedPath, importedFrom)
99
100 candidateURLs, err := importer.expandImportToCandidateURLs(importedFrom, importedPath)
101 if err != nil {
102 return jsonnet.Contents{}, "", fmt.Errorf("Could not get candidate URLs for when importing %s (imported from %s): %v", importedPath, importedFrom, err)
103 }
104
105 var tried []string
106 for _, u := range candidateURLs {
107 foundAt := u.String()
108 if c, ok := importer.cache[foundAt]; ok {
109 return c, foundAt, nil
110 }
111
112 tried = append(tried, foundAt)
113 importedData, err := importer.tryImport(foundAt)
114 if err == nil {
115 importer.cache[foundAt] = importedData
116 return importedData, foundAt, nil
117 } else if err != errNotFound {
118 return jsonnet.Contents{}, "", err
119 }
120 }
121
122 return jsonnet.Contents{}, "", fmt.Errorf("Couldn't open import %q, no match locally or in library search paths. Tried: %s",
123 importedPath,
124 strings.Join(tried, ";"),
125 )
126}
127
128func (importer *universalImporter) tryImport(url string) (jsonnet.Contents, error) {
129 res, err := importer.HTTPClient.Get(url)
130 if err != nil {
131 return jsonnet.Contents{}, err
132 }
133 defer res.Body.Close()
134 log.Debugf("GET %q -> %s", url, res.Status)
135 if res.StatusCode == http.StatusNotFound {
136 return jsonnet.Contents{}, errNotFound
137 } else if res.StatusCode != http.StatusOK {
138 return jsonnet.Contents{}, fmt.Errorf("error reading content: %s", res.Status)
139 }
140
141 bodyBytes, err := ioutil.ReadAll(res.Body)
142 if err != nil {
143 return jsonnet.Contents{}, err
144 }
145 return jsonnet.MakeContents(string(bodyBytes)), nil
146}
147
148func (importer *universalImporter) expandImportToCandidateURLs(importedFrom, importedPath string) ([]*url.URL, error) {
149 importedPathURL, err := url.Parse(importedPath)
150 if err != nil {
151 return nil, fmt.Errorf("Import path %q is not valid", importedPath)
152 }
153 if importedPathURL.IsAbs() {
154 return []*url.URL{importedPathURL}, nil
155 }
156
157 importDirURL, err := url.Parse(importedFrom)
158 if err != nil {
159 return nil, fmt.Errorf("Invalid import dir %q: %v", importedFrom, err)
160 }
161
162 candidateURLs := make([]*url.URL, 1, len(importer.BaseSearchURLs)+1)
163 candidateURLs[0] = importDirURL.ResolveReference(importedPathURL)
164
165 for _, u := range importer.BaseSearchURLs {
166 candidateURLs = append(candidateURLs, u.ResolveReference(importedPathURL))
167 }
168
169 return candidateURLs, nil
170}