// Package admin manage repos package admin import ( "errors" "fmt" "io" "io/fs" "log" "os" "path/filepath" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/storage/filesystem" "github.com/go-git/go-git/v5/storage/memory" "gopkg.in/ini.v1" "sigs.k8s.io/yaml" ) const ( // Read mode operations for repo Read Action = 0 // Write mode operations for repo Write = 1 // Admin mode operations for repo Admin = 2 // GitExportMagic magic file name for daemon export GitExportMagic = "git-daemon-export-ok" // GitWebExportMagic magic filename for web repos GitWebExportMagic = "git-web-export-ok" ) // Action composite type for modes type Action int // GitWeb git web configuration type GitWeb struct { Owner string `json:"owner"` Description string `json:"description"` Category string `json:"category"` URL string `json:"url"` } // Permission authorization controls type Permission struct { Role string `json:"role"` Mode Action `json:"mode"` } // GitRepo git repository type GitRepo struct { // Public "git-daemon-export-ok" magic file for git-http-backend Public bool `json:"public"` // Name game of repository Name string `json:"name"` // Web config settings GitWebConfig *GitWeb `json:"git_web_config"` // Permissions for authorization Permissions []*Permission `json:"permissions"` } // ServerRepos repos that are part of this server instance type ServerRepos struct { Name string `json:"name"` Version string `json:"version"` Repos []*GitRepo `json:"repos"` basePath string } func loadFromGit(gitURL, filePath string) ([]byte, error) { fs := memfs.New() storer := memory.NewStorage() _, err := git.Clone(storer, fs, &git.CloneOptions{ URL: gitURL, }) if err != nil { // log.error fmt.Printf("coudln't clone mgmt repo %s", err) return []byte(""), errors.New("coudln't clone mgmt repo") } file, err := fs.Open(filePath) if err != nil { fmt.Printf("Failed to open gitserver config %s", err) return []byte(""), errors.New("coudln't open git config file from mgmt repo") } defer file.Close() return io.ReadAll(file) } func loadLocalFile(path string) ([]byte, error) { file, err := os.Open(path) if err != nil { log.Printf("config file not opened %s", path) return []byte{}, err } defer file.Close() configBytes, err := io.ReadAll(file) if err != nil { log.Print("config file not read") return []byte{}, err } return configBytes, nil } // loadServerConfig configPath should be the absolutepath to the configmap if it's not in a repo func loadServerConfig(mgmtRepo bool, baseDir, configPath string) (*ServerRepos, error) { var ( configBytes []byte err error ) if mgmtRepo { repoURI := filepath.Join("file:///", baseDir, "mgmt.git") configBytes, err = loadFromGit(repoURI, configPath) if err != nil { // log.error log.Print("Failed to load config file from git") return &ServerRepos{}, err } } else { configBytes, err = loadLocalFile(configPath) if err != nil { // log.error log.Print("Failed to load config file from file system") return &ServerRepos{}, err } } config := &ServerRepos{} err = yaml.Unmarshal(configBytes, &config) if err != nil { return &ServerRepos{}, errors.New("could not parse gitserver config") } return config, nil } // ServerPolicies generate casbin policies func (s *ServerRepos) ServerPolicies() [][]string { policies := [][]string{} for _, repo := range s.Repos { policies = append(policies, repo.CasbinPolicies()...) } return policies } // ConfigureRepos run reconciler for all repos func (s *ServerRepos) ConfigureRepos() { for _, repo := range s.Repos { repo.ReconcileRepo(s.basePath) } } func readOnlyPaths(role, repoName string) [][]string { return [][]string{ []string{role, fmt.Sprintf("/%s/info/refs", repoName), "GET"}, []string{role, fmt.Sprintf("/%s/git-upload-pack", repoName), "POST"}, } } func writePaths(role, repoName string) [][]string { return [][]string{[]string{role, fmt.Sprintf("/%s/git-receive-pack", repoName), "POST"}} } // Policy generate policy for repo base on mode func (p *Permission) Policy(repoName string) [][]string { policies := [][]string{} // if read mode or greater e.g. write mode roleName := fmt.Sprintf("role:%s", p.Role) if p.Mode >= Read { policies = append(policies, readOnlyPaths(roleName, repoName)...) } // if write mode if p.Mode >= Write { policies = append(policies, writePaths(roleName, repoName)...) } return policies } // CasbinPolicies generate all policies func (r *GitRepo) CasbinPolicies() [][]string { policies := [][]string{} for _, perm := range r.Permissions { policies = append(policies, perm.Policy(r.Name)...) } return policies } // ReconcileRepo update repo export settings, update web config func (r *GitRepo) ReconcileRepo(basePath string) { // if exist -> continue repoBase := filepath.Join(basePath, fmt.Sprintf("%s.git", r.Name)) _, err := os.Stat(repoBase) if errors.Is(err, fs.ErrNotExist) { // if no exist -> init bare repoFs := osfs.New(repoBase) strg := filesystem.NewStorage(repoFs, nil) _, _ = git.Init(strg, nil) } // set export file for git-http-backend okExport := filepath.Join(repoBase, GitExportMagic) _, err = os.Stat(okExport) if errors.Is(err, fs.ErrNotExist) { // Create web export f, err := os.Create(okExport) f.Close() if err != nil { log.Fatalf("%s coudln't be created %s", GitExportMagic, err) } } r.ConfigureExport(repoBase) if r.GitWebConfig == nil { r.GitWebConfig = &GitWeb{} } r.GitWebConfig.ReconcileGitConf(repoBase) } // ConfigureExport setup repo for sharing and configure web settings func (r *GitRepo) ConfigureExport(repoBase string) { okExport := filepath.Join(repoBase, GitWebExportMagic) _, err := os.Create(okExport) if err != nil { log.Fatalf("%s coudln't be created %s", GitWebExportMagic, err) } } // ReconcileGitConf reconcile gitweb configuration section of gitconfig func (r *GitWeb) ReconcileGitConf(repoBase string) { confPath := filepath.Join(repoBase, "config") cfg, err := ini.Load(confPath) if err != nil { log.Fatalf("Coudln't read gitconfig %s", err) } // check if empty, delete if (GitWeb{} == *r) { if cfg.HasSection("gitweb") { cfg.DeleteSection("gitweb") if err := cfg.SaveTo(confPath); err != nil { log.Fatalf("Coudln't save gitconfig %s", err) } } return } section := cfg.Section("gitweb") section.Key("description").SetValue(r.Description) section.Key("owner").SetValue(r.Owner) section.Key("url").SetValue(r.URL) section.Key("category").SetValue(r.Category) if err := cfg.SaveTo(confPath); err != nil { log.Fatalf("Coudln't save gitconfig %s", err) } }