diff --git a/cmd/docker-buildx/config.go b/cmd/docker-buildx/config.go index e831d9e..3cef961 100644 --- a/cmd/docker-buildx/config.go +++ b/cmd/docker-buildx/config.go @@ -200,31 +200,38 @@ func settingsFlags(settings *plugin.Settings) []cli.Flag { EnvVars: []string{"PLUGIN_REGISTRY", "DOCKER_REGISTRY"}, Usage: "sets docker registry to authenticate with", Value: "https://index.docker.io/v1/", - Destination: &settings.Login.Registry, + Destination: &settings.DefaultLogin.Registry, }, &cli.StringFlag{ Name: "docker.username", EnvVars: []string{"PLUGIN_USERNAME", "DOCKER_USERNAME"}, Usage: "sets username to authenticates with", - Destination: &settings.Login.Username, + Destination: &settings.DefaultLogin.Username, }, &cli.StringFlag{ Name: "docker.password", EnvVars: []string{"PLUGIN_PASSWORD", "DOCKER_PASSWORD"}, Usage: "sets password to authenticates with", - Destination: &settings.Login.Password, + Destination: &settings.DefaultLogin.Password, }, &cli.StringFlag{ Name: "docker.email", EnvVars: []string{"PLUGIN_EMAIL", "DOCKER_EMAIL"}, Usage: "sets email address to authenticates with", - Destination: &settings.Login.Email, + Destination: &settings.DefaultLogin.Email, }, &cli.StringFlag{ Name: "docker.config", EnvVars: []string{"PLUGIN_CONFIG", "DOCKER_PLUGIN_CONFIG"}, Usage: "sets content of the docker daemon json config", - Destination: &settings.Login.Config, + Destination: &settings.DefaultLogin.Config, + }, + &cli.StringFlag{ + Name: "logins", + EnvVars: []string{"PLUGIN_LOGINS"}, + Usage: "list of login", + Destination: &settings.LoginsRaw, + Value: "[]", }, &cli.BoolFlag{ Name: "docker.purge", diff --git a/cmd/docker-buildx/tools.go b/cmd/docker-buildx/tools.go index 464dc0a..5cfae35 100644 --- a/cmd/docker-buildx/tools.go +++ b/cmd/docker-buildx/tools.go @@ -1,3 +1,4 @@ +//go:build tools // +build tools package tools diff --git a/docs.md b/docs.md index b067213..f1ad96c 100644 --- a/docs.md +++ b/docs.md @@ -16,11 +16,12 @@ Woodpecker CI plugin to build multiarch Docker images with buildx. This plugin i - Build without push - Use custom registries - Build based on existing tags when needed +- Push to multible registries/repos It will automatically generate buildkit configuration to use custom CA certificate if following conditions are met: - Setting `buildkit_config` is not set -- Custom `registry` value is provided +- Custom `registry`/`logins` value is provided - File exists `/etc/docker/certs.d//ca.crt` > NB! To mount custom CA you can use Woodpecker CI runner configuration environment `WOODPECKER_BACKEND_DOCKER_VOLUMES` with value `/etc/ssl/certs:/etc/ssl/certs:ro,/etc/docker/certs.d:/etc/docker/certs.d:ro`. And have created file `/etc/docker/certs.d//ca.crt` with CA certificate on runner server host. @@ -112,3 +113,23 @@ It will automatically generate buildkit configuration to use custom CA certifica | `no_cache` | `false` | disables the usage of cached intermediate containers | `add_host` | *none* | sets additional host:ip mapping | `output` | *none* | sets build output in format `type=[,=]` +| `logins` | *none* | option to log into multible registrys + +## Multi registry push example + +Only supported with `woodpecker >= 1.0.0` (next-da997fa3). + +```yml +settings: + repo: a6543/tmp,codeberg.org/6543/tmp + tag: demo + logins: + - registry: https://index.docker.io/v1/ + username: a6543 + password: + from_secret: docker_token + - registry: https://codeberg.org + username: "6543" + password: + from_secret: cb_token +``` diff --git a/go.mod b/go.mod index dbcfe8b..7bc572a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module codeberg.org/woodpecker-plugins/plugin-docker-buildx -go 1.17 +go 1.18 require ( github.com/coreos/go-semver v0.3.0 diff --git a/go.sum b/go.sum index 38aabe7..947b4d1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= @@ -36,10 +35,8 @@ github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk= github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= @@ -50,38 +47,23 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f h1:OKYpQQVE3DKSc3r3zHVzq46vq5YH7x8xpR3/k9ixmUg= golang.org/x/tools v0.1.11-0.20220513221640-090b14e8501f/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.3.3 h1:oDx7VAwstgpYpb3wv0oxiZlxY+foCpRAwY7Vk6XpAgA= honnef.co/go/tools v0.3.3/go.mod h1:jzwdWgg7Jdq75wlfblQxO4neNaFFSvgc1tD5Wv8U0Yw= diff --git a/plugin/docker.go b/plugin/docker.go index 7b06f05..7a18b86 100644 --- a/plugin/docker.go +++ b/plugin/docker.go @@ -10,19 +10,6 @@ import ( "github.com/urfave/cli/v2" ) -// helper function to create the docker login command. -func commandLogin(login Login) *exec.Cmd { - if login.Email != "" { - return commandLoginEmail(login) - } - return exec.Command( - dockerExe, "login", - "-u", login.Username, - "-p", login.Password, - login.Registry, - ) -} - // helper to check if args match "docker pull " func isCommandPull(args []string) bool { return len(args) > 2 && args[1] == "pull" diff --git a/plugin/impl.go b/plugin/impl.go index 5866a14..af1bded 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -1,6 +1,7 @@ package plugin import ( + "encoding/json" "fmt" "net/url" "os" @@ -68,18 +69,50 @@ type Build struct { // Settings for the Plugin. type Settings struct { - Daemon Daemon - Login Login - Build Build - Dryrun bool - Cleanup bool + Daemon Daemon + Logins []Login + LoginsRaw string + DefaultLogin Login + Build Build + Dryrun bool + Cleanup bool +} + +func (l Login) anonymous() bool { + return l.Username == "" || l.Password == "" +} + +// Init initialise plugin settings +func (p *Plugin) InitSettings() error { + if err := json.Unmarshal([]byte(p.settings.LoginsRaw), &p.settings.Logins); err != nil { + return fmt.Errorf("Could not unmarshal logins: %v", err) + } + + p.settings.Build.Branch = p.pipeline.Repo.Branch + p.settings.Build.Ref = p.pipeline.Commit.Ref + if p.settings.DefaultLogin.anonymous() { + p.settings.Logins = append(p.settings.Logins, p.settings.DefaultLogin) + } else { + p.settings.Logins = prepend(p.settings.Logins, p.settings.DefaultLogin) + } + + p.settings.Daemon.Registry = p.settings.Logins[0].Registry + + return nil } // Validate handles the settings validation of the plugin. func (p *Plugin) Validate() error { - p.settings.Build.Branch = p.pipeline.Repo.Branch - p.settings.Build.Ref = p.pipeline.Commit.Ref - p.settings.Daemon.Registry = p.settings.Login.Registry + if err := p.InitSettings(); err != nil { + return err + } + + // beside the default login all other logins need to set a username and password + for _, l := range p.settings.Logins[1:] { + if l.anonymous() { + return fmt.Errorf("beside the default login all other logins need to set a username and password") + } + } if p.settings.Build.TagsAuto { // return true if tag event or default branch @@ -120,28 +153,38 @@ func (p *Plugin) Validate() error { } func (p *Plugin) writeBuildkitConfig() error { + // no buildkit config, automatically generate buildkit configuration to use a custom CA certificate for each registry if p.settings.Daemon.BuildkitConfig == "" && p.settings.Daemon.Registry != "" { - registry := p.settings.Daemon.Registry - u, err := url.Parse(registry) - if err == nil && u.Host != "" { - registry = u.Host - } + for _, login := range p.settings.Logins { + if registry := login.Registry; registry != "" { + u, err := url.Parse(registry) + if err != nil { + return fmt.Errorf("could not parse registry address: %s: %v", registry, err) + } + if u.Host != "" { + registry = u.Host + } - caPath := fmt.Sprintf("/etc/docker/certs.d/%s/ca.crt", registry) - ca, err := os.Open(caPath) - if err != nil && !os.IsNotExist(err) { - logrus.Warnf("error reading %s: %v", caPath, err) - } else if err == nil { - ca.Close() - p.settings.Daemon.BuildkitConfig = fmt.Sprintf(buildkitConfigTemplate, registry, caPath) + caPath := fmt.Sprintf("/etc/docker/certs.d/%s/ca.crt", registry) + ca, err := os.Open(caPath) + if err != nil && !os.IsNotExist(err) { + logrus.Warnf("error reading %s: %v", caPath, err) + } else if err == nil { + ca.Close() + p.settings.Daemon.BuildkitConfig += fmt.Sprintf(buildkitConfigTemplate, registry, caPath) + } + } } } + + // save buildkit config as described if p.settings.Daemon.BuildkitConfig != "" { err := os.WriteFile(buildkitConfig, []byte(p.settings.Daemon.BuildkitConfig), 0o600) if err != nil { return fmt.Errorf("error writing buildkit.toml: %s", err) } } + return nil } @@ -164,23 +207,19 @@ func (p *Plugin) Execute() error { } // Create Auth Config File - if p.settings.Login.Config != "" { + if p.settings.Logins[0].Config != "" { os.MkdirAll(dockerHome, 0o600) path := filepath.Join(dockerHome, "config.json") - err := os.WriteFile(path, []byte(p.settings.Login.Config), 0o600) + err := os.WriteFile(path, []byte(p.settings.Logins[0].Config), 0o600) if err != nil { return fmt.Errorf("error writing config.json: %s", err) } } // login to the Docker registry - if p.settings.Login.Password != "" { - cmd := commandLogin(p.settings.Login) - err := cmd.Run() - if err != nil { - return fmt.Errorf("error authenticating: %s", err) - } + if err := p.Login(); err != nil { + return err } if err := p.writeBuildkitConfig(); err != nil { @@ -188,9 +227,9 @@ func (p *Plugin) Execute() error { } switch { - case p.settings.Login.Password != "": + case p.settings.Logins[0].Password != "": fmt.Println("Detected registry credentials") - case p.settings.Login.Config != "": + case p.settings.Logins[0].Config != "": fmt.Println("Detected registry credentials file") default: fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.") @@ -228,3 +267,7 @@ func (p *Plugin) Execute() error { return nil } + +func prepend[Type any](slice []Type, elems ...Type) []Type { + return append(elems, slice...) +} diff --git a/plugin/login.go b/plugin/login.go new file mode 100644 index 0000000..27d419d --- /dev/null +++ b/plugin/login.go @@ -0,0 +1,36 @@ +package plugin + +import ( + "fmt" + "os/exec" +) + +// login to the registrys +func (p *Plugin) Login() error { + registrys := make(map[string]bool) + for _, login := range p.settings.Logins { + if !registrys[login.Registry] && !login.anonymous() { + // only log into a registry once + registrys[login.Registry] = true + cmd := commandLogin(login) + err := cmd.Run() + if err != nil { + return fmt.Errorf("error authenticating: %s", err) + } + } + } + return nil +} + +// helper function to create the docker login command. +func commandLogin(login Login) *exec.Cmd { + if login.Email != "" { + return commandLoginEmail(login) + } + return exec.Command( + dockerExe, "login", + "-u", login.Username, + "-p", login.Password, + login.Registry, + ) +} diff --git a/plugin/tags.go b/plugin/tags.go index 76f7b56..e5307ad 100644 --- a/plugin/tags.go +++ b/plugin/tags.go @@ -27,7 +27,7 @@ func DefaultTagSuffix(ref, suffix string) ([]string, error) { return tags, nil } -func splitOff(input string, delim string) string { +func splitOff(input, delim string) string { parts := strings.SplitN(input, delim, 2) if len(parts) == 2 { diff --git a/plugin/tags_test.go b/plugin/tags_test.go index 89b65da..e9bffdc 100644 --- a/plugin/tags_test.go +++ b/plugin/tags_test.go @@ -6,7 +6,7 @@ import ( ) func Test_stripTagPrefix(t *testing.T) { - var tests = []struct { + tests := []struct { Before string After string }{ @@ -24,7 +24,7 @@ func Test_stripTagPrefix(t *testing.T) { } func TestDefaultTags(t *testing.T) { - var tests = []struct { + tests := []struct { Before string After []string }{ @@ -50,7 +50,7 @@ func TestDefaultTags(t *testing.T) { } func TestDefaultTagsError(t *testing.T) { - var tests = []string{ + tests := []string{ "refs/tags/x1.0.0", "refs/tags/20190203", } @@ -64,7 +64,7 @@ func TestDefaultTagsError(t *testing.T) { } func TestDefaultTagSuffix(t *testing.T) { - var tests = []struct { + tests := []struct { Before string Suffix string After []string