Add enable/disable/reset commands

This commit is contained in:
Felipe M 2025-07-09 14:01:23 +02:00
parent fd6e4a4513
commit 1ea8f2b38a
No known key found for this signature in database
GPG key ID: 52E5D65FCF99808A
11 changed files with 221 additions and 163 deletions

View file

@ -22,18 +22,22 @@ pluginctl/
#### 1. **Separation of Concerns**
- **CLI Framework**: `cmd/pluginctl/main.go` handles argument parsing, command routing, and error handling
- **Command Implementation**: Each command gets its own file (e.g., `info.go`, `build.go`, `deploy.go`)
- **Command Implementation**: Each command gets its own file (e.g., `info.go`, `build.go`, `deploy.go`) **IN THE ROOT FOLDER, NOT IN cmd/pluginctl/**
- **Utility Functions**: Common plugin operations in `plugin.go`
**CRITICAL ARCHITECTURE RULE**: ALL COMMAND LOGIC MUST BE IN SEPARATE FILES IN THE ROOT FOLDER. The cmd/pluginctl/main.go file should ONLY contain CLI framework code (argument parsing, command routing, wrapper functions). Never put command implementation logic directly in main.go.
#### 2. **Plugin Manifest Handling**
- **Always use official Mattermost types**: Import `github.com/mattermost/mattermost/server/public/model` and use `model.Manifest`
- **Validation**: Always validate plugin.json existence and format before operations
- **Path handling**: Support both current directory and custom path operations
#### 3. **Command Structure**
- **Main command router**: Add new commands to the `runCommand()` function in `main.go`
- **Command functions**: Name pattern: `run[Command]Command(args []string) error`
- **Main command router**: Add new commands to the `runCommand()` function in `cmd/pluginctl/main.go`
- **Command functions**: Name pattern: `run[Command]Command(args []string, pluginPath string) error`
- **Error handling**: Return descriptive errors, let main.go handle exit codes
- **Command implementation**: Each command's logic goes in a separate file in the root folder (e.g., `enable.go`, `disable.go`, `reset.go`)
- **Command wrapper functions**: The main.go file contains simple wrapper functions that call the actual command implementations
#### 4. **Code Organization**
- **No inline implementations**: Keep command logic in separate files

102
client.go Normal file
View file

@ -0,0 +1,102 @@
package pluginctl
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"time"
"github.com/mattermost/mattermost/server/public/model"
)
const commandTimeout = 120 * time.Second
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
}
// runPluginCommand executes a plugin command using the plugin from the current folder.
func runPluginCommand(
_ []string,
pluginPath string,
action func(context.Context, *model.Client4, string) error,
) error {
// Load plugin manifest to get the plugin ID
manifest, err := LoadPluginManifestFromPath(pluginPath)
if err != nil {
return fmt.Errorf("failed to load plugin manifest: %w", err)
}
pluginID := manifest.Id
if pluginID == "" {
return errors.New("plugin ID not found in manifest")
}
ctx, cancel := context.WithTimeout(context.Background(), commandTimeout)
defer cancel()
client, err := getClient(ctx)
if err != nil {
return err
}
return action(ctx, client, pluginID)
}

View file

@ -1,14 +1,11 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"runtime/debug"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/pluginctl"
)
const (
@ -34,7 +31,7 @@ func main() {
commandArgs := args[1:]
// Determine plugin path from flag, environment variable, or current directory
effectivePluginPath := getEffectivePluginPath(pluginPath)
effectivePluginPath := pluginctl.GetEffectivePluginPath(pluginPath)
if err := runCommand(command, commandArgs, effectivePluginPath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@ -46,6 +43,12 @@ func runCommand(command string, args []string, pluginPath string) error {
switch command {
case "info":
return runInfoCommand(args, pluginPath)
case "enable":
return runEnableCommand(args, pluginPath)
case "disable":
return runDisableCommand(args, pluginPath)
case "reset":
return runResetCommand(args, pluginPath)
case "help":
showUsage()
@ -58,160 +61,25 @@ func runCommand(command string, args []string, pluginPath string) error {
}
func runInfoCommand(args []string, pluginPath string) error {
// Convert to absolute path
absPath, err := filepath.Abs(pluginPath)
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
return pluginctl.RunInfoCommand(args, pluginPath)
}
return infoCommandWithPath(absPath)
}
func runVersionCommand(args []string) error {
version := getVersion()
func runVersionCommand(_ []string) error {
version := pluginctl.GetVersion()
fmt.Printf("pluginctl version %s\n", version)
return nil
}
// getVersion returns the version information from build info.
func getVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
func runEnableCommand(args []string, pluginPath string) error {
return pluginctl.RunEnableCommand(args, pluginPath)
}
// First try to get version from main module
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
func runDisableCommand(args []string, pluginPath string) error {
return pluginctl.RunDisableCommand(args, pluginPath)
}
// Look for version in build settings (set by goreleaser)
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
// Return short commit hash if no version tag
if len(setting.Value) >= 7 {
return setting.Value[:7]
}
return setting.Value
}
}
return "dev"
}
// getEffectivePluginPath determines the plugin path from flag, environment variable, or current directory.
func getEffectivePluginPath(flagPath string) string {
// Priority: 1. Command line flag, 2. Environment variable, 3. Current directory
if flagPath != "" {
return flagPath
}
if envPath := os.Getenv(EnvPluginPath); envPath != "" {
return envPath
}
// Default to current directory
cwd, err := os.Getwd()
if err != nil {
return "."
}
return cwd
}
func infoCommandWithPath(path string) error {
manifest, err := loadPluginManifestFromPath(path)
if err != nil {
return fmt.Errorf("failed to load plugin manifest from %s: %w", path, err)
}
return printPluginInfo(manifest)
}
func loadPluginManifestFromPath(dir string) (*model.Manifest, error) {
manifestPath := filepath.Join(dir, "plugin.json")
if _, err := os.Stat(manifestPath); os.IsNotExist(err) {
return nil, fmt.Errorf("plugin.json not found in directory %s", dir)
}
data, err := os.ReadFile(manifestPath)
if err != nil {
return nil, fmt.Errorf("failed to read plugin.json: %w", err)
}
var manifest model.Manifest
if err := json.Unmarshal(data, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse plugin.json: %w", err)
}
return &manifest, nil
}
func printPluginInfo(manifest *model.Manifest) error {
fmt.Printf("Plugin Information:\n")
fmt.Printf("==================\n\n")
fmt.Printf("ID: %s\n", manifest.Id)
fmt.Printf("Name: %s\n", manifest.Name)
fmt.Printf("Version: %s\n", manifest.Version)
if manifest.MinServerVersion != "" {
fmt.Printf("Min MM Version: %s\n", manifest.MinServerVersion)
} else {
fmt.Printf("Min MM Version: Not specified\n")
}
if manifest.Description != "" {
fmt.Printf("Description: %s\n", manifest.Description)
}
fmt.Printf("\nCode Components:\n")
fmt.Printf("================\n")
if hasServerCode(manifest) {
fmt.Printf("Server Code: Yes\n")
if manifest.Server != nil && len(manifest.Server.Executables) > 0 {
fmt.Printf(" Executables: ")
first := true
for platform := range manifest.Server.Executables {
if !first {
fmt.Printf(", ")
}
fmt.Printf("%s", platform)
first = false
}
fmt.Printf("\n")
}
} else {
fmt.Printf("Server Code: No\n")
}
if hasWebappCode(manifest) {
fmt.Printf("Webapp Code: Yes\n")
if manifest.Webapp != nil && manifest.Webapp.BundlePath != "" {
fmt.Printf(" Bundle Path: %s\n", manifest.Webapp.BundlePath)
}
} else {
fmt.Printf("Webapp Code: No\n")
}
if manifest.SettingsSchema != nil {
fmt.Printf("Settings Schema: Yes\n")
} else {
fmt.Printf("Settings Schema: No\n")
}
return nil
}
func hasServerCode(manifest *model.Manifest) bool {
return manifest.Server != nil && len(manifest.Server.Executables) > 0
}
func hasWebappCode(manifest *model.Manifest) bool {
return manifest.Webapp != nil && manifest.Webapp.BundlePath != ""
func runResetCommand(args []string, pluginPath string) error {
return pluginctl.RunResetCommand(args, pluginPath)
}
func showUsage() {
@ -225,18 +93,29 @@ Global Options:
Commands:
info Display plugin information
enable Enable plugin from current directory in Mattermost server
disable Disable plugin from current directory in Mattermost server
reset Reset plugin from current directory (disable then enable)
help Show this help message
version Show version information
Examples:
pluginctl info # Show info for plugin in current directory
pluginctl --plugin-path /path/to/plugin info # Show info for plugin at specific path
pluginctl enable # Enable plugin from current directory
pluginctl disable # Disable plugin from current directory
pluginctl reset # Reset plugin from current directory (disable then enable)
export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
pluginctl info # Show info using environment variable
pluginctl version # Show version information
Environment Variables:
PLUGINCTL_PLUGIN_PATH Default plugin directory path
MM_LOCALSOCKETPATH Path to Mattermost local socket
MM_SERVICESETTINGS_SITEURL Mattermost server URL
MM_ADMIN_TOKEN Admin token for authentication
MM_ADMIN_USERNAME Admin username for authentication
MM_ADMIN_PASSWORD Admin password for authentication
For more information about Mattermost plugin development, visit:
https://developers.mattermost.com/integrate/plugins/

23
disable.go Normal file
View file

@ -0,0 +1,23 @@
package pluginctl
import (
"context"
"fmt"
"log"
"github.com/mattermost/mattermost/server/public/model"
)
func RunDisableCommand(args []string, pluginPath string) error {
return runPluginCommand(args, pluginPath, disablePlugin)
}
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
}

23
enable.go Normal file
View file

@ -0,0 +1,23 @@
package pluginctl
import (
"context"
"fmt"
"log"
"github.com/mattermost/mattermost/server/public/model"
)
func RunEnableCommand(args []string, pluginPath string) error {
return runPluginCommand(args, pluginPath, enablePlugin)
}
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
}

View file

@ -1,4 +1,4 @@
package main
package pluginctl
import (
"fmt"

View file

@ -1,4 +1,4 @@
package main
package pluginctl
import (
"bytes"

View file

@ -1,4 +1,4 @@
package main
package pluginctl
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package main
package pluginctl
import (
"encoding/json"

25
reset.go Normal file
View file

@ -0,0 +1,25 @@
package pluginctl
import (
"context"
"github.com/mattermost/mattermost/server/public/model"
)
func RunResetCommand(args []string, pluginPath string) error {
return runPluginCommand(args, pluginPath, resetPlugin)
}
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
}

View file

@ -1,4 +1,4 @@
package main
package pluginctl
import (
"fmt"
@ -7,14 +7,14 @@ import (
// RunVersionCommand implements the 'version' command functionality.
func RunVersionCommand(args []string) error {
version := getVersion()
version := GetVersion()
fmt.Printf("pluginctl version %s\n", version)
return nil
}
// getVersion returns the version information from build info.
func getVersion() string {
// GetVersion returns the version information from build info.
func GetVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
@ -29,9 +29,11 @@ func getVersion() string {
for _, setting := range info.Settings {
if setting.Key == "vcs.revision" {
// Return short commit hash if no version tag
if len(setting.Value) >= 7 {
return setting.Value[:7]
const shortHashLength = 7
if len(setting.Value) >= shortHashLength {
return setting.Value[:shortHashLength]
}
return setting.Value
}
}