mirror of
https://github.com/fatedier/frp.git
synced 2026-03-20 08:49:16 +08:00
add persistent proxy/visitor store with CRUD API and web UI (#5188)
This commit is contained in:
117
pkg/config/source/aggregator.go
Normal file
117
pkg/config/source/aggregator.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type Aggregator struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
configSource *ConfigSource
|
||||
storeSource *StoreSource
|
||||
}
|
||||
|
||||
func NewAggregator(configSource *ConfigSource) *Aggregator {
|
||||
if configSource == nil {
|
||||
configSource = NewConfigSource()
|
||||
}
|
||||
return &Aggregator{
|
||||
configSource: configSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Aggregator) SetStoreSource(storeSource *StoreSource) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.storeSource = storeSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) ConfigSource() *ConfigSource {
|
||||
return a.configSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) StoreSource() *StoreSource {
|
||||
return a.storeSource
|
||||
}
|
||||
|
||||
func (a *Aggregator) getSourcesLocked() []Source {
|
||||
sources := make([]Source, 0, 2)
|
||||
if a.configSource != nil {
|
||||
sources = append(sources, a.configSource)
|
||||
}
|
||||
if a.storeSource != nil {
|
||||
sources = append(sources, a.storeSource)
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
a.mu.RLock()
|
||||
entries := a.getSourcesLocked()
|
||||
a.mu.RUnlock()
|
||||
|
||||
if len(entries) == 0 {
|
||||
return nil, nil, errors.New("no sources configured")
|
||||
}
|
||||
|
||||
proxyMap := make(map[string]v1.ProxyConfigurer)
|
||||
visitorMap := make(map[string]v1.VisitorConfigurer)
|
||||
|
||||
for _, src := range entries {
|
||||
proxies, visitors, err := src.Load()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("load source: %w", err)
|
||||
}
|
||||
for _, p := range proxies {
|
||||
proxyMap[p.GetBaseConfig().Name] = p
|
||||
}
|
||||
for _, v := range visitors {
|
||||
visitorMap[v.GetBaseConfig().Name] = v
|
||||
}
|
||||
}
|
||||
proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap)
|
||||
return proxies, visitors, nil
|
||||
}
|
||||
|
||||
func (a *Aggregator) mapsToSortedSlices(
|
||||
proxyMap map[string]v1.ProxyConfigurer,
|
||||
visitorMap map[string]v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
||||
proxies := make([]v1.ProxyConfigurer, 0, len(proxyMap))
|
||||
for _, p := range proxyMap {
|
||||
proxies = append(proxies, p)
|
||||
}
|
||||
sort.Slice(proxies, func(i, j int) bool {
|
||||
return proxies[i].GetBaseConfig().Name < proxies[j].GetBaseConfig().Name
|
||||
})
|
||||
|
||||
visitors := make([]v1.VisitorConfigurer, 0, len(visitorMap))
|
||||
for _, v := range visitorMap {
|
||||
visitors = append(visitors, v)
|
||||
}
|
||||
sort.Slice(visitors, func(i, j int) bool {
|
||||
return visitors[i].GetBaseConfig().Name < visitors[j].GetBaseConfig().Name
|
||||
})
|
||||
|
||||
return proxies, visitors
|
||||
}
|
||||
217
pkg/config/source/aggregator_test.go
Normal file
217
pkg/config/source/aggregator_test.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// mockProxy creates a TCP proxy config for testing
|
||||
func mockProxy(name string) v1.ProxyConfigurer {
|
||||
cfg := &v1.TCPProxyConfig{}
|
||||
cfg.Name = name
|
||||
cfg.Type = "tcp"
|
||||
cfg.LocalPort = 8080
|
||||
cfg.RemotePort = 9090
|
||||
return cfg
|
||||
}
|
||||
|
||||
// mockVisitor creates a STCP visitor config for testing
|
||||
func mockVisitor(name string) v1.VisitorConfigurer {
|
||||
cfg := &v1.STCPVisitorConfig{}
|
||||
cfg.Name = name
|
||||
cfg.Type = "stcp"
|
||||
cfg.ServerName = "test-server"
|
||||
return cfg
|
||||
}
|
||||
|
||||
func newTestStoreSource(t *testing.T) *StoreSource {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(t, err)
|
||||
return storeSource
|
||||
}
|
||||
|
||||
func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator {
|
||||
t.Helper()
|
||||
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
if storeSource != nil {
|
||||
agg.SetStoreSource(storeSource)
|
||||
}
|
||||
return agg
|
||||
}
|
||||
|
||||
func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := NewAggregator(nil)
|
||||
require.NotNil(agg)
|
||||
require.NotNil(agg.ConfigSource())
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestNewAggregator_WithoutStore(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
require.NotNil(agg)
|
||||
require.Same(configSource, agg.ConfigSource())
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestNewAggregator_WithStore(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
configSource := NewConfigSource()
|
||||
agg := NewAggregator(configSource)
|
||||
agg.SetStoreSource(storeSource)
|
||||
|
||||
require.Same(configSource, agg.ConfigSource())
|
||||
require.Same(storeSource, agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestAggregator_SetStoreSource_Overwrite(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := newTestAggregator(t, nil)
|
||||
first := newTestStoreSource(t)
|
||||
second := newTestStoreSource(t)
|
||||
|
||||
agg.SetStoreSource(first)
|
||||
require.Same(first, agg.StoreSource())
|
||||
|
||||
agg.SetStoreSource(second)
|
||||
require.Same(second, agg.StoreSource())
|
||||
|
||||
agg.SetStoreSource(nil)
|
||||
require.Nil(agg.StoreSource())
|
||||
}
|
||||
|
||||
func TestAggregator_MergeBySourceOrder(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
|
||||
configSource := agg.ConfigSource()
|
||||
|
||||
configShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||
configShared.LocalPort = 1111
|
||||
configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig)
|
||||
configOnly.LocalPort = 1112
|
||||
|
||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
storeShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
||||
storeShared.LocalPort = 2222
|
||||
storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig)
|
||||
storeOnly.LocalPort = 2223
|
||||
err = storeSource.AddProxy(storeShared)
|
||||
require.NoError(err)
|
||||
err = storeSource.AddProxy(storeOnly)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(visitors, 0)
|
||||
require.Len(proxies, 3)
|
||||
|
||||
var sharedProxy *v1.TCPProxyConfig
|
||||
for _, p := range proxies {
|
||||
if p.GetBaseConfig().Name == "shared" {
|
||||
sharedProxy = p.(*v1.TCPProxyConfig)
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(sharedProxy)
|
||||
require.Equal(2222, sharedProxy.LocalPort)
|
||||
}
|
||||
|
||||
func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
configSource := agg.ConfigSource()
|
||||
|
||||
lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||
lowProxy.LocalPort = 1111
|
||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
disabled := false
|
||||
highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
||||
highProxy.LocalPort = 2222
|
||||
highProxy.Enabled = &disabled
|
||||
err = storeSource.AddProxy(highProxy)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 0)
|
||||
|
||||
proxy := proxies[0].(*v1.TCPProxyConfig)
|
||||
require.Equal("shared-proxy", proxy.Name)
|
||||
require.Equal(1111, proxy.LocalPort)
|
||||
}
|
||||
|
||||
func TestAggregator_VisitorMerge(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
agg := newTestAggregator(t, storeSource)
|
||||
|
||||
err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")})
|
||||
require.NoError(err)
|
||||
err = storeSource.AddVisitor(mockVisitor("visitor2"))
|
||||
require.NoError(err)
|
||||
|
||||
_, visitors, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(visitors, 2)
|
||||
}
|
||||
|
||||
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
agg := newTestAggregator(t, nil)
|
||||
err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, _, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Equal("ssh", proxies[0].GetBaseConfig().Name)
|
||||
|
||||
proxies[0].GetBaseConfig().Name = "alice.ssh"
|
||||
|
||||
proxies2, _, err := agg.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies2, 1)
|
||||
require.Equal("ssh", proxies2[0].GetBaseConfig().Name)
|
||||
}
|
||||
65
pkg/config/source/base_source.go
Normal file
65
pkg/config/source/base_source.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// baseSource provides shared state and behavior for Source implementations.
|
||||
// It manages proxy/visitor storage.
|
||||
// Concrete types (ConfigSource, StoreSource) embed this struct.
|
||||
type baseSource struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
proxies map[string]v1.ProxyConfigurer
|
||||
visitors map[string]v1.VisitorConfigurer
|
||||
}
|
||||
|
||||
func newBaseSource() baseSource {
|
||||
return baseSource{
|
||||
proxies: make(map[string]v1.ProxyConfigurer),
|
||||
visitors: make(map[string]v1.VisitorConfigurer),
|
||||
}
|
||||
}
|
||||
|
||||
// Load returns all enabled proxy and visitor configurations.
|
||||
// Configurations with Enabled explicitly set to false are filtered out.
|
||||
func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||
for _, p := range s.proxies {
|
||||
// Filter out disabled proxies (nil or true means enabled)
|
||||
if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, p)
|
||||
}
|
||||
|
||||
visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||
for _, v := range s.visitors {
|
||||
// Filter out disabled visitors (nil or true means enabled)
|
||||
if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
||||
continue
|
||||
}
|
||||
visitors = append(visitors, v)
|
||||
}
|
||||
|
||||
return cloneConfigurers(proxies, visitors)
|
||||
}
|
||||
48
pkg/config/source/base_source_test.go
Normal file
48
pkg/config/source/base_source_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{
|
||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
||||
Name: "proxy1",
|
||||
Type: "tcp",
|
||||
},
|
||||
}
|
||||
visitorCfg := &v1.STCPVisitorConfig{
|
||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
||||
Name: "visitor1",
|
||||
Type: "stcp",
|
||||
},
|
||||
}
|
||||
|
||||
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||
require.NoError(err)
|
||||
|
||||
firstProxies, firstVisitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(firstProxies, 1)
|
||||
require.Len(firstVisitors, 1)
|
||||
|
||||
// Mutate loaded objects as runtime completion would do.
|
||||
firstProxies[0].Complete()
|
||||
firstVisitors[0].Complete()
|
||||
|
||||
secondProxies, secondVisitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(secondProxies, 1)
|
||||
require.Len(secondVisitors, 1)
|
||||
|
||||
require.Empty(secondProxies[0].GetBaseConfig().LocalIP)
|
||||
require.Empty(secondVisitors[0].GetBaseConfig().BindAddr)
|
||||
}
|
||||
43
pkg/config/source/clone.go
Normal file
43
pkg/config/source/clone.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func cloneConfigurers(
|
||||
proxies []v1.ProxyConfigurer,
|
||||
visitors []v1.VisitorConfigurer,
|
||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||
clonedProxies := make([]v1.ProxyConfigurer, 0, len(proxies))
|
||||
clonedVisitors := make([]v1.VisitorConfigurer, 0, len(visitors))
|
||||
|
||||
for _, cfg := range proxies {
|
||||
if cfg == nil {
|
||||
return nil, nil, fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
clonedProxies = append(clonedProxies, cfg.Clone())
|
||||
}
|
||||
for _, cfg := range visitors {
|
||||
if cfg == nil {
|
||||
return nil, nil, fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
clonedVisitors = append(clonedVisitors, cfg.Clone())
|
||||
}
|
||||
return clonedProxies, clonedVisitors, nil
|
||||
}
|
||||
65
pkg/config/source/config_source.go
Normal file
65
pkg/config/source/config_source.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// ConfigSource implements Source for in-memory configuration.
|
||||
// All operations are thread-safe.
|
||||
type ConfigSource struct {
|
||||
baseSource
|
||||
}
|
||||
|
||||
func NewConfigSource() *ConfigSource {
|
||||
return &ConfigSource{
|
||||
baseSource: newBaseSource(),
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceAll replaces all proxy and visitor configurations atomically.
|
||||
func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies))
|
||||
for _, p := range proxies {
|
||||
if p == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
name := p.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
nextProxies[name] = p
|
||||
}
|
||||
nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors))
|
||||
for _, v := range visitors {
|
||||
if v == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
name := v.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
nextVisitors[name] = v
|
||||
}
|
||||
s.proxies = nextProxies
|
||||
s.visitors = nextVisitors
|
||||
return nil
|
||||
}
|
||||
173
pkg/config/source/config_source_test.go
Normal file
173
pkg/config/source/config_source_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestNewConfigSource(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
require.NotNil(src)
|
||||
}
|
||||
|
||||
func TestConfigSource_ReplaceAll(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2)
|
||||
require.Len(visitors, 1)
|
||||
|
||||
// ReplaceAll again should replace everything
|
||||
err = src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy3")},
|
||||
nil,
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err = src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 0)
|
||||
require.Equal("proxy3", proxies[0].GetBaseConfig().Name)
|
||||
|
||||
// ReplaceAll with nil proxy should fail
|
||||
err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil)
|
||||
require.Error(err)
|
||||
|
||||
// ReplaceAll with empty name proxy should fail
|
||||
err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil)
|
||||
require.Error(err)
|
||||
}
|
||||
|
||||
func TestConfigSource_Load(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2)
|
||||
require.Len(visitors, 1)
|
||||
}
|
||||
|
||||
// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out
|
||||
// proxies and visitors with Enabled explicitly set to false.
|
||||
func TestConfigSource_Load_FiltersDisabled(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
disabled := false
|
||||
enabled := true
|
||||
|
||||
// Create enabled proxy (nil Enabled = enabled by default)
|
||||
enabledProxy := mockProxy("enabled-proxy")
|
||||
|
||||
// Create disabled proxy
|
||||
disabledProxy := &v1.TCPProxyConfig{}
|
||||
disabledProxy.Name = "disabled-proxy"
|
||||
disabledProxy.Type = "tcp"
|
||||
disabledProxy.Enabled = &disabled
|
||||
|
||||
// Create explicitly enabled proxy
|
||||
explicitEnabledProxy := &v1.TCPProxyConfig{}
|
||||
explicitEnabledProxy.Name = "explicit-enabled-proxy"
|
||||
explicitEnabledProxy.Type = "tcp"
|
||||
explicitEnabledProxy.Enabled = &enabled
|
||||
|
||||
// Create enabled visitor (nil Enabled = enabled by default)
|
||||
enabledVisitor := mockVisitor("enabled-visitor")
|
||||
|
||||
// Create disabled visitor
|
||||
disabledVisitor := &v1.STCPVisitorConfig{}
|
||||
disabledVisitor.Name = "disabled-visitor"
|
||||
disabledVisitor.Type = "stcp"
|
||||
disabledVisitor.Enabled = &disabled
|
||||
|
||||
err := src.ReplaceAll(
|
||||
[]v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy},
|
||||
[]v1.VisitorConfigurer{enabledVisitor, disabledVisitor},
|
||||
)
|
||||
require.NoError(err)
|
||||
|
||||
// Load should filter out disabled configs
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 2, "Should have 2 enabled proxies")
|
||||
require.Len(visitors, 1, "Should have 1 enabled visitor")
|
||||
|
||||
// Verify the correct proxies are returned
|
||||
proxyNames := make([]string, 0, len(proxies))
|
||||
for _, p := range proxies {
|
||||
proxyNames = append(proxyNames, p.GetBaseConfig().Name)
|
||||
}
|
||||
require.Contains(proxyNames, "enabled-proxy")
|
||||
require.Contains(proxyNames, "explicit-enabled-proxy")
|
||||
require.NotContains(proxyNames, "disabled-proxy")
|
||||
|
||||
// Verify the correct visitor is returned
|
||||
require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name)
|
||||
}
|
||||
|
||||
func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
src := NewConfigSource()
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
||||
require.NoError(err)
|
||||
|
||||
proxies, visitors, err := src.Load()
|
||||
require.NoError(err)
|
||||
require.Len(proxies, 1)
|
||||
require.Len(visitors, 1)
|
||||
require.Empty(proxies[0].GetBaseConfig().LocalIP)
|
||||
require.Empty(visitors[0].GetBaseConfig().BindAddr)
|
||||
require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
37
pkg/config/source/source.go
Normal file
37
pkg/config/source/source.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
// Source is the interface for configuration sources.
|
||||
// A Source provides proxy and visitor configurations from various backends.
|
||||
// Aggregator currently uses the built-in config source as base and an optional
|
||||
// store source as higher-priority overlay.
|
||||
type Source interface {
|
||||
// Load loads the proxy and visitor configurations from this source.
|
||||
// Returns the loaded configurations and any error encountered.
|
||||
// A disabled entry in one source is source-local filtering, not a cross-source
|
||||
// tombstone for entries from lower-priority sources.
|
||||
//
|
||||
// Error handling contract with Aggregator:
|
||||
// - When err is nil, returned slices are consumed.
|
||||
// - When err is non-nil, Aggregator aborts the merge and returns the error.
|
||||
// - To publish best-effort or partial results, return those results with
|
||||
// err set to nil.
|
||||
Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error)
|
||||
}
|
||||
359
pkg/config/source/store.go
Normal file
359
pkg/config/source/store.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
type StoreSourceConfig struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
type storeData struct {
|
||||
Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"`
|
||||
Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"`
|
||||
}
|
||||
|
||||
type StoreSource struct {
|
||||
baseSource
|
||||
config StoreSourceConfig
|
||||
}
|
||||
|
||||
var (
|
||||
ErrAlreadyExists = errors.New("already exists")
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
|
||||
if cfg.Path == "" {
|
||||
return nil, fmt.Errorf("path is required")
|
||||
}
|
||||
|
||||
s := &StoreSource{
|
||||
baseSource: newBaseSource(),
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
if err := s.loadFromFile(); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to load existing data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) loadFromFile() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.loadFromFileUnlocked()
|
||||
}
|
||||
|
||||
func (s *StoreSource) loadFromFileUnlocked() error {
|
||||
data, err := os.ReadFile(s.config.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var stored storeData
|
||||
if err := v1.WithDisallowUnknownFields(false, func() error {
|
||||
return json.Unmarshal(data, &stored)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
||||
}
|
||||
|
||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
||||
|
||||
for _, tp := range stored.Proxies {
|
||||
if tp.ProxyConfigurer != nil {
|
||||
proxyCfg := tp.ProxyConfigurer
|
||||
name := proxyCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
s.proxies[name] = proxyCfg
|
||||
}
|
||||
}
|
||||
|
||||
for _, tv := range stored.Visitors {
|
||||
if tv.VisitorConfigurer != nil {
|
||||
visitorCfg := tv.VisitorConfigurer
|
||||
name := visitorCfg.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
s.visitors[name] = visitorCfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) saveToFileUnlocked() error {
|
||||
stored := storeData{
|
||||
Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)),
|
||||
Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)),
|
||||
}
|
||||
|
||||
for _, p := range s.proxies {
|
||||
stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p})
|
||||
}
|
||||
for _, v := range s.visitors {
|
||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(stored, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(s.config.Path)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := s.config.Path + ".tmp"
|
||||
|
||||
f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Sync(); err != nil {
|
||||
f.Close()
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to sync temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, s.config.Path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
|
||||
name := proxy.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.proxies[name]; exists {
|
||||
return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name)
|
||||
}
|
||||
|
||||
s.proxies[name] = proxy
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
delete(s.proxies, name)
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
}
|
||||
|
||||
name := proxy.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldProxy, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
||||
}
|
||||
|
||||
s.proxies[name] = proxy
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.proxies[name] = oldProxy
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) RemoveProxy(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldProxy, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
||||
}
|
||||
|
||||
delete(s.proxies, name)
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.proxies[name] = oldProxy
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
p, exists := s.proxies[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
|
||||
name := visitor.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.visitors[name]; exists {
|
||||
return fmt.Errorf("%w: visitor %q", ErrAlreadyExists, name)
|
||||
}
|
||||
|
||||
s.visitors[name] = visitor
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
delete(s.visitors, name)
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
}
|
||||
|
||||
name := visitor.GetBaseConfig().Name
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldVisitor, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
||||
}
|
||||
|
||||
s.visitors[name] = visitor
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.visitors[name] = oldVisitor
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) RemoveVisitor(name string) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("visitor name cannot be empty")
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
oldVisitor, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
||||
}
|
||||
|
||||
delete(s.visitors, name)
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
s.visitors[name] = oldVisitor
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
v, exists := s.visitors[name]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
||||
for _, p := range s.proxies {
|
||||
result = append(result, p)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
||||
for _, v := range s.visitors {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
144
pkg/config/source/store_test.go
Normal file
144
pkg/config/source/store_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright 2026 The frp Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
|
||||
func setDisallowUnknownFieldsForStoreTest(t *testing.T, value bool) func() {
|
||||
t.Helper()
|
||||
v1.DisallowUnknownFieldsMu.Lock()
|
||||
prev := v1.DisallowUnknownFields
|
||||
v1.DisallowUnknownFields = value
|
||||
v1.DisallowUnknownFieldsMu.Unlock()
|
||||
return func() {
|
||||
v1.DisallowUnknownFieldsMu.Lock()
|
||||
v1.DisallowUnknownFields = prev
|
||||
v1.DisallowUnknownFieldsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func getDisallowUnknownFieldsForStoreTest() bool {
|
||||
v1.DisallowUnknownFieldsMu.Lock()
|
||||
defer v1.DisallowUnknownFieldsMu.Unlock()
|
||||
return v1.DisallowUnknownFields
|
||||
}
|
||||
|
||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
err = storeSource.AddProxy(proxyCfg)
|
||||
require.NoError(err)
|
||||
err = storeSource.AddVisitor(visitorCfg)
|
||||
require.NoError(err)
|
||||
|
||||
gotProxy := storeSource.GetProxy("proxy1")
|
||||
require.NotNil(gotProxy)
|
||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||
|
||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||
require.NotNil(gotVisitor)
|
||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
|
||||
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
|
||||
proxyCfg := &v1.TCPProxyConfig{}
|
||||
proxyCfg.Name = "proxy1"
|
||||
proxyCfg.Type = "tcp"
|
||||
proxyCfg.LocalPort = 10080
|
||||
|
||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
||||
visitorCfg.Name = "visitor1"
|
||||
visitorCfg.Type = "xtcp"
|
||||
visitorCfg.ServerName = "server1"
|
||||
visitorCfg.SecretKey = "secret"
|
||||
visitorCfg.BindPort = 10081
|
||||
|
||||
stored := storeData{
|
||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
||||
}
|
||||
data, err := json.Marshal(stored)
|
||||
require.NoError(err)
|
||||
err = os.WriteFile(path, data, 0o600)
|
||||
require.NoError(err)
|
||||
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
gotProxy := storeSource.GetProxy("proxy1")
|
||||
require.NotNil(gotProxy)
|
||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
||||
|
||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
||||
require.NotNil(gotVisitor)
|
||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
|
||||
func TestStoreSource_LoadFromFile_UnknownFieldsNotAffectedByAmbientStrictness(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
restore := setDisallowUnknownFieldsForStoreTest(t, true)
|
||||
t.Cleanup(restore)
|
||||
|
||||
path := filepath.Join(t.TempDir(), "store.json")
|
||||
raw := []byte(`{
|
||||
"proxies": [
|
||||
{"name":"proxy1","type":"tcp","localPort":10080,"unexpected":"value"}
|
||||
],
|
||||
"visitors": [
|
||||
{"name":"visitor1","type":"xtcp","serverName":"server1","secretKey":"secret","bindPort":10081,"unexpected":"value"}
|
||||
]
|
||||
}`)
|
||||
err := os.WriteFile(path, raw, 0o600)
|
||||
require.NoError(err)
|
||||
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||
require.True(getDisallowUnknownFieldsForStoreTest())
|
||||
}
|
||||
Reference in New Issue
Block a user