ipn/ipnlocal: allow retrieval of serve config ETags from local API
This change adds API to ipn.LocalBackend to retrieve the ETag when querying for the current serve config. This allows consumers of ipn.LocalBackend.SetServeConfig to utilize the concurrency control offered by ETags. Previous to this change, utilizing serve config ETags required copying the local backend's internal ETag calcuation. The local API server was previously copying the local backend's ETag calculation as described above. With this change, the local API server now uses the new ETag retrieval function instead. Serve config ETags are therefore now opaque to clients, in line with best practices. Fixes tailscale/corp#35857 Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
+17
-22
@@ -9,9 +9,7 @@ import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -222,16 +220,6 @@ func TestGetServeHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func getEtag(t *testing.T, b any) string {
|
||||
t.Helper()
|
||||
bts, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256(bts)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// TestServeConfigForeground tests the inter-dependency
|
||||
// between a ServeConfig and a WatchIPNBus:
|
||||
// 1. Creating a WatchIPNBus returns a sessionID, that
|
||||
@@ -544,8 +532,14 @@ func TestServeConfigServices(t *testing.T) {
|
||||
func TestServeConfigETag(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
|
||||
// a nil config with initial etag should succeed
|
||||
err := b.SetServeConfig(nil, getEtag(t, nil))
|
||||
// the etag should be valid even when there is no config
|
||||
_, emptyStateETag, err := b.ServeConfigETag()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// a nil config with the empty-state etag should succeed
|
||||
err = b.SetServeConfig(nil, emptyStateETag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -556,7 +550,7 @@ func TestServeConfigETag(t *testing.T) {
|
||||
t.Fatal("expected an error but got nil")
|
||||
}
|
||||
|
||||
// a new config with no etag should succeed
|
||||
// a new config with the empty-state etag should succeed
|
||||
conf := &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
@@ -564,15 +558,14 @@ func TestServeConfigETag(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
}
|
||||
err = b.SetServeConfig(conf, getEtag(t, nil))
|
||||
err = b.SetServeConfig(conf, emptyStateETag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
confView := b.ServeConfig()
|
||||
etag := getEtag(t, confView)
|
||||
if etag == "" {
|
||||
t.Fatal("expected to get an etag but got an empty string")
|
||||
confView, etag, err := b.ServeConfigETag()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf = confView.AsStruct()
|
||||
mak.Set(&conf.AllowFunnel, "example.ts.net:443", true)
|
||||
@@ -596,8 +589,10 @@ func TestServeConfigETag(t *testing.T) {
|
||||
}
|
||||
|
||||
// replacing an existing config with the new etag should succeed
|
||||
newCfg := b.ServeConfig()
|
||||
etag = getEtag(t, newCfg)
|
||||
_, etag, err = b.ServeConfigETag()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = b.SetServeConfig(nil, etag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user