mirror of https://github.com/tailscale/tailscale/
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>ox/corp-19592
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