diff --git a/go.mod b/go.mod index d050ca4c3..828e5ad6c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/smartcontractkit/chainlink-common go 1.24.5 require ( + github.com/Masterminds/semver/v3 v3.4.0 github.com/XSAM/otelsql v0.37.0 github.com/andybalholm/brotli v1.1.1 github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c @@ -76,7 +77,6 @@ require ( ) require ( - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apache/arrow-go/v18 v18.3.1 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect diff --git a/pkg/capabilities/registry/base.go b/pkg/capabilities/registry/base.go index 64c44f739..8599ec646 100644 --- a/pkg/capabilities/registry/base.go +++ b/pkg/capabilities/registry/base.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "strings" "sync" + "github.com/Masterminds/semver/v3" + "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types/core" @@ -36,11 +39,58 @@ func (r *baseRegistry) Get(_ context.Context, id string) (capabilities.BaseCapab r.mu.RLock() defer r.mu.RUnlock() c, ok := r.m[id] - if !ok { - return nil, fmt.Errorf("capability not found with id %s", id) + if ok { + return c, nil + } + + // Find compatible version (>= requested version with same major) + parts := strings.Split(id, "@") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid capability id format: %s", id) } + name, verStr := parts[0], parts[1] - return c, nil + reqVer, err := semver.NewVersion(verStr) + if err != nil { + return nil, fmt.Errorf("invalid version in capability id %q: %w", id, err) + } + reqIsPrerelease := reqVer.Prerelease() != "" + + var bestCap capabilities.BaseCapability + var bestVer *semver.Version + for key, cap := range r.m { + p := strings.Split(key, "@") + if len(p) != 2 { + continue + } + if p[0] != name { + continue + } + v, err := semver.NewVersion(p[1]) + if err != nil { + continue + } + if v.Major() != reqVer.Major() { + continue + } + // If the request is stable, skip pre-release candidates + if !reqIsPrerelease && v.Prerelease() != "" { + continue + } + + if v.GreaterThan(reqVer) { + if bestVer == nil || v.LessThan(bestVer) { + bestCap = cap + bestVer = v + } + } + } + + if bestCap != nil { + r.lggr.Debugw("found compatible capability", "id", name+"@"+bestVer.String()) + return bestCap, nil + } + return nil, fmt.Errorf("no compatible capability found for id %s", id) } // GetTrigger gets a capability from the registry and tries to coerce it to the TriggerCapability interface. diff --git a/pkg/capabilities/registry/base_test.go b/pkg/capabilities/registry/base_test.go index 6eee0bb82..4e32004dd 100644 --- a/pkg/capabilities/registry/base_test.go +++ b/pkg/capabilities/registry/base_test.go @@ -59,6 +59,95 @@ func TestRegistry(t *testing.T) { assert.Equal(t, c, cs[0]) } +func TestRegistryCompatibleVersions(t *testing.T) { + ctx := t.Context() + + t.Run("Compatible minor version", func(t *testing.T) { + r := registry.NewBaseRegistry(logger.Test(t)) + id := "capability-1@1.5.0" + ci, err := capabilities.NewCapabilityInfo( + id, + capabilities.CapabilityTypeAction, + "capability-1-description", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(ctx, c) + require.NoError(t, err) + _, err = r.Get(ctx, "capability-1@1.0.0") + require.NoError(t, err) + }) + + t.Run("Incompatible minor version", func(t *testing.T) { + r := registry.NewBaseRegistry(logger.Test(t)) + id := "capability-1@1.1.0" + ci, err := capabilities.NewCapabilityInfo( + id, + capabilities.CapabilityTypeAction, + "capability-1-description", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(ctx, c) + require.NoError(t, err) + _, err = r.Get(ctx, "capability-1@1.2.0") + require.Error(t, err) + }) + + t.Run("Incompatible major version", func(t *testing.T) { + r := registry.NewBaseRegistry(logger.Test(t)) + id := "capability-1@2.0.0" + ci, err := capabilities.NewCapabilityInfo( + id, + capabilities.CapabilityTypeAction, + "capability-1-description", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(ctx, c) + require.NoError(t, err) + _, err = r.Get(ctx, "capability-1@1.0.0") + require.Error(t, err) + }) + + t.Run("Don't match pre-release tags if requested version if not pre-release", func(t *testing.T) { + r := registry.NewBaseRegistry(logger.Test(t)) + id := "capability-1@1.5.0-alpha" + ci, err := capabilities.NewCapabilityInfo( + id, + capabilities.CapabilityTypeAction, + "capability-1-description", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(ctx, c) + require.NoError(t, err) + _, err = r.Get(ctx, "capability-1@1.0.0") + require.Error(t, err) + }) + + t.Run("Match pre-release tags if requested version is pre-release", func(t *testing.T) { + r := registry.NewBaseRegistry(logger.Test(t)) + id := "capability-1@1.5.0-alpha" + ci, err := capabilities.NewCapabilityInfo( + id, + capabilities.CapabilityTypeAction, + "capability-1-description", + ) + require.NoError(t, err) + + c := &mockCapability{CapabilityInfo: ci} + err = r.Add(ctx, c) + require.NoError(t, err) + _, err = r.Get(ctx, "capability-1@1.0.0-alpha") + require.NoError(t, err) + }) +} + func TestRegistry_NoDuplicateIDs(t *testing.T) { r := registry.NewBaseRegistry(logger.Test(t)) ctx := t.Context()