job.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. package job
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. "path/filepath"
  8. "reflect"
  9. "strings"
  10. "time"
  11. "git.bazzel.dev/bmallen/helios/pkg/state"
  12. "git.bazzel.dev/bmallen/helios/pkg/symbols"
  13. "github.com/go-git/go-git/v6"
  14. "github.com/go-git/go-git/v6/plumbing"
  15. "github.com/go-git/go-git/v6/plumbing/transport/http"
  16. "github.com/otiai10/copy"
  17. "github.com/sirupsen/logrus"
  18. "github.com/traefik/yaegi/interp"
  19. "github.com/traefik/yaegi/stdlib"
  20. "golang.org/x/mod/modfile"
  21. "gopkg.in/yaml.v3"
  22. )
  23. type Job struct {
  24. state *state.State
  25. sourceDir string
  26. files []string
  27. source string
  28. ref string
  29. user string
  30. password string
  31. globals map[string]any
  32. timeout time.Duration
  33. autoimport bool
  34. autocleanup bool
  35. module string
  36. errors []error
  37. log *logrus.Logger
  38. logs []string
  39. workspaceCreated bool
  40. workspacedir string
  41. i *interp.Interpreter
  42. }
  43. func New() *Job {
  44. j := &Job{
  45. state: state.New(),
  46. timeout: 10 * time.Second,
  47. log: logrus.New(),
  48. globals: map[string]any{},
  49. workspacedir: "workspace",
  50. }
  51. j.log.AddHook(j)
  52. return j
  53. }
  54. func NewWithState(state *state.State) *Job {
  55. j := New()
  56. j.state = state
  57. return j
  58. }
  59. func (j *Job) Levels() []logrus.Level {
  60. return logrus.AllLevels
  61. }
  62. func (j *Job) Fire(e *logrus.Entry) error {
  63. // j.logs = append(j.logs, fmt.Sprintf("%s: %s", e.Level.String(), e.Message))
  64. j.logs = append(j.logs, e.Message)
  65. return nil
  66. }
  67. func (j *Job) error(err error) *Job {
  68. if err != nil {
  69. j.log.Error(err)
  70. if j.errors == nil {
  71. j.errors = []error{}
  72. }
  73. j.errors = append(j.errors, err)
  74. }
  75. return j
  76. }
  77. func (j *Job) SourceDir(dir string) *Job {
  78. j.sourceDir = dir
  79. return j
  80. }
  81. func (j *Job) AutoCleanup() *Job {
  82. j.autocleanup = true
  83. return j
  84. }
  85. func (j *Job) Module(module string) *Job {
  86. j.module = module
  87. return j
  88. }
  89. func (j *Job) Source(url, ref string) *Job {
  90. if j.source != "" {
  91. return j.error(errors.New("source has already been set"))
  92. }
  93. j.source = url
  94. j.ref = url
  95. err := os.MkdirAll(filepath.Join(j.workspacedir, j.ID()), 0770)
  96. if err != nil {
  97. return j.error(err)
  98. }
  99. j.log.Info("Cloning...")
  100. _, err = git.PlainClone(
  101. filepath.Join(j.workspacedir, j.ID(), "repo"),
  102. &git.CloneOptions{
  103. URL: j.source,
  104. ReferenceName: plumbing.ReferenceName(ref),
  105. Progress: j.log.WriterLevel(logrus.DebugLevel),
  106. Auth: &http.BasicAuth{
  107. Username: j.user, // yes, this can be anything except an empty string
  108. Password: j.password,
  109. },
  110. })
  111. j.error(err)
  112. j.sourceDir = filepath.Join(j.workspacedir, j.ID(), "repo")
  113. return j
  114. }
  115. func (j *Job) User(user string) *Job {
  116. j.user = user
  117. return j
  118. }
  119. func (j *Job) Password(password string) *Job {
  120. j.password = password
  121. return j
  122. }
  123. func (j *Job) Timeout(timeout time.Duration) *Job {
  124. j.timeout = timeout
  125. return j
  126. }
  127. func (j *Job) AutoImport() *Job {
  128. j.autoimport = true
  129. return j
  130. }
  131. func (j *Job) Run(f string) *Job {
  132. if j.sourceDir == "" {
  133. return j.error(errors.New("must provide source"))
  134. }
  135. start := time.Now()
  136. workdir := filepath.Join(j.workspacedir, j.state.ID(), "src", j.module)
  137. if !j.workspaceCreated {
  138. if j.module == "" {
  139. modfiledata, err := os.ReadFile(filepath.Join(j.sourceDir, "go.mod"))
  140. if err != nil {
  141. return j.error(err)
  142. }
  143. fff, err := modfile.Parse("go.mod", modfiledata, nil)
  144. if err != nil {
  145. return j.error(err)
  146. }
  147. // for _, r := range fff.Require {
  148. // j.log.Info(" ", r.Mod.String())
  149. // }
  150. j.module = fff.Module.Mod.Path
  151. }
  152. j.log.Info("Creating workspace...")
  153. err := os.MkdirAll(filepath.Join(j.workspacedir, j.state.ID(), "src"), 0770)
  154. if err != nil {
  155. return j.error(err)
  156. }
  157. workdir = filepath.Join(j.workspacedir, j.state.ID(), "src", j.module)
  158. err = copy.Copy(j.sourceDir, workdir)
  159. if err != nil {
  160. return j.error(err)
  161. }
  162. j.workspaceCreated = true
  163. }
  164. if j.autocleanup {
  165. defer func() {
  166. }()
  167. }
  168. j.log.Info("Setting up new interpreter...")
  169. workspace, err := filepath.Abs(filepath.Join(j.workspacedir, j.state.ID()))
  170. if err != nil {
  171. return j.error(err)
  172. }
  173. if j.autocleanup {
  174. defer func() {
  175. err := os.RemoveAll(workspace)
  176. if err != nil {
  177. j.error(err)
  178. }
  179. }()
  180. }
  181. j.i = interp.New(interp.Options{
  182. GoPath: workspace,
  183. Unrestricted: true,
  184. Stdin: nil,
  185. Stdout: j.log.WriterLevel(logrus.InfoLevel),
  186. Stderr: j.log.WriterLevel(logrus.WarnLevel),
  187. })
  188. err = j.i.Use(stdlib.Symbols)
  189. if err != nil {
  190. return j.error(err)
  191. }
  192. err = j.i.Use(symbols.Symbols)
  193. if err != nil {
  194. return j.error(err)
  195. }
  196. err = j.i.Use(j.state.Symbols())
  197. if err != nil {
  198. return j.error(err)
  199. }
  200. if j.autoimport {
  201. j.i.ImportUsed()
  202. }
  203. j.log.Info("Starting job...")
  204. err = os.Chdir(workdir)
  205. if err != nil {
  206. return j.error(err)
  207. }
  208. j.files = append(j.files, f)
  209. if strings.HasSuffix(f, ".go") {
  210. data, err := os.ReadFile(f)
  211. if err != nil {
  212. return j.error(err)
  213. }
  214. ctx, _ := context.WithTimeout(context.Background(), j.timeout)
  215. _, err = j.i.EvalWithContext(ctx, string(data))
  216. if err != nil {
  217. j.error(err)
  218. }
  219. } else {
  220. ctx, _ := context.WithTimeout(context.Background(), j.timeout)
  221. _, err := j.i.EvalPathWithContext(ctx, filepath.Join(j.module, f))
  222. if err != nil {
  223. j.error(err)
  224. }
  225. }
  226. for k, v := range j.i.Globals() {
  227. if v.CanInterface() {
  228. if v.Kind() == reflect.Func {
  229. continue
  230. }
  231. j.globals[k] = v.Interface()
  232. }
  233. }
  234. j.log.Info("Job finished in: ", time.Now().Sub(start))
  235. return j
  236. }
  237. func (j *Job) Globals() map[string]any {
  238. return j.globals
  239. }
  240. func (j *Job) State() *state.State {
  241. return j.state
  242. }
  243. func (j *Job) ID() string {
  244. return j.state.ID()
  245. }
  246. func (j *Job) Error() error {
  247. if j.errors != nil {
  248. err := []string{}
  249. if len(j.errors) > 1 {
  250. err = append(err, fmt.Sprintf("!!! %d: errors in stack !!!", len(j.errors)))
  251. }
  252. for i, e := range j.errors {
  253. err = append(err, fmt.Sprintf("%d: %s", i, e.Error()))
  254. }
  255. return errors.New(strings.Join(err, "\n"))
  256. }
  257. return nil
  258. }
  259. type Report struct {
  260. Module string
  261. ID string
  262. Source string
  263. Files []string
  264. Globals map[string]any
  265. State map[string]any
  266. Errors []string
  267. Logs []string
  268. }
  269. func (j *Job) Report() *Job {
  270. report := Report{
  271. ID: j.ID(),
  272. Source: j.source,
  273. Files: j.files,
  274. Module: j.module,
  275. Globals: map[string]any{},
  276. State: map[string]any{},
  277. Logs: j.logs,
  278. }
  279. for k, v := range j.Globals() {
  280. report.Globals[k] = v
  281. }
  282. for _, v := range j.errors {
  283. report.Errors = append(report.Errors, v.Error())
  284. }
  285. j.state.Range(func(key string, value any) bool {
  286. report.State[key] = value
  287. return true
  288. })
  289. out, err := yaml.Marshal(report)
  290. if err != nil {
  291. j.log.Error(err)
  292. } else {
  293. fmt.Print(string(out))
  294. }
  295. return j
  296. }