mirror of
https://github.com/tailscale/tailscale.git
synced 2025-07-29 23:33:45 +00:00

Adds a new reconciler for ProxyGroups of type kube-apiserver that will provision a Tailscale Service for each replica to advertise. Adds two new condition types to the ProxyGroup, TailscaleServiceValid and TailscaleServiceConfigured, to post updates on the state of that reconciler in a way that's consistent with the service-pg reconciler. The created Tailscale Service name is configurable via a new ProxyGroup field spec.kubeAPISserver.ServiceName, which expects a string of the form "svc:<dns-label>". Lots of supporting changes were needed to implement this in a way that's consistent with other operator workflows, including: * Pulled containerboot's ensureServicesUnadvertised and certManager into kube/ libraries to be shared with k8s-proxy. Use those in k8s-proxy to aid Service cert sharing between replicas and graceful Service shutdown. * For certManager, add an initial wait to the cert loop to wait until the domain appears in the devices's netmap to avoid a guaranteed error on the first issue attempt when it's quick to start. * Made several methods in ingress-for-pg.go and svc-for-pg.go into functions to share with the new reconciler * Added a Resource struct to the owner refs stored in Tailscale Service annotations to be able to distinguish between Ingress- and ProxyGroup- based Services that need cleaning up in the Tailscale API. * Added a ListVIPServices method to the internal tailscale client to aid cleaning up orphaned Services * Support for reading config from a kube Secret, and partial support for config reloading, to prevent us having to force Pod restarts when config changes. * Fixed up the zap logger so it's possible to set debug log level. Updates #13358 Change-Id: Ia9607441157dd91fb9b6ecbc318eecbef446e116 Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
80 lines
2.6 KiB
Go
80 lines
2.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !plan9
|
|
|
|
package conf
|
|
|
|
import (
|
|
"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) {
|
|
cfg, err := Load([]byte(tc.inputConfig))
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|