mattermost-community-enterp.../channels/app/platform/support_packet.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

316 lines
9.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"bytes"
"encoding/json"
"os"
"runtime"
rpprof "runtime/pprof"
"strings"
"time"
"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/shared/mlog"
"github.com/mattermost/mattermost/server/public/shared/request"
)
const (
envVarInstallType = "MM_INSTALL_TYPE"
unknownDataPoint = "unknown"
)
func (ps *PlatformService) GenerateSupportPacket(rctx request.CTX, options *model.SupportPacketOptions) ([]model.FileData, error) {
functions := map[string]func(request.CTX) (*model.FileData, error){
"diagnostics": ps.getSupportPacketDiagnostics,
"config": ps.getSanitizedConfigFile,
"cpu profile": ps.getCPUProfile,
"heap profile": ps.getHeapProfile,
"goroutines": ps.getGoroutineProfile,
}
if options != nil && options.IncludeLogs {
functions["mattermost log"] = ps.GetLogFile
}
var (
fileDatas []model.FileData
rErr *multierror.Error
)
for name, fn := range functions {
fileData, err := fn(rctx)
if err != nil {
rctx.Logger().Error("Failed to generate file for Support Packet",
mlog.String("file", name),
mlog.Err(err),
)
rErr = multierror.Append(rErr, err)
}
if fileData != nil {
fileDatas = append(fileDatas, *fileData)
}
}
if options != nil && options.IncludeLogs {
advancedLogs, err := ps.GetAdvancedLogs(rctx)
if err != nil {
rctx.Logger().Error("Failed to read advanced log files for Support Packet", mlog.Err(err))
rErr = multierror.Append(rErr, err)
}
for _, log := range advancedLogs {
fileDatas = append(fileDatas, *log)
}
}
return fileDatas, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model.FileData, error) {
var (
rErr *multierror.Error
err error
d model.SupportPacketDiagnostics
)
d.Version = model.CurrentSupportPacketVersion
/* License */
if license := ps.License(); license != nil {
d.License.Company = license.Customer.Company
d.License.Users = model.SafeDereference(license.Features.Users)
d.License.SkuShortName = license.SkuShortName
d.License.IsTrial = license.IsTrial
d.License.IsGovSKU = license.IsGovSku
}
/* Server */
d.Server.OS = runtime.GOOS
d.Server.Architecture = runtime.GOARCH
d.Server.Hostname, err = os.Hostname()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting hostname"))
}
d.Server.Version = model.CurrentVersion
d.Server.BuildHash = model.BuildHash
installationType := os.Getenv(envVarInstallType)
if installationType == "" {
installationType = unknownDataPoint
}
d.Server.InstallationType = installationType
/* Config */
d.Config.Source = ps.DescribeConfig()
/* DB */
d.Database.Type, d.Database.SchemaVersion, err = ps.DatabaseTypeAndSchemaVersion()
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB type and schema version"))
}
databaseVersion, err := ps.Store.GetDbVersion(false)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting DB version"))
} else {
d.Database.Version = databaseVersion
}
d.Database.MasterConnectios = ps.Store.TotalMasterDbConnections()
d.Database.ReplicaConnectios = ps.Store.TotalReadDbConnections()
d.Database.SearchConnections = ps.Store.TotalSearchDbConnections()
/* File store */
d.FileStore.Status = model.StatusOk
err = ps.FileBackend().TestConnection()
if err != nil {
d.FileStore.Status = model.StatusFail
d.FileStore.Error = err.Error()
}
d.FileStore.Driver = ps.FileBackend().DriverName()
/* Websockets */
d.Websocket.Connections = ps.TotalWebsocketConnections()
/* Cluster */
if cluster := ps.Cluster(); cluster != nil {
d.Cluster.ID = cluster.GetClusterId()
clusterInfo, e := cluster.GetClusterInfos()
if e != nil {
rErr = multierror.Append(rErr, errors.Wrap(e, "error while getting cluster infos"))
} else {
d.Cluster.NumberOfNodes = max(len(clusterInfo), 1) // clusterInfo is empty if the node is the only one in the cluster
}
}
/* LDAP */
if ldap := ps.LdapDiagnostic(); ldap != nil && (*ps.Config().LdapSettings.Enable || *ps.Config().LdapSettings.EnableSync) {
d.LDAP.Status = model.StatusOk
appErr := ldap.RunTest(rctx)
if appErr != nil {
d.LDAP.Status = model.StatusFail
d.LDAP.Error = appErr.Error()
}
severName, serverVersion := unknownDataPoint, unknownDataPoint
// Only if the LDAP test was successful, try to get the LDAP server info
if d.LDAP.Status == model.StatusOk {
severName, serverVersion, err = ldap.GetVendorNameAndVendorVersion(rctx)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "error while getting LDAP vendor info"))
}
if severName == "" {
severName = unknownDataPoint
}
if serverVersion == "" {
serverVersion = unknownDataPoint
}
}
d.LDAP.ServerName = severName
d.LDAP.ServerVersion = serverVersion
}
/* SAML */
if idpDescriptorURL := model.SafeDereference(ps.Config().SamlSettings.IdpDescriptorURL); idpDescriptorURL != "" {
d.SAML.ProviderType = detectSAMLProviderType(idpDescriptorURL)
}
/* Elastic Search */
if se := ps.SearchEngine.ElasticsearchEngine; se != nil {
d.ElasticSearch.Backend = *ps.Config().ElasticsearchSettings.Backend
d.ElasticSearch.ServerVersion = se.GetFullVersion()
d.ElasticSearch.ServerPlugins = se.GetPlugins()
if *ps.Config().ElasticsearchSettings.EnableIndexing {
appErr := se.TestConfig(rctx, ps.Config())
if appErr != nil {
d.ElasticSearch.Error = appErr.Error()
}
}
}
b, err := yaml.Marshal(&d)
if err != nil {
rErr = multierror.Append(errors.Wrap(err, "failed to marshal Support Packet into yaml"))
}
fileData := &model.FileData{
Filename: "diagnostics.yaml",
Body: b,
}
return fileData, rErr.ErrorOrNil()
}
func (ps *PlatformService) getSanitizedConfigFile(rctx request.CTX) (*model.FileData, error) {
config := ps.getSanitizedConfig(rctx, &model.SanitizeOptions{PartiallyRedactDataSources: true})
spConfig := model.SupportPacketConfig{
Config: config,
FeatureFlags: *config.FeatureFlags,
}
sanitizedConfigPrettyJSON, err := json.MarshalIndent(spConfig, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to sanitized config into json")
}
fileData := &model.FileData{
Filename: "sanitized_config.json",
Body: sanitizedConfigPrettyJSON,
}
return fileData, nil
}
func (ps *PlatformService) getCPUProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.StartCPUProfile(&b)
if err != nil {
return nil, errors.Wrap(err, "failed to start CPU profile")
}
time.Sleep(cpuProfileDuration)
rpprof.StopCPUProfile()
fileData := &model.FileData{
Filename: "cpu.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getHeapProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("heap").WriteTo(&b, 0)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup heap profile")
}
fileData := &model.FileData{
Filename: "heap.prof",
Body: b.Bytes(),
}
return fileData, nil
}
func (ps *PlatformService) getGoroutineProfile(_ request.CTX) (*model.FileData, error) {
var b bytes.Buffer
err := rpprof.Lookup("goroutine").WriteTo(&b, 2)
if err != nil {
return nil, errors.Wrap(err, "failed to lookup goroutine profile")
}
fileData := &model.FileData{
Filename: "goroutines",
Body: b.Bytes(),
}
return fileData, nil
}
// detectSAMLProviderType attempts to identify the SAML provider type based on the IdpDescriptorURL.
// It returns "unknown" if the provider cannot be identified.
func detectSAMLProviderType(idpDescriptorURL string) string {
if idpDescriptorURL == "" {
return unknownDataPoint
}
// Normalize URL to lowercase for case-insensitive matching
normalizedURL := strings.ToLower(idpDescriptorURL)
// Check for common SAML provider patterns in the EntityID/IdpDescriptorURL
// Order matters: more specific patterns should come before generic ones
switch {
case strings.Contains(normalizedURL, "login.microsoftonline.com") || strings.Contains(normalizedURL, "sts.windows.net"):
return "Azure AD"
case strings.Contains(normalizedURL, ".okta.com") || strings.Contains(normalizedURL, ".oktapreview.com"):
return "Okta"
case strings.Contains(normalizedURL, ".auth0.com"):
return "Auth0"
case strings.Contains(normalizedURL, ".onelogin.com"):
return "OneLogin"
case strings.Contains(normalizedURL, "accounts.google.com"):
return "Google Workspace"
case strings.Contains(normalizedURL, "sso.jumpcloud.com"):
return "JumpCloud"
case strings.Contains(normalizedURL, "duo.com/saml2"):
return "Duo"
case strings.Contains(normalizedURL, ".centrify.com"):
return "Centrify"
case strings.Contains(normalizedURL, "/realms/"):
return "Keycloak"
case strings.Contains(normalizedURL, "/adfs/") || strings.Contains(normalizedURL, "/FederationMetadata/"):
return "ADFS"
case strings.Contains(normalizedURL, "shibboleth.net") || strings.Contains(normalizedURL, "/idp/shibboleth"):
return "Shibboleth"
default:
return unknownDataPoint
}
}