-
Notifications
You must be signed in to change notification settings - Fork 18
/
larker.go
128 lines (104 loc) · 3.08 KB
/
larker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
package larker
import (
"context"
"errors"
"fmt"
"github.com/cirruslabs/cirrus-cli/pkg/larker/fs"
"github.com/cirruslabs/cirrus-cli/pkg/larker/fs/dummy"
"github.com/cirruslabs/cirrus-cli/pkg/larker/loader"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
"gopkg.in/yaml.v2"
)
var (
ErrLoadFailed = errors.New("load failed")
ErrExecFailed = errors.New("exec failed")
ErrMainFailed = errors.New("failed to call main")
ErrMainUnexpectedResult = errors.New("main returned unexpected result")
)
type Larker struct {
fs fs.FileSystem
env map[string]string
}
func New(opts ...Option) *Larker {
lrk := &Larker{
fs: dummy.New(),
env: make(map[string]string),
}
// weird global init by Starlark
// we need floats at least for configuring CPUs for containers
resolve.AllowFloat = true
// Apply options
for _, opt := range opts {
opt(lrk)
}
return lrk
}
func (larker *Larker) Main(ctx context.Context, source string) (string, error) {
discard := func(thread *starlark.Thread, msg string) {}
thread := &starlark.Thread{
Load: loader.NewLoader(ctx, larker.fs, larker.env).LoadFunc(),
Print: discard,
}
resCh := make(chan starlark.Value)
errCh := make(chan error)
go func() {
// Execute the source code for the main() to be visible
globals, err := starlark.ExecFile(thread, ".cirrus.star", source, nil)
if err != nil {
errCh <- fmt.Errorf("%w: %v", ErrLoadFailed, err)
return
}
// Retrieve main()
main, ok := globals["main"]
if !ok {
errCh <- fmt.Errorf("%w: main() not found", ErrMainFailed)
return
}
// Ensure that main() is a function
if _, ok := main.(*starlark.Function); !ok {
errCh <- fmt.Errorf("%w: main is not a function", ErrMainFailed)
}
// Prepare a context to pass to main() as it's first argument
mainCtx := &Context{}
mainResult, err := starlark.Call(thread, main, starlark.Tuple{mainCtx}, nil)
if err != nil {
errCh <- fmt.Errorf("%w: %v", ErrExecFailed, err)
return
}
resCh <- mainResult
}()
var mainResult starlark.Value
select {
case mainResult = <-resCh:
case err := <-errCh:
return "", err
case <-ctx.Done():
return "", ctx.Err()
}
// main() should return a list of tasks
starlarkList, ok := mainResult.(*starlark.List)
if !ok {
return "", fmt.Errorf("%w: result is not a list", ErrMainUnexpectedResult)
}
// Recurse into starlarkList and convert starlark.List's to []interface{}'s and
// starlark.Dict's to yaml.MapSlice's to make them YAML-serializable
yamlList := convertList(starlarkList)
if len(yamlList) == 0 {
return "", nil
}
// Adapt a list of tasks to a YAML configuration format that expects a map on it's outer layer
var serializableMainResult yaml.MapSlice
for _, listItem := range yamlList {
serializableMainResult = append(serializableMainResult, yaml.MapItem{
Key: "task",
Value: listItem,
})
}
// Produce the YAML configuration
yamlBytes, err := yaml.Marshal(&serializableMainResult)
if err != nil {
return "", fmt.Errorf("%w: cannot marshal into YAML: %v", ErrMainUnexpectedResult, err)
}
return string(yamlBytes), nil
}