Files
team/server/public/model/support_packet.go
abab96223c [MM-67979] [MM-67980] Add SMTP and push proxy connectivity status to support packet diagnostics (#35837)
* MM-67979 MM-67980: Add SMTP and push proxy connectivity to support packet

Adds a `notifications` section to `diagnostics.yaml` in the support
packet with SMTP email and push proxy connectivity probe results.

- `notifications.email.status`: ok/fail/disabled based on whether
  SendEmailNotifications is enabled and an SMTP connection can be
  established using mail.TestConnection()
- `notifications.push.status`: ok/fail/disabled based on whether
  SendPushNotifications is enabled and an HTTP GET to the configured
  PushNotificationServer URL succeeds
- Error messages are included in the `error` field on failure
- No email or push notification is sent during the probe

* fix: handle errcheck lint violations in support_packet_test.go

Suppress unhandled error return values from rw.WriteString calls in
the mock SMTP server used in tests.

* fix: use 127.0.0.1 directly in SMTP reachability test

Replace localhost:0 with 127.0.0.1:0 for the mock SMTP listener so
that it always binds to the loopback interface. In CI Docker containers
localhost may resolve to the container IP rather than 127.0.0.1, causing
the SMTP dial to fail with connection refused. Also switch from string
manipulation to net.TCPAddr type assertion for reliable host/port
extraction.

* fix: override MM_EMAILSETTINGS_SMTPSERVER env var in SMTP reachability test

The CI environment sets MM_EMAILSETTINGS_SMTPSERVER=inbucket via
test.env. Mattermost's config Store.Set() calls GetEnvironment()
(os.Environ()) on every UpdateConfig, so env vars silently override
any programmatic config change. Use t.Setenv before UpdateConfig so
the env var points to 127.0.0.1 for the duration of the subtest.

* Add model.StatusDisabled constant and use it in support_packet.go

Replace "disabled" string literals with model.StatusDisabled for
consistency with model.StatusOk and model.StatusFail.

* refactor: use utils.GetHostnameFromSiteURL, extract testPushProxyConnection helper, set LDAP StatusDisabled

- Replace manual url.Parse with utils.GetHostnameFromSiteURL (consistent with app/config.go)
- Extract push proxy HTTP check into testPushProxyConnection with TODO to move to its own package
- Set d.LDAP.Status = model.StatusDisabled when LDAP is not configured
- Replace "disabled" string literals in tests with model.StatusDisabled

* feat: add status field to ElasticSearch diagnostics with ok/fail/disabled

When indexing is enabled, reports ok or fail based on TestConfig result.
When indexing is disabled or the engine is unavailable, reports disabled.
Backend/ServerVersion/ServerPlugins are still collected when the engine
exists regardless of indexing status.

* fix: update Happy path test for LDAP and ES StatusDisabled assertions

Both are disabled in the test environment so they now report StatusDisabled.

* feat: use GET /version endpoint for push proxy connectivity check

Use url.JoinPath to construct the /version path safely, replacing
raw root URL access. Also validate the HTTP status code so non-2xx/3xx
responses are treated as failures.

---------

Co-authored-by: Mattermost Build <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
2026-04-28 16:11:43 +02:00

209 lines
7.1 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"io"
"time"
)
const (
CurrentSupportPacketVersion = 2
SupportPacketErrorFile = "warning.txt"
)
type SupportPacketDiagnostics struct {
Version int `yaml:"version"`
License struct {
Company string `yaml:"company"`
Users int `yaml:"users"`
SkuShortName string `yaml:"sku_short_name"`
IsTrial bool `yaml:"is_trial,omitempty"`
IsGovSKU bool `yaml:"is_gov_sku,omitempty"`
} `yaml:"license"`
Server struct {
OS string `yaml:"os"`
Architecture string `yaml:"architecture"`
CPUCores int `yaml:"cpu_cores"`
TotalMemoryMB uint64 `yaml:"total_memory_mb"`
ContainerCPULimit float64 `yaml:"container_cpu_limit,omitempty"`
ContainerMemoryLimitMB uint64 `yaml:"container_memory_limit_mb,omitempty"`
OpenFileDescriptors int64 `yaml:"open_file_descriptors"`
MaxFileDescriptors int64 `yaml:"max_file_descriptors"`
Hostname string `yaml:"hostname"`
ProcessID int `yaml:"process_id"`
StartedAt time.Time `yaml:"started_at"`
HostStartedAt time.Time `yaml:"host_started_at,omitempty"`
Version string `yaml:"version"`
BuildHash string `yaml:"build_hash"`
GoVersion string `yaml:"go_version"`
InstallationType string `yaml:"installation_type"`
} `yaml:"server"`
Config struct {
Source string `yaml:"store_type"`
} `yaml:"config"`
Database struct {
Type string `yaml:"type"`
Version string `yaml:"version"`
SchemaVersion string `yaml:"schema_version"`
MasterConnectios int `yaml:"master_connections"`
ReplicaConnectios int `yaml:"replica_connections"`
SearchConnections int `yaml:"search_connections"`
} `yaml:"database"`
FileStore struct {
Status string `yaml:"file_status"`
Error string `yaml:"erorr,omitempty"`
Driver string `yaml:"file_driver"`
FilesystemType string `yaml:"filesystem_type,omitempty"`
TotalMB uint64 `yaml:"total_mb,omitempty"`
AvailableMB uint64 `yaml:"available_mb,omitempty"`
} `yaml:"file_store"`
Websocket struct {
Connections int `yaml:"connections"`
} `yaml:"websocket"`
Cluster struct {
ID string `yaml:"id"`
NumberOfNodes int `yaml:"number_of_nodes"`
} `yaml:"cluster"`
Notifications struct {
Email struct {
Status string `yaml:"status"`
Error string `yaml:"error,omitempty"`
} `yaml:"email,omitempty"`
Push struct {
Status string `yaml:"status"`
Error string `yaml:"error,omitempty"`
} `yaml:"push,omitempty"`
} `yaml:"notifications,omitempty"`
LDAP struct {
Status string `yaml:"status,omitempty"`
Error string `yaml:"error,omitempty"`
ServerName string `yaml:"server_name,omitempty"`
ServerVersion string `yaml:"server_version,omitempty"`
} `yaml:"ldap"`
SAML struct {
ProviderType string `yaml:"provider_type,omitempty"`
} `yaml:"saml"`
ElasticSearch struct {
Status string `yaml:"status,omitempty"`
Backend string `yaml:"backend,omitempty"`
ServerVersion string `yaml:"server_version,omitempty"`
ServerPlugins []string `yaml:"server_plugins,omitempty"`
Error string `yaml:"error,omitempty"`
} `yaml:"elastic"`
}
type SupportPacketStats struct {
RegisteredUsers int64 `yaml:"registered_users"`
ActiveUsers int64 `yaml:"active_users"`
DailyActiveUsers int64 `yaml:"daily_active_users"`
MonthlyActiveUsers int64 `yaml:"monthly_active_users"`
DeactivatedUsers int64 `yaml:"deactivated_users"`
Guests int64 `yaml:"guests"`
SingleChannelGuests int64 `yaml:"single_channel_guests"`
BotAccounts int64 `yaml:"bot_accounts"`
Posts int64 `yaml:"posts"`
Channels int64 `yaml:"channels"`
Teams int64 `yaml:"teams"`
SlashCommands int64 `yaml:"slash_commands"`
IncomingWebhooks int64 `yaml:"incoming_webhooks"`
OutgoingWebhooks int64 `yaml:"outgoing_webhooks"`
}
// SupportPacketJobList contains the list of latest run enterprise job runs.
// It is included in the Support Packet.
type SupportPacketJobList struct {
LDAPSyncJobs []*Job `yaml:"ldap_sync_jobs"`
DataRetentionJobs []*Job `yaml:"data_retention_jobs"`
MessageExportJobs []*Job `yaml:"message_export_jobs"`
ElasticPostIndexingJobs []*Job `yaml:"elastic_post_indexing_jobs"`
ElasticPostAggregationJobs []*Job `yaml:"elastic_post_aggregation_jobs"`
MigrationJobs []*Job `yaml:"migration_jobs"`
}
// SupportPacketPermissionInfo contains the list of schemes and the list of roles.
// It is included in the Support Packet.
type SupportPacketPermissionInfo struct {
Roles []*Role `yaml:"roles"`
Schemes []*Scheme `yaml:"schemes"`
}
// SupportPacketConfig contains the Mattermost configuration. In contrast to [Config], it also contains the list of Feature Flags.
// It is included in the Support Packet.
type SupportPacketConfig struct {
*Config
FeatureFlags FeatureFlags `json:"FeatureFlags"`
}
// SupportPacketPluginList contains the list of enabled and disabled plugins.
// It is included in the Support Packet.
type SupportPacketPluginList struct {
Enabled []Manifest `json:"enabled"`
Disabled []Manifest `json:"disabled"`
}
// SupportPacketDatabaseSchema contains the database schema information.
// It is included in the Support Packet.
type SupportPacketDatabaseSchema struct {
DatabaseCollation string `yaml:"database_collation,omitempty"`
DatabaseEncoding string `yaml:"database_encoding,omitempty"`
Tables []DatabaseTable `yaml:"tables"`
}
// DatabaseTable represents a table in the database schema.
type DatabaseTable struct {
Name string `yaml:"name"`
Collation string `yaml:"collation,omitempty"`
Options map[string]string `yaml:"options,omitempty"`
Columns []DatabaseColumn `yaml:"columns"`
Indexes []DatabaseIndex `yaml:"indexes,omitempty"`
}
// DatabaseColumn represents a column in a database table.
type DatabaseColumn struct {
Name string `yaml:"name"`
DataType string `yaml:"data_type"`
MaxLength int64 `yaml:"max_length,omitempty"`
IsNullable bool `yaml:"is_nullable"`
}
// DatabaseIndex represents an index in a database table.
type DatabaseIndex struct {
Name string `yaml:"name"`
Definition string `yaml:"definition"`
}
type FileData struct {
Filename string
Body []byte
}
type SupportPacketOptions struct {
IncludeLogs bool `json:"include_logs"` // IncludeLogs is the option to include server logs
PluginPackets []string `json:"plugin_packets"` // PluginPackets is a list of pluginids to call hooks
}
// SupportPacketOptionsFromReader decodes a json-encoded request from the given io.Reader.
func SupportPacketOptionsFromReader(reader io.Reader) (*SupportPacketOptions, error) {
var r *SupportPacketOptions
err := json.NewDecoder(reader).Decode(&r)
if err != nil {
return nil, err
}
return r, nil
}