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  }