tailscale/ipn/store/kubestore/store_kube.go
Maisem Ali e0d291ab8a ipn/store: add support for stores to hook into a custom dialer
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>
2023-03-29 16:35:46 -07:00

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
}