Skip to main content

Preparing Infrahub

Note

This guide assumes you have a running Infrahub instance and the Vidra Operator installed in your Kubernetes cluster. If you haven't set up Infrahub yet, please refer to the Infrahub installation guide.

To use Infrahub, you need to define a schema resembling your resources (for example, we created a Webserver containing Deployment, Service, and Ingress, and another one called VirtualMachine). See the Infrahub schema documentation for more information. An example schema for a Webserver resource is provided at https://infrahub-operator.github.io/vidra/guides/infrahub#example-schema-for-webserver.

This guide will show you how to prepare Infrahub for use with the Vidra Operator using the example of a Webserver resource as example.

Note

All code snippets with example in the title can be changed to your own tailored solution.

Note

There is a Demo Repo with all the necessary resources to get started with Infrahub and Vidra Operator. You can fork the repo and use it as a starting point for your own Infrahub instance.

GraphQL Queries

Vidra Operator uses a GraphQL query to fetch data from Infrahub resources. Below is the query that needs to be added to Infrahub.

Note

You can add GraphQL queries to Infrahub using the Infrahub UI, the Infrahub CLI, or through git integration. For reproducibility, we recommend using git integration, as demonstrated in this guide.

Query ArtifactIDs

This query is necessary because it gathers the IDs of the relevant Artifacts. InfrahubSync CR of Vidra Operator will only download if the checksum changed.

query ArtifactIDs($artifactname: [String]) {
CoreArtifact(name__values: $artifactname) {
edges {
node {
id
storage_id {
id
}
checksum {
value
}
name {
value
}
}
}
}
}

Example Query Webserver Details

This query is used to get Webserver resources and is needed in the transformator later on.

query GetWebserver($webserver: String!) {
KubernetesWebserver(name__value: $webserver) {
edges {
node {
name {
value
}
port {
value
}
containerport {
value
}
replicas {
value
}
image {
value
}
namespace {
value
}
host {
value
}
}
}
}
}

Example Transformator

The transformator is a Python script that transforms the data fetched from Infrahub into Kubernetes manifests. It uses the GraphQL queries defined above to fetch the necessary data and then generates the manifests based on a YAML template stored in the same Git repository. The transformator example below is for the Webserver resource, but it is as generic as possible and can easily be used for other resources, as it searches for the keys obtained from the GraphQL query and creates customizedkeyvalue map and searches and replaces them in the YAML template. For exxample if name is a field in Infrahub and it has a value of my-webserver, the transformator will replace name in the YAML template with my-webserver. The same applies to all other fields in the Infrahub resource.

Note

To get more information about creating your own transformator, see the Infrahub documentation.

from typing import Dict, Any
from infrahub_sdk.transforms import InfrahubTransform
from .helperfunctions import HelperFunctions
from pathlib import Path

""" This public module provides:
- Get information from the GraphQL
- Compare the values with the default YAML templates
"""


class TransformWebserver(InfrahubTransform):
"""Transform data into a YAML string format based on a template."""

query = "GetWebserver"

async def transform(self, data: Dict[str, Any]) -> str:
"""Transform the input data into a string format based on a YAML template.

Replacing values with the matching keys from the data.
"""
currentpath = Path(__file__).resolve()
pathfile = str(currentpath.parents[1]) + "/YAML_Templates/webserver.yaml"
resultstring = ""

try:
with open(pathfile, "r") as yamlfile:
# Filter and extract the relevant keys from the input data
customizedkeyvalue = HelperFunctions.filternesteddict(data)
if not customizedkeyvalue:
raise ValueError("No matching keys found in the input data.")

# Iterate through each line in the YAML template
for line in yamlfile:
if ":" in line:
lineprefix = line.split(":")
lineresult = HelperFunctions.process_line(
"".join(str(element) for element in lineprefix[1:]),
customizedkeyvalue,
)
resultstring += lineprefix[0] + ":" + lineresult
else:
resultstring += line

except FileNotFoundError:
raise FileNotFoundError("YAML template file not found.")
except Exception as e:
raise RuntimeError(f"An error occurred during the transformation: {e}")

return resultstring
from typing import Dict, Any, cast
import re


class HelperFunctions:
"""Helper functions to process nested dictionaries and lines in text."""

singledict: Dict[str, str] = {}

@staticmethod
def filternesteddict(nesteddict: Dict[str, Any], key: str = "") -> Dict[str, str]:
"""Filter nested dictionaries and store the result in a global dictionary."""
for nestedkey, value in nesteddict.items():
# Check if dictionary is nested
if isinstance(value, dict):
HelperFunctions.filternesteddict(value, nestedkey)
continue
if isinstance(value, list) and (
isinstance(value[0], dict) or isinstance(value[0], list)
):
HelperFunctions.filternesteddict(
cast(Dict[str, Any], value[0]), nestedkey
)
continue

# Write the key-value pair to the global single dictionary
HelperFunctions.singledict[key.lower()] = str(value).lower()

return HelperFunctions.singledict

@staticmethod
def match_key_in_line(line: str, key: str) -> bool:
"""Check if a specific key is present in the line (case-insensitive)."""
pattern = rf"\W{re.escape(key)}\W" # Searching for a non-word character (like -), the key word and non-word character.
return bool(re.search(pattern, line, re.IGNORECASE))

@staticmethod
def process_line(line: str, customizedkeyvalue: Dict[str, Any]) -> str:
"""Process each line, replacing matching keys with values from the input data."""
for key, value in customizedkeyvalue.items():
if HelperFunctions.match_key_in_line(line, key):
line = line.replace(key, value)
return line

Example YAML Template

This is an example YAML template for a Webserver resources. It will be used in the transformator; the values specified in Infrahub will dynamically be replaced in the template.

---
apiVersion: v1
kind: Namespace
metadata:
name: ns-namespace

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dep-name
namespace: ns-namespace
labels:
app: l-name
spec:
replicas: replicas
selector:
matchLabels:
app: l-name
template:
metadata:
labels:
app: l-name
spec:
containers:
- name: con-name
image: image
ports:
- containerPort: containerport

---
apiVersion: v1
kind: Service
metadata:
name: svc-name
namespace: ns-namespace
labels:
app: l-name
spec:
ports:
- port: port
name: http
selector:
app: l-name

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ing-name
namespace: ns-namespace
spec:
rules:
- host: host
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-name
port:
number: port

Example .infrahub.yml

This is an example of how the final artifact definition for the Webserver resource looks. It defines the artifact name, parameters, content type, targets, and transformation function.

queries:
- name: GetWebserver
file_path: "GraphQL/GetWebserver.gql"
- name: ArtifactIDs
file_path: "GraphQL/ArtifactIDs.gql"

schemas:
- "Schema/service-schema.yaml"

python_transforms:
- name: TransformWebserver
class_name: TransformWebserver
file_path: "python_transform/transform_webserver.py"

artifact_definitions:
- name: "Webserver_Artifact_Definition"
artifact_name: "Webserver_Manifest"
parameters:
webserver: "name__value"
content_type: "application/yaml"
targets: "g_webserver"
transformation: "TransformWebserver"

Once the artifact definition is created by integrating the git repo with all resources, you can create the Webserver resource in Infrahub and add it to the target group g_webserver. The Vidra Operator will then use the transformator to generate the Kubernetes manifests based on the data fetched from Infrahub.

Example Schema for Webserver

version: "1.0"
generics:
- name: Resource
namespace: Kubernetes
description: Generic Device Data
branch: aware
include_in_menu: false
display_labels:
- name__value
order_by:
- name__value
uniqueness_constraints:
- ["name__value", "namespace__value"]
attributes:
- name: name
kind: Text
description: Name of your Webservice
order_weight: 1
- name: namespace
kind: Text
description: Namespace name - Default ns-namespace
order_weight: 2
- name: description
kind: Text
description: Additional information about the Webservice
optional: true
order_weight: 3
nodes:
- name: Webserver
namespace: Kubernetes
icon: mdi:hand-extended
include_in_menu: true
generate_template: true
inherit_from:
- KubernetesRessource
- CoreArtifactTarget
attributes:
- name: port
kind: Number
description: The port number on which the Service is reachable
optional: false
regex: ^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-9][0-9]{0,3})$ # yamllint disable-line rule:line-length
- name: containerport
kind: Number
description: The port number on which the Container is reachable
optional: false
regex: ^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-9][0-9]{0,3})$ # yamllint disable-line rule:line-length
- name: replicas
kind: Number
description: The number of replicas of the Deployment
optional: false
regex: ^[1-5]$
- name: version
kind: Number
description: The version of the Deployment
optional: true
regex: ^[1-5]$
- name: host
kind: Text
description: URL to the Webserver x.iac-ba.network.garden
read_only: true
optional: false
computed_attribute:
kind: Jinja2
jinja2_template: "{{ name__value }}.iac-ba.network.garden"
- name: image
kind: Dropdown
optional: false
choices:
- name: httpd:latest
description: Image for the Apache Webserver
color: "#7f7fff"
- name: nginx:latest
description: Image for the Nginx Webserver
color: "#aeeeee"
- name: marcincuber/2048-game
description: Image for classic 2048 game
color: "#008000"
- name: public.ecr.aws/pahudnet/nyancat-docker-image
description: Image for Nyan Cat Docker image
color: "#FFFF00"