This commit is contained in:
BENEDEK László 2024-10-05 01:43:59 +02:00
commit 94ad55ac2e
10 changed files with 437 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
buildx-manager
test.yml

52
buildx/build.go Normal file
View File

@ -0,0 +1,52 @@
package buildx
import (
"path/filepath"
"git.tek.govt.hu/dowerx/buildx-manager/config"
)
func RepoToCommands(repo *Repository) ([]Command, error) {
cfg := config.GetConfig()
workdir, err := filepath.Abs(repo.Path)
if err != nil {
return nil, err
}
globalArgs := make([]string, 0)
for _, arg := range repo.GlobalArguments {
globalArgs = append(globalArgs, "--build-arg", arg.Key+"="+arg.Value)
}
commands := make([]Command, 0)
// builds
for _, build := range repo.Builds {
id := uid()
cmd := Command{Program: cfg.DockerExecutable, Arguments: []string{"buildx", "build", "-f", build.Dockerfile, "--platform", "linux/" + build.Architecture, "--tag", id, cfg.Action}, WorkingDirectory: workdir}
// args
cmd.Arguments = append(cmd.Arguments, globalArgs...)
for _, arg := range build.Arguments {
cmd.Arguments = append(cmd.Arguments, "--build-arg", arg.Key+"="+arg.Value)
}
// tags
for _, tag := range build.Tags {
addUniqueTag(id, repo.Library+"/"+tag)
}
commands = append(commands, cmd)
}
// tags
for _, rtag := range repo.Tags {
for _, tag := range rtag.Tags {
addGroupTag(repo.Library+"/"+rtag.Name, repo.Library+"/"+tag)
}
}
return commands, err
}

26
buildx/command.go Normal file
View File

@ -0,0 +1,26 @@
package buildx
import (
"os"
"os/exec"
"git.tek.govt.hu/dowerx/buildx-manager/config"
)
type Command struct {
Program string
Arguments []string
WorkingDirectory string
}
func (c *Command) Run() error {
cmd := exec.Command(c.Program, c.Arguments...)
cmd.Dir = c.WorkingDirectory
if config.GetConfig().Verbose {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
return cmd.Run()
}

105
buildx/load.go Normal file
View File

@ -0,0 +1,105 @@
package buildx
import (
"os"
"github.com/drone/envsubst"
"gopkg.in/yaml.v3"
)
func subst(ref *string) error {
result, err := envsubst.EvalEnv(*ref)
*ref = result
return err
}
func LoadJob(path string) (*Job, error) {
var job Job
file, err := os.ReadFile(path)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(file, &job)
if err != nil {
return nil, err
}
// envsubst
// registries
for i := range job.Registries {
if err = subst(&job.Registries[i]); err != nil {
return nil, err
}
}
// repos
for i := range job.Repositories {
// name
if err = subst(&job.Repositories[i].Name); err != nil {
return nil, err
}
// library
if err = subst(&job.Repositories[i].Library); err != nil {
return nil, err
}
// path
if err = subst(&job.Repositories[i].Path); err != nil {
return nil, err
}
// global args
for j := range job.Repositories[i].GlobalArguments {
// key
if err = subst(&job.Repositories[i].GlobalArguments[j].Key); err != nil {
return nil, err
}
// value
if err = subst(&job.Repositories[i].GlobalArguments[j].Value); err != nil {
return nil, err
}
}
// tags
for j := range job.Repositories[i].Tags {
// name
if err = subst(&job.Repositories[i].Tags[j].Name); err != nil {
return nil, err
}
// tags
for u := range job.Repositories[i].Tags[j].Tags {
if err = subst(&job.Repositories[i].Tags[j].Tags[u]); err != nil {
return nil, err
}
}
}
// builds
for j := range job.Repositories[i].Builds {
// arch
if err = subst(&job.Repositories[i].Builds[j].Architecture); err != nil {
return nil, err
}
// dockerfile
if err = subst(&job.Repositories[i].Builds[j].Dockerfile); err != nil {
return nil, err
}
// tags
for u := range job.Repositories[i].Builds[j].Tags {
if err = subst(&job.Repositories[i].Builds[j].Tags[u]); err != nil {
return nil, err
}
}
}
}
return &job, err
}

62
buildx/tag.go Normal file
View File

@ -0,0 +1,62 @@
package buildx
import (
"git.tek.govt.hu/dowerx/buildx-manager/config"
"golang.org/x/exp/rand"
)
var uniqueTags map[string][]string = make(map[string][]string)
var groupTags map[string][]string = make(map[string][]string)
var runes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func uid() string {
for {
id := make([]rune, 16)
for i := range id {
id[i] = runes[rand.Intn(len(runes))]
}
_, exists := uniqueTags[string(id)]
if !exists {
return string(id)
}
}
}
func addUniqueTag(id string, tag string) {
uniqueTags[id] = append(uniqueTags[id], tag)
}
func addGroupTag(id string, tag string) {
groupTags[id] = append(groupTags[id], tag)
}
func TagsToCommands(registries []string) []Command {
cfg := config.GetConfig()
commands := make([]Command, 0)
for _, registry := range registries {
// unique tags
for id, tags := range uniqueTags {
for _, tag := range tags {
cmd := Command{Program: cfg.DockerExecutable, Arguments: []string{"buildx", "imagetools", "create", "-t", registry + "/" + tag, id}}
commands = append(commands, cmd)
}
}
// group tags
for newTag, tags := range groupTags {
cmd := Command{Program: cfg.DockerExecutable, Arguments: []string{"buildx", "imagetools", "create", "-t", registry + "/" + newTag}}
for _, tag := range tags {
cmd.Arguments = append(cmd.Arguments, registry+"/"+tag)
}
commands = append(commands, cmd)
}
}
return commands
}

38
buildx/types.go Normal file
View File

@ -0,0 +1,38 @@
package buildx
const (
AMD64 string = "amd64"
ARM64 string = "arm64"
ARMv7 string = "armv7"
)
type Argument struct {
Key string `yaml:"key"`
Value string `yaml:"value"`
}
type Tag struct {
Name string `yaml:"name"`
Tags []string `yaml:"tags"`
}
type Build struct {
Architecture string `yaml:"arch"`
Dockerfile string `yaml:"dockerfile" default:"Dockerfile"`
Tags []string `yaml:"tags"`
Arguments []Argument `yaml:"args"`
}
type Repository struct {
Name string `yaml:"name"`
Library string `yaml:"library"`
Path string `yaml:"path"`
GlobalArguments []Argument `yaml:"args"`
Tags []Tag `yaml:"tags"`
Builds []Build `yaml:"builds"`
}
type Job struct {
Registries []string `yaml:"registries"`
Repositories []Repository `yaml:"repos"`
}

41
config/config.go Normal file
View File

@ -0,0 +1,41 @@
package config
import "flag"
var config *Config
type Config struct {
File string
Action string
Verbose bool
Parallel bool
Dryrun bool
DockerExecutable string
}
func GetConfig() Config {
if config != nil {
return *config
}
config = &Config{}
flag.StringVar(&config.File, "f", "", "job definition")
flag.StringVar(&config.Action, "a", "", "action")
flag.BoolVar(&config.Verbose, "v", false, "verbose")
flag.BoolVar(&config.Parallel, "p", false, "parallel")
flag.BoolVar(&config.Dryrun, "dry", false, "dryrun")
flag.StringVar(&config.DockerExecutable, "docker", "docker", "docker executable")
flag.Parse()
if config.File == "" {
panic("missing job definition")
}
if config.Action != "load" && config.Action != "push" {
panic("action must be \"load\" or \"push\"")
}
return *config
}

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module git.tek.govt.hu/dowerx/buildx-manager
go 1.23.0
require (
github.com/drone/envsubst v1.0.3 // indirect
github.com/sanity-io/litter v1.5.5 // indirect
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

13
go.sum Normal file
View File

@ -0,0 +1,13 @@
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

88
main.go Normal file
View File

@ -0,0 +1,88 @@
package main
import (
"fmt"
"time"
"git.tek.govt.hu/dowerx/buildx-manager/buildx"
"git.tek.govt.hu/dowerx/buildx-manager/config"
"github.com/sanity-io/litter"
"golang.org/x/exp/rand"
)
func main() {
rand.Seed(uint64(time.Now().Unix()))
cfg := config.GetConfig()
job, err := buildx.LoadJob(cfg.File)
if err != nil {
panic(err)
}
buildCommands := make([]buildx.Command, 0)
for _, repo := range job.Repositories {
cmd, err := buildx.RepoToCommands(&repo)
if err != nil {
panic(err)
}
buildCommands = append(buildCommands, cmd...)
}
tagCommands := buildx.TagsToCommands(job.Registries)
if cfg.Verbose {
fmt.Println("config:")
litter.Dump(cfg)
fmt.Println("\njob loaded:")
litter.Dump(job)
fmt.Println("\nbuild commands:", len(buildCommands))
litter.Dump(buildCommands)
fmt.Println("\ntag commands:", len(tagCommands))
litter.Dump(tagCommands)
}
if cfg.Dryrun {
return
}
// build
if cfg.Parallel {
errors := make(chan error)
// start builds
for _, cmd := range buildCommands {
go func() {
errors <- cmd.Run()
}()
}
// wait for builds
for i := 0; i < len(buildCommands); i++ {
for err := range errors {
if err != nil {
panic(err)
}
}
}
} else {
for _, cmd := range buildCommands {
err = cmd.Run()
if err != nil {
panic(err)
}
}
}
// tag
for _, cmd := range tagCommands {
err = cmd.Run()
if err != nil {
panic(err)
}
}
}