Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
56 changes: 53 additions & 3 deletions pkg/capabilities/registry/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
89 changes: 89 additions & 0 deletions pkg/capabilities/registry/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading