Skip to content

Plugin Development

Damir Mukimov edited this page Dec 15, 2025 · 4 revisions

Plugin Development Guide

Create custom plugins to extend ZoneKit functionality


Overview

The plugin system allows you to extend the functionality of ZoneKit with custom integrations for email providers, DNS services, or any other use case.


Architecture

Plugin Interface

All plugins must implement the plugin.Plugin interface:

type Plugin interface {
    Name() string
    Description() string
    Version() string
    Commands() []Command
}

Plugin Commands

Each plugin can provide multiple commands. Commands receive a Context that provides:

Component Description
Domain Domain name being operated on
DNS DNS service for managing records
Args Additional command arguments
Flags Command flags (dry-run, replace, confirm, etc.)
Output Output writer for displaying results

Creating a Plugin

Step 1: Create Plugin Package

Create a new directory for your plugin:

mkdir -p pkg/plugin/myemail

Step 2: Implement Plugin Interface

Create pkg/plugin/myemail/myemail.go:

package myemail

import (
    "fmt"
    "zonekit-manager/pkg/dns"
    "zonekit-manager/pkg/plugin"
)

const (
    pluginName        = "myemail"
    pluginVersion     = "1.0.0"
    pluginDescription = "My Email Provider integration"
)

type MyEmailPlugin struct{}

func New() *MyEmailPlugin {
    return &MyEmailPlugin{}
}

func (p *MyEmailPlugin) Name() string {
    return pluginName
}

func (p *MyEmailPlugin) Description() string {
    return pluginDescription
}

func (p *MyEmailPlugin) Version() string {
    return pluginVersion
}

func (p *MyEmailPlugin) Commands() []plugin.Command {
    return []plugin.Command{
        {
            Name:        "setup",
            Description: "Set up email DNS records",
            LongDescription: `Set up all necessary DNS records for My Email Provider.
This will add:
- MX records for mail routing
- SPF record for sender authentication
- DKIM records for email signing
- DMARC record for email policy`,
            Execute: func(ctx *plugin.Context) error {
                return p.setup(ctx)
            },
        },
        {
            Name:        "verify",
            Description: "Verify email DNS records",
            Execute: func(ctx *plugin.Context) error {
                return p.verify(ctx)
            },
        },
    }
}

func (p *MyEmailPlugin) setup(ctx *plugin.Context) error {
    dryRun, _ := ctx.Flags["dry-run"].(bool)
    replace, _ := ctx.Flags["replace"].(bool)

    // Get existing records if not replacing
    var existingRecords []dns.Record
    var err error
    if !replace {
        existingRecords, err = ctx.DNS.GetRecords(ctx.Domain)
        if err != nil {
            return fmt.Errorf("failed to get existing records: %w", err)
        }
    }

    // Generate DNS records for your email provider
    records := []dns.Record{
        {
            HostName:   "@",
            RecordType: dns.RecordTypeMX,
            Address:    "mail.example.com.",
            TTL:        dns.DefaultTTL,
            MXPref:     10,
        },
        {
            HostName:   "@",
            RecordType: dns.RecordTypeTXT,
            Address:    "v=spf1 include:_spf.example.com -all",
            TTL:        dns.DefaultTTL,
        },
    }

    ctx.Output.Printf("Setting up email DNS records for %s\n", ctx.Domain)

    if dryRun {
        ctx.Output.Println("DRY RUN MODE - No changes will be made")
        ctx.Output.Println("Records to be added:")
        for _, record := range records {
            ctx.Output.Printf("  %s %s → %s\n", record.HostName, record.RecordType, record.Address)
        }
        return nil
    }

    // Apply changes
    var allRecords []dns.Record
    if replace {
        allRecords = records
    } else {
        allRecords = existingRecords
        allRecords = append(allRecords, records...)
    }

    err = ctx.DNS.SetRecords(ctx.Domain, allRecords)
    if err != nil {
        return fmt.Errorf("failed to set DNS records: %w", err)
    }

    ctx.Output.Printf("Successfully set up email DNS records for %s\n", ctx.Domain)
    return nil
}

func (p *MyEmailPlugin) verify(ctx *plugin.Context) error {
    records, err := ctx.DNS.GetRecords(ctx.Domain)
    if err != nil {
        return fmt.Errorf("failed to get DNS records: %w", err)
    }

    ctx.Output.Printf("Verifying email setup for %s\n", ctx.Domain)

    // Check for required records
    hasMX := false
    hasSPF := false

    for _, record := range records {
        if record.RecordType == dns.RecordTypeMX && record.HostName == "@" {
            hasMX = true
        }
        if record.RecordType == dns.RecordTypeTXT &&
           record.HostName == "@" &&
           strings.Contains(record.Address, "v=spf1") {
            hasSPF = true
        }
    }

    if hasMX {
        ctx.Output.Println("MX record found")
    } else {
        ctx.Output.Println("MX record missing")
    }

    if hasSPF {
        ctx.Output.Println("SPF record found")
    } else {
        ctx.Output.Println("SPF record missing")
    }

    return nil
}

Step 3: Register Plugin

In cmd/root.go, add your plugin registration:

import (
    "zonekit-manager/pkg/plugin/myemail"
)

func initPlugins() {
    // Existing plugins
    plugin.Register(migadu.New())

    // Your plugin
    plugin.Register(myemail.New())
}

Step 4: Use Your Plugin

# List plugins
zonekit plugin list

# Get plugin info
zonekit plugin info myemail

# Setup email
zonekit plugin myemail setup example.com

# Verify setup
zonekit plugin myemail verify example.com

# Dry run
zonekit plugin myemail setup example.com --dry-run

Plugin Context

The Context provided to plugin commands includes:

Field Type Description
Domain string The domain name being operated on
DNS Service DNS service instance for managing records
Args []string Additional command arguments
Flags map[string]interface{} Command flags (dry-run, replace, confirm, etc.)
Output OutputWriter Output writer for displaying messages

Best Practices

  1. Validate Input: Always validate domain names and other inputs
  2. Support Dry Run: Implement --dry-run flag support
  3. Conflict Detection: Check for existing records before modifying
  4. Clear Output: Use ctx.Output for all user-facing messages
  5. Error Handling: Return descriptive errors with context
  6. Documentation: Provide clear descriptions for all commands
  7. Use Constants: Use DNS constants from dns package (e.g., dns.RecordTypeMX, dns.DefaultTTL)

Extending the System

To add new plugin types or capabilities:

  1. Extend the Plugin interface if needed
  2. Add new fields to Context for additional capabilities
  3. Update the plugin registry to support new features
  4. Document the changes in the wiki

Reference


Contributing Plugins

If you create a useful plugin, consider contributing it back to the project:

  1. Fork the repository
  2. Create your plugin in pkg/plugin/<plugin-name>/
  3. Register it in cmd/root.go
  4. Add tests
  5. Submit a pull request

See Contributing Guide for more details.

Clone this wiki locally