From e3f5a574fee6c9e44d03afdacc0ff3776f26821d Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh Date: Mon, 26 Sep 2022 10:05:59 +1000 Subject: [PATCH] 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 --- cmd/authelia-scripts/cmd/build.go | 6 +- cmd/authelia-scripts/cmd/docker.go | 25 +++-- cmd/authelia-scripts/cmd/helpers.go | 75 ++++++------- cmd/authelia-scripts/cmd/helpers_docker.go | 119 +++++++++++++++++++- cmd/authelia-scripts/cmd/types.go | 123 +++++++++++++++++++++ cmd/authelia-scripts/cmd/xflags.go | 4 +- internal/utils/version.go | 5 +- internal/utils/version_test.go | 16 +-- 8 files changed, 301 insertions(+), 72 deletions(-) diff --git a/cmd/authelia-scripts/cmd/build.go b/cmd/authelia-scripts/cmd/build.go index 337b5ccb8..fa72b83f6 100644 --- a/cmd/authelia-scripts/cmd/build.go +++ b/cmd/authelia-scripts/cmd/build.go @@ -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() diff --git a/cmd/authelia-scripts/cmd/docker.go b/cmd/authelia-scripts/cmd/docker.go index d37b12285..c06385a80 100644 --- a/cmd/authelia-scripts/cmd/docker.go +++ b/cmd/authelia-scripts/cmd/docker.go @@ -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(?P(?P\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(?P(?P\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) { diff --git a/cmd/authelia-scripts/cmd/helpers.go b/cmd/authelia-scripts/cmd/helpers.go index 728ade11e..12f7e7dc8 100644 --- a/cmd/authelia-scripts/cmd/helpers.go +++ b/cmd/authelia-scripts/cmd/helpers.go @@ -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 } diff --git a/cmd/authelia-scripts/cmd/helpers_docker.go b/cmd/authelia-scripts/cmd/helpers_docker.go index 89d098238..11fd93fe8 100644 --- a/cmd/authelia-scripts/cmd/helpers_docker.go +++ b/cmd/authelia-scripts/cmd/helpers_docker.go @@ -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/(\' | 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 +} diff --git a/cmd/authelia-scripts/cmd/types.go b/cmd/authelia-scripts/cmd/types.go index 4da3d28fc..a2712908c 100644 --- a/cmd/authelia-scripts/cmd/types.go +++ b/cmd/authelia-scripts/cmd/types.go @@ -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 +} diff --git a/cmd/authelia-scripts/cmd/xflags.go b/cmd/authelia-scripts/cmd/xflags.go index a2876211f..f9f91f735 100644 --- a/cmd/authelia-scripts/cmd/xflags.go +++ b/cmd/authelia-scripts/cmd/xflags.go @@ -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(), " ")) } diff --git a/internal/utils/version.go b/internal/utils/version.go index 3e051d708..24fa855a5 100644 --- a/internal/utils/version.go +++ b/internal/utils/version.go @@ -35,10 +35,11 @@ var BuildNumber = "0" // BuildTag i.e. v1.0.0. If dirty and tagged are present it returns -dirty. Otherwise, the following is the // format: untagged--dirty- (, ). 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, " ") diff --git a/internal/utils/version_test.go b/internal/utils/version_test.go index 3b8aed08a..68aa9f522 100644 --- a/internal/utils/version_test.go +++ b/internal/utils/version_test.go @@ -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) }