Skip to content
This repository was archived by the owner on Nov 17, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const (
flagTLACodeFile = "tla-code-file"
flagResolver = "resolve-images"
flagResolvFail = "resolve-images-error"
flagCacheDir = "cache-dir"
)

var clientConfig clientcmd.ClientConfig
Expand All @@ -84,6 +85,7 @@ func init() {
RootCmd.MarkPersistentFlagFilename(flagTLACodeFile)
RootCmd.PersistentFlags().String(flagResolver, "noop", "Change implementation of resolveImage native function. One of: noop, registry")
RootCmd.PersistentFlags().String(flagResolvFail, "warn", "Action when resolveImage fails. One of ignore,warn,error")
RootCmd.PersistentFlags().String(flagCacheDir, "", "Directory to cache remote files. Defaults to the current user's cache directory.")

// The "usual" clientcmd/kubectl flags
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
Expand Down Expand Up @@ -252,7 +254,20 @@ func JsonnetVM(cmd *cobra.Command) (*jsonnet.VM, error) {
return nil, fmt.Errorf("Unable to determine current working directory: %v", err)
}

vm.Importer(utils.MakeUniversalImporter(searchUrls))
// Determine the caching directory
cacheDir, err := flags.GetString(flagCacheDir)
if err != nil {
return nil, err
}
if cacheDir == "" {
userCache, err := os.UserCacheDir()
if err != nil {
return nil, err
}
cacheDir = filepath.Join(userCache, "kubecfg")
}

vm.Importer(utils.MakeUniversalImporter(searchUrls, cacheDir))

for _, spec := range []struct {
flagName string
Expand Down
154 changes: 154 additions & 0 deletions utils/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package utils

import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"

assetfs "github.com/elazarl/go-bindata-assetfs"
jsonnet "github.com/google/go-jsonnet"
log "github.com/sirupsen/logrus"
)

var errNotFound = errors.New("Not found")

// cache implements a dumb local cache for files fetched remotely.
type httpCache struct {
// The location of the cache directory
cacheDir string
// The http client used for requests
httpClient *http.Client
}

func NewHTTPCache(cacheDir string) *httpCache {
// Reconstructed copy of http.DefaultTransport (to avoid
// modifying the default)
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
t.RegisterProtocol("internal", http.NewFileTransport(newInternalFS("lib")))

return &httpCache{
cacheDir: cacheDir,
httpClient: &http.Client{
Transport: t,
},
}
}

var httpRegex = regexp.MustCompile("^(https?)://")
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was originally going to just escape the // - hence the capture group.

Ultimately, liked just stripping the protocol and nesting directories more. Makes for an easier to manage cache as well imo.


func (h *httpCache) getLocalPath(url string) string {
return filepath.Join(h.cacheDir, httpRegex.ReplaceAllString(url, ""))
}

func (h *httpCache) tryLocalCache(url string) (jsonnet.Contents, error) {
localPath := h.getLocalPath(url)
bytes, err := ioutil.ReadFile(localPath)
if err != nil {
return jsonnet.Contents{}, err
}
log.Debugf("Read %q from local cache at %q", url, localPath)
return jsonnet.MakeContents(string(bytes)), nil
}

func (h *httpCache) writeToCache(url string, contents []byte) error {
localPath := h.getLocalPath(url)
localPathDir := filepath.Dir(localPath)
finfo, err := os.Stat(localPathDir)
if err != nil {
if !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(localPathDir, 0755); err != nil {
return err
}
}
if err == nil && !finfo.IsDir() {
return fmt.Errorf("%q is not a directory, it cannot be used for caching", localPathDir)
}
return ioutil.WriteFile(localPath, contents, 0644)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know off hand if this will replace an existing symlink to nothing (the afore discussed power-user way of disabling caching for certain things) or return an error. I should test it probably before this is merged. ​I assume if it doesn't error, we'll want to check for a pre-existing dead link or whatever to make sure we don't mess with something the user did on purpose.

}

func (h *httpCache) Get(url string) (jsonnet.Contents, error) {
isHTTP := httpRegex.MatchString(url)

// If this is an http url, try the local cache first
if isHTTP {
contents, err := h.tryLocalCache(url)
if err == nil {
return contents, nil
}
log.Debugf("Error reading %q from local cache: %s", url, err)
}

// Attempt a normal GET
res, err := h.httpClient.Get(url)
if err != nil {
return jsonnet.Contents{}, err
}
defer res.Body.Close()

log.Debugf("GET %q -> %s", url, res.Status)
if res.StatusCode == http.StatusNotFound {
return jsonnet.Contents{}, errNotFound
} else if res.StatusCode != http.StatusOK {
return jsonnet.Contents{}, fmt.Errorf("error reading content: %s", res.Status)
}

bodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return jsonnet.Contents{}, err
}

// If it was an http url, write the contents to the local cache
if isHTTP {
if err := h.writeToCache(url, bodyBytes); err != nil {
log.Debugf("Error writing %q to the local cache: %s", url, err)
}
}

return jsonnet.MakeContents(string(bodyBytes)), nil
}

//go:generate go-bindata -nometadata -ignore .*_test\.|~$DOLLAR -pkg $GOPACKAGE -o bindata.go -prefix ../ ../lib/...
func newInternalFS(prefix string) http.FileSystem {
// Asset/AssetDir returns `fmt.Errorf("Asset %s not found")`,
// which does _not_ get mapped to 404 by `http.FileSystem`.
// Need to convert to `os.ErrNotExist` explicitly ourselves.
mapNotFound := func(err error) error {
if err != nil && strings.Contains(err.Error(), "not found") {
err = os.ErrNotExist
}
return err
}
return &assetfs.AssetFS{
Asset: func(path string) ([]byte, error) {
ret, err := Asset(path)
return ret, mapNotFound(err)
},
AssetDir: func(path string) ([]string, error) {
ret, err := AssetDir(path)
return ret, mapNotFound(err)
},
Prefix: prefix,
}
}
25 changes: 25 additions & 0 deletions utils/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package utils

import (
"os"
"testing"
)

func TestInternalFS(t *testing.T) {
fs := newInternalFS("lib")
if _, err := fs.Open("kubecfg.libsonnet"); err != nil {
t.Errorf("opening kubecfg.libsonnet failed! %v", err)
}
if _, err := fs.Open("noexist"); !os.IsNotExist(err) {
t.Errorf("Incorrect noexist error: %v", err)
}
if _, err := fs.Open("noexist/foo"); !os.IsNotExist(err) {
t.Errorf("Incorrect noexist dir error: %v", err)
}

// This test really belongs somewhere else, but it's easiest
// to do here.
if _, err := fs.Open("kubecfg_test.jsonnet"); err == nil {
t.Errorf("kubecfg_test.jsonnet should not have been embedded")
}
}
80 changes: 5 additions & 75 deletions utils/importer.go
Original file line number Diff line number Diff line change
@@ -1,56 +1,24 @@
package utils

import (
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"

assetfs "github.com/elazarl/go-bindata-assetfs"
jsonnet "github.com/google/go-jsonnet"
log "github.com/sirupsen/logrus"
)

var errNotFound = errors.New("Not found")

var extVarKindRE = regexp.MustCompile("^<(?:extvar|top-level-arg):.+>$")

//go:generate go-bindata -nometadata -ignore .*_test\.|~$DOLLAR -pkg $GOPACKAGE -o bindata.go -prefix ../ ../lib/...
func newInternalFS(prefix string) http.FileSystem {
// Asset/AssetDir returns `fmt.Errorf("Asset %s not found")`,
// which does _not_ get mapped to 404 by `http.FileSystem`.
// Need to convert to `os.ErrNotExist` explicitly ourselves.
mapNotFound := func(err error) error {
if err != nil && strings.Contains(err.Error(), "not found") {
err = os.ErrNotExist
}
return err
}
return &assetfs.AssetFS{
Asset: func(path string) ([]byte, error) {
ret, err := Asset(path)
return ret, mapNotFound(err)
},
AssetDir: func(path string) ([]string, error) {
ret, err := AssetDir(path)
return ret, mapNotFound(err)
},
Prefix: prefix,
}
}

/*
MakeUniversalImporter creates an importer that handles resolving imports from the filesystem and HTTP/S.

In addition to the standard importer, supports:
- URLs in import statements
- URLs in library search paths
- Local caching for files retrieved from remote locations

A real-world example:
- You have https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master in your search URLs.
Expand All @@ -62,35 +30,17 @@ A real-world example:
will be resolved as https://raw.githubusercontent.com/ksonnet/ksonnet-lib/master/ksonnet.beta.2/k8s.libsonnet
and downloaded from that location.
*/
func MakeUniversalImporter(searchURLs []*url.URL) jsonnet.Importer {
// Reconstructed copy of http.DefaultTransport (to avoid
// modifying the default)
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
t.RegisterProtocol("internal", http.NewFileTransport(newInternalFS("lib")))

func MakeUniversalImporter(searchURLs []*url.URL, cacheDir string) jsonnet.Importer {
return &universalImporter{
BaseSearchURLs: searchURLs,
HTTPClient: &http.Client{Transport: t},
HTTPCache: NewHTTPCache(cacheDir),
cache: map[string]jsonnet.Contents{},
}
}

type universalImporter struct {
BaseSearchURLs []*url.URL
HTTPClient *http.Client
HTTPCache *httpCache
cache map[string]jsonnet.Contents
}

Expand All @@ -110,7 +60,7 @@ func (importer *universalImporter) Import(importedFrom, importedPath string) (js
}

tried = append(tried, foundAt)
importedData, err := importer.tryImport(foundAt)
importedData, err := importer.HTTPCache.Get(foundAt)
if err == nil {
importer.cache[foundAt] = importedData
return importedData, foundAt, nil
Expand All @@ -125,26 +75,6 @@ func (importer *universalImporter) Import(importedFrom, importedPath string) (js
)
}

func (importer *universalImporter) tryImport(url string) (jsonnet.Contents, error) {
res, err := importer.HTTPClient.Get(url)
if err != nil {
return jsonnet.Contents{}, err
}
defer res.Body.Close()
log.Debugf("GET %q -> %s", url, res.Status)
if res.StatusCode == http.StatusNotFound {
return jsonnet.Contents{}, errNotFound
} else if res.StatusCode != http.StatusOK {
return jsonnet.Contents{}, fmt.Errorf("error reading content: %s", res.Status)
}

bodyBytes, err := ioutil.ReadAll(res.Body)
if err != nil {
return jsonnet.Contents{}, err
}
return jsonnet.MakeContents(string(bodyBytes)), nil
}

func (importer *universalImporter) expandImportToCandidateURLs(importedFrom, importedPath string) ([]*url.URL, error) {
importedPathURL, err := url.Parse(importedPath)
if err != nil {
Expand Down
20 changes: 0 additions & 20 deletions utils/importer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,10 @@ package utils

import (
"net/url"
"os"
"reflect"
"testing"
)

func TestInternalFS(t *testing.T) {
fs := newInternalFS("lib")
if _, err := fs.Open("kubecfg.libsonnet"); err != nil {
t.Errorf("opening kubecfg.libsonnet failed! %v", err)
}
if _, err := fs.Open("noexist"); !os.IsNotExist(err) {
t.Errorf("Incorrect noexist error: %v", err)
}
if _, err := fs.Open("noexist/foo"); !os.IsNotExist(err) {
t.Errorf("Incorrect noexist dir error: %v", err)
}

// This test really belongs somewhere else, but it's easiest
// to do here.
if _, err := fs.Open("kubecfg_test.jsonnet"); err == nil {
t.Errorf("kubecfg_test.jsonnet should not have been embedded")
}
}

func TestExpandImportToCandidateURLs(t *testing.T) {
importer := universalImporter{
BaseSearchURLs: []*url.URL{
Expand Down