job.go 7.1 KB

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