ef56bac838
### 📖 Summary Originally, the cache_to and cache_from were being converted into string arrays. Then their contents were looped over to add to the build command. This has the side affect of stopping the user from setting additional options for the cache-to and cache-from args since their command input format uses commas. i.e. `type=registry,ref=imagepath,mode=max` would result in `--cache-to type=registry --cache-to ref=imagepath --cache-to mode=max`. The command was not designed to be used that way. The one reason I can think of for cache_to and cache_from to be arrays is so you could have multiple cache registries. But I can't confirm that the build command even works like this. ### 📑 Build PR Images? PR images are not needed ### 💬 Details _No response_ Reviewed-on: https://codeberg.org/woodpecker-plugins/docker-buildx/pulls/129 Reviewed-by: Patrick Schratz <pat-s@noreply.codeberg.org> Co-authored-by: David Kovari <dakovari@gmail.com> Co-committed-by: David Kovari <dakovari@gmail.com>
406 lines
12 KiB
Go
406 lines
12 KiB
Go
package plugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli/v2"
|
|
|
|
"codeberg.org/woodpecker-plugins/plugin-docker-buildx/utils"
|
|
)
|
|
|
|
// Daemon defines Docker daemon parameters.
|
|
type Daemon struct {
|
|
Registry string // Docker registry
|
|
Mirror string // Docker registry mirror
|
|
Insecure bool // Docker daemon enable insecure registries
|
|
StorageDriver string // Docker daemon storage driver
|
|
StoragePath string // Docker daemon storage path
|
|
Disabled bool // Docker daemon is disabled (already running)
|
|
Debug bool // Docker daemon started in debug mode
|
|
Bip string // Docker daemon network bridge IP address
|
|
DNS cli.StringSlice // Docker daemon dns server
|
|
DNSSearch cli.StringSlice // Docker daemon dns search domain
|
|
MTU string // Docker daemon mtu setting
|
|
IPv6 bool // Docker daemon IPv6 networking
|
|
Experimental bool // Docker daemon enable experimental mode
|
|
BuildkitConfig string // Docker buildkit config
|
|
BuildkitDriverOpt cli.StringSlice // Docker buildkit driveropt args
|
|
BuildkitDebug bool // Docker buildkit debug setting
|
|
}
|
|
|
|
// Login defines Docker login parameters.
|
|
type Login struct {
|
|
// Generic
|
|
Registry string // Docker registry address
|
|
Username string // Docker registry username
|
|
Password string // Docker registry password
|
|
Email string // Docker registry email
|
|
Config string // Docker Auth Config
|
|
Mirrors []string // Docker registry mirrors
|
|
// ECR
|
|
Aws_access_key_id string `json:"aws_access_key_id"` // AWS access key id
|
|
Aws_secret_access_key string `json:"aws_secret_access_key"` // AWS secret access key
|
|
Aws_region string `json:"aws_region"` // AWS region
|
|
}
|
|
|
|
// Build defines Docker build parameters.
|
|
type Build struct {
|
|
Remote string // Git remote URL
|
|
Ref string // Git commit ref
|
|
Branch string // Git repository branch
|
|
Dockerfile string // Docker build Dockerfile
|
|
Context string // Docker build context
|
|
TagsAuto bool // Docker build auto tag
|
|
TagsDefaultName string // Docker build auto tag name override
|
|
TagsSuffix string // Docker build tags with suffix
|
|
Tags cli.StringSlice // Docker build tags
|
|
TagsFile string // Docker build tags read from an file
|
|
LabelsAuto bool // Docker build auto labels
|
|
Labels cli.StringSlice // Docker build labels
|
|
Platforms cli.StringSlice // Docker build target platforms
|
|
Args cli.StringSlice // Docker build args
|
|
ArgsEnv cli.StringSlice // Docker build args from env
|
|
Target string // Docker build target
|
|
Output string // Docker build output
|
|
Pull bool // Docker build pull
|
|
CacheFrom string // Docker build cache-from
|
|
CacheTo string // Docker build cache-to
|
|
CacheImages cli.StringSlice // Docker build cache images
|
|
Compress bool // Docker build compress
|
|
Repo cli.StringSlice // Docker build repository
|
|
NoCache bool // Docker build no-cache
|
|
AddHost cli.StringSlice // Docker build add-host
|
|
Quiet bool // Docker build quiet
|
|
}
|
|
|
|
// Settings for the Plugin.
|
|
type Settings struct {
|
|
// ECR
|
|
AwsRegion string `json:"aws_region"` // AWS region
|
|
EcrScanOnPush bool `json:"ecr_scan_on_push"` // ECR scan on push
|
|
EcrRepositoryPolicy string `json:"ecr_repository_policy"` // ECR repository policy
|
|
EcrLifecyclePolicy string `json:"ecr_lifecycle_policy"` // ECR lifecycle policy
|
|
EcrCreateRepository bool `json:"ecr_create_repository"` // ECR create repository
|
|
AwsAccessKeyId string `json:"aws_access_key_id"` // AWS access key id
|
|
AwsSecretAccessKey string `json:"aws_secret_access_key"` // AWS secret access key
|
|
|
|
// Generic
|
|
Daemon Daemon
|
|
Logins []Login
|
|
LoginsRaw string
|
|
DefaultLogin Login
|
|
Build Build
|
|
Dryrun bool
|
|
Cleanup bool
|
|
CustomCertStore string // e.g. for "/etc/docker/certs.d/<registry>/ca.crt"
|
|
}
|
|
|
|
func (l Login) anonymous() bool {
|
|
return l.Username == "" || l.Password == ""
|
|
}
|
|
|
|
// Init initialise plugin settings
|
|
func (p *Plugin) InitSettings() error {
|
|
if p.settings.LoginsRaw != "" {
|
|
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
|
|
|
|
// check if any Login struct contains AWS credentials
|
|
for _, login := range p.settings.Logins {
|
|
if strings.Contains(login.Registry, "amazonaws.com") {
|
|
p.EcrInit()
|
|
}
|
|
}
|
|
|
|
if p.settings.AwsAccessKeyId != "" && p.settings.AwsSecretAccessKey != "" {
|
|
p.EcrInit()
|
|
}
|
|
|
|
if p.settings.DefaultLogin.Registry != "" && p.settings.Daemon.Mirror != "" {
|
|
p.settings.DefaultLogin.Mirrors = []string{p.settings.Daemon.Mirror}
|
|
}
|
|
|
|
if len(p.settings.Logins) == 0 {
|
|
p.settings.Logins = []Login{p.settings.DefaultLogin}
|
|
} else if !p.settings.DefaultLogin.anonymous() {
|
|
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 {
|
|
if err := p.InitSettings(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isSingleTag(p.settings.Build.TagsDefaultName) {
|
|
return fmt.Errorf("'%s' is not a valid, single tag", p.settings.Build.TagsDefaultName)
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
}
|
|
|
|
// overload tags flag with tags.file if set
|
|
if p.settings.Build.TagsFile != "" {
|
|
tagsFile, err := os.ReadFile(p.settings.Build.TagsFile)
|
|
if err != nil {
|
|
return fmt.Errorf("could not read tags file: %w", err)
|
|
}
|
|
|
|
// split file content into slice of tags
|
|
tagsFileList := strings.Split(strings.TrimSpace(string(tagsFile)), "\n")
|
|
// trim space of each tag
|
|
tagsFileList = utils.Map(tagsFileList, func(s string) string { return strings.TrimSpace(s) })
|
|
|
|
// finally overwrite
|
|
p.settings.Build.Tags = *cli.NewStringSlice(tagsFileList...)
|
|
}
|
|
|
|
if p.settings.Build.TagsAuto {
|
|
// we only generate tags on default branch or an tag event
|
|
if UseDefaultTag(
|
|
p.settings.Build.Ref,
|
|
p.settings.Build.Branch,
|
|
) {
|
|
tag, err := DefaultTagSuffix(
|
|
p.settings.Build.Ref,
|
|
p.settings.Build.TagsDefaultName,
|
|
p.settings.Build.TagsSuffix,
|
|
)
|
|
if err != nil {
|
|
logrus.Printf("cannot build docker image for %s, invalid semantic version", p.settings.Build.Ref)
|
|
return err
|
|
}
|
|
|
|
// include user supplied tags
|
|
tag = append(tag, p.sanitizedUserTags()...)
|
|
|
|
p.settings.Build.Tags = *cli.NewStringSlice(tag...)
|
|
} else {
|
|
logrus.Printf("skipping automated docker build for %s", p.settings.Build.Ref)
|
|
return nil
|
|
}
|
|
} else {
|
|
p.settings.Build.Tags = *cli.NewStringSlice(p.sanitizedUserTags()...)
|
|
}
|
|
|
|
if p.settings.Build.LabelsAuto {
|
|
p.settings.Build.Labels = *cli.NewStringSlice(p.Labels()...)
|
|
}
|
|
|
|
if err := p.generateBuildkitConfig(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) sanitizedUserTags() []string {
|
|
// ignore empty tags
|
|
var tags []string
|
|
for _, t := range p.settings.Build.Tags.Value() {
|
|
t = strings.TrimSpace(t)
|
|
if t != "" {
|
|
tags = append(tags, t)
|
|
}
|
|
}
|
|
return tags
|
|
}
|
|
|
|
type BuildkitConfigTOML struct {
|
|
Debug bool `toml:"debug,omitempty"` // needs to be public for toml lib to use
|
|
Registry map[string]*RegistryInfo `toml:"registry,omitempty"`
|
|
}
|
|
|
|
type RegistryInfo struct {
|
|
Mirrors []string `toml:"mirrors,omitempty"`
|
|
CA []string `toml:"ca,omitempty"`
|
|
}
|
|
|
|
func (p *Plugin) generateBuildkitConfig() error {
|
|
// no buildkit config, automatically generate buildkit configuration
|
|
if p.settings.Daemon.BuildkitConfig == "" {
|
|
|
|
cfg := BuildkitConfigTOML{}
|
|
cfg.Registry = make(map[string]*RegistryInfo)
|
|
|
|
if p.settings.Daemon.BuildkitDebug {
|
|
cfg.Debug = p.settings.Daemon.BuildkitDebug
|
|
logrus.Println("buildkit debug enabled")
|
|
}
|
|
|
|
// use a custom CA certificate for each registry
|
|
if p.settings.Daemon.Registry != "" {
|
|
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
|
|
}
|
|
|
|
// docker hub fix
|
|
if registry == "index.docker.io" {
|
|
registry = "docker.io"
|
|
}
|
|
|
|
caPath := fmt.Sprintf("%s/%s/ca.crt", p.settings.CustomCertStore, 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()
|
|
logrus.Infof("found ca file for '%s' registry", registry)
|
|
// add registry and ca path to buildkit.toml
|
|
if cfg.Registry[registry] == nil {
|
|
cfg.Registry[registry] = new(RegistryInfo)
|
|
}
|
|
cfg.Registry[registry].CA = []string{caPath}
|
|
}
|
|
|
|
if len(login.Mirrors) != 0 {
|
|
if cfg.Registry[registry] == nil {
|
|
cfg.Registry[registry] = new(RegistryInfo)
|
|
}
|
|
cfg.Registry[registry].Mirrors = login.Mirrors
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.Debug || len(cfg.Registry) > 0 {
|
|
tomlData, err := toml.Marshal(cfg)
|
|
if err != nil {
|
|
return fmt.Errorf("error marshaling buildkit.toml: %s", err)
|
|
} else {
|
|
p.settings.Daemon.BuildkitConfig = string(tomlData)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Plugin) writeBuildkitConfig() error {
|
|
// 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
|
|
}
|
|
|
|
// Execute provides the implementation of the plugin.
|
|
func (p *Plugin) Execute() error {
|
|
// start the Docker daemon server
|
|
if !p.settings.Daemon.Disabled {
|
|
// If no custom DNS value set start internal DNS server
|
|
if len(p.settings.Daemon.DNS.Value()) == 0 {
|
|
ip, err := getContainerIP()
|
|
if err != nil {
|
|
logrus.Warnf("error detecting IP address: %v", err)
|
|
} else if ip != "" {
|
|
p.startCoredns()
|
|
p.settings.Daemon.DNS.Set(ip)
|
|
}
|
|
}
|
|
|
|
p.startDaemon()
|
|
}
|
|
|
|
// poll the docker daemon until it is started. This ensures the daemon is
|
|
// ready to accept connections before we proceed.
|
|
for i := 0; i < 15; i++ {
|
|
cmd := commandInfo()
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
break
|
|
}
|
|
time.Sleep(time.Second * 1)
|
|
}
|
|
|
|
// Create Auth Config File
|
|
if p.settings.Logins[0].Config != "" {
|
|
os.MkdirAll(dockerHome, 0o600)
|
|
|
|
path := filepath.Join(dockerHome, "config.json")
|
|
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 err := p.Login(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := p.writeBuildkitConfig(); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch {
|
|
case p.settings.Logins[0].Password != "":
|
|
fmt.Println("Detected registry credentials")
|
|
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.")
|
|
}
|
|
|
|
// add proxy build args
|
|
addProxyBuildArgs(&p.settings.Build)
|
|
|
|
var cmds []*exec.Cmd
|
|
cmds = append(cmds, commandVersion()) // docker version
|
|
cmds = append(cmds, commandInfo()) // docker info
|
|
cmds = append(cmds, commandBuilder(p.settings.Daemon))
|
|
cmds = append(cmds, commandBuildx())
|
|
cmds = append(cmds, commandBuild(p.settings.Build, p.settings.Dryrun)) // docker build
|
|
|
|
// execute all commands in batch mode.
|
|
for _, cmd := range cmds {
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
trace(cmd)
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func prepend[Type any](slice []Type, elems ...Type) []Type {
|
|
return append(elems, slice...)
|
|
}
|