2023-03-16 22:50:02 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2023-03-25 00:37:01 +02:00
|
|
|
"path"
|
2023-03-16 22:50:02 +02:00
|
|
|
"strings"
|
2023-03-24 22:31:33 +02:00
|
|
|
"sync"
|
2023-03-25 00:37:01 +02:00
|
|
|
"time"
|
2023-03-16 22:50:02 +02:00
|
|
|
|
|
|
|
"gopkg.in/yaml.v3"
|
|
|
|
)
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
// RequirementsFile structure
|
2023-03-16 22:50:02 +02:00
|
|
|
type RequirementsFile []RequirementsEntry
|
2023-03-25 00:37:01 +02:00
|
|
|
|
|
|
|
// RequirementsEntry is requirements.yml's entry structure
|
2023-03-16 22:50:02 +02:00
|
|
|
type RequirementsEntry struct {
|
2023-03-25 00:37:01 +02:00
|
|
|
Src string `yaml:"src,omitempty"`
|
|
|
|
Version string `yaml:"version,omitempty"`
|
2023-03-24 22:31:33 +02:00
|
|
|
Name string `yaml:"name,omitempty"`
|
2023-03-25 00:37:01 +02:00
|
|
|
Include string `yaml:"include,omitempty"`
|
2023-03-16 22:50:02 +02:00
|
|
|
}
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
// GalaxyInstallInfo is meta/.galaxy_install_info struct
|
|
|
|
type GalaxyInstallInfo struct {
|
|
|
|
InstallDate string `yaml:"install_date"`
|
|
|
|
Version string `yaml:"version"`
|
2023-03-16 22:50:02 +02:00
|
|
|
}
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
var (
|
|
|
|
rolesPath string
|
|
|
|
requirementsPath string
|
|
|
|
ignoredVersions = map[string]bool{
|
|
|
|
"main": true,
|
|
|
|
"master": true,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2023-03-16 22:50:02 +02:00
|
|
|
func main() {
|
|
|
|
flag.StringVar(&requirementsPath, "r", "requirements.yml", "ansible-galaxy requirements file")
|
2023-03-25 00:37:01 +02:00
|
|
|
flag.StringVar(&rolesPath, "p", "roles/galaxy/", "path to install roles")
|
2023-03-16 22:50:02 +02:00
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
log.Println("updating requirements.yml...")
|
|
|
|
updateRequirements(requirementsPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateRequirements(path string) {
|
2023-03-25 00:37:01 +02:00
|
|
|
entries, installOnly := parseRequirements(path)
|
2023-03-24 22:31:33 +02:00
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
2023-03-25 00:37:01 +02:00
|
|
|
wg.Add(len(entries) + len(installOnly))
|
2023-03-16 22:50:02 +02:00
|
|
|
for i, entry := range entries {
|
2023-03-24 22:31:33 +02:00
|
|
|
go func(i int, entry RequirementsEntry, wg *sync.WaitGroup) {
|
|
|
|
newVersion := getNewVersion(entry.Src, entry.Version)
|
|
|
|
if newVersion != "" {
|
|
|
|
log.Println(entry.Src, entry.Version, "->", newVersion)
|
|
|
|
entry.Version = newVersion
|
2023-03-25 00:37:01 +02:00
|
|
|
installRole(entry)
|
2023-03-24 22:31:33 +02:00
|
|
|
entries[i] = entry
|
|
|
|
}
|
2023-03-25 00:37:01 +02:00
|
|
|
if !isInstalled(entry) {
|
|
|
|
installRole(entry)
|
|
|
|
}
|
2023-03-24 22:31:33 +02:00
|
|
|
wg.Done()
|
|
|
|
}(i, entry, &wg)
|
2023-03-16 22:50:02 +02:00
|
|
|
}
|
2023-03-25 00:37:01 +02:00
|
|
|
for _, entry := range installOnly {
|
|
|
|
go func(entry RequirementsEntry, wg *sync.WaitGroup) {
|
|
|
|
if !isInstalled(entry) {
|
|
|
|
installRole(entry)
|
|
|
|
}
|
|
|
|
wg.Done()
|
|
|
|
}(entry, &wg)
|
|
|
|
}
|
2023-03-24 22:31:33 +02:00
|
|
|
wg.Wait()
|
|
|
|
|
2023-03-16 22:50:02 +02:00
|
|
|
outb, err := yaml.Marshal(entries)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: ", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := os.WriteFile(path, outb, 0600); err != nil {
|
|
|
|
log.Println("ERROR: ", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
// parseRequirements parses requirements.yml file and tries to update it
|
|
|
|
// if it founds any includes within that file, they will be returned as second return value
|
|
|
|
func parseRequirements(path string) (RequirementsFile, RequirementsFile) {
|
2023-03-16 22:50:02 +02:00
|
|
|
fileb, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: ", err)
|
2023-03-25 00:37:01 +02:00
|
|
|
return RequirementsFile{}, RequirementsFile{}
|
2023-03-16 22:50:02 +02:00
|
|
|
}
|
|
|
|
var req RequirementsFile
|
|
|
|
if err := yaml.Unmarshal(fileb, &req); err != nil {
|
|
|
|
log.Println("ERROR: ", err)
|
|
|
|
}
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
return req, parseAdditionalRequirements(req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseAdditionalRequirements(req RequirementsFile) RequirementsFile {
|
|
|
|
additional := make([]RequirementsEntry, 0)
|
|
|
|
for _, entry := range req {
|
|
|
|
if entry.Include != "" {
|
|
|
|
// no recursive iteration over deeper levels, because it's not used anywhere
|
|
|
|
additionalLvl1, additionalLvl2 := parseRequirements(entry.Include)
|
|
|
|
additional = append(additional, additionalLvl1...)
|
|
|
|
additional = append(additional, additionalLvl2...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return additional
|
2023-03-16 22:50:02 +02:00
|
|
|
}
|
|
|
|
|
2023-03-24 22:31:33 +02:00
|
|
|
func getNewVersion(src, version string) string {
|
|
|
|
if ignoredVersions[version] {
|
2023-03-16 22:50:02 +02:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// not a git repo
|
2023-03-24 22:31:33 +02:00
|
|
|
if !strings.Contains(src, "git") {
|
2023-03-16 22:50:02 +02:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-03-24 22:31:33 +02:00
|
|
|
repo := strings.Replace(src, "git+https", "https", 1)
|
2023-03-25 00:37:01 +02:00
|
|
|
tags, err := execute("git ls-remote -tq --sort=-version:refname "+repo, "")
|
2023-03-16 22:50:02 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: ", err)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if tags == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
lastline := strings.Split(tags, "\n")[0]
|
|
|
|
tagidx := strings.Index(lastline, "refs/tags/")
|
|
|
|
if tagidx == -1 {
|
|
|
|
log.Println("ERROR: lastline: ", lastline)
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
last := strings.Replace(lastline[tagidx:], "refs/tags/", "", 1)
|
2023-03-24 22:31:33 +02:00
|
|
|
last = strings.Replace(last, "^{}", "", 1) // NOTE: very weird case with some github repos, didn't find out why it does that
|
|
|
|
if last != version {
|
2023-03-16 22:50:02 +02:00
|
|
|
return last
|
|
|
|
}
|
|
|
|
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2023-03-25 00:37:01 +02:00
|
|
|
func execute(command string, dir string) (string, error) {
|
2023-03-16 22:50:02 +02:00
|
|
|
slice := strings.Split(command, " ")
|
2023-03-25 00:37:01 +02:00
|
|
|
cmd := exec.Command(slice[0], slice[1:]...)
|
|
|
|
cmd.Dir = dir
|
|
|
|
out, err := cmd.CombinedOutput()
|
2023-03-16 22:50:02 +02:00
|
|
|
if out == nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2023-03-16 23:29:28 +02:00
|
|
|
|
2023-03-16 22:50:02 +02:00
|
|
|
return strings.TrimSuffix(string(out), "\n"), err
|
|
|
|
}
|
2023-03-25 00:37:01 +02:00
|
|
|
|
|
|
|
// getRoleName returns either role name or repo name from url
|
|
|
|
func getRoleName(entry RequirementsEntry) string {
|
|
|
|
if entry.Name != "" {
|
|
|
|
return entry.Name
|
|
|
|
}
|
|
|
|
return strings.TrimSuffix(path.Base(entry.Src), ".git")
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateInstallInfo(version string) ([]byte, error) {
|
|
|
|
info := GalaxyInstallInfo{
|
|
|
|
InstallDate: time.Now().Format("Mon 02 Jan 2006 03:04:05 PM "), // the trailing space is done by ansible-galaxy
|
|
|
|
Version: version,
|
|
|
|
}
|
|
|
|
return yaml.Marshal(info)
|
|
|
|
}
|
|
|
|
|
|
|
|
func isInstalled(entry RequirementsEntry) bool {
|
|
|
|
_, err := os.Stat(path.Join(rolesPath, getRoleName(entry)))
|
|
|
|
if err == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return os.IsExist(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func installRole(entry RequirementsEntry) {
|
|
|
|
name := getRoleName(entry)
|
|
|
|
log.Println("Installing", name, entry.Version)
|
|
|
|
|
|
|
|
repo := strings.Replace(entry.Src, "git+", "", 1)
|
|
|
|
tmpdir, err := os.MkdirTemp("", "")
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: cannot create tmp dir:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tmpfile := tmpdir + ".tar"
|
|
|
|
|
|
|
|
// clone repo
|
|
|
|
var clone strings.Builder
|
|
|
|
clone.WriteString("git clone -q --depth 1 -b ")
|
|
|
|
clone.WriteString(entry.Version)
|
|
|
|
clone.WriteString(" ")
|
|
|
|
clone.WriteString(repo)
|
|
|
|
clone.WriteString(" ")
|
|
|
|
clone.WriteString(tmpdir)
|
|
|
|
_, err = execute(clone.String(), "")
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: cannot clone repo:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// create archive from the cloned source
|
|
|
|
var archive strings.Builder
|
|
|
|
archive.WriteString("git archive --prefix=")
|
|
|
|
archive.WriteString(name)
|
|
|
|
archive.WriteString("/ --output=")
|
|
|
|
archive.WriteString(tmpfile)
|
|
|
|
archive.WriteString(" ")
|
|
|
|
archive.WriteString(entry.Version)
|
|
|
|
_, err = execute(archive.String(), tmpdir)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: cannot archive repo:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// extract the archive into roles path
|
|
|
|
out, err := execute("tar -xf "+tmpfile, rolesPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: cannot extract archive:", err)
|
|
|
|
log.Println(out)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// write install info file
|
|
|
|
outb, err := generateInstallInfo(entry.Version)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("ERROR: cannot generate install info:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := os.WriteFile(path.Join(rolesPath, name, "meta", ".galaxy_install_info"), outb, 0600); err != nil {
|
|
|
|
log.Println("ERROR: cannot write install info:", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|