@ -4,33 +4,37 @@
package kubestore
package kubestore
import (
import (
"bytes"
"context"
"context"
"encoding/json"
"fmt"
"strings"
"strings"
"testing"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/ipn/store/mem"
"tailscale.com/kube/kubeapi"
"tailscale.com/kube/kubeapi"
"tailscale.com/kube/kubeclient"
"tailscale.com/kube/kubeclient"
)
)
func Test UpdateStateSecret ( t * testing . T ) {
func Test WriteState ( t * testing . T ) {
tests := [ ] struct {
tests := [ ] struct {
name string
name string
initial map [ string ] [ ] byte
initial map [ string ] [ ] byte
updates map [ string ] [ ] byte
key ipn . StateKey
value [ ] byte
wantData map [ string ] [ ] byte
wantData map [ string ] [ ] byte
allowPatch bool
allowPatch bool
} {
} {
{
{
name : "basic_ upda te",
name : "basic_ wri te",
initial : map [ string ] [ ] byte {
initial : map [ string ] [ ] byte {
"existing" : [ ] byte ( "old" ) ,
"existing" : [ ] byte ( "old" ) ,
} ,
} ,
updates : map [ string ] [ ] byte {
key : "foo" ,
"foo" : [ ] byte ( "bar" ) ,
value : [ ] byte ( "bar" ) ,
} ,
wantData : map [ string ] [ ] byte {
wantData : map [ string ] [ ] byte {
"existing" : [ ] byte ( "old" ) ,
"existing" : [ ] byte ( "old" ) ,
"foo" : [ ] byte ( "bar" ) ,
"foo" : [ ] byte ( "bar" ) ,
@ -42,35 +46,17 @@ func TestUpdateStateSecret(t *testing.T) {
initial : map [ string ] [ ] byte {
initial : map [ string ] [ ] byte {
"foo" : [ ] byte ( "old" ) ,
"foo" : [ ] byte ( "old" ) ,
} ,
} ,
updates : map [ string ] [ ] byte {
key : "foo" ,
"foo" : [ ] byte ( "new" ) ,
value : [ ] byte ( "new" ) ,
} ,
wantData : map [ string ] [ ] byte {
wantData : map [ string ] [ ] byte {
"foo" : [ ] byte ( "new" ) ,
"foo" : [ ] byte ( "new" ) ,
} ,
} ,
allowPatch : true ,
allowPatch : true ,
} ,
} ,
{
{
name : "multiple_updates" ,
name : "create_new_secret" ,
initial : map [ string ] [ ] byte {
key : "foo" ,
"keep" : [ ] byte ( "keep" ) ,
value : [ ] byte ( "bar" ) ,
} ,
updates : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"baz" : [ ] byte ( "qux" ) ,
} ,
wantData : map [ string ] [ ] byte {
"keep" : [ ] byte ( "keep" ) ,
"foo" : [ ] byte ( "bar" ) ,
"baz" : [ ] byte ( "qux" ) ,
} ,
allowPatch : true ,
} ,
{
name : "create_new_secret" ,
updates : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
wantData : map [ string ] [ ] byte {
wantData : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"foo" : [ ] byte ( "bar" ) ,
} ,
} ,
@ -81,29 +67,23 @@ func TestUpdateStateSecret(t *testing.T) {
initial : map [ string ] [ ] byte {
initial : map [ string ] [ ] byte {
"foo" : [ ] byte ( "old" ) ,
"foo" : [ ] byte ( "old" ) ,
} ,
} ,
updates : map [ string ] [ ] byte {
key : "foo" ,
"foo" : [ ] byte ( "new" ) ,
value : [ ] byte ( "new" ) ,
} ,
wantData : map [ string ] [ ] byte {
wantData : map [ string ] [ ] byte {
"foo" : [ ] byte ( "new" ) ,
"foo" : [ ] byte ( "new" ) ,
} ,
} ,
allowPatch : false ,
allowPatch : false ,
} ,
} ,
{
{
name : "sanitize_key s ",
name : "sanitize_key ",
initial : map [ string ] [ ] byte {
initial : map [ string ] [ ] byte {
"clean-key" : [ ] byte ( "old" ) ,
"clean-key" : [ ] byte ( "old" ) ,
} ,
} ,
updates : map [ string ] [ ] byte {
key : "dirty@key" ,
"dirty@key" : [ ] byte ( "new" ) ,
value : [ ] byte ( "new" ) ,
"also/bad" : [ ] byte ( "value" ) ,
"good.key" : [ ] byte ( "keep" ) ,
} ,
wantData : map [ string ] [ ] byte {
wantData : map [ string ] [ ] byte {
"clean-key" : [ ] byte ( "old" ) ,
"clean-key" : [ ] byte ( "old" ) ,
"dirty_key" : [ ] byte ( "new" ) ,
"dirty_key" : [ ] byte ( "new" ) ,
"also_bad" : [ ] byte ( "value" ) ,
"good.key" : [ ] byte ( "keep" ) ,
} ,
} ,
allowPatch : true ,
allowPatch : true ,
} ,
} ,
@ -152,13 +132,13 @@ func TestUpdateStateSecret(t *testing.T) {
s := & Store {
s := & Store {
client : client ,
client : client ,
canPatch : tt . allowPatch ,
canPatch : tt . allowPatch ,
secretName : "t e st -secret ",
secretName : "t s-stat e",
memory : mem . Store { } ,
memory : mem . Store { } ,
}
}
err := s . updateStateSecret( tt . updates )
err := s . WriteState( tt . key , tt . value )
if err != nil {
if err != nil {
t . Errorf ( " updateStateSecret () error = %v", err )
t . Errorf ( " WriteState () error = %v", err )
return
return
}
}
@ -168,16 +148,576 @@ func TestUpdateStateSecret(t *testing.T) {
}
}
// Verify memory store was updated
// Verify memory store was updated
for k , v := range tt . updates {
got , err := s . memory . ReadState ( ipn . StateKey ( sanitizeKey ( string ( tt . key ) ) ) )
got , err := s . memory . ReadState ( ipn . StateKey ( sanitizeKey ( k ) ) )
if err != nil {
t . Errorf ( "reading from memory store: %v" , err )
}
if ! cmp . Equal ( got , tt . value ) {
t . Errorf ( "memory store key %q = %v, want %v" , tt . key , got , tt . value )
}
} )
}
}
func TestWriteTLSCertAndKey ( t * testing . T ) {
const (
testDomain = "my-app.tailnetxyz.ts.net"
testCert = "fake-cert"
testKey = "fake-key"
)
tests := [ ] struct {
name string
initial map [ string ] [ ] byte // pre-existing cert and key
certShareMode string
allowPatch bool // whether client can patch the Secret
wantSecretName string // name of the Secret where cert and key should be written
wantSecretData map [ string ] [ ] byte
wantMemoryStore map [ ipn . StateKey ] [ ] byte
} {
{
name : "basic_write" ,
initial : map [ string ] [ ] byte {
"existing" : [ ] byte ( "old" ) ,
} ,
allowPatch : true ,
wantSecretName : "ts-state" ,
wantSecretData : map [ string ] [ ] byte {
"existing" : [ ] byte ( "old" ) ,
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "cert_share_mode_write" ,
certShareMode : "rw" ,
allowPatch : true ,
wantSecretName : "my-app.tailnetxyz.ts.net" ,
wantSecretData : map [ string ] [ ] byte {
"tls.crt" : [ ] byte ( testCert ) ,
"tls.key" : [ ] byte ( testKey ) ,
} ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "cert_share_mode_write_update_existing" ,
initial : map [ string ] [ ] byte {
"tls.crt" : [ ] byte ( "old-cert" ) ,
"tls.key" : [ ] byte ( "old-key" ) ,
} ,
certShareMode : "rw" ,
allowPatch : true ,
wantSecretName : "my-app.tailnetxyz.ts.net" ,
wantSecretData : map [ string ] [ ] byte {
"tls.crt" : [ ] byte ( testCert ) ,
"tls.key" : [ ] byte ( testKey ) ,
} ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "update_existing" ,
initial : map [ string ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( "old-cert" ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( "old-key" ) ,
} ,
certShareMode : "" ,
allowPatch : true ,
wantSecretName : "ts-state" ,
wantSecretData : map [ string ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "patch_denied" ,
certShareMode : "" ,
allowPatch : false ,
wantSecretName : "ts-state" ,
wantSecretData : map [ string ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
// Set POD_NAME for testing selectors
envknob . Setenv ( "POD_NAME" , "ingress-proxies-1" )
defer envknob . Setenv ( "POD_NAME" , "" )
secret := tt . initial // track current state
client := & kubeclient . FakeClient {
GetSecretImpl : func ( ctx context . Context , name string ) ( * kubeapi . Secret , error ) {
if secret == nil {
return nil , & kubeapi . Status { Code : 404 }
}
return & kubeapi . Secret { Data : secret } , nil
} ,
CheckSecretPermissionsImpl : func ( ctx context . Context , name string ) ( bool , bool , error ) {
return tt . allowPatch , true , nil
} ,
CreateSecretImpl : func ( ctx context . Context , s * kubeapi . Secret ) error {
if s . Name != tt . wantSecretName {
t . Errorf ( "CreateSecret called with wrong name, got %q, want %q" , s . Name , tt . wantSecretName )
}
secret = s . Data
return nil
} ,
UpdateSecretImpl : func ( ctx context . Context , s * kubeapi . Secret ) error {
if s . Name != tt . wantSecretName {
t . Errorf ( "UpdateSecret called with wrong name, got %q, want %q" , s . Name , tt . wantSecretName )
}
secret = s . Data
return nil
} ,
JSONPatchResourceImpl : func ( ctx context . Context , name , resourceType string , patches [ ] kubeclient . JSONPatch ) error {
if ! tt . allowPatch {
return & kubeapi . Status { Reason : "Forbidden" }
}
if name != tt . wantSecretName {
t . Errorf ( "JSONPatchResource called with wrong name, got %q, want %q" , name , tt . wantSecretName )
}
if secret == nil {
secret = make ( map [ string ] [ ] byte )
}
for _ , p := range patches {
if p . Op == "add" && p . Path == "/data" {
secret = p . Value . ( map [ string ] [ ] byte )
} else if p . Op == "add" && strings . HasPrefix ( p . Path , "/data/" ) {
key := strings . TrimPrefix ( p . Path , "/data/" )
secret [ key ] = p . Value . ( [ ] byte )
}
}
return nil
} ,
}
s := & Store {
client : client ,
canPatch : tt . allowPatch ,
secretName : tt . wantSecretName ,
certShareMode : tt . certShareMode ,
memory : mem . Store { } ,
}
err := s . WriteTLSCertAndKey ( testDomain , [ ] byte ( testCert ) , [ ] byte ( testKey ) )
if err != nil {
t . Errorf ( "WriteTLSCertAndKey() error = '%v'" , err )
return
}
// Verify secret data
if diff := cmp . Diff ( secret , tt . wantSecretData ) ; diff != "" {
t . Errorf ( "secret data mismatch (-got +want):\n%s" , diff )
}
// Verify memory store was updated
for key , want := range tt . wantMemoryStore {
got , err := s . memory . ReadState ( key )
if err != nil {
if err != nil {
t . Errorf ( "reading from memory store: %v" , err )
t . Errorf ( "reading from memory store: %v" , err )
continue
continue
}
}
if ! cmp . Equal ( got , v ) {
if ! cmp . Equal ( got , want ) {
t . Errorf ( "memory store key %q = %v, want %v" , k , got , v )
t . Errorf ( "memory store key %q = %v, want %v" , k ey, got , want )
}
}
}
}
} )
} )
}
}
}
}
func TestReadTLSCertAndKey ( t * testing . T ) {
const (
testDomain = "my-app.tailnetxyz.ts.net"
testCert = "fake-cert"
testKey = "fake-key"
)
tests := [ ] struct {
name string
memoryStore map [ ipn . StateKey ] [ ] byte // pre-existing memory store state
certShareMode string
domain string
secretData map [ string ] [ ] byte // data to return from mock GetSecret
secretGetErr error // error to return from mock GetSecret
wantCert [ ] byte
wantKey [ ] byte
wantErr error
// what should end up in memory store after the store is created
wantMemoryStore map [ ipn . StateKey ] [ ] byte
} {
{
name : "found" ,
memoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
domain : testDomain ,
wantCert : [ ] byte ( testCert ) ,
wantKey : [ ] byte ( testKey ) ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "not_found" ,
domain : testDomain ,
wantErr : ipn . ErrStateNotExist ,
} ,
{
name : "cert_share_ro_mode_found_in_secret" ,
certShareMode : "ro" ,
domain : testDomain ,
secretData : map [ string ] [ ] byte {
"tls.crt" : [ ] byte ( testCert ) ,
"tls.key" : [ ] byte ( testKey ) ,
} ,
wantCert : [ ] byte ( testCert ) ,
wantKey : [ ] byte ( testKey ) ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "cert_share_ro_mode_found_in_memory" ,
certShareMode : "ro" ,
memoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
domain : testDomain ,
wantCert : [ ] byte ( testCert ) ,
wantKey : [ ] byte ( testKey ) ,
wantMemoryStore : map [ ipn . StateKey ] [ ] byte {
"my-app.tailnetxyz.ts.net.crt" : [ ] byte ( testCert ) ,
"my-app.tailnetxyz.ts.net.key" : [ ] byte ( testKey ) ,
} ,
} ,
{
name : "cert_share_ro_mode_not_found" ,
certShareMode : "ro" ,
domain : testDomain ,
secretGetErr : & kubeapi . Status { Code : 404 } ,
wantErr : ipn . ErrStateNotExist ,
} ,
{
name : "cert_share_ro_mode_empty_cert_in_secret" ,
certShareMode : "ro" ,
domain : testDomain ,
secretData : map [ string ] [ ] byte {
"tls.crt" : { } ,
"tls.key" : [ ] byte ( testKey ) ,
} ,
wantErr : ipn . ErrStateNotExist ,
} ,
{
name : "cert_share_ro_mode_kube_api_error" ,
certShareMode : "ro" ,
domain : testDomain ,
secretGetErr : fmt . Errorf ( "api error" ) ,
wantErr : fmt . Errorf ( "getting TLS Secret %q: api error" , sanitizeKey ( testDomain ) ) ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
client := & kubeclient . FakeClient {
GetSecretImpl : func ( ctx context . Context , name string ) ( * kubeapi . Secret , error ) {
if tt . secretGetErr != nil {
return nil , tt . secretGetErr
}
return & kubeapi . Secret { Data : tt . secretData } , nil
} ,
}
s := & Store {
client : client ,
secretName : "ts-state" ,
certShareMode : tt . certShareMode ,
memory : mem . Store { } ,
}
// Initialize memory store
for k , v := range tt . memoryStore {
s . memory . WriteState ( k , v )
}
gotCert , gotKey , err := s . ReadTLSCertAndKey ( tt . domain )
if tt . wantErr != nil {
if err == nil {
t . Errorf ( "ReadTLSCertAndKey() error = nil, want error containing %v" , tt . wantErr )
return
}
if ! strings . Contains ( err . Error ( ) , tt . wantErr . Error ( ) ) {
t . Errorf ( "ReadTLSCertAndKey() error = %v, want error containing %v" , err , tt . wantErr )
}
return
}
if err != nil {
t . Errorf ( "ReadTLSCertAndKey() unexpected error: %v" , err )
return
}
if ! bytes . Equal ( gotCert , tt . wantCert ) {
t . Errorf ( "ReadTLSCertAndKey() gotCert = %v, want %v" , gotCert , tt . wantCert )
}
if ! bytes . Equal ( gotKey , tt . wantKey ) {
t . Errorf ( "ReadTLSCertAndKey() gotKey = %v, want %v" , gotKey , tt . wantKey )
}
// Verify memory store contents after operation
if tt . wantMemoryStore != nil {
for key , want := range tt . wantMemoryStore {
got , err := s . memory . ReadState ( key )
if err != nil {
t . Errorf ( "reading from memory store: %v" , err )
continue
}
if ! bytes . Equal ( got , want ) {
t . Errorf ( "memory store key %q = %v, want %v" , key , got , want )
}
}
}
} )
}
}
func TestNewWithClient ( t * testing . T ) {
const (
secretName = "ts-state"
testCert = "fake-cert"
testKey = "fake-key"
)
certSecretsLabels := map [ string ] string {
"tailscale.com/secret-type" : "certs" ,
"tailscale.com/managed" : "true" ,
"tailscale.com/proxy-group" : "ingress-proxies" ,
}
// Helper function to create Secret objects for testing
makeSecret := func ( name string , labels map [ string ] string , certSuffix string ) kubeapi . Secret {
return kubeapi . Secret {
ObjectMeta : kubeapi . ObjectMeta {
Name : name ,
Labels : labels ,
} ,
Data : map [ string ] [ ] byte {
"tls.crt" : [ ] byte ( testCert + certSuffix ) ,
"tls.key" : [ ] byte ( testKey + certSuffix ) ,
} ,
}
}
tests := [ ] struct {
name string
stateSecretContents map [ string ] [ ] byte // data in state Secret
TLSSecrets [ ] kubeapi . Secret // list of TLS cert Secrets
certMode string
secretGetErr error // error to return from GetSecret
secretsListErr error // error to return from ListSecrets
wantMemoryStoreContents map [ ipn . StateKey ] [ ] byte
wantErr error
} {
{
name : "empty_state_secret" ,
stateSecretContents : map [ string ] [ ] byte { } ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte { } ,
} ,
{
name : "state_secret_not_found" ,
secretGetErr : & kubeapi . Status { Code : 404 } ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte { } ,
} ,
{
name : "state_secret_get_error" ,
secretGetErr : fmt . Errorf ( "some error" ) ,
wantErr : fmt . Errorf ( "error loading state from kube Secret: some error" ) ,
} ,
{
name : "load_existing_state" ,
stateSecretContents : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"baz" : [ ] byte ( "qux" ) ,
} ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"baz" : [ ] byte ( "qux" ) ,
} ,
} ,
{
name : "load_select_certs_in_read_only_mode" ,
certMode : "ro" ,
stateSecretContents : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
TLSSecrets : [ ] kubeapi . Secret {
makeSecret ( "app1.tailnetxyz.ts.net" , certSecretsLabels , "1" ) ,
makeSecret ( "app2.tailnetxyz.ts.net" , certSecretsLabels , "2" ) ,
makeSecret ( "some-other-secret" , nil , "3" ) ,
makeSecret ( "app3.other-proxies.ts.net" , map [ string ] string {
"tailscale.com/secret-type" : "certs" ,
"tailscale.com/managed" : "true" ,
"tailscale.com/proxy-group" : "some-other-proxygroup" ,
} , "4" ) ,
} ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"app1.tailnetxyz.ts.net.crt" : [ ] byte ( testCert + "1" ) ,
"app1.tailnetxyz.ts.net.key" : [ ] byte ( testKey + "1" ) ,
"app2.tailnetxyz.ts.net.crt" : [ ] byte ( testCert + "2" ) ,
"app2.tailnetxyz.ts.net.key" : [ ] byte ( testKey + "2" ) ,
} ,
} ,
{
name : "load_select_certs_in_read_write_mode" ,
certMode : "rw" ,
stateSecretContents : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
TLSSecrets : [ ] kubeapi . Secret {
makeSecret ( "app1.tailnetxyz.ts.net" , certSecretsLabels , "1" ) ,
makeSecret ( "app2.tailnetxyz.ts.net" , certSecretsLabels , "2" ) ,
makeSecret ( "some-other-secret" , nil , "3" ) ,
makeSecret ( "app3.other-proxies.ts.net" , map [ string ] string {
"tailscale.com/secret-type" : "certs" ,
"tailscale.com/managed" : "true" ,
"tailscale.com/proxy-group" : "some-other-proxygroup" ,
} , "4" ) ,
} ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
"app1.tailnetxyz.ts.net.crt" : [ ] byte ( testCert + "1" ) ,
"app1.tailnetxyz.ts.net.key" : [ ] byte ( testKey + "1" ) ,
"app2.tailnetxyz.ts.net.crt" : [ ] byte ( testCert + "2" ) ,
"app2.tailnetxyz.ts.net.key" : [ ] byte ( testKey + "2" ) ,
} ,
} ,
{
name : "list_cert_secrets_fails" ,
certMode : "ro" ,
stateSecretContents : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
secretsListErr : fmt . Errorf ( "list error" ) ,
// The error is logged but not returned, and state is still loaded
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
} ,
{
name : "cert_secrets_not_loaded_when_not_in_share_mode" ,
certMode : "" ,
stateSecretContents : map [ string ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
TLSSecrets : [ ] kubeapi . Secret {
makeSecret ( "app1.tailnetxyz.ts.net" , certSecretsLabels , "1" ) ,
} ,
wantMemoryStoreContents : map [ ipn . StateKey ] [ ] byte {
"foo" : [ ] byte ( "bar" ) ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
envknob . Setenv ( "TS_CERT_SHARE_MODE" , tt . certMode )
t . Setenv ( "POD_NAME" , "ingress-proxies-1" )
client := & kubeclient . FakeClient {
GetSecretImpl : func ( ctx context . Context , name string ) ( * kubeapi . Secret , error ) {
if tt . secretGetErr != nil {
return nil , tt . secretGetErr
}
if name == secretName {
return & kubeapi . Secret { Data : tt . stateSecretContents } , nil
}
return nil , & kubeapi . Status { Code : 404 }
} ,
CheckSecretPermissionsImpl : func ( ctx context . Context , name string ) ( bool , bool , error ) {
return true , true , nil
} ,
ListSecretsImpl : func ( ctx context . Context , selector map [ string ] string ) ( * kubeapi . SecretList , error ) {
if tt . secretsListErr != nil {
return nil , tt . secretsListErr
}
var matchingSecrets [ ] kubeapi . Secret
for _ , secret := range tt . TLSSecrets {
matches := true
for k , v := range selector {
if secret . Labels [ k ] != v {
matches = false
break
}
}
if matches {
matchingSecrets = append ( matchingSecrets , secret )
}
}
return & kubeapi . SecretList { Items : matchingSecrets } , nil
} ,
}
s , err := newWithClient ( t . Logf , client , secretName )
if tt . wantErr != nil {
if err == nil {
t . Errorf ( "NewWithClient() error = nil, want error containing %v" , tt . wantErr )
return
}
if ! strings . Contains ( err . Error ( ) , tt . wantErr . Error ( ) ) {
t . Errorf ( "NewWithClient() error = %v, want error containing %v" , err , tt . wantErr )
}
return
}
if err != nil {
t . Errorf ( "NewWithClient() unexpected error: %v" , err )
return
}
// Verify memory store contents
gotJSON , err := s . memory . ExportToJSON ( )
if err != nil {
t . Errorf ( "ExportToJSON failed: %v" , err )
return
}
var got map [ ipn . StateKey ] [ ] byte
if err := json . Unmarshal ( gotJSON , & got ) ; err != nil {
t . Errorf ( "failed to unmarshal memory store JSON: %v" , err )
return
}
want := tt . wantMemoryStoreContents
if want == nil {
want = map [ ipn . StateKey ] [ ] byte { }
}
if diff := cmp . Diff ( got , want ) ; diff != "" {
t . Errorf ( "memory store contents mismatch (-got +want):\n%s" , diff )
}
} )
}
}