feat(buildinfo): ability to see build info [EE-2552] (#7107)

* feat(buildinfo): ability to see build info [EE-2252]

* handle dark theme

* feat: add build info to status version

* feat: include ldflags in azure pipeline

* echo shell commands in azure build

* clean up main log

* allow tests to pass

* use data from backend

* allow clicking off modal to dismiss

* add placeholder versions

* refactor

* update button class

* fix modal displaying behind elements

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
pull/7152/head
itsconquest 2022-07-15 11:09:38 +12:00 committed by GitHub
parent f5e774c89d
commit a0d349e0b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 323 additions and 88 deletions

9
api/build/variables.go Normal file
View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -25,9 +25,7 @@ export async function getStatus() {
try {
const { data } = await axios.get<StatusResponse>(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() {

View File

@ -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;
}

View File

@ -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 (
<div className={clsx(styles.root, 'text-center')}>
{process.env.PORTAINER_EDITION === 'CE' && <UpdateNotification />}
<div className="text-xs space-x-1 text-gray-5 be:text-gray-6">
<span>&copy;</span>
<span>Portainer {Edition}</span>
<>
<DialogOverlay className={styles.dialog} isOpen={showBuildInfo}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" onClick={toggleModal}>
×
</button>
<h5 className="modal-title">Portainer {Edition}</h5>
</div>
<div className="modal-body">
<div className={styles.versionInfo}>
<table>
<tbody>
<tr>
<td>
<span className="inline-flex items-center">
<Server size="13" className="space-right" />
Server Version: {ServerVersion}
</span>
</td>
<td>
<span className="inline-flex items-center">
<Database size="13" className="space-right" />
Database Version: {DatabaseVersion}
</span>
</td>
</tr>
<tr>
<td>
<span className="inline-flex items-center">
<Hash size="13" className="space-right" />
CI Build Number: {Build.BuildNumber}
</span>
</td>
<td>
<span>
<Tag size="13" className="space-right" />
Image Tag: {Build.ImageTag}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div className={styles.toolsList}>
<span className="inline-flex items-center">
<Tool size="13" className="space-right" />
Compilation tools:
</span>
<span data-cy="portainerSidebar-versionNumber">{Version}</span>
<div className={styles.tools}>
<span className="text-muted small">
Nodejs v{Build.NodejsVersion}
</span>
<span className="text-muted small">
Yarn v{Build.YarnVersion}
</span>
<span className="text-muted small">
Webpack v{Build.WebpackVersion}
</span>
<span className="text-muted small">
Go v{Build.GoVersion}
</span>
</div>
</div>
</div>
<div className="modal-footer">
<Button className="bootbox-accept" onClick={toggleModal}>
Ok
</Button>
</div>
</div>
</div>
</DialogOverlay>
{process.env.PORTAINER_EDITION === 'CE' && (
<a
href="https://www.portainer.io/install-BE-now"
className="text-blue-6 font-medium"
target="_blank"
rel="noreferrer"
<div className={clsx(styles.root, 'text-center')}>
{process.env.PORTAINER_EDITION === 'CE' && <UpdateNotification />}
<div className="text-xs space-x-1 text-gray-5 be:text-gray-6">
<span>&copy;</span>
<span>Portainer {Edition}</span>
<span
data-cy="portainerSidebar-versionNumber"
onClick={toggleModal}
// Accessibility requirements for a clickable span
onKeyPress={toggleModal}
role="button"
tabIndex={0}
>
Upgrade
</a>
)}
{Version}
</span>
{process.env.PORTAINER_EDITION === 'CE' && (
<a
href="https://www.portainer.io/install-BE-now"
className="text-blue-6 font-medium"
target="_blank"
rel="noreferrer"
>
Upgrade
</a>
)}
</div>
</div>
</div>
</>
);
}
function useStatus() {
return useQuery(['status'], () => getStatus());
}
function useVersionStatus() {
return useQuery(['version'], () => getVersionStatus());
}

View File

@ -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/

View File

@ -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