github.com/cilium/cilium@v1.16.2/clustermesh-apiserver/clustermesh/users_mgmt.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package clustermesh 5 6 import ( 7 "context" 8 "fmt" 9 "os" 10 "sync" 11 12 "github.com/cilium/hive/cell" 13 "github.com/spf13/pflag" 14 "gopkg.in/yaml.v3" 15 16 cmtypes "github.com/cilium/cilium/pkg/clustermesh/types" 17 "github.com/cilium/cilium/pkg/controller" 18 "github.com/cilium/cilium/pkg/fswatcher" 19 "github.com/cilium/cilium/pkg/kvstore" 20 "github.com/cilium/cilium/pkg/logging/logfields" 21 "github.com/cilium/cilium/pkg/promise" 22 ) 23 24 const ( 25 usersMgmtCtrl = "clustermesh-users-management" 26 ) 27 28 var usersManagementCell = cell.Module( 29 "clustermesh-users-management", 30 "ClusterMesh Etcd Users Management", 31 32 kvstore.GlobalUserMgmtClientPromiseCell, 33 cell.Config(UsersManagementConfig{}), 34 cell.Invoke(registerUsersManager), 35 ) 36 37 var usersManagementControllerGroup = controller.NewGroup("clustermesh-users-management") 38 39 type UsersManagementConfig struct { 40 ClusterUsersEnabled bool 41 ClusterUsersConfigPath string 42 } 43 44 func (UsersManagementConfig) Flags(flags *pflag.FlagSet) { 45 flags.Bool("cluster-users-enabled", false, 46 "Enable the management of etcd users for remote clusters") 47 flags.String("cluster-users-config-path", "/var/lib/cilium/etcd-config/users.yaml", 48 "The path of the config file with the list of remote cluster users") 49 } 50 51 type usersConfigFile struct { 52 Users []struct { 53 Name string `yaml:"name"` 54 Role string `yaml:"role"` 55 } `yaml:"users"` 56 } 57 58 type usersManager struct { 59 UsersManagementConfig 60 clusterInfo cmtypes.ClusterInfo 61 62 client kvstore.BackendOperationsUserMgmt 63 clientPromise promise.Promise[kvstore.BackendOperationsUserMgmt] 64 65 manager *controller.Manager 66 users map[string]string 67 68 stop chan struct{} 69 wg sync.WaitGroup 70 } 71 72 func registerUsersManager( 73 lc cell.Lifecycle, 74 cfg UsersManagementConfig, 75 cinfo cmtypes.ClusterInfo, 76 clientPromise promise.Promise[kvstore.BackendOperationsUserMgmt], 77 ) error { 78 if !cfg.ClusterUsersEnabled { 79 log.Info("etcd users management disabled") 80 return nil 81 } 82 83 manager := usersManager{ 84 UsersManagementConfig: cfg, 85 clientPromise: clientPromise, 86 87 manager: controller.NewManager(), 88 users: make(map[string]string), 89 90 stop: make(chan struct{}), 91 } 92 93 lc.Append(&manager) 94 return nil 95 } 96 97 func (us *usersManager) Start(cell.HookContext) error { 98 log.WithField(logfields.Path, us.ClusterUsersConfigPath). 99 Info("Starting managing etcd users based on configuration") 100 101 configWatcher, err := fswatcher.New([]string{us.ClusterUsersConfigPath}) 102 if err != nil { 103 log.WithError(err).Error("Unable to setup config watcher") 104 return fmt.Errorf("unable to setup config watcher: %w", err) 105 } 106 107 us.manager.UpdateController(usersMgmtCtrl, controller.ControllerParams{ 108 Group: usersManagementControllerGroup, 109 Context: context.Background(), 110 DoFunc: us.sync, 111 }) 112 113 us.wg.Add(1) 114 go func() { 115 defer us.wg.Done() 116 117 for { 118 select { 119 case <-configWatcher.Events: 120 us.manager.TriggerController(usersMgmtCtrl) 121 case err := <-configWatcher.Errors: 122 log.WithError(err).WithField(logfields.Path, us.ClusterUsersConfigPath). 123 Warning("Error encountered while watching file with fsnotify") 124 case <-us.stop: 125 log.Info("Closing") 126 configWatcher.Close() 127 return 128 } 129 } 130 }() 131 132 return nil 133 } 134 135 func (us *usersManager) Stop(cell.HookContext) error { 136 log.WithField(logfields.Path, us.ClusterUsersConfigPath). 137 Info("Stopping managing etcd users based on configuration") 138 139 us.manager.RemoveAllAndWait() 140 close(us.stop) 141 us.wg.Wait() 142 return nil 143 } 144 145 func (us *usersManager) sync(ctx context.Context) error { 146 if us.client == nil { 147 client, err := us.clientPromise.Await(ctx) 148 if err != nil { 149 log.WithError(err).Error("Unable to retrieve the kvstore client") 150 return err 151 } 152 us.client = client 153 } 154 155 config, err := os.ReadFile(us.ClusterUsersConfigPath) 156 if err != nil { 157 log.WithError(err).WithField(logfields.Path, us.ClusterUsersConfigPath). 158 Error("Failed reading users configuration file") 159 return err 160 } 161 162 var users usersConfigFile 163 if err := yaml.Unmarshal(config, &users); err != nil { 164 log.WithError(err).WithField(logfields.Path, us.ClusterUsersConfigPath). 165 Error("Failed un-marshalling users configuration file") 166 return err 167 } 168 169 // Mark all users as stale 170 stale := make(map[string]struct{}, len(us.users)) 171 for user := range us.users { 172 stale[user] = struct{}{} 173 } 174 175 for _, user := range users.Users { 176 if user.Name == us.clusterInfo.Name { 177 continue 178 } 179 180 role, found := us.users[user.Name] 181 if !found || role != user.Role { 182 if err := us.client.UserEnforcePresence(ctx, user.Name, []string{user.Role}); err != nil { 183 log.WithError(err).WithField(logfields.User, user.Name).Error("Failed configuring user") 184 return err 185 } 186 187 log.WithField(logfields.User, user.Name).Info("User successfully configured") 188 } 189 190 us.users[user.Name] = user.Role 191 delete(stale, user.Name) 192 } 193 194 // Delete all stale users 195 for user := range stale { 196 if err := us.client.UserEnforceAbsence(ctx, user); err != nil { 197 log.WithError(err).WithField(logfields.User, user).Error("Failed removing user") 198 return err 199 } 200 201 log.WithField(logfields.User, user).Info("User successfully removed") 202 delete(us.users, user) 203 } 204 205 return nil 206 }