diff --git a/id.go b/id.go new file mode 100644 index 00000000..d36c008b --- /dev/null +++ b/id.go @@ -0,0 +1,97 @@ +package runn + +import ( + "crypto/sha1" //#nosec G505 + "encoding/hex" + "errors" + "io" + "path/filepath" + "strings" + + "github.com/rs/xid" + "github.com/samber/lo" +) + +// generateIDsUsingPath generates IDs using path of runbooks. +// ref: https://github.com/k1LoW/runn/blob/main/docs/designs/id.md +func generateIDsUsingPath(ops []*operator) error { + if len(ops) == 0 { + return nil + } + type tmp struct { + o *operator + p string + rp []string + id string + } + var ss []*tmp + max := 0 + for _, o := range ops { + p, err := filepath.Abs(filepath.Clean(o.bookPath)) + if err != nil { + return err + } + rp := reversePath(p) + ss = append(ss, &tmp{ + o: o, + p: p, + rp: rp, + }) + if len(rp) >= max { + max = len(rp) + } + } + for i := 1; i <= max; i++ { + ids := []string{} + for _, s := range ss { + var ( + id string + err error + ) + if len(s.rp) < i { + id, err = generateID(strings.Join(s.rp, "/")) + if err != nil { + return err + } + } else { + id, err = generateID(strings.Join(s.rp[:i], "/")) + if err != nil { + return err + } + } + s.id = id + ids = append(ids, id) + } + if len(lo.Uniq(ids)) == len(ss) { + // Set ids + for _, s := range ss { + s.o.id = s.id + } + return nil + } + } + return errors.New("failed to generate ids") +} + +func generateID(p string) (string, error) { + if p == "" { + return generateRandomID() + } + h := sha1.New() //#nosec G401 + if _, err := io.WriteString(h, p); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +func generateRandomID() (string, error) { + h := sha1.New() //#nosec G401 + if _, err := io.WriteString(h, xid.New().String()); err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +func reversePath(p string) []string { + return lo.Reverse(strings.Split(filepath.ToSlash(p), "/")) +} diff --git a/id_test.go b/id_test.go index 709914f1..6f71d5b8 100644 --- a/id_test.go +++ b/id_test.go @@ -12,6 +12,50 @@ import ( "github.com/samber/lo" ) +func TestGenerateIDsUsingPath(t *testing.T) { + tests := []struct { + paths []string + seedReversePaths []string + }{ + { + []string{"a.yml", "b.yml", "c.yml"}, + []string{"a.yml", "b.yml", "c.yml"}, + }, + { + []string{"path/to/a.yml", "path/to/b.yml", "path/to/c.yml"}, + []string{"a.yml", "b.yml", "c.yml"}, + }, + { + []string{"path/to/bb/a.yml", "path/to/aa/a.yml"}, + []string{"a.yml/bb", "a.yml/aa"}, + }, + { + []string{"path/to/bb/a.yml", "../../path/to/aa/a.yml"}, + []string{"a.yml/bb", "a.yml/aa"}, + }, + } + for _, tt := range tests { + ops := []*operator{} + for _, p := range tt.paths { + ops = append(ops, &operator{ + bookPath: p, + }) + } + if err := generateIDsUsingPath(ops); err != nil { + t.Fatal(err) + } + for i, o := range ops { + want, err := generateID(tt.seedReversePaths[i]) + if err != nil { + t.Fatal(err) + } + if o.id != want { + t.Errorf("want %s, got %s", want, o.id) + } + } + } +} + func BenchmarkReversePath(b *testing.B) { for i := 0; i < b.N; i++ { p := "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" diff --git a/operator.go b/operator.go index 3032864c..dead3749 100644 --- a/operator.go +++ b/operator.go @@ -20,7 +20,6 @@ import ( "github.com/goccy/go-json" "github.com/k1LoW/concgroup" "github.com/k1LoW/stopw" - "github.com/rs/xid" "github.com/ryo-yamaoka/otchkiss" "go.uber.org/multierr" ) @@ -412,9 +411,12 @@ func New(opts ...Option) (*operator, error) { if err := bk.applyOptions(opts...); err != nil { return nil, err } - + id, err := generateID(bk.path) + if err != nil { + return nil, err + } o := &operator{ - id: generateRunbookID(), + id: id, httpRunners: map[string]*httpRunner{}, dbRunners: map[string]*dbRunner{}, grpcRunners: map[string]*grpcRunner{}, @@ -1144,6 +1146,7 @@ func Load(pathp string, opts ...Option) (*operators, error) { } skipPaths := []string{} om := map[string]*operator{} + opss := []*operator{} for _, b := range books { o, err := New(append([]Option{b}, opts...)...) if err != nil { @@ -1157,6 +1160,11 @@ func Load(pathp string, opts ...Option) (*operators, error) { } } om[o.bookPath] = o + opss = append(opss, o) + } + + if err := generateIDsUsingPath(opss); err != nil { + return nil, err } for p, o := range om { @@ -1356,6 +1364,9 @@ func copyOperators(ops []*operator, opts []Option) ([]*operator, error) { } c = append(c, oo) } + if err := generateIDsUsingPath(c); err != nil { + return nil, err + } return c, nil } @@ -1408,10 +1419,6 @@ func pop(s map[string]any) (string, any, bool) { return "", nil, false } -func generateRunbookID() string { - return xid.New().String() -} - func contains(s []string, e string) bool { for _, v := range s { if e == v {