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. Complete VIEW Verification Checklist β
Before connecting Lynq to your VIEW, verify it meets all requirements:
Step 1: Verify Required Columns Exist
-- Check VIEW structure
DESCRIBE node_configs;
-- Expected output:
-- +-------------------+--------------+------+-----+---------+-------+
-- | Field | Type | Null | Key | Default | Extra |
-- +-------------------+--------------+------+-----+---------+-------+
-- | node_id | varchar(255) | YES | | NULL | | β
Required (uid)
-- | is_active | varchar(1) | YES | | NULL | | β
Required (activate)
-- | node_url | varchar(500) | YES | | NULL | | Optional
-- | subscription_plan | varchar(50) | YES | | NULL | | Extra mapping
-- +-------------------+--------------+------+-----+---------+-------+Step 2: Verify Data Types
-- Check actual data returned
SELECT
node_id,
TYPEOF(node_id) AS uid_type,
is_active,
TYPEOF(is_active) AS activate_type
FROM node_configs
LIMIT 5;
-- All values should be strings
-- is_active should be '0' or '1' (not 0 or 1 as integers)Step 3: Verify Activation Values
-- Count by activation value (should see only valid values)
SELECT
is_active,
COUNT(*) as count,
CASE
WHEN is_active IN ('1', 'true', 'TRUE', 'yes', 'YES') THEN 'β
Active'
WHEN is_active IN ('0', 'false', 'FALSE', 'no', 'NO', '') THEN 'βΈοΈ Inactive'
ELSE 'β Invalid'
END AS status
FROM node_configs
GROUP BY is_active;
-- Example good output:
-- +----------+-------+-----------+
-- | is_active| count | status |
-- +----------+-------+-----------+
-- | 1 | 5 | β
Active |
-- | 0 | 3 | βΈοΈ Inactive |
-- +----------+-------+-----------+Step 4: Verify No Duplicate UIDs
-- Should return empty result set
SELECT node_id, COUNT(*) as count
FROM node_configs
GROUP BY node_id
HAVING count > 1;
-- If duplicates found, fix your VIEW:
-- Option A: Add DISTINCT
-- Option B: Add GROUP BY
-- Option C: Fix source dataStep 5: Verify No NULL UIDs
-- Should return empty result set
SELECT * FROM node_configs WHERE node_id IS NULL OR node_id = '';Step 6: Preview What Lynq Will See
-- This query mimics what the operator runs
SELECT * FROM node_configs;
-- Check the result:
-- 1. All expected rows appear
-- 2. UIDs are unique and non-empty
-- 3. activate values are valid
-- 4. Extra columns contain expected dataStep 7: Test with Read-Only User
# Connect as the user Lynq will use
mysql -h mysql-server -u node_reader -p
# Run the same query
mysql> SELECT * FROM mydb.node_configs;
# Verify results match Step 6Step 8: Verify from Kubernetes
# Test connectivity from cluster
kubectl run mysql-test --rm -it --image=mysql:8 -- \
mysql -h mysql.default.svc.cluster.local \
-u node_reader \
-p'your-password' \
-e "SELECT * FROM mydb.node_configs"
# Expected: Same output as Step 6Complete Verification Script:
-- Run this script to validate your VIEW before deployment
-- Save as verify_view.sql and run: mysql -u root < verify_view.sql
USE mydb;
-- 1. Structure check
SELECT '=== STEP 1: VIEW Structure ===' AS step;
DESCRIBE node_configs;
-- 2. Required columns check
SELECT '=== STEP 2: Required Columns ===' AS step;
SELECT
MAX(CASE WHEN COLUMN_NAME = 'node_id' THEN 'FOUND' ELSE 'MISSING' END) AS uid_column,
MAX(CASE WHEN COLUMN_NAME = 'is_active' THEN 'FOUND' ELSE 'MISSING' END) AS activate_column
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = 'node_configs';
-- 3. Data validation
SELECT '=== STEP 3: Activation Value Distribution ===' AS step;
SELECT is_active, COUNT(*) as count FROM node_configs GROUP BY is_active;
-- 4. Duplicate check
SELECT '=== STEP 4: Duplicate UIDs (should be empty) ===' AS step;
SELECT node_id, COUNT(*) FROM node_configs GROUP BY node_id HAVING COUNT(*) > 1;
-- 5. NULL check
SELECT '=== STEP 5: NULL UIDs (should be empty) ===' AS step;
SELECT * FROM node_configs WHERE node_id IS NULL OR node_id = '';
-- 6. Sample data
SELECT '=== STEP 6: Sample Data Preview ===' AS step;
SELECT * FROM node_configs LIMIT 10;
-- 7. Active node count
SELECT '=== STEP 7: Expected LynqNodes ===' AS step;
SELECT COUNT(*) AS expected_lynqnodes FROM node_configs WHERE is_active IN ('1', 'true', 'TRUE', 'yes', 'YES');Expected Lynq Behavior After Verification:
# After deploying LynqHub, verify operator synced correctly
$ kubectl get lynqhub production-nodes -o jsonpath='{.status}'
# Expected:
# {
# "desired": 5, # Matches Step 7 count
# "ready": 5,
# "failed": 0,
# "referencingTemplates": 1,
# "conditions": [{
# "type": "Ready",
# "status": "True",
# "reason": "SyncSucceeded",
# "message": "Successfully synced 5 nodes from database"
# }]
# }
# List created LynqNodes
$ kubectl get lynqnodes
NAME READY DESIRED AGE
node-1 5/5 5 1m
node-2 5/5 5 1m
node-3 5/5 5 1m
node-4 5/5 5 1m
node-5 5/5 5 1m5. 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
