Skip to main content
A ubx plugin is any executable binary that implements the plugin gRPC protocol. You can write plugins in any language that supports gRPC.

Protocol

The protocol is defined in ubx-plugin-protocol (Protocol Buffers). The service definition:
service UbxPlugin {
  rpc Compile (CompileRequest)  returns (CompileResponse);
  rpc Plan    (PlanRequest)     returns (PlanResponse);
  rpc Apply   (ApplyRequest)    returns (stream ProgressEvent);
  rpc Destroy (DestroyRequest)  returns (stream ProgressEvent);
}

Launch handshake

ubx launches the plugin binary with the environment variable UBX_PLUGIN_LAUNCH=1. The plugin must:
  1. Start a gRPC server on a random available local port
  2. Print the port number to stdout (as a plain integer, e.g. 50051\n)
  3. Serve requests until the parent process closes the connection

CompileRequest

message CompileRequest {
  string stack_dir         = 1;   // absolute path to the stack directory
  StackMeta meta           = 2;   // stack name, backend URI
  bytes ir_json            = 3;   // JSON-serialised IRProgram
  string ir_schema_version = 4;   // IR schema version for compatibility checks
}

message CompileResponse {
  string artifact_uri = 1;   // opaque URI to pass to Apply/Destroy
  string error        = 2;   // non-empty if compilation failed
}

PlanRequest / PlanResponse

message PlanRequest {
  string stack_dir         = 1;
  StackMeta meta           = 2;
  bytes ir_json            = 3;
  string ir_schema_version = 4;
  repeated string targets  = 5;   // empty = all resources
}

message PlanResponse {
  string summary = 1;   // human-readable plan summary
  string error   = 2;
}

ApplyRequest + ProgressEvent stream

message ApplyRequest {
  string artifact_uri      = 1;
  StackMeta meta           = 2;
  repeated string targets  = 3;
}

message ProgressEvent {
  oneof event {
    PhaseEvent      phase      = 1;
    ResourceEvent   resource   = 2;
    DiagnosticEvent diagnostic = 3;
    SummaryEvent    summary    = 4;
  }
}

IRProgram structure

The IR JSON contains:
{
  "stack_name":   "networking",
  "engine":       "myengine",
  "runtime":      "",
  "inputs":       [...],
  "locals":       [...],
  "resources":    [...],
  "outputs":      [...],
  "modules":      [...],
  "providers":    [...],
  "backend":      {...},
  "cross_stacks": [...]
}
Resource declarations include type_path (array of segments), props (key-value pairs with typed IR expressions), and for_clause / when information.

Minimal Go plugin skeleton

package main

import (
    "fmt"
    "net"
    "os"

    pluginv1 "github.com/ubiquex/ubx-plugin-protocol/pluginv1"
    "google.golang.org/grpc"
)

type server struct {
    pluginv1.UnimplementedUbxPluginServer
}

func (s *server) Compile(ctx context.Context, req *pluginv1.CompileRequest) (*pluginv1.CompileResponse, error) {
    // Parse req.IrJson, generate code, return artifact_uri
    return &pluginv1.CompileResponse{ArtifactUri: "/tmp/myengine-artifact"}, nil
}

// ... implement Plan, Apply, Destroy

func main() {
    if os.Getenv("UBX_PLUGIN_LAUNCH") != "1" {
        fmt.Fprintf(os.Stderr, "start via ubx, not directly\n")
        os.Exit(1)
    }

    lis, err := net.Listen("tcp", ":0")
    if err != nil {
        panic(err)
    }
    port := lis.Addr().(*net.TCPAddr).Port
    fmt.Println(port)   // tell ubx which port we're on

    s := grpc.NewServer()
    pluginv1.RegisterUbxPluginServer(s, &server{})
    s.Serve(lis)
}

Installing a custom plugin

ubx plugin install file:///path/to/my-plugin-binary
This copies the binary to ~/.ubx/plugins/<name>/ubx-plugin-<name> and records it in the lock file.

Naming convention

Plugin binaries must follow the ubx-plugin-<name> naming convention. The <name> part becomes the engine name used in workspace.xcl.

IR schema version

The ir_schema_version field in requests lets your plugin detect and reject incompatible IR versions. ubx passes the current IR schema version; your plugin should return an error if the version is unsupported.