Update dependencies
This commit is contained in:
186
vendor/tailscale.com/ipn/store/awsstore/store_aws.go
generated
vendored
Normal file
186
vendor/tailscale.com/ipn/store/awsstore/store_aws.go
generated
vendored
Normal file
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !ts_omit_aws
|
||||
|
||||
// Package awsstore contains an ipn.StateStore implementation using AWS SSM.
|
||||
package awsstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/config"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||
ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
parameterNameRxStr = `^parameter(/.*)`
|
||||
)
|
||||
|
||||
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
||||
|
||||
// awsSSMClient is an interface allowing us to mock the couple of
|
||||
// API calls we are leveraging with the AWSStore provider
|
||||
type awsSSMClient interface {
|
||||
GetParameter(ctx context.Context,
|
||||
params *ssm.GetParameterInput,
|
||||
optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
|
||||
|
||||
PutParameter(ctx context.Context,
|
||||
params *ssm.PutParameterInput,
|
||||
optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
|
||||
}
|
||||
|
||||
// store is a store which leverages AWS SSM parameter store
|
||||
// to persist the state
|
||||
type awsStore struct {
|
||||
ssmClient awsSSMClient
|
||||
ssmARN arn.ARN
|
||||
|
||||
memory mem.Store
|
||||
}
|
||||
|
||||
// New returns a new ipn.StateStore using the AWS SSM storage
|
||||
// location given by ssmARN.
|
||||
//
|
||||
// Note that we store the entire store in a single parameter
|
||||
// key, therefore if the state is above 8kb, it can cause
|
||||
// Tailscaled to only only store new state in-memory and
|
||||
// restarting Tailscaled can fail until you delete your state
|
||||
// from the AWS Parameter Store.
|
||||
func New(_ logger.Logf, ssmARN string) (ipn.StateStore, error) {
|
||||
return newStore(ssmARN, nil)
|
||||
}
|
||||
|
||||
// newStore is NewStore, but for tests. If client is non-nil, it's
|
||||
// used instead of making one.
|
||||
func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
|
||||
s := &awsStore{
|
||||
ssmClient: client,
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
// Parse the ARN
|
||||
if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
|
||||
}
|
||||
|
||||
// Validate the ARN corresponds to the SSM service
|
||||
if s.ssmARN.Service != "ssm" {
|
||||
return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
|
||||
}
|
||||
|
||||
// Validate the ARN corresponds to a parameter store resource
|
||||
if !parameterNameRx.MatchString(s.ssmARN.Resource) {
|
||||
return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
|
||||
}
|
||||
|
||||
if s.ssmClient == nil {
|
||||
var cfg aws.Config
|
||||
if cfg, err = config.LoadDefaultConfig(
|
||||
context.TODO(),
|
||||
config.WithRegion(s.ssmARN.Region),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.ssmClient = ssm.NewFromConfig(cfg)
|
||||
}
|
||||
|
||||
// Hydrate cache with the potentially current state
|
||||
if err := s.LoadState(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
// LoadState attempts to read the state from AWS SSM parameter store key.
|
||||
func (s *awsStore) LoadState() error {
|
||||
param, err := s.ssmClient.GetParameter(
|
||||
context.TODO(),
|
||||
&ssm.GetParameterInput{
|
||||
Name: aws.String(s.ParameterName()),
|
||||
WithDecryption: aws.Bool(true),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
var pnf *ssmTypes.ParameterNotFound
|
||||
if errors.As(err, &pnf) {
|
||||
// Create the parameter as it does not exist yet
|
||||
// and return directly as it is defacto empty
|
||||
return s.persistState()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Load the content in-memory
|
||||
return s.memory.LoadFromJSON([]byte(*param.Parameter.Value))
|
||||
}
|
||||
|
||||
// ParameterName returns the parameter name extracted from
|
||||
// the provided ARN
|
||||
func (s *awsStore) ParameterName() (name string) {
|
||||
values := parameterNameRx.FindStringSubmatch(s.ssmARN.Resource)
|
||||
if len(values) == 2 {
|
||||
name = values[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// String returns the awsStore and the ARN of the SSM parameter store
|
||||
// configured to store the state
|
||||
func (s *awsStore) String() string { return fmt.Sprintf("awsStore(%q)", s.ssmARN.String()) }
|
||||
|
||||
// ReadState implements the Store interface.
|
||||
func (s *awsStore) ReadState(id ipn.StateKey) (bs []byte, err error) {
|
||||
return s.memory.ReadState(id)
|
||||
}
|
||||
|
||||
// WriteState implements the Store interface.
|
||||
func (s *awsStore) WriteState(id ipn.StateKey, bs []byte) (err error) {
|
||||
// Write the state in-memory
|
||||
if err = s.memory.WriteState(id, bs); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Persist the state in AWS SSM parameter store
|
||||
return s.persistState()
|
||||
}
|
||||
|
||||
// PersistState saves the states into the AWS SSM parameter store
|
||||
func (s *awsStore) persistState() error {
|
||||
// Generate JSON from in-memory cache
|
||||
bs, err := s.memory.ExportToJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store in AWS SSM parameter store.
|
||||
//
|
||||
// We use intelligent tiering so that when the state is below 4kb, it uses Standard tiering
|
||||
// which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
|
||||
// doubling the capacity to 8kb per the following docs:
|
||||
// https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
|
||||
_, err = s.ssmClient.PutParameter(
|
||||
context.TODO(),
|
||||
&ssm.PutParameterInput{
|
||||
Name: aws.String(s.ParameterName()),
|
||||
Value: aws.String(string(bs)),
|
||||
Overwrite: aws.Bool(true),
|
||||
Tier: ssmTypes.ParameterTierIntelligentTiering,
|
||||
Type: ssmTypes.ParameterTypeSecureString,
|
||||
},
|
||||
)
|
||||
return err
|
||||
}
|
||||
18
vendor/tailscale.com/ipn/store/awsstore/store_aws_stub.go
generated
vendored
Normal file
18
vendor/tailscale.com/ipn/store/awsstore/store_aws_stub.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !linux || ts_omit_aws
|
||||
|
||||
package awsstore
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func New(logger.Logf, string) (ipn.StateStore, error) {
|
||||
return nil, fmt.Errorf("AWS store is not supported on %v", runtime.GOOS)
|
||||
}
|
||||
139
vendor/tailscale.com/ipn/store/kubestore/store_kube.go
generated
vendored
Normal file
139
vendor/tailscale.com/ipn/store/kubestore/store_kube.go
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package kubestore contains an ipn.StateStore implementation using Kubernetes Secrets.
|
||||
package kubestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/kube/kubeapi"
|
||||
"tailscale.com/kube/kubeclient"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Store is an ipn.StateStore that uses a Kubernetes Secret for persistence.
|
||||
type Store struct {
|
||||
client kubeclient.Client
|
||||
canPatch bool
|
||||
secretName string
|
||||
}
|
||||
|
||||
// New returns a new Store that persists to the named secret.
|
||||
func New(_ logger.Logf, secretName string) (*Store, error) {
|
||||
c, err := kubeclient.New()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
|
||||
// Derive the API server address from the environment variables
|
||||
c.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
}
|
||||
canPatch, _, err := c.CheckSecretPermissions(context.Background(), secretName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Store{
|
||||
client: c,
|
||||
canPatch: canPatch,
|
||||
secretName: secretName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error)) {
|
||||
s.client.SetDialer(d)
|
||||
}
|
||||
|
||||
func (s *Store) String() string { return "kube.Store" }
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret, err := s.client.GetSecret(ctx, s.secretName)
|
||||
if err != nil {
|
||||
if st, ok := err.(*kubeapi.Status); ok && st.Code == 404 {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
b, ok := secret.Data[sanitizeKey(id)]
|
||||
if !ok {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func sanitizeKey(k ipn.StateKey) string {
|
||||
// The only valid characters in a Kubernetes secret key are alphanumeric, -,
|
||||
// _, and .
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == '.' {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, string(k))
|
||||
}
|
||||
|
||||
// WriteState implements the StateStore interface.
|
||||
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
secret, err := s.client.GetSecret(ctx, s.secretName)
|
||||
if err != nil {
|
||||
if kubeclient.IsNotFoundErr(err) {
|
||||
return s.client.CreateSecret(ctx, &kubeapi.Secret{
|
||||
TypeMeta: kubeapi.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Secret",
|
||||
},
|
||||
ObjectMeta: kubeapi.ObjectMeta{
|
||||
Name: s.secretName,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
sanitizeKey(id): bs,
|
||||
},
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
if s.canPatch {
|
||||
if len(secret.Data) == 0 { // if user has pre-created a blank Secret
|
||||
m := []kubeclient.JSONPatch{
|
||||
{
|
||||
Op: "add",
|
||||
Path: "/data",
|
||||
Value: map[string][]byte{sanitizeKey(id): bs},
|
||||
},
|
||||
}
|
||||
if err := s.client.JSONPatchSecret(ctx, s.secretName, m); err != nil {
|
||||
return fmt.Errorf("error patching Secret %s with a /data field: %v", s.secretName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
m := []kubeclient.JSONPatch{
|
||||
{
|
||||
Op: "add",
|
||||
Path: "/data/" + sanitizeKey(id),
|
||||
Value: bs,
|
||||
},
|
||||
}
|
||||
if err := s.client.JSONPatchSecret(ctx, s.secretName, m); err != nil {
|
||||
return fmt.Errorf("error patching Secret %s with /data/%s field", s.secretName, sanitizeKey(id))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
secret.Data[sanitizeKey(id)] = bs
|
||||
if err := s.client.UpdateSecret(ctx, secret); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
70
vendor/tailscale.com/ipn/store/mem/store_mem.go
generated
vendored
Normal file
70
vendor/tailscale.com/ipn/store/mem/store_mem.go
generated
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package mem provides an in-memory ipn.StateStore implementation.
|
||||
package mem
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// New returns a new Store.
|
||||
func New(logger.Logf, string) (ipn.StateStore, error) {
|
||||
return new(Store), nil
|
||||
}
|
||||
|
||||
// Store is an ipn.StateStore that keeps state in memory only.
|
||||
type Store struct {
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
cache map[ipn.StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *Store) String() string { return "mem.Store" }
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
func (s *Store) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
bs, ok := s.cache[id]
|
||||
if !ok {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// WriteState implements the StateStore interface.
|
||||
func (s *Store) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cache == nil {
|
||||
s.cache = map[ipn.StateKey][]byte{}
|
||||
}
|
||||
s.cache[id] = bytes.Clone(bs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadFromJSON attempts to unmarshal json content into the
|
||||
// in-memory cache.
|
||||
func (s *Store) LoadFromJSON(data []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return json.Unmarshal(data, &s.cache)
|
||||
}
|
||||
|
||||
// ExportToJSON exports the content of the cache to
|
||||
// JSON formatted []byte.
|
||||
func (s *Store) ExportToJSON() ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if len(s.cache) == 0 {
|
||||
// Avoid "null" serialization.
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return json.MarshalIndent(s.cache, "", " ")
|
||||
}
|
||||
18
vendor/tailscale.com/ipn/store/store_aws.go
generated
vendored
Normal file
18
vendor/tailscale.com/ipn/store/store_aws.go
generated
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (ts_aws || (linux && (arm64 || amd64))) && !ts_omit_aws
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"tailscale.com/ipn/store/awsstore"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAvailableExternalStores = append(registerAvailableExternalStores, registerAWSStore)
|
||||
}
|
||||
|
||||
func registerAWSStore() {
|
||||
Register("arn:", awsstore.New)
|
||||
}
|
||||
25
vendor/tailscale.com/ipn/store/store_kube.go
generated
vendored
Normal file
25
vendor/tailscale.com/ipn/store/store_kube.go
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build (ts_kube || (linux && (arm64 || amd64))) && !ts_omit_kube
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/kubestore"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerAvailableExternalStores = append(registerAvailableExternalStores, registerKubeStore)
|
||||
}
|
||||
|
||||
func registerKubeStore() {
|
||||
Register("kube:", func(logf logger.Logf, path string) (ipn.StateStore, error) {
|
||||
secretName := strings.TrimPrefix(path, "kube:")
|
||||
return kubestore.New(logf, secretName)
|
||||
})
|
||||
}
|
||||
188
vendor/tailscale.com/ipn/store/stores.go
generated
vendored
Normal file
188
vendor/tailscale.com/ipn/store/stores.go
generated
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package store provides various implementation of ipn.StateStore.
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// Provider returns a StateStore for the provided path.
|
||||
// The arg is of the form "prefix:rest", where prefix was previously registered with Register.
|
||||
type Provider func(logf logger.Logf, arg string) (ipn.StateStore, error)
|
||||
|
||||
var regOnce sync.Once
|
||||
|
||||
var registerAvailableExternalStores []func()
|
||||
|
||||
func registerDefaultStores() {
|
||||
Register("mem:", mem.New)
|
||||
|
||||
for _, f := range registerAvailableExternalStores {
|
||||
f()
|
||||
}
|
||||
}
|
||||
|
||||
var knownStores map[string]Provider
|
||||
|
||||
// New returns a StateStore based on the provided arg
|
||||
// and registered stores.
|
||||
// The arg is of the form "prefix:rest", where prefix was previously
|
||||
// registered with Register.
|
||||
//
|
||||
// By default the following stores are registered:
|
||||
//
|
||||
// - if the string begins with "mem:", the suffix
|
||||
// is ignored and an in-memory store is used.
|
||||
// - (Linux-only) if the string begins with "arn:",
|
||||
// the suffix an AWS ARN for an SSM.
|
||||
// - (Linux-only) if the string begins with "kube:",
|
||||
// the suffix is a Kubernetes secret name
|
||||
// - In all other cases, the path is treated as a filepath.
|
||||
func New(logf logger.Logf, path string) (ipn.StateStore, error) {
|
||||
regOnce.Do(registerDefaultStores)
|
||||
for prefix, sf := range knownStores {
|
||||
if strings.HasPrefix(path, prefix) {
|
||||
// We can't strip the prefix here as some NewStoreFunc (like arn:)
|
||||
// expect the prefix.
|
||||
return sf(logf, path)
|
||||
}
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
path = TryWindowsAppDataMigration(logf, path)
|
||||
}
|
||||
return NewFileStore(logf, path)
|
||||
}
|
||||
|
||||
// Register registers a prefix to be used for
|
||||
// NewStore. It panics if the prefix is empty, or if the
|
||||
// prefix is already registered.
|
||||
// The provided fn is called with the path passed to NewStore;
|
||||
// the prefix is not stripped.
|
||||
func Register(prefix string, fn Provider) {
|
||||
if len(prefix) == 0 {
|
||||
panic("prefix is empty")
|
||||
}
|
||||
if _, ok := knownStores[prefix]; ok {
|
||||
panic(fmt.Sprintf("%q already registered", prefix))
|
||||
}
|
||||
mak.Set(&knownStores, prefix, fn)
|
||||
}
|
||||
|
||||
// TryWindowsAppDataMigration attempts to copy the Windows state file
|
||||
// from its old location to the new location. (Issue 2856)
|
||||
//
|
||||
// Tailscale 1.14 and before stored state under %LocalAppData%
|
||||
// (usually "C:\WINDOWS\system32\config\systemprofile\AppData\Local"
|
||||
// when tailscaled.exe is running as a non-user system service).
|
||||
// However it is frequently cleared for almost any reason: Windows
|
||||
// updates, System Restore, even various System Cleaner utilities.
|
||||
//
|
||||
// Returns a string of the path to use for the state file.
|
||||
// This will be a fallback %LocalAppData% path if migration fails,
|
||||
// a %ProgramData% path otherwise.
|
||||
func TryWindowsAppDataMigration(logf logger.Logf, path string) string {
|
||||
if path != paths.DefaultTailscaledStateFile() {
|
||||
// If they're specifying a non-default path, just trust that they know
|
||||
// what they are doing.
|
||||
return path
|
||||
}
|
||||
oldFile := paths.LegacyStateFilePath()
|
||||
return paths.TryConfigFileMigration(logf, oldFile, path)
|
||||
}
|
||||
|
||||
// FileStore is a StateStore that uses a JSON file for persistence.
|
||||
type FileStore struct {
|
||||
path string
|
||||
|
||||
mu sync.RWMutex
|
||||
cache map[ipn.StateKey][]byte
|
||||
}
|
||||
|
||||
// Path returns the path that NewFileStore was called with.
|
||||
func (s *FileStore) Path() string { return s.path }
|
||||
|
||||
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
|
||||
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(logf logger.Logf, path string) (ipn.StateStore, error) {
|
||||
// We unconditionally call this to ensure that our perms are correct
|
||||
if err := paths.MkStateDir(filepath.Dir(path)); err != nil {
|
||||
return nil, fmt.Errorf("creating state directory: %w", err)
|
||||
}
|
||||
|
||||
bs, err := os.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
|
||||
if err == nil && len(bs) == 0 {
|
||||
logf("store.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Write out an initial file, to verify that we can write
|
||||
// to the path.
|
||||
if err = atomicfile.WriteFile(path, []byte("{}"), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileStore{
|
||||
path: path,
|
||||
cache: map[ipn.StateKey][]byte{},
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := &FileStore{
|
||||
path: path,
|
||||
cache: map[ipn.StateKey][]byte{},
|
||||
}
|
||||
if err := json.Unmarshal(bs, &ret.cache); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
func (s *FileStore) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
bs, ok := s.cache[id]
|
||||
if !ok {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return bs, nil
|
||||
}
|
||||
|
||||
// WriteState implements the StateStore interface.
|
||||
func (s *FileStore) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if bytes.Equal(s.cache[id], bs) {
|
||||
return nil
|
||||
}
|
||||
s.cache[id] = bytes.Clone(bs)
|
||||
bs, err := json.MarshalIndent(s.cache, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return atomicfile.WriteFile(s.path, bs, 0600)
|
||||
}
|
||||
Reference in New Issue
Block a user