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** #### 1. **Separation of Concerns**
- **CLI Framework**: `cmd/pluginctl/main.go` handles argument parsing, command routing, and error handling - **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` - **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** #### 2. **Plugin Manifest Handling**
- **Always use official Mattermost types**: Import `github.com/mattermost/mattermost/server/public/model` and use `model.Manifest` - **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 - **Validation**: Always validate plugin.json existence and format before operations
- **Path handling**: Support both current directory and custom path operations - **Path handling**: Support both current directory and custom path operations
#### 3. **Command Structure** #### 3. **Command Structure**
- **Main command router**: Add new commands to the `runCommand()` function in `main.go` - **Main command router**: Add new commands to the `runCommand()` function in `cmd/pluginctl/main.go`
- **Command functions**: Name pattern: `run[Command]Command(args []string) error` - **Command functions**: Name pattern: `run[Command]Command(args []string, pluginPath string) error`
- **Error handling**: Return descriptive errors, let main.go handle exit codes - **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** #### 4. **Code Organization**
- **No inline implementations**: Keep command logic in separate files - **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 package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime/debug"
"github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/pluginctl"
) )
const ( const (
@ -34,7 +31,7 @@ func main() {
commandArgs := args[1:] commandArgs := args[1:]
// Determine plugin path from flag, environment variable, or current directory // 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 { if err := runCommand(command, commandArgs, effectivePluginPath); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@ -46,6 +43,12 @@ func runCommand(command string, args []string, pluginPath string) error {
switch command { switch command {
case "info": case "info":
return runInfoCommand(args, pluginPath) return runInfoCommand(args, pluginPath)
case "enable":
return runEnableCommand(args, pluginPath)
case "disable":
return runDisableCommand(args, pluginPath)
case "reset":
return runResetCommand(args, pluginPath)
case "help": case "help":
showUsage() showUsage()
@ -58,160 +61,25 @@ func runCommand(command string, args []string, pluginPath string) error {
} }
func runInfoCommand(args []string, pluginPath string) error { func runInfoCommand(args []string, pluginPath string) error {
// Convert to absolute path return pluginctl.RunInfoCommand(args, pluginPath)
absPath, err := filepath.Abs(pluginPath)
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
} }
return infoCommandWithPath(absPath) func runVersionCommand(_ []string) error {
} version := pluginctl.GetVersion()
func runVersionCommand(args []string) error {
version := getVersion()
fmt.Printf("pluginctl version %s\n", version) fmt.Printf("pluginctl version %s\n", version)
return nil return nil
} }
func runEnableCommand(args []string, pluginPath string) error {
// getVersion returns the version information from build info. return pluginctl.RunEnableCommand(args, pluginPath)
func getVersion() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
} }
// First try to get version from main module func runDisableCommand(args []string, pluginPath string) error {
if info.Main.Version != "" && info.Main.Version != "(devel)" { return pluginctl.RunDisableCommand(args, pluginPath)
return info.Main.Version
} }
// Look for version in build settings (set by goreleaser) func runResetCommand(args []string, pluginPath string) error {
for _, setting := range info.Settings { return pluginctl.RunResetCommand(args, pluginPath)
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 showUsage() { func showUsage() {
@ -225,18 +93,29 @@ Global Options:
Commands: Commands:
info Display plugin information 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 help Show this help message
version Show version information version Show version information
Examples: Examples:
pluginctl info # Show info for plugin in current directory pluginctl info # Show info for plugin in current directory
pluginctl --plugin-path /path/to/plugin info # Show info for plugin at specific path 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 export PLUGINCTL_PLUGIN_PATH=/path/to/plugin
pluginctl info # Show info using environment variable pluginctl info # Show info using environment variable
pluginctl version # Show version information pluginctl version # Show version information
Environment Variables: Environment Variables:
PLUGINCTL_PLUGIN_PATH Default plugin directory path 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: For more information about Mattermost plugin development, visit:
https://developers.mattermost.com/integrate/plugins/ 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 ( import (
"fmt" "fmt"

View file

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

View file

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

View file

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