Skip to content

Commit 3cc6d50

Browse files
authored
Support loading Starlark modules from Git (#85)
* Move loader to a separate package * Make context available to the loader * Support loading Starlark modules from Git See #53. * Use bounded filesystem wrapper when working with Git * git.Retrieve(): return more contextualized errors * Provide a hint when loading of the module that ends with .start fails
1 parent 47bfb43 commit 3cc6d50

File tree

10 files changed

+464
-11
lines changed

10 files changed

+464
-11
lines changed

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ require (
1313
github.com/docker/distribution v2.7.1+incompatible // indirect
1414
github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible
1515
github.com/docker/go-connections v0.4.0 // indirect
16-
github.com/docker/go-units v0.4.0 // indirect
16+
github.com/docker/go-units v0.4.0
17+
github.com/go-git/go-billy/v5 v5.0.0
1718
github.com/go-git/go-git/v5 v5.1.0
1819
github.com/go-test/deep v1.0.7
1920
github.com/gogo/protobuf v1.3.1 // indirect

pkg/larker/larker.go

+3-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"github.com/cirruslabs/cirrus-cli/pkg/larker/fs"
8+
"github.com/cirruslabs/cirrus-cli/pkg/larker/loader"
89
"go.starlark.net/resolve"
910
"go.starlark.net/starlark"
1011
"gopkg.in/yaml.v2"
@@ -18,8 +19,7 @@ var (
1819
)
1920

2021
type Larker struct {
21-
fs fs.FileSystem
22-
loader *Loader
22+
fs fs.FileSystem
2323
}
2424

2525
func New(opts ...Option) *Larker {
@@ -36,17 +36,14 @@ func New(opts ...Option) *Larker {
3636
opt(lrk)
3737
}
3838

39-
// Some fields can only be set after we apply the options
40-
lrk.loader = NewLoader(lrk.fs)
41-
4239
return lrk
4340
}
4441

4542
func (larker *Larker) Main(ctx context.Context, source string) (string, error) {
4643
discard := func(thread *starlark.Thread, msg string) {}
4744

4845
thread := &starlark.Thread{
49-
Load: larker.loader.LoadFunc(),
46+
Load: loader.NewLoader(ctx, larker.fs).LoadFunc(),
5047
Print: discard,
5148
}
5249

pkg/larker/larker_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/cirruslabs/cirrus-cli/pkg/larker"
88
"github.com/cirruslabs/cirrus-cli/pkg/larker/fs"
99
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
1011
"io/ioutil"
1112
"path/filepath"
1213
"testing"
@@ -102,3 +103,55 @@ func TestCycle(t *testing.T) {
102103
assert.Error(t, err)
103104
assert.True(t, errors.Is(err, larker.ErrLoadFailed))
104105
}
106+
107+
// TestLoadGitHelpers ensures that we can use https://github.com/cirrus-templates/helpers
108+
// as demonstrated in it's README.md.
109+
//
110+
// Note that we lock the revision in the .cirrus.star's load statement to prevent failures in the future.
111+
func TestLoadGitHelpers(t *testing.T) {
112+
dir := testutil.TempDirPopulatedWith(t, "testdata/load-git-helpers")
113+
114+
// Read the source code
115+
source, err := ioutil.ReadFile(filepath.Join(dir, ".cirrus.star"))
116+
if err != nil {
117+
t.Fatal(err)
118+
}
119+
120+
// Run the source code
121+
lrk := larker.New(larker.WithFileSystem(fs.NewLocalFileSystem(dir)))
122+
result, err := lrk.Main(context.Background(), string(source))
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
127+
// Compare the output
128+
expected, err := ioutil.ReadFile(filepath.Join(dir, "expected.yml"))
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
assert.YAMLEq(t, string(expected), result)
133+
}
134+
135+
// TestLoadTypoStarVsStart ensures that we return a user-friendly hint when loading of the module
136+
// that ends with ".start" fails.
137+
func TestLoadTypoStarVsStart(t *testing.T) {
138+
dir := testutil.TempDir(t)
139+
140+
lrk := larker.New(larker.WithFileSystem(fs.NewLocalFileSystem(dir)))
141+
142+
// No hint
143+
_, err := lrk.Main(context.Background(), "load(\"some/lib.star\", \"symbol\")\n")
144+
require.Error(t, err)
145+
assert.NotContains(t, err.Error(), "perhaps you've meant")
146+
147+
// Hint when loading from Git
148+
_, err = lrk.Main(context.Background(),
149+
"load(\"github.com/cirrus-templates/helpers/dir/lib.start@master\", \"symbol\")\n")
150+
require.Error(t, err)
151+
assert.Contains(t, err.Error(), "instead of the .start?")
152+
153+
// Hint when loading from FS
154+
_, err = lrk.Main(context.Background(), "load(\"dir/lib.start\", \"symbol\")\n")
155+
require.Error(t, err)
156+
assert.Contains(t, err.Error(), "instead of the .start?")
157+
}
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package bounded
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/go-git/go-billy/v5"
7+
"github.com/go-git/go-billy/v5/memfs"
8+
"os"
9+
)
10+
11+
var ErrExhausted = errors.New("filesystem limit exhausted")
12+
13+
type FileSystem struct {
14+
maxFiles int
15+
maxBytes int
16+
17+
filesCreated int
18+
bytesWritten int
19+
20+
billy.Filesystem
21+
}
22+
23+
type File struct {
24+
bfs *FileSystem
25+
26+
billy.File
27+
}
28+
29+
func NewFilesystem(maxBytes, maxFiles int) billy.Filesystem {
30+
return &FileSystem{
31+
maxFiles: maxFiles,
32+
maxBytes: maxBytes,
33+
Filesystem: memfs.New(),
34+
}
35+
}
36+
37+
func (bfs *FileSystem) Create(filename string) (billy.File, error) {
38+
return bfs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
39+
}
40+
41+
func (bfs *FileSystem) Open(filename string) (billy.File, error) {
42+
return bfs.OpenFile(filename, os.O_RDONLY, 0)
43+
}
44+
45+
func (bfs *FileSystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
46+
if err := bfs.moreFiles(1); err != nil {
47+
return nil, err
48+
}
49+
50+
file, err := bfs.Filesystem.OpenFile(filename, flag, perm)
51+
52+
// Accounting
53+
if err == nil && (flag&os.O_CREATE) != 0 {
54+
bfs.filesCreated++
55+
}
56+
57+
return &File{
58+
bfs: bfs,
59+
File: file,
60+
}, err
61+
}
62+
63+
func (bfs *FileSystem) MkdirAll(filename string, perm os.FileMode) error {
64+
if err := bfs.moreFiles(1); err != nil {
65+
return err
66+
}
67+
68+
err := bfs.Filesystem.MkdirAll(filename, perm)
69+
70+
// Accounting
71+
if err == nil {
72+
bfs.filesCreated++
73+
}
74+
75+
return err
76+
}
77+
78+
func (bfs *FileSystem) moreFiles(n int) error {
79+
if (bfs.filesCreated + n) > bfs.maxFiles {
80+
return fmt.Errorf("%w: attempted to create more than %d files", ErrExhausted, bfs.maxFiles)
81+
}
82+
83+
return nil
84+
}
85+
86+
func (bfs *FileSystem) moreBytes(n int) error {
87+
if (bfs.bytesWritten + n) > bfs.maxBytes {
88+
return fmt.Errorf("%w: attempted to write more than %d bytes", ErrExhausted, bfs.maxBytes)
89+
}
90+
91+
return nil
92+
}
93+
94+
func (bf *File) Write(p []byte) (int, error) {
95+
if err := bf.bfs.moreBytes(len(p)); err != nil {
96+
return -1, err
97+
}
98+
99+
n, err := bf.File.Write(p)
100+
101+
// Accounting
102+
if err == nil {
103+
bf.bfs.bytesWritten += n
104+
}
105+
106+
return n, err
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package bounded_test
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/cirruslabs/cirrus-cli/pkg/larker/loader/git/bounded"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
"testing"
10+
)
11+
12+
func TestByteBounds(t *testing.T) {
13+
const maxBytes = 65536
14+
15+
fs := bounded.NewFilesystem(maxBytes, 1)
16+
17+
file, err := fs.Create("some-file.txt")
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
22+
for i := 0; i < maxBytes; i++ {
23+
_, err := file.Write([]byte("A"))
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
}
28+
29+
_, err = file.Write([]byte("A"))
30+
require.Error(t, err)
31+
assert.True(t, errors.Is(err, bounded.ErrExhausted))
32+
}
33+
34+
func TestFileBounds(t *testing.T) {
35+
const maxFiles = 128
36+
37+
fs := bounded.NewFilesystem(1, maxFiles)
38+
39+
for i := 0; i < maxFiles; i++ {
40+
_, err := fs.Create(fmt.Sprintf("%d.txt", i))
41+
if err != nil {
42+
t.Fatal(err)
43+
}
44+
}
45+
46+
_, err := fs.Create("some-file.txt")
47+
require.Error(t, err)
48+
assert.True(t, errors.Is(err, bounded.ErrExhausted))
49+
}

0 commit comments

Comments
 (0)