github.com/cilium/cilium@v1.16.2/clustermesh-apiserver/clustermesh/users_mgmt_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package clustermesh
     5  
     6  import (
     7  	"context"
     8  	"os"
     9  	"path"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/cilium/hive/cell"
    15  	"github.com/cilium/hive/hivetest"
    16  	"github.com/stretchr/testify/require"
    17  	"go.uber.org/goleak"
    18  
    19  	"github.com/cilium/cilium/operator/watchers"
    20  	cmtypes "github.com/cilium/cilium/pkg/clustermesh/types"
    21  	"github.com/cilium/cilium/pkg/hive"
    22  	"github.com/cilium/cilium/pkg/kvstore"
    23  	"github.com/cilium/cilium/pkg/promise"
    24  )
    25  
    26  const (
    27  	users1 = "users:\n- name: foo\n  role: r1\n- name: bar\n  role: r2\n- name: qux\n  role: r3\n"
    28  	users2 = "users:\n- name: baz\n  role: r3\n- name: foo\n  role: r1\n- name: qux\n  role: r4\n"
    29  )
    30  
    31  type fakeUserMgmtClient struct {
    32  	created map[string]string
    33  	deleted map[string]int
    34  }
    35  
    36  func (f *fakeUserMgmtClient) init() {
    37  	f.created = make(map[string]string)
    38  	f.deleted = make(map[string]int)
    39  }
    40  
    41  func (f *fakeUserMgmtClient) UserEnforcePresence(_ context.Context, name string, roles []string) error {
    42  	// The existing value (if any) is concatenated, to detect if this is called twice for the same name
    43  	f.created[name] = f.created[name] + strings.Join(roles, "|")
    44  	return nil
    45  }
    46  
    47  func (f *fakeUserMgmtClient) UserEnforceAbsence(_ context.Context, name string) error {
    48  	f.deleted[name]++
    49  	return nil
    50  }
    51  
    52  func TestMain(m *testing.M) {
    53  	goleak.VerifyTestMain(
    54  		m,
    55  		// To ignore goroutine started from sigs.k8s.io/controller-runtime/pkg/log.go
    56  		// init function
    57  		goleak.IgnoreTopFunction("time.Sleep"),
    58  		// Delaying workqueues used by resource.Resource[T].Events leaks this waitingLoop goroutine.
    59  		// It does stop when shutting down but is not guaranteed to before we actually exit.
    60  		goleak.IgnoreTopFunction("k8s.io/client-go/util/workqueue.(*delayingType).waitingLoop"),
    61  	)
    62  }
    63  
    64  func TestUsersManagement(t *testing.T) {
    65  	defer func() {
    66  		// force cleanup of goroutines run from initialization of watchers.nodeQueue,
    67  		// otherwise goleak complains
    68  		watchers.NodeQueueShutDown()
    69  		time.Sleep(50 * time.Millisecond)
    70  	}()
    71  
    72  	var client fakeUserMgmtClient
    73  	client.init()
    74  
    75  	tmpdir, err := os.MkdirTemp("", "clustermesh-config")
    76  	require.NoError(t, err)
    77  	defer os.RemoveAll(tmpdir)
    78  
    79  	cfgPath := path.Join(tmpdir, "users.yaml")
    80  	require.NoError(t, os.WriteFile(cfgPath, []byte(users1), 0600))
    81  
    82  	hive := hive.New(
    83  		cell.Provide(func() UsersManagementConfig {
    84  			return UsersManagementConfig{
    85  				ClusterUsersEnabled:    true,
    86  				ClusterUsersConfigPath: cfgPath,
    87  			}
    88  		}),
    89  
    90  		cell.Provide(func() cmtypes.ClusterInfo {
    91  			return cmtypes.ClusterInfo{ID: 10, Name: "fred"}
    92  		}),
    93  
    94  		cell.Provide(func(lc cell.Lifecycle) promise.Promise[kvstore.BackendOperationsUserMgmt] {
    95  			resolver, promise := promise.New[kvstore.BackendOperationsUserMgmt]()
    96  			resolver.Resolve(&client)
    97  			return promise
    98  		}),
    99  
   100  		cell.Invoke(registerUsersManager),
   101  	)
   102  
   103  	ctx, cancel := context.WithCancel(context.Background())
   104  	defer cancel()
   105  
   106  	tlog := hivetest.Logger(t)
   107  	if err := hive.Start(tlog, ctx); err != nil {
   108  		t.Fatalf("failed to start: %s", err)
   109  	}
   110  
   111  	defer func() {
   112  		if err := hive.Stop(tlog, ctx); err != nil {
   113  			t.Fatalf("failed to stop: %s", err)
   114  		}
   115  	}()
   116  
   117  	require.Eventuallyf(t, func() bool {
   118  		return len(client.created) == 3 && len(client.deleted) == 0
   119  	}, time.Second, 10*time.Millisecond,
   120  		"Failed waiting for events to be triggered: created: %v, deleted: %v",
   121  		client.created, client.deleted)
   122  
   123  	require.Equal(t, "r1", client.created["foo"])
   124  	require.Equal(t, "r2", client.created["bar"])
   125  	require.Equal(t, "r3", client.created["qux"])
   126  
   127  	client.init()
   128  
   129  	// Update the users config file, and require that changes are propagated
   130  	// We first write to a different file and then rename it, to avoid the possible
   131  	// race condition caused by truncate + write if we detect the event sufficiently
   132  	// fast (i.e., we first read an empty file, and then the expected one).
   133  	cfgPath2 := path.Join(tmpdir, "users.yaml.2")
   134  	require.NoError(t, os.WriteFile(cfgPath2, []byte(users2), 0600))
   135  	require.NoError(t, os.Rename(cfgPath2, cfgPath))
   136  
   137  	require.Eventuallyf(t, func() bool {
   138  		return len(client.created) == 2 && len(client.deleted) == 1
   139  	}, time.Second, 10*time.Millisecond,
   140  		"Failed waiting for events to be triggered: created: %v, deleted: %v",
   141  		client.created, client.deleted)
   142  
   143  	require.Equal(t, "r3", client.created["baz"])
   144  	require.Equal(t, "r4", client.created["qux"])
   145  	require.Equal(t, 1, client.deleted["bar"])
   146  }