DataSource Configuration Guide β
Complete guide for configuring datasources with Lynq.
Overview β
Lynq reads node data from external datasources and automatically provisions Kubernetes resources. This guide covers database setup, column mappings, and data transformation techniques.
Supported Datasources β
| Datasource | Status | Since | Guide |
|---|---|---|---|
| MySQL | β Stable | v1.0 | MySQL Guide |
| PostgreSQL | π§ Planned | v1.2 | Coming soon |
| Custom | π‘ Contribute | - | Contribution Guide |
Want to Add a Datasource?
Lynq uses a pluggable adapter pattern. Contributing a new datasource is straightforward! See: Contributing a New Datasource
Scope
Examples focus on MySQL (currently supported). PostgreSQL support is planned for v1.2, and the same patterns will apply to other relational sources.
MySQL Connection β
Basic Configuration β
apiVersion: operator.lynq.sh/v1
kind: LynqHub
metadata:
name: my-hub
spec:
source:
type: mysql
mysql:
host: mysql.default.svc.cluster.local
port: 3306
username: node_reader
passwordRef:
name: mysql-credentials
key: password
database: nodes
table: node_configs
syncInterval: 1mConnection Details β
| Field | Description | Default / Recommendation |
|---|---|---|
host | MySQL server hostname or IP | Cluster DNS entry |
port | MySQL server port | 3306 |
username | Database username (use read-only credentials) | node_reader |
passwordRef | Reference to a Kubernetes Secret containing the password | mysql-credentials |
database | Database name | nodes |
table | Table or view containing node data | node_configs |
syncInterval | How often to poll the database (e.g., 30s, 1m, 5m) | 1m |
Column Mappings β
Required Mappings β
Two columns are required for every node:
valueMappings:
uid: node_id # Unique node identifier
activate: is_active # Activation flagDEPRECATED: hostOrUrl
hostOrUrl mapping is deprecated since v1.1.11 and will be removed in v1.3.0.
Legacy usage (deprecated):
valueMappings:
uid: node_id
hostOrUrl: node_url # β οΈ DEPRECATED
activate: is_activeNew recommended approach:
valueMappings:
uid: node_id
activate: is_active
extraValueMappings:
nodeUrl: node_url # β
Use extraValueMappings instead
# In template, use toHost() function:
# {{ .nodeUrl | toHost }} # Extract hostname from URLuid - Node Identifier β
- Type: String
- Required: Yes
- Purpose: Unique identifier for each node
- Examples:
"node-123","acme-corp","customer-456" - Used in: Resource naming, labels, template variables
activate - Activation Flag β οΈ β
- Type: String (truthy values)
- Required: Yes
- Purpose: Controls whether node resources are created
Activation rules
| Accepted values (case-sensitive) | Result |
|---|---|
"1", "true", "TRUE", "True", "yes", "YES", "Yes" | Node is marked active and resources reconcile. |
| Rejected values | Result |
|---|---|
"0", "false", "FALSE", "no", "", NULL, any other string | Node is ignored during sync. |
- Only the exact accepted strings above are considered active.
- Boolean columns work if they stringify to
"1"or"true". - Integer columns (
TINYINT) work when they return the string"1".
Extra Mappings β
Add custom variables for use in templates:
extraValueMappings:
planId: subscription_plan # Maps "subscription_plan" column to {{ .planId }}
region: deployment_region # Maps "deployment_region" column to {{ .region }}
maxUsers: max_user_count # Maps "max_user_count" column to {{ .maxUsers }}
apiKey: api_token # Maps "api_token" column to {{ .apiKey }}These variables become available in all templates as {{ .planId }}, {{ .region }}, etc.
Database Schema Examples β
Example 1: Simple Node Table β
CREATE TABLE node_configs (
node_id VARCHAR(255) PRIMARY KEY,
node_url VARCHAR(500) NOT NULL,
is_active TINYINT(1) DEFAULT 0,
subscription_plan VARCHAR(50),
deployment_region VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Sample data
INSERT INTO node_configs (node_id, node_url, is_active, subscription_plan, deployment_region) VALUES
('acme-corp', 'https://acme.example.com', 1, 'enterprise', 'us-east-1'),
('beta-inc', 'https://beta.example.com', 1, 'startup', 'eu-west-1'),
('gamma-llc', 'https://gamma.example.com', 0, 'trial', 'ap-south-1'); -- Not activeHub Configuration:
valueMappings:
uid: node_id
hostOrUrl: node_url
activate: is_active
extraValueMappings:
planId: subscription_plan
region: deployment_regionExample 2: Boolean and String Activate Column β
-- Option A: TINYINT (returns "1" or "0" as string)
CREATE TABLE nodes (
id VARCHAR(255) PRIMARY KEY,
url VARCHAR(500),
active TINYINT(1) DEFAULT 0 -- β
Returns "1" (truthy) or "0" (falsy)
);
-- Option B: VARCHAR with explicit values
CREATE TABLE nodes (
id VARCHAR(255) PRIMARY KEY,
url VARCHAR(500),
status VARCHAR(20) DEFAULT 'inactive' -- Values: "active" or "inactive"
);
-- Option C: BOOLEAN (MySQL stores as TINYINT)
CREATE TABLE nodes (
id VARCHAR(255) PRIMARY KEY,
url VARCHAR(500),
enabled BOOLEAN DEFAULT FALSE -- β
Returns "1" or "0" as string
);Hub Configurations:
For Option A or C (TINYINT/BOOLEAN):
valueMappings:
uid: id
hostOrUrl: url
activate: active # β
"1" is truthy, "0" is falsyFor Option B (VARCHAR status):
# β Won't work directly - "active" is not a valid truthy value
# Use a VIEW with transformation (see below)Data Transformation with Views β
If your database schema doesn't match the required format, create a MySQL VIEW to transform data.
Use Case 1: Transform Status String to Truthy Value β
Problem: Your status column has values like "active", "inactive", "suspended"
Solution: Create a view that maps to truthy values:
CREATE VIEW node_configs AS
SELECT
id AS node_id,
url AS node_url,
CASE
WHEN status = 'active' THEN '1'
ELSE '0'
END AS is_active,
plan AS subscription_plan,
region AS deployment_region
FROM nodes
WHERE status IN ('active', 'inactive'); -- Exclude suspendedHub Configuration:
spec:
source:
mysql:
table: node_configs # β
Use VIEW name, not original table
valueMappings:
uid: node_id
hostOrUrl: node_url
activate: is_active # β
Now returns "1" or "0"Use Case 2: Combine Multiple Columns β
Problem: Node URL is split across multiple columns
-- Original table
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
subdomain VARCHAR(100),
domain VARCHAR(100), -- e.g., "myapp.com"
enabled TINYINT(1)
);
-- View to combine columns
CREATE VIEW node_configs AS
SELECT
CONCAT('customer-', customer_id) AS node_id,
CONCAT('https://', subdomain, '.', domain) AS node_url,
IF(enabled = 1, '1', '0') AS is_active
FROM customers
WHERE enabled = 1;Use Case 3: Filter and Transform β
Problem: You want to exclude certain nodes or apply business logic
CREATE VIEW active_paying_nodes AS
SELECT
node_id,
node_url,
'1' AS is_active, -- Always active in this view
subscription_tier,
MAX(license_count) AS max_users
FROM nodes n
JOIN subscriptions s ON n.id = s.node_id
WHERE
s.status = 'active'
AND s.payment_status = 'paid'
AND s.expiry_date > NOW()
GROUP BY node_id, node_url, subscription_tier;Use Case 4: Add Computed Columns β
Problem: You need derived data in templates
CREATE VIEW node_configs AS
SELECT
node_id,
node_url,
is_active,
subscription_plan,
-- Computed columns
CONCAT('https://cdn-', deployment_region, '.example.com') AS cdn_url,
CASE subscription_plan
WHEN 'enterprise' THEN '100'
WHEN 'business' THEN '50'
ELSE '10'
END AS max_replicas,
DATE_FORMAT(created_at, '%Y-%m-%d') AS created_date
FROM nodes;Use in templates:
extraValueMappings:
cdnUrl: cdn_url
maxReplicas: max_replicas
createdDate: created_date# In deployment template
spec:
replicas: {{ .maxReplicas }} # Uses computed value
containers:
- name: app
env:
- name: CDN_URL
value: "{{ .cdnUrl }}"Best Practices β
1. Use Read-Only Database User β
-- Create read-only user
CREATE USER 'node_reader'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT ON nodes.node_configs TO 'node_reader'@'%';
FLUSH PRIVILEGES;2. Use Views for Data Isolation β
-- Original table has sensitive data
CREATE TABLE nodes_internal (
id VARCHAR(255),
url VARCHAR(500),
active TINYINT(1),
api_secret VARCHAR(500), -- Sensitive!
billing_email VARCHAR(255), -- Sensitive!
internal_notes TEXT -- Sensitive!
);
-- View exposes only necessary columns
CREATE VIEW node_configs AS
SELECT
id AS node_id,
url AS node_url,
IF(active = 1, '1', '0') AS is_active
FROM nodes_internal;
-- Grant access only to view
GRANT SELECT ON nodes.node_configs TO 'node_reader'@'%';3. Add Indexes for Performance β
-- Index on activation column for faster filtering
CREATE INDEX idx_active ON nodes(is_active);
-- Composite index for common queries
CREATE INDEX idx_active_created ON nodes(is_active, created_at);4. Validate Data Before Deployment β
-- Check for invalid activate values
SELECT node_id, is_active
FROM node_configs
WHERE is_active NOT IN ('0', '1', 'true', 'false', 'yes', 'no');
-- Check for missing required fields
SELECT node_id
FROM node_configs
WHERE node_url IS NULL OR node_url = '';
-- Check for duplicate UIDs
SELECT node_id, COUNT(*) as count
FROM node_configs
GROUP BY node_id
HAVING count > 1;5. Use Appropriate Sync Intervals β
# High-frequency (more API calls, faster sync)
syncInterval: 30s # For development/testing
# Medium-frequency (balanced)
syncInterval: 1m # Recommended for production
# Low-frequency (fewer API calls, slower sync)
syncInterval: 5m # For large deployments (1000+ nodes)Troubleshooting β
Problem: Nodes Not Being Created β
Check 1: Verify activate column values
SELECT node_id, is_active,
CASE is_active
WHEN '1' THEN 'β
Valid'
WHEN 'true' THEN 'β
Valid'
WHEN 'TRUE' THEN 'β
Valid'
WHEN 'yes' THEN 'β
Valid'
ELSE 'β Invalid'
END AS status
FROM node_configs;Check 2: Query what operator sees
-- Run the exact query operator uses
SELECT node_id, node_url, is_active
FROM node_configs;Check 3: Check operator logs
kubectl logs -n lynq-system -l control-plane=controller-manager | grep -i "query\|node"Problem: View Not Updating β
Solution 1: Refresh view definition
DROP VIEW IF EXISTS node_configs;
CREATE VIEW node_configs AS
SELECT ...;Solution 2: Check view dependencies
SHOW CREATE VIEW node_configs;Problem: Connection Errors β
Check database accessibility from cluster:
kubectl run mysql-test --rm -it --image=mysql:8 -- \
mysql -h mysql.default.svc.cluster.local -u node_reader -pCheck credentials:
# Verify secret exists
kubectl get secret mysql-secret -o yaml
# Decode password
kubectl get secret mysql-secret -o jsonpath='{.data.password}' | base64 -dComplete Example β
Database Setup β
-- Create main nodes table
CREATE TABLE nodes (
id INT AUTO_INCREMENT PRIMARY KEY,
company_name VARCHAR(255) NOT NULL,
subdomain VARCHAR(100) NOT NULL UNIQUE,
status ENUM('active', 'suspended', 'trial') DEFAULT 'trial',
plan VARCHAR(50) DEFAULT 'free',
region VARCHAR(50) DEFAULT 'us-east-1',
max_users INT DEFAULT 5,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Create transformation view
CREATE VIEW node_configs AS
SELECT
CONCAT('node-', id) AS node_id,
CONCAT('https://', subdomain, '.myapp.com') AS node_url,
CASE status
WHEN 'active' THEN '1'
WHEN 'trial' THEN '1'
ELSE '0'
END AS is_active,
plan AS subscription_plan,
region AS deployment_region,
max_users AS max_user_count
FROM nodes;
-- Create read-only user
CREATE USER 'node_reader'@'%' IDENTIFIED BY 'secure_password_here';
GRANT SELECT ON mydb.node_configs TO 'node_reader'@'%';
FLUSH PRIVILEGES;
-- Insert sample data
INSERT INTO nodes (company_name, subdomain, status, plan, region, max_users) VALUES
('Acme Corp', 'acme', 'active', 'enterprise', 'us-east-1', 100),
('Beta Inc', 'beta', 'trial', 'startup', 'eu-west-1', 10),
('Gamma LLC', 'gamma', 'suspended', 'free', 'ap-south-1', 5);Kubernetes Secret β
apiVersion: v1
kind: Secret
metadata:
name: mysql-credentials
namespace: lynq-system
type: Opaque
stringData:
password: secure_password_hereLynqHub β
apiVersion: operator.lynq.sh/v1
kind: LynqHub
metadata:
name: production-nodes
spec:
source:
type: mysql
mysql:
host: mysql.default.svc.cluster.local
port: 3306
username: node_reader
passwordRef:
name: mysql-credentials
key: password
database: mydb
table: node_configs # β
Use view name
syncInterval: 1m
valueMappings:
uid: node_id
hostOrUrl: node_url
activate: is_active
extraValueMappings:
planId: subscription_plan
region: deployment_region
maxUsers: max_user_countQuery Results β
node_id node_url is_active subscription_plan deployment_region max_user_count
-------------- ------------------------------- ---------- ------------------ ------------------ --------------
node-1 https://acme.myapp.com 1 enterprise us-east-1 100
node-2 https://beta.myapp.com 1 startup eu-west-1 10
node-3 https://gamma.myapp.com 0 free ap-south-1 5Result: 2 LynqNode CRs created (node-1, node-2). node-3 is skipped because is_active = "0".
Contributing a New Datasource β
Want to add support for PostgreSQL, MongoDB, REST APIs, or other datasources?
Lynq uses a pluggable adapter pattern that makes it easy to add new datasources. You only need to:
- Implement 2 methods:
QueryNodes()andClose() - Register your adapter: Add it to the factory function
- Add API types: Define your datasource configuration
- Write tests: Ensure quality and reliability
- Document: Help users configure your datasource
Why Contribute? β
- β Easy to implement - Clear interface, reference implementation
- β Well-structured - Adapter pattern isolates your code
- β Impactful - Help the community use their preferred datasources
- β Recognized - Contributors listed in release notes and README
Getting Started β
π Full Guide: Contributing a New Datasource
The guide includes:
- Step-by-step implementation instructions
- Complete code examples (including PostgreSQL)
- Testing strategies
- Documentation templates
- PR checklist
Example Adapters β
MySQL (reference implementation):
- Location:
internal/datasource/mysql.go - Lines of code: ~200
- Features: Connection pooling, filtering, mapping
PostgreSQL (example in guide):
- Full implementation shown
- Shows SQL dialect differences
- Demonstrates best practices
Architecture β
Quick Start β
// 1. Create your adapter file
// internal/datasource/your_adapter.go
package datasource
type YourAdapter struct {
conn *YourConnection
}
func NewYourAdapter(config Config) (*YourAdapter, error) {
// Connect to your datasource
return &YourAdapter{conn: conn}, nil
}
func (a *YourAdapter) QueryNodes(ctx context.Context, config QueryConfig) ([]NodeRow, error) {
// Query and return node data
return nodes, nil
}
func (a *YourAdapter) Close() error {
// Cleanup
return a.conn.Close()
}
// 2. Register in factory (internal/datasource/interface.go)
case SourceTypeYours:
return NewYourAdapter(config)Community Support β
Need help? We're here!
- π¬ GitHub Discussions - Ask questions
- π Full Guide - Detailed instructions
- π Issues - Report problems
- π§ Email - Direct contact
Start contributing today and make Lynq work with your favorite datasource! π
See Also β
- Configuration Guide - Hub and template configuration
- Templates Guide - Using template variables
- Security Guide - Securing database credentials
- Troubleshooting Guide - Common issues and solutions
- Contributing a New Datasource - Full contribution guide
