README
¶
glua - Go to Lua Bridge with type safety
A comprehensive toolkit for embedding Lua in Go applications with full type safety and IDE autocomplete support. Designed specifically for Kubernetes API types but works with any Go structs.
Table of Contents
- Installation
- Quick Start
- Usage Guide
- Creating Custom Lua Modules
- IDE Setup
- API Reference
- Features
- Use Cases
- Performance
- Testing
- Contributing
Installation
Using in Your Project
Add glua to your go.mod:
go get github.com/thomas-maurice/glua@latest
Or manually add to go.mod and run go mod tidy:
require github.com/thomas-maurice/glua v0.0.4 // or latest version
Installing Lua Stubs for IDE Autocomplete
Download the latest Lua stubs for IDE autocomplete support:
# Download and extract to your project
VERSION=v0.0.4 # Replace with the latest version
curl -sL https://github.com/thomas-maurice/glua/releases/download/${VERSION}/glua-stubs_${VERSION}.tar.gz | tar xz
# This extracts to library/*.gen.lua
# Configure your IDE to recognize the library/ directory
VS Code Setup (with Lua extension):
Create or update .vscode/settings.json:
{
"Lua.workspace.library": ["library"]
}
Now you'll get autocomplete for all glua modules in your Lua scripts!
Installing stubgen Binary (Optional)
If you want to generate stubs for your own modules:
# Linux/macOS
VERSION=v0.0.4 # Replace with the latest version
curl -sL https://github.com/thomas-maurice/glua/releases/download/${VERSION}/stubgen_${VERSION}_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/').tar.gz | tar xz
sudo mv stubgen /usr/local/bin/
# Verify installation
stubgen --help
Cloning the Repository
To work on glua or run the examples:
git clone https://github.com/thomas-maurice/glua.git
cd glua
go mod download
Requirements
- Go 1.24 or later
- For Kubernetes support:
k8s.io/client-go,k8s.io/api,k8s.io/apimachinery - For integration tests:
kindandkubectl
Quick Start
Here's a complete example showing the main features:
package main
import (
"github.com/thomas-maurice/glua/pkg/glua"
"github.com/thomas-maurice/glua/pkg/modules/kubernetes"
lua "github.com/yuin/gopher-lua"
corev1 "k8s.io/api/core/v1"
)
func main() {
L := lua.NewState()
defer L.Close()
// 1. Load Kubernetes module
L.PreloadModule("kubernetes", kubernetes.Loader)
// 2. Create translator for Go ↔ Lua conversion
translator := glua.NewTranslator()
// 3. Convert Go struct to Lua table
pod := &corev1.Pod{ /* ... */ }
luaTable, _ := translator.ToLua(L, pod)
L.SetGlobal("myPod", luaTable)
// 4. Execute Lua script that uses kubernetes module
L.DoString(`
local k8s = require("kubernetes")
local pod = myPod
-- Parse Kubernetes quantities
local memBytes = k8s.parse_memory(pod.spec.containers[1].resources.limits["memory"])
local cpuMillis = k8s.parse_cpu(pod.spec.containers[1].resources.limits["cpu"])
local timestamp = k8s.parse_time(pod.metadata.creationTimestamp)
print(string.format("Memory: %.2f MB", memBytes / (1024 * 1024)))
print(string.format("CPU: %d millicores", cpuMillis))
-- Modify and return
modifiedPod = pod
`)
// 5. Convert modified Lua table back to Go
modifiedTable := L.GetGlobal("modifiedPod")
var reconstructedPod corev1.Pod
translator.FromLua(L, modifiedTable, &reconstructedPod)
// Round-trip complete! Data integrity preserved
}
Usage Guide
Building
The project includes a Makefile for common tasks:
# Run all tests and build binaries (default)
make
# Run tests only
make test
# Build binaries only
make build
# Clean build artifacts
make clean
# Show help
make help
Built binaries:
bin/stubgen- Generates Lua LSP stubs for IDE autocompletebin/example- Complete working example with all features
Go to Lua Conversion
Convert any Go struct to a Lua table with full type preservation:
translator := glua.NewTranslator()
// Works with any Go type
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "my-pod",
CreationTimestamp: metav1.Time{Time: time.Now()},
Labels: map[string]string{"app": "demo"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "nginx",
Image: "nginx:latest",
}},
},
}
luaTable, err := translator.ToLua(L, pod)
if err != nil {
panic(err)
}
L.SetGlobal("myPod", luaTable)
Features:
- Preserves timestamps (RFC3339 strings)
- Preserves resource quantities (CPU/memory strings like "100m", "256Mi")
- Handles nested structures, arrays, and maps
- Converts via JSON for robustness
Lua to Go Conversion
Convert Lua tables back to Go structs with type safety:
// After Lua modifies the table
modifiedTable := L.GetGlobal("modifiedPod")
var reconstructedPod corev1.Pod
err := translator.FromLua(L, modifiedTable, &reconstructedPod)
if err != nil {
panic(err)
}
// Full round-trip integrity - data is identical to original
Features:
- Full round-trip integrity (original == reconstructed)
- Automatic type coercion
- Preserves all complex Kubernetes types
- Handles any
LValue(LTable, LString, LNumber, etc.)
Type Registry and LSP Stub Generation
Generate Lua LSP annotations for IDE autocomplete:
registry := glua.NewTypeRegistry()
// Register your types
registry.Register(&corev1.Pod{})
registry.Register(&corev1.Service{})
registry.Register(&corev1.ConfigMap{})
// Process and generate stubs
registry.Process()
stubs, _ := registry.GenerateStubs()
// Write to file for IDE consumption
os.WriteFile("annotations.gen.lua", []byte(stubs), 0644)
This generates complete type definitions:
---@meta
---@class corev1.Pod
---@field kind string
---@field apiVersion string
---@field metadata v1.ObjectMeta
---@field spec corev1.PodSpec
---@field status corev1.PodStatus
---@class corev1.PodSpec
---@field containers corev1.Container[]
---@field volumes corev1.Volume[]
---@field nodeName string
-- ... all fields with correct types
---@class corev1.Container
---@field name string
---@field image string
---@field resources corev1.ResourceRequirements
-- ... complete definitions
Now in your Lua scripts, you get full autocomplete:
---@type corev1.Pod
local pod = myPod
-- IDE shows autocomplete for all fields!
print(pod.metadata.name)
print(pod.spec.containers[1].image)
Kubernetes Module
The built-in Kubernetes module provides utility functions for parsing K8s resource quantities and timestamps.
Load in Go:
L.PreloadModule("kubernetes", kubernetes.Loader)
Use in Lua:
local k8s = require("kubernetes")
-- Parse memory quantities (returns bytes)
local memBytes, err = k8s.parse_memory("256Mi") -- 268435456
local memBytes2 = k8s.parse_memory("1Gi") -- 1073741824
-- Parse CPU quantities (returns millicores)
local cpuMillis, err = k8s.parse_cpu("100m") -- 100
local cpuMillis2 = k8s.parse_cpu("1.5") -- 1500
-- Parse timestamps (returns Unix timestamp)
local timestamp, err = k8s.parse_time("2025-10-03T16:39:00Z") -- 1759509540
-- Format timestamps (Unix timestamp → RFC3339 string)
local timeStr, err = k8s.format_time(1759509540) -- "2025-10-03T16:39:00Z"
-- All functions return (value, error) tuple
local bytes, err = k8s.parse_memory("invalid")
if err then
print("Parse error: " .. err)
end
Example use case - process pod resource limits/requests:
local k8s = require("kubernetes")
local pod = myPod
for i, container in ipairs(pod.spec.containers) do
print("Container: " .. container.name)
-- Parse memory limit
if container.resources.limits["memory"] then
local memBytes = k8s.parse_memory(container.resources.limits["memory"])
print(string.format(" Memory limit: %.2f MB", memBytes / (1024 * 1024)))
end
-- Parse CPU limit
if container.resources.limits["cpu"] then
local cpuMillis = k8s.parse_cpu(container.resources.limits["cpu"])
print(string.format(" CPU limit: %d millicores", cpuMillis))
end
end
K8s Client Module
The k8sclient module provides a dynamic Kubernetes client for Lua, allowing full CRUD operations on any Kubernetes resource directly from Lua scripts.
Load in Go:
import "github.com/thomas-maurice/glua/pkg/modules/k8sclient"
config, _ := clientcmd.BuildConfigFromFlags("", kubeconfig)
L.PreloadModule("k8sclient", k8sclient.Loader(config))
Use in Lua:
local client = require("k8sclient")
-- Define GVK (Group/Version/Kind)
local pod_gvk = {group = "", version = "v1", kind = "Pod"}
-- Create a Pod
local pod = {
apiVersion = "v1",
kind = "Pod",
metadata = {name = "nginx", namespace = "default"},
spec = {
containers = {{
name = "nginx",
image = "nginx:alpine"
}}
}
}
local created, err = client.create(pod)
-- Get a resource
local fetched, err = client.get(pod_gvk, "default", "nginx")
-- Update a resource
fetched.metadata.labels = {app = "web"}
local updated, err = client.update(fetched)
-- List resources
local pods, err = client.list(pod_gvk, "default")
for i, pod in ipairs(pods) do
print(pod.metadata.name)
end
-- Delete a resource
local err = client.delete(pod_gvk, "default", "nginx")
Complete Example: See example/k8sclient/ for a full working example with nginx Pod, ConfigMaps, and Kind cluster integration.
Run the example:
make test-k8sclient # Runs with temporary Kind cluster
Error Handling
Proper error handling throughout:
// Go side
luaTable, err := translator.ToLua(L, pod)
if err != nil {
log.Fatalf("Conversion failed: %v", err)
}
err = translator.FromLua(L, luaTable, &reconstructedPod)
if err != nil {
log.Fatalf("Reconstruction failed: %v", err)
}
-- Lua side
local k8s = require("kubernetes")
local bytes, err = k8s.parse_memory("256Mi")
if err then
print("Error: " .. err)
return
end
print("Parsed successfully: " .. bytes)
Round-Trip Integrity
glua ensures perfect round-trip conversion:
// Original Go struct
originalPod := sample.GetPod()
// Convert to Lua
luaTable, _ := translator.ToLua(L, originalPod)
L.SetGlobal("pod", luaTable)
// Process in Lua (even if unchanged)
L.DoString("modifiedPod = pod")
// Convert back to Go
modifiedTable := L.GetGlobal("modifiedPod")
var reconstructedPod corev1.Pod
translator.FromLua(L, modifiedTable, &reconstructedPod)
// Verify integrity
originalJSON, _ := json.Marshal(originalPod)
reconstructedJSON, _ := json.Marshal(reconstructedPod)
if string(originalJSON) == string(reconstructedJSON) {
fmt.Println("Perfect round-trip!")
}
This works for:
- Timestamps (RFC3339 strings preserved)
- Resource quantities ("100m", "256Mi" preserved as strings)
- Maps and labels
- Nested arrays and structures
- Complex Kubernetes objects
Creating Custom Lua Modules
Step 1: Create Module
You can create two types of Lua modules: function-based modules (simple) and UserData-based modules (for stateful objects).
Option A: Simple Function-Based Module
Create pkg/modules/mymodule/mymodule.go:
package mymodule
import (
lua "github.com/yuin/gopher-lua"
)
// Loader: creates the mymodule Lua module
//
// @luamodule mymodule
func Loader(L *lua.LState) int {
mod := L.SetFuncs(L.NewTable(), exports)
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"greet": greet,
"add": add,
}
// greet: returns a personalized greeting
//
// @luafunc greet
// @luaparam name string The name to greet
// @luareturn string The greeting message
func greet(L *lua.LState) int {
name := L.CheckString(1)
L.Push(lua.LString("Hello, " + name + "!"))
return 1
}
// add: adds two numbers
//
// @luafunc add
// @luaparam a number First number
// @luaparam b number Second number
// @luareturn number Sum of a and b
// @luareturn string|nil Error message if any
func add(L *lua.LState) int {
a := L.CheckNumber(1)
b := L.CheckNumber(2)
L.Push(lua.LNumber(a + b))
L.Push(lua.LNil)
return 2
}
Option B: UserData-Based Module (Stateful Objects)
For modules that need to maintain state or provide object-oriented APIs, use UserData. This example shows how to create a Logger object with methods (like the built-in log module):
Create pkg/modules/counter/counter.go:
package counter
import (
lua "github.com/yuin/gopher-lua"
)
const counterTypeName = "counter.Counter"
// Counter: a simple counter object
type Counter struct {
value int
}
// Loader: creates the counter Lua module
//
// @luamodule counter
func Loader(L *lua.LState) int {
// Register the Counter type with methods
registerCounterType(L)
// Create module table with functions
mod := L.SetFuncs(L.NewTable(), exports)
L.Push(mod)
return 1
}
var exports = map[string]lua.LGFunction{
"new": newCounter,
}
// registerCounterType: registers the Counter UserData type with its metatable
func registerCounterType(L *lua.LState) {
mt := L.NewTypeMetatable(counterTypeName)
L.SetField(mt, "__index", L.SetFuncs(L.NewTable(), counterMethods))
}
// counterMethods: map of Counter object methods
var counterMethods = map[string]lua.LGFunction{
"increment": counterIncrement,
"decrement": counterDecrement,
"get": counterGet,
"reset": counterReset,
}
// wrapCounter: wraps a Counter in UserData for use in Lua
func wrapCounter(L *lua.LState, c *Counter) *lua.LUserData {
ud := L.NewUserData()
ud.Value = c
L.SetMetatable(ud, L.GetTypeMetatable(counterTypeName))
return ud
}
// checkCounter: extracts Counter from UserData
func checkCounter(L *lua.LState, n int) *Counter {
ud := L.CheckUserData(n)
if c, ok := ud.Value.(*Counter); ok {
return c
}
L.ArgError(n, "Counter expected")
return nil
}
// newCounter: creates a new Counter object
//
// @luafunc new
// @luaparam initialValue number Optional initial value (default: 0)
// @luareturn counter.Counter counter.Counter A new counter object
func newCounter(L *lua.LState) int {
initialValue := 0
if L.GetTop() >= 1 {
initialValue = int(L.CheckNumber(1))
}
counter := &Counter{value: initialValue}
ud := wrapCounter(L, counter)
L.Push(ud)
return 1
}
// counterIncrement: increments the counter
//
// @luamethod counter.Counter increment
// @luaparam self counter.Counter The counter object
// @luaparam amount number Optional amount to add (default: 1)
func counterIncrement(L *lua.LState) int {
c := checkCounter(L, 1)
amount := 1
if L.GetTop() >= 2 {
amount = int(L.CheckNumber(2))
}
c.value += amount
return 0
}
// counterDecrement: decrements the counter
//
// @luamethod counter.Counter decrement
// @luaparam self counter.Counter The counter object
// @luaparam amount number Optional amount to subtract (default: 1)
func counterDecrement(L *lua.LState) int {
c := checkCounter(L, 1)
amount := 1
if L.GetTop() >= 2 {
amount = int(L.CheckNumber(2))
}
c.value -= amount
return 0
}
// counterGet: gets the current counter value
//
// @luamethod counter.Counter get
// @luaparam self counter.Counter The counter object
// @luareturn number The current counter value
func counterGet(L *lua.LState) int {
c := checkCounter(L, 1)
L.Push(lua.LNumber(c.value))
return 1
}
// counterReset: resets the counter to zero
//
// @luamethod counter.Counter reset
// @luaparam self counter.Counter The counter object
func counterReset(L *lua.LState) int {
c := checkCounter(L, 1)
c.value = 0
return 0
}
Usage in Lua:
local counter = require("counter")
-- Create counter objects
local c1 = counter.new()
local c2 = counter.new(10)
-- Use object methods with : notation
c1:increment()
c1:increment(5)
print(c1:get()) -- 6
c2:decrement()
print(c2:get()) -- 9
c1:reset()
print(c1:get()) -- 0
Key concepts for UserData objects:
- Type Name: Unique identifier for your UserData type (e.g.,
"counter.Counter") - Metatable: Defines methods available on your object via
__index - Wrapper Function:
wrapCounter()creates UserData from Go struct - Checker Function:
checkCounter()extracts Go struct from UserData - Method Signature: Methods use
:notation in Lua, which implicitly passesselfas first argument - Annotations: Use
@luamethod ModuleName.ClassName methodNamefor methods vs@luafuncfor module functions
See also: The built-in log module (pkg/modules/log/) is a complete real-world example of UserData objects with proper stub generation.
Step 2: Generate Stubs with stubgen
The stubgen tool scans your Go code for special annotations and generates Lua LSP stubs for IDE autocomplete.
Run stubgen:
# Scan pkg/modules directory and generate stubs in library/
make stubgen
# Or manually:
go run ./cmd/stubgen -dir pkg/modules -output-dir library
# For a single combined file:
go run ./cmd/stubgen -dir pkg/modules -output mymodules.gen.lua
What stubgen looks for:
The tool scans for these comment annotations in your Go code:
Module and Function Annotations
-
@luamodule <name>- Marks the Loader function (required for each module)- Must be directly above the
Loaderfunction - Example:
@luamodule mymodule
- Must be directly above the
-
@luafunc <name>- Defines a module-level function- For functions like
mymodule.greet() - Example:
@luafunc greet
- For functions like
-
@luamethod <ClassName> <methodName>- Defines a method on a UserData object- For object methods like
logger:info() - Class name should be namespaced (e.g.,
log.Logger,counter.Counter) - Example:
@luamethod log.Logger info
- For object methods like
-
@luaclass <ClassName>- Defines a data structure type with manual field annotations- For table-like data structures where you want explicit control over field documentation
- Use with
@luafieldto define each field with custom descriptions - Class name can be simple (
GVKMatcher) or namespaced (mymodule.Config) - Example:
@luaclass GVKMatcher - TIP: For complex Go structs with many nested fields, Type Registry offers automatic field discovery
-
@luafield <fieldName> <type> [description]- Defines a field in a@luaclass- Must be used after
@luaclassannotation <fieldName>: Name of the field<type>: Lua type of the field (string,number,table, etc.)[description]: Optional human-readable description- Example:
@luafield group string The API group
- Must be used after
-
@luaconst <NAME> <type> [description]- Defines a module constant- For constants like
k8sclient.POD,k8sclient.DEPLOYMENT <NAME>: Constant name (typically UPPERCASE)<type>: Lua type of the constant (table,string,number, etc.)[description]: Optional human-readable description- Can be placed anywhere in the file (not just above functions)
- Example:
@luaconst POD table Pod GVK constant {group="", version="v1", kind="Pod"}
- For constants like
Parameter Annotations
@luaparam <name> <type> [description]- Defines a function/method parameter<name>: Parameter name (useselffor the implicit object parameter in methods)<type>: Lua type (string,number,boolean,table,any, or custom type likelog.Logger)[description]: Optional human-readable description- Examples:
@luaparam msg string The message to log@luaparam count number@luaparam ... any Optional key-value pairs(for variadic parameters)@luaparam self log.Logger The logger object(for methods)
Return Value Annotations
@luareturn <type> [description]- Defines a return value- Can have multiple
@luareturnannotations for multiple return values <type>: Lua type or custom type[description]: Optional human-readable description- Type can include union types:
string|nilfor optional returns - Examples:
@luareturn string The greeting message@luareturn log.Logger log.Logger A new logger with additional fields@luareturn err string|nil Error message if any
- Can have multiple
Annotation Placement
Annotations must be in Go-style comments (//) directly above the function:
// functionName: brief description of what it does
//
// @luafunc functionName
// @luaparam param1 string Description of param1
// @luaparam param2 number Description of param2
// @luareturn result string Description of return value
// @luareturn err string|nil Error message if operation failed
func functionName(L *lua.LState) int { ... }
For methods:
// methodName: brief description of what it does
//
// @luamethod ClassName methodName
// @luaparam self ClassName The object instance
// @luaparam param1 string Description of param1
// @luareturn result any Description of return value
func methodName(L *lua.LState) int { ... }
For standalone classes (data structures):
// Loader: creates the mymodule Lua module
//
// @luamodule mymodule
//
// @luaclass GVKMatcher
// @luafield group string The API group
// @luafield version string The API version
// @luafield kind string The resource kind
func Loader(L *lua.LState) int { ... }
For constants (can appear anywhere in the file):
// @luaconst POD table Pod GVK constant {group="", version="v1", kind="Pod"}
// @luaconst DEPLOYMENT table Deployment GVK constant {group="apps", version="v1", kind="Deployment"}
func setupConstants(L *lua.LState, mod *lua.LTable) {
L.SetField(mod, "POD", createGVKTable(L, "", "v1", "Pod"))
L.SetField(mod, "DEPLOYMENT", createGVKTable(L, "apps", "v1", "Deployment"))
}
Example from our code above:
// Module-level function annotation
// @luamodule mymodule <- Tells stubgen this is a Lua module
func Loader(L *lua.LState) int { ... }
// @luafunc greet <- Function name in Lua
// @luaparam name string The name to greet <- Parameter with type and description
// @luareturn string The greeting message <- Return type and description
func greet(L *lua.LState) int { ... }
// UserData method annotation
// @luamethod counter.Counter increment <- Method on Counter class
// @luaparam self counter.Counter The counter object
// @luaparam amount number Optional amount to add
func counterIncrement(L *lua.LState) int { ... }
Generated output for simple module (library/mymodule.gen.lua):
---@meta mymodule
---@class mymodule
local mymodule = {}
---@param name string The name to greet
---@return string The greeting message
function mymodule.greet(name) end
---@param a number First number
---@param b number Second number
---@return number Sum of a and b
---@return string|nil Error message if any
function mymodule.add(a, b) end
return mymodule
Generated output for UserData module (library/counter.gen.lua):
---@meta counter
---@class counter.Counter
local Counter = {}
---@param amount number Optional amount to add (default: 1)
function Counter:increment(amount) end
---@param amount number Optional amount to subtract (default: 1)
function Counter:decrement(amount) end
---@return number The current counter value
function Counter:get() end
function Counter:reset() end
---@class counter
---@field Counter counter.Counter
local counter = {}
---@param initialValue number Optional initial value (default: 0)
---@return counter.Counter A new counter object
function counter.new(initialValue) end
counter.Counter = Counter
return counter
Note how the UserData class (Counter) is defined first with its methods, then the module (counter) is defined with the class as a field, and finally they're linked together with counter.Counter = Counter. This structure enables proper IDE autocomplete.
Choosing Between @luaclass and Type Registry
Both approaches are fully supported and actively used. Choose based on your needs:
Use @luaclass + @luafield annotations when:
- You want explicit control over documentation and field descriptions
- Defining simple table-like data structures (e.g., configuration objects)
- You need custom field descriptions that differ from Go struct tags
- Working with interface types or non-struct data
- Example:
GVKMatcherwith detailed field descriptions
Use Type Registry (typeRegistry.Register()) when:
- You want automatic field discovery from Go struct definitions
- Working with complex Go structs with many nested fields
- Using third-party types (e.g., Kubernetes API types)
- You have many similar types that need consistent documentation
- Field names and types from JSON tags are sufficient
- Example: Kubernetes resources, complex configuration structs
Example comparison:
// @luaclass approach - simple, manual
// @luaclass GVKMatcher
// @luafield group string API group
// @luafield version string API version
// @luafield kind string Resource kind
// Type Registry approach - automatic, comprehensive
typeRegistry.Register(corev1.Pod{}) // Auto-discovers ALL fields
typeRegistry.Register(corev1.Service{}) // Including nested types
typeRegistry.Register(corev1.ConfigMap{}) // With proper type references
The Type Registry automatically processes nested types, creates proper type hierarchies, and handles complex struct relationships. See the Type Registry section for details.
How stubgen works:
- Stubgen scans all
.gofiles in the specified directory - Finds functions with
@luamoduleannotation (these are module Loaders) - Finds functions with
@luafuncannotation (these are exported Lua functions) - Extracts
@luaparamand@luareturnannotations for each function - Generates EmmyLua-compatible annotation files that LSP servers understand
- Outputs one
.gen.luafile per module (or a single combined file)
Verification:
# Check generated files
ls library/
# Output: json.gen.lua kubernetes.gen.lua mymodule.gen.lua spew.gen.lua
# View generated stub
cat library/mymodule.gen.lua
Step 3: Register and Use
package main
import (
"your-project/pkg/modules/mymodule"
lua "github.com/yuin/gopher-lua"
)
func main() {
L := lua.NewState()
defer L.Close()
// Register module
L.PreloadModule("mymodule", mymodule.Loader)
// Use in Lua
L.DoString(`
local m = require("mymodule")
print(m.greet("World")) -- Hello, World!
print(m.add(5, 3)) -- 8
`)
}
Stubgen Tool
The stubgen command generates Lua LSP stubs from your Go module code.
Usage:
# Generate stubs for all modules (recommended)
make stubgen
# Or manually:
go run ./cmd/stubgen -dir pkg/modules -output-dir library
# Single combined file:
go run ./cmd/stubgen -dir pkg/modules -output stubs.lua
Options:
-dir: Directory to scan for Go modules (default: ".")-output-dir: Generate per-module files in this directory (recommended for LSP)-output: Generate single combined file (default: "module_stubs.gen.lua")
What it does:
Scans Go files for these annotations:
@luamodule <name>- Marks module Loader function@luafunc <name>- Module-level function@luamethod <ClassName> <methodName>- Method on UserData object (e.g.,@luamethod log.Logger info)@luaconst <NAME> <type> [description]- Module constant (e.g.,@luaconst POD table)- Can appear anywhere in the file
- Generates
---@typeannotations
@luaparam <name> <type> [description]- Function/method parameter- Supports variadic:
@luaparam ... any - For methods, include:
@luaparam self ClassName
- Supports variadic:
@luareturn <type> [description]- Return value- Supports multiple returns
- Supports union types:
string|nil
Generates Lua LSP annotation files that IDEs use for autocomplete. For UserData objects, it automatically generates the proper class structure with methods and exports the class as a module field.
See detailed annotation reference in the "Creating Custom Lua Modules" section above.
IDE Setup
This section explains how to enable autocomplete for your Lua scripts.
VSCode Setup
-
Install Lua Language Server extension: Lua
-
Generate stubs:
# Generate module stubs (for kubernetes, custom modules)
make stubgen # Creates library/kubernetes.gen.lua, etc.
# Generate type stubs (run your app that uses TypeRegistry)
go run . # Creates annotations.gen.lua
- Create
.vscode/settings.json:
{
"Lua.workspace.library": [
"${workspaceFolder}/library",
"${workspaceFolder}/annotations.gen.lua"
],
"Lua.runtime.version": "Lua 5.1",
"Lua.diagnostics.globals": ["myPod", "originalPod"]
}
- Reload VSCode and enjoy autocomplete.
Neovim Setup
- Install lua-language-server:
:MasonInstall lua-language-server
- Generate stubs:
make stubgen
go run . # If using TypeRegistry
- Create
.luarc.jsonin project root:
{
"runtime": { "version": "Lua 5.1" },
"workspace": {
"library": [".", "library", "annotations.gen.lua"],
"checkThirdParty": false
},
"diagnostics": {
"globals": ["myPod", "originalPod", "modifiedPod"]
}
}
- Restart LSP:
:LspRestart
What You Get
---@type corev1.Pod
local pod = myPod
-- Full autocomplete with Ctrl+Space
pod.metadata.name
pod.spec.containers[1].image
local k8s = require("kubernetes")
k8s.parse_memory("256Mi") -- Shows parameters and return types
API Reference
Translator
type Translator struct{}
// NewTranslator: creates a new bidirectional Go ↔ Lua translator
func NewTranslator() *Translator
// ToLua: converts a Go value to a Lua value
// Supports structs, maps, slices, primitives
// Preserves timestamps and resource quantities
func (t *Translator) ToLua(L *lua.LState, o interface{}) (lua.LValue, error)
// FromLua: converts a Lua value to a Go value
// Accepts any LValue (LTable, LString, LNumber, etc.)
// Requires pointer to output variable
func (t *Translator) FromLua(L *lua.LState, lv lua.LValue, output interface{}) error
TypeRegistry
type TypeRegistry struct{}
// NewTypeRegistry: creates a new type registry for stub generation
func NewTypeRegistry() *TypeRegistry
// Register: registers a Go type for Lua stub generation
func (r *TypeRegistry) Register(obj interface{}) error
// Process: processes all registered types and their dependencies
func (r *TypeRegistry) Process() error
// GenerateStubs: generates Lua LSP annotation code
func (r *TypeRegistry) GenerateStubs() (string, error)
Kubernetes Module (Lua API)
local k8s = require("kubernetes")
-- Parse memory: "256Mi" → 268435456 (bytes)
bytes, err = k8s.parse_memory(quantity)
-- Parse CPU: "100m" → 100 (millicores)
millis, err = k8s.parse_cpu(quantity)
-- Parse time: "2025-10-03T16:39:00Z" → 1759509540 (Unix timestamp)
timestamp, err = k8s.parse_time(timestr)
-- Format time: 1759509540 → "2025-10-03T16:39:00Z"
timestr, err = k8s.format_time(timestamp)
All functions return (value, error) tuples.
Features
- Bidirectional Conversion: Seamlessly convert Go structs to Lua tables and vice versa with full round-trip integrity
- Automatic Stub Generation: Generate Lua LSP annotations from Go types for IDE autocomplete
- Lua Module System: Create type-safe Lua modules with Go functions
- IDE Support: Full autocomplete and type checking in VSCode, Neovim, and other editors
- Kubernetes Ready: Built-in support for K8s API types and resource quantities
- K8s Dynamic Client: Full CRUD operations on any Kubernetes resource from Lua scripts
- Type Safety: Preserve complex types like timestamps, quantities, maps, and nested structures
- Well Tested: 79%+ code coverage with comprehensive unit and integration tests
Use Cases
Kubernetes Admission Controllers
Process and validate K8s resources in Lua scripts:
// Load validation script
L.DoString(validationScript)
// Convert admission request to Lua
luaRequest, _ := translator.ToLua(L, admissionRequest)
L.SetGlobal("request", luaRequest)
// Run validation logic in Lua
L.DoString(`
local pod = request.object
if pod.spec.containers[1].resources.limits["memory"] == nil then
reject("Memory limit required")
end
`)
Policy Engines
Define policies in Lua, enforce in Go:
local k8s = require("kubernetes")
function validate_pod(pod)
-- Policy: Memory must be under 2GB
local memBytes = k8s.parse_memory(pod.spec.containers[1].resources.limits["memory"])
if memBytes > 2 * 1024 * 1024 * 1024 then
return false, "Memory limit exceeds 2GB"
end
return true, nil
end
Configuration Processing
Process complex config files with Lua logic:
config := LoadConfig()
luaConfig, _ := translator.ToLua(L, config)
L.SetGlobal("config", luaConfig)
L.DoString(configProcessingScript)
var processedConfig Config
translator.FromLua(L, L.GetGlobal("result"), &processedConfig)
Performance
Performance benchmarks demonstrate glua's efficiency for production use:
BenchmarkGoToLuaSimple-16 409915 3074 ns/op 4327 B/op 45 allocs/op
BenchmarkGoToLuaComplex-16 75747 15458 ns/op 23979 B/op 221 allocs/op
BenchmarkGoToLuaPod-16 29530 40777 ns/op 58909 B/op 468 allocs/op
BenchmarkLuaToGoSimple-16 609123 2044 ns/op 1000 B/op 23 allocs/op
BenchmarkLuaToGoComplex-16 124522 9876 ns/op 4901 B/op 118 allocs/op
BenchmarkRoundTripSimple-16 202911 5640 ns/op 5330 B/op 68 allocs/op
BenchmarkRoundTripPod-16 30727 39612 ns/op 42924 B/op 391 allocs/op
BenchmarkLuaFieldAccess-16 88826 13688 ns/op 33937 B/op 114 allocs/op
BenchmarkLuaNestedFieldAccess-16 49378 24432 ns/op 37745 B/op 271 allocs/op
BenchmarkLuaArrayIteration-16 42882 28862 ns/op 36633 B/op 335 allocs/op
BenchmarkLuaMapIteration-16 72069 17139 ns/op 34833 B/op 125 allocs/op
BenchmarkLuaFieldModification-16 74764 16009 ns/op 34705 B/op 154 allocs/op
BenchmarkLuaComplexOperation-16 19200 76427 ns/op 195660 B/op 458 allocs/op
Key performance characteristics:
- Simple conversions: ~3µs Go→Lua, ~2µs Lua→Go
- Kubernetes Pod: ~40µs full round-trip conversion
- Field access: ~14µs for simple Lua operations
- Production ready: Suitable for request processing, admission controllers, policy evaluation
Run benchmarks yourself:
make bench # View benchmark results
make bench-update # Update benchmarks/README.md with latest results
Testing
# Run ALL tests: unit + k8sclient integration (recommended)
make test
# Or just run make (default target runs all tests)
make
# Unit tests only
make test-unit
# K8s integration test only (requires Kind & kubectl)
make test-k8sclient
# Verbose per-package
make test-verbose
# Fast (no race detection)
make test-short
Coverage: 79%+ overall with comprehensive unit and integration tests
What's tested:
- Go ↔ Lua conversions in real Lua VMs (not just Go unit tests)
- Kubernetes module functions with actual K8s types
- K8s client CRUD operations with real Kind cluster
- Round-trip integrity (Go → Lua → Go preserves data)
- Stub generation from Go code
- Race detection enabled
- CI/CD across Go 1.21, 1.22, 1.23
Example Application
The example/ directory contains a complete working demo showing all features.
# From repo root
make example && ./bin/example
# Or
cd example && go run .
Features demonstrated:
- Go → Lua conversion (Pod struct to Lua table)
- Lua script execution with kubernetes module
- Parsing timestamps, CPU, and memory quantities
- Lua → Go conversion (table back to Pod struct)
- Round-trip integrity verification
- Stub generation for IDE autocomplete
- Error handling
To get autocomplete in the example:
- Run
make stubgenfrom repo root - Run
go run .from example/ directory - Open
script.luain your IDE - autocomplete works
Troubleshooting
Autocomplete doesn't work
- Run
make stubgento generate module stubs - Check
.luarc.jsonor.vscode/settings.jsonincludes"library"directory - Verify
library/kubernetes.gen.luaexists and starts with---@meta - Restart LSP:
:LspRestart(Neovim) or reload window (VSCode)
Module not found error
Ensure L.PreloadModule("mymodule", mymodule.Loader) is called before L.DoString()
Stubgen finds no modules
Check @luamodule annotation is directly above Loader function in Go code
Round-trip data mismatch
Ensure all struct fields are exported (capitalized) and JSON-marshallable
Contributing
Contributions welcome! Please:
- Ensure all tests pass (
go test -cover -race ./...) - Add tests for new functionality
- Follow existing code style (gofmt)
- Update documentation
- Add examples for new features
License
MIT License - see LICENSE file for details
Credits
Built on top of gopher-lua by Yusuke Inuzuka.
Kubernetes API types from k8s.io/api.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
run-script
command
|
|
|
stubgen
command
|
|
|
k8sclient
command
|
|
|
kubernetes
command
|
|
|
log
command
|
|
|
spew
command
|
|
|
webhook
command
|
|
|
pkg
|
|
|
modules/base64/stubgen
command
|
|
|
modules/fs/stubgen
command
|
|
|
modules/hash/stubgen
command
|
|
|
modules/hex/stubgen
command
|
|
|
modules/http/stubgen
command
|
|
|
modules/json/stubgen
command
|
|
|
modules/k8sclient/stubgen
command
|
|
|
modules/kubernetes/stubgen
command
|
|
|
modules/log/stubgen
command
|
|
|
modules/spew/stubgen
command
|
|
|
modules/template/stubgen
command
|
|
|
modules/yaml/stubgen
command
|
|