From 899719f8062c23a0f1e2450c65f3ce04b5473a0a Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 18 Nov 2024 16:59:02 +0100 Subject: [PATCH] feat: mechanism to fingerprint arbitrary data (#392) Provides a facility to efficiently fingerprint the injector configuration. --------- Signed-off-by: Eliott Bouhana Co-authored-by: Eliott Bouhana --- internal/fingerprint/fingerprint.go | 108 ++++++++++++++++++ internal/fingerprint/fingerprint_test.go | 80 +++++++++++++ internal/fingerprint/types.go | 91 +++++++++++++++ internal/injector/aspect/advice/advice.go | 3 + internal/injector/aspect/advice/assign.go | 11 +- internal/injector/aspect/advice/block.go | 11 +- internal/injector/aspect/advice/call.go | 17 ++- internal/injector/aspect/advice/call_test.go | 8 +- .../injector/aspect/advice/code/template.go | 59 ++++------ internal/injector/aspect/advice/import.go | 5 + internal/injector/aspect/advice/inject.go | 15 ++- internal/injector/aspect/advice/struct.go | 5 + internal/injector/aspect/advice/wrap.go | 11 +- internal/injector/aspect/aspect.go | 11 ++ internal/injector/aspect/context/golang.go | 7 ++ internal/injector/aspect/join/all-of.go | 5 + .../injector/aspect/join/configuration.go | 5 + internal/injector/aspect/join/declaration.go | 9 ++ internal/injector/aspect/join/directive.go | 5 + internal/injector/aspect/join/expression.go | 5 + internal/injector/aspect/join/function.go | 73 ++++-------- internal/injector/aspect/join/join.go | 11 +- internal/injector/aspect/join/not.go | 5 + internal/injector/aspect/join/one-of.go | 5 + internal/injector/aspect/join/package.go | 9 ++ internal/injector/aspect/join/struct.go | 13 +++ 26 files changed, 473 insertions(+), 114 deletions(-) create mode 100644 internal/fingerprint/fingerprint.go create mode 100644 internal/fingerprint/fingerprint_test.go create mode 100644 internal/fingerprint/types.go diff --git a/internal/fingerprint/fingerprint.go b/internal/fingerprint/fingerprint.go new file mode 100644 index 00000000..8389f8c9 --- /dev/null +++ b/internal/fingerprint/fingerprint.go @@ -0,0 +1,108 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +package fingerprint + +import ( + "crypto/sha512" + "encoding/base64" + "hash" + "io" + "strconv" + "sync" +) + +type Hasher struct { + hash hash.Hash +} + +type Hashable interface { + Hash(h *Hasher) error +} + +var pool = sync.Pool{New: func() any { return &Hasher{hash: sha512.New()} }} + +// New returns a [Hasher] from the pool, ready to use. +func New() *Hasher { + h, _ := pool.Get().(*Hasher) + return h +} + +// Close returns this [Hasher] to the pool. +func (h *Hasher) Close() { + h.hash.Reset() + pool.Put(h) +} + +// Finish obtains this [Hasher]'s current fingerprint. It does not change the +// underlying state of the [Hasher]. +func (h *Hasher) Finish() string { + var buf [sha512.Size]byte + return base64.URLEncoding.EncodeToString(h.hash.Sum(buf[:0])) +} + +// Named hashes a named list of values. This creates explicit grouping of the +// values, avoiding that the concatenation of two things has a different hash +// than those same two things one after the other. +func (h *Hasher) Named(name string, vals ...Hashable) error { + var ( + soh = []byte{0x01} // Start of key-value-pair beacon + sot = []byte{0x02} // Start of value & end of key beacon + etx = []byte{0x03} // End of key-value-pair beacon + ) + + if _, err := h.hash.Write(soh); err != nil { + return err + } + + if _, err := io.WriteString(h.hash, name); err != nil { + return err + } + + if _, err := h.hash.Write(sot); err != nil { + return err + } + + for idx, val := range vals { + if _, err := h.hash.Write(soh); err != nil { + return err + } + if _, err := io.WriteString(h.hash, strconv.Itoa(idx)); err != nil { + return err + } + if _, err := h.hash.Write(sot); err != nil { + return err + } + if err := val.Hash(h); err != nil { + return err + } + if _, err := h.hash.Write(etx); err != nil { + return err + } + } + + if _, err := h.hash.Write(etx); err != nil { + return err + } + + _, err := io.WriteString(h.hash, name) + return err +} + +// Fingerprint is a short-hand for creating a new [Hasher], calling +// [Hashable.Hash] on the provided value (unless it is nil), and then returning +// the [Hasher.Finish] result. +func Fingerprint(val Hashable) (string, error) { + h := New() + defer h.Close() + + if val != nil { + if err := val.Hash(h); err != nil { + return "", err + } + } + + return h.Finish(), nil +} diff --git a/internal/fingerprint/fingerprint_test.go b/internal/fingerprint/fingerprint_test.go new file mode 100644 index 00000000..d506478a --- /dev/null +++ b/internal/fingerprint/fingerprint_test.go @@ -0,0 +1,80 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +package fingerprint_test + +import ( + "fmt" + "testing" + + "github.com/DataDog/orchestrion/internal/fingerprint" + "github.com/stretchr/testify/require" +) + +func TestCast(t *testing.T) { + type mySlice []int + require.Equal( + t, + fingerprint.List[fingerprint.Int]{0, -1, -2}, + fingerprint.Cast(mySlice{0, 1, 2}, func(i int) fingerprint.Int { return fingerprint.Int(-i) }), + ) +} + +func TestFingerprint(t *testing.T) { + cases := map[string]struct { + hashable fingerprint.Hashable + hash string + }{ + "nil": { + hashable: nil, + hash: "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg_SpIdNs6c5H0NE8XYXysP-DGNKHfuwvY7kxvUdBeoGlODJ6-SfaPg==", + }, + "bool.true": { + hashable: fingerprint.Bool(true), + hash: "kSDNX67wegjpcf8CSj_L6h46a0QUKm2CyijGxC5PhSWVvPU9gdd28QVBBFq9t8N5UGKUFdDcZsjYbGSlYG0y3g==", + }, + "bool.false": { + hashable: fingerprint.Bool(false), + hash: "cZ-mfu9JxLKiuD8MYr3diMEGqq234hrgV8iAK3AONvgf4_FEgS2LBdZtxmPZCLJWReFTJiz21FeqNOaEr54yjQ==", + }, + "int": { + hashable: fingerprint.Int(0), + hash: "MbygIJTreBJqUXsgaojHPPqexvcExwMNGCEsrOgg8CXwC_DqaNvz86VDbKY7U797-ArY1d59g1nQt_7Z28OrmQ==", + }, + "string.empty": { + hashable: fingerprint.String(""), + hash: "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg_SpIdNs6c5H0NE8XYXysP-DGNKHfuwvY7kxvUdBeoGlODJ6-SfaPg==", + }, + "string.test": { + hashable: fingerprint.String("test"), + hash: "7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc_iBml1JQODbJ6wYX4oOHV-E-IvIh_1nsUNzLDBMxfqa2Ob1f1ACio_w==", + }, + "list.empty": { + hashable: fingerprint.List[fingerprint.Hashable]{}, + hash: "694_EPPzcQS8IJWmv4sxWZrSlHzcKAJoolH265TUzk5OE1HPYVrIkRPsOHTn3ZIgYz8wuIqBln5yfhTio_MFqg==", + }, + "list.items": { + hashable: fingerprint.List[fingerprint.Hashable]{fingerprint.Bool(true), fingerprint.Int(0), fingerprint.String("test")}, + hash: "RhBvcI5pNAl8TwC67UQypsmZTcj-Hbc9Zh2rAkTklU_rb4G8cORxp2dJHc1cPXq218SkkCCqPM4lU0te3a4Ufg==", + }, + "map": { + hashable: fingerprint.Map( + map[int]bool{1: true, 2: false}, + func(k int, v bool) (string, fingerprint.Bool) { + return fmt.Sprintf("key-%d", k), fingerprint.Bool(v) + }, + ), + hash: "X27oOwHUqLYTjDj82abW23Q5n1zyH2LnrHtzFE0vQfMUZcq5u-rUOuMKvrNxWd8GnvcJVLHc-lc8OJZhw07nFA==", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + hash, err := fingerprint.Fingerprint(tc.hashable) + require.NoError(t, err) + require.Equal(t, tc.hash, hash) + }) + } +} diff --git a/internal/fingerprint/types.go b/internal/fingerprint/types.go new file mode 100644 index 00000000..f1bc8b04 --- /dev/null +++ b/internal/fingerprint/types.go @@ -0,0 +1,91 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +package fingerprint + +import ( + "io" + "slices" + "strconv" + "strings" +) + +type Bool bool + +func (b Bool) Hash(h *Hasher) error { + _, err := io.WriteString(h.hash, strconv.FormatBool(bool(b))) + return err +} + +type Int int + +func (i Int) Hash(h *Hasher) error { + _, err := io.WriteString(h.hash, strconv.Itoa(int(i))) + return err +} + +type List[T Hashable] []T + +func (l List[T]) Hash(h *Hasher) error { + list := make([]Hashable, len(l)+1) + list[0] = Int(len(l)) + for idx, val := range l { + list[idx+1] = val + } + + return h.Named("list", list...) +} + +type String string + +func (s String) Hash(h *Hasher) error { + _, err := io.WriteString(h.hash, string(s)) + return err +} + +func Cast[E any, T ~[]E, H Hashable](slice T, fn func(E) H) List[H] { + res := make(List[H], len(slice)) + for idx, val := range slice { + res[idx] = fn(val) + } + return res +} + +type ( + mapped[T Hashable] []mappedItem[T] + mappedItem[T Hashable] struct { + key string + val T + } +) + +func Map[K comparable, V any, H Hashable](m map[K]V, fn func(K, V) (string, H)) mapped[H] { + res := make(mapped[H], 0, len(m)) + + for key, val := range m { + mkey, mval := fn(key, val) + res = append(res, mappedItem[H]{mkey, mval}) + } + + slices.SortFunc(res, func(l mappedItem[H], r mappedItem[H]) int { + return strings.Compare(l.key, r.key) + }) + + return res +} + +func (m mapped[T]) Hash(h *Hasher) error { + list := make([]Hashable, len(m)+1) + list[0] = Int(len(m)) + for idx, item := range m { + list[idx+1] = item + } + + return h.Named("map", list...) +} + +func (m mappedItem[T]) Hash(h *Hasher) error { + return h.Named(m.key, m.val) +} diff --git a/internal/injector/aspect/advice/advice.go b/internal/injector/aspect/advice/advice.go index 8682cb05..dc7ed11e 100644 --- a/internal/injector/aspect/advice/advice.go +++ b/internal/injector/aspect/advice/advice.go @@ -8,6 +8,7 @@ package advice import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" ) @@ -29,4 +30,6 @@ type Advice interface { // short-circuit and not do anything; e.g. import injection may be skipped if // the import already exists). Apply(context.AdviceContext) (bool, error) + + fingerprint.Hashable } diff --git a/internal/injector/aspect/advice/assign.go b/internal/injector/aspect/advice/assign.go index 6f62e20b..6e431b64 100644 --- a/internal/injector/aspect/advice/assign.go +++ b/internal/injector/aspect/advice/assign.go @@ -8,6 +8,7 @@ package advice import ( "fmt" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice/code" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" @@ -16,10 +17,10 @@ import ( ) type assignValue struct { - Template code.Template + Template *code.Template } -func AssignValue(template code.Template) *assignValue { +func AssignValue(template *code.Template) *assignValue { return &assignValue{template} } @@ -52,9 +53,13 @@ func (a *assignValue) AsCode() jen.Code { return jen.Qual(pkgPath, "AssignValue").Call(a.Template.AsCode()) } +func (a *assignValue) Hash(h *fingerprint.Hasher) error { + return h.Named("assign-value", a.Template) +} + func init() { unmarshalers["assign-value"] = func(node *yaml.Node) (Advice, error) { - var template code.Template + var template *code.Template if err := node.Decode(&template); err != nil { return nil, err } diff --git a/internal/injector/aspect/advice/block.go b/internal/injector/aspect/advice/block.go index 5e166c70..e12ca802 100644 --- a/internal/injector/aspect/advice/block.go +++ b/internal/injector/aspect/advice/block.go @@ -8,6 +8,7 @@ package advice import ( "fmt" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice/code" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" @@ -16,13 +17,13 @@ import ( ) type prependStatements struct { - Template code.Template + Template *code.Template } // PrependStmts prepends statements to the matched *dst.BlockStmt. This action // can only be used if the selector matches on a *dst.BlockStmt. The prepended // statements are wrapped in a new block statement to prevent scope leakage. -func PrependStmts(template code.Template) *prependStatements { +func PrependStmts(template *code.Template) *prependStatements { return &prependStatements{Template: template} } @@ -51,13 +52,17 @@ func (a *prependStatements) AsCode() jen.Code { return jen.Qual(pkgPath, "PrependStmts").Call(a.Template.AsCode()) } +func (a *prependStatements) Hash(h *fingerprint.Hasher) error { + return h.Named("prepend-statements", a.Template) +} + func (a *prependStatements) AddedImports() []string { return a.Template.AddedImports() } func init() { unmarshalers["prepend-statements"] = func(node *yaml.Node) (Advice, error) { - var template code.Template + var template *code.Template if err := node.Decode(&template); err != nil { return nil, err } diff --git a/internal/injector/aspect/advice/call.go b/internal/injector/aspect/advice/call.go index f1830489..8bf0fff5 100644 --- a/internal/injector/aspect/advice/call.go +++ b/internal/injector/aspect/advice/call.go @@ -10,6 +10,7 @@ import ( "regexp" "strings" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice/code" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/DataDog/orchestrion/internal/injector/aspect/join" @@ -20,12 +21,12 @@ import ( type appendArgs struct { TypeName join.TypeName - Templates []code.Template + Templates []*code.Template } // AppendArgs appends arguments of a given type to the end of a function call. All arguments must be // of the same type, as they may be appended at the tail end of a variadic call. -func AppendArgs(typeName join.TypeName, templates ...code.Template) *appendArgs { +func AppendArgs(typeName join.TypeName, templates ...*code.Template) *appendArgs { return &appendArgs{typeName, templates} } @@ -118,6 +119,10 @@ func (a *appendArgs) AddedImports() []string { return imports } +func (a *appendArgs) Hash(h *fingerprint.Hasher) error { + return h.Named("append-args", a.TypeName, fingerprint.List[*code.Template](a.Templates)) +} + type redirectCall struct { ImportPath string Name string @@ -154,6 +159,10 @@ func (r *redirectCall) AsCode() jen.Code { return jen.Qual(pkgPath, "ReplaceFunction").Call(jen.Lit(r.ImportPath), jen.Lit(r.Name)) } +func (r *redirectCall) Hash(h *fingerprint.Hasher) error { + return h.Named("replace-function", fingerprint.String(r.ImportPath), fingerprint.String(r.Name)) +} + func (r *redirectCall) AddedImports() []string { if r.ImportPath != "" { return []string{r.ImportPath} @@ -164,8 +173,8 @@ func (r *redirectCall) AddedImports() []string { func init() { unmarshalers["append-args"] = func(node *yaml.Node) (Advice, error) { var args struct { - TypeName string `yaml:"type"` - Values []code.Template `yaml:"values"` + TypeName string `yaml:"type"` + Values []*code.Template `yaml:"values"` } if err := node.Decode(&args); err != nil { diff --git a/internal/injector/aspect/advice/call_test.go b/internal/injector/aspect/advice/call_test.go index b54b7ba2..40f426f3 100644 --- a/internal/injector/aspect/advice/call_test.go +++ b/internal/injector/aspect/advice/call_test.go @@ -20,23 +20,23 @@ func TestAppendArgs(t *testing.T) { t.Run("AddedImports", func(t *testing.T) { type testCase struct { argType join.TypeName - args []code.Template + args []*code.Template expectedImports []string } testCases := map[string]testCase{ "imports-none": { argType: join.MustTypeName("any"), - args: []code.Template{code.MustTemplate("true", nil, context.GoLangVersion{})}, + args: []*code.Template{code.MustTemplate("true", nil, context.GoLangVersion{})}, }, "imports-from-arg-type": { argType: join.MustTypeName("*net/http.Request"), - args: []code.Template{code.MustTemplate("true", nil, context.GoLangVersion{})}, + args: []*code.Template{code.MustTemplate("true", nil, context.GoLangVersion{})}, expectedImports: []string{"net/http"}, }, "imports-from-templates": { argType: join.MustTypeName("any"), - args: []code.Template{ + args: []*code.Template{ code.MustTemplate("imp.Value", map[string]string{"imp": "github.com/namespace/foo"}, context.GoLangVersion{}), code.MustTemplate("imp.Value", map[string]string{"imp": "github.com/namespace/bar"}, context.GoLangVersion{}), }, diff --git a/internal/injector/aspect/advice/code/template.go b/internal/injector/aspect/advice/code/template.go index 3254cc7b..226652b3 100644 --- a/internal/injector/aspect/advice/code/template.go +++ b/internal/injector/aspect/advice/code/template.go @@ -10,12 +10,12 @@ import ( "errors" "fmt" "go/token" - "regexp" "sort" "strconv" "strings" "text/template" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/DataDog/orchestrion/internal/version" "github.com/dave/dst" @@ -52,14 +52,14 @@ package _ // imports map. The imports map associates names to import paths. The produced // AST nodes will feature qualified *dst.Ident nodes in all places where a // property of mapped names is selected. -func NewTemplate(text string, imports map[string]string, lang context.GoLangVersion) (Template, error) { +func NewTemplate(text string, imports map[string]string, lang context.GoLangVersion) (*Template, error) { template := template.Must(wrapper.Clone()) template, err := template.Parse(text) - return Template{template, imports, text, lang}, err + return &Template{template, imports, text, lang}, err } // MustTemplate is the same as NewTemplate, but panics if an error occurs. -func MustTemplate(text string, imports map[string]string, lang context.GoLangVersion) (template Template) { +func MustTemplate(text string, imports map[string]string, lang context.GoLangVersion) (template *Template) { var err error if template, err = NewTemplate(text, imports, lang); err != nil { panic(err) @@ -221,6 +221,15 @@ func (t *Template) AsCode() jen.Code { ) } +func (t *Template) Hash(h *fingerprint.Hasher) error { + return h.Named( + "template", + fingerprint.Map(t.Imports, func(k string, v string) (string, fingerprint.String) { return k, fingerprint.String(v) }), + fingerprint.String(t.Source), + t.Lang, + ) +} + func (t *Template) AddedImports() []string { imports := make([]string, 0, len(t.Imports)) for _, path := range t.Imports { @@ -240,8 +249,13 @@ func (t *Template) UnmarshalYAML(node *yaml.Node) (err error) { return } - *t, err = NewTemplate(cfg.Template, cfg.Imports, cfg.Lang) - return err + newT, err := NewTemplate(cfg.Template, cfg.Imports, cfg.Lang) + if err != nil { + return err + } + + *t = *newT + return nil } func numberLines(text string) string { @@ -254,36 +268,3 @@ func numberLines(text string) string { return strings.Join(lines, "\n") } - -func (t *Template) RenderHTML() string { - var buf strings.Builder - - if len(t.Imports) > 0 { - keys := make([]string, 0, len(t.Imports)) - nameLen := 0 - for name := range t.Imports { - keys = append(keys, name) - if l := len(name); l > nameLen { - nameLen = l - } - } - sort.Strings(keys) - - _, _ = buf.WriteString("\n\n```go\n") - _, _ = buf.WriteString("// Using the following synthetic imports:\n") - _, _ = buf.WriteString("import (\n") - for _, name := range keys { - _, _ = fmt.Fprintf(&buf, "\t%-*s %q\n", nameLen, name, t.Imports[name]) - } - _, _ = buf.WriteString(")\n```") - } - - _, _ = buf.WriteString("\n\n```go-template\n") - // Render with tabs so it's more go-esque! - _, _ = buf.WriteString(regexp.MustCompile(`(?m)^(?: )+`).ReplaceAllStringFunc(t.Source, func(orig string) string { - return strings.Repeat("\t", len(orig)/2) - })) - _, _ = buf.WriteString("\n```\n") - - return buf.String() -} diff --git a/internal/injector/aspect/advice/import.go b/internal/injector/aspect/advice/import.go index 73c816eb..2afd41b6 100644 --- a/internal/injector/aspect/advice/import.go +++ b/internal/injector/aspect/advice/import.go @@ -8,6 +8,7 @@ package advice import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -32,6 +33,10 @@ func (a addBlankImport) AddedImports() []string { return []string{string(a)} } +func (a addBlankImport) Hash(h *fingerprint.Hasher) error { + return h.Named("add-blank-import", fingerprint.String(a)) +} + func init() { unmarshalers["add-blank-import"] = func(node *yaml.Node) (Advice, error) { var path string diff --git a/internal/injector/aspect/advice/inject.go b/internal/injector/aspect/advice/inject.go index 427f687f..5091b8f8 100644 --- a/internal/injector/aspect/advice/inject.go +++ b/internal/injector/aspect/advice/inject.go @@ -8,6 +8,7 @@ package advice import ( "sort" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice/code" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" @@ -15,13 +16,13 @@ import ( ) type injectDeclarations struct { - Template code.Template + Template *code.Template Links []string } // InjectDeclarations merges all declarations in the provided source file into the current file. The package name of both // original & injected files must match. -func InjectDeclarations(template code.Template, links []string) injectDeclarations { +func InjectDeclarations(template *code.Template, links []string) injectDeclarations { return injectDeclarations{template, links} } @@ -65,6 +66,14 @@ func (a injectDeclarations) AsCode() jen.Code { ) } +func (a injectDeclarations) Hash(h *fingerprint.Hasher) error { + return h.Named( + "inject-declarations", + fingerprint.Cast(a.Links, func(s string) fingerprint.String { return fingerprint.String(s) }), + a.Template, + ) +} + func (a injectDeclarations) AddedImports() []string { return append(a.Template.AddedImports(), a.Links...) } @@ -72,7 +81,7 @@ func (a injectDeclarations) AddedImports() []string { func init() { unmarshalers["inject-declarations"] = func(node *yaml.Node) (Advice, error) { var config struct { - Template code.Template `yaml:",inline"` + Template *code.Template `yaml:",inline"` Links []string } if err := node.Decode(&config); err != nil { diff --git a/internal/injector/aspect/advice/struct.go b/internal/injector/aspect/advice/struct.go index 56b2efae..af2d9fb7 100644 --- a/internal/injector/aspect/advice/struct.go +++ b/internal/injector/aspect/advice/struct.go @@ -8,6 +8,7 @@ package advice import ( "fmt" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/DataDog/orchestrion/internal/injector/aspect/join" "github.com/dave/dst" @@ -52,6 +53,10 @@ func (a *addStructField) AsCode() jen.Code { return jen.Qual(pkgPath, "AddStructField").Call(jen.Lit(a.Name), a.TypeName.AsCode()) } +func (a *addStructField) Hash(h *fingerprint.Hasher) error { + return h.Named("add-struct-field", fingerprint.String(a.Name), a.TypeName) +} + func (a *addStructField) AddedImports() []string { if path := a.TypeName.ImportPath(); path != "" { return []string{path} diff --git a/internal/injector/aspect/advice/wrap.go b/internal/injector/aspect/advice/wrap.go index e5397cc1..c6bd47c8 100644 --- a/internal/injector/aspect/advice/wrap.go +++ b/internal/injector/aspect/advice/wrap.go @@ -8,6 +8,7 @@ package advice import ( "fmt" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice/code" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" @@ -16,10 +17,10 @@ import ( ) type wrapExpression struct { - Template code.Template + Template *code.Template } -func WrapExpression(template code.Template) *wrapExpression { +func WrapExpression(template *code.Template) *wrapExpression { return &wrapExpression{Template: template} } @@ -56,13 +57,17 @@ func (a *wrapExpression) AsCode() jen.Code { return jen.Qual(pkgPath, "WrapExpression").Call(a.Template.AsCode()) } +func (a *wrapExpression) Hash(h *fingerprint.Hasher) error { + return h.Named("wrap-expression", a.Template) +} + func (a *wrapExpression) AddedImports() []string { return a.Template.AddedImports() } func init() { unmarshalers["wrap-expression"] = func(node *yaml.Node) (Advice, error) { - var template code.Template + var template *code.Template if err := node.Decode(&template); err != nil { return nil, err } diff --git a/internal/injector/aspect/aspect.go b/internal/injector/aspect/aspect.go index 4fe788bd..c1639c29 100644 --- a/internal/injector/aspect/aspect.go +++ b/internal/injector/aspect/aspect.go @@ -8,6 +8,7 @@ package aspect import ( "errors" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/advice" "github.com/DataDog/orchestrion/internal/injector/aspect/join" "github.com/dave/jennifer/jen" @@ -38,6 +39,16 @@ func (a *Aspect) AsCode() (jp jen.Code, adv jen.Code) { return } +func (a *Aspect) Hash(h *fingerprint.Hasher) error { + return h.Named( + "aspect", + fingerprint.String(a.ID), + fingerprint.Bool(a.TracerInternal), + a.JoinPoint, + fingerprint.List[advice.Advice](a.Advice), + ) +} + func (a *Aspect) AddedImports() (imports []string) { // "unsafe" is always implied, because it's special-cased in the go toolchain, and is not a "normal" module. implied := map[string]struct{}{"unsafe": {}} diff --git a/internal/injector/aspect/context/golang.go b/internal/injector/aspect/context/golang.go index 0e239c0d..7d72e938 100644 --- a/internal/injector/aspect/context/golang.go +++ b/internal/injector/aspect/context/golang.go @@ -9,6 +9,7 @@ import ( "fmt" "go/version" + "github.com/DataDog/orchestrion/internal/fingerprint" "gopkg.in/yaml.v3" ) @@ -53,6 +54,12 @@ func Compare(left GoLangVersion, right GoLangVersion) int { return version.Compare(version.Lang(left.label), version.Lang(right.label)) } +var _ fingerprint.Hashable = (*GoLangVersion)(nil) + +func (g GoLangVersion) Hash(h *fingerprint.Hasher) error { + return h.Named("GoLangVersion", fingerprint.String(g.label)) +} + var _ yaml.Unmarshaler = (*GoLangVersion)(nil) func (g *GoLangVersion) UnmarshalYAML(node *yaml.Node) error { diff --git a/internal/injector/aspect/join/all-of.go b/internal/injector/aspect/join/all-of.go index b5c5e205..dca9daf3 100644 --- a/internal/injector/aspect/join/all-of.go +++ b/internal/injector/aspect/join/all-of.go @@ -6,6 +6,7 @@ package join import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -49,6 +50,10 @@ func (o allOf) AsCode() jen.Code { }) } +func (o allOf) Hash(h *fingerprint.Hasher) error { + return h.Named("all-of", fingerprint.List[Point](o)) +} + func init() { unmarshalers["all-of"] = func(node *yaml.Node) (Point, error) { var nodes []yaml.Node diff --git a/internal/injector/aspect/join/configuration.go b/internal/injector/aspect/join/configuration.go index 59dd94e3..a81bc11b 100644 --- a/internal/injector/aspect/join/configuration.go +++ b/internal/injector/aspect/join/configuration.go @@ -8,6 +8,7 @@ package join import ( "sort" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -51,6 +52,10 @@ func (jp configuration) AsCode() jen.Code { })) } +func (jp configuration) Hash(h *fingerprint.Hasher) error { + return h.Named("configuration", fingerprint.Map(jp, func(k string, v string) (string, fingerprint.String) { return k, fingerprint.String(v) })) +} + func init() { unmarshalers["configuration"] = func(node *yaml.Node) (Point, error) { var c configuration diff --git a/internal/injector/aspect/join/declaration.go b/internal/injector/aspect/join/declaration.go index d8e38cc9..6e374277 100644 --- a/internal/injector/aspect/join/declaration.go +++ b/internal/injector/aspect/join/declaration.go @@ -9,6 +9,7 @@ import ( "fmt" "regexp" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -60,6 +61,10 @@ func (i *declarationOf) AsCode() jen.Code { return jen.Qual(pkgPath, "DeclarationOf").Call(jen.Lit(i.ImportPath), jen.Lit(i.Name)) } +func (i *declarationOf) Hash(h *fingerprint.Hasher) error { + return h.Named("declaration-of", fingerprint.String(i.ImportPath), fingerprint.String(i.Name)) +} + type valueDeclaration struct { TypeName TypeName } @@ -97,6 +102,10 @@ func (i *valueDeclaration) AsCode() jen.Code { return jen.Qual(pkgPath, "ValueDeclaration").Call(i.TypeName.AsCode()) } +func (i *valueDeclaration) Hash(h *fingerprint.Hasher) error { + return h.Named("value-declaration", i.TypeName) +} + // See: https://regex101.com/r/OXDfJ1/1 var symbolNamePattern = regexp.MustCompile(`\A(.+)\.([\p{L}_][\p{L}_\p{Nd}]*)\z`) diff --git a/internal/injector/aspect/join/directive.go b/internal/injector/aspect/join/directive.go index 3abd3563..4c235905 100644 --- a/internal/injector/aspect/join/directive.go +++ b/internal/injector/aspect/join/directive.go @@ -10,6 +10,7 @@ import ( "unicode" "unicode/utf8" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -79,6 +80,10 @@ func (d directive) AsCode() jen.Code { return jen.Qual(pkgPath, "Directive").Call(jen.Lit(string(d))) } +func (d directive) Hash(h *fingerprint.Hasher) error { + return h.Named("directive", fingerprint.String(d)) +} + func init() { unmarshalers["directive"] = func(node *yaml.Node) (Point, error) { var name string diff --git a/internal/injector/aspect/join/expression.go b/internal/injector/aspect/join/expression.go index ff89ee7b..ab5db0b8 100644 --- a/internal/injector/aspect/join/expression.go +++ b/internal/injector/aspect/join/expression.go @@ -9,6 +9,7 @@ import ( "fmt" "regexp" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -57,6 +58,10 @@ func (i *functionCall) AsCode() jen.Code { return jen.Qual(pkgPath, "FunctionCall").Call(jen.Lit(i.ImportPath), jen.Lit(i.Name)) } +func (i *functionCall) Hash(h *fingerprint.Hasher) error { + return h.Named("function-call", fingerprint.String(i.ImportPath), fingerprint.String(i.Name)) +} + // See: https://regex101.com/r/fjLo1l/1 var funcNamePattern = regexp.MustCompile(`\A(?:(.+)\.)?([\p{L}_][\p{L}_\p{Nd}]*)\z`) diff --git a/internal/injector/aspect/join/function.go b/internal/injector/aspect/join/function.go index f67e37d1..45c58fcd 100644 --- a/internal/injector/aspect/join/function.go +++ b/internal/injector/aspect/join/function.go @@ -9,6 +9,7 @@ import ( "fmt" "strings" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -26,6 +27,7 @@ type ( FunctionOption interface { // asCode produces a jen.Code representation of the receiver. asCode() jen.Code + fingerprint.Hashable impliesImported() []string evaluate(functionInformation) bool @@ -84,6 +86,10 @@ func (s *functionDeclaration) AsCode() jen.Code { }) } +func (s *functionDeclaration) Hash(h *fingerprint.Hasher) error { + return h.Named("function", fingerprint.List[FunctionOption](s.Options)) +} + type functionName string func Name(name string) FunctionOption { @@ -102,6 +108,10 @@ func (fo functionName) asCode() jen.Code { return jen.Qual(pkgPath, "Name").Call(jen.Lit(string(fo))) } +func (fo functionName) Hash(h *fingerprint.Hasher) error { + return h.Named("name", fingerprint.String(fo)) +} + type signature struct { Arguments []TypeName Results []TypeName @@ -183,55 +193,12 @@ func (fo *signature) asCode() jen.Code { }) } -type oneOfFunctions []FunctionOption - -func OneOfFunctions(opts ...FunctionOption) oneOfFunctions { - return oneOfFunctions(opts) -} - -func (fo oneOfFunctions) impliesImported() (list []string) { - // We can only assume a package is imported if all candidates imply it. - counts := make(map[string]uint) - for _, opt := range fo { - for _, path := range opt.impliesImported() { - counts[path]++ - } - } - - total := uint(len(fo)) - list = make([]string, 0, len(counts)) - for path, count := range counts { - if count == total { - list = append(list, path) - } - } - return -} - -func (fo oneOfFunctions) evaluate(info functionInformation) bool { - for _, opt := range fo { - if opt.evaluate(info) { - return true - } - } - return false -} - -func (fo oneOfFunctions) AsCode() jen.Code { - if len(fo) == 1 { - return (fo)[0].asCode() - } - - return jen.Qual(pkgPath, "OneOfFunctions").CallFunc(func(g *jen.Group) { - for _, opt := range fo { - g.Line().Add(opt.asCode()) - } - g.Line().Empty() - }) -} - -func (oneOfFunctions) toHTML() string { - return "one-of" +func (fo *signature) Hash(h *fingerprint.Hasher) error { + return h.Named( + "signature", + fingerprint.List[TypeName](fo.Arguments), + fingerprint.List[TypeName](fo.Results), + ) } type receiver struct { @@ -254,6 +221,10 @@ func (fo *receiver) asCode() jen.Code { return jen.Qual(pkgPath, "Receiver").Call(fo.TypeName.AsCode()) } +func (fo *receiver) Hash(h *fingerprint.Hasher) error { + return h.Named("receiver", fo.TypeName) +} + type functionBody struct { Function Point } @@ -294,6 +265,10 @@ func (s *functionBody) AsCode() jen.Code { return jen.Qual(pkgPath, "FunctionBody").Call(s.Function.AsCode()) } +func (s *functionBody) Hash(h *fingerprint.Hasher) error { + return h.Named("function-body", s.Function) +} + func init() { unmarshalers["function-body"] = func(node *yaml.Node) (Point, error) { up, err := FromYAML(node) diff --git a/internal/injector/aspect/join/join.go b/internal/injector/aspect/join/join.go index c5791e96..53ceedc0 100644 --- a/internal/injector/aspect/join/join.go +++ b/internal/injector/aspect/join/join.go @@ -12,6 +12,7 @@ import ( "fmt" "regexp" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -33,6 +34,8 @@ type Point interface { // node or not. The node's ancestry is also provided to allow Point to make // decisions based on parent nodes. Matches(ctx context.AspectContext) bool + + fingerprint.Hashable } type TypeName struct { @@ -150,10 +153,6 @@ func (n TypeName) AsCode() jen.Code { return jen.Qual(pkgPath, "MustTypeName").Call(jen.Lit(str.String())) } -func (n TypeName) RenderHTML() string { - var ptr string - if n.pointer { - ptr = "*" - } - return fmt.Sprintf(`{{}}`, n.path, n.name, ptr) +func (n TypeName) Hash(h *fingerprint.Hasher) error { + return h.Named("type-name", fingerprint.String(n.name), fingerprint.String(n.path), fingerprint.Bool(n.pointer)) } diff --git a/internal/injector/aspect/join/not.go b/internal/injector/aspect/join/not.go index 2b79132c..bf6299d4 100644 --- a/internal/injector/aspect/join/not.go +++ b/internal/injector/aspect/join/not.go @@ -6,6 +6,7 @@ package join import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -31,6 +32,10 @@ func (n not) AsCode() jen.Code { return jen.Qual(pkgPath, "Not").Call(n.JoinPoint.AsCode()) } +func (n not) Hash(h *fingerprint.Hasher) error { + return h.Named("not", n.JoinPoint) +} + func init() { unmarshalers["not"] = func(node *yaml.Node) (Point, error) { jp, err := FromYAML(node) diff --git a/internal/injector/aspect/join/one-of.go b/internal/injector/aspect/join/one-of.go index 5698068f..ca1513ce 100644 --- a/internal/injector/aspect/join/one-of.go +++ b/internal/injector/aspect/join/one-of.go @@ -6,6 +6,7 @@ package join import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -60,6 +61,10 @@ func (o oneOf) AsCode() jen.Code { }) } +func (o oneOf) Hash(h *fingerprint.Hasher) error { + return h.Named("one-of", fingerprint.List[Point](o)) +} + func init() { unmarshalers["one-of"] = func(node *yaml.Node) (Point, error) { var nodes []yaml.Node diff --git a/internal/injector/aspect/join/package.go b/internal/injector/aspect/join/package.go index 83f8ad1b..00d3cfc0 100644 --- a/internal/injector/aspect/join/package.go +++ b/internal/injector/aspect/join/package.go @@ -6,6 +6,7 @@ package join import ( + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/jennifer/jen" "gopkg.in/yaml.v3" @@ -29,6 +30,10 @@ func (p importPath) AsCode() jen.Code { return jen.Qual(pkgPath, "ImportPath").Call(jen.Lit(string(p))) } +func (p importPath) Hash(h *fingerprint.Hasher) error { + return h.Named("import-path", fingerprint.String(p)) +} + type packageName string func PackageName(name string) packageName { @@ -47,6 +52,10 @@ func (p packageName) AsCode() jen.Code { return jen.Qual(pkgPath, "PackageName").Call(jen.Lit(string(p))) } +func (p packageName) Hash(h *fingerprint.Hasher) error { + return h.Named("import-path", fingerprint.String(p)) +} + func init() { unmarshalers["import-path"] = func(node *yaml.Node) (Point, error) { var name string diff --git a/internal/injector/aspect/join/struct.go b/internal/injector/aspect/join/struct.go index a76913ac..dddf1530 100644 --- a/internal/injector/aspect/join/struct.go +++ b/internal/injector/aspect/join/struct.go @@ -9,6 +9,7 @@ import ( "fmt" "go/token" + "github.com/DataDog/orchestrion/internal/fingerprint" "github.com/DataDog/orchestrion/internal/injector/aspect/context" "github.com/dave/dst" "github.com/dave/jennifer/jen" @@ -55,6 +56,10 @@ func (s *structDefinition) AsCode() jen.Code { return jen.Qual(pkgPath, "StructDefinition").Call(s.TypeName.AsCode()) } +func (s *structDefinition) Hash(h *fingerprint.Hasher) error { + return h.Named("struct-definition", s.TypeName) +} + type ( StructLiteralMatch int structLiteral struct { @@ -158,6 +163,10 @@ func (s *structLiteral) AsCode() jen.Code { return jen.Qual(pkgPath, "StructLiteral").Call(s.TypeName.AsCode(), s.Match.asCode()) } +func (s *structLiteral) Hash(h *fingerprint.Hasher) error { + return h.Named("struct-literal", s.TypeName, fingerprint.String(s.Field), s.Match) +} + func init() { unmarshalers["struct-definition"] = func(node *yaml.Node) (Point, error) { var spec string @@ -250,3 +259,7 @@ func (s StructLiteralMatch) asCode() jen.Code { } return jen.Qual(pkgPath, constName) } + +func (s StructLiteralMatch) Hash(h *fingerprint.Hasher) error { + return h.Named("struct-literal-match", fingerprint.Int(s)) +}