Add enable/disable/reset commands
This commit is contained in:
parent
fd6e4a4513
commit
1ea8f2b38a
11 changed files with 221 additions and 163 deletions
10
CLAUDE.md
10
CLAUDE.md
|
@ -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
102
client.go
Normal 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)
|
||||
}
|
|
@ -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 infoCommandWithPath(absPath)
|
||||
return pluginctl.RunInfoCommand(args, pluginPath)
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
// First try to get version from main module
|
||||
if info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||
return info.Main.Version
|
||||
}
|
||||
|
||||
// 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"
|
||||
func runEnableCommand(args []string, pluginPath string) error {
|
||||
return pluginctl.RunEnableCommand(args, pluginPath)
|
||||
}
|
||||
|
||||
// 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 runDisableCommand(args []string, pluginPath string) error {
|
||||
return pluginctl.RunDisableCommand(args, pluginPath)
|
||||
}
|
||||
|
||||
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
23
disable.go
Normal 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
23
enable.go
Normal 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
|
||||
}
|
2
info.go
2
info.go
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package pluginctl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package pluginctl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package pluginctl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package pluginctl
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
25
reset.go
Normal file
25
reset.go
Normal 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
|
||||
}
|
14
version.go
14
version.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue