build: add container labels and annotations (#4071)

This adds a new helper which retrieves the build metadata, uses it to generate container labels, and refactors XFlags uses to utilize the same machinery.

Co-authored-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/4079/head
Amir Zarrinkafsh 2022-09-26 10:05:59 +10:00 committed by GitHub
parent a7a217a036
commit e3f5a574fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 301 additions and 72 deletions

View File

@ -40,7 +40,7 @@ func cmdBuildRun(cobraCmd *cobra.Command, args []string) {
cmdCleanRun(cobraCmd, args) cmdCleanRun(cobraCmd, args)
xflags, err := getXFlags(branch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "") buildMetaData, err := getBuild(branch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -62,11 +62,11 @@ func cmdBuildRun(cobraCmd *cobra.Command, args []string) {
if buildkite { if buildkite {
log.Info("Building Authelia Go binaries with gox...") log.Info("Building Authelia Go binaries with gox...")
buildAutheliaBinaryGOX(xflags) buildAutheliaBinaryGOX(buildMetaData.XFlags())
} else { } else {
log.Info("Building Authelia Go binary...") log.Info("Building Authelia Go binary...")
buildAutheliaBinaryGO(xflags) buildAutheliaBinaryGO(buildMetaData.XFlags())
} }
cleanAssets() cleanAssets()

View File

@ -13,15 +13,17 @@ import (
var container string var container string
var containers = []string{"dev", "coverage"} var (
var defaultContainer = "dev" containers = []string{"dev", "coverage"}
var ciBranch = os.Getenv("BUILDKITE_BRANCH") defaultContainer = "dev"
var ciPullRequest = os.Getenv("BUILDKITE_PULL_REQUEST") ciBranch = os.Getenv("BUILDKITE_BRANCH")
var ciTag = os.Getenv("BUILDKITE_TAG") ciPullRequest = os.Getenv("BUILDKITE_PULL_REQUEST")
var dockerTags = regexp.MustCompile(`v(?P<Patch>(?P<Minor>(?P<Major>\d+)\.\d+)\.\d+.*)`) ciTag = os.Getenv("BUILDKITE_TAG")
var ignoredSuffixes = regexp.MustCompile("alpha|beta") dockerTags = regexp.MustCompile(`v(?P<Patch>(?P<Minor>(?P<Major>\d+)\.\d+)\.\d+.*)`)
var publicRepo = regexp.MustCompile(`.*:.*`) ignoredSuffixes = regexp.MustCompile("alpha|beta")
var tags = dockerTags.FindStringSubmatch(ciTag) publicRepo = regexp.MustCompile(`.*:.*`)
tags = dockerTags.FindStringSubmatch(ciTag)
)
func newDockerCmd() (cmd *cobra.Command) { func newDockerCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
@ -143,13 +145,12 @@ func dockerBuildOfficialImage(arch string) error {
filename := "Dockerfile" filename := "Dockerfile"
dockerfile := fmt.Sprintf("%s.%s", filename, arch) dockerfile := fmt.Sprintf("%s.%s", filename, arch)
flags, err := getXFlags(ciBranch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "") buildMetaData, err := getBuild(ciBranch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return docker.Build(IntermediateDockerImageName, dockerfile, ".", return docker.Build(IntermediateDockerImageName, dockerfile, ".", buildMetaData)
strings.Join(flags, " "))
} }
func login(docker *Docker, registry string) { func login(docker *Docker, registry string) {

View File

@ -2,66 +2,63 @@ package cmd
import ( import (
"fmt" "fmt"
"strings" "strconv"
"time" "time"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
func getXFlags(branch, build, extra string) (flags []string, err error) { func getBuild(branch, buildNumber, extra string) (b *Build, err error) {
if branch == "" { var out string
out, _, err := utils.RunCommandAndReturnOutput("git rev-parse --abbrev-ref HEAD")
if err != nil { b = &Build{
return flags, fmt.Errorf("error getting branch with git rev-parse: %w", err) Branch: branch,
Extra: extra,
}
if buildNumber != "" {
if b.Number, err = strconv.Atoi(buildNumber); err != nil {
return nil, fmt.Errorf("error parsing provided build number: %w", err)
}
}
if b.Branch == "" {
if out, _, err = utils.RunCommandAndReturnOutput("git rev-parse --abbrev-ref HEAD"); err != nil {
return nil, fmt.Errorf("error getting branch with git rev-parse: %w", err)
} }
if out == "" { if out == "" {
branch = "master" b.Branch = "master"
} else { } else {
branch = out b.Branch = out
} }
} }
gitTagCommit, _, err := utils.RunCommandAndReturnOutput("git rev-list --tags --max-count=1") var (
if err != nil { gitTagCommit string
return flags, fmt.Errorf("error getting tag commit with git rev-list: %w", err) )
if gitTagCommit, _, err = utils.RunCommandAndReturnOutput("git rev-list --tags --max-count=1"); err != nil {
return nil, fmt.Errorf("error getting tag commit with git rev-list: %w", err)
} }
tag, _, err := utils.RunCommandAndReturnOutput(fmt.Sprintf("git describe --tags --abbrev=0 %s", gitTagCommit)) if b.Tag, _, err = utils.RunCommandAndReturnOutput(fmt.Sprintf("git describe --tags --abbrev=0 %s", gitTagCommit)); err != nil {
if err != nil { return nil, fmt.Errorf("error getting tag with git describe: %w", err)
return flags, fmt.Errorf("error getting tag with git describe: %w", err)
} }
commit, _, err := utils.RunCommandAndReturnOutput("git rev-parse HEAD") if b.Commit, _, err = utils.RunCommandAndReturnOutput("git rev-parse HEAD"); err != nil {
if err != nil { return nil, fmt.Errorf("error getting commit with git rev-parse: %w", err)
return flags, fmt.Errorf("error getting commit with git rev-parse: %w", err)
} }
var states []string if gitTagCommit == b.Commit {
b.Tagged = true
if gitTagCommit == commit {
states = append(states, "tagged")
} else {
states = append(states, "untagged")
} }
if _, exitCode, _ := utils.RunCommandAndReturnOutput("git diff --quiet"); exitCode != 0 { if _, exitCode, _ := utils.RunCommandAndReturnOutput("git diff --quiet"); exitCode == 0 {
states = append(states, "dirty") b.Clean = true
} else {
states = append(states, "clean")
} }
if build == "" { b.Date = time.Now()
build = "manual"
}
return []string{ return b, nil
fmt.Sprintf(fmtLDFLAGSX, "BuildBranch", branch),
fmt.Sprintf(fmtLDFLAGSX, "BuildTag", tag),
fmt.Sprintf(fmtLDFLAGSX, "BuildCommit", commit),
fmt.Sprintf(fmtLDFLAGSX, "BuildDate", time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700")),
fmt.Sprintf(fmtLDFLAGSX, "BuildState", strings.Join(states, " ")),
fmt.Sprintf(fmtLDFLAGSX, "BuildExtra", extra),
fmt.Sprintf(fmtLDFLAGSX, "BuildNumber", build),
}, nil
} }

View File

@ -1,6 +1,13 @@
package cmd package cmd
import ( import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -8,11 +15,20 @@ import (
type Docker struct{} type Docker struct{}
// Build build a docker image. // Build build a docker image.
func (d *Docker) Build(tag, dockerfile, target, ldflags string) error { func (d *Docker) Build(tag, dockerfile, target string, buildMetaData *Build) error {
return utils.CommandWithStdout( args := []string{"build", "-t", tag, "-f", dockerfile, "--progress=plain"}
"docker", "build", "-t", tag, "-f", dockerfile,
"--progress=plain", "--build-arg", "LDFLAGS_EXTRA="+ldflags, for label, value := range buildMetaData.ContainerLabels() {
target).Run() if value == "" {
continue
}
args = append(args, "--label", fmt.Sprintf("%s=%s", label, value))
}
args = append(args, "--build-arg", "LDFLAGS_EXTRA="+strings.Join(buildMetaData.XFlags(), " "), target)
return utils.CommandWithStdout("docker", args...).Run()
} }
// Tag tag a docker image. // Tag tag a docker image.
@ -27,10 +43,101 @@ func (d *Docker) Login(username, password, registry string) error {
// Manifest push a docker manifest to dockerhub. // Manifest push a docker manifest to dockerhub.
func (d *Docker) Manifest(tag1, tag2 string) error { func (d *Docker) Manifest(tag1, tag2 string) error {
return utils.CommandWithStdout("docker", "build", "-t", tag1, "-t", tag2, "--platform", "linux/amd64,linux/arm/v7,linux/arm64", "--builder", "buildx", "--push", ".").Run() args := []string{"build", "-t", tag1, "-t", tag2}
annotations := ""
buildMetaData, err := getBuild(ciBranch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "")
if err != nil {
return err
}
for label, value := range buildMetaData.ContainerLabels() {
if value == "" {
continue
}
annotations += fmt.Sprintf("annotation.%s=%s,", label, value)
args = append(args, "--label", fmt.Sprintf("%s=%s", label, value))
}
var baseImageTag string
from, err := getDockerfileDirective("Dockerfile", "FROM")
if err == nil {
baseImageTag = from[strings.IndexRune(from, ':')+1:]
args = append(args, "--label", "org.opencontainers.image.base.name=docker.io/library/alpine:"+baseImageTag)
}
resp, err := http.Get("https://hub.docker.com/v2/repositories/library/alpine/tags/" + baseImageTag + "/images")
if err != nil {
return err
}
defer resp.Body.Close()
images := DockerImages{}
if err = json.NewDecoder(resp.Body).Decode(&images); err != nil {
return err
}
var (
digestAMD64, digestARM, digestARM64 string
)
for _, platform := range []string{"linux/amd64", "linux/arm/v7", "linux/arm64"} {
for _, image := range images {
if !image.Match(platform) {
continue
}
switch platform {
case "linux/amd64":
digestAMD64 = image.Digest
case "linux/arm/v7":
digestARM = image.Digest
case "linux/arm64":
digestARM64 = image.Digest
}
}
}
finalArgs := make([]string, len(args))
copy(finalArgs, args)
finalArgs = append(finalArgs, "--output", "type=image,\"name="+dockerhub+"/"+DockerImageName+","+ghcr+"/"+DockerImageName+"\","+annotations+"annotation.org.opencontainers.image.base.name=docker.io/library/alpine:"+baseImageTag+",annotation[linux/amd64].org.opencontainers.image.base.digest="+digestAMD64+",annotation[linux/arm/v7].org.opencontainers.image.base.digest="+digestARM+",annotation[linux/arm64].org.opencontainers.image.base.digest="+digestARM64, "--platform", "linux/amd64,linux/arm/v7,linux/arm64", "--builder", "buildx", "--push", ".")
if err = utils.CommandWithStdout("docker", finalArgs...).Run(); err != nil {
return err
}
return nil
} }
// PublishReadme push README.md to dockerhub. // PublishReadme push README.md to dockerhub.
func (d *Docker) PublishReadme() error { func (d *Docker) PublishReadme() error {
return utils.CommandWithStdout("bash", "-c", `token=$(curl -fs --retry 3 -H "Content-Type: application/json" -X "POST" -d '{"username": "'$DOCKER_USERNAME'", "password": "'$DOCKER_PASSWORD'"}' https://hub.docker.com/v2/users/login/ | jq -r .token) && jq -n --arg msg "$(cat README.md | sed -r 's/(\<img\ src\=\")(\.\/)/\1https:\/\/github.com\/authelia\/authelia\/raw\/master\//' | sed 's/\.\//https:\/\/github.com\/authelia\/authelia\/blob\/master\//g' | sed '/start \[contributing\]/ a <a href="https://github.com/authelia/authelia/graphs/contributors"><img src="https://opencollective.com/authelia-sponsors/contributors.svg?width=890" /></a>' | sed '/Thanks goes to/,/### Backers/{/### Backers/!d}')" '{"registry":"registry-1.docker.io","full_description": $msg }' | curl -fs --retry 3 -o /dev/null -L -X "PATCH" -H "Content-Type: application/json" -H "Authorization: JWT $token" -d @- https://hub.docker.com/v2/repositories/authelia/authelia/`).Run() return utils.CommandWithStdout("bash", "-c", `token=$(curl -fs --retry 3 -H "Content-Type: application/json" -X "POST" -d '{"username": "'$DOCKER_USERNAME'", "password": "'$DOCKER_PASSWORD'"}' https://hub.docker.com/v2/users/login/ | jq -r .token) && jq -n --arg msg "$(cat README.md | sed -r 's/(\<img\ src\=\")(\.\/)/\1https:\/\/github.com\/authelia\/authelia\/raw\/master\//' | sed 's/\.\//https:\/\/github.com\/authelia\/authelia\/blob\/master\//g' | sed '/start \[contributing\]/ a <a href="https://github.com/authelia/authelia/graphs/contributors"><img src="https://opencollective.com/authelia-sponsors/contributors.svg?width=890" /></a>' | sed '/Thanks goes to/,/### Backers/{/### Backers/!d}')" '{"registry":"registry-1.docker.io","full_description": $msg }' | curl -fs --retry 3 -o /dev/null -L -X "PATCH" -H "Content-Type: application/json" -H "Authorization: JWT $token" -d @- https://hub.docker.com/v2/repositories/authelia/authelia/`).Run()
} }
func getDockerfileDirective(filePath, directive string) (from string, err error) {
var f *os.File
if f, err = os.Open(filePath); err != nil {
return "", err
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
data := s.Text()
if strings.HasPrefix(data, directive+" ") {
return data[5:], nil
}
}
return "", nil
}

View File

@ -1,7 +1,130 @@
package cmd package cmd
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/authelia/authelia/v4/internal/utils"
)
// HostEntry represents an entry in /etc/hosts. // HostEntry represents an entry in /etc/hosts.
type HostEntry struct { type HostEntry struct {
Domain string Domain string
IP string IP string
} }
// DockerImages represents some of the data from the docker images API.
type DockerImages []DockerImage
// DockerImage represents some of the data from the docker images API.
type DockerImage struct {
Architecture string `json:"architecture"`
Variant interface{} `json:"variant"`
Digest string `json:"digest"`
OS string `json:"os"`
}
// Match returns true if this image matches the platform.
func (d DockerImage) Match(platform string) bool {
parts := []string{d.OS, d.Architecture}
if strings.Join(parts, "/") == platform {
return true
}
if d.Variant == nil {
return false
}
parts = append(parts, d.Variant.(string))
return strings.Join(parts, "/") == platform
}
// Build represents a builds metadata.
type Build struct {
Branch string
Tag string
Commit string
Tagged bool
Clean bool
Extra string
Number int
Date time.Time
}
// States returns the state tags for this Build.
func (b Build) States() []string {
var states []string
if b.Tagged {
states = append(states, "tagged")
} else {
states = append(states, "untagged")
}
if b.Clean {
states = append(states, "clean")
} else {
states = append(states, "dirty")
}
return states
}
// State returns the state tags string for this Build.
func (b Build) State() string {
return strings.Join(b.States(), " ")
}
// XFlags returns the XFlags for this Build.
func (b Build) XFlags() []string {
return []string{
fmt.Sprintf(fmtLDFLAGSX, "BuildBranch", b.Branch),
fmt.Sprintf(fmtLDFLAGSX, "BuildTag", b.Tag),
fmt.Sprintf(fmtLDFLAGSX, "BuildCommit", b.Commit),
fmt.Sprintf(fmtLDFLAGSX, "BuildDate", b.Date.Format("Mon, 02 Jan 2006 15:04:05 -0700")),
fmt.Sprintf(fmtLDFLAGSX, "BuildState", b.State()),
fmt.Sprintf(fmtLDFLAGSX, "BuildExtra", b.Extra),
fmt.Sprintf(fmtLDFLAGSX, "BuildNumber", strconv.Itoa(b.Number)),
}
}
// ContainerLabels returns the container labels for this Build.
func (b Build) ContainerLabels() (labels map[string]string) {
var version string
switch {
case b.Clean && b.Tagged:
version = utils.VersionAdv(b.Tag, b.State(), b.Commit, b.Branch, b.Extra)
case b.Clean:
version = fmt.Sprintf("%s-pre+%s.%s", b.Tag, b.Branch, b.Commit)
case b.Tagged:
version = fmt.Sprintf("%s-dirty", b.Tag)
default:
version = fmt.Sprintf("%s-dirty+%s.%s", b.Tag, b.Branch, b.Commit)
}
if strings.HasPrefix(version, "v") && len(version) > 1 {
version = version[1:]
}
labels = map[string]string{
"org.opencontainers.image.created": b.Date.Format(time.RFC3339),
"org.opencontainers.image.authors": "",
"org.opencontainers.image.url": "https://github.com/authelia/authelia/pkgs/container/authelia",
"org.opencontainers.image.documentation": "https://www.authelia.com",
"org.opencontainers.image.source": fmt.Sprintf("https://github.com/authelia/authelia/tree/%s", b.Commit),
"org.opencontainers.image.version": version,
"org.opencontainers.image.revision": b.Commit,
"org.opencontainers.image.vendor": "Authelia",
"org.opencontainers.image.licenses": "Apache-2.0",
"org.opencontainers.image.ref.name": "",
"org.opencontainers.image.title": "authelia",
"org.opencontainers.image.description": "Authelia is an open-source authentication and authorization server providing two-factor authentication and single sign-on (SSO) for your applications via a web portal.",
}
return labels
}

View File

@ -37,10 +37,10 @@ func cmdXFlagsRun(cobraCmd *cobra.Command, _ []string) {
log.Fatal(err) log.Fatal(err)
} }
flags, err := getXFlags("", build, extra) buildMetaData, err := getBuild("", build, extra)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println(strings.Join(flags, " ")) fmt.Println(strings.Join(buildMetaData.XFlags(), " "))
} }

View File

@ -35,10 +35,11 @@ var BuildNumber = "0"
// BuildTag i.e. v1.0.0. If dirty and tagged are present it returns <BuildTag>-dirty. Otherwise, the following is the // BuildTag i.e. v1.0.0. If dirty and tagged are present it returns <BuildTag>-dirty. Otherwise, the following is the
// format: untagged-<BuildTag>-dirty-<BuildExtra> (<BuildBranch>, <BuildCommit>). // format: untagged-<BuildTag>-dirty-<BuildExtra> (<BuildBranch>, <BuildCommit>).
func Version() (versionString string) { func Version() (versionString string) {
return version(BuildTag, BuildState, BuildCommit, BuildBranch, BuildExtra) return VersionAdv(BuildTag, BuildState, BuildCommit, BuildBranch, BuildExtra)
} }
func version(tag, state, commit, branch, extra string) (version string) { // VersionAdv takes inputs to generate the version.
func VersionAdv(tag, state, commit, branch, extra string) (version string) {
b := strings.Builder{} b := strings.Builder{}
states := strings.Split(state, " ") states := strings.Split(state, " ")

View File

@ -15,27 +15,27 @@ func TestVersionDefault(t *testing.T) {
func TestVersion(t *testing.T) { func TestVersion(t *testing.T) {
var v string var v string
v = version("v4.90.0", "tagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") v = VersionAdv("v4.90.0", "tagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "")
assert.Equal(t, "v4.90.0", v) assert.Equal(t, "v4.90.0", v)
v = version("v4.90.0", "tagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "freshports") v = VersionAdv("v4.90.0", "tagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "freshports")
assert.Equal(t, "v4.90.0-freshports", v) assert.Equal(t, "v4.90.0-freshports", v)
v = version("v4.90.0", "tagged dirty", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") v = VersionAdv("v4.90.0", "tagged dirty", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "")
assert.Equal(t, "v4.90.0-dirty", v) assert.Equal(t, "v4.90.0-dirty", v)
v = version("v4.90.0", "untagged dirty", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") v = VersionAdv("v4.90.0", "untagged dirty", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "")
assert.Equal(t, "untagged-v4.90.0-dirty (master, 50d8b4a)", v) assert.Equal(t, "untagged-v4.90.0-dirty (master, 50d8b4a)", v)
v = version("v4.90.0", "untagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") v = VersionAdv("v4.90.0", "untagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "")
assert.Equal(t, "untagged-v4.90.0 (master, 50d8b4a)", v) assert.Equal(t, "untagged-v4.90.0 (master, 50d8b4a)", v)
v = version("v4.90.0", "untagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "freshports") v = VersionAdv("v4.90.0", "untagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "freshports")
assert.Equal(t, "untagged-v4.90.0-freshports (master, 50d8b4a)", v) assert.Equal(t, "untagged-v4.90.0-freshports (master, 50d8b4a)", v)
v = version("v4.90.0", "untagged clean", "", "master", "") v = VersionAdv("v4.90.0", "untagged clean", "", "master", "")
assert.Equal(t, "untagged-v4.90.0 (master, unknown)", v) assert.Equal(t, "untagged-v4.90.0 (master, unknown)", v)
v = version("v4.90.0", "", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") v = VersionAdv("v4.90.0", "", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "")
assert.Equal(t, "untagged-v4.90.0-dirty (master, 50d8b4a)", v) assert.Equal(t, "untagged-v4.90.0-dirty (master, 50d8b4a)", v)
} }