Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

24 changes: 0 additions & 24 deletions .github/ISSUE_TEMPLATE/feature_request.md

This file was deleted.

5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ crossplane xpkg build -f package --embed-runtime-image=runtime

## License

This project is licensed under the Apache License 2.0.
Copyright 2026-present Michal Bakalarski and Netclab Contributors

This function was originally created using the
Crossplane function-template-python project.
The project is published under [Apache 2.0 License](LICENSE)
2 changes: 1 addition & 1 deletion example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ hatch run development

```shell
# Then, in another terminal, call it with these example manifests
crossplane render xr.yaml composition.yaml functions.yaml \
crossplane render xr1.yaml composition.yaml functions.yaml \
--required-resources secret.yaml --extra-resources environment.yaml -r
```
4 changes: 2 additions & 2 deletions example/composition.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ metadata:
name: eoscommands
spec:
compositeTypeRef:
apiVersion: eos.netclab.dev/v1alpha1
kind: EosCommand
apiVersion: netclab.dev/v1alpha1
kind: CliConfig
mode: Pipeline
pipeline:
- step: environmentConfigs
Expand Down
6 changes: 3 additions & 3 deletions example/xr1.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: eos.netclab.dev/v1alpha1
kind: EosCommand
apiVersion: netclab.dev/v1alpha1
kind: CliConfig
metadata:
name: eoscommand-2
name: eoscommand-1
spec:
endpoint: ceos01.default.svc.cluster.local
removeContainer: false
Expand Down
6 changes: 3 additions & 3 deletions example/xr2.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: eos.netclab.dev/v1alpha1
kind: EosCommand
apiVersion: netclab.dev/v1alpha1
kind: CliConfig
metadata:
name: eoscommand-1
name: eoscommand-2
spec:
endpoint: ceos01.default.svc.cluster.local
removeContainer: true
Expand Down
36 changes: 21 additions & 15 deletions function/fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,11 @@ async def RunFunction(

rsp = response.to(req)

composite = resource.struct_to_dict(req.observed.composite.resource)
name_prefix = composite.get("metadata").get("name")
composite_spec: dict = composite.get("spec")
fqdn = composite_spec.get("endpoint")
cmds = composite_spec.get("cmds")
remove_container = composite_spec.get("removeContainer")
observed_xr = resource.struct_to_dict(req.observed.composite.resource)
observed_xr_name = observed_xr.get("metadata").get("name")
fqdn = observed_xr["spec"].get("endpoint")
cmds = observed_xr["spec"].get("cmds")
remove_container = observed_xr["spec"].get("removeContainer")

environment = resource.struct_to_dict(
req.context["apiextensions.crossplane.io/environment"]
Expand Down Expand Up @@ -77,7 +76,7 @@ async def RunFunction(
log.info("Generated command paths", count=len(command_paths))

for path in command_paths:
name = name_prefix + "-" + name_from_path(path)
name = name_based_on_path(observed_xr_name, path)

path_log = log.bind(resource=name, path=" | ".join(path))
path_log.debug("Creating resource")
Expand Down Expand Up @@ -108,7 +107,7 @@ async def RunFunction(
"isRemovedCheck": removed_logic,
}

resource_data = construct_resource_request(jsonrpc_ops, jsonrpc_cfg)
resource_data = construct_request_resource(name, jsonrpc_ops, jsonrpc_cfg)

resource.update(
rsp.desired.resources[name],
Expand All @@ -118,6 +117,16 @@ async def RunFunction(
return rsp


def name_based_on_path(observed: str, path: list[str]) -> str:
"""name_based_on_path function."""
joined = "|".join(path)
hashed_path = hashlib.sha256(
joined.encode("utf-8"), usedforsecurity=False
).hexdigest()
full = f"{observed}-{hashed_path}"
return full[:253].rstrip("-")


def toggle_no(cmd: str) -> str:
"""To clarify intent of build_remove_path()."""
return cmd.removeprefix("no ") if cmd.startswith("no ") else f"no {cmd}"
Expand Down Expand Up @@ -180,12 +189,6 @@ def walk_cmds(cmds: dict, path: list[str] | None = None) -> list[list[str]]:
return results


def name_from_path(path: list[str]) -> str:
"""name_from_path function."""
joined = "|".join(path)
return hashlib.sha1(joined.encode(), usedforsecurity=False).hexdigest()[:10]


def get_envs(environment: dict) -> tuple[int, str, bool]:
"""Extract jsonrpc configuration from the environment."""
if not environment:
Expand All @@ -200,11 +203,14 @@ def get_envs(environment: dict) -> tuple[int, str, bool]:
return port, scheme, insecure_skip_tls_verify


def construct_resource_request(ops: dict, config: dict) -> dict:
def construct_request_resource(name: str, ops: dict, config: dict) -> dict:
"""Construct the resource request for the given data."""
return {
"apiVersion": "http.crossplane.io/v1alpha2",
"kind": "Request",
"metadata": {
"name": name,
},
"spec": {
"forProvider": {
"insecureSkipTLSVerify": config["insecureSkipTLSVerify"],
Expand Down
18 changes: 12 additions & 6 deletions tests/test_fn.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ def setUp(self) -> None:
logging.configure(level=logging.Level.DISABLED)

async def test_run_function_generates_request(self) -> None:
"""Generates a valid HTTP Request from an EosCommand."""
"""Generates a valid HTTP Request from an CliConfig."""

# ----------------------------
# Inputs
# ----------------------------

composite = {
"apiVersion": "eos.netclab.dev/v1alpha1",
"kind": "EosCommand",
"apiVersion": "netclab.dev/v1alpha1",
"kind": "CliConfig",
"metadata": {"name": "eoscommand-1"},
"spec": {
"endpoint": "ceos01.default.svc.cluster.local",
Expand Down Expand Up @@ -99,15 +99,21 @@ async def test_run_function_generates_request(self) -> None:
# Resource existence
# ----------------------------

request_name = "eoscommand-1-5cc898bff667884f5d8706ea7c4543e267a567c90662a48ac7efb4964dca9015" # noqa E501

self.assertGreater(
len(resp.desired.resources), 0, "No desired resources generated"
)

self.assertIn(
"eoscommand-1-b1ed383535",
request_name,
resp.desired.resources,
"Expected Request resource not found",
)

resource_msg = resp.desired.resources["eoscommand-1-b1ed383535"].resource
request_resource = resp.desired.resources[request_name].resource

result = MessageToDict(resource_msg)
result = MessageToDict(request_resource)

# ----------------------------
# Top-level assertions
Expand Down