mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
WIP: Support --config=kube:<secret-name>
This commit is contained in:
parent
f6d4d03355
commit
a51f41812b
@ -8,11 +8,9 @@
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
|
|
||||||
"tailscale.com/kube/kubeapi"
|
"tailscale.com/kube/kubeapi"
|
||||||
"tailscale.com/kube/kubeclient"
|
"tailscale.com/kube/kubeclient"
|
||||||
@ -85,9 +83,4 @@ func initKubeClient(root string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error creating kube client: %v", err)
|
log.Fatalf("Error creating kube client: %v", err)
|
||||||
}
|
}
|
||||||
if (root != "/") || os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
|
|
||||||
// Derive the API server address from the environment variables
|
|
||||||
// Used to set http server in tests, or optionally enabled by flag
|
|
||||||
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -96,13 +96,13 @@
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"iter"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@ -721,26 +721,26 @@ func tailscaledConfigFilePath() string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("error reading tailscaled config directory %q: %v", dir, err)
|
log.Fatalf("error reading tailscaled config directory %q: %v", dir, err)
|
||||||
}
|
}
|
||||||
maxCompatVer := tailcfg.CapabilityVersion(-1)
|
selectedFile := kubeutils.SelectConfigFileName(fileNames(fe))
|
||||||
for _, e := range fe {
|
if selectedFile == "" {
|
||||||
// We don't check if type if file as in most cases this will
|
|
||||||
// come from a mounted kube Secret, where the directory contents
|
|
||||||
// will be various symlinks.
|
|
||||||
if e.Type().IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cv, err := kubeutils.CapVerFromFileName(e.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("skipping file %q in tailscaled config directory %q: %v", e.Name(), dir, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cv > maxCompatVer && cv <= tailcfg.CurrentCapabilityVersion {
|
|
||||||
maxCompatVer = cv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if maxCompatVer == -1 {
|
|
||||||
log.Fatalf("no tailscaled config file found in %q for current capability version %q", dir, tailcfg.CurrentCapabilityVersion)
|
log.Fatalf("no tailscaled config file found in %q for current capability version %q", dir, tailcfg.CurrentCapabilityVersion)
|
||||||
}
|
}
|
||||||
log.Printf("Using tailscaled config file %q for capability version %q", maxCompatVer, tailcfg.CurrentCapabilityVersion)
|
log.Printf("Using tailscaled config file %q", selectedFile)
|
||||||
return path.Join(dir, kubeutils.TailscaledConfigFileName(maxCompatVer))
|
return filepath.Join(dir, selectedFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileNames(fe []fs.DirEntry) iter.Seq[string] {
|
||||||
|
return func(yield func(string) bool) {
|
||||||
|
for _, e := range fe {
|
||||||
|
// We don't check if type if file as in most cases this will
|
||||||
|
// come from a mounted kube Secret, where the directory contents
|
||||||
|
// will be various symlinks.
|
||||||
|
if e.Type().IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !yield(e.Name()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -459,6 +459,7 @@ func pgTailscaledConfig(pg *tsapi.ProxyGroup, class *tsapi.ProxyClass, idx int32
|
|||||||
conf.AuthKey = key
|
conf.AuthKey = key
|
||||||
}
|
}
|
||||||
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
||||||
|
capVerConfigs[0] = *conf // Becomes "tailscaled" key.
|
||||||
capVerConfigs[106] = *conf
|
capVerConfigs[106] = *conf
|
||||||
return capVerConfigs, nil
|
return capVerConfigs, nil
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
@ -359,19 +360,12 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
|||||||
return "", "", nil, fmt.Errorf("error calculating hash of tailscaled configs: %w", err)
|
return "", "", nil, fmt.Errorf("error calculating hash of tailscaled configs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
latest := tailcfg.CapabilityVersion(-1)
|
for capVer, cfg := range configs {
|
||||||
var latestConfig ipn.ConfigVAlpha
|
b, err := json.Marshal(cfg)
|
||||||
for key, val := range configs {
|
|
||||||
fn := tsoperator.TailscaledConfigFileName(key)
|
|
||||||
b, err := json.Marshal(val)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", nil, fmt.Errorf("error marshalling tailscaled config: %w", err)
|
return "", "", nil, fmt.Errorf("error marshalling tailscaled config: %w", err)
|
||||||
}
|
}
|
||||||
mak.Set(&secret.StringData, fn, string(b))
|
mak.Set(&secret.StringData, tsoperator.TailscaledConfigFileName(capVer), string(b))
|
||||||
if key > latest {
|
|
||||||
latest = key
|
|
||||||
latestConfig = val
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if stsC.ServeConfig != nil {
|
if stsC.ServeConfig != nil {
|
||||||
@ -383,12 +377,12 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if orig != nil {
|
if orig != nil {
|
||||||
logger.Debugf("patching the existing proxy Secret with tailscaled config %s", sanitizeConfigBytes(latestConfig))
|
logger.Debugf("patching the existing proxy Secret with tailscaled config %s", sanitizeConfigBytes(configs))
|
||||||
if err := a.Patch(ctx, secret, client.MergeFrom(orig)); err != nil {
|
if err := a.Patch(ctx, secret, client.MergeFrom(orig)); err != nil {
|
||||||
return "", "", nil, err
|
return "", "", nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debugf("creating a new Secret for the proxy with tailscaled config %s", sanitizeConfigBytes(latestConfig))
|
logger.Debugf("creating a new Secret for the proxy with tailscaled config %s", sanitizeConfigBytes(configs))
|
||||||
if err := a.Create(ctx, secret); err != nil {
|
if err := a.Create(ctx, secret); err != nil {
|
||||||
return "", "", nil, err
|
return "", "", nil, err
|
||||||
}
|
}
|
||||||
@ -396,13 +390,21 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
|||||||
return secret.Name, hash, configs, nil
|
return secret.Name, hash, configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitizeConfigBytes returns ipn.ConfigVAlpha in string form with redacted
|
// sanitizeConfigBytes returns latest ipn.ConfigVAlpha in string form with
|
||||||
// auth key.
|
// redacted auth key.
|
||||||
func sanitizeConfigBytes(c ipn.ConfigVAlpha) string {
|
func sanitizeConfigBytes(c tailscaledConfigs) string {
|
||||||
if c.AuthKey != nil {
|
maxCapVer := tailcfg.CapabilityVersion(-1)
|
||||||
c.AuthKey = ptr.To("**redacted**")
|
var latestConfig ipn.ConfigVAlpha
|
||||||
|
for capVer, cfg := range c {
|
||||||
|
if (capVer > maxCapVer && maxCapVer != 0) || capVer == 0 {
|
||||||
|
maxCapVer = capVer
|
||||||
|
latestConfig = cfg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sanitizedBytes, err := json.Marshal(c)
|
if latestConfig.AuthKey != nil {
|
||||||
|
latestConfig.AuthKey = ptr.To("**redacted**")
|
||||||
|
}
|
||||||
|
sanitizedBytes, err := json.Marshal(latestConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "invalid config"
|
return "invalid config"
|
||||||
}
|
}
|
||||||
@ -831,6 +833,7 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
|
|||||||
conf.AuthKey = key
|
conf.AuthKey = key
|
||||||
}
|
}
|
||||||
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
||||||
|
capVerConfigs[0] = *conf // Becomes "tailscaled" key.
|
||||||
capVerConfigs[95] = *conf
|
capVerConfigs[95] = *conf
|
||||||
// legacy config should not contain NoStatefulFiltering field.
|
// legacy config should not contain NoStatefulFiltering field.
|
||||||
conf.NoStatefulFiltering.Clear()
|
conf.NoStatefulFiltering.Clear()
|
||||||
@ -838,30 +841,16 @@ func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *co
|
|||||||
return capVerConfigs, nil
|
return capVerConfigs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authKeyFromSecret(s *corev1.Secret) (key *string, err error) {
|
func authKeyFromSecret(s *corev1.Secret) (*string, error) {
|
||||||
latest := tailcfg.CapabilityVersion(-1)
|
selectedKey := tsoperator.SelectConfigFileName(maps.Keys(s.Data))
|
||||||
latestStr := ""
|
|
||||||
for k, data := range s.Data {
|
|
||||||
// write to StringData, read from Data as StringData is write-only
|
|
||||||
if len(data) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v, err := tsoperator.CapVerFromFileName(k)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if v > latest {
|
|
||||||
latestStr = k
|
|
||||||
latest = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Allow for configs that don't contain an auth key. Perhaps
|
// Allow for configs that don't contain an auth key. Perhaps
|
||||||
// users have some mechanisms to delete them. Auth key is
|
// users have some mechanisms to delete them. Auth key is
|
||||||
// normally not needed after the initial login.
|
// normally not needed after the initial login.
|
||||||
if latestStr != "" {
|
if selectedKey == "" {
|
||||||
return readAuthKey(s, latestStr)
|
return nil, nil
|
||||||
}
|
}
|
||||||
return key, nil
|
|
||||||
|
return readAuthKey(s, selectedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldRetainAuthKey returns true if the state stored in a proxy's state Secret suggests that auth key should be
|
// shouldRetainAuthKey returns true if the state stored in a proxy's state Secret suggests that auth key should be
|
||||||
|
@ -119,7 +119,7 @@ func defaultPort() uint16 {
|
|||||||
tunname string
|
tunname string
|
||||||
|
|
||||||
cleanUp bool
|
cleanUp bool
|
||||||
confFile string // empty, file path, or "vm:user-data"
|
confFile string // empty, file path, or "vm:user-data", or "kube:<secret-name>"
|
||||||
debug string
|
debug string
|
||||||
port uint16
|
port uint16
|
||||||
statepath string
|
statepath string
|
||||||
@ -166,13 +166,13 @@ func main() {
|
|||||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||||
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||||
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use a Kubernetes Secret or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||||
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
||||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||||
flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2)")
|
flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2), or 'kube:<secret-name>' to read the '.data.tailscaled' key from a Kubernetes Secret")
|
||||||
|
|
||||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
||||||
beCLI()
|
beCLI()
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
@ -17,7 +18,7 @@
|
|||||||
|
|
||||||
// Config describes a config file.
|
// Config describes a config file.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Path string // disk path of HuJSON, or VMUserDataPath
|
Path string // disk path of HuJSON, or [VMUserDataPath], or kube:<secret-name>
|
||||||
Raw []byte // raw bytes from disk, in HuJSON form
|
Raw []byte // raw bytes from disk, in HuJSON form
|
||||||
Std []byte // standardized JSON form
|
Std []byte // standardized JSON form
|
||||||
Version string // "alpha0" for now
|
Version string // "alpha0" for now
|
||||||
@ -35,9 +36,16 @@ func (c *Config) WantRunning() bool {
|
|||||||
return c != nil && !c.Parsed.Enabled.EqualBool(false)
|
return c != nil && !c.Parsed.Enabled.EqualBool(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// VMUserDataPath is a sentinel value for Load to use to get the data
|
const (
|
||||||
// from the VM's metadata service's user-data field.
|
// VMUserDataPath is a sentinel value for Load to use to get the data
|
||||||
const VMUserDataPath = "vm:user-data"
|
// from the VM's metadata service's user-data field.
|
||||||
|
VMUserDataPath = "vm:user-data"
|
||||||
|
|
||||||
|
// kubePrefix indicates the config should be read from a Kubernetes Secret.
|
||||||
|
// The remaining string should be the name of the Secret within the same
|
||||||
|
// namespace as tailscaled's own pod.
|
||||||
|
kubePrefix = "kube:"
|
||||||
|
)
|
||||||
|
|
||||||
// Load reads and parses the config file at the provided path on disk.
|
// Load reads and parses the config file at the provided path on disk.
|
||||||
func Load(path string) (*Config, error) {
|
func Load(path string) (*Config, error) {
|
||||||
@ -45,9 +53,11 @@ func Load(path string) (*Config, error) {
|
|||||||
c.Path = path
|
c.Path = path
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch path {
|
switch {
|
||||||
case VMUserDataPath:
|
case path == VMUserDataPath:
|
||||||
c.Raw, err = readVMUserData()
|
c.Raw, err = readVMUserData()
|
||||||
|
case strings.HasPrefix(path, "kube:"):
|
||||||
|
c.Raw, err = readKubeSecret(strings.TrimPrefix(path, "kube:"))
|
||||||
default:
|
default:
|
||||||
c.Raw, err = os.ReadFile(path)
|
c.Raw, err = os.ReadFile(path)
|
||||||
}
|
}
|
||||||
@ -74,7 +84,9 @@ func Load(path string) (*Config, error) {
|
|||||||
c.Version = ver.Version
|
c.Version = ver.Version
|
||||||
|
|
||||||
jd := json.NewDecoder(bytes.NewReader(c.Std))
|
jd := json.NewDecoder(bytes.NewReader(c.Std))
|
||||||
jd.DisallowUnknownFields()
|
// Not not disallow unknown fields. Older clients need to be able to read
|
||||||
|
// newer configs to support version tearing between the creator and
|
||||||
|
// consumer of config files.
|
||||||
err = jd.Decode(&c.Parsed)
|
err = jd.Decode(&c.Parsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
return nil, fmt.Errorf("error parsing config file %s: %w", path, err)
|
||||||
|
32
ipn/conffile/kube.go
Normal file
32
ipn/conffile/kube.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package conffile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/kube/kubeclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readKubeSecret(name string) ([]byte, error) {
|
||||||
|
c, err := kubeclient.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
secret, err := c.GetSecret(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config from Secret %q: %w", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contents, ok := secret.Data["tailscaled.hujson"]; ok {
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return secret.Data["tailscaled"], nil
|
||||||
|
}
|
@ -8,7 +8,6 @@
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -31,10 +30,6 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
canPatch, _, err := c.CheckSecretPermissions(context.Background(), secretName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -41,10 +42,33 @@ func TailscaledConfigFileName(cap tailcfg.CapabilityVersion) string {
|
|||||||
// CapVerFromFileName parses the capability version from a tailscaled
|
// CapVerFromFileName parses the capability version from a tailscaled
|
||||||
// config file name previously generated by TailscaledConfigFileNameForCap.
|
// config file name previously generated by TailscaledConfigFileNameForCap.
|
||||||
func CapVerFromFileName(name string) (tailcfg.CapabilityVersion, error) {
|
func CapVerFromFileName(name string) (tailcfg.CapabilityVersion, error) {
|
||||||
if name == "tailscaled" {
|
switch name {
|
||||||
|
case "tailscaled", "tailscaled.hujson":
|
||||||
|
// Unversioned names.
|
||||||
return 0, nil
|
return 0, nil
|
||||||
|
default:
|
||||||
|
var cap tailcfg.CapabilityVersion
|
||||||
|
_, err := fmt.Sscanf(name, "cap-%d.hujson", &cap)
|
||||||
|
return cap, err
|
||||||
}
|
}
|
||||||
var cap tailcfg.CapabilityVersion
|
}
|
||||||
_, err := fmt.Sscanf(name, "cap-%d.hujson", &cap)
|
|
||||||
return cap, err
|
func SelectConfigFileName(files iter.Seq[string]) string {
|
||||||
|
maxCapVer := tailcfg.CapabilityVersion(-1)
|
||||||
|
var selectedName string
|
||||||
|
for fileName := range files {
|
||||||
|
capVer, err := CapVerFromFileName(fileName)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 0 is "unversioned" (by capability - there is still a version inside
|
||||||
|
// the config itself). Always prefer it to files that have a capability
|
||||||
|
// version.
|
||||||
|
if (capVer > maxCapVer && maxCapVer != 0) || capVer == 0 {
|
||||||
|
maxCapVer = capVer
|
||||||
|
selectedName = fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedName
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@
|
|||||||
const (
|
const (
|
||||||
saPath = "/var/run/secrets/kubernetes.io/serviceaccount"
|
saPath = "/var/run/secrets/kubernetes.io/serviceaccount"
|
||||||
defaultURL = "https://kubernetes.default.svc"
|
defaultURL = "https://kubernetes.default.svc"
|
||||||
|
envAPIHost = "KUBERNETES_SERVICE_HOST"
|
||||||
|
envAPIPort = "KUBERNETES_SERVICE_PORT_HTTPS"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rootPathForTests is set by tests to override the root path to the
|
// rootPathForTests is set by tests to override the root path to the
|
||||||
@ -61,7 +63,6 @@ type Client interface {
|
|||||||
JSONPatchSecret(context.Context, string, []JSONPatch) error
|
JSONPatchSecret(context.Context, string, []JSONPatch) error
|
||||||
CheckSecretPermissions(context.Context, string) (bool, bool, error)
|
CheckSecretPermissions(context.Context, string) (bool, bool, error)
|
||||||
SetDialer(dialer func(context.Context, string, string) (net.Conn, error))
|
SetDialer(dialer func(context.Context, string, string) (net.Conn, error))
|
||||||
SetURL(string)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
@ -87,8 +88,12 @@ func New() (Client, error) {
|
|||||||
if ok := cp.AppendCertsFromPEM(caCert); !ok {
|
if ok := cp.AppendCertsFromPEM(caCert); !ok {
|
||||||
return nil, fmt.Errorf("kube: error in creating root cert pool")
|
return nil, fmt.Errorf("kube: error in creating root cert pool")
|
||||||
}
|
}
|
||||||
|
url := defaultURL
|
||||||
|
if host, port := os.Getenv(envAPIHost), os.Getenv(envAPIPort); host != "" && port != "" {
|
||||||
|
url = fmt.Sprintf("https://%s:%s", host, port)
|
||||||
|
}
|
||||||
return &client{
|
return &client{
|
||||||
url: defaultURL,
|
url: url,
|
||||||
ns: string(ns),
|
ns: string(ns),
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
@ -100,12 +105,6 @@ func New() (Client, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetURL sets the URL to use for the Kubernetes API.
|
|
||||||
// This is used only for testing.
|
|
||||||
func (c *client) SetURL(url string) {
|
|
||||||
c.url = url
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetDialer sets the dialer to use when establishing a connection
|
// SetDialer sets the dialer to use when establishing a connection
|
||||||
// to the Kubernetes API server.
|
// to the Kubernetes API server.
|
||||||
func (c *client) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
func (c *client) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||||
|
@ -23,7 +23,6 @@ func (fc *FakeClient) CheckSecretPermissions(ctx context.Context, name string) (
|
|||||||
func (fc *FakeClient) GetSecret(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
func (fc *FakeClient) GetSecret(ctx context.Context, name string) (*kubeapi.Secret, error) {
|
||||||
return fc.GetSecretImpl(ctx, name)
|
return fc.GetSecretImpl(ctx, name)
|
||||||
}
|
}
|
||||||
func (fc *FakeClient) SetURL(_ string) {}
|
|
||||||
func (fc *FakeClient) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
func (fc *FakeClient) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||||
}
|
}
|
||||||
func (fc *FakeClient) StrategicMergePatchSecret(context.Context, string, *kubeapi.Secret, string) error {
|
func (fc *FakeClient) StrategicMergePatchSecret(context.Context, string, *kubeapi.Secret, string) error {
|
||||||
|
Loading…
Reference in New Issue
Block a user