diff --git a/api/build/variables.go b/api/build/variables.go new file mode 100644 index 000000000..d20f64c42 --- /dev/null +++ b/api/build/variables.go @@ -0,0 +1,9 @@ +package build + +// Variables to be set during the build time +var BuildNumber string +var ImageTag string +var NodejsVersion string +var YarnVersion string +var WebpackVersion string +var GoVersion string diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 68806b9aa..6f7f8917d 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -16,6 +16,7 @@ import ( "github.com/portainer/libhelm" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/apikey" + "github.com/portainer/portainer/api/build" "github.com/portainer/portainer/api/chisel" "github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/crypto" @@ -743,7 +744,15 @@ func main() { for { server := buildServer(flags) - logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion) + logrus.WithFields(logrus.Fields{ + "Version": portainer.APIVersion, + "BuildNumber": build.BuildNumber, + "ImageTag": build.ImageTag, + "NodejsVersion": build.NodejsVersion, + "YarnVersion": build.YarnVersion, + "WebpackVersion": build.WebpackVersion, + "GoVersion": build.GoVersion}, + ).Print("[INFO] [cmd,main] Starting Portainer") err := server.Start() logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err) } diff --git a/api/http/handler/status/handler.go b/api/http/handler/status/handler.go index 19c64b4bb..b0cd04bab 100644 --- a/api/http/handler/status/handler.go +++ b/api/http/handler/status/handler.go @@ -27,7 +27,7 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demo h.Handle("/status", bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet) h.Handle("/status/version", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet) + bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/status/status_inspect_version.go b/api/http/handler/status/status_inspect_version.go deleted file mode 100644 index 99c5a4282..000000000 --- a/api/http/handler/status/status_inspect_version.go +++ /dev/null @@ -1,62 +0,0 @@ -package status - -import ( - "encoding/json" - "net/http" - - "github.com/coreos/go-semver/semver" - - portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/client" - - "github.com/portainer/libhttp/response" -) - -type inspectVersionResponse struct { - // Whether portainer has an update available - UpdateAvailable bool `json:"UpdateAvailable" example:"false"` - // The latest version available - LatestVersion string `json:"LatestVersion" example:"2.0.0"` -} - -type githubData struct { - TagName string `json:"tag_name"` -} - -// @id StatusInspectVersion -// @summary Check for portainer updates -// @description Check if portainer has an update available -// @description **Access policy**: authenticated -// @security ApiKeyAuth -// @security jwt -// @tags status -// @produce json -// @success 200 {object} inspectVersionResponse "Success" -// @router /status/version [get] -func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) { - motd, err := client.Get(portainer.VersionCheckURL, 5) - if err != nil { - response.JSON(w, &inspectVersionResponse{UpdateAvailable: false}) - return - } - - var data githubData - err = json.Unmarshal(motd, &data) - if err != nil { - response.JSON(w, &inspectVersionResponse{UpdateAvailable: false}) - return - } - - resp := inspectVersionResponse{ - UpdateAvailable: false, - } - - currentVersion := semver.New(portainer.APIVersion) - latestVersion := semver.New(data.TagName) - if currentVersion.LessThan(*latestVersion) { - resp.UpdateAvailable = true - resp.LatestVersion = data.TagName - } - - response.JSON(w, &resp) -} diff --git a/api/http/handler/status/version.go b/api/http/handler/status/version.go new file mode 100644 index 000000000..27de4277a --- /dev/null +++ b/api/http/handler/status/version.go @@ -0,0 +1,105 @@ +package status + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/coreos/go-semver/semver" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/build" + "github.com/portainer/portainer/api/http/client" + + "github.com/portainer/libhttp/response" + log "github.com/sirupsen/logrus" +) + +type versionResponse struct { + // Whether portainer has an update available + UpdateAvailable bool `json:"UpdateAvailable" example:"false"` + // The latest version available + LatestVersion string `json:"LatestVersion" example:"2.0.0"` + + ServerVersion string + DatabaseVersion string + Build BuildInfo +} + +type BuildInfo struct { + BuildNumber string + ImageTag string + NodejsVersion string + YarnVersion string + WebpackVersion string + GoVersion string +} + +// @id Version +// @summary Check for portainer updates +// @description Check if portainer has an update available +// @description **Access policy**: authenticated +// @security ApiKeyAuth +// @security jwt +// @tags status +// @produce json +// @success 200 {object} versionResponse "Success" +// @router /status/version [get] +func (handler *Handler) version(w http.ResponseWriter, r *http.Request) { + result := &versionResponse{ + ServerVersion: portainer.APIVersion, + DatabaseVersion: strconv.Itoa(portainer.DBVersion), + Build: BuildInfo{ + BuildNumber: build.BuildNumber, + ImageTag: build.ImageTag, + NodejsVersion: build.NodejsVersion, + YarnVersion: build.YarnVersion, + WebpackVersion: build.WebpackVersion, + GoVersion: build.GoVersion, + }, + } + + latestVersion := getLatestVersion() + if hasNewerVersion(portainer.APIVersion, latestVersion) { + result.UpdateAvailable = true + result.LatestVersion = latestVersion + } + + response.JSON(w, &result) +} + +func getLatestVersion() string { + motd, err := client.Get(portainer.VersionCheckURL, 5) + if err != nil { + log.WithError(err).Debug("couldn't fetch latest Portainer release version") + return "" + } + + var data struct { + TagName string `json:"tag_name"` + } + + err = json.Unmarshal(motd, &data) + if err != nil { + log.WithError(err).Debug("couldn't parse latest Portainer version") + return "" + } + + return data.TagName +} + +func hasNewerVersion(currentVersion, latestVersion string) bool { + currentVersionSemver, err := semver.NewVersion(currentVersion) + if err != nil { + log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver") + return false + } + + latestVersionSemver, err := semver.NewVersion(latestVersion) + if err != nil { + log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver") + return false + } + + return currentVersionSemver.LessThan(*latestVersionSemver) +} diff --git a/app/portainer/services/api/status.service.ts b/app/portainer/services/api/status.service.ts index f18599122..6c838b3fe 100644 --- a/app/portainer/services/api/status.service.ts +++ b/app/portainer/services/api/status.service.ts @@ -25,9 +25,7 @@ export async function getStatus() { try { const { data } = await axios.get(buildUrl()); - if (process.env.PORTAINER_EDITION !== 'CE') { - data.Edition = 'Business Edition'; - } + data.Edition = 'Community Edition'; return data; } catch (error) { @@ -46,6 +44,16 @@ export interface VersionResponse { UpdateAvailable: boolean; // The latest version available LatestVersion: string; + ServerVersion: string; + DatabaseVersion: string; + Build: { + BuildNumber: string; + ImageTag: string; + NodejsVersion: string; + YarnVersion: string; + WebpackVersion: string; + GoVersion: string; + }; } export async function getVersionStatus() { diff --git a/app/react/sidebar/Footer.module.css b/app/react/sidebar/Footer.module.css index 288847c21..87001a28b 100644 --- a/app/react/sidebar/Footer.module.css +++ b/app/react/sidebar/Footer.module.css @@ -1,3 +1,42 @@ :global(#page-wrapper:not(.open)) .root { display: none; } + +.dialog { + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; +} + +.versionInfo { + margin: 5px 15px 10px; + font-size: 12px; +} + +.versionInfo table { + width: 100%; + table-layout: fixed; +} + +.versionInfo td { + padding-bottom: 10px; +} + +.toolsList { + padding: 15px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background-color: rgba(211, 211, 211, 0.2); + width: max-width; + font-family: Arial; + word-break: break-all; +} + +.toolsList span { + margin-bottom: 10px; +} + +.tools span { + margin-right: 10px; +} diff --git a/app/react/sidebar/Footer.tsx b/app/react/sidebar/Footer.tsx index aaeab9d71..4ea94c05c 100644 --- a/app/react/sidebar/Footer.tsx +++ b/app/react/sidebar/Footer.tsx @@ -1,44 +1,150 @@ +import { useState } from 'react'; import { useQuery } from 'react-query'; import clsx from 'clsx'; +import { Database, Hash, Server, Tag, Tool } from 'react-feather'; +import { DialogOverlay } from '@reach/dialog'; -import { getStatus } from '@/portainer/services/api/status.service'; +import { + getStatus, + getVersionStatus, +} from '@/portainer/services/api/status.service'; + +import { Button } from '@@/buttons'; import { UpdateNotification } from './UpdateNotifications'; import styles from './Footer.module.css'; +import '@reach/dialog/styles.css'; export function Footer() { + const [showBuildInfo, setShowBuildInfo] = useState(false); const statusQuery = useStatus(); + const versionQuery = useVersionStatus(); - if (!statusQuery.data) { + if (!statusQuery.data || !versionQuery.data) { return null; } const { Edition, Version } = statusQuery.data; + const { ServerVersion, DatabaseVersion, Build } = versionQuery.data; + + function toggleModal() { + setShowBuildInfo(!showBuildInfo); + } return ( -
- {process.env.PORTAINER_EDITION === 'CE' && } -
- © - Portainer {Edition} + <> + +
+
+
+ +
Portainer {Edition}
+
+
+
+ + + + + + + + + + + +
+ + + Server Version: {ServerVersion} + + + + + Database Version: {DatabaseVersion} + +
+ + + CI Build Number: {Build.BuildNumber} + + + + + Image Tag: {Build.ImageTag} + +
+
+
+ + + Compilation tools: + - {Version} +
+ + Nodejs v{Build.NodejsVersion} + + + Yarn v{Build.YarnVersion} + + + Webpack v{Build.WebpackVersion} + + + Go v{Build.GoVersion} + +
+
+
+
+ +
+
+
+
- {process.env.PORTAINER_EDITION === 'CE' && ( - + {process.env.PORTAINER_EDITION === 'CE' && } +
+ © + Portainer {Edition} + + - Upgrade - - )} + {Version} + + + {process.env.PORTAINER_EDITION === 'CE' && ( + + Upgrade + + )} +
-
+ ); } function useStatus() { return useQuery(['status'], () => getStatus()); } + +function useVersionStatus() { + return useQuery(['version'], () => getVersionStatus()); +} diff --git a/build/build_binary.sh b/build/build_binary.sh index 24715d378..a9dc2da80 100755 --- a/build/build_binary.sh +++ b/build/build_binary.sh @@ -2,6 +2,14 @@ set -x mkdir -p dist +# populate tool versions +BUILDNUMBER="N/A" +CONTAINER_IMAGE_TAG="N/A" +NODE_VERSION="0" +YARN_VERSION="0" +WEBPACK_VERSION="0" +GO_VERSION="0" + cd api # the go get adds 8 seconds go get -t -d -v ./... @@ -9,6 +17,12 @@ go get -t -d -v ./... # the build takes 2 seconds GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build \ --installsuffix cgo \ - --ldflags '-s' \ + --ldflags "-s \ + -X 'github.com/portainer/portainer/api/build.BuildNumber=${BUILDNUMBER}' \ + -X 'github.com/portainer/portainer/api/build.ImageTag=${CONTAINER_IMAGE_TAG}' \ + -X 'github.com/portainer/portainer/api/build.NodejsVersion=${NODE_VERSION}' \ + -X 'github.com/portainer/portainer/api/build.YarnVersion=${YARN_VERSION}' \ + -X 'github.com/portainer/portainer/api/build.WebpackVersion=${WEBPACK_VERSION}' \ + -X 'github.com/portainer/portainer/api/build.GoVersion=${GO_VERSION}'" \ -o "../dist/portainer" \ ./cmd/portainer/ diff --git a/build/build_binary_azuredevops.sh b/build/build_binary_azuredevops.sh index f6cbbdeba..9ebf89297 100755 --- a/build/build_binary_azuredevops.sh +++ b/build/build_binary_azuredevops.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +set -x PLATFORM=$1 ARCH=$2 @@ -15,10 +16,16 @@ cp -R api ${GOPATH}/src/github.com/portainer/portainer/api cd 'api/cmd/portainer' go get -t -d -v ./... -GOOS=${PLATFORM} GOARCH=${ARCH} CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' +GOOS=${PLATFORM} GOARCH=${ARCH} CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags "-s \ +-X 'github.com/portainer/portainer/api/build.BuildNumber=${BUILDNUMBER}' \ +-X 'github.com/portainer/portainer/api/build.ImageTag=${CONTAINER_IMAGE_TAG}' \ +-X 'github.com/portainer/portainer/api/build.NodejsVersion=${NODE_VERSION}' \ +-X 'github.com/portainer/portainer/api/build.YarnVersion=${YARN_VERSION}' \ +-X 'github.com/portainer/portainer/api/build.WebpackVersion=${WEBPACK_VERSION}' \ +-X 'github.com/portainer/portainer/api/build.GoVersion=${GO_VERSION}'" if [ "${PLATFORM}" == 'windows' ]; then mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/${binary}.exe" "$BUILD_SOURCESDIRECTORY/dist/portainer.exe" -else +else mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer" fi