From 0d7b33022c659444617d15c95c3cbfc66561689b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 18 Jun 2021 14:35:43 +1000 Subject: [PATCH] build: add enhanced information (#2067) This commit adjusts the build flags to include version information in the LDFLAGS using the -X options. Additionally this makes the information recorded at build time more comprehensive. All build information can now be obtained via the `authelia build` command, and the `authelia version` command is now `authelia --version`. Lastly this adjusts the Dockerfile to utilize docker cache more effectively. --- .dockerignore | 2 +- Dockerfile | 23 ++++--- Dockerfile.arm32v7 | 36 +++++----- Dockerfile.arm64v8 | 37 +++++----- Dockerfile.coverage | 27 +++++--- cmd/authelia-scripts/cmd_build.go | 14 ++-- cmd/authelia-scripts/cmd_docker.go | 17 +---- cmd/authelia-scripts/cmd_xflags.go | 39 +++++++++++ cmd/authelia-scripts/const.go | 2 + cmd/authelia-scripts/docker.go | 7 +- cmd/authelia-scripts/helpers.go | 67 +++++++++++++++++++ cmd/authelia-scripts/main.go | 2 +- cmd/authelia/const.go | 21 ++++++ cmd/authelia/constants.go | 7 -- cmd/authelia/main.go | 20 ++++-- internal/suites/suite_cli_test.go | 22 +++++- internal/utils/const.go | 4 ++ internal/utils/exec.go | 14 ++++ internal/utils/version.go | 104 +++++++++++++++++++++++++++++ internal/utils/version_test.go | 41 ++++++++++++ 20 files changed, 418 insertions(+), 88 deletions(-) create mode 100644 cmd/authelia-scripts/cmd_xflags.go create mode 100644 cmd/authelia-scripts/helpers.go create mode 100644 cmd/authelia/const.go delete mode 100644 cmd/authelia/constants.go create mode 100644 internal/utils/version.go create mode 100644 internal/utils/version_test.go diff --git a/.dockerignore b/.dockerignore index 22ed8232..1a3b8262 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,4 +20,4 @@ examples # Other internal/server/public_html authelia.service -bootstrap.sh \ No newline at end of file +bootstrap.sh diff --git a/Dockerfile b/Dockerfile index 114bc606..ff211327 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,22 +3,27 @@ # ======================================= FROM golang:1.16.5-alpine AS builder-backend -ARG BUILD_TAG -ARG BUILD_COMMIT - WORKDIR /go/src/app +RUN \ +echo ">> Downloading required apk's..." && \ +apk --no-cache add gcc musl-dev + +COPY go.mod go.sum ./ + +RUN \ +echo ">> Downloading go modules..." && \ +go mod download + COPY / ./ +ARG LDFLAGS_EXTRA # CGO_ENABLED=1 is required for building go-sqlite3 RUN \ -apk --no-cache add gcc musl-dev && \ -go mod download && \ mv public_html internal/server/public_html && \ -echo "Write tag ${BUILD_TAG} and commit ${BUILD_COMMIT} in binary." && \ -sed -i "s/__BUILD_TAG__/${BUILD_TAG}/" cmd/authelia/constants.go && \ -sed -i "s/__BUILD_COMMIT__/${BUILD_COMMIT}/" cmd/authelia/constants.go && \ -GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags netgo -ldflags '-s -w -linkmode external -extldflags -static' -trimpath -o authelia ./cmd/authelia +echo ">> Starting go build..." && \ +GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -tags netgo \ +-ldflags "-s -w -linkmode external ${LDFLAGS_EXTRA} -extldflags -static" -trimpath -o authelia ./cmd/authelia # =================================== # ===== Authelia official image ===== diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 index ec8c4500..5d42122c 100644 --- a/Dockerfile.arm32v7 +++ b/Dockerfile.arm32v7 @@ -3,25 +3,30 @@ # ======================================= FROM golang:1.16.5-alpine AS builder-backend -ARG BUILD_TAG -ARG BUILD_COMMIT -ARG CC_VERSION="v15" - WORKDIR /go/src/app +# CGO_ENABLED=1 and gcc cross-compiler is required for building go-sqlite3 +ARG CC_VERSION="v15" +RUN \ +echo ">> Downloading required apk's..." && \ +apk --no-cache add curl && \ +echo ">> Downloading cross-compiler..." && \ +curl -Lfs "https://github.com/just-containers/musl-cross-make/releases/download/${CC_VERSION}/gcc-9.2.0-arm-linux-musleabihf.tar.xz" | tar -xJ --directory / + +COPY go.mod go.sum ./ + +RUN \ +echo ">> Downloading go modules..." && \ +go mod download + COPY / ./ -# CGO_ENABLED=1 and gcc cross-compiler is required for building go-sqlite3 +ARG LDFLAGS_EXTRA RUN \ -apk --no-cache add curl && \ -curl -Lfs -o /tmp/gcc-9.2.0-arm-linux-musleabihf.tar.xz "https://github.com/just-containers/musl-cross-make/releases/download/${CC_VERSION}/gcc-9.2.0-arm-linux-musleabihf.tar.xz" && \ -tar xf /tmp/gcc-9.2.0-arm-linux-musleabihf.tar.xz -C / && \ -go mod download && \ mv public_html internal/server/public_html && \ -echo "Write tag ${BUILD_TAG} and commit ${BUILD_COMMIT} in binary." && \ -sed -i "s/__BUILD_TAG__/${BUILD_TAG}/" cmd/authelia/constants.go && \ -sed -i "s/__BUILD_COMMIT__/${BUILD_COMMIT}/" cmd/authelia/constants.go && \ -GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-musleabihf-gcc CGO_LDFLAGS=-fuse-ld=bfd go build -tags netgo -ldflags '-s -w -linkmode external -extldflags -static' -trimpath -o authelia ./cmd/authelia +echo ">> Starting go build..." && \ +GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-musleabihf-gcc CGO_LDFLAGS=-fuse-ld=bfd go build -tags netgo \ +-ldflags "-s -w -linkmode external ${LDFLAGS_EXTRA} -extldflags -static" -trimpath -o authelia ./cmd/authelia # =================================== # ===== Authelia official image ===== @@ -32,8 +37,9 @@ WORKDIR /app COPY ./qemu-arm-static /usr/bin/qemu-arm-static -RUN apk --no-cache add ca-certificates su-exec tzdata && \ - rm /usr/bin/qemu-arm-static +RUN \ +apk --no-cache add ca-certificates su-exec tzdata && \ +rm /usr/bin/qemu-arm-static COPY --from=builder-backend /go/src/app/authelia /go/src/app/LICENSE /go/src/app/entrypoint.sh /go/src/app/healthcheck.sh ./ diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 index 23d3ac78..dd2fbef6 100644 --- a/Dockerfile.arm64v8 +++ b/Dockerfile.arm64v8 @@ -3,25 +3,31 @@ # ======================================= FROM golang:1.16.5-alpine AS builder-backend -ARG BUILD_TAG -ARG BUILD_COMMIT -ARG CC_VERSION="v15" - WORKDIR /go/src/app +# CGO_ENABLED=1 and gcc cross-compiler is required for building go-sqlite3 +ARG CC_VERSION="v15" +RUN \ +echo ">> Downloading required apk's..." && \ +apk --no-cache add curl && \ +echo ">> Downloading cross-compiler..." && \ +curl -Lfs "https://github.com/just-containers/musl-cross-make/releases/download/${CC_VERSION}/gcc-9.2.0-aarch64-linux-musl.tar.xz" | tar -xJ --directory / + +COPY go.mod go.sum ./ + +RUN \ +echo ">> Downloading go modules..." && \ +go mod download + COPY / ./ -# CGO_ENABLED=1 and gcc cross-compiler is required for building go-sqlite3 +ARG LDFLAGS_EXTRA RUN \ -apk --no-cache add curl && \ -curl -Lfs -o /tmp/gcc-9.2.0-aarch64-linux-musl.tar.xz "https://github.com/just-containers/musl-cross-make/releases/download/${CC_VERSION}/gcc-9.2.0-aarch64-linux-musl.tar.xz" && \ -tar xf /tmp/gcc-9.2.0-aarch64-linux-musl.tar.xz -C / && \ -go mod download && \ mv public_html internal/server/public_html && \ -echo "Write tag ${BUILD_TAG} and commit ${BUILD_COMMIT} in binary." && \ -sed -i "s/__BUILD_TAG__/${BUILD_TAG}/" cmd/authelia/constants.go && \ -sed -i "s/__BUILD_COMMIT__/${BUILD_COMMIT}/" cmd/authelia/constants.go && \ -GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-musl-gcc CGO_LDFLAGS=-fuse-ld=bfd go build -tags netgo -ldflags '-s -w -linkmode external -extldflags -static' -trimpath -o authelia ./cmd/authelia +echo ">> Starting go build..." && \ +GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-musl-gcc CGO_LDFLAGS=-fuse-ld=bfd go build -tags netgo -ldflags \ +"-s -w -linkmode external ${LDFLAGS_EXTRA} -extldflags -static" \ +-trimpath -o authelia ./cmd/authelia # =================================== # ===== Authelia official image ===== @@ -32,8 +38,9 @@ WORKDIR /app COPY ./qemu-aarch64-static /usr/bin/qemu-aarch64-static -RUN apk --no-cache add ca-certificates su-exec tzdata && \ - rm /usr/bin/qemu-aarch64-static +RUN \ +apk --no-cache add ca-certificates su-exec tzdata && \ +rm /usr/bin/qemu-aarch64-static COPY --from=builder-backend /go/src/app/authelia /go/src/app/LICENSE /go/src/app/entrypoint.sh /go/src/app/healthcheck.sh ./ diff --git a/Dockerfile.coverage b/Dockerfile.coverage index 441eb4ca..a8640a6d 100644 --- a/Dockerfile.coverage +++ b/Dockerfile.coverage @@ -4,7 +4,8 @@ FROM node:16-alpine AS builder-frontend WORKDIR /node/src/app -COPY web . + +COPY web ./ # Install the dependencies and build RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn coverage @@ -14,26 +15,30 @@ RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn coverage # ======================================= FROM golang:1.16.5-alpine AS builder-backend -ARG BUILD_TAG -ARG BUILD_COMMIT - WORKDIR /go/src/app +# gcc and musl-dev are required for building go-sqlite3 +RUN \ +echo ">> Downloading required apk's..." && \ +apk --no-cache add gcc musl-dev + +COPY go.mod go.sum ./ + +RUN \ +echo ">> Downloading go modules..." && \ +go mod download + COPY / ./ # Prepare static files to be embedded in Go binary COPY --from=builder-frontend /node/src/app/build internal/server/public_html -# gcc and musl-dev are required for building go-sqlite3 +ARG LDFLAGS_EXTRA RUN \ -apk --no-cache add gcc musl-dev && \ -go mod download && \ mv api internal/server/public_html/api && \ -echo "Write tag ${BUILD_TAG} and commit ${BUILD_COMMIT} in binary." && \ -sed -i "s/__BUILD_TAG__/${BUILD_TAG}/" cmd/authelia/constants.go && \ -sed -i "s/__BUILD_COMMIT__/${BUILD_COMMIT}/" cmd/authelia/constants.go && \ cd cmd/authelia && \ -go test -c --tags coverage -covermode=atomic -o authelia -coverpkg github.com/authelia/authelia/... +echo ">> Starting go build (coverage via go test)..." && \ +go test -c --tags coverage -covermode=atomic -ldflags "${LDFLAGS_EXTRA}" -o authelia -coverpkg github.com/authelia/authelia/... # =================================== # ===== Authelia official image ===== diff --git a/cmd/authelia-scripts/cmd_build.go b/cmd/authelia-scripts/cmd_build.go index 212a783b..b8d56589 100644 --- a/cmd/authelia-scripts/cmd_build.go +++ b/cmd/authelia-scripts/cmd_build.go @@ -2,6 +2,7 @@ package main import ( "os" + "strings" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -9,8 +10,8 @@ import ( "github.com/authelia/authelia/internal/utils" ) -func buildAutheliaBinary() { - cmd := utils.CommandWithStdout("go", "build", "-o", "../../"+OutputDir+"/authelia") +func buildAutheliaBinary(xflags []string) { + cmd := utils.CommandWithStdout("go", "build", "-o", "../../"+OutputDir+"/authelia", "-ldflags", strings.Join(xflags, " ")) cmd.Dir = "cmd/authelia" cmd.Env = append(os.Environ(), @@ -109,8 +110,13 @@ func Build(cobraCmd *cobra.Command, args []string) { Clean(cobraCmd, args) + xflags, err := getXFlags("", "0", "") + if err != nil { + log.Fatal(err) + } + log.Debug("Creating `" + OutputDir + "` directory") - err := os.MkdirAll(OutputDir, os.ModePerm) + err = os.MkdirAll(OutputDir, os.ModePerm) if err != nil { log.Fatal(err) @@ -123,6 +129,6 @@ func Build(cobraCmd *cobra.Command, args []string) { buildSwagger() log.Debug("Building Authelia Go binary...") - buildAutheliaBinary() + buildAutheliaBinary(xflags) cleanAssets() } diff --git a/cmd/authelia-scripts/cmd_docker.go b/cmd/authelia-scripts/cmd_docker.go index f396c4e5..a2141102 100644 --- a/cmd/authelia-scripts/cmd_docker.go +++ b/cmd/authelia-scripts/cmd_docker.go @@ -82,24 +82,13 @@ func dockerBuildOfficialImage(arch string) error { } } - gitTag := ciTag - if gitTag == "" { - // If commit is not tagged, mark the build has having master tag. - gitTag = masterTag - } - - cmd := utils.Shell("git rev-parse HEAD") - cmd.Stdout = nil - cmd.Stderr = nil - commitBytes, err := cmd.Output() - + flags, err := getXFlags(ciBranch, os.Getenv("BUILDKITE_BUILD_NUMBER"), "") if err != nil { log.Fatal(err) } - commitHash := strings.Trim(string(commitBytes), "\n") - - return docker.Build(IntermediateDockerImageName, dockerfile, ".", gitTag, commitHash) + return docker.Build(IntermediateDockerImageName, dockerfile, ".", + strings.Join(flags, " ")) } // DockerBuildCmd Command for building docker image of Authelia. diff --git a/cmd/authelia-scripts/cmd_xflags.go b/cmd/authelia-scripts/cmd_xflags.go new file mode 100644 index 00000000..ccd2ac1d --- /dev/null +++ b/cmd/authelia-scripts/cmd_xflags.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func init() { + xflagsCmd.Flags().StringP("build", "b", "0", "Sets the BuildNumber flag value") + xflagsCmd.Flags().StringP("extra", "e", "", "Sets the BuildExtra flag value") +} + +var xflagsCmd = &cobra.Command{ + Use: "xflags", + Run: runXFlags, + Short: "Generate X LDFlags for building Authelia", +} + +func runXFlags(cobraCmd *cobra.Command, _ []string) { + build, err := cobraCmd.Flags().GetString("build") + if err != nil { + log.Fatal(err) + } + + extra, err := cobraCmd.Flags().GetString("extra") + if err != nil { + log.Fatal(err) + } + + flags, err := getXFlags("", build, extra) + if err != nil { + log.Fatal(err) + } + + fmt.Println(strings.Join(flags, " ")) +} diff --git a/cmd/authelia-scripts/const.go b/cmd/authelia-scripts/const.go index 87b2ed99..34e80c82 100644 --- a/cmd/authelia-scripts/const.go +++ b/cmd/authelia-scripts/const.go @@ -18,3 +18,5 @@ const masterTag = "master" const stringFalse = "false" const stringTrue = "true" const webDirectory = "web" + +const fmtLDFLAGSX = "-X 'github.com/authelia/authelia/internal/utils.%s=%s'" diff --git a/cmd/authelia-scripts/docker.go b/cmd/authelia-scripts/docker.go index 10fcc25a..4371643d 100644 --- a/cmd/authelia-scripts/docker.go +++ b/cmd/authelia-scripts/docker.go @@ -8,10 +8,11 @@ import ( type Docker struct{} // Build build a docker image. -func (d *Docker) Build(tag, dockerfile, target, gitTag, gitCommit string) error { +func (d *Docker) Build(tag, dockerfile, target, ldflags string) error { return utils.CommandWithStdout( - "docker", "build", "-t", tag, "-f", dockerfile, "--build-arg", - "BUILD_TAG="+gitTag, "--build-arg", "BUILD_COMMIT="+gitCommit, target).Run() + "docker", "build", "-t", tag, "-f", dockerfile, + "--build-arg", "LDFLAGS_EXTRA="+ldflags, + target).Run() } // Tag tag a docker image. diff --git a/cmd/authelia-scripts/helpers.go b/cmd/authelia-scripts/helpers.go new file mode 100644 index 00000000..2b71d1e6 --- /dev/null +++ b/cmd/authelia-scripts/helpers.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "strings" + "time" + + "github.com/authelia/authelia/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, err + } + + if out == "" { + branch = "master" + } else { + branch = out + } + } + + gitTagCommit, _, err := utils.RunCommandAndReturnOutput("git rev-list --tags --max-count=1") + if err != nil { + return flags, err + } + + tag, _, err := utils.RunCommandAndReturnOutput("git describe --tags --abbrev=0 " + gitTagCommit) + if err != nil { + return flags, err + } + + commit, _, err := utils.RunCommandAndReturnOutput("git rev-parse HEAD") + if err != nil { + return flags, err + } + + var states []string + + if gitTagCommit == commit { + states = append(states, "tagged") + } else { + states = append(states, "untagged") + } + + if _, exitCode, _ := utils.RunCommandAndReturnOutput("git diff --quiet"); exitCode != 0 { + states = append(states, "dirty") + } else { + states = append(states, "clean") + } + + if build == "" { + build = "manual" + } + + 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 +} diff --git a/cmd/authelia-scripts/main.go b/cmd/authelia-scripts/main.go index c3c3d05d..3375cc28 100755 --- a/cmd/authelia-scripts/main.go +++ b/cmd/authelia-scripts/main.go @@ -135,7 +135,7 @@ func main() { cobraCommands = append(cobraCommands, command) } - cobraCommands = append(cobraCommands, commands.HashPasswordCmd, commands.CertificatesCmd, commands.RSACmd) + cobraCommands = append(cobraCommands, commands.HashPasswordCmd, commands.CertificatesCmd, commands.RSACmd, xflagsCmd) rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the log level for the command") rootCmd.AddCommand(cobraCommands...) diff --git a/cmd/authelia/const.go b/cmd/authelia/const.go new file mode 100644 index 00000000..296025de --- /dev/null +++ b/cmd/authelia/const.go @@ -0,0 +1,21 @@ +package main + +const fmtAutheliaLong = `authelia %s + +An open-source authentication and authorization server providing +two-factor authentication and single sign-on (SSO) for your +applications via a web portal. + +Documentation is available at: https://www.authelia.com/docs +` + +const fmtAutheliaBuild = `Last Tag: %s +State: %s +Branch: %s +Commit: %s +Build Number: %s +Build OS: %s +Build Arch: %s +Build Date: %s +Extra: %s +` diff --git a/cmd/authelia/constants.go b/cmd/authelia/constants.go deleted file mode 100644 index 14f2cf13..00000000 --- a/cmd/authelia/constants.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -// BuildTag tag used to bootstrap Authelia binary. -var BuildTag = "__BUILD_TAG__" - -// BuildCommit commit used to bootstrap Authelia binary. -var BuildCommit = "__BUILD_COMMIT__" diff --git a/cmd/authelia/main.go b/cmd/authelia/main.go index d51b5e99..05af593e 100644 --- a/cmd/authelia/main.go +++ b/cmd/authelia/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "runtime" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -56,6 +57,8 @@ func startServer() { logger.Fatalf("Cannot initialize logger: %v", err) } + logger.Infof("Authelia %s is starting", utils.Version()) + switch config.Logging.Level { case "error": logger.Info("Logging severity set to error") @@ -145,24 +148,31 @@ func startServer() { func main() { logger := logging.Logger() + + version := utils.Version() + rootCmd := &cobra.Command{ Use: "authelia", Run: func(cmd *cobra.Command, args []string) { startServer() }, + Version: version, + Short: fmt.Sprintf("authelia %s", version), + Long: fmt.Sprintf(fmtAutheliaLong, version), } rootCmd.Flags().StringVar(&configPathFlag, "config", "", "Configuration file") - versionCmd := &cobra.Command{ - Use: "version", - Short: "Show the version of Authelia", + buildCmd := &cobra.Command{ + Use: "build", + Short: "Show the build of Authelia", Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Authelia version %s, build %s\n", BuildTag, BuildCommit) + fmt.Printf(fmtAutheliaBuild, utils.BuildTag, utils.BuildState, utils.BuildBranch, utils.BuildCommit, + utils.BuildNumber, runtime.GOOS, runtime.GOARCH, utils.BuildDate, utils.BuildExtra) }, } - rootCmd.AddCommand(versionCmd, commands.HashPasswordCmd, + rootCmd.AddCommand(buildCmd, commands.HashPasswordCmd, commands.ValidateConfigCmd, commands.CertificatesCmd, commands.RSACmd) diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index 9aaa1640..eef2d714 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -2,6 +2,7 @@ package suites import ( "os" + "regexp" "testing" "github.com/stretchr/testify/suite" @@ -37,10 +38,25 @@ func (s *CLISuite) SetupTest() { s.coverageArg = coverageArg } -func (s *CLISuite) TestShouldPrintVersion() { - output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "version"}) +func (s *CLISuite) TestShouldPrintBuildInformation() { + output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build"}) s.Assert().Nil(err) - s.Assert().Contains(output, "Authelia version") + s.Assert().Contains(output, "Last Tag: ") + s.Assert().Contains(output, "State: ") + s.Assert().Contains(output, "Branch: ") + s.Assert().Contains(output, "Build Number: ") + s.Assert().Contains(output, "Build OS: ") + s.Assert().Contains(output, "Build Arch: ") + s.Assert().Contains(output, "Build Date: ") + + r := regexp.MustCompile(`^Last Tag: v\d+\.\d+\.\d+\nState: (tagged|untagged) (clean|dirty)\nBranch: [^\s\n]+\nCommit: [0-9a-f]{40}\nBuild Number: \d+\nBuild OS: (linux|darwin|windows|freebsd)\nBuild Arch: (amd64|arm|arm64)\nBuild Date: (Sun|Mon|Tue|Wed|Thu|Fri|Sat), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}\nExtra: \n`) + s.Assert().Regexp(r, output) +} + +func (s *CLISuite) TestShouldPrintVersion() { + output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "--version"}) + s.Assert().Nil(err) + s.Assert().Contains(output, "authelia version") } func (s *CLISuite) TestShouldValidateConfig() { diff --git a/internal/utils/const.go b/internal/utils/const.go index ba6afa9f..5190484f 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -39,6 +39,10 @@ const ( // Month is an int based representation of the time unit. Month = Year / 12 + + clean = "clean" + tagged = "tagged" + unknown = "unknown" ) // ErrTimeoutReached error thrown when a timeout is reached. diff --git a/internal/utils/exec.go b/internal/utils/exec.go index 0e5c3dff..7cfad8ea 100644 --- a/internal/utils/exec.go +++ b/internal/utils/exec.go @@ -44,6 +44,20 @@ func Shell(command string) *exec.Cmd { return CommandWithStdout("bash", "-c", command) } +// RunCommandAndReturnOutput runs a shell command then returns the stdout and the exit code. +func RunCommandAndReturnOutput(command string) (output string, exitCode int, err error) { + cmd := Shell(command) + cmd.Stdout = nil + cmd.Stderr = nil + + outputBytes, err := cmd.Output() + if err != nil { + return "", cmd.ProcessState.ExitCode(), err + } + + return strings.Trim(string(outputBytes), "\n"), cmd.ProcessState.ExitCode(), nil +} + // RunCommandUntilCtrlC run a command until ctrl-c is hit. func RunCommandUntilCtrlC(cmd *exec.Cmd) { mutex := sync.Mutex{} diff --git a/internal/utils/version.go b/internal/utils/version.go new file mode 100644 index 00000000..38c43819 --- /dev/null +++ b/internal/utils/version.go @@ -0,0 +1,104 @@ +package utils + +import ( + "fmt" + "strings" +) + +// BuildTag is replaced by LDFLAGS at build time with the latest tag at or before the current commit. +var BuildTag = "unknown" + +// BuildState is replaced by LDFLAGS at build time with `tagged` or `untagged` depending on if the commit is tagged, and +// `clean` or `dirty` depending on the working tree state. For example if the commit was tagged and the working tree +// was dirty it would be "tagged dirty". This is used to determine the version string output mode. +var BuildState = "untagged dirty" + +// BuildExtra is replaced by LDFLAGS at build time with a blank string by default. People porting Authelia can use this +// to add a suffix to their versions. +var BuildExtra = "" + +// BuildDate is replaced by LDFLAGS at build time with the date the build started. +var BuildDate = "" + +// BuildCommit is replaced by LDFLAGS at build time with the current commit. +var BuildCommit = "unknown" + +// BuildBranch is replaced by LDFLAGS at build time with the current branch. +var BuildBranch = "master" + +// BuildNumber is replaced by LDFLAGS at build time with the CI build number. +var BuildNumber = "0" + +// Version returns the Authelia version. +// +// The format of the string is dependent on the values in BuildState. If tagged and clean are present it returns the +// 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) +} + +func version(tag, state, commit, branch, extra string) (version string) { + b := strings.Builder{} + + states := strings.Split(state, " ") + + isClean := IsStringInSlice(clean, states) + isTagged := IsStringInSlice(tagged, states) + + if isClean && isTagged { + b.WriteString(tag) + + if extra != "" { + b.WriteRune('-') + b.WriteString(extra) + } + + return b.String() + } + + if isTagged && !isClean { + b.WriteString(tag) + b.WriteString("-dirty") + + return b.String() + } + + if !isTagged { + b.WriteString("untagged-") + } + + b.WriteString(tag) + + if !isClean { + b.WriteString("-dirty") + } + + if extra != "" { + b.WriteRune('-') + b.WriteString(extra) + } + + b.WriteString(fmt.Sprintf(" (%s, %s)", branch, commitShort(commit))) + + return b.String() +} + +func commitShort(commitLong string) (commit string) { + if commitLong == "" { + return unknown + } + + b := strings.Builder{} + + for i, r := range commitLong { + b.WriteRune(r) + + if i >= 6 { + break + } + } + + return b.String() +} diff --git a/internal/utils/version_test.go b/internal/utils/version_test.go new file mode 100644 index 00000000..3b8aed08 --- /dev/null +++ b/internal/utils/version_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVersionDefault(t *testing.T) { + v := Version() + + assert.Equal(t, "untagged-unknown-dirty (master, unknown)", v) +} + +func TestVersion(t *testing.T) { + var v string + + v = version("v4.90.0", "tagged clean", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") + assert.Equal(t, "v4.90.0", v) + + v = version("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", "") + assert.Equal(t, "v4.90.0-dirty", v) + + v = version("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", "") + assert.Equal(t, "untagged-v4.90.0 (master, 50d8b4a)", v) + + v = version("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", "") + assert.Equal(t, "untagged-v4.90.0 (master, unknown)", v) + + v = version("v4.90.0", "", "50d8b4a941c26b89482c94ab324b5a274f9ced66", "master", "") + assert.Equal(t, "untagged-v4.90.0-dirty (master, 50d8b4a)", v) +}