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

View File

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

View File

@ -2,66 +2,63 @@ package cmd
import (
"fmt"
"strings"
"strconv"
"time"
"github.com/authelia/authelia/v4/internal/utils"
)
func getXFlags(branch, build, extra string) (flags []string, err error) {
if branch == "" {
out, _, err := utils.RunCommandAndReturnOutput("git rev-parse --abbrev-ref HEAD")
if err != nil {
return flags, fmt.Errorf("error getting branch with git rev-parse: %w", err)
func getBuild(branch, buildNumber, extra string) (b *Build, err error) {
var out string
b = &Build{
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 == "" {
branch = "master"
b.Branch = "master"
} else {
branch = out
b.Branch = out
}
}
gitTagCommit, _, err := utils.RunCommandAndReturnOutput("git rev-list --tags --max-count=1")
if err != nil {
return flags, fmt.Errorf("error getting tag commit with git rev-list: %w", err)
var (
gitTagCommit string
)
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 err != nil {
return flags, fmt.Errorf("error getting tag with git describe: %w", err)
if b.Tag, _, err = utils.RunCommandAndReturnOutput(fmt.Sprintf("git describe --tags --abbrev=0 %s", gitTagCommit)); err != nil {
return nil, fmt.Errorf("error getting tag with git describe: %w", err)
}
commit, _, err := utils.RunCommandAndReturnOutput("git rev-parse HEAD")
if err != nil {
return flags, fmt.Errorf("error getting commit with git rev-parse: %w", err)
if b.Commit, _, err = utils.RunCommandAndReturnOutput("git rev-parse HEAD"); err != nil {
return nil, fmt.Errorf("error getting commit with git rev-parse: %w", err)
}
var states []string
if gitTagCommit == commit {
states = append(states, "tagged")
} else {
states = append(states, "untagged")
if gitTagCommit == b.Commit {
b.Tagged = true
}
if _, exitCode, _ := utils.RunCommandAndReturnOutput("git diff --quiet"); exitCode != 0 {
states = append(states, "dirty")
} else {
states = append(states, "clean")
if _, exitCode, _ := utils.RunCommandAndReturnOutput("git diff --quiet"); exitCode == 0 {
b.Clean = true
}
if build == "" {
build = "manual"
}
b.Date = time.Now()
return []string{
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
return b, nil
}

View File

@ -1,6 +1,13 @@
package cmd
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/authelia/authelia/v4/internal/utils"
)
@ -8,11 +15,20 @@ import (
type Docker struct{}
// Build build a docker image.
func (d *Docker) Build(tag, dockerfile, target, ldflags string) error {
return utils.CommandWithStdout(
"docker", "build", "-t", tag, "-f", dockerfile,
"--progress=plain", "--build-arg", "LDFLAGS_EXTRA="+ldflags,
target).Run()
func (d *Docker) Build(tag, dockerfile, target string, buildMetaData *Build) error {
args := []string{"build", "-t", tag, "-f", dockerfile, "--progress=plain"}
for label, value := range buildMetaData.ContainerLabels() {
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.
@ -27,10 +43,101 @@ func (d *Docker) Login(username, password, registry string) error {
// Manifest push a docker manifest to dockerhub.
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.
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()
}
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
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/authelia/authelia/v4/internal/utils"
)
// HostEntry represents an entry in /etc/hosts.
type HostEntry struct {
Domain 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)
}
flags, err := getXFlags("", build, extra)
buildMetaData, err := getBuild("", build, extra)
if err != nil {
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
// format: untagged-<BuildTag>-dirty-<BuildExtra> (<BuildBranch>, <BuildCommit>).
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{}
states := strings.Split(state, " ")

View File

@ -15,27 +15,27 @@ func TestVersionDefault(t *testing.T) {
func TestVersion(t *testing.T) {
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)
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)
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)
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)
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)
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)
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)
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)
}