glua

module
v0.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 13, 2025 License: MIT

README

glua - Go to Lua Bridge with type safety

Tests Go Report Card GoDoc

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

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.1.0 // 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.1.0  # 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.1.0  # 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: kind and kubectl

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 autocomplete
  • bin/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:

  1. Type Name: Unique identifier for your UserData type (e.g., "counter.Counter")
  2. Metatable: Defines methods available on your object via __index
  3. Wrapper Function: wrapCounter() creates UserData from Go struct
  4. Checker Function: checkCounter() extracts Go struct from UserData
  5. Method Signature: Methods use : notation in Lua, which implicitly passes self as first argument
  6. Annotations: Use @luamethod ModuleName.ClassName methodName for methods vs @luafunc for 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 Loader function
    • Example: @luamodule mymodule
  • @luafunc <name> - Defines a module-level function

    • For functions like mymodule.greet()
    • Example: @luafunc greet
  • @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
  • @luaclass <ClassName> - Defines a standalone class/type (for data structures without methods)

    • For data types like GVKMatcher, Config, etc.
    • Use with @luafield to define the class's fields
    • Class name can be simple (GVKMatcher) or namespaced (mymodule.Config)
    • Example: @luaclass GVKMatcher
  • @luafield <fieldName> <type> [description] - Defines a field in a class

    • Must be used after @luaclass annotation
    • <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
  • @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"}
Parameter Annotations
  • @luaparam <name> <type> [description] - Defines a function/method parameter
    • <name>: Parameter name (use self for the implicit object parameter in methods)
    • <type>: Lua type (string, number, boolean, table, any, or custom type like log.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 @luareturn annotations for multiple return values
    • <type>: Lua type or custom type
    • [description]: Optional human-readable description
    • Type can include union types: string|nil for 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
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.

How it works:

  1. Stubgen scans all .go files in the specified directory
  2. Finds functions with @luamodule annotation (these are module Loaders)
  3. Finds functions with @luafunc annotation (these are exported Lua functions)
  4. Extracts @luaparam and @luareturn annotations for each function
  5. Generates EmmyLua-compatible annotation files that LSP servers understand
  6. Outputs one .gen.lua file 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 ---@type annotations
  • @luaparam <name> <type> [description] - Function/method parameter
    • Supports variadic: @luaparam ... any
    • For methods, include: @luaparam self ClassName
  • @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
  1. Install Lua Language Server extension: Lua

  2. 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
  1. Create .vscode/settings.json:
{
  "Lua.workspace.library": [
    "${workspaceFolder}/library",
    "${workspaceFolder}/annotations.gen.lua"
  ],
  "Lua.runtime.version": "Lua 5.1",
  "Lua.diagnostics.globals": ["myPod", "originalPod"]
}
  1. Reload VSCode and enjoy autocomplete.
Neovim Setup
  1. Install lua-language-server:
:MasonInstall lua-language-server
  1. Generate stubs:
make stubgen
go run .  # If using TypeRegistry
  1. Create .luarc.json in project root:
{
  "runtime": { "version": "Lua 5.1" },
  "workspace": {
    "library": [".", "library", "annotations.gen.lua"],
    "checkThirdParty": false
  },
  "diagnostics": {
    "globals": ["myPod", "originalPod", "modifiedPod"]
  }
}
  1. 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:

  1. Run make stubgen from repo root
  2. Run go run . from example/ directory
  3. Open script.lua in your IDE - autocomplete works

Troubleshooting

Autocomplete doesn't work
  1. Run make stubgen to generate module stubs
  2. Check .luarc.json or .vscode/settings.json includes "library" directory
  3. Verify library/kubernetes.gen.lua exists and starts with ---@meta
  4. 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:

  1. Ensure all tests pass (go test -cover -race ./...)
  2. Add tests for new functionality
  3. Follow existing code style (gofmt)
  4. Update documentation
  5. 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

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL