github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/backend/default-backend.go (about) 1 package backend 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/criteo/command-launcher/internal/command" 13 "github.com/criteo/command-launcher/internal/repository" 14 "gopkg.in/yaml.v3" 15 ) 16 17 // DefaultBackend supports multiple managed repositories and 1 dropin repository 18 // It contains: 19 // - 1 dropin local repository - index 0 20 // - 1 default managed repository - index 1 21 // - n additional managed repository - index 2 .. 22 type DefaultBackend struct { 23 homeDir string 24 sources []*PackageSource 25 26 cmdsCache map[string]command.Command 27 groupCmds []command.Command 28 executableCmds []command.Command 29 30 userAlias map[string]string 31 tmpAlias map[string]string 32 } 33 34 const DROPIN_REPO_INDEX = 0 35 const DEFAULT_REPO_INDEX = 1 36 const DEFAULT_REPO_ID = "default" 37 const DROPIN_REPO_ID = "dropin" 38 39 const RENAME_FILE_NAME = "rename.json" 40 41 // Create a new default backend with multiple local repository directories 42 // When any of these repositories failed to load, an error is returned. 43 func NewDefaultBackend(homeDir string, dropinSource *PackageSource, defaultSource *PackageSource, additionalSources ...*PackageSource) (Backend, error) { 44 backend := &DefaultBackend{ 45 // input properties 46 homeDir: homeDir, 47 sources: append([]*PackageSource{dropinSource, defaultSource}, additionalSources...), 48 49 // data need to be reset during reload 50 cmdsCache: map[string]command.Command{}, 51 groupCmds: []command.Command{}, 52 executableCmds: []command.Command{}, 53 userAlias: map[string]string{}, 54 tmpAlias: map[string]string{}, 55 } 56 err := backend.Reload() 57 return backend, err 58 } 59 60 func (backend *DefaultBackend) Reload() error { 61 for _, s := range backend.sources { 62 s.Repo = nil 63 } 64 backend.cmdsCache = make(map[string]command.Command) 65 backend.groupCmds = []command.Command{} 66 backend.executableCmds = []command.Command{} 67 backend.userAlias = make(map[string]string) 68 backend.tmpAlias = make(map[string]string) 69 70 err := backend.loadRepos() 71 backend.loadAlias() 72 backend.extractCmds() 73 return err 74 } 75 76 func (backend *DefaultBackend) loadRepos() error { 77 failures := []string{} 78 for _, src := range backend.sources { 79 repo, err := repository.CreateLocalRepository(src.Name, src.RepoDir, nil) 80 if err != nil { 81 failures = append(failures, err.Error()) 82 src.Failure = err 83 } else { 84 src.Repo = repo 85 } 86 } 87 if len(failures) > 0 { 88 return errors.New(fmt.Sprintf("failed to load repositories: %s", strings.Join(failures, "\n"))) 89 } 90 return nil 91 } 92 93 func (backend *DefaultBackend) loadAlias() error { 94 if renameFile, err := os.Open(filepath.Join(backend.homeDir, RENAME_FILE_NAME)); err == nil { 95 defer renameFile.Close() 96 if err != nil { 97 return fmt.Errorf("no such rename file found (%s)", err) 98 } 99 100 stat, err := renameFile.Stat() 101 if err != nil { 102 return fmt.Errorf("cannot read rename file (%s)", err) 103 } 104 105 var payload = make([]byte, stat.Size()) 106 nb, err := renameFile.Read(payload) 107 if err != nil && err != io.EOF || nb != int(stat.Size()) { 108 return fmt.Errorf("cannot read the rename file (%s)", err) 109 } 110 111 err = yaml.Unmarshal(payload, backend.userAlias) 112 113 if err != nil { 114 backend.userAlias = map[string]string{} 115 return fmt.Errorf("cannot read the manifest content, it is neither a valid JSON nor YAML (%s)", err) 116 } 117 return nil 118 } else { 119 return err 120 } 121 } 122 123 func (backend *DefaultBackend) setRuntimeByAlias(cmd command.Command) { 124 // first check runtime filter 125 if rename, ok := backend.tmpAlias[cmd.FullGroup()]; ok { 126 cmd.SetRuntimeGroup(rename) 127 } 128 if rename, ok := backend.tmpAlias[cmd.FullName()]; ok { 129 cmd.SetRuntimeName(rename) 130 } 131 // override any tmp filer if it defined by user 132 if rename, ok := backend.userAlias[cmd.FullGroup()]; ok { 133 cmd.SetRuntimeGroup(rename) 134 } 135 if rename, ok := backend.userAlias[cmd.FullName()]; ok { 136 cmd.SetRuntimeName(rename) 137 } 138 } 139 140 func (backend *DefaultBackend) extractCmds() { 141 for _, src := range backend.sources { 142 repo := src.Repo 143 if repo == nil { 144 continue 145 } 146 // first extract group commands 147 cmds := repo.InstalledGroupCommands() 148 for _, cmd := range cmds { 149 backend.setRuntimeByAlias(cmd) 150 151 key := getCmdSearchKey(cmd) 152 if _, exist := backend.cmdsCache[key]; exist || isReservedCmd(key) { 153 // conflict 154 cmd.SetRuntimeName(cmd.FullName()) 155 backend.tmpAlias[cmd.FullName()] = cmd.FullName() 156 key = getCmdSearchKey(cmd) 157 } 158 159 backend.cmdsCache[key] = cmd 160 backend.groupCmds = append(backend.groupCmds, cmd) 161 } 162 163 // now extract executable commands 164 cmds = repo.InstalledExecutableCommands() 165 for _, cmd := range cmds { 166 backend.setRuntimeByAlias(cmd) 167 168 key := getCmdSearchKey(cmd) 169 if _, exist := backend.cmdsCache[key]; exist || isReservedCmd(key) { 170 // conflict 171 if cmd.Group() == "" { 172 cmd.SetRuntimeName(cmd.FullName()) 173 backend.tmpAlias[cmd.FullName()] = cmd.FullName() 174 } else { 175 cmd.SetRuntimeGroup(cmd.FullGroup()) 176 backend.tmpAlias[cmd.FullGroup()] = cmd.FullGroup() 177 } 178 } 179 180 key = getCmdSearchKey(cmd) 181 backend.cmdsCache[key] = cmd 182 backend.executableCmds = append(backend.executableCmds, cmd) 183 } 184 } 185 } 186 187 func getCmdSearchKey(cmd command.Command) string { 188 switch cmd.Type() { 189 case "group": 190 return fmt.Sprintf("#%s", cmd.RuntimeName()) 191 case "executable": 192 return fmt.Sprintf("%s#%s", cmd.RuntimeGroup(), cmd.RuntimeName()) 193 case "system": 194 return cmd.Name() 195 } 196 return "" 197 } 198 199 func isReservedCmd(searchKey string) bool { 200 _, exist := RESERVED_CMD_SEARCH_KEY[searchKey] 201 return exist 202 } 203 204 /* Implement the Backend interface */ 205 206 func (backend *DefaultBackend) FindCommand(group string, name string) (command.Command, error) { 207 searchKey := fmt.Sprintf("%s#%s", group, name) 208 cmd, ok := backend.cmdsCache[searchKey] 209 if !ok { 210 return nil, fmt.Errorf("no command with group %s and name %s", group, name) 211 } 212 return cmd, nil 213 } 214 215 func (backend DefaultBackend) FindCommandByFullName(fullName string) (command.Command, error) { 216 for _, c := range backend.cmdsCache { 217 if c.FullName() == fullName { 218 return c, nil 219 } 220 } 221 return nil, fmt.Errorf("Not found") 222 } 223 224 func (backend DefaultBackend) GroupCommands() []command.Command { 225 return backend.groupCmds 226 } 227 228 func (backend DefaultBackend) ExecutableCommands() []command.Command { 229 return backend.executableCmds 230 } 231 232 func (backend DefaultBackend) SystemCommand(name string) command.Command { 233 sysCmds := backend.DefaultRepository().InstalledSystemCommands() 234 switch name { 235 case repository.SYSTEM_LOGIN_COMMAND: 236 return sysCmds.Login 237 case repository.SYSTEM_METRICS_COMMAND: 238 return sysCmds.Metrics 239 } 240 return nil 241 } 242 243 func (backend *DefaultBackend) RenameCommand(cmd command.Command, new_name string) error { 244 if cmd.Group() == "" && isReservedCmd(fmt.Sprintf("#%s", new_name)) { 245 return fmt.Errorf("%s is a reserved command name, can't rename a top level command to it", new_name) 246 } 247 248 if new_name == "" { 249 if _, ok := backend.userAlias[cmd.FullName()]; ok { 250 delete(backend.userAlias, cmd.FullName()) 251 } 252 } else { 253 backend.userAlias[cmd.FullName()] = new_name 254 } 255 256 payload, err := json.Marshal(backend.userAlias) 257 if err != nil { 258 return fmt.Errorf("can't encode rename in json: %v", err) 259 } 260 261 err = os.WriteFile(filepath.Join(backend.homeDir, RENAME_FILE_NAME), payload, 0755) 262 if err != nil { 263 return fmt.Errorf("can't write rename filen: %v", err) 264 } 265 return nil 266 } 267 268 func (backend *DefaultBackend) AllRenamedCommands() map[string]string { 269 return backend.userAlias 270 } 271 272 func (backend *DefaultBackend) FindSystemCommand(name string) (command.Command, error) { 273 return nil, nil 274 } 275 276 func (backend DefaultBackend) DefaultRepository() repository.PackageRepository { 277 return backend.sources[DEFAULT_REPO_INDEX].Repo 278 } 279 280 func (backend DefaultBackend) DropinRepository() repository.PackageRepository { 281 return backend.sources[DROPIN_REPO_INDEX].Repo 282 } 283 284 func (backend DefaultBackend) AllPackageSources() []*PackageSource { 285 return backend.sources 286 } 287 288 func (backend DefaultBackend) AllRepositories() []repository.PackageRepository { 289 repos := []repository.PackageRepository{} 290 for _, src := range backend.sources { 291 repos = append(repos, src.Repo) 292 } 293 return repos 294 } 295 296 func (backend DefaultBackend) Debug() { 297 for _, c := range backend.groupCmds { 298 fmt.Printf("%-30s %-30s %s\n", c.RuntimeGroup(), c.RuntimeName(), c.ID()) 299 } 300 for _, c := range backend.executableCmds { 301 fmt.Printf("%-30s %-30s %s\n", c.RuntimeGroup(), c.RuntimeName(), c.ID()) 302 } 303 for k, c := range backend.cmdsCache { 304 fmt.Printf("%-30s %-30s\n", k, c.ID()) 305 } 306 }