浏览代码

Update wrapper

Ben 4 天之前
父节点
当前提交
4db9bb3727
共有 15 个文件被更改,包括 890 次插入37 次删除
  1. 0 3
      .gitignore
  2. 3 0
      build.sh
  3. 49 0
      cmd/main.go
  4. 21 1
      go.mod
  5. 40 0
      go.sum
  6. 379 0
      pkg/job/job.go
  7. 7 6
      pkg/symbols/symbols.go
  8. 139 0
      pkg/workflow/workflow.go
  9. 4 0
      test.sh
  10. 76 11
      tools/aws.go
  11. 0 13
      tools/cloudformation.go
  12. 6 1
      tools/docker.go
  13. 53 0
      tools/file.go
  14. 105 0
      tools/ssl.go
  15. 8 2
      tools/terraform.go

+ 0 - 3
.gitignore

@@ -1,5 +1,2 @@
 workspace
 test
-pkg/job
-main.go
-main

+ 3 - 0
build.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+
+go build -o ~/bin/helios .

+ 49 - 0
cmd/main.go

@@ -0,0 +1,49 @@
+package main
+
+import (
+	"flag"
+	"log"
+
+	"git.bazzel.dev/bmallen/helios/pkg/job"
+)
+
+var (
+	srcdir      = flag.String("d", "", "source dir")
+	src         = flag.String("s", "", "source url")
+	ref         = flag.String("r", "", "source url ref")
+	f           = flag.String("f", "main.go", "file to run")
+	skipcleanup = flag.Bool("skipcleanup", false, "skip auto cleanup")
+	report      = flag.Bool("report", false, "output report")
+)
+
+func main() {
+	flag.Parse()
+
+	j := job.New()
+
+	if !*skipcleanup {
+		j.AutoCleanup()
+	}
+
+	if srcdir != nil {
+		if *srcdir != "" {
+			j.SourceDir(*srcdir)
+		}
+	}
+
+	if src != nil {
+		if *src != "" {
+			j.Source(*src, *ref)
+		}
+	}
+
+	j.Run(*f)
+
+	if *report {
+		j.Report()
+	}
+
+	if j.Error() != nil {
+		log.Fatal(j.Error())
+	}
+}

+ 21 - 1
go.mod

@@ -4,11 +4,16 @@ go 1.25.0
 
 require (
 	dario.cat/mergo v1.0.2
+	github.com/aws/aws-sdk-go-v2 v1.41.2
+	github.com/aws/aws-sdk-go-v2/config v1.32.10
+	github.com/aws/aws-sdk-go-v2/service/ec2 v1.291.0
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1
 	github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3
 	github.com/google/uuid v1.6.0
 	github.com/hashicorp/go-version v1.8.0
 	github.com/hashicorp/hc-install v0.9.3
 	github.com/hashicorp/terraform-exec v0.25.0
+	github.com/hashicorp/terraform-json v0.27.2
 	github.com/moby/go-archive v0.2.0
 	github.com/moby/moby/api v1.53.0
 	github.com/moby/moby/client v0.2.2
@@ -26,6 +31,22 @@ require (
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/ProtonMail/go-crypto v1.3.0 // indirect
 	github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.19.10 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
+	github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
+	github.com/aws/smithy-go v1.24.1 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/cloudflare/circl v1.6.3 // indirect
 	github.com/containerd/errdefs v1.0.0 // indirect
@@ -44,7 +65,6 @@ require (
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
-	github.com/hashicorp/terraform-json v0.27.2 // indirect
 	github.com/kevinburke/ssh_config v1.6.0 // indirect
 	github.com/klauspost/compress v1.18.4 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect

+ 40 - 0
go.sum

@@ -14,6 +14,46 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
 github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls=
+github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c=
+github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
+github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.291.0 h1:E0/zdPeHKCpXVRAImhnHJYgpfZnTCjnr6i75gZIhwHs=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.291.0/go.mod h1:2dMnUs1QzlGzsm46i9oBHAxVHQp7b6qF7PljWcgVEVE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9 h1:IJRzQTvdpjHRPItx9gzNcz7Y1F+xqAR+xiy9rr5ZYl8=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.9/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1 h1:giB30dEeoar5bgDnkE0q+z7cFjcHaCjulpmPVmuKR84=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.96.1/go.mod h1:071TH4M3botFLWDbzQLfBR7tXYi7Fs2RsXSiH7nlUlY=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
+github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0=
+github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=

+ 379 - 0
pkg/job/job.go

@@ -0,0 +1,379 @@
+package job
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strings"
+	"time"
+
+	"git.bazzel.dev/bmallen/helios/pkg/state"
+	"git.bazzel.dev/bmallen/helios/pkg/symbols"
+	"github.com/go-git/go-git/v6"
+	"github.com/go-git/go-git/v6/plumbing"
+	"github.com/go-git/go-git/v6/plumbing/transport/http"
+	"github.com/otiai10/copy"
+	"github.com/sirupsen/logrus"
+	"github.com/traefik/yaegi/interp"
+	"github.com/traefik/yaegi/stdlib"
+	"golang.org/x/mod/modfile"
+	"gopkg.in/yaml.v3"
+)
+
+type Job struct {
+	state            *state.State
+	sourceDir        string
+	files            []string
+	source           string
+	ref              string
+	user             string
+	password         string
+	globals          map[string]any
+	timeout          time.Duration
+	autoimport       bool
+	autocleanup      bool
+	module           string
+	errors           []error
+	log              *logrus.Logger
+	logs             []string
+	workspaceCreated bool
+	workspacedir     string
+	i                *interp.Interpreter
+}
+
+type BlankFormatter struct{}
+
+// Format renders a log entry to only the message part.
+func (f *BlankFormatter) Format(entry *logrus.Entry) ([]byte, error) {
+	// Return the message string as a byte slice, adding a newline character.
+	return []byte(entry.Message + "\n"), nil
+}
+
+func New() *Job {
+	j := &Job{
+		state:        state.New(),
+		timeout:      10 * time.Second,
+		log:          logrus.New(),
+		globals:      map[string]any{},
+		workspacedir: "workspace",
+	}
+
+	j.log.Formatter = &BlankFormatter{}
+	j.log.AddHook(j)
+
+	return j
+}
+
+func NewWithState(state *state.State) *Job {
+	j := New()
+	j.state = state
+	return j
+}
+
+func (j *Job) Levels() []logrus.Level {
+	return logrus.AllLevels
+}
+func (j *Job) Fire(e *logrus.Entry) error {
+	// j.logs = append(j.logs, fmt.Sprintf("%s: %s", e.Level.String(), e.Message))
+	j.logs = append(j.logs, e.Message)
+	return nil
+}
+
+func (j *Job) error(err error) *Job {
+	if err != nil {
+		j.log.Error(err)
+
+		if j.errors == nil {
+			j.errors = []error{}
+		}
+
+		j.errors = append(j.errors, err)
+	}
+
+	return j
+}
+
+func (j *Job) SourceDir(dir string) *Job {
+	j.sourceDir = dir
+	return j
+}
+
+func (j *Job) AutoCleanup() *Job {
+	j.autocleanup = true
+	return j
+}
+
+func (j *Job) Module(module string) *Job {
+	j.module = module
+	return j
+}
+
+func (j *Job) Source(url, ref string) *Job {
+	if j.source != "" {
+		return j.error(errors.New("source has already been set"))
+	}
+
+	j.source = url
+	j.ref = url
+
+	err := os.MkdirAll(filepath.Join(j.workspacedir, j.ID()), 0770)
+	if err != nil {
+		return j.error(err)
+	}
+
+	j.log.Info("Cloning...")
+	_, err = git.PlainClone(
+		filepath.Join(j.workspacedir, j.ID(), "repo"),
+		&git.CloneOptions{
+			URL:           j.source,
+			ReferenceName: plumbing.ReferenceName(ref),
+			Progress:      j.log.WriterLevel(logrus.DebugLevel),
+			Auth: &http.BasicAuth{
+				Username: j.user, // yes, this can be anything except an empty string
+				Password: j.password,
+			},
+		})
+
+	j.error(err)
+	j.sourceDir = filepath.Join(j.workspacedir, j.ID(), "repo")
+
+	return j
+}
+
+func (j *Job) User(user string) *Job {
+	j.user = user
+	return j
+}
+
+func (j *Job) Password(password string) *Job {
+	j.password = password
+	return j
+}
+
+func (j *Job) Timeout(timeout time.Duration) *Job {
+	j.timeout = timeout
+	return j
+}
+
+func (j *Job) AutoImport() *Job {
+	j.autoimport = true
+
+	return j
+}
+
+func (j *Job) Run(f string) *Job {
+	if j.sourceDir == "" {
+		return j.error(errors.New("must provide source"))
+	}
+
+	start := time.Now()
+	workdir := filepath.Join(j.workspacedir, j.state.ID(), "src", j.module)
+
+	if !j.workspaceCreated {
+		if j.module == "" {
+			modfiledata, err := os.ReadFile(filepath.Join(j.sourceDir, "go.mod"))
+			if err != nil {
+				return j.error(err)
+			}
+
+			fff, err := modfile.Parse("go.mod", modfiledata, nil)
+			if err != nil {
+				return j.error(err)
+			}
+
+			// for _, r := range fff.Require {
+			// 	j.log.Info("  ", r.Mod.String())
+			// }
+
+			j.module = fff.Module.Mod.Path
+		}
+
+		j.log.Info("Creating workspace...")
+		err := os.MkdirAll(filepath.Join(j.workspacedir, j.state.ID(), "src"), 0770)
+		if err != nil {
+			return j.error(err)
+		}
+
+		workdir = filepath.Join(j.workspacedir, j.state.ID(), "src", j.module)
+
+		err = copy.Copy(j.sourceDir, workdir)
+		if err != nil {
+			return j.error(err)
+		}
+
+		j.workspaceCreated = true
+	}
+
+	if j.autocleanup {
+		defer func() {
+
+		}()
+	}
+
+	j.log.Info("Setting up new interpreter...")
+	workspace, err := filepath.Abs(filepath.Join(j.workspacedir, j.state.ID()))
+	if err != nil {
+		return j.error(err)
+	}
+
+	if j.autocleanup {
+		defer func() {
+			err := os.RemoveAll(workspace)
+			if err != nil {
+				j.error(err)
+			}
+		}()
+	}
+
+	j.i = interp.New(interp.Options{
+		GoPath:       workspace,
+		Unrestricted: true,
+		Stdin:        nil,
+		Stdout:       j.log.WriterLevel(logrus.InfoLevel),
+		Stderr:       j.log.WriterLevel(logrus.WarnLevel),
+	})
+
+	err = j.i.Use(stdlib.Symbols)
+	if err != nil {
+		return j.error(err)
+	}
+
+	err = j.i.Use(symbols.Symbols)
+	if err != nil {
+		return j.error(err)
+	}
+
+	err = j.i.Use(j.state.Symbols())
+	if err != nil {
+		return j.error(err)
+	}
+
+	if j.autoimport {
+		j.i.ImportUsed()
+	}
+
+	j.log.Info("Starting job...")
+	err = os.Chdir(workdir)
+	if err != nil {
+		return j.error(err)
+	}
+
+	j.files = append(j.files, f)
+
+	if strings.HasSuffix(f, ".go") {
+		data, err := os.ReadFile(f)
+		if err != nil {
+			return j.error(err)
+		}
+
+		ctx, _ := context.WithTimeout(context.Background(), j.timeout)
+		_, err = j.i.EvalWithContext(ctx, string(data))
+		if err != nil {
+			j.error(err)
+		}
+	} else {
+		ctx, _ := context.WithTimeout(context.Background(), j.timeout)
+		_, err := j.i.EvalPathWithContext(ctx, filepath.Join(j.module, f))
+		if err != nil {
+			j.error(err)
+		}
+	}
+
+	fmt.Println(j.module)
+	sss := j.i.Symbols("main")
+	for p, vv := range sss {
+		for k, v := range vv {
+			fmt.Println(p, k, v.Type(), v)
+		}
+	}
+
+	for k, v := range j.i.Globals() {
+		if v.CanInterface() {
+			if v.Kind() == reflect.Func {
+				continue
+			}
+			j.globals[k] = v.Interface()
+		}
+	}
+
+	j.log.Info("Job finished in: ", time.Now().Sub(start))
+
+	return j
+}
+
+func (j *Job) Globals() map[string]any {
+	return j.globals
+}
+
+func (j *Job) State() *state.State {
+	return j.state
+}
+
+func (j *Job) ID() string {
+	return j.state.ID()
+}
+
+func (j *Job) Error() error {
+	if j.errors != nil {
+		err := []string{}
+
+		if len(j.errors) > 1 {
+			err = append(err, fmt.Sprintf("!!! %d: errors in stack !!!", len(j.errors)))
+		}
+
+		for i, e := range j.errors {
+			err = append(err, fmt.Sprintf("%d: %s", i, e.Error()))
+		}
+
+		return errors.New(strings.Join(err, "\n"))
+	}
+	return nil
+}
+
+type Report struct {
+	Module  string
+	ID      string
+	Source  string
+	Files   []string
+	Globals map[string]any
+	State   map[string]any
+	Errors  []string
+	Logs    []string
+}
+
+func (j *Job) Report() *Job {
+	report := Report{
+		ID:      j.ID(),
+		Source:  j.source,
+		Files:   j.files,
+		Module:  j.module,
+		Globals: map[string]any{},
+		State:   map[string]any{},
+		Logs:    j.logs,
+	}
+
+	for k, v := range j.Globals() {
+		report.Globals[k] = v
+	}
+
+	for _, v := range j.errors {
+		report.Errors = append(report.Errors, v.Error())
+	}
+
+	j.state.Range(func(key string, value any) bool {
+		report.State[key] = value
+		return true
+	})
+
+	out, err := yaml.Marshal(report)
+	if err != nil {
+		j.log.Error(err)
+	} else {
+		fmt.Print(string(out))
+	}
+
+	return j
+}

+ 7 - 6
pkg/symbols/symbols.go

@@ -17,12 +17,13 @@ var (
 			"LookPath":       reflect.ValueOf(exec.LookPath),
 		},
 		"git.bazzel.dev/bmallen/helios/tools/tools": {
-			"Docker":         reflect.ValueOf(tools.Docker),
-			"CloudFormation": reflect.ValueOf(tools.CloudFormation),
-			"Terraform":      reflect.ValueOf(tools.Terraform),
-			"Git":            reflect.ValueOf(tools.Git),
-			"AWS":            reflect.ValueOf(tools.AWS),
-			"SetLogger":      reflect.ValueOf(tools.SetLogger),
+			"Docker":    reflect.ValueOf(tools.Docker),
+			"Terraform": reflect.ValueOf(tools.Terraform),
+			"Git":       reflect.ValueOf(tools.Git),
+			"AWS":       reflect.ValueOf(tools.AWS),
+			"SSL":       reflect.ValueOf(tools.SSL),
+			"File":      reflect.ValueOf(tools.File),
+			"SetLogger": reflect.ValueOf(tools.SetLogger),
 		},
 		"github.com/google/uuid/uuid": {
 			"New":       reflect.ValueOf(uuid.New),

+ 139 - 0
pkg/workflow/workflow.go

@@ -0,0 +1,139 @@
+package workflow
+
+import (
+	"time"
+
+	"git.bazzel.dev/bmallen/helios/pkg/state"
+	"github.com/sirupsen/logrus"
+)
+
+type (
+	Workflow struct {
+		name  string
+		jobs  jobs
+		c     Context
+		start time.Time
+		end   time.Time
+		log   *logrus.Logger
+	}
+	job struct {
+		name     string
+		steps    steps
+		needs    []string
+		workflow *Workflow
+		start    time.Time
+		end      time.Time
+	}
+	jobs      []*job
+	jobOption interface {
+		Job(j *job)
+	}
+	step struct {
+		name     string
+		f        StepFunc
+		start    time.Time
+		end      time.Time
+		workflow *Workflow
+		job      *job
+	}
+	steps []*step
+	needs struct {
+		name string
+	}
+
+	Context interface {
+		Get(key string) any
+		Set(key string, value any)
+		Delete(key string)
+		Keys() []string
+		Range(f func(key string, value any) bool)
+		ID() string
+	}
+	StepFunc func(ctx Context) error
+)
+
+func NewWorkflow(name string) *Workflow {
+	return &Workflow{
+		name: name,
+		jobs: jobs{},
+		c:    state.New(),
+		log:  logrus.New(),
+	}
+}
+
+func (w *Workflow) Run() (err error) {
+	w.start = time.Now()
+	defer func() {
+		w.end = time.Now()
+	}()
+
+	for _, j := range w.jobs {
+		j.start = time.Now()
+		w.log.Printf("# Job: %s\n", j.name)
+		for _, step := range j.steps {
+			w.log.Printf("- Step: %s / %s\n", j.name, step.name)
+			err := step.Run()
+			w.log.Printf("  Duration: %s\n", step.Duration())
+			if err != nil {
+				break
+			}
+		}
+		j.end = time.Now()
+		if err != nil {
+			break
+		}
+	}
+
+	return err
+}
+
+func (w *Workflow) Job(name string, opts ...jobOption) *Workflow {
+	job := &job{
+		name:     name,
+		steps:    steps{},
+		needs:    []string{},
+		workflow: w,
+	}
+
+	for _, opt := range opts {
+		opt.Job(job)
+	}
+
+	w.jobs = append(w.jobs, job)
+	return w
+}
+
+func Step(name string, f StepFunc) jobOption {
+	return &step{
+		name: name,
+		f:    f,
+	}
+}
+
+func (s *step) Job(j *job) {
+	s.job = j
+	s.workflow = j.workflow
+	j.steps = append(j.steps, s)
+}
+
+func (s *step) Run() error {
+	s.start = time.Now()
+	err := s.f(s.workflow.c)
+	s.end = time.Now()
+
+	return err
+}
+
+func (s *step) Duration() time.Duration {
+	return s.end.Sub(s.start)
+}
+
+func Needs(name string) jobOption {
+	return &needs{
+		name: name,
+	}
+}
+
+func (o *needs) Job(j *job) {
+	j.needs = append(j.needs, o.name)
+}

+ 4 - 0
test.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+go run main.go -s https://git.bazzel.dev/bmallen/helios-job-example.git| yq
+

+ 76 - 11
tools/aws.go

@@ -1,27 +1,92 @@
 package tools
 
-func AWS() *aws {
-	return &aws{}
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
+)
+
+func AWS() *awstool {
+	ctx := context.TODO()
+
+	accessKeyID := os.Getenv("AWS_ACCESS_KEY_ID")
+	secretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
+	sessionToken := os.Getenv("AWS_SESSION_TOKEN")
+
+	staticProvider := credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, sessionToken)
+
+	cfg, err := config.LoadDefaultConfig(ctx,
+		config.WithCredentialsProvider(staticProvider),
+		config.WithRegion(os.Getenv("AWS_REGION")),
+	)
+	if err != nil {
+		log.Write([]byte(fmt.Sprintf("unable to load SDK config, %v", err)))
+	}
+
+	return &awstool{
+		cfg: cfg,
+	}
 }
 
 type (
-	aws struct{}
-	s3  struct {
-		aws *aws
+	awstool struct {
+		cfg aws.Config
+	}
+	s3 struct {
+		aws *awstool
 	}
 	ec2 struct {
-		aws *aws
+		aws *awstool
+	}
+	cloudformation struct {
+		aws *awstool
+	}
+	imagebuilder struct {
+		aws *awstool
+	}
+	ecs struct {
+		aws *awstool
+	}
+	eks struct {
+		aws *awstool
 	}
 )
 
-func (t *aws) S3() *s3   { return &s3{aws: t} }
-func (t *aws) EC2() *ec2 { return &ec2{aws: t} }
+func (t *awstool) S3() *s3                         { return &s3{aws: t} }
+func (t *awstool) EC2() *ec2                       { return &ec2{aws: t} }
+func (t *awstool) CloudFormation() *cloudformation { return &cloudformation{aws: t} }
+func (t *awstool) ImageBuilder() *imagebuilder     { return &imagebuilder{aws: t} }
+func (t *awstool) ECS() *ecs                       { return &ecs{aws: t} }
+func (t *awstool) EKS() *eks                       { return &eks{aws: t} }
 
-func (t *s3) Put(src, dst string) error  { return nil }
-func (t *s3) Get(src, dst string) error  { return nil }
-func (t *s3) List(src, dst string) error { return nil }
+func (t *awstool) Filter(filters map[string][]string) []types.Filter {
+	filter := []types.Filter{}
 
+	for k, v := range filters {
+		filter = append(filter, types.Filter{
+			Name:   aws.String(k),
+			Values: v,
+		})
+	}
+	return filter
+}
+
+func (t *s3) Put(src, dst string) error             { return nil }
+func (t *s3) Get(src, dst string) error             { return nil }
+func (t *s3) ListObjects(bucket, path string) error { return nil }
+func (t *s3) ListBuckets() error                    { return nil }
+
+func (t *ec2) FindLatestAMI() error    { return nil }
 func (t *ec2) Create() error    { return nil }
 func (t *ec2) Stop() error      { return nil }
 func (t *ec2) Start() error     { return nil }
 func (t *ec2) Terminate() error { return nil }
+
+func (t *cloudformation) CreateStack() {}
+func (t *cloudformation) UpdateStack() {}
+func (t *cloudformation) DeleteStack() {}

+ 0 - 13
tools/cloudformation.go

@@ -1,13 +0,0 @@
-package tools
-
-func CloudFormation() *cloudformation {
-	return &cloudformation{}
-}
-
-type (
-	cloudformation struct{}
-)
-
-func (t *cloudformation) CreateStack() {}
-func (t *cloudformation) UpdateStack() {}
-func (t *cloudformation) DeleteStack() {}

+ 6 - 1
tools/docker.go

@@ -156,7 +156,12 @@ func (t *dockerContainer) HostConfig() *container.HostConfig        { return t.h
 func (t *dockerContainer) NetworkConfig() *network.NetworkingConfig { return t.network }
 func (t *dockerContainer) Exec()                                    {}
 func (t *dockerContainer) Run(destOut io.Writer, destErr io.Writer, timeout time.Duration) (int64, error) {
-	err := t.Start()
+	err := t.Create()
+	if err != nil {
+		return -1, err
+	}
+
+	err = t.Start()
 	if err != nil {
 		return -1, err
 	}

+ 53 - 0
tools/file.go

@@ -0,0 +1,53 @@
+package tools
+
+import (
+	"encoding/json"
+	"os"
+
+	"gopkg.in/yaml.v3"
+)
+
+func File() *file {
+	return &file{}
+}
+
+type (
+	file struct {
+	}
+)
+
+func (t *file) WriteYaml(path string, obj any) error {
+	out, err := yaml.Marshal(obj)
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile(path, out, 0644)
+}
+
+func (t *file) WriteJSON(path string, obj any) error {
+	out, err := json.Marshal(obj)
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile(path, out, 0644)
+}
+
+func (t *file) ReadYaml(path string, obj any) error {
+	in, err := os.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	return yaml.Unmarshal(in, obj)
+}
+
+func (t *file) ReadJSON(path string, obj any) error {
+	in, err := os.ReadFile(path)
+	if err != nil {
+		return err
+	}
+
+	return json.Unmarshal(in, obj)
+}

+ 105 - 0
tools/ssl.go

@@ -0,0 +1,105 @@
+package tools
+
+import (
+	"bytes"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"fmt"
+	"math/big"
+	"os"
+	"time"
+)
+
+func SSL() *ssl {
+	return &ssl{}
+}
+
+type (
+	ssl struct {
+	}
+	sslcert struct {
+		privateKey *rsa.PrivateKey
+		Template   x509.Certificate
+	}
+)
+
+func (t *ssl) Cert(cn string) (res *sslcert) {
+	res = &sslcert{}
+	var err error
+	
+	res.privateKey, err = rsa.GenerateKey(rand.Reader, 4096)
+	if err != nil {
+		log.Write([]byte(fmt.Sprintf("failed to generate private key: %w", err)))
+		return
+	}
+
+	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+	if err != nil {
+		log.Write([]byte(fmt.Sprintf("failed to generate serial number: %w", err)))
+		return
+	}
+
+	res.Template = x509.Certificate{
+		SerialNumber: serialNumber,
+		Subject: pkix.Name{
+			// Organization: []string{org},
+			CommonName: cn, // Essential for browsers/clients to trust the cert
+		},
+		NotBefore: time.Now(),
+		NotAfter:  time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+		BasicConstraintsValid: true,
+		IsCA:                  true, // Mark as a CA to sign other certs if needed
+
+		DNSNames: []string{cn},
+	}
+
+	return
+}
+
+func (res *sslcert) WriteCSR(file string) error {
+	csrTemplate := x509.CertificateRequest{
+		Subject:  res.Template.Subject,
+		DNSNames: res.Template.DNSNames,
+	}
+
+	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, res.privateKey)
+	if err != nil {
+		return fmt.Errorf("Failed to create certificate request: %v", err)
+	}
+
+	csrPEM := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE REQUEST",
+		Bytes: csrBytes,
+	})
+
+	return os.WriteFile(file, csrPEM, 0644)
+}
+
+func (res *sslcert) WritePEMs(key, crt string) error {
+	certBytes, err := x509.CreateCertificate(rand.Reader, &res.Template, &res.Template, res.privateKey.PublicKey, res.privateKey)
+	if err != nil {
+		return fmt.Errorf("failed to create certificate: %w", err)
+	}
+
+	certPEM := new(bytes.Buffer)
+	pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
+
+	keyPEM := new(bytes.Buffer)
+	pem.Encode(keyPEM, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(res.privateKey)})
+
+	if err := os.WriteFile(crt, certPEM.Bytes(), 0644); err != nil {
+		return fmt.Errorf("failed to write cert.pem: %w", err)
+	}
+	if err := os.WriteFile(key, keyPEM.Bytes(), 0600); err != nil {
+		return fmt.Errorf("failed to write key.pem: %w", err)
+	}
+
+	return nil
+}

+ 8 - 2
tools/terraform.go

@@ -41,8 +41,12 @@ func (_ tfLogParser) Write(p []byte) (n int, err error) {
 	if err != nil {
 		return 0, err
 	}
-
-	msg := []byte(fmt.Sprintf("[%s] %s\n", logEntry.Level(), logEntry.Message()))
+	var msg []byte
+	if logEntry.Level() == "" {
+		msg = []byte(fmt.Sprintf("%s\n", logEntry.Message()))
+	} else {
+		msg = []byte(fmt.Sprintf("[%s] %s\n", logEntry.Level(), logEntry.Message()))
+	}
 	log.Write(msg)
 
 	return len(msg), nil
@@ -77,6 +81,8 @@ func (t *terraform) Init() error {
 	}
 
 	if install {
+		log.Write([]byte(fmt.Sprintf("Installing Terraform version: %s", t.version)))
+
 		installer := &releases.ExactVersion{
 			Product: product.Terraform,
 			Version: version.Must(version.NewVersion(t.version)),