Creating a Marmot Plugin
This guide walks you through creating a simple HelloWorld plugin for Marmot that demonstrates the core concepts of plugin development.
Documentation Comments: The
+marmot:comments throughout the code are used by Marmot's documentation generator. Always include these comments - they provide metadata for the plugin registry and generate user-facing documentation.
1. Create the Plugin Package
Create a new package in the internal/plugin/providers directory:
mkdir -p internal/plugin/providers/helloworld
2. Implement the Source Interface
Create source.go in your plugin directory:
package helloworld
import (
"context"
"fmt"
"time"
"github.com/marmotdata/marmot/internal/core/asset"
"github.com/marmotdata/marmot/internal/core/lineage"
"github.com/marmotdata/marmot/internal/mrn"
"github.com/marmotdata/marmot/internal/plugin"
"github.com/rs/zerolog/log"
)
// +marmot:name=HelloWorld
// +marmot:description=A simple plugin that creates "hello" and "world" assets with lineage.
// +marmot:status=experimental
type Source struct {
config *Config
}
// Config for HelloWorld plugin
// +marmot:config
type Config struct {
plugin.BaseConfig `json:",inline"`
// Add a simple config option
Greeting string `json:"greeting" description:"Optional custom greeting message"`
}
// Example configuration for the plugin
// +marmot:example-config
var _ = `
greeting: "Hello, Marmot!"
tags:
- "hello"
- "example"
`
// Validate checks if the configuration is valid
func (s *Source) Validate(rawConfig plugin.RawPluginConfig) error {
config, err := plugin.UnmarshalPluginConfig[Config](rawConfig)
if err != nil {
return fmt.Errorf("unmarshaling config: %w", err)
}
s.config = config
return nil
}
// Discover creates our hello and world assets
func (s *Source) Discover(ctx context.Context, pluginConfig plugin.RawPluginConfig) (*plugin.DiscoveryResult, error) {
if err := s.Validate(pluginConfig); err != nil {
return nil, fmt.Errorf("validating config: %w", err)
}
log.Info().Msg("HelloWorld plugin starting asset discovery")
helloAsset := createHelloAsset(s.config)
worldAsset := createWorldAsset(s.config)
helloMRN := *helloAsset.MRN
worldMRN := *worldAsset.MRN
// Create lineage between assets
lineageEdge := lineage.LineageEdge{
Source: helloMRN,
Target: worldMRN,
Type: "PRODUCES",
Description: "Hello produces World",
Metadata: map[string]interface{}{"created_by": "helloworld_plugin"},
}
log.Info().
Str("hello_mrn", helloMRN).
Str("world_mrn", worldMRN).
Msg("Created lineage relationship")
return &plugin.DiscoveryResult{
Assets: []asset.Asset{helloAsset, worldAsset},
Lineage: []lineage.LineageEdge{lineageEdge},
}, nil
}
func createHelloAsset(config *Config) asset.Asset {
name := "hello"
mrnValue := mrn.New("Example", "HelloWorld", name)
description := "Hello asset created by HelloWorld plugin"
metadata := map[string]interface{}{
"type": "foo",
}
if config.Greeting != "" {
metadata["greeting"] = config.Greeting
}
return asset.Asset{
Name: &name,
MRN: &mrnValue,
Type: "Example",
Providers: []string{"HelloWorld"},
Description: &description,
Metadata: metadata,
Tags: config.Tags,
Sources: []asset.AssetSource{{
Name: "HelloWorld",
LastSyncAt: time.Now(),
Properties: metadata,
Priority: 1,
}},
}
}
func createWorldAsset(config *Config) asset.Asset {
name := "world"
mrnValue := mrn.New("Example", "HelloWorld", name)
description := "World asset created by HelloWorld plugin"
metadata := map[string]interface{}{
"type": "bar",
}
return asset.Asset{
Name: &name,
MRN: &mrnValue,
Type: "Example",
Providers: []string{"HelloWorld"},
Description: &description,
Metadata: metadata,
Tags: config.Tags,
Sources: []asset.AssetSource{{
Name: "HelloWorld",
LastSyncAt: time.Now(),
Properties: metadata,
Priority: 1,
}},
}
}
3. Define Metadata Types
Create a simple metadata.go file. This defines what metadata is available and exported from your plugin.
package helloworld
// HelloWorldFields represents example metadata fields
// +marmot:metadata
type HelloWorldFields struct {
Type string `json:"type" metadata:"type" description:"The type of asset created"`
Greeting string `json:"greeting" metadata:"greeting" description:"Optional custom greeting message"`
}
4. Register the Plugin
Create an init() function at the end of source.go to auto-register your plugin:
func init() {
meta := plugin.PluginMeta{
ID: "helloworld",
Name: "HelloWorld",
Description: "A simple plugin that creates hello and world assets with lineage",
Icon: "wave",
Category: "example",
ConfigSpec: plugin.GenerateConfigSpec(Config{}),
}
if err := plugin.GetRegistry().Register(meta, &Source{}); err != nil {
log.Fatal().Err(err).Msg("Failed to register HelloWorld plugin")
}
}
The init() function:
- Runs automatically when the package is imported
- Defines plugin metadata (ID, name, description, icon, category)
- Auto-generates the configuration spec from your Config struct
- Registers the plugin with the global registry
Important: Your plugin must be imported in internal/api/v1/server.go to trigger registration:
import (
_ "github.com/marmotdata/marmot/internal/plugin/providers/helloworld"
)
5. Test the Plugin
Create a test configuration file hello.yaml:
runs:
- helloworld:
greeting: "Hello from my first plugin!"
tags:
- "example"
- "hello"
Run the ingestion:
go run cmd/main.go ingest -c hello.yaml -H http://localhost:8080 -k your-api-key
After running, you should see two new assets in your catalog:
- An asset named "hello"
- An asset named "world"
- A lineage relationship showing "hello" produces "world"
Configuration Spec Generation
The plugin.GenerateConfigSpec() function automatically generates a UI-ready configuration schema from your Config struct using struct tags:
type Config struct {
plugin.BaseConfig `json:",inline"`
// Text input
Greeting string `json:"greeting" description:"Custom greeting message"`
// Dropdown/select (using oneof validation)
Mode string `json:"mode" description:"Operation mode" validate:"oneof=simple advanced"`
// Sensitive field (password input)
APIKey string `json:"api_key" description:"API authentication key" sensitive:"true"`
// Number input with validation
Timeout int `json:"timeout" description:"Request timeout in seconds" validate:"min=1,max=300" default:"30"`
// Required field
Host string `json:"host" description:"Server hostname" validate:"required"`
// Nested object
TLS *TLSConfig `json:"tls,omitempty" description:"TLS configuration"`
}
Supported tags:
json: Field name in JSONdescription: Help text shown in UIvalidate: Validation rules (required, min, max, oneof, etc.)sensitive: Marks field as password/secretdefault: Default value
Plugin Interface
All plugins must implement the plugin.Source interface:
type Source interface {
Validate(rawConfig RawPluginConfig) error
Discover(ctx context.Context, config RawPluginConfig) (*DiscoveryResult, error)
}
Validate: Unmarshals and validates configuration before discovery runs Discover: Performs the actual asset discovery and returns assets + lineage