mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
e0d291ab8a
For stores like k8s secrets we need to dial out to the k8s API as though Tailscale wasn't running. The issue currently only manifests when you try to use an exit node while running inside a k8s cluster and are trying to use Kubernetes secrets as the backing store. This doesn't address cmd/containerboot, which I'll do in a follow up. Updates #7695 Signed-off-by: Maisem Ali <maisem@tailscale.com>
102 lines
2.4 KiB
Go
102 lines
2.4 KiB
Go
// 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"
|
|
"net"
|
|
"strings"
|
|
"time"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/kube"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
// Store is an ipn.StateStore that uses a Kubernetes Secret for persistence.
|
|
type Store struct {
|
|
client *kube.Client
|
|
secretName string
|
|
}
|
|
|
|
// New returns a new Store that persists to the named secret.
|
|
func New(_ logger.Logf, secretName string) (*Store, error) {
|
|
c, err := kube.New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Store{
|
|
client: c,
|
|
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.(*kube.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 st, ok := err.(*kube.Status); ok && st.Code == 404 {
|
|
return s.client.CreateSecret(ctx, &kube.Secret{
|
|
TypeMeta: kube.TypeMeta{
|
|
APIVersion: "v1",
|
|
Kind: "Secret",
|
|
},
|
|
ObjectMeta: kube.ObjectMeta{
|
|
Name: s.secretName,
|
|
},
|
|
Data: map[string][]byte{
|
|
sanitizeKey(id): bs,
|
|
},
|
|
})
|
|
}
|
|
return err
|
|
}
|
|
secret.Data[sanitizeKey(id)] = bs
|
|
if err := s.client.UpdateSecret(ctx, secret); err != nil {
|
|
return err
|
|
}
|
|
return err
|
|
}
|