Files
zeekay 8feb24ea1f One name for this fork, and it is ours
The server module named github.com/hanzoai/team/server/v8 — a path that
resolves to the retired workspace's repository, not this one — while
server/public still published github.com/mattermost/mattermost/server/public.
Both are github.com/hanzoteam/server/... now, which is where this code
actually lives.

That second half is what made the binary unbootable. Upstream's public path
stayed claimed by upstream, mattermost-plugin-ai imported it, and the build
linked public/model twice; public/plugin's gob.Register then ran twice under
one name and panicked at init, before main. Reverting the rename traded the
brand for a working binary. Rebranding the plugin alongside it needs no such
trade: the agents fork is github.com/hanzoteam/agents/v2 against our public
module, so no module named mattermost/mattermost is in the graph at all.

Measured: `go list -deps ./cmd/mattermost` names exactly one public/model, and
it is ours.
2026-08-28 23:35:36 -07:00

369 lines
9.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package upgrader
import (
"archive/tar"
"bytes"
"compress/gzip"
_ "embed"
"fmt"
"io"
"net/http"
"os"
"os/user"
"path"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"github.com/pkg/errors"
"golang.org/x/crypto/openpgp" //nolint:staticcheck
"github.com/hanzoteam/server/server/public/model"
"github.com/hanzoteam/server/server/public/shared/mlog"
)
//go:embed pubkey.gpg
var mattermostBuildPublicKeys []byte
var (
upgradePercentage int64
m sync.Mutex
upgradeError error
upgrading int32
)
type writeCounter struct {
total int64
read int64
}
func (wc *writeCounter) Write(p []byte) (int, error) {
n := len(p)
wc.read += int64(n)
if wc.total <= 0 {
// skip the percentage calculation for invalid totals
setUpgradePercentage(50)
return n, nil
}
percentage := (wc.read * 100) / wc.total
if percentage == 0 {
percentage = 1
} else if percentage >= 100 {
percentage = 99
}
setUpgradePercentage(percentage)
return n, nil
}
func getUpgradePercentage() int64 {
return atomic.LoadInt64(&upgradePercentage)
}
func setUpgradePercentage(to int64) {
atomic.StoreInt64(&upgradePercentage, to)
}
func getUpgradeError() error {
m.Lock()
defer m.Unlock()
return upgradeError
}
func setUpgradeError(err error) {
m.Lock()
defer m.Unlock()
upgradeError = err
}
func getCurrentVersionTgzURL() string {
version := model.CurrentVersion
if strings.HasPrefix(model.BuildNumber, version+"-rc") {
version = model.BuildNumber
}
return "https://releases.mattermost.com/" + version + "/mattermost-" + version + "-linux-amd64.tar.gz"
}
func verifySignature(filename string, sigfilename string, publicKey []byte) error {
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(publicKey))
if err != nil {
mlog.Debug("Unable to load the public key to verify the file signature", mlog.Err(err))
return NewInvalidSignature()
}
mattermost_tar, err := os.Open(filename)
if err != nil {
mlog.Debug("Unable to open the Hanzo Team .tar file to verify the file signature", mlog.Err(err))
return NewInvalidSignature()
}
signature, err := os.Open(sigfilename)
if err != nil {
mlog.Debug("Unable to open the Hanzo Team .sig file verify the file signature", mlog.Err(err))
return NewInvalidSignature()
}
_, err = openpgp.CheckDetachedSignature(keyring, mattermost_tar, signature)
if err != nil {
mlog.Debug("Unable to verify the Hanzo Team file signature", mlog.Err(err))
return NewInvalidSignature()
}
return nil
}
func canIWriteTheExecutable() error {
executablePath, err := os.Executable()
if err != nil {
return errors.New("error getting the path of the executable")
}
executableInfo, err := os.Stat(path.Dir(executablePath))
if err != nil {
return errors.New("error getting the executable info")
}
stat, ok := executableInfo.Sys().(*syscall.Stat_t)
if !ok {
return errors.New("error getting the executable info")
}
fileUID := int(stat.Uid)
fileUser, err := user.LookupId(strconv.Itoa(fileUID))
if err != nil {
return errors.New("error getting the executable info")
}
mattermostUID := os.Getuid()
mattermostUser, err := user.LookupId(strconv.Itoa(mattermostUID))
if err != nil {
return errors.New("error getting the executable info")
}
mode := executableInfo.Mode()
if fileUID != mattermostUID && mode&(1<<1) == 0 && mode&(1<<7) == 0 {
return NewInvalidPermissions("invalid-user-and-permission", path.Dir(executablePath), mattermostUser.Username, fileUser.Username)
}
if fileUID != mattermostUID && mode&(1<<1) == 0 && mode&(1<<7) != 0 {
return NewInvalidPermissions("invalid-user", path.Dir(executablePath), mattermostUser.Username, fileUser.Username)
}
if fileUID == mattermostUID && mode&(1<<7) == 0 {
return NewInvalidPermissions("invalid-permission", path.Dir(executablePath), mattermostUser.Username, fileUser.Username)
}
return nil
}
func canIUpgrade() error {
if runtime.GOARCH != "amd64" {
return NewInvalidArch()
}
if runtime.GOOS != "linux" {
return NewInvalidArch()
}
return canIWriteTheExecutable()
}
func CanIUpgradeToE0() error {
if err := canIUpgrade(); err != nil {
return errors.Wrap(err, "unable to upgrade from TE to E0")
}
if model.BuildEnterpriseReady == "true" {
mlog.Warn("Unable to upgrade from TE to E0. The server is already running E0.")
return errors.New("you cannot upgrade your server from TE to E0 because you are already running Hanzo Team")
}
return nil
}
func UpgradeToE0() error {
if !atomic.CompareAndSwapInt32(&upgrading, 0, 1) {
mlog.Warn("Trying to upgrade while another upgrade is running")
return errors.New("another upgrade is already running")
}
defer atomic.CompareAndSwapInt32(&upgrading, 1, 0)
setUpgradePercentage(1)
setUpgradeError(nil)
executablePath, err := os.Executable()
if err != nil {
setUpgradeError(errors.New("error getting the executable path"))
mlog.Error("Unable to get the path of the Hanzo Team executable", mlog.Err(err))
setUpgradePercentage(0)
return err
}
filename, err := download(getCurrentVersionTgzURL())
if err != nil {
if filename != "" {
os.Remove(filename)
}
setUpgradeError(fmt.Errorf("error downloading the new Hanzo Team server binary file (percentage: %d)", getUpgradePercentage()))
mlog.Error("Unable to download the Hanzo Team server binary file", mlog.Int("percentage", getUpgradePercentage()), mlog.String("url", getCurrentVersionTgzURL()), mlog.Err(err))
setUpgradePercentage(0)
return err
}
defer os.Remove(filename)
sigfilename, err := download(getCurrentVersionTgzURL() + ".sig")
if err != nil {
if sigfilename != "" {
os.Remove(sigfilename)
}
setUpgradeError(errors.New("error downloading the signature file of the new server"))
mlog.Error("Unable to download the signature file of the new Hanzo Team server", mlog.String("url", getCurrentVersionTgzURL()+".sig"), mlog.Err(err))
setUpgradePercentage(0)
return err
}
defer os.Remove(sigfilename)
err = verifySignature(filename, sigfilename, mattermostBuildPublicKeys)
if err != nil {
setUpgradeError(errors.New("unable to verify the signature of the downloaded file"))
mlog.Error("Unable to verify the signature of the downloaded file", mlog.Err(err))
setUpgradePercentage(0)
return err
}
err = extractBinary(executablePath, filename)
if err != nil {
setUpgradeError(err)
mlog.Error("Unable to extract the binary from the downloaded file", mlog.Err(err))
setUpgradePercentage(0)
return err
}
setUpgradePercentage(100)
return nil
}
func UpgradeToE0Status() (int64, error) {
return getUpgradePercentage(), getUpgradeError()
}
func download(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.Copy(io.Discard, resp.Body)
return "", errors.Errorf("error downloading file %s: %s", url, resp.Status)
}
out, err := os.CreateTemp("", "*_mattermost.tar.gz")
if err != nil {
return "", err
}
defer out.Close()
counter := &writeCounter{total: resp.ContentLength}
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
if err != nil {
return "", err
}
return out.Name(), nil
}
func getFilePermissionsOrDefault(filename string, def os.FileMode) os.FileMode {
file, err := os.Open(filename)
if err != nil {
mlog.Warn("Unable to get the file permissions", mlog.String("filename", filename), mlog.Err(err))
return def
}
defer file.Close()
fileStats, err := file.Stat()
if err != nil {
mlog.Warn("Unable to get the file permissions", mlog.String("filename", filename), mlog.Err(err))
return def
}
return fileStats.Mode()
}
func extractBinary(executablePath string, filename string) error {
gzipStream, err := os.Open(filename)
if err != nil {
return err
}
uncompressedStream, err := gzip.NewReader(gzipStream)
if err != nil {
return err
}
tarReader := tar.NewReader(uncompressedStream)
for {
header, err := tarReader.Next()
if err == io.EOF {
return errors.New("unable to find the Hanzo Team binary in the downloaded version")
}
if err != nil {
return err
}
if header.Typeflag == tar.TypeReg && header.Name == "mattermost/bin/mattermost" {
permissions := getFilePermissionsOrDefault(executablePath, 0755)
tmpFile, err := os.CreateTemp(path.Dir(executablePath), "*")
if err != nil {
return err
}
tmpFileName := tmpFile.Name()
os.Remove(tmpFileName)
err = os.Rename(executablePath, tmpFileName)
if err != nil {
return err
}
outFile, err := os.Create(executablePath)
if err != nil {
err2 := os.Rename(tmpFileName, executablePath)
if err2 != nil {
mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.")
return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually")
}
return err
}
defer outFile.Close()
if _, err = io.Copy(outFile, tarReader); err != nil {
err2 := os.Remove(executablePath)
if err2 != nil {
mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.")
return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually")
}
err2 = os.Rename(tmpFileName, executablePath)
if err2 != nil {
mlog.Fatal("Unable to restore the backup of the executable file. Restore the executable file manually.")
return errors.Wrap(err2, "critical error: unable to upgrade the binary or restore the old binary version. Please restore it manually")
}
return err
}
err = os.Remove(tmpFileName)
if err != nil {
mlog.Warn("Unable to clean up the binary backup file.", mlog.Err(err))
}
err = os.Chmod(executablePath, permissions)
if err != nil {
mlog.Warn("Unable to set the correct permissions for the file.", mlog.Err(err))
}
break
}
}
return nil
}