mirror of https://github.com/portainer/portainer
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
parent
f5e774c89d
commit
a0d349e0b3
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>©</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>©</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());
|
||||
}
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue