diff --git a/build/manifest/main.go b/build/manifest/main.go deleted file mode 100644 index 890414a..0000000 --- a/build/manifest/main.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "strings" - - "github.com/mattermost/mattermost/server/public/model" - "github.com/pkg/errors" -) - -const pluginIDGoFileTemplate = `// This file is automatically generated. Do not modify it manually. - -package main - -import ( - "encoding/json" - "strings" - - "github.com/mattermost/mattermost/server/public/model" -) - -var manifest *model.Manifest - -const manifestStr = ` + "`" + ` -%s -` + "`" + ` - -func init() { - _ = json.NewDecoder(strings.NewReader(manifestStr)).Decode(&manifest) -} -` - -const pluginIDJSFileTemplate = `// This file is automatically generated. Do not modify it manually. - -const manifest = JSON.parse(` + "`" + ` -%s -` + "`" + `); - -export default manifest; -` - -// These build-time vars are read from shell commands and populated in ../setup.mk -var ( - BuildHashShort string - BuildTagLatest string - BuildTagCurrent string -) - -func main() { - if len(os.Args) <= 1 { - panic("no cmd specified") - } - - manifest, err := findManifest() - if err != nil { - panic("failed to find manifest: " + err.Error()) - } - - cmd := os.Args[1] - switch cmd { - case "id": - dumpPluginID(manifest) - - case "version": - dumpPluginVersion(manifest) - - case "has_server": - if manifest.HasServer() { - fmt.Printf("true") - } - - case "has_webapp": - if manifest.HasWebapp() { - fmt.Printf("true") - } - - case "apply": - if err := applyManifest(manifest); err != nil { - panic("failed to apply manifest: " + err.Error()) - } - - case "dist": - if err := distManifest(manifest); err != nil { - panic("failed to write manifest to dist directory: " + err.Error()) - } - - case "check": - if err := manifest.IsValid(); err != nil { - panic("failed to check manifest: " + err.Error()) - } - - default: - panic("unrecognized command: " + cmd) - } -} - -func findManifest() (*model.Manifest, error) { - _, manifestFilePath, err := model.FindManifest(".") - if err != nil { - return nil, errors.Wrap(err, "failed to find manifest in current working directory") - } - manifestFile, err := os.Open(manifestFilePath) - if err != nil { - return nil, errors.Wrapf(err, "failed to open %s", manifestFilePath) - } - defer manifestFile.Close() - - // Re-decode the manifest, disallowing unknown fields. When we write the manifest back out, - // we don't want to accidentally clobber anything we won't preserve. - var manifest model.Manifest - decoder := json.NewDecoder(manifestFile) - decoder.DisallowUnknownFields() - if err = decoder.Decode(&manifest); err != nil { - return nil, errors.Wrap(err, "failed to parse manifest") - } - - // If no version is listed in the manifest, generate one based on the state of the current - // commit, and use the first version we find (to prevent causing errors) - if manifest.Version == "" { - var version string - tags := strings.Fields(BuildTagCurrent) - for _, t := range tags { - if strings.HasPrefix(t, "v") { - version = t - break - } - } - if version == "" { - if BuildTagLatest != "" { - version = BuildTagLatest + "+" + BuildHashShort - } else { - version = "v0.0.0+" + BuildHashShort - } - } - manifest.Version = strings.TrimPrefix(version, "v") - } - - // If no release notes specified, generate one from the latest tag, if present. - if manifest.ReleaseNotesURL == "" && BuildTagLatest != "" { - manifest.ReleaseNotesURL = manifest.HomepageURL + "releases/tag/" + BuildTagLatest - } - - return &manifest, nil -} - -// dumpPluginId writes the plugin id from the given manifest to standard out -func dumpPluginID(manifest *model.Manifest) { - fmt.Printf("%s", manifest.Id) -} - -// dumpPluginVersion writes the plugin version from the given manifest to standard out -func dumpPluginVersion(manifest *model.Manifest) { - fmt.Printf("%s", manifest.Version) -} - -// applyManifest propagates the plugin_id into the server and webapp folders, as necessary -func applyManifest(manifest *model.Manifest) error { - if manifest.HasServer() { - // generate JSON representation of Manifest. - manifestBytes, err := json.MarshalIndent(manifest, "", " ") - if err != nil { - return err - } - manifestStr := string(manifestBytes) - - // write generated code to file by using Go file template. - if err := os.WriteFile( - "server/manifest.go", - []byte(fmt.Sprintf(pluginIDGoFileTemplate, manifestStr)), - 0600, - ); err != nil { - return errors.Wrap(err, "failed to write server/manifest.go") - } - } - - if manifest.HasWebapp() { - // generate JSON representation of Manifest. - // JSON is very similar and compatible with JS's object literals. so, what we do here - // is actually JS code generation. - manifestBytes, err := json.MarshalIndent(manifest, "", " ") - if err != nil { - return err - } - manifestStr := string(manifestBytes) - - // Escape newlines - manifestStr = strings.ReplaceAll(manifestStr, `\n`, `\\n`) - - // write generated code to file by using JS file template. - if err := os.WriteFile( - "webapp/src/manifest.ts", - []byte(fmt.Sprintf(pluginIDJSFileTemplate, manifestStr)), - 0600, - ); err != nil { - return errors.Wrap(err, "failed to open webapp/src/manifest.ts") - } - } - - return nil -} - -// distManifest writes the manifest file to the dist directory -func distManifest(manifest *model.Manifest) error { - manifestBytes, err := json.MarshalIndent(manifest, "", " ") - if err != nil { - return err - } - - if err := os.WriteFile(fmt.Sprintf("dist/%s/plugin.json", manifest.Id), manifestBytes, 0600); err != nil { - return errors.Wrap(err, "failed to write plugin.json") - } - - return nil -} diff --git a/build/pluginctl/logs.go b/build/pluginctl/logs.go deleted file mode 100644 index f20e8bb..0000000 --- a/build/pluginctl/logs.go +++ /dev/null @@ -1,185 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "slices" - "strings" - "time" - - "github.com/mattermost/mattermost/server/public/model" -) - -const ( - logsPerPage = 100 // logsPerPage is the number of log entries to fetch per API call - timeStampFormat = "2006-01-02 15:04:05.000 Z07:00" -) - -// logs fetches the latest 500 log entries from Mattermost, -// and prints only the ones related to the plugin to stdout. -func logs(ctx context.Context, client *model.Client4, pluginID string) error { - err := checkJSONLogsSetting(ctx, client) - if err != nil { - return err - } - - logs, err := fetchLogs(ctx, client, 0, 500, pluginID, time.Unix(0, 0)) - if err != nil { - return fmt.Errorf("failed to fetch log entries: %w", err) - } - - err = printLogEntries(logs) - if err != nil { - return fmt.Errorf("failed to print logs entries: %w", err) - } - - return nil -} - -// watchLogs fetches log entries from Mattermost and print them to stdout. -// It will return without an error when ctx is canceled. -func watchLogs(ctx context.Context, client *model.Client4, pluginID string) error { - err := checkJSONLogsSetting(ctx, client) - if err != nil { - return err - } - - now := time.Now() - var oldestEntry string - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return nil - case <-ticker.C: - var page int - for { - logs, err := fetchLogs(ctx, client, page, logsPerPage, pluginID, now) - if err != nil { - return fmt.Errorf("failed to fetch log entries: %w", err) - } - - var allNew bool - logs, oldestEntry, allNew = checkOldestEntry(logs, oldestEntry) - - err = printLogEntries(logs) - if err != nil { - return fmt.Errorf("failed to print logs entries: %w", err) - } - - if !allNew { - // No more logs to fetch - break - } - page++ - } - } - } -} - -// checkOldestEntry check a if logs contains new log entries. -// It returns the filtered slice of log entries, the new oldest entry and whether or not all entries were new. -func checkOldestEntry(logs []string, oldest string) ([]string, string, bool) { - if len(logs) == 0 { - return nil, oldest, false - } - - newOldestEntry := logs[(len(logs) - 1)] - - i := slices.Index(logs, oldest) - switch i { - case -1: - // Every log entry is new - return logs, newOldestEntry, true - case len(logs) - 1: - // No new log entries - return nil, oldest, false - default: - // Filter out oldest log entry - return logs[i+1:], newOldestEntry, false - } -} - -// fetchLogs fetches log entries from Mattermost -// and filters them based on pluginID and timestamp. -func fetchLogs(ctx context.Context, client *model.Client4, page, perPage int, pluginID string, since time.Time) ([]string, error) { - logs, _, err := client.GetLogs(ctx, page, perPage) - if err != nil { - return nil, fmt.Errorf("failed to get logs from Mattermost: %w", err) - } - - logs, err = filterLogEntries(logs, pluginID, since) - if err != nil { - return nil, fmt.Errorf("failed to filter log entries: %w", err) - } - - return logs, nil -} - -// filterLogEntries filters a given slice of log entries by pluginID. -// It also filters out any entries which timestamps are older then since. -func filterLogEntries(logs []string, pluginID string, since time.Time) ([]string, error) { - type logEntry struct { - PluginID string `json:"plugin_id"` - Timestamp string `json:"timestamp"` - } - - var ret []string - - for _, e := range logs { - var le logEntry - err := json.Unmarshal([]byte(e), &le) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal log entry into JSON: %w", err) - } - if le.PluginID != pluginID { - continue - } - - let, err := time.Parse(timeStampFormat, le.Timestamp) - if err != nil { - return nil, fmt.Errorf("unknown timestamp format: %w", err) - } - if let.Before(since) { - continue - } - - // Log entries returned by the API have a newline a prefix. - // Remove that to make printing consistent. - e = strings.TrimPrefix(e, "\n") - - ret = append(ret, e) - } - - return ret, nil -} - -// printLogEntries prints a slice of log entries to stdout. -func printLogEntries(entries []string) error { - for _, e := range entries { - _, err := io.WriteString(os.Stdout, e+"\n") - if err != nil { - return fmt.Errorf("failed to write log entry to stdout: %w", err) - } - } - - return nil -} - -func checkJSONLogsSetting(ctx context.Context, client *model.Client4) error { - cfg, _, err := client.GetConfig(ctx) - if err != nil { - return fmt.Errorf("failed to fetch config: %w", err) - } - if cfg.LogSettings.FileJson == nil || !*cfg.LogSettings.FileJson { - return errors.New("JSON output for file logs are disabled. Please enable LogSettings.FileJson via the configration in Mattermost.") //nolint:revive,stylecheck - } - - return nil -} diff --git a/build/pluginctl/logs_test.go b/build/pluginctl/logs_test.go deleted file mode 100644 index 2917a27..0000000 --- a/build/pluginctl/logs_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "fmt" - "testing" - "time" -) - -func TestCheckOldestEntry(t *testing.T) { - for name, tc := range map[string]struct { - logs []string - oldest string - expectedLogs []string - expectedOldest string - expectedAllNew bool - }{ - "nil logs": { - logs: nil, - oldest: "oldest", - expectedLogs: nil, - expectedOldest: "oldest", - expectedAllNew: false, - }, - "empty logs": { - logs: []string{}, - oldest: "oldest", - expectedLogs: nil, - expectedOldest: "oldest", - expectedAllNew: false, - }, - "no new entries, one old entry": { - logs: []string{"old"}, - oldest: "old", - expectedLogs: []string{}, - expectedOldest: "old", - expectedAllNew: false, - }, - "no new entries, multipile old entries": { - logs: []string{"old1", "old2", "old3"}, - oldest: "old3", - expectedLogs: []string{}, - expectedOldest: "old3", - expectedAllNew: false, - }, - "one new entry, no old entry": { - logs: []string{"new"}, - oldest: "old", - expectedLogs: []string{"new"}, - expectedOldest: "new", - expectedAllNew: true, - }, - "multipile new entries, no old entry": { - logs: []string{"new1", "new2", "new3"}, - oldest: "old", - expectedLogs: []string{"new1", "new2", "new3"}, - expectedOldest: "new3", - expectedAllNew: true, - }, - "one new entry, one old entry": { - logs: []string{"old", "new"}, - oldest: "old", - expectedLogs: []string{"new"}, - expectedOldest: "new", - expectedAllNew: false, - }, - "one new entry, multipile old entries": { - logs: []string{"old1", "old2", "old3", "new"}, - oldest: "old3", - expectedLogs: []string{"new"}, - expectedOldest: "new", - expectedAllNew: false, - }, - "multipile new entries, ultipile old entries": { - logs: []string{"old1", "old2", "old3", "new1", "new2", "new3"}, - oldest: "old3", - expectedLogs: []string{"new1", "new2", "new3"}, - expectedOldest: "new3", - expectedAllNew: false, - }, - } { - t.Run(name, func(t *testing.T) { - logs, oldest, allNew := checkOldestEntry(tc.logs, tc.oldest) - - if allNew != tc.expectedAllNew { - t.Logf("expected allNew: %v, got %v", tc.expectedAllNew, allNew) - t.Fail() - } - if oldest != tc.expectedOldest { - t.Logf("expected oldest: %v, got %v", tc.expectedOldest, oldest) - t.Fail() - } - - compareSlice(t, tc.expectedLogs, logs) - }) - } -} - -func TestFilterLogEntries(t *testing.T) { - now := time.Now() - - for name, tc := range map[string]struct { - logs []string - pluginID string - since time.Time - expectedLogs []string - expectedErr bool - }{ - "nil slice": { - logs: nil, - expectedLogs: nil, - expectedErr: false, - }, - "empty slice": { - logs: []string{}, - expectedLogs: nil, - expectedErr: false, - }, - "no JSON": { - logs: []string{ - `{"foo"`, - }, - expectedLogs: nil, - expectedErr: true, - }, - "unknown time format": { - logs: []string{ - `{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53"}`, - }, - pluginID: "some.plugin.id", - expectedLogs: nil, - expectedErr: true, - }, - "one matching entry": { - logs: []string{ - `{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`, - }, - pluginID: "some.plugin.id", - expectedLogs: []string{ - `{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`, - }, - expectedErr: false, - }, - "filter out non plugin entries": { - logs: []string{ - `{"message":"bar1", "timestamp": "2023-12-18 10:58:52.091 +01:00"}`, - `{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`, - `{"message":"bar2", "timestamp": "2023-12-18 10:58:54.091 +01:00"}`, - }, - pluginID: "some.plugin.id", - expectedLogs: []string{ - `{"message":"foo", "plugin_id": "some.plugin.id", "timestamp": "2023-12-18 10:58:53.091 +01:00"}`, - }, - expectedErr: false, - }, - "filter out old entries": { - logs: []string{ - fmt.Sprintf(`{"message":"old2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(-2*time.Second).Format(timeStampFormat)), - fmt.Sprintf(`{"message":"old1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(-1*time.Second).Format(timeStampFormat)), - fmt.Sprintf(`{"message":"now", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Format(timeStampFormat)), - fmt.Sprintf(`{"message":"new1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(1*time.Second).Format(timeStampFormat)), - fmt.Sprintf(`{"message":"new2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(2*time.Second).Format(timeStampFormat)), - }, - pluginID: "some.plugin.id", - since: now, - expectedLogs: []string{ - fmt.Sprintf(`{"message":"new1", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(1*time.Second).Format(timeStampFormat)), - fmt.Sprintf(`{"message":"new2", "plugin_id": "some.plugin.id", "timestamp": "%s"}`, now.Add(2*time.Second).Format(timeStampFormat)), - }, - expectedErr: false, - }, - } { - t.Run(name, func(t *testing.T) { - logs, err := filterLogEntries(tc.logs, tc.pluginID, tc.since) - if tc.expectedErr { - if err == nil { - t.Logf("expected error, got nil") - t.Fail() - } - } else { - if err != nil { - t.Logf("expected no error, got %v", err) - t.Fail() - } - } - compareSlice(t, tc.expectedLogs, logs) - }) - } -} - -func compareSlice[S ~[]E, E comparable](t *testing.T, expected, got S) { - if len(expected) != len(got) { - t.Logf("expected len: %v, got %v", len(expected), len(got)) - t.FailNow() - } - - for i := 0; i < len(expected); i++ { - if expected[i] != got[i] { - t.Logf("expected [%d]: %v, got %v", i, expected[i], got[i]) - t.Fail() - } - } -} diff --git a/build/pluginctl/main.go b/build/pluginctl/main.go deleted file mode 100644 index 2f80af5..0000000 --- a/build/pluginctl/main.go +++ /dev/null @@ -1,184 +0,0 @@ -// main handles deployment of the plugin to a development server using the Client4 API. -package main - -import ( - "context" - "errors" - "fmt" - "log" - "net" - "os" - "time" - - "github.com/mattermost/mattermost/server/public/model" -) - -const commandTimeout = 120 * time.Second - -const helpText = ` -Usage: - pluginctl deploy - pluginctl disable - pluginctl enable - pluginctl reset -` - -func main() { - err := pluginctl() - if err != nil { - fmt.Printf("Failed: %s\n", err.Error()) - fmt.Print(helpText) - os.Exit(1) - } -} - -func pluginctl() error { - if len(os.Args) < 3 { - return errors.New("invalid number of arguments") - } - - ctx, cancel := context.WithTimeout(context.Background(), commandTimeout) - defer cancel() - - client, err := getClient(ctx) - if err != nil { - return err - } - - switch os.Args[1] { - case "deploy": - if len(os.Args) < 4 { - return errors.New("invalid number of arguments") - } - return deploy(ctx, client, os.Args[2], os.Args[3]) - case "disable": - return disablePlugin(ctx, client, os.Args[2]) - case "enable": - return enablePlugin(ctx, client, os.Args[2]) - case "reset": - return resetPlugin(ctx, client, os.Args[2]) - case "logs": - return logs(ctx, client, os.Args[2]) - case "logs-watch": - return watchLogs(context.WithoutCancel(ctx), client, os.Args[2]) // Keep watching forever - default: - return errors.New("invalid second argument") - } -} - -func getClient(ctx context.Context) (*model.Client4, error) { - socketPath := os.Getenv("MM_LOCALSOCKETPATH") - if socketPath == "" { - socketPath = model.LocalModeSocketPath - } - - client, connected := getUnixClient(socketPath) - if connected { - log.Printf("Connecting using local mode over %s", socketPath) - return client, nil - } - - if os.Getenv("MM_LOCALSOCKETPATH") != "" { - log.Printf("No socket found at %s for local mode deployment. Attempting to authenticate with credentials.", socketPath) - } - - siteURL := os.Getenv("MM_SERVICESETTINGS_SITEURL") - adminToken := os.Getenv("MM_ADMIN_TOKEN") - adminUsername := os.Getenv("MM_ADMIN_USERNAME") - adminPassword := os.Getenv("MM_ADMIN_PASSWORD") - - if siteURL == "" { - return nil, errors.New("MM_SERVICESETTINGS_SITEURL is not set") - } - - client = model.NewAPIv4Client(siteURL) - - if adminToken != "" { - log.Printf("Authenticating using token against %s.", siteURL) - client.SetToken(adminToken) - return client, nil - } - - if adminUsername != "" && adminPassword != "" { - client := model.NewAPIv4Client(siteURL) - log.Printf("Authenticating as %s against %s.", adminUsername, siteURL) - _, _, err := client.Login(ctx, adminUsername, adminPassword) - if err != nil { - return nil, fmt.Errorf("failed to login as %s: %w", adminUsername, err) - } - - return client, nil - } - - return nil, errors.New("one of MM_ADMIN_TOKEN or MM_ADMIN_USERNAME/MM_ADMIN_PASSWORD must be defined") -} - -func getUnixClient(socketPath string) (*model.Client4, bool) { - _, err := net.Dial("unix", socketPath) - if err != nil { - return nil, false - } - - return model.NewAPIv4SocketClient(socketPath), true -} - -// deploy attempts to upload and enable a plugin via the Client4 API. -// It will fail if plugin uploads are disabled. -func deploy(ctx context.Context, client *model.Client4, pluginID, bundlePath string) error { - pluginBundle, err := os.Open(bundlePath) - if err != nil { - return fmt.Errorf("failed to open %s: %w", bundlePath, err) - } - defer pluginBundle.Close() - - log.Print("Uploading plugin via API.") - _, _, err = client.UploadPluginForced(ctx, pluginBundle) - if err != nil { - return fmt.Errorf("failed to upload plugin bundle: %s", err.Error()) - } - - log.Print("Enabling plugin.") - _, err = client.EnablePlugin(ctx, pluginID) - if err != nil { - return fmt.Errorf("failed to enable plugin: %s", err.Error()) - } - - return nil -} - -// disablePlugin attempts to disable the plugin via the Client4 API. -func disablePlugin(ctx context.Context, client *model.Client4, pluginID string) error { - log.Print("Disabling plugin.") - _, err := client.DisablePlugin(ctx, pluginID) - if err != nil { - return fmt.Errorf("failed to disable plugin: %w", err) - } - - return nil -} - -// enablePlugin attempts to enable the plugin via the Client4 API. -func enablePlugin(ctx context.Context, client *model.Client4, pluginID string) error { - log.Print("Enabling plugin.") - _, err := client.EnablePlugin(ctx, pluginID) - if err != nil { - return fmt.Errorf("failed to enable plugin: %w", err) - } - - return nil -} - -// resetPlugin attempts to reset the plugin via the Client4 API. -func resetPlugin(ctx context.Context, client *model.Client4, pluginID string) error { - err := disablePlugin(ctx, client, pluginID) - if err != nil { - return err - } - - err = enablePlugin(ctx, client, pluginID) - if err != nil { - return err - } - - return nil -}