mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-21 18:42:36 +00:00
cmd/{k8s-operator,k8s-proxy}: add kube-apiserver ProxyGroup type (#16266)
Adds a new k8s-proxy command to convert operator's in-process proxy to a separately deployable type of ProxyGroup: kube-apiserver. k8s-proxy reads in a new config file written by the operator, modelled on tailscaled's conffile but with some modifications to ensure multiple versions of the config can co-exist within a file. This should make it much easier to support reading that config file from a Kube Secret with a stable file name. To avoid needing to give the operator ClusterRole{,Binding} permissions, the helm chart now optionally deploys a new static ServiceAccount for the API Server proxy to use if in auth mode. Proxies deployed by kube-apiserver ProxyGroups currently work the same as the operator's in-process proxy. They do not yet leverage Tailscale Services for presenting a single HA DNS name. Updates #13358 Change-Id: Ib6ead69b2173c5e1929f3c13fb48a9a5362195d8 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
This commit is contained in:
101
kube/k8s-proxy/conf/conf.go
Normal file
101
kube/k8s-proxy/conf/conf.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// Package conf contains code to load, manipulate, and access config file
|
||||
// settings for k8s-proxy.
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
const v1Alpha1 = "v1alpha1"
|
||||
|
||||
// Config describes a config file.
|
||||
type Config struct {
|
||||
Path string // disk path of HuJSON
|
||||
Raw []byte // raw bytes from disk, in HuJSON form
|
||||
Std []byte // standardized JSON form
|
||||
Version string // "v1alpha1"
|
||||
|
||||
// Parsed is the parsed config, converted from its on-disk version to the
|
||||
// latest known format.
|
||||
Parsed ConfigV1Alpha1
|
||||
}
|
||||
|
||||
// VersionedConfig allows specifying config at the root of the object, or in
|
||||
// a versioned sub-object.
|
||||
// e.g. {"version": "v1alpha1", "authKey": "abc123"}
|
||||
// or {"version": "v1beta1", "a-beta-config": "a-beta-value", "v1alpha1": {"authKey": "abc123"}}
|
||||
type VersionedConfig struct {
|
||||
Version string `json:",omitempty"` // "v1alpha1"
|
||||
|
||||
// Latest version of the config.
|
||||
*ConfigV1Alpha1
|
||||
|
||||
// Backwards compatibility version(s) of the config. Fields and sub-fields
|
||||
// from here should only be added to, never changed in place.
|
||||
V1Alpha1 *ConfigV1Alpha1 `json:",omitempty"`
|
||||
// V1Beta1 *ConfigV1Beta1 `json:",omitempty"` // Not yet used.
|
||||
}
|
||||
|
||||
type ConfigV1Alpha1 struct {
|
||||
AuthKey *string `json:",omitempty"` // Tailscale auth key to use.
|
||||
Hostname *string `json:",omitempty"` // Tailscale device hostname.
|
||||
State *string `json:",omitempty"` // Path to the Tailscale state.
|
||||
LogLevel *string `json:",omitempty"` // "debug", "info". Defaults to "info".
|
||||
App *string `json:",omitempty"` // e.g. kubetypes.AppProxyGroupKubeAPIServer
|
||||
KubeAPIServer *KubeAPIServer `json:",omitempty"` // Config specific to the API Server proxy.
|
||||
}
|
||||
|
||||
type KubeAPIServer struct {
|
||||
AuthMode opt.Bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Load reads and parses the config file at the provided path on disk.
|
||||
func Load(path string) (c Config, err error) {
|
||||
c.Path = path
|
||||
|
||||
c.Raw, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("error reading config file %q: %w", path, err)
|
||||
}
|
||||
c.Std, err = hujson.Standardize(c.Raw)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("error parsing config file %q HuJSON/JSON: %w", path, err)
|
||||
}
|
||||
var ver VersionedConfig
|
||||
if err := json.Unmarshal(c.Std, &ver); err != nil {
|
||||
return c, fmt.Errorf("error parsing config file %q: %w", path, err)
|
||||
}
|
||||
rootV1Alpha1 := (ver.Version == v1Alpha1)
|
||||
backCompatV1Alpha1 := (ver.V1Alpha1 != nil)
|
||||
switch {
|
||||
case ver.Version == "":
|
||||
return c, fmt.Errorf("error parsing config file %q: no \"version\" field provided", path)
|
||||
case rootV1Alpha1 && backCompatV1Alpha1:
|
||||
// Exactly one of these should be set.
|
||||
return c, fmt.Errorf("error parsing config file %q: both root and v1alpha1 config provided", path)
|
||||
case rootV1Alpha1 != backCompatV1Alpha1:
|
||||
c.Version = v1Alpha1
|
||||
switch {
|
||||
case rootV1Alpha1 && ver.ConfigV1Alpha1 != nil:
|
||||
c.Parsed = *ver.ConfigV1Alpha1
|
||||
case backCompatV1Alpha1:
|
||||
c.Parsed = *ver.V1Alpha1
|
||||
default:
|
||||
c.Parsed = ConfigV1Alpha1{}
|
||||
}
|
||||
default:
|
||||
return c, fmt.Errorf("error parsing config file %q: unsupported \"version\" value %q; want \"%s\"", path, ver.Version, v1Alpha1)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
86
kube/k8s-proxy/conf/conf_test.go
Normal file
86
kube/k8s-proxy/conf/conf_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Test that the config file can be at the root of the object, or in a versioned sub-object.
|
||||
// or {"version": "v1beta1", "a-beta-config": "a-beta-value", "v1alpha1": {"authKey": "abc123"}}
|
||||
func TestVersionedConfig(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
inputConfig string
|
||||
expectedConfig ConfigV1Alpha1
|
||||
expectedError string
|
||||
}{
|
||||
"root_config_v1alpha1": {
|
||||
inputConfig: `{"version": "v1alpha1", "authKey": "abc123"}`,
|
||||
expectedConfig: ConfigV1Alpha1{AuthKey: ptr.To("abc123")},
|
||||
},
|
||||
"backwards_compat_v1alpha1_config": {
|
||||
// Client doesn't know about v1beta1, so it should read in v1alpha1.
|
||||
inputConfig: `{"version": "v1beta1", "beta-key": "beta-value", "authKey": "def456", "v1alpha1": {"authKey": "abc123"}}`,
|
||||
expectedConfig: ConfigV1Alpha1{AuthKey: ptr.To("abc123")},
|
||||
},
|
||||
"unknown_key_allowed": {
|
||||
// Adding new keys to the config doesn't require a version bump.
|
||||
inputConfig: `{"version": "v1alpha1", "unknown-key": "unknown-value", "authKey": "abc123"}`,
|
||||
expectedConfig: ConfigV1Alpha1{AuthKey: ptr.To("abc123")},
|
||||
},
|
||||
"version_only_no_authkey": {
|
||||
inputConfig: `{"version": "v1alpha1"}`,
|
||||
expectedConfig: ConfigV1Alpha1{},
|
||||
},
|
||||
"both_config_v1alpha1": {
|
||||
inputConfig: `{"version": "v1alpha1", "authKey": "abc123", "v1alpha1": {"authKey": "def456"}}`,
|
||||
expectedError: "both root and v1alpha1 config provided",
|
||||
},
|
||||
"empty_config": {
|
||||
inputConfig: `{}`,
|
||||
expectedError: `no "version" field provided`,
|
||||
},
|
||||
"v1beta1_without_backwards_compat": {
|
||||
inputConfig: `{"version": "v1beta1", "beta-key": "beta-value", "authKey": "def456"}`,
|
||||
expectedError: `unsupported "version" value "v1beta1"; want "v1alpha1"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(path, []byte(tc.inputConfig), 0644); err != nil {
|
||||
t.Fatalf("failed to write config file: %v", err)
|
||||
}
|
||||
cfg, err := Load(path)
|
||||
switch {
|
||||
case tc.expectedError == "" && err != nil:
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
case tc.expectedError != "":
|
||||
if err == nil {
|
||||
t.Fatalf("expected error %q, got nil", tc.expectedError)
|
||||
} else if !strings.Contains(err.Error(), tc.expectedError) {
|
||||
t.Fatalf("expected error %q, got %q", tc.expectedError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
if cfg.Version != "v1alpha1" {
|
||||
t.Fatalf("expected version %q, got %q", "v1alpha1", cfg.Version)
|
||||
}
|
||||
// Diff actual vs expected config.
|
||||
if diff := cmp.Diff(cfg.Parsed, tc.expectedConfig); diff != "" {
|
||||
t.Fatalf("Unexpected parsed config (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user