Files
mostlymatter/server/channels/app/support_packet.go
Jesse Hallam 73d5f507a4 Cherry-pick Go 1.25.8 upgrade for release-10.11 (#36125)
* ci: shard server Postgres tests into 4 parallel runners (#35739)

* ci: add test sharding plumbing to server CI

Add infrastructure for upcoming test sharding without changing behavior:

- Add shard-index and shard-total inputs to server-test-template.yml
  (defaults preserve existing single-runner behavior)
- Add timing cache restore step (activates only when shard-total > 1)
- Add merge-postgres-test-results job to server-ci.yml that:
  - Merges JUnit XML reports from shard artifacts
  - Saves timing data cache for future shard balancing
  - Handles both single-artifact and multi-shard scenarios
- Add .gitignore entries for timing cache and shard work files

Co-authored-by: Claude <claude@anthropic.com>

* ci: shard server Postgres tests into 4 parallel runners

Extract sharding logic into standalone, tested scripts and enable
4-shard parallel test execution for server Postgres CI:

Scripts:
- server/scripts/shard-split.js: Node.js bin-packing solver that
  assigns test packages to shards using timing data from previous runs.
  Two-tier strategy: light packages (<2min) whole, heavy packages
  (api4, app) split at individual test level.
- server/scripts/run-shard-tests.sh: Multi-run wrapper that calls
  gotestsum directly for each package group with -run regex filters.
- server/scripts/shard-split.test.js: 8 test cases covering round-robin
  fallback, timing-based balancing, heavy package splitting, JUnit XML
  fallback, and enterprise package separation.

Workflow changes:
- server-test-template.yml: Add shard splitting step that discovers test
  packages and runs the solver. Modified Run Tests step to use wrapper
  script when sharding is active.
- server-ci.yml: Add 4-shard matrix to test-postgres-normal. Update
  merge job artifact patterns for shard-specific names.

Performance: 7.2 min with timing cache vs 62.5 min baseline = 88%
wall-time improvement. First run without cache uses JUnit XML fallback
or round-robin, then populates the cache for subsequent runs.

Co-authored-by: Claude <claude@anthropic.com>

* fix: raise heavy package threshold to 5 min to preserve test isolation

sqlstore integrity tests scan the entire database and fail when other
packages' test data is present. At 182s, sqlstore was just over the
120s threshold and getting split at test level. Raising to 300s keeps
only api4 (~38 min) and app (~15 min) as heavy — where the real
sharding gains are — while sqlstore, elasticsearch, etc. stay whole
and maintain their test isolation guarantees.

Co-authored-by: Claude <claude@anthropic.com>

* ci: only save test timing cache on default branch

PR branches always restore from master's timing cache via restore-keys
prefix matching. Timing data is stable day-to-day so this eliminates
cache misses on first PR runs and reduces cache storage.

Co-authored-by: Claude <claude@anthropic.com>

* ci: skip FIPS tests on PRs (enterprise CI handles compile check)

Per review feedback: the enterprise CI already runs a FIPS compile
check on every PR. Running the full FIPS test suite on PRs is redundant
since it uses the identical test suite as non-FIPS — the only
FIPS-specific failure mode is a build failure from non-approved crypto
imports, which the enterprise compile check catches.

Full FIPS tests continue to run on every push to master.

Co-authored-by: Claude <claude@anthropic.com>

* fix: address review feedback on run-shard-tests.sh

- Remove set -e so all test runs execute even if earlier ones fail;
  track failures and exit with error at the end (wiggin77)
- Remove unused top-level COVERAGE_FLAG variable (wiggin77)
- Fix RUN_IDX increment position so report, json, and coverage files
  share the same index (wiggin77)
- Update workflow comment: heavy threshold is 5 min, not 2 min (wiggin77)

Co-authored-by: Claude <claude@anthropic.com>

* style: use node: prefix for built-in fs module in shard-split.js

Co-authored-by: Claude <claude@anthropic.com>

* fix: avoid interpolating file paths into generated shell script

Read shard package lists from files at runtime instead of interpolating
them into the generated script via printf. This prevents theoretical
shell metacharacter injection from directory names, as flagged by
DryRun Security.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): rename merged artifact to match server-ci-report glob

The merged artifact was named postgres-server-test-logs-merged which
does not match the *-test-logs pattern in server-ci-report.yml,
causing Postgres test results to be missing from PR/commit reports.

Also pins junit-report-merger to exact version 7.0.0 for supply chain
safety.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): pass RACE_MODE env into Docker container

RACE_MODE was set on the host runner but never included in the docker
run --env list. The light-package path worked because the heredoc
expanded on the host, but run-shard-tests.sh reads RACE_MODE at
runtime inside the container where it was unset. This caused heavy
packages (api4, app) to silently lose -race detection.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): discover new tests in heavy packages not in timing cache

Tests not present in the timing cache (newly added or renamed) would
not appear in any shard -run regex, causing them to silently skip.
After building items from the cache, run go test -list to discover
current test names and assign any cache-missing tests to shards via
the normal bin-packing algorithm with a small default duration.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): add missing line continuation backslash in docker run

The previous --env FIPS_ENABLED line was missing a trailing backslash
after adding --env RACE_MODE, causing docker run to see a truncated
command and fail with "requires at least 1 argument".

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): add setup-go step for shard test discovery

go test -list in shard-split.js runs on the host runner via execSync,
but Go is only available inside the Docker container. Without this
step, every invocation fails silently and new-test discovery is a
no-op. Adding actions/setup-go before the shard split step ensures
the Go toolchain is available on the host.

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>

* updated go to version 1.25.8 (#35817)

* updated go to version 1.25.8

* updated gotestsum version to work with go 1.25.8

go 1.25 does not work with indirect tools 0.11 dependency pulled by
gotestsum.

* Use sync.WaitGroup.Go to simplify goroutine creation

Replace the wg.Add(1) + go func() { defer wg.Done() }() pattern with
wg.Go(), which was introduced in Go 1.25.

* pushes fips image on workflow dispatch to allow fips test to run on go version update

* fix new requirements for FIPS compliance imposed on updating to go 1.25.8

* updates openssl symbol check for library shipped with FIPS new versions

go-openssl v2 shipped with FIPS versions starting from 1.25 uses mkcgo to generate
bindings causing symbol names to be different.

* removes temp workflow-dispatch condition

* keep versions out of agents md file

* upgrade golangci-lint (#35845)

* test: clean up channel store data after TestChannelStore (#36066)

TestChannelStore sub-tests create channels, members, and team members
using fake TeamIds and UserIds (model.NewId() for non-existent rows).
These records are left in the database and cause integrity tests
(TestCheck*) running in the same binary to fail their full-table scans.

Register a t.Cleanup on TestChannelStore that purges the affected
tables entirely. A blanket purge is safe: the schema enforces no FK
constraints, and every test suite creates its own data independently.

* Fix command injection in server-test-template workflow (#36080)

Replace the unquoted heredoc (which embedded GITHUB_HEAD_REF into a
generated script) with a cp of the existing run-shard-tests.sh, which
already handles the light-only case. Pass BUILD_NUMBER and TEST_TARGET
as explicit docker env vars instead of interpolating them into script
content.

* fix(ci): restore testname format in sharded gotestsum runs (#36078)

run-shard-tests.sh called gotestsum directly without --format, so it
fell back to gotestsum's default (pkgname) instead of the testname
format set by the Makefile. Pass --format "${GOTESTSUM_FORMAT:-testname}"
to match the Makefile default.

Co-authored-by: Mattermost Build <build@mattermost.com>

* fix(lint): fix pre-existing golangci-lint v2.11.4 issues

Fix misspelling in comment and redundant nil check flagged by the
upgraded linter.

* ci: use golang image for test runner on release-10.11

mattermost-build-server images are not built for release branches.
Use the official golang image which is always available for any Go version.

* ci: use mattermost/mattermost-build-server for release-10.11

The mattermostdevelopment/ images are only built for master.
The production mattermost/ images are built for release branches.

* ci: use mattermost/mattermost-build-server in mmctl test template

The mattermostdevelopment/ images are only built for master.
The production mattermost/ images are built for release branches.

---------

Co-authored-by: Pavel Zeman <pavel.zeman@mattermost.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Carlos Garcia <carlos.garcia@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-04-16 09:14:15 -03:00

394 строки
13 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"path/filepath"
"slices"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) []model.FileData {
functions := map[string]func(c request.CTX) (*model.FileData, error){
"metadata": a.getSupportPacketMetadata,
"stats": a.getSupportPacketStats,
"jobs": a.getSupportPacketJobList,
"permissions": a.getSupportPacketPermissionsInfo,
"plugins": a.getPluginsFile,
"schema": a.getSupportPacketDatabaseSchema,
}
var (
// If any errors we come across within this function, we will log it in a warning.txt file so that we know why certain files did not get produced if any
warnings *multierror.Error
// Creating an array of files that we are going to be adding to our zip file
fileDatas []model.FileData
wg sync.WaitGroup
mut sync.Mutex // Protects warnings and fileDatas
)
wg.Go(func() {
for name, fn := range functions {
fileData, err := fn(rctx)
mut.Lock()
if err != nil {
rctx.Logger().Error("Failed to generate file for Support Packet",
mlog.String("file", name),
mlog.Err(err),
)
warnings = multierror.Append(warnings, err)
}
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
}
mut.Unlock()
}
// Generate platform support packet
files, err := a.Srv().Platform().GenerateSupportPacket(rctx, options)
mut.Lock()
if err != nil {
warnings = multierror.Append(warnings, err)
}
if fileDatas != nil {
if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable {
hostname := cluster.GetMyClusterInfo().Hostname
for _, file := range files {
// When running in a cluster, the files are generated with the cluster node name as the directory, e.g. 7917b92f9e4c/mattermost.log
fileDatas = append(fileDatas, model.FileData{
Filename: filepath.Join(hostname, file.Filename),
Body: file.Body,
})
}
} else {
// When running in standalone mode, all files are generated with the same directory name, e.g. mattermost.log.
fileDatas = append(fileDatas, files...)
}
}
mut.Unlock()
})
// Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time
if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable {
wg.Go(func() {
files, err := cluster.GenerateSupportPacket(rctx, options)
mut.Lock()
if err != nil {
rctx.Logger().Error("Failed to generate Support Packet from cluster nodes", mlog.Err(err))
warnings = multierror.Append(warnings, err)
}
for _, node := range files {
fileDatas = append(fileDatas, node...)
}
mut.Unlock()
})
}
wg.Wait()
pluginContext := pluginContext(rctx)
a.ch.RunMultiHook(func(hooks plugin.Hooks, manifest *model.Manifest) bool {
// If the plugin defined the support_packet prop it means there is a UI element to include it in the support packet.
// Check if the plugin is in the list of plugins to include in the Support Packet.
if _, ok := manifest.Props["support_packet"]; ok {
if !slices.Contains(options.PluginPackets, manifest.Id) {
return true
}
}
// Otherwise, just call the hook as the plugin decided to always include it in the Support Packet.
pluginData, err := hooks.GenerateSupportData(pluginContext)
if err != nil {
rctx.Logger().Warn("Failed to generate plugin file for Support Packet", mlog.String("plugin", manifest.Id), mlog.Err(err))
warnings = multierror.Append(warnings, err)
return true
}
for _, data := range pluginData {
fileDatas = append(fileDatas, *data)
}
return true
}, plugin.GenerateSupportDataID)
// Adding a warning.txt file to the fileDatas if any warning
if warnings != nil {
fileDatas = append(fileDatas, model.FileData{
Filename: model.SupportPacketErrorFile,
Body: []byte(warnings.Error()),
})
}
return fileDatas
}
func (a *App) getSupportPacketStats(rctx request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
stats model.SupportPacketStats
)
stats.RegisteredUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeDeleted: true})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get registered user count"))
}
stats.ActiveUsers, err = a.Srv().Store().User().Count(model.UserCountOptions{})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get active user count"))
}
stats.DailyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(DayMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get daily active user count"))
}
stats.MonthlyActiveUsers, err = a.Srv().Store().User().AnalyticsActiveCount(MonthMilliseconds, model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: false})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get monthly active user count"))
}
stats.DeactivatedUsers, err = a.Srv().Store().User().AnalyticsGetInactiveUsersCount()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get deactivated user count"))
}
stats.Guests, err = a.Srv().Store().User().AnalyticsGetGuestCount()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get guest count"))
}
stats.BotAccounts, err = a.Srv().Store().User().Count(model.UserCountOptions{IncludeBotAccounts: true, ExcludeRegularUsers: true})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get bot acount count"))
}
stats.Posts, err = a.Srv().Store().Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get post count"))
}
openChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypeOpen)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get open channels count"))
}
privateChannels, err := a.Srv().Store().Channel().AnalyticsTypeCount("", model.ChannelTypePrivate)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get private channels count"))
}
stats.Channels = openChannels + privateChannels
stats.Teams, err = a.Srv().Store().Team().AnalyticsTeamCount(nil)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get team count"))
}
stats.SlashCommands, err = a.Srv().Store().Command().AnalyticsCommandCount("")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get command count"))
}
stats.IncomingWebhooks, err = a.Srv().Store().Webhook().AnalyticsIncomingCount("", "")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get incoming webhook count"))
}
stats.OutgoingWebhooks, err = a.Srv().Store().Webhook().AnalyticsOutgoingCount("")
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to get outgoing webhook count"))
}
b, err := yaml.Marshal(&stats)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
}
fileData := &model.FileData{
Filename: "stats.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (a *App) getSupportPacketJobList(rctx request.CTX) (*model.FileData, error) {
const numberOfJobsRuns = 5
var (
rErr *multierror.Error
err error
jobs model.SupportPacketJobList
)
jobs.LDAPSyncJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeLdapSync, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP sync jobs"))
}
jobs.DataRetentionJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeDataRetention, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting data retention jobs"))
}
jobs.MessageExportJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMessageExport, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting message export jobs"))
}
jobs.ElasticPostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostIndexing, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post indexing jobs"))
}
jobs.ElasticPostAggregationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeElasticsearchPostAggregation, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting ES post aggregation jobs"))
}
jobs.BlevePostIndexingJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeBlevePostIndexing, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting bleve post indexing jobs"))
}
jobs.MigrationJobs, err = a.Srv().Store().Job().GetAllByTypePage(rctx, model.JobTypeMigrations, 0, numberOfJobsRuns)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting migration jobs"))
}
b, err := yaml.Marshal(&jobs)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal jobs list into yaml"))
}
fileData := &model.FileData{
Filename: "jobs.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (a *App) getSupportPacketPermissionsInfo(_ request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
permissions model.SupportPacketPermissionInfo
)
var allSchemes []*model.Scheme
perPage := 100
page := 0
for {
schemes, appErr := a.GetSchemesPage("", page, perPage)
if appErr != nil {
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of schemes"))
break
}
allSchemes = append(allSchemes, schemes...)
if len(schemes) < perPage {
break
}
page++
}
for _, s := range allSchemes {
s.Sanitize()
}
permissions.Schemes = allSchemes
roles, appErr := a.GetAllRoles()
if appErr != nil {
rErr = multierror.Append(errors.Wrap(appErr, "failed to get list of roles"))
}
for _, r := range roles {
r.Sanitize()
}
permissions.Roles = roles
b, err := yaml.Marshal(&permissions)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal permission info into yaml"))
}
fileData := &model.FileData{
Filename: "permissions.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (a *App) getPluginsFile(_ request.CTX) (*model.FileData, error) {
// Getting the plugins installed on the server, prettify it, and then add them to the file data array
plugins, appErr := a.GetPlugins()
if appErr != nil {
return nil, errors.Wrap(appErr, "failed to get plugin list for Support Packet")
}
var pluginList model.SupportPacketPluginList
for _, p := range plugins.Active {
pluginList.Enabled = append(pluginList.Enabled, p.Manifest)
}
for _, p := range plugins.Inactive {
pluginList.Disabled = append(pluginList.Disabled, p.Manifest)
}
pluginsPrettyJSON, err := json.MarshalIndent(pluginList, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal plugin list into json")
}
fileData := &model.FileData{
Filename: "plugins.json",
Body: pluginsPrettyJSON,
}
return fileData, nil
}
func (a *App) getSupportPacketMetadata(_ request.CTX) (*model.FileData, error) {
metadata, err := model.GeneratePacketMetadata(model.SupportPacketType, a.TelemetryId(), a.License(), nil)
if err != nil {
return nil, errors.Wrap(err, "failed to generate Packet metadata")
}
b, err := yaml.Marshal(metadata)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal Packet metadata into yaml")
}
fileData := &model.FileData{
Filename: model.PacketMetadataFileName,
Body: b,
}
return fileData, nil
}
func (a *App) getSupportPacketDatabaseSchema(rctx request.CTX) (*model.FileData, error) {
if *a.Config().SqlSettings.DriverName != model.DatabaseDriverPostgres {
return nil, nil
}
schemaInfo, err := a.Srv().Store().GetSchemaDefinition()
if err != nil {
return nil, errors.Wrap(err, "failed to get schema definition")
}
schemaDump, err := yaml.Marshal(schemaInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal schema into YAML")
}
return &model.FileData{
Filename: "database_schema.yaml",
Body: schemaDump,
}, nil
}