Skip to content

Commit 2aac916

Browse files
authored
cmd/{containerboot,k8s-operator},kube/kubetypes: kube Ingress L7 proxies only advertise HTTPS endpoint when ready (tailscale#14171)
cmd/containerboot,kube/kubetypes,cmd/k8s-operator: detect if Ingress is created in a tailnet that has no HTTPS This attempts to make Kubernetes Operator L7 Ingress setup failures more explicit: - the Ingress resource now only advertises HTTPS endpoint via status.ingress.loadBalancer.hostname when/if the proxy has succesfully loaded serve config - the proxy attempts to catch cases where HTTPS is disabled for the tailnet and logs a warning Updates tailscale#12079 Updates tailscale#10407 Signed-off-by: Irbe Krumina <[email protected]>
1 parent aa43388 commit 2aac916

File tree

12 files changed

+443
-128
lines changed

12 files changed

+443
-128
lines changed

cmd/containerboot/kube.go

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,55 @@ import (
99
"context"
1010
"encoding/json"
1111
"fmt"
12-
"log"
1312
"net/http"
1413
"net/netip"
1514
"os"
1615

1716
"tailscale.com/kube/kubeapi"
1817
"tailscale.com/kube/kubeclient"
18+
"tailscale.com/kube/kubetypes"
1919
"tailscale.com/tailcfg"
2020
)
2121

22-
// storeDeviceID writes deviceID to 'device_id' data field of the named
23-
// Kubernetes Secret.
24-
func storeDeviceID(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID) error {
22+
// kubeClient is a wrapper around Tailscale's internal kube client that knows how to talk to the kube API server. We use
23+
// this rather than any of the upstream Kubernetes client libaries to avoid extra imports.
24+
type kubeClient struct {
25+
kubeclient.Client
26+
stateSecret string
27+
}
28+
29+
func newKubeClient(root string, stateSecret string) (*kubeClient, error) {
30+
if root != "/" {
31+
// If we are running in a test, we need to set the root path to the fake
32+
// service account directory.
33+
kubeclient.SetRootPathForTesting(root)
34+
}
35+
var err error
36+
kc, err := kubeclient.New("tailscale-container")
37+
if err != nil {
38+
return nil, fmt.Errorf("Error creating kube client: %w", err)
39+
}
40+
if (root != "/") || os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
41+
// Derive the API server address from the environment variables
42+
// Used to set http server in tests, or optionally enabled by flag
43+
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
44+
}
45+
return &kubeClient{Client: kc, stateSecret: stateSecret}, nil
46+
}
47+
48+
// storeDeviceID writes deviceID to 'device_id' data field of the client's state Secret.
49+
func (kc *kubeClient) storeDeviceID(ctx context.Context, deviceID tailcfg.StableNodeID) error {
2550
s := &kubeapi.Secret{
2651
Data: map[string][]byte{
27-
"device_id": []byte(deviceID),
52+
kubetypes.KeyDeviceID: []byte(deviceID),
2853
},
2954
}
30-
return kc.StrategicMergePatchSecret(ctx, secretName, s, "tailscale-container")
55+
return kc.StrategicMergePatchSecret(ctx, kc.stateSecret, s, "tailscale-container")
3156
}
3257

33-
// storeDeviceEndpoints writes device's tailnet IPs and MagicDNS name to fields
34-
// 'device_ips', 'device_fqdn' of the named Kubernetes Secret.
35-
func storeDeviceEndpoints(ctx context.Context, secretName string, fqdn string, addresses []netip.Prefix) error {
58+
// storeDeviceEndpoints writes device's tailnet IPs and MagicDNS name to fields 'device_ips', 'device_fqdn' of client's
59+
// state Secret.
60+
func (kc *kubeClient) storeDeviceEndpoints(ctx context.Context, fqdn string, addresses []netip.Prefix) error {
3661
var ips []string
3762
for _, addr := range addresses {
3863
ips = append(ips, addr.Addr().String())
@@ -44,24 +69,36 @@ func storeDeviceEndpoints(ctx context.Context, secretName string, fqdn string, a
4469

4570
s := &kubeapi.Secret{
4671
Data: map[string][]byte{
47-
"device_fqdn": []byte(fqdn),
48-
"device_ips": deviceIPs,
72+
kubetypes.KeyDeviceFQDN: []byte(fqdn),
73+
kubetypes.KeyDeviceIPs: deviceIPs,
4974
},
5075
}
51-
return kc.StrategicMergePatchSecret(ctx, secretName, s, "tailscale-container")
76+
return kc.StrategicMergePatchSecret(ctx, kc.stateSecret, s, "tailscale-container")
77+
}
78+
79+
// storeHTTPSEndpoint writes an HTTPS endpoint exposed by this device via 'tailscale serve' to the client's state
80+
// Secret. In practice this will be the same value that gets written to 'device_fqdn', but this should only be called
81+
// when the serve config has been successfully set up.
82+
func (kc *kubeClient) storeHTTPSEndpoint(ctx context.Context, ep string) error {
83+
s := &kubeapi.Secret{
84+
Data: map[string][]byte{
85+
kubetypes.KeyHTTPSEndpoint: []byte(ep),
86+
},
87+
}
88+
return kc.StrategicMergePatchSecret(ctx, kc.stateSecret, s, "tailscale-container")
5289
}
5390

5491
// deleteAuthKey deletes the 'authkey' field of the given kube
5592
// secret. No-op if there is no authkey in the secret.
56-
func deleteAuthKey(ctx context.Context, secretName string) error {
93+
func (kc *kubeClient) deleteAuthKey(ctx context.Context) error {
5794
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
5895
m := []kubeclient.JSONPatch{
5996
{
6097
Op: "remove",
6198
Path: "/data/authkey",
6299
},
63100
}
64-
if err := kc.JSONPatchResource(ctx, secretName, kubeclient.TypeSecrets, m); err != nil {
101+
if err := kc.JSONPatchResource(ctx, kc.stateSecret, kubeclient.TypeSecrets, m); err != nil {
65102
if s, ok := err.(*kubeapi.Status); ok && s.Code == http.StatusUnprocessableEntity {
66103
// This is kubernetes-ese for "the field you asked to
67104
// delete already doesn't exist", aka no-op.
@@ -72,22 +109,19 @@ func deleteAuthKey(ctx context.Context, secretName string) error {
72109
return nil
73110
}
74111

75-
var kc kubeclient.Client
76-
77-
func initKubeClient(root string) {
78-
if root != "/" {
79-
// If we are running in a test, we need to set the root path to the fake
80-
// service account directory.
81-
kubeclient.SetRootPathForTesting(root)
112+
// storeCapVerUID stores the current capability version of tailscale and, if provided, UID of the Pod in the tailscale
113+
// state Secret.
114+
// These two fields are used by the Kubernetes Operator to observe the current capability version of tailscaled running in this container.
115+
func (kc *kubeClient) storeCapVerUID(ctx context.Context, podUID string) error {
116+
capVerS := fmt.Sprintf("%d", tailcfg.CurrentCapabilityVersion)
117+
d := map[string][]byte{
118+
kubetypes.KeyCapVer: []byte(capVerS),
82119
}
83-
var err error
84-
kc, err = kubeclient.New("tailscale-container")
85-
if err != nil {
86-
log.Fatalf("Error creating kube client: %v", err)
120+
if podUID != "" {
121+
d[kubetypes.KeyPodUID] = []byte(podUID)
87122
}
88-
if (root != "/") || os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
89-
// Derive the API server address from the environment variables
90-
// Used to set http server in tests, or optionally enabled by flag
91-
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
123+
s := &kubeapi.Secret{
124+
Data: d,
92125
}
126+
return kc.StrategicMergePatchSecret(ctx, kc.stateSecret, s, "tailscale-container")
93127
}

cmd/containerboot/kube_test.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,22 @@ func TestSetupKube(t *testing.T) {
2121
cfg *settings
2222
wantErr bool
2323
wantCfg *settings
24-
kc kubeclient.Client
24+
kc *kubeClient
2525
}{
2626
{
2727
name: "TS_AUTHKEY set, state Secret exists",
2828
cfg: &settings{
2929
AuthKey: "foo",
3030
KubeSecret: "foo",
3131
},
32-
kc: &kubeclient.FakeClient{
32+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
3333
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
3434
return false, false, nil
3535
},
3636
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
3737
return nil, nil
3838
},
39-
},
39+
}},
4040
wantCfg: &settings{
4141
AuthKey: "foo",
4242
KubeSecret: "foo",
@@ -48,14 +48,14 @@ func TestSetupKube(t *testing.T) {
4848
AuthKey: "foo",
4949
KubeSecret: "foo",
5050
},
51-
kc: &kubeclient.FakeClient{
51+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
5252
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
5353
return false, true, nil
5454
},
5555
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
5656
return nil, &kubeapi.Status{Code: 404}
5757
},
58-
},
58+
}},
5959
wantCfg: &settings{
6060
AuthKey: "foo",
6161
KubeSecret: "foo",
@@ -67,14 +67,14 @@ func TestSetupKube(t *testing.T) {
6767
AuthKey: "foo",
6868
KubeSecret: "foo",
6969
},
70-
kc: &kubeclient.FakeClient{
70+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
7171
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
7272
return false, false, nil
7373
},
7474
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
7575
return nil, &kubeapi.Status{Code: 404}
7676
},
77-
},
77+
}},
7878
wantCfg: &settings{
7979
AuthKey: "foo",
8080
KubeSecret: "foo",
@@ -87,14 +87,14 @@ func TestSetupKube(t *testing.T) {
8787
AuthKey: "foo",
8888
KubeSecret: "foo",
8989
},
90-
kc: &kubeclient.FakeClient{
90+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
9191
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
9292
return false, false, nil
9393
},
9494
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
9595
return nil, &kubeapi.Status{Code: 403}
9696
},
97-
},
97+
}},
9898
wantCfg: &settings{
9999
AuthKey: "foo",
100100
KubeSecret: "foo",
@@ -111,11 +111,11 @@ func TestSetupKube(t *testing.T) {
111111
AuthKey: "foo",
112112
KubeSecret: "foo",
113113
},
114-
kc: &kubeclient.FakeClient{
114+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
115115
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
116116
return false, false, errors.New("broken")
117117
},
118-
},
118+
}},
119119
wantErr: true,
120120
},
121121
{
@@ -127,14 +127,14 @@ func TestSetupKube(t *testing.T) {
127127
wantCfg: &settings{
128128
KubeSecret: "foo",
129129
},
130-
kc: &kubeclient.FakeClient{
130+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
131131
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
132132
return false, true, nil
133133
},
134134
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
135135
return nil, &kubeapi.Status{Code: 404}
136136
},
137-
},
137+
}},
138138
},
139139
{
140140
// Interactive login using URL in Pod logs
@@ -145,28 +145,28 @@ func TestSetupKube(t *testing.T) {
145145
wantCfg: &settings{
146146
KubeSecret: "foo",
147147
},
148-
kc: &kubeclient.FakeClient{
148+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
149149
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
150150
return false, false, nil
151151
},
152152
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
153153
return &kubeapi.Secret{}, nil
154154
},
155-
},
155+
}},
156156
},
157157
{
158158
name: "TS_AUTHKEY not set, state Secret contains auth key, we do not have RBAC to patch it",
159159
cfg: &settings{
160160
KubeSecret: "foo",
161161
},
162-
kc: &kubeclient.FakeClient{
162+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
163163
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
164164
return false, false, nil
165165
},
166166
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
167167
return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
168168
},
169-
},
169+
}},
170170
wantCfg: &settings{
171171
KubeSecret: "foo",
172172
},
@@ -177,14 +177,14 @@ func TestSetupKube(t *testing.T) {
177177
cfg: &settings{
178178
KubeSecret: "foo",
179179
},
180-
kc: &kubeclient.FakeClient{
180+
kc: &kubeClient{stateSecret: "foo", Client: &kubeclient.FakeClient{
181181
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) {
182182
return true, false, nil
183183
},
184184
GetSecretImpl: func(context.Context, string) (*kubeapi.Secret, error) {
185185
return &kubeapi.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil
186186
},
187-
},
187+
}},
188188
wantCfg: &settings{
189189
KubeSecret: "foo",
190190
AuthKey: "foo",
@@ -194,9 +194,9 @@ func TestSetupKube(t *testing.T) {
194194
}
195195

196196
for _, tt := range tests {
197-
kc = tt.kc
197+
kc := tt.kc
198198
t.Run(tt.name, func(t *testing.T) {
199-
if err := tt.cfg.setupKube(context.Background()); (err != nil) != tt.wantErr {
199+
if err := tt.cfg.setupKube(context.Background(), kc); (err != nil) != tt.wantErr {
200200
t.Errorf("settings.setupKube() error = %v, wantErr %v", err, tt.wantErr)
201201
}
202202
if diff := cmp.Diff(*tt.cfg, *tt.wantCfg); diff != "" {

0 commit comments

Comments
 (0)