Command Source
Command Source is a feed generator which executes a custom script at set intervals and exposes the resulting feed over HTTP.
The main functionality is captured in the below diagram:

In the diagram you can see the main 2 components:
- commandsource container is responsible for running the script and storing the resulting feed in the "feed storage"
- webserver container is responsible for exposing the feed stored in "feed storage"
When deployed, Command Source will expose a feed URL at the following address:
[Service / Ingress]/feed
Services created for Command Source are as follows:
- HTTP: Port
8080
on Servicecommandsource-[Instance Set Name]
- HTTPS (if enabled): Port
8443
on Servicecommandsource-[Instance Set Name]-https
Script
Requirements
For Command Source to be able to interpret the output of the script as data for a feed, the script must produce the following:
- Feed codes file (JSON)
- Feed snapshot file (TSV)
- Metadata (JSON)
Feed codes file
The feed codes file is commonly referred to as feed-codes.json
. In this file the categories are defined and assigned a code (an ID which we can use in filtering rules). Example of the contents of this file:
{
"v": 1,
"codes": [
{"code": 1, "title": "documentation"},
{"code": 2, "title": "examples"},
{"code": 3, "title": "netherlands"},
]
}
The above example feed-codes.json
defines numerical codes for 3 categories. This includes a documentation
category, with code 1
.
The format to which this file has to adhere is as follows:
# (Required) Internal version field, currently must be set to 1
v: integer
# (Optional) Indicates if a codename should be used instead of the numeric code.
# This defaults to false, which is recommended for most feeds.
# Do not turn this on unless you fully understand the implications.
named_codes: boolean
# (Required) A list of objects describing each code
codes: list of code objects
Where "code objects" must adhere to:
# (Required) The code-number of this category (1-65535).
# Classification codes do not have to be consecutive.
code: uint16
# (Required) The title for this category (should be concise)
title: string
# (Optional) Description for this code (can be verbose)
desc: string
# (Optional) String name to be used for code when named_codes is set
codename: string
Feed snapshot file
The feed snapshot file is a tab-separated file which contains the DomainPaths and which categories they should belong to. Example of the contents of this file (note: The \t
characters represent a tab
since this is a tab-separated file):
The above (combined with the previous chapter's feed codes example) ends up with the following DomainPath to Category mapping:
Category Code | Category Title | DomainPaths |
---|---|---|
1 |
documentation |
doc.powerdns.com |
2 |
examples |
example.com example.co.uk example.net example.nl |
3 |
netherlands |
example.nl |
Notes about this file format:
- Empty lines are allowed
- Lines starting with a # can be used for comments
- The same DomainPath is allowed to appear multiple times. All the codes discovered this way are combined and appear in the generated feed
Metadata JSON
When the Feed codes & snapshot files have been generated, the Command Source engine needs to be informed about it so they can be processed before they are served by the webserver. Minimalistic example of the contents of this metadata:
The format to which this metadata has to adhere is as follows:
# (Required) Internal version field, currently must be set to 1
v: integer
# (Required) Path to the feed codes file (JSON)
codes_file: string
# (Required) Path to the feed snapshot file (TSV)
# Required
csv_file: string
# (Optional) Version field which can be used by Command Source to quickly determine whether
# a generated set of data needs to lead to the feed being rebuilt. A script can
# access the `OXFEED_LAST_VERSION` environment variable to see the version
# returned by the previous iteration of the script execution.
# This can potentially be used to create logic to prevent attempting to generate large feeds
# more often than necessary. For example, when loading feed codes and snapshots from a database
# you could use this to check whether or not the contents of the database have changed and
# hence require generating an updated feed
version: string
Command Source response
When the Feed codes & snapshot files are ready and the metadata has been constructed, Command Source needs to be informed. This is done by having the script output a line of text to the stdout in a specific format. This line should be as follows:
Using our example from the previous chapters, we should output this:
OXFEED_GENERATOR_RESULT: {"v": 1, "codes_file": "/tmp/feed-codes.json", "csv_file": "/tmp/feed-snapshot.tsv"}
Note: This must be one line and will only be read if the script exits with code 0
Execution Environment
Currently Command Source provides support for the following script execution engines:
- Python 3
- Shell script (sh)
Scripts executed by Command Source have access to the following:
- All files contained within the Configmap referenced by
scriptConfigMap
can be accessed by the script inside the directory stored in environment variable namedCC_SCRIPT_DIR
(/script
) - All items inside the secret referenced by
scriptSecretName
can be accessed by the script inside the directory stored in environment variable namedCC_SECRETS_DIR
(/script_secrets
) - A writable local volume is accessible by the script, the path of which is stored in environment variable named
CC_TEMP_DIR
(/tmp
)
Script Example
Below is an example Python3 script which will generate the feed used in the above chapters:
import json, tempfile
# Write feed codes JSON file
codes = {
"v": 1,
"codes": [
{"code": 1, "title": "documentation"},
{"code": 2, "title": "examples"},
{"code": 3, "title": "netherlands"},
]
}
_, codes_path = tempfile.mkstemp()
with open(codes_path, 'w') as f:
json.dump(codes, f)
# Write feed snapshot TSV file
_, snapshot_path = tempfile.mkstemp()
with open(snapshot_path, 'w') as f:
print("doc.powerdns.com\t1", file=f)
print("example.com\t2", file=f)
print("example.co.uk\t2", file=f)
print("example.net\t2", file=f)
print("example.nl\t2,3", file=f)
# Construct metadata JSON
result = {
"v": 1,
"csv_file": snapshot_path,
"codes_file": codes_path,
}
# Output for Command Source to be informed of updated feed
print("OXFEED_GENERATOR_RESULT: {}".format(json.dumps(result)))
To make the above script available, we need to deploy it in a ConfigMap. This could look as follows:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-cmdsource-configmap
data:
script.py: |
import json, tempfile
# Write feed codes JSON file
codes = {
"v": 1,
"codes": [
{"code": 1, "title": "documentation"},
{"code": 2, "title": "examples"},
{"code": 3, "title": "netherlands"},
]
}
_, codes_path = tempfile.mkstemp()
with open(codes_path, 'w') as f:
json.dump(codes, f)
# Write feed snapshot TSV file
_, snapshot_path = tempfile.mkstemp()
with open(snapshot_path, 'w') as f:
print("doc.powerdns.com\t1", file=f)
print("example.com\t2", file=f)
print("example.co.uk\t2", file=f)
print("example.net\t2", file=f)
print("example.nl\t2,3", file=f)
# Construct metadata JSON
result = {
"v": 1,
"csv_file": snapshot_path,
"codes_file": codes_path,
}
# Output for Command Source to be informed of updated feed
print("OXFEED_GENERATOR_RESULT: {}".format(json.dumps(result)))
We can now use the above ConfigMap in our Controlplane deployment:
Configuration Reference
Instance Sets
Instances of Command Source can be defined under the root node commandSources
. Each instance set should be a key-value pair, with:
- key: Name of the instance set
- value: Dictionary holding the configuration of the instance set
In a minimal configuration you will need to specify at least the name of a configmap holding a script and the name of the script which should be run.
For example:
In the above example Command Source will mount the my-script-configmap
to a local volume and attempt to run the myscript.py
script using Python3 each iteration.
It can also be helpful to have more control over the frequency at which the script is executed. You can do this as follows:
commandSources:
mycmdsource:
scriptConfigMap: my-script-configmap
scriptName: myscript.py
interval: 15s
timeout: 2m
In the above example the script will be executed with 15 second intervals between executions. If an execution is not finished within 2 minutes it will be stopped.
Parameters which can be used to further configure Command Source instances for a specific instance set are shown in the below table.
Parameter | Type | Default | Description |
---|---|---|---|
affinity |
k8s: Affinity |
pod affinity (Kubernetes docs: Affinity and anti-affinity). If unset, a default anti-affinity is applied using antiAffinityPreset to spread pods across nodes |
|
agentLogLevel |
string |
"info" |
Verbosity of logging for the agent container. Available options: "debug" "info" "warn" "error" |
agentLogFormat |
string |
"text" |
Format of logging for the agent container. Available options: "text" "json" |
agentResources |
k8s: Resources |
|
Resources allocated to the agent container if resourceDefaults (global) is true |
antiAffinityPreset |
string |
"preferred" |
pod anti affinity preset. Available options: "preferred" "required" |
containerSecurityContext |
k8s: SecurityContext |
|
SecurityContext applied to each container |
disableSticky |
boolean |
false |
If true , disable the operator-controlled selection of a primary pod to handle all inbound traffic |
executor |
string |
"python3" |
Execution engine for script. Available options: "python3" "sh" |
hostNetwork |
boolean |
false |
Use host networking for pods |
ingress |
k8s: IngressSpec |
{} |
Ingress configuration |
interval |
go: DurationString |
60s |
Interval between script executions. |
logFormat |
string |
"human" |
Format of logging. Available options: "human" "json" |
logLevel |
string |
"info" |
Level of logging. Available options: "debug" "info" "warn" "error" |
nodeSelector |
k8s: NodeSelector |
{} |
Kubernetes pod nodeSelector |
podAnnotations |
k8s: Annotations |
{} |
Annotations to be added to each pod |
podDisruptionBudget |
k8s: PodDisruptionBudgetSpec |
{} |
Spec of PodDisruptionBudget to be applied to deployment |
podLabels |
k8s: Labels |
{} |
Labels to be added to each pod |
podSecurityContext |
k8s: PodSecurityContext |
|
SecurityContext applied to each pod |
readyInterval |
integer |
5 |
How often readiness of the Command Source should be calculated in seconds |
replicas |
integer |
2 |
Default number of replicas in a Deployment |
resources |
k8s: Resources |
|
Resources allocated to the commandsource container if resourceDefaults (global) is true |
scriptConfigMap |
string |
Name of the configmap containing a script | |
scriptName |
string |
Name of the data item within scriptConfigMap which contains the script which should be executed |
|
scriptSecretName |
string |
Name of a Secret which should be mounted to the commandsource container. Items stored in the Secret are available as files (name of item in secret = name of file) in the folder /script_secrets |
|
service |
Service |
|
HTTP Service configuration |
serviceHTTPS |
Service |
|
HTTPS Service configuration Note: Only created when tls.enabled = true |
serviceLabels |
k8s: Labels |
{} |
Labels to be added to each service |
timeout |
go: DurationString |
10m |
Maximum duration of an execution. If it exceeds this duration the attempt will be stopped. |
tls |
Inbound TLS |
|
TLS configuration for inbound HTTPS traffic |
tolerations |
List of k8s: Tolerations |
[] |
Kubernetes pod Tolerations |
topologySpreadConstraints |
List of k8s: TopologySpreadConstraint |
[] |
Kubernetes pod topology spread constraints |
unreadyAfterNoSuccessPeriod |
integer |
0 |
If Command Source has not successfully ran for this many seconds, mark the pod as Unready. Disabled if 0 (default) |
users |
List of string |
["feeduser"] |
List of usernames for which a Secret containing the username and a generated password is created, which is given access to the feed. Note: The generated passwords are stored in base64 encoded form in the secret. Make sure you decode them |
userSecrets |
List of string |
[] |
List of names of Secrets containing username and password items which should act as credentials allowed to access the feed |
webserverHTTPExtra |
List of string |
[] |
Extra lines of NGINX configuration to be injected into the server directive for the HTTP traffic. Should not include a trailing ; . For example:
|
webserverHTTPSExtra |
List of string |
[] |
Extra lines of NGINX configuration to be injected into the server directive for the HTTPS traffic. Should not include a trailing ; . For example:
|
webserverResources |
k8s: Resources |
|
Resources allocated to the webserver container if resourceDefaults (global) is true |
webserverSecretName |
string |
Name of a secret containing username: base64_password pairs to be granted access to the feed. base64_password should be the base64 encoded representation of the actual password.Deprecated: Replaced by userSecrets |
|
webserverUsers |
List of string |
["csuser"] |
List of usernames for which a password should be generated and allowed access to the feed. By default a user named csuser will be created with a randomized password which can be obtained from the corresponding secret. Note: The generated passwords are stored in base64 encoded form in the secret. Make sure you decode them. Deprecated: Replaced by users |
Service Configuration
Parameters to configure the service object for this deployment. For example:
commandSources:
mycmdsource:
scriptConfigMap: my-script-configmap
scriptName: myscript.py
service:
type: LoadBalancer
annotations:
metallb.universe.tf/address-pool: name_of_pool
Parameter | Type | Default | Description |
---|---|---|---|
allocateLoadBalancerNodePorts |
boolean |
true |
If true, services with type LoadBalancer automatically assign NodePorts. Can be set to false if the LoadBalancer provider does not rely on NodePorts |
annotations |
k8s: Annotations |
{} |
Annotations for the service |
clusterIP |
string |
Static cluster IP, must be in the cluster's range of cluster IPs and not in use. Randomly assigned when not specified. | |
clusterIPs |
List of string |
List of static cluster IPs, must be in the cluster's range of cluster IPs and not in use. | |
externalIPs |
List of string |
List of IP addresses for which nodes in the cluster will also accept traffic for this service. These IPs are not managed by Kubernetes and must be user-defined on the cluster's nodes | |
externalTrafficPolicy |
string |
Cluster |
Can be set to Local to let nodes distribute traffic received on one of the externally-facing addresses (NodePort and LoadBalancer ) solely to endpoints on the node itself |
healthCheckNodePort |
integer |
For services with type LoadBalancer and externalTrafficPolicy Local you can configure this value to choose a static port for the NodePort which external systems (LoadBalancer provider mainly) can use to determine which node holds endpoints for this service |
|
internalTrafficPolicy |
string |
Cluster |
Can be set to Local to let nodes distribute traffic received on the ClusterIP solely to endpoints on the node itself |
ipv4 |
boolean |
false |
If true, force the Service to include support for IPv4, ignoring globally configured IP Family settings and/or cluster defaults. If ipv4 is set to true and ipv6 remains false , the result will be an ipv4 -only SingleStack Service. If both are false , global settings and/or cluster defaults are used. If both are true , a PreferDualStack Service is created |
ipv6 |
boolean |
false |
If true, force the Service to include support for IPv6, ignoring globally configured IP Family settings and/or cluster defaults. If ipv6 is set to true and ipv4 remains false , the result will be an ipv6 -only SingleStack Service. If both are false , global settings and/or cluster defaults are used. If both are true , a PreferDualStack Service is created |
labels |
k8s: Labels |
{} |
Labels to be added to the service |
loadBalancerIP |
string |
Deprecated Kubernetes feature, available for backwards compatibility: IP address to attempt to claim for use by this LoadBalancer. Replaced by annotations specific to each LoadBalancer provider |
|
loadBalancerSourceRanges |
List of string |
If supported by the LoadBalancer provider, restrict traffic to this LoadBalancer to these ranges | |
loadBalancerClass |
string |
Used to select a non-default type of LoadBalancer class to ensure the appropriate LoadBalancer provisioner attempt to manage this LoadBalancer service | |
publishNotReadyAddresses |
boolean |
false |
Service is populated with endpoints regardless of readiness state |
sessionAffinity |
string |
None |
Can be set to ClientIP to attempt to maintain session affinity. |
sessionAffinityConfig |
k8s: SessionAffinityConfig |
{} |
Configuration of session affinity |
type |
string |
ClusterIP |
Type of service. Available options: "ClusterIP" "LoadBalancer" "NodePort" |
Inbound TLS
Parameters to configure TLS for inbound traffic. When enabled
is set to true
and a certificate source is configured, an additional Service with the name suffix -https
will be created. An example:
commandSources:
mycmdsource:
scriptConfigMap: my-script-configmap
scriptName: myscript.py
tls:
enabled: true
certSecretName: my-commandsource-certificate
In the above example the certificate present in Secret my-commandsource-certificate
will be attempted to be used to start a TLS-enabled listener.
Parameter | Type | Default | Description |
---|---|---|---|
certSecretName |
string |
Name of a Secret object containing a certificate (must contain the tls.key , tls.crt items) |
|
certManager |
boolean |
false |
Toggle to have a request created for Certmanager to provision a certificate. By default, this will request for a Certificate covering the following: - commandsource-[Name of instance set]-https - commandsource-[Name of instance set]-https.[Namespace] - commandsource-[Name of instance set]-https.[Namespace].svc Additional entries can be configured using extraDNSNames |
enabled |
boolean |
false |
Toggle to enable TLS If set to true , a certSecretName must be set or certManager must be set to true to ensure a valid certificate is available |
extraDNSNames |
List of string |
[] |
List of additional entries to be added to the Certificate requested from Certmanager |
issuerGroup |
string |
"cert-manager.io" |
Group to which issuer specified under issuerKind belongsDefault value is inherited from the global certManager configuration |
issuerKind |
string |
"ClusterIssuer" |
Type of Certmanager issuer to request a Certificate from Default value is inherited from the global certManager configuration |
issuerName |
string |
"" |
Name of the issuer from which to request a Certificate Default value is inherited from the global certManager configuration |
certSpecExtra |
CertificateSpec | {} |
Extra configuration to be injected into the Certmanager Certificate object's spec field.Disallowed options: "secretName" "commonName" "dnsNames" "issuerRef" (These are configured automatically and/or via other options) |
certLabels |
k8s: Labels |
{} |
Extra labels for the Certmanager Certificate object |
certAnnotations |
k8s: Annotations |
{} |
Extra annotations for the Certmanager Certificate object |