cmd/containerboot,kube,ipn/store/kubestore: allow interactive login on kube, check Secret create perms, allow empty state Secret (#11326)
cmd/containerboot,kube,ipn/store/kubestore: allow interactive login and empty state Secrets, check perms * Allow users to pre-create empty state Secrets * Add a fake internal kube client, test functionality that has dependencies on kube client operations. * Fix an issue where interactive login was not allowed in an edge case where state Secret does not exist * Make the CheckSecretPermissions method report whether we have permissions to create/patch a Secret if it's determined that these operations will be needed Updates tailscale/tailscale#11170 Signed-off-by: Irbe Krumina <irbe@tailscale.com>main
parent
1e6cdb7d86
commit
1452faf510
@ -0,0 +1,206 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"testing" |
||||
|
||||
"github.com/google/go-cmp/cmp" |
||||
"tailscale.com/kube" |
||||
) |
||||
|
||||
func TestSetupKube(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
cfg *settings |
||||
wantErr bool |
||||
wantCfg *settings |
||||
kc kube.Client |
||||
}{ |
||||
{ |
||||
name: "TS_AUTHKEY set, state Secret exists", |
||||
cfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return nil, nil |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY set, state Secret does not exist, we have permissions to create it", |
||||
cfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, true, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return nil, &kube.Status{Code: 404} |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY set, state Secret does not exist, we do not have permissions to create it", |
||||
cfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return nil, &kube.Status{Code: 404} |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to retrieve the state Secret", |
||||
cfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return nil, &kube.Status{Code: 403} |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY set, we encounter a non-404 error when trying to check Secret permissions", |
||||
cfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantCfg: &settings{ |
||||
AuthKey: "foo", |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, errors.New("broken") |
||||
}, |
||||
}, |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
// Interactive login using URL in Pod logs
|
||||
name: "TS_AUTHKEY not set, state Secret does not exist, we have permissions to create it", |
||||
cfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantCfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, true, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return nil, &kube.Status{Code: 404} |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
// Interactive login using URL in Pod logs
|
||||
name: "TS_AUTHKEY not set, state Secret exists, but does not contain auth key", |
||||
cfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantCfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return &kube.Secret{}, nil |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY not set, state Secret contains auth key, we do not have RBAC to patch it", |
||||
cfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return false, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return &kube.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
wantErr: true, |
||||
}, |
||||
{ |
||||
name: "TS_AUTHKEY not set, state Secret contains auth key, we have RBAC to patch it", |
||||
cfg: &settings{ |
||||
KubeSecret: "foo", |
||||
}, |
||||
kc: &kube.FakeClient{ |
||||
CheckSecretPermissionsImpl: func(context.Context, string) (bool, bool, error) { |
||||
return true, false, nil |
||||
}, |
||||
GetSecretImpl: func(context.Context, string) (*kube.Secret, error) { |
||||
return &kube.Secret{Data: map[string][]byte{"authkey": []byte("foo")}}, nil |
||||
}, |
||||
}, |
||||
wantCfg: &settings{ |
||||
KubeSecret: "foo", |
||||
AuthKey: "foo", |
||||
KubernetesCanPatch: true, |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
kc = tt.kc |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
if err := tt.cfg.setupKube(context.Background()); (err != nil) != tt.wantErr { |
||||
t.Errorf("settings.setupKube() error = %v, wantErr %v", err, tt.wantErr) |
||||
} |
||||
if diff := cmp.Diff(*tt.cfg, *tt.wantCfg); diff != "" { |
||||
t.Errorf("unexpected contents of settings after running settings.setupKube()\n(-got +want):\n%s", diff) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
@ -0,0 +1,37 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package kube provides a client to interact with Kubernetes.
|
||||
// This package is Tailscale-internal and not meant for external consumption.
|
||||
// Further, the API should not be considered stable.
|
||||
package kube |
||||
|
||||
import ( |
||||
"context" |
||||
"net" |
||||
) |
||||
|
||||
var _ Client = &FakeClient{} |
||||
|
||||
type FakeClient struct { |
||||
GetSecretImpl func(context.Context, string) (*Secret, error) |
||||
CheckSecretPermissionsImpl func(ctx context.Context, name string) (bool, bool, error) |
||||
} |
||||
|
||||
func (fc *FakeClient) CheckSecretPermissions(ctx context.Context, name string) (bool, bool, error) { |
||||
return fc.CheckSecretPermissionsImpl(ctx, name) |
||||
} |
||||
func (fc *FakeClient) GetSecret(ctx context.Context, name string) (*Secret, error) { |
||||
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) StrategicMergePatchSecret(context.Context, string, *Secret, string) error { |
||||
return nil |
||||
} |
||||
func (fc *FakeClient) JSONPatchSecret(context.Context, string, []JSONPatch) error { |
||||
return nil |
||||
} |
||||
func (fc *FakeClient) UpdateSecret(context.Context, *Secret) error { return nil } |
||||
func (fc *FakeClient) CreateSecret(context.Context, *Secret) error { return nil } |
||||
Loading…
Reference in new issue