mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-25 19:56:46 +00:00
Compare commits
4 Commits
test/proxy
...
v0.66.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7b016be2 | ||
|
|
9e01ea7aae | ||
|
|
cfc7ec8bb9 | ||
|
|
b3bbc0e5c6 |
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Community Support
|
||||||
|
url: https://forum.netbird.io/
|
||||||
|
about: Community support forum
|
||||||
|
- name: Cloud Support
|
||||||
|
url: https://docs.netbird.io/help/report-bug-issues
|
||||||
|
about: Contact us for support
|
||||||
|
- name: Client/Connection Troubleshooting
|
||||||
|
url: https://docs.netbird.io/help/troubleshooting-client
|
||||||
|
about: See our client troubleshooting guide for help addressing common issues
|
||||||
|
- name: Self-host Troubleshooting
|
||||||
|
url: https://docs.netbird.io/selfhosted/troubleshooting
|
||||||
|
about: See our self-host troubleshooting guide for help addressing common issues
|
||||||
@@ -46,8 +46,10 @@ const (
|
|||||||
cmdSFTP = "<sftp>"
|
cmdSFTP = "<sftp>"
|
||||||
cmdNonInteractive = "<idle>"
|
cmdNonInteractive = "<idle>"
|
||||||
|
|
||||||
// DefaultJWTMaxTokenAge is the default maximum age for JWT tokens accepted by the SSH server
|
// DefaultJWTMaxTokenAge is the default maximum age for JWT tokens accepted by the SSH server.
|
||||||
DefaultJWTMaxTokenAge = 5 * 60
|
// Set to 10 minutes to accommodate identity providers like Azure Entra ID
|
||||||
|
// that backdate the iat claim by up to 5 minutes.
|
||||||
|
DefaultJWTMaxTokenAge = 10 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package manager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"math/rand/v2"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/shared/management/status"
|
"github.com/netbirdio/netbird/shared/management/status"
|
||||||
@@ -13,108 +13,20 @@ const (
|
|||||||
exposeTTL = 90 * time.Second
|
exposeTTL = 90 * time.Second
|
||||||
exposeReapInterval = 30 * time.Second
|
exposeReapInterval = 30 * time.Second
|
||||||
maxExposesPerPeer = 10
|
maxExposesPerPeer = 10
|
||||||
|
exposeReapBatch = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type trackedExpose struct {
|
type exposeReaper struct {
|
||||||
mu sync.Mutex
|
manager *Manager
|
||||||
domain string
|
|
||||||
accountID string
|
|
||||||
peerID string
|
|
||||||
lastRenewed time.Time
|
|
||||||
expiring bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type exposeTracker struct {
|
// StartExposeReaper starts a background goroutine that reaps expired ephemeral services from the DB.
|
||||||
activeExposes sync.Map
|
func (r *exposeReaper) StartExposeReaper(ctx context.Context) {
|
||||||
exposeCreateMu sync.Mutex
|
|
||||||
manager *Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
func exposeKey(peerID, domain string) string {
|
|
||||||
return peerID + ":" + domain
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrackExposeIfAllowed atomically checks the per-peer limit and registers a new
|
|
||||||
// active expose session under the same lock. Returns (true, false) if the expose
|
|
||||||
// was already tracked (duplicate), (false, true) if tracking succeeded, and
|
|
||||||
// (false, false) if the peer has reached the limit.
|
|
||||||
func (t *exposeTracker) TrackExposeIfAllowed(peerID, domain, accountID string) (alreadyTracked, ok bool) {
|
|
||||||
t.exposeCreateMu.Lock()
|
|
||||||
defer t.exposeCreateMu.Unlock()
|
|
||||||
|
|
||||||
key := exposeKey(peerID, domain)
|
|
||||||
_, loaded := t.activeExposes.LoadOrStore(key, &trackedExpose{
|
|
||||||
domain: domain,
|
|
||||||
accountID: accountID,
|
|
||||||
peerID: peerID,
|
|
||||||
lastRenewed: time.Now(),
|
|
||||||
})
|
|
||||||
if loaded {
|
|
||||||
return true, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.CountPeerExposes(peerID) > maxExposesPerPeer {
|
|
||||||
t.activeExposes.Delete(key)
|
|
||||||
return false, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// UntrackExpose removes an active expose session from tracking.
|
|
||||||
func (t *exposeTracker) UntrackExpose(peerID, domain string) {
|
|
||||||
t.activeExposes.Delete(exposeKey(peerID, domain))
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountPeerExposes returns the number of active expose sessions for a peer.
|
|
||||||
func (t *exposeTracker) CountPeerExposes(peerID string) int {
|
|
||||||
count := 0
|
|
||||||
t.activeExposes.Range(func(_, val any) bool {
|
|
||||||
if expose := val.(*trackedExpose); expose.peerID == peerID {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaxExposesPerPeer returns the maximum number of concurrent exposes allowed per peer.
|
|
||||||
func (t *exposeTracker) MaxExposesPerPeer() int {
|
|
||||||
return maxExposesPerPeer
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenewTrackedExpose updates the in-memory lastRenewed timestamp for a tracked expose.
|
|
||||||
// Returns false if the expose is not tracked or is being reaped.
|
|
||||||
func (t *exposeTracker) RenewTrackedExpose(peerID, domain string) bool {
|
|
||||||
key := exposeKey(peerID, domain)
|
|
||||||
val, ok := t.activeExposes.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
expose := val.(*trackedExpose)
|
|
||||||
expose.mu.Lock()
|
|
||||||
if expose.expiring {
|
|
||||||
expose.mu.Unlock()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
expose.lastRenewed = time.Now()
|
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopTrackedExpose removes an active expose session from tracking.
|
|
||||||
// Returns false if the expose was not tracked.
|
|
||||||
func (t *exposeTracker) StopTrackedExpose(peerID, domain string) bool {
|
|
||||||
key := exposeKey(peerID, domain)
|
|
||||||
_, ok := t.activeExposes.LoadAndDelete(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartExposeReaper starts a background goroutine that reaps expired expose sessions.
|
|
||||||
func (t *exposeTracker) StartExposeReaper(ctx context.Context) {
|
|
||||||
go func() {
|
go func() {
|
||||||
|
// start with a random delay
|
||||||
|
rn := rand.IntN(10)
|
||||||
|
time.Sleep(time.Duration(rn) * time.Second)
|
||||||
|
|
||||||
ticker := time.NewTicker(exposeReapInterval)
|
ticker := time.NewTicker(exposeReapInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -123,41 +35,31 @@ func (t *exposeTracker) StartExposeReaper(ctx context.Context) {
|
|||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
t.reapExpiredExposes()
|
r.reapExpiredExposes(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *exposeTracker) reapExpiredExposes() {
|
func (r *exposeReaper) reapExpiredExposes(ctx context.Context) {
|
||||||
t.activeExposes.Range(func(key, val any) bool {
|
expired, err := r.manager.store.GetExpiredEphemeralServices(ctx, exposeTTL, exposeReapBatch)
|
||||||
expose := val.(*trackedExpose)
|
if err != nil {
|
||||||
expose.mu.Lock()
|
log.Errorf("failed to get expired ephemeral services: %v", err)
|
||||||
expired := time.Since(expose.lastRenewed) > exposeTTL
|
return
|
||||||
if expired {
|
}
|
||||||
expose.expiring = true
|
|
||||||
}
|
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
if !expired {
|
for _, svc := range expired {
|
||||||
return true
|
log.Infof("reaping expired expose session for peer %s, domain %s", svc.SourcePeer, svc.Domain)
|
||||||
|
|
||||||
|
err := r.manager.deleteExpiredPeerService(ctx, svc.AccountID, svc.SourcePeer, svc.ID)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("reaping expired expose session for peer %s, domain %s", expose.peerID, expose.domain)
|
if s, ok := status.FromError(err); ok && s.ErrorType == status.NotFound {
|
||||||
|
log.Debugf("service %s was already deleted by another instance", svc.Domain)
|
||||||
err := t.manager.deleteServiceFromPeer(context.Background(), expose.accountID, expose.peerID, expose.domain, true)
|
} else {
|
||||||
|
log.Errorf("failed to delete expired peer-exposed service for domain %s: %v", svc.Domain, err)
|
||||||
s, _ := status.FromError(err)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
t.activeExposes.Delete(key)
|
|
||||||
case s.ErrorType == status.NotFound:
|
|
||||||
log.Debugf("service %s was already deleted", expose.domain)
|
|
||||||
default:
|
|
||||||
log.Errorf("failed to delete expired peer-exposed service for domain %s: %v", expose.domain, err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,184 +10,62 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExposeKey(t *testing.T) {
|
|
||||||
assert.Equal(t, "peer1:example.com", exposeKey("peer1", "example.com"))
|
|
||||||
assert.Equal(t, "peer2:other.com", exposeKey("peer2", "other.com"))
|
|
||||||
assert.NotEqual(t, exposeKey("peer1", "a.com"), exposeKey("peer1", "b.com"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrackExposeIfAllowed(t *testing.T) {
|
|
||||||
t.Run("first track succeeds", func(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
assert.False(t, alreadyTracked, "first track should not be duplicate")
|
|
||||||
assert.True(t, ok, "first track should be allowed")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("duplicate track detected", func(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
|
|
||||||
alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
assert.True(t, alreadyTracked, "second track should be duplicate")
|
|
||||||
assert.False(t, ok)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("rejects when at limit", func(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
for i := range maxExposesPerPeer {
|
|
||||||
_, ok := tracker.TrackExposeIfAllowed("peer1", "domain-"+string(rune('a'+i))+".com", "acct1")
|
|
||||||
assert.True(t, ok, "track %d should be allowed", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
alreadyTracked, ok := tracker.TrackExposeIfAllowed("peer1", "over-limit.com", "acct1")
|
|
||||||
assert.False(t, alreadyTracked)
|
|
||||||
assert.False(t, ok, "should reject when at limit")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("other peer unaffected by limit", func(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
for i := range maxExposesPerPeer {
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "domain-"+string(rune('a'+i))+".com", "acct1")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := tracker.TrackExposeIfAllowed("peer2", "a.com", "acct1")
|
|
||||||
assert.True(t, ok, "other peer should still be within limit")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUntrackExpose(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
assert.Equal(t, 1, tracker.CountPeerExposes("peer1"))
|
|
||||||
|
|
||||||
tracker.UntrackExpose("peer1", "a.com")
|
|
||||||
assert.Equal(t, 0, tracker.CountPeerExposes("peer1"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCountPeerExposes(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
|
|
||||||
assert.Equal(t, 0, tracker.CountPeerExposes("peer1"))
|
|
||||||
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "b.com", "acct1")
|
|
||||||
tracker.TrackExposeIfAllowed("peer2", "a.com", "acct1")
|
|
||||||
|
|
||||||
assert.Equal(t, 2, tracker.CountPeerExposes("peer1"), "peer1 should have 2 exposes")
|
|
||||||
assert.Equal(t, 1, tracker.CountPeerExposes("peer2"), "peer2 should have 1 expose")
|
|
||||||
assert.Equal(t, 0, tracker.CountPeerExposes("peer3"), "peer3 should have 0 exposes")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMaxExposesPerPeer(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
assert.Equal(t, maxExposesPerPeer, tracker.MaxExposesPerPeer())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewTrackedExpose(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
|
|
||||||
found := tracker.RenewTrackedExpose("peer1", "a.com")
|
|
||||||
assert.False(t, found, "should not find untracked expose")
|
|
||||||
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
|
|
||||||
found = tracker.RenewTrackedExpose("peer1", "a.com")
|
|
||||||
assert.True(t, found, "should find tracked expose")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRenewTrackedExpose_RejectsExpiring(t *testing.T) {
|
|
||||||
tracker := &exposeTracker{}
|
|
||||||
tracker.TrackExposeIfAllowed("peer1", "a.com", "acct1")
|
|
||||||
|
|
||||||
// Simulate reaper marking the expose as expiring
|
|
||||||
key := exposeKey("peer1", "a.com")
|
|
||||||
val, _ := tracker.activeExposes.Load(key)
|
|
||||||
expose := val.(*trackedExpose)
|
|
||||||
expose.mu.Lock()
|
|
||||||
expose.expiring = true
|
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
found := tracker.RenewTrackedExpose("peer1", "a.com")
|
|
||||||
assert.False(t, found, "should reject renewal when expiring")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReapExpiredExposes(t *testing.T) {
|
func TestReapExpiredExposes(t *testing.T) {
|
||||||
mgr, _ := setupIntegrationTest(t)
|
mgr, testStore := setupIntegrationTest(t)
|
||||||
tracker := mgr.exposeTracker
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Manually expire the tracked entry
|
// Manually expire the service by backdating meta_last_renewed_at
|
||||||
key := exposeKey(testPeerID, resp.Domain)
|
expireEphemeralService(t, testStore, testAccountID, resp.Domain)
|
||||||
val, _ := tracker.activeExposes.Load(key)
|
|
||||||
expose := val.(*trackedExpose)
|
|
||||||
expose.mu.Lock()
|
|
||||||
expose.lastRenewed = time.Now().Add(-2 * exposeTTL)
|
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
// Add an active (non-expired) tracking entry
|
// Create a non-expired service
|
||||||
tracker.activeExposes.Store(exposeKey("peer1", "active.com"), &trackedExpose{
|
resp2, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
domain: "active.com",
|
Port: 8081,
|
||||||
accountID: testAccountID,
|
Protocol: "http",
|
||||||
peerID: "peer1",
|
|
||||||
lastRenewed: time.Now(),
|
|
||||||
})
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tracker.reapExpiredExposes()
|
mgr.exposeReaper.reapExpiredExposes(ctx)
|
||||||
|
|
||||||
_, exists := tracker.activeExposes.Load(key)
|
// Expired service should be deleted
|
||||||
assert.False(t, exists, "expired expose should be removed")
|
_, err = testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain)
|
||||||
|
require.Error(t, err, "expired service should be deleted")
|
||||||
|
|
||||||
_, exists = tracker.activeExposes.Load(exposeKey("peer1", "active.com"))
|
// Non-expired service should remain
|
||||||
assert.True(t, exists, "active expose should remain")
|
_, err = testStore.GetServiceByDomain(ctx, testAccountID, resp2.Domain)
|
||||||
|
require.NoError(t, err, "active service should remain")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReapExpiredExposes_SetsExpiringFlag(t *testing.T) {
|
func TestReapAlreadyDeletedService(t *testing.T) {
|
||||||
mgr, _ := setupIntegrationTest(t)
|
mgr, testStore := setupIntegrationTest(t)
|
||||||
tracker := mgr.exposeTracker
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
key := exposeKey(testPeerID, resp.Domain)
|
expireEphemeralService(t, testStore, testAccountID, resp.Domain)
|
||||||
val, _ := tracker.activeExposes.Load(key)
|
|
||||||
expose := val.(*trackedExpose)
|
|
||||||
|
|
||||||
// Expire it
|
// Delete the service before reaping
|
||||||
expose.mu.Lock()
|
err = mgr.StopServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain)
|
||||||
expose.lastRenewed = time.Now().Add(-2 * exposeTTL)
|
require.NoError(t, err)
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
// Renew should succeed before reaping
|
// Reaping should handle the already-deleted service gracefully
|
||||||
assert.True(t, tracker.RenewTrackedExpose(testPeerID, resp.Domain), "renew should succeed before reaper runs")
|
mgr.exposeReaper.reapExpiredExposes(ctx)
|
||||||
|
|
||||||
// Re-expire and reap
|
|
||||||
expose.mu.Lock()
|
|
||||||
expose.lastRenewed = time.Now().Add(-2 * exposeTTL)
|
|
||||||
expose.mu.Unlock()
|
|
||||||
|
|
||||||
tracker.reapExpiredExposes()
|
|
||||||
|
|
||||||
// Entry is deleted, renew returns false
|
|
||||||
assert.False(t, tracker.RenewTrackedExpose(testPeerID, resp.Domain), "renew should fail after reap")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConcurrentTrackAndCount(t *testing.T) {
|
func TestConcurrentReapAndRenew(t *testing.T) {
|
||||||
mgr, _ := setupIntegrationTest(t)
|
mgr, testStore := setupIntegrationTest(t)
|
||||||
tracker := mgr.exposeTracker
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
@@ -198,59 +76,133 @@ func TestConcurrentTrackAndCount(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually expire all tracked entries
|
// Expire all services
|
||||||
tracker.activeExposes.Range(func(_, val any) bool {
|
services, err := testStore.GetAccountServices(ctx, store.LockingStrengthNone, testAccountID)
|
||||||
expose := val.(*trackedExpose)
|
require.NoError(t, err)
|
||||||
expose.mu.Lock()
|
for _, svc := range services {
|
||||||
expose.lastRenewed = time.Now().Add(-2 * exposeTTL)
|
if svc.Source == rpservice.SourceEphemeral {
|
||||||
expose.mu.Unlock()
|
expireEphemeralService(t, testStore, testAccountID, svc.Domain)
|
||||||
return true
|
}
|
||||||
})
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(2)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
tracker.reapExpiredExposes()
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
tracker.CountPeerExposes(testPeerID)
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
assert.Equal(t, 0, tracker.CountPeerExposes(testPeerID), "all expired exposes should be reaped")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrackedExposeMutexProtectsLastRenewed(t *testing.T) {
|
|
||||||
expose := &trackedExpose{
|
|
||||||
lastRenewed: time.Now().Add(-1 * time.Hour),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range 100 {
|
mgr.exposeReaper.reapExpiredExposes(ctx)
|
||||||
expose.mu.Lock()
|
|
||||||
expose.lastRenewed = time.Now()
|
|
||||||
expose.mu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for range 100 {
|
_, _ = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
expose.mu.Lock()
|
|
||||||
_ = time.Since(expose.lastRenewed)
|
|
||||||
expose.mu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
expose.mu.Lock()
|
count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
require.False(t, expose.lastRenewed.IsZero(), "lastRenewed should not be zero after concurrent access")
|
require.NoError(t, err)
|
||||||
expose.mu.Unlock()
|
assert.Equal(t, int64(0), count, "all expired services should be reaped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewEphemeralService(t *testing.T) {
|
||||||
|
mgr, _ := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("renew succeeds for active service", func(t *testing.T) {
|
||||||
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
|
Port: 8082,
|
||||||
|
Protocol: "http",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("renew fails for nonexistent domain", func(t *testing.T) {
|
||||||
|
err := mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, "nonexistent.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "no active expose session")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCountAndExistsEphemeralServices(t *testing.T) {
|
||||||
|
mgr, _ := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), count)
|
||||||
|
|
||||||
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
|
Port: 8083,
|
||||||
|
Protocol: "http",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
count, err = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), count)
|
||||||
|
|
||||||
|
exists, err := mgr.store.EphemeralServiceExists(ctx, store.LockingStrengthNone, testAccountID, testPeerID, resp.Domain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, exists, "service should exist")
|
||||||
|
|
||||||
|
exists, err = mgr.store.EphemeralServiceExists(ctx, store.LockingStrengthNone, testAccountID, testPeerID, "no-such.domain")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, exists, "non-existent service should not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMaxExposesPerPeerEnforced(t *testing.T) {
|
||||||
|
mgr, _ := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
for i := range maxExposesPerPeer {
|
||||||
|
_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
|
Port: 8090 + i,
|
||||||
|
Protocol: "http",
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "expose %d should succeed", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
|
Port: 9999,
|
||||||
|
Protocol: "http",
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "maximum number of active expose sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReapSkipsRenewedService(t *testing.T) {
|
||||||
|
mgr, testStore := setupIntegrationTest(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
|
Port: 8086,
|
||||||
|
Protocol: "http",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Expire the service
|
||||||
|
expireEphemeralService(t, testStore, testAccountID, resp.Domain)
|
||||||
|
|
||||||
|
// Renew it before the reaper runs
|
||||||
|
err = mgr.RenewServiceFromPeer(ctx, testAccountID, testPeerID, resp.Domain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Reaper should skip it because the re-check sees a fresh timestamp
|
||||||
|
mgr.exposeReaper.reapExpiredExposes(ctx)
|
||||||
|
|
||||||
|
_, err = testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain)
|
||||||
|
require.NoError(t, err, "renewed service should survive reaping")
|
||||||
|
}
|
||||||
|
|
||||||
|
// expireEphemeralService backdates meta_last_renewed_at to force expiration.
|
||||||
|
func expireEphemeralService(t *testing.T, s store.Store, accountID, domain string) {
|
||||||
|
t.Helper()
|
||||||
|
svc, err := s.GetServiceByDomain(context.Background(), accountID, domain)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expired := time.Now().Add(-2 * exposeTTL)
|
||||||
|
svc.Meta.LastRenewedAt = &expired
|
||||||
|
err = s.UpdateService(context.Background(), svc)
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ type Manager struct {
|
|||||||
permissionsManager permissions.Manager
|
permissionsManager permissions.Manager
|
||||||
proxyController proxy.Controller
|
proxyController proxy.Controller
|
||||||
clusterDeriver ClusterDeriver
|
clusterDeriver ClusterDeriver
|
||||||
exposeTracker *exposeTracker
|
exposeReaper *exposeReaper
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewManager creates a new service manager.
|
// NewManager creates a new service manager.
|
||||||
@@ -49,13 +49,13 @@ func NewManager(store store.Store, accountManager account.Manager, permissionsMa
|
|||||||
proxyController: proxyController,
|
proxyController: proxyController,
|
||||||
clusterDeriver: clusterDeriver,
|
clusterDeriver: clusterDeriver,
|
||||||
}
|
}
|
||||||
mgr.exposeTracker = &exposeTracker{manager: mgr}
|
mgr.exposeReaper = &exposeReaper{manager: mgr}
|
||||||
return mgr
|
return mgr
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartExposeReaper delegates to the expose tracker.
|
// StartExposeReaper starts the background goroutine that reaps expired ephemeral services.
|
||||||
func (m *Manager) StartExposeReaper(ctx context.Context) {
|
func (m *Manager) StartExposeReaper(ctx context.Context) {
|
||||||
m.exposeTracker.StartExposeReaper(ctx)
|
m.exposeReaper.StartExposeReaper(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
|
func (m *Manager) GetAllServices(ctx context.Context, accountID, userID string) ([]*service.Service, error) {
|
||||||
@@ -215,6 +215,52 @@ func (m *Manager) persistNewService(ctx context.Context, accountID string, servi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// persistNewEphemeralService creates an ephemeral service inside a single transaction
|
||||||
|
// that also enforces the duplicate and per-peer limit checks atomically.
|
||||||
|
// The count and exists queries use FOR UPDATE locking to serialize concurrent creates
|
||||||
|
// for the same peer, preventing the per-peer limit from being bypassed.
|
||||||
|
func (m *Manager) persistNewEphemeralService(ctx context.Context, accountID, peerID string, svc *service.Service) error {
|
||||||
|
return m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
|
// Lock the peer row to serialize concurrent creates for the same peer.
|
||||||
|
// Without this, when no ephemeral rows exist yet, FOR UPDATE on the services
|
||||||
|
// table returns no rows and acquires no locks, allowing concurrent inserts
|
||||||
|
// to bypass the per-peer limit.
|
||||||
|
if _, err := transaction.GetPeerByID(ctx, store.LockingStrengthUpdate, accountID, peerID); err != nil {
|
||||||
|
return fmt.Errorf("lock peer row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := transaction.EphemeralServiceExists(ctx, store.LockingStrengthUpdate, accountID, peerID, svc.Domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check existing expose: %w", err)
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return status.Errorf(status.AlreadyExists, "peer already has an active expose session for this domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := transaction.CountEphemeralServicesByPeer(ctx, store.LockingStrengthUpdate, accountID, peerID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("count peer exposes: %w", err)
|
||||||
|
}
|
||||||
|
if count >= int64(maxExposesPerPeer) {
|
||||||
|
return status.Errorf(status.PreconditionFailed, "peer has reached the maximum number of active expose sessions (%d)", maxExposesPerPeer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.checkDomainAvailable(ctx, transaction, accountID, svc.Domain, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateTargetReferences(ctx, transaction, accountID, svc.Targets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := transaction.CreateService(ctx, svc); err != nil {
|
||||||
|
return fmt.Errorf("create service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) checkDomainAvailable(ctx context.Context, transaction store.Store, accountID, domain, excludeServiceID string) error {
|
func (m *Manager) checkDomainAvailable(ctx context.Context, transaction store.Store, accountID, domain, excludeServiceID string) error {
|
||||||
existingService, err := transaction.GetServiceByDomain(ctx, accountID, domain)
|
existingService, err := transaction.GetServiceByDomain(ctx, accountID, domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -412,10 +458,6 @@ func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceI
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Source == service.SourceEphemeral {
|
|
||||||
m.exposeTracker.UntrackExpose(s.SourcePeer, s.Domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.accountManager.StoreEvent(ctx, userID, serviceID, accountID, activity.ServiceDeleted, s.EventMeta())
|
m.accountManager.StoreEvent(ctx, userID, serviceID, accountID, activity.ServiceDeleted, s.EventMeta())
|
||||||
|
|
||||||
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Delete, "", m.proxyController.GetOIDCValidationConfig()), s.ProxyCluster)
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, s.ToProtoMapping(service.Delete, "", m.proxyController.GetOIDCValidationConfig()), s.ProxyCluster)
|
||||||
@@ -457,9 +499,6 @@ func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID strin
|
|||||||
oidcCfg := m.proxyController.GetOIDCValidationConfig()
|
oidcCfg := m.proxyController.GetOIDCValidationConfig()
|
||||||
|
|
||||||
for _, svc := range services {
|
for _, svc := range services {
|
||||||
if svc.Source == service.SourceEphemeral {
|
|
||||||
m.exposeTracker.UntrackExpose(svc.SourcePeer, svc.Domain)
|
|
||||||
}
|
|
||||||
m.accountManager.StoreEvent(ctx, userID, svc.ID, accountID, activity.ServiceDeleted, svc.EventMeta())
|
m.accountManager.StoreEvent(ctx, userID, svc.ID, accountID, activity.ServiceDeleted, svc.EventMeta())
|
||||||
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", oidcCfg), svc.ProxyCluster)
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", oidcCfg), svc.ProxyCluster)
|
||||||
}
|
}
|
||||||
@@ -681,26 +720,13 @@ func (m *Manager) CreateServiceFromPeer(ctx context.Context, accountID, peerID s
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
svc.Meta.LastRenewedAt = &now
|
|
||||||
svc.SourcePeer = peerID
|
svc.SourcePeer = peerID
|
||||||
|
|
||||||
if err := m.persistNewService(ctx, accountID, svc); err != nil {
|
now := time.Now()
|
||||||
return nil, err
|
svc.Meta.LastRenewedAt = &now
|
||||||
}
|
|
||||||
|
|
||||||
alreadyTracked, allowed := m.exposeTracker.TrackExposeIfAllowed(peerID, svc.Domain, accountID)
|
if err := m.persistNewEphemeralService(ctx, accountID, peerID, svc); err != nil {
|
||||||
if alreadyTracked {
|
return nil, err
|
||||||
if err := m.deleteServiceFromPeer(ctx, accountID, peerID, svc.Domain, false); err != nil {
|
|
||||||
log.WithContext(ctx).Debugf("failed to delete duplicate expose service for domain %s: %v", svc.Domain, err)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(status.AlreadyExists, "peer already has an active expose session for this domain")
|
|
||||||
}
|
|
||||||
if !allowed {
|
|
||||||
if err := m.deleteServiceFromPeer(ctx, accountID, peerID, svc.Domain, false); err != nil {
|
|
||||||
log.WithContext(ctx).Debugf("failed to delete service after limit exceeded for domain %s: %v", svc.Domain, err)
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(status.PreconditionFailed, "peer has reached the maximum number of active expose sessions (%d)", maxExposesPerPeer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := addPeerInfoToEventMeta(svc.EventMeta(), peer)
|
meta := addPeerInfoToEventMeta(svc.EventMeta(), peer)
|
||||||
@@ -748,26 +774,17 @@ func (m *Manager) buildRandomDomain(name string) (string, error) {
|
|||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenewServiceFromPeer renews the in-memory TTL tracker for the peer's expose session.
|
// RenewServiceFromPeer updates the DB timestamp for the peer's ephemeral service.
|
||||||
// Returns an error if the expose is not actively tracked.
|
func (m *Manager) RenewServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error {
|
||||||
func (m *Manager) RenewServiceFromPeer(_ context.Context, _, peerID, domain string) error {
|
return m.store.RenewEphemeralService(ctx, accountID, peerID, domain)
|
||||||
if !m.exposeTracker.RenewTrackedExpose(peerID, domain) {
|
|
||||||
return status.Errorf(status.NotFound, "no active expose session for domain %s", domain)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopServiceFromPeer stops a peer's active expose session by untracking and deleting the service.
|
// StopServiceFromPeer stops a peer's active expose session by deleting the service from the DB.
|
||||||
func (m *Manager) StopServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error {
|
func (m *Manager) StopServiceFromPeer(ctx context.Context, accountID, peerID, domain string) error {
|
||||||
if err := m.deleteServiceFromPeer(ctx, accountID, peerID, domain, false); err != nil {
|
if err := m.deleteServiceFromPeer(ctx, accountID, peerID, domain, false); err != nil {
|
||||||
log.WithContext(ctx).Errorf("failed to delete peer-exposed service for domain %s: %v", domain, err)
|
log.WithContext(ctx).Errorf("failed to delete peer-exposed service for domain %s: %v", domain, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.exposeTracker.StopTrackedExpose(peerID, domain) {
|
|
||||||
log.WithContext(ctx).Warnf("expose tracker entry for domain %s already removed; service was deleted", domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,6 +865,57 @@ func (m *Manager) deletePeerService(ctx context.Context, accountID, peerID, serv
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteExpiredPeerService deletes an ephemeral service by ID after re-checking
|
||||||
|
// that it is still expired under a row lock. This prevents deleting a service
|
||||||
|
// that was renewed between the batch query and this delete, and ensures only one
|
||||||
|
// management instance processes the deletion
|
||||||
|
func (m *Manager) deleteExpiredPeerService(ctx context.Context, accountID, peerID, serviceID string) error {
|
||||||
|
var svc *service.Service
|
||||||
|
deleted := false
|
||||||
|
err := m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
|
||||||
|
var err error
|
||||||
|
svc, err = transaction.GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Source != service.SourceEphemeral || svc.SourcePeer != peerID {
|
||||||
|
return status.Errorf(status.PermissionDenied, "service does not match expected ephemeral owner")
|
||||||
|
}
|
||||||
|
|
||||||
|
if svc.Meta.LastRenewedAt != nil && time.Since(*svc.Meta.LastRenewedAt) <= exposeTTL {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = transaction.DeleteService(ctx, accountID, serviceID); err != nil {
|
||||||
|
return fmt.Errorf("delete service: %w", err)
|
||||||
|
}
|
||||||
|
deleted = true
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deleted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := m.store.GetPeerByID(ctx, store.LockingStrengthNone, accountID, peerID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithContext(ctx).Debugf("failed to get peer %s for event metadata: %v", peerID, err)
|
||||||
|
peer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
meta := addPeerInfoToEventMeta(svc.EventMeta(), peer)
|
||||||
|
m.accountManager.StoreEvent(ctx, peerID, serviceID, accountID, activity.PeerServiceExposeExpired, meta)
|
||||||
|
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", m.proxyController.GetOIDCValidationConfig()), svc.ProxyCluster)
|
||||||
|
m.accountManager.UpdateAccountPeers(ctx, accountID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func addPeerInfoToEventMeta(meta map[string]any, peer *nbpeer.Peer) map[string]any {
|
func addPeerInfoToEventMeta(meta map[string]any, peer *nbpeer.Peer) map[string]any {
|
||||||
if peer == nil {
|
if peer == nil {
|
||||||
return meta
|
return meta
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ func setupIntegrationTest(t *testing.T) (*Manager, store.Store) {
|
|||||||
domains: []string{"test.netbird.io"},
|
domains: []string{"test.netbird.io"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
mgr.exposeTracker = &exposeTracker{manager: mgr}
|
mgr.exposeReaper = &exposeReaper{manager: mgr}
|
||||||
|
|
||||||
return mgr, testStore
|
return mgr, testStore
|
||||||
}
|
}
|
||||||
@@ -1017,36 +1017,38 @@ func TestStopServiceFromPeer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteService_UntracksEphemeralExpose(t *testing.T) {
|
func TestDeleteService_DeletesEphemeralExpose(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
mgr, _ := setupIntegrationTest(t)
|
mgr, testStore := setupIntegrationTest(t)
|
||||||
|
|
||||||
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, mgr.exposeTracker.CountPeerExposes(testPeerID), "expose should be tracked after create")
|
|
||||||
|
|
||||||
// Look up the service by domain to get its store ID
|
count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
svc, err := mgr.store.GetServiceByDomain(ctx, testAccountID, resp.Domain)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), count, "one ephemeral service should exist after create")
|
||||||
|
|
||||||
|
svc, err := testStore.GetServiceByDomain(ctx, testAccountID, resp.Domain)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Delete via the API path (user-initiated)
|
|
||||||
err = mgr.DeleteService(ctx, testAccountID, testUserID, svc.ID)
|
err = mgr.DeleteService(ctx, testAccountID, testUserID, svc.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 0, mgr.exposeTracker.CountPeerExposes(testPeerID), "expose should be untracked after API delete")
|
count, err = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), count, "ephemeral service should be deleted after API delete")
|
||||||
|
|
||||||
// A new expose should succeed (not blocked by stale tracking)
|
|
||||||
_, err = mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
_, err = mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
|
||||||
Port: 9090,
|
Port: 9090,
|
||||||
Protocol: "http",
|
Protocol: "http",
|
||||||
})
|
})
|
||||||
assert.NoError(t, err, "new expose should succeed after API delete cleared tracking")
|
assert.NoError(t, err, "new expose should succeed after API delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteAllServices_UntracksEphemeralExposes(t *testing.T) {
|
func TestDeleteAllServices_DeletesEphemeralExposes(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
mgr, _ := setupIntegrationTest(t)
|
mgr, _ := setupIntegrationTest(t)
|
||||||
|
|
||||||
@@ -1058,12 +1060,16 @@ func TestDeleteAllServices_UntracksEphemeralExposes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 3, mgr.exposeTracker.CountPeerExposes(testPeerID), "all exposes should be tracked")
|
count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(3), count, "all ephemeral services should exist")
|
||||||
|
|
||||||
err := mgr.DeleteAllServices(ctx, testAccountID, testUserID)
|
err = mgr.DeleteAllServices(ctx, testAccountID, testUserID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, 0, mgr.exposeTracker.CountPeerExposes(testPeerID), "all exposes should be untracked after DeleteAllServices")
|
count, err = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), count, "all ephemeral services should be deleted after DeleteAllServices")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRenewServiceFromPeer(t *testing.T) {
|
func TestRenewServiceFromPeer(t *testing.T) {
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ type Service struct {
|
|||||||
Meta Meta `gorm:"embedded;embeddedPrefix:meta_"`
|
Meta Meta `gorm:"embedded;embeddedPrefix:meta_"`
|
||||||
SessionPrivateKey string `gorm:"column:session_private_key"`
|
SessionPrivateKey string `gorm:"column:session_private_key"`
|
||||||
SessionPublicKey string `gorm:"column:session_public_key"`
|
SessionPublicKey string `gorm:"column:session_public_key"`
|
||||||
Source string `gorm:"default:'permanent'"`
|
Source string `gorm:"default:'permanent';index:idx_service_source_peer"`
|
||||||
SourcePeer string
|
SourcePeer string `gorm:"index:idx_service_source_peer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(accountID, name, domain, proxyCluster string, targets []*Target, enabled bool) *Service {
|
func NewService(accountID, name, domain, proxyCluster string, targets []*Target, enabled bool) *Service {
|
||||||
|
|||||||
@@ -294,9 +294,9 @@ func (w *Worker) generateProperties(ctx context.Context) properties {
|
|||||||
localUsers++
|
localUsers++
|
||||||
} else {
|
} else {
|
||||||
idpUsers++
|
idpUsers++
|
||||||
idpType := extractIdpType(idpID)
|
|
||||||
embeddedIdpTypes[idpType]++
|
|
||||||
}
|
}
|
||||||
|
idpType := extractIdpType(idpID)
|
||||||
|
embeddedIdpTypes[idpType]++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,6 +531,9 @@ func createPostRequest(ctx context.Context, endpoint string, payloadStr string)
|
|||||||
// Connector IDs are formatted as "<type>-<xid>" (e.g., "okta-abc123", "zitadel-xyz").
|
// Connector IDs are formatted as "<type>-<xid>" (e.g., "okta-abc123", "zitadel-xyz").
|
||||||
// Returns the type prefix, or "oidc" if no known prefix is found.
|
// Returns the type prefix, or "oidc" if no known prefix is found.
|
||||||
func extractIdpType(connectorID string) string {
|
func extractIdpType(connectorID string) string {
|
||||||
|
if connectorID == "local" {
|
||||||
|
return "local"
|
||||||
|
}
|
||||||
idx := strings.LastIndex(connectorID, "-")
|
idx := strings.LastIndex(connectorID, "-")
|
||||||
if idx <= 0 {
|
if idx <= 0 {
|
||||||
return "oidc"
|
return "oidc"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func (mockDatasource) GetAllConnectedPeers() map[string]struct{} {
|
|||||||
func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
|
func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
|
||||||
localUserID := dex.EncodeDexUserID("10", "local")
|
localUserID := dex.EncodeDexUserID("10", "local")
|
||||||
idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
|
idpUserID := dex.EncodeDexUserID("20", "zitadel-d5uv82dra0haedlf6kv0")
|
||||||
|
oidcUserID := dex.EncodeDexUserID("30", "d6jvvp69kmnc73c9pl40")
|
||||||
return []*types.Account{
|
return []*types.Account{
|
||||||
{
|
{
|
||||||
Id: "1",
|
Id: "1",
|
||||||
@@ -206,6 +207,13 @@ func (mockDatasource) GetAllAccounts(_ context.Context) []*types.Account {
|
|||||||
"1": {},
|
"1": {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
oidcUserID: {
|
||||||
|
Id: oidcUserID,
|
||||||
|
IsServiceUser: false,
|
||||||
|
PATs: map[string]*types.PersonalAccessToken{
|
||||||
|
"1": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Networks: []*networkTypes.Network{
|
Networks: []*networkTypes.Network{
|
||||||
{
|
{
|
||||||
@@ -278,14 +286,14 @@ func TestGenerateProperties(t *testing.T) {
|
|||||||
if properties["rules"] != 4 {
|
if properties["rules"] != 4 {
|
||||||
t.Errorf("expected 4 rules, got %d", properties["rules"])
|
t.Errorf("expected 4 rules, got %d", properties["rules"])
|
||||||
}
|
}
|
||||||
if properties["users"] != 2 {
|
if properties["users"] != 3 {
|
||||||
t.Errorf("expected 1 users, got %d", properties["users"])
|
t.Errorf("expected 3 users, got %d", properties["users"])
|
||||||
}
|
}
|
||||||
if properties["setup_keys_usage"] != 2 {
|
if properties["setup_keys_usage"] != 2 {
|
||||||
t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
|
t.Errorf("expected 1 setup_keys_usage, got %d", properties["setup_keys_usage"])
|
||||||
}
|
}
|
||||||
if properties["pats"] != 4 {
|
if properties["pats"] != 5 {
|
||||||
t.Errorf("expected 4 personal_access_tokens, got %d", properties["pats"])
|
t.Errorf("expected 5 personal_access_tokens, got %d", properties["pats"])
|
||||||
}
|
}
|
||||||
if properties["peers_ssh_enabled"] != 2 {
|
if properties["peers_ssh_enabled"] != 2 {
|
||||||
t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
|
t.Errorf("expected 2 peers_ssh_enabled, got %d", properties["peers_ssh_enabled"])
|
||||||
@@ -369,14 +377,20 @@ func TestGenerateProperties(t *testing.T) {
|
|||||||
if properties["local_users_count"] != 1 {
|
if properties["local_users_count"] != 1 {
|
||||||
t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
|
t.Errorf("expected 1 local_users_count, got %d", properties["local_users_count"])
|
||||||
}
|
}
|
||||||
if properties["idp_users_count"] != 1 {
|
if properties["idp_users_count"] != 2 {
|
||||||
t.Errorf("expected 1 idp_users_count, got %d", properties["idp_users_count"])
|
t.Errorf("expected 2 idp_users_count, got %d", properties["idp_users_count"])
|
||||||
|
}
|
||||||
|
if properties["embedded_idp_users_local"] != 1 {
|
||||||
|
t.Errorf("expected 1 embedded_idp_users_local, got %v", properties["embedded_idp_users_local"])
|
||||||
}
|
}
|
||||||
if properties["embedded_idp_users_zitadel"] != 1 {
|
if properties["embedded_idp_users_zitadel"] != 1 {
|
||||||
t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
|
t.Errorf("expected 1 embedded_idp_users_zitadel, got %v", properties["embedded_idp_users_zitadel"])
|
||||||
}
|
}
|
||||||
if properties["embedded_idp_count"] != 1 {
|
if properties["embedded_idp_users_oidc"] != 1 {
|
||||||
t.Errorf("expected 1 embedded_idp_count, got %v", properties["embedded_idp_count"])
|
t.Errorf("expected 1 embedded_idp_users_oidc, got %v", properties["embedded_idp_users_oidc"])
|
||||||
|
}
|
||||||
|
if properties["embedded_idp_count"] != 3 {
|
||||||
|
t.Errorf("expected 3 embedded_idp_count, got %v", properties["embedded_idp_count"])
|
||||||
}
|
}
|
||||||
|
|
||||||
if properties["services"] != 2 {
|
if properties["services"] != 2 {
|
||||||
@@ -436,7 +450,8 @@ func TestExtractIdpType(t *testing.T) {
|
|||||||
{"microsoft-abc123", "microsoft"},
|
{"microsoft-abc123", "microsoft"},
|
||||||
{"authentik-abc123", "authentik"},
|
{"authentik-abc123", "authentik"},
|
||||||
{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
|
{"keycloak-d5uv82dra0haedlf6kv0", "keycloak"},
|
||||||
{"local", "oidc"},
|
{"local", "local"},
|
||||||
|
{"d6jvvp69kmnc73c9pl40", "oidc"},
|
||||||
{"", "oidc"},
|
{"", "oidc"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5040,6 +5040,99 @@ func (s *SqlStore) GetAccountServices(ctx context.Context, lockStrength LockingS
|
|||||||
return serviceList, nil
|
return serviceList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenewEphemeralService updates the last_renewed_at timestamp for an ephemeral service.
|
||||||
|
func (s *SqlStore) RenewEphemeralService(ctx context.Context, accountID, peerID, domain string) error {
|
||||||
|
result := s.db.Model(&rpservice.Service{}).
|
||||||
|
Where("account_id = ? AND source_peer = ? AND domain = ? AND source = ?", accountID, peerID, domain, rpservice.SourceEphemeral).
|
||||||
|
Update("meta_last_renewed_at", time.Now())
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to renew ephemeral service: %v", result.Error)
|
||||||
|
return status.Errorf(status.Internal, "renew ephemeral service")
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return status.Errorf(status.NotFound, "no active expose session for domain %s", domain)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExpiredEphemeralServices returns ephemeral services whose last renewal exceeds the given TTL.
|
||||||
|
// Only the fields needed for reaping are selected. The limit parameter caps the batch size to
|
||||||
|
// avoid loading too many rows in a single tick. Rows with empty source_peer are excluded to
|
||||||
|
// skip malformed legacy data.
|
||||||
|
func (s *SqlStore) GetExpiredEphemeralServices(ctx context.Context, ttl time.Duration, limit int) ([]*rpservice.Service, error) {
|
||||||
|
cutoff := time.Now().Add(-ttl)
|
||||||
|
var services []*rpservice.Service
|
||||||
|
result := s.db.
|
||||||
|
Select("id", "account_id", "source_peer", "domain").
|
||||||
|
Where("source = ? AND source_peer <> '' AND meta_last_renewed_at < ?", rpservice.SourceEphemeral, cutoff).
|
||||||
|
Limit(limit).
|
||||||
|
Find(&services)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to get expired ephemeral services: %v", result.Error)
|
||||||
|
return nil, status.Errorf(status.Internal, "get expired ephemeral services")
|
||||||
|
}
|
||||||
|
return services, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountEphemeralServicesByPeer returns the count of ephemeral services for a specific peer.
|
||||||
|
// Use LockingStrengthUpdate inside a transaction to serialize concurrent create operations.
|
||||||
|
// The locking is applied via a row-level SELECT ... FOR UPDATE (not on the aggregate) to
|
||||||
|
// stay compatible with Postgres, which disallows FOR UPDATE on COUNT(*).
|
||||||
|
func (s *SqlStore) CountEphemeralServicesByPeer(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (int64, error) {
|
||||||
|
if lockStrength == LockingStrengthNone {
|
||||||
|
var count int64
|
||||||
|
result := s.db.Model(&rpservice.Service{}).
|
||||||
|
Where("account_id = ? AND source_peer = ? AND source = ?", accountID, peerID, rpservice.SourceEphemeral).
|
||||||
|
Count(&count)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to count ephemeral services: %v", result.Error)
|
||||||
|
return 0, status.Errorf(status.Internal, "count ephemeral services")
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
result := s.db.Model(&rpservice.Service{}).
|
||||||
|
Clauses(clause.Locking{Strength: string(lockStrength)}).
|
||||||
|
Select("id").
|
||||||
|
Where("account_id = ? AND source_peer = ? AND source = ?", accountID, peerID, rpservice.SourceEphemeral).
|
||||||
|
Pluck("id", &ids)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to count ephemeral services: %v", result.Error)
|
||||||
|
return 0, status.Errorf(status.Internal, "count ephemeral services")
|
||||||
|
}
|
||||||
|
return int64(len(ids)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EphemeralServiceExists checks if an ephemeral service exists for the given peer and domain.
|
||||||
|
// Use LockingStrengthUpdate inside a transaction to serialize concurrent create operations.
|
||||||
|
func (s *SqlStore) EphemeralServiceExists(ctx context.Context, lockStrength LockingStrength, accountID, peerID, domain string) (bool, error) {
|
||||||
|
if lockStrength == LockingStrengthNone {
|
||||||
|
var count int64
|
||||||
|
result := s.db.Model(&rpservice.Service{}).
|
||||||
|
Where("account_id = ? AND source_peer = ? AND domain = ? AND source = ?", accountID, peerID, domain, rpservice.SourceEphemeral).
|
||||||
|
Count(&count)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to check ephemeral service existence: %v", result.Error)
|
||||||
|
return false, status.Errorf(status.Internal, "check ephemeral service existence")
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var id string
|
||||||
|
result := s.db.Model(&rpservice.Service{}).
|
||||||
|
Clauses(clause.Locking{Strength: string(lockStrength)}).
|
||||||
|
Select("id").
|
||||||
|
Where("account_id = ? AND source_peer = ? AND domain = ? AND source = ?", accountID, peerID, domain, rpservice.SourceEphemeral).
|
||||||
|
Limit(1).
|
||||||
|
Pluck("id", &id)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.WithContext(ctx).Errorf("failed to check ephemeral service existence: %v", result.Error)
|
||||||
|
return false, status.Errorf(status.Internal, "check ephemeral service existence")
|
||||||
|
}
|
||||||
|
return id != "", nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SqlStore) GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) {
|
func (s *SqlStore) GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error) {
|
||||||
tx := s.db
|
tx := s.db
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,11 @@ type Store interface {
|
|||||||
GetServices(ctx context.Context, lockStrength LockingStrength) ([]*rpservice.Service, error)
|
GetServices(ctx context.Context, lockStrength LockingStrength) ([]*rpservice.Service, error)
|
||||||
GetAccountServices(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*rpservice.Service, error)
|
GetAccountServices(ctx context.Context, lockStrength LockingStrength, accountID string) ([]*rpservice.Service, error)
|
||||||
|
|
||||||
|
RenewEphemeralService(ctx context.Context, accountID, peerID, domain string) error
|
||||||
|
GetExpiredEphemeralServices(ctx context.Context, ttl time.Duration, limit int) ([]*rpservice.Service, error)
|
||||||
|
CountEphemeralServicesByPeer(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (int64, error)
|
||||||
|
EphemeralServiceExists(ctx context.Context, lockStrength LockingStrength, accountID, peerID, domain string) (bool, error)
|
||||||
|
|
||||||
GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error)
|
GetCustomDomain(ctx context.Context, accountID string, domainID string) (*domain.Domain, error)
|
||||||
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
|
ListFreeDomains(ctx context.Context, accountID string) ([]string, error)
|
||||||
ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error)
|
ListCustomDomains(ctx context.Context, accountID string) ([]*domain.Domain, error)
|
||||||
|
|||||||
@@ -208,6 +208,21 @@ func (mr *MockStoreMockRecorder) CountAccountsByPrivateDomain(ctx, domain interf
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccountsByPrivateDomain", reflect.TypeOf((*MockStore)(nil).CountAccountsByPrivateDomain), ctx, domain)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAccountsByPrivateDomain", reflect.TypeOf((*MockStore)(nil).CountAccountsByPrivateDomain), ctx, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountEphemeralServicesByPeer mocks base method.
|
||||||
|
func (m *MockStore) CountEphemeralServicesByPeer(ctx context.Context, lockStrength LockingStrength, accountID, peerID string) (int64, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CountEphemeralServicesByPeer", ctx, lockStrength, accountID, peerID)
|
||||||
|
ret0, _ := ret[0].(int64)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountEphemeralServicesByPeer indicates an expected call of CountEphemeralServicesByPeer.
|
||||||
|
func (mr *MockStoreMockRecorder) CountEphemeralServicesByPeer(ctx, lockStrength, accountID, peerID interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountEphemeralServicesByPeer", reflect.TypeOf((*MockStore)(nil).CountEphemeralServicesByPeer), ctx, lockStrength, accountID, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
// CreateAccessLog mocks base method.
|
// CreateAccessLog mocks base method.
|
||||||
func (m *MockStore) CreateAccessLog(ctx context.Context, log *accesslogs.AccessLogEntry) error {
|
func (m *MockStore) CreateAccessLog(ctx context.Context, log *accesslogs.AccessLogEntry) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -686,6 +701,21 @@ func (mr *MockStoreMockRecorder) DeleteZoneDNSRecords(ctx, accountID, zoneID int
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZoneDNSRecords", reflect.TypeOf((*MockStore)(nil).DeleteZoneDNSRecords), ctx, accountID, zoneID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteZoneDNSRecords", reflect.TypeOf((*MockStore)(nil).DeleteZoneDNSRecords), ctx, accountID, zoneID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EphemeralServiceExists mocks base method.
|
||||||
|
func (m *MockStore) EphemeralServiceExists(ctx context.Context, lockStrength LockingStrength, accountID, peerID, domain string) (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "EphemeralServiceExists", ctx, lockStrength, accountID, peerID, domain)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// EphemeralServiceExists indicates an expected call of EphemeralServiceExists.
|
||||||
|
func (mr *MockStoreMockRecorder) EphemeralServiceExists(ctx, lockStrength, accountID, peerID, domain interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EphemeralServiceExists", reflect.TypeOf((*MockStore)(nil).EphemeralServiceExists), ctx, lockStrength, accountID, peerID, domain)
|
||||||
|
}
|
||||||
|
|
||||||
// ExecuteInTransaction mocks base method.
|
// ExecuteInTransaction mocks base method.
|
||||||
func (m *MockStore) ExecuteInTransaction(ctx context.Context, f func(Store) error) error {
|
func (m *MockStore) ExecuteInTransaction(ctx context.Context, f func(Store) error) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -1362,6 +1392,21 @@ func (mr *MockStoreMockRecorder) GetDNSRecordByID(ctx, lockStrength, accountID,
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSRecordByID", reflect.TypeOf((*MockStore)(nil).GetDNSRecordByID), ctx, lockStrength, accountID, zoneID, recordID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDNSRecordByID", reflect.TypeOf((*MockStore)(nil).GetDNSRecordByID), ctx, lockStrength, accountID, zoneID, recordID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetExpiredEphemeralServices mocks base method.
|
||||||
|
func (m *MockStore) GetExpiredEphemeralServices(ctx context.Context, ttl time.Duration, limit int) ([]*service.Service, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetExpiredEphemeralServices", ctx, ttl, limit)
|
||||||
|
ret0, _ := ret[0].([]*service.Service)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExpiredEphemeralServices indicates an expected call of GetExpiredEphemeralServices.
|
||||||
|
func (mr *MockStoreMockRecorder) GetExpiredEphemeralServices(ctx, ttl, limit interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpiredEphemeralServices", reflect.TypeOf((*MockStore)(nil).GetExpiredEphemeralServices), ctx, ttl, limit)
|
||||||
|
}
|
||||||
|
|
||||||
// GetGroupByID mocks base method.
|
// GetGroupByID mocks base method.
|
||||||
func (m *MockStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types2.Group, error) {
|
func (m *MockStore) GetGroupByID(ctx context.Context, lockStrength LockingStrength, accountID, groupID string) (*types2.Group, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
@@ -2401,6 +2446,20 @@ func (mr *MockStoreMockRecorder) RemoveResourceFromGroup(ctx, accountId, groupID
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceFromGroup", reflect.TypeOf((*MockStore)(nil).RemoveResourceFromGroup), ctx, accountId, groupID, resourceID)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveResourceFromGroup", reflect.TypeOf((*MockStore)(nil).RemoveResourceFromGroup), ctx, accountId, groupID, resourceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenewEphemeralService mocks base method.
|
||||||
|
func (m *MockStore) RenewEphemeralService(ctx context.Context, accountID, peerID, domain string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "RenewEphemeralService", ctx, accountID, peerID, domain)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenewEphemeralService indicates an expected call of RenewEphemeralService.
|
||||||
|
func (mr *MockStoreMockRecorder) RenewEphemeralService(ctx, accountID, peerID, domain interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewEphemeralService", reflect.TypeOf((*MockStore)(nil).RenewEphemeralService), ctx, accountID, peerID, domain)
|
||||||
|
}
|
||||||
|
|
||||||
// RevokeProxyAccessToken mocks base method.
|
// RevokeProxyAccessToken mocks base method.
|
||||||
func (m *MockStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) error {
|
func (m *MockStore) RevokeProxyAccessToken(ctx context.Context, tokenID string) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|||||||
Reference in New Issue
Block a user