Skip to content
Open
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
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
exclude: "^docs/conf.py|^docs/build/"

repos:
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
name: isort (python)

- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
language_version: python3
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"name": "Launch_kit",
"type": "debugpy",
"request": "launch",
"program": "implementing_kit.py",
"program": "components/frontend/main.py",
"console": "integratedTerminal",
"justMyCode": false,
}
Expand Down
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Use an official Python runtime as a parent image
FROM python:3.12-alpine

# Set the working directory in the container
WORKDIR /kit

# Copy only the necessary files to install dependencies
COPY setup.py setup.cfg ./

# Install dependencies
RUN pip install --no-cache-dir .

# Copy the rest of the application code
COPY . .

# Install the local package
RUN pip install --no-cache-dir .
3 changes: 2 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ verify_ssl = true
name = "pypi"

[packages]
kubefox = {file = ".", editable = true, path = "."}
kubefox-sdk = {file = ".", editable = true, path = "."}

[dev-packages]
kubefox-sdk = {file = ".", editable = true, path = ".", extras = ["dev"]}

[requires]
python_version = "3.12"
756 changes: 685 additions & 71 deletions Pipfile.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Auto-generate files from .proto:

Running the python script locally and taking the hello-world creds:
- sudo mkdir -p /var/run/secrets/kubernetes.io/serviceaccount/ && sudo chown ${USER}:${USER} /var/run/secrets/kubernetes.io/serviceaccount/
- kubectl create token -n kubefox-debug hello-world-frontend-976e059 > /var/run/secrets/kubernetes.io/serviceaccount/token
- kubectl create token -n kubefox-debug hello-world-frontend-976e059 > /var/run/secrets/kubernetes.io/serviceaccount/token
- kubectl create token -n kubefox-debug hello-world-frontend-976e059 >/tmp/kubefox/hello-world-token
6 changes: 6 additions & 0 deletions app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# This is only for testing
name: hello-world
title: Hello World
description: A simple App demonstrating the use of KubeFox in python.
environment:
KUBEFOX_LOG_LEVEL: debug
8 changes: 8 additions & 0 deletions components/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This image is built from the Dockerfile at the root of the project
FROM kubefox-sdk:0.0.1

WORKDIR /app
COPY . .

# TODO: Install new dependencies that are required for the component

58 changes: 58 additions & 0 deletions components/frontend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Test running the Python Kit SDK"""

import asyncio
import logging
import random

from opentelemetry import trace

from kit import Kit
from kit.proto.protobuf_msgs_pb2 import Category, Event, EventContext, MatchedEvent

logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(module)s:%(lineno)d - %(levelname)s]: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

tracer = trace.get_tracer(__name__)

# TODO: Extract this out into a "nice to use" interface that exposes the same functionality as the golang client


def return_response(event: MatchedEvent) -> Event:
my_context = EventContext(
platform="debug",
virtual_environment="qa",
app_deployment="hello-world-main",
release_manifest="",
)
target_component = event.event.target
target_component.id = "1234"
target_component.hash = "976e059"
my_event = Event(
context=my_context,
type="io.kubefox.kubefox",
content_type="text/plain; charset=UTF-8",
ttl=event.event.ttl,
source=target_component,
target=event.event.source,
content=b"hello world",
category=Category.RESPONSE,
parent_id=event.event.id,
parent_span=event.event.parent_span,
)
return my_event


async def my_cool_function(event: MatchedEvent) -> Event:
with tracer.start_as_current_span("my_cool_function"):
await asyncio.sleep(random.uniform(0.01, 1))
return return_response(event)


if __name__ == "__main__":
instance = Kit.new()
instance.export = False
instance.route("Path(`/{{.Vars.subPath}}/hello`)", my_cool_function)
asyncio.run(instance.start())
15 changes: 0 additions & 15 deletions implementing_kit.py

This file was deleted.

Empty file added kit/api/__init__.py
Empty file.
31 changes: 31 additions & 0 deletions kit/api/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from dataclasses import dataclass
from typing import Self

from kit.proto.protobuf_msgs_pb2 import Component as ProtoObject
from kit.utils import utils


@dataclass
class Component:
proto_object: ProtoObject

@classmethod
def new_component(cls, typ, app, name, component_hash) -> Self:
return cls(
ProtoObject(
Type=str(typ),
App=utils.clean_name(app),
Name=utils.clean_name(name),
Hash=component_hash,
)
)

@classmethod
def new_target_component(cls, typ, name) -> Self:
return cls(ProtoObject(Type=str(typ), Name=utils.clean_name(name)))

@classmethod
def new_platform_component(cls, typ, name, component_hash) -> Self:
return cls(
ProtoObject(Type=str(typ), Name=utils.clean_name(name), Hash=component_hash)
)
5 changes: 3 additions & 2 deletions vars.py → kit/api/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
import os
import re

# Misc
SECRET_MASK = "••••••"
Expand Down Expand Up @@ -216,7 +216,8 @@ def is_adapter(c):
REGEXP_IMAGE = re.compile(r"^.*:[a-z0-9-]{40}$")
REGEXP_NAME = re.compile(r"^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$")
REGEXP_UUID = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
)

KUBEFOX_HOME = os.getenv("KUBEFOX_HOME", os.path.join("/", "tmp", "kubefox"))

Expand Down
156 changes: 156 additions & 0 deletions kit/api/env_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import re
import string
from dataclasses import dataclass, field
from typing import Dict, List, Optional

from dataclasses_json import LetterCase, dataclass_json


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class EnvVarDefinition:
type: str
required: bool


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class EnvSchema:
vars: Dict[str, EnvVarDefinition] = field(default_factory=dict)
secrets: Dict[str, EnvVarDefinition] = field(default_factory=dict)


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class EnvTemplate:
name: str
template: str
env_schema: EnvSchema = field(default_factory=EnvSchema)
parse_err: Optional[Exception] = None

def __post_init__(self):
resolved = " ".join(self.template.split())
try:
self.tree = string.Template(resolved)
except Exception as e:
self.parse_err = e
return

# Simulate parsing the template and extracting variables
for match in re.finditer(r"\{\{(\w+)\.(\w+)\}\}", self.template):
section, name = match.groups()
if section in ["Vars", "Env"]:
self.env_schema.vars[name] = EnvVarDefinition(type="", required=True)
elif section == "Secrets":
self.env_schema.secrets[name] = EnvVarDefinition(type="", required=True)

def parse_error(self) -> Optional[Exception]:
return self.parse_err

def resolve(self, data: "Data", include_secrets: bool) -> str:
if data is None:
data = Data()

env_var_data = {
"Vars": {k: v for k, v in data.vars.items()},
"Env": {k: v for k, v in data.vars.items()},
"Secrets": (
{k: v for k, v in data.secrets.items()} if include_secrets else {}
),
}

try:
result = self.tree.safe_substitute(env_var_data)
return result.replace("<no value>", "")
except KeyError as e:
raise ValueError(f"Missing key in template: {e}")


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Data:
vars: Dict[str, "Val"] = field(default_factory=dict)
secrets: Dict[str, "Val"] = field(default_factory=dict)


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class ProblemSource:
kind: str
name: str
observed_generation: int
path: str
value: str


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Problem:
type: str
message: str
causes: List[ProblemSource]


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Val:
type: str
value: str

def env_var_type(self) -> str:
return self.type

def array_string(self) -> List[str]:
return self.value.split(",")


@dataclass
class EnvVar:
val: Val

def __str__(self) -> str:
if self.val.type in ["ArrayNumber", "ArrayString"]:
return (
"{"
+ "|".join(f"^{re.escape(s)}$" for s in self.val.array_string())
+ "}"
)
return self.val.value


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class EnvVarSchema:
vars: Dict[str, EnvVarDefinition] = field(default_factory=dict)
secrets: Dict[str, EnvVarDefinition] = field(default_factory=dict)

def validate(
self, typ: str, vars: Dict[str, Val], src: ProblemSource, append_name: bool
) -> List[Problem]:
problems = []
for var_name, var_def in self.vars.items():
val = vars.get(var_name)
if not val and var_def.required:
src_copy = src
if append_name:
src_copy.path = f"{src.path}.{var_name}"
problems.append(
Problem(
type="VarNotFound",
message=f'{typ} "{var_name}" not found but is required.',
causes=[src_copy],
)
)
elif val and var_def.type and val.env_var_type() != var_def.type:
src_copy = src
src_copy.path = f"{src.path}.{var_name}.type"
src_copy.value = var_def.type
problems.append(
Problem(
type="VarWrongType",
message=f'{typ} "{var_name}" has wrong type; wanted "{
var_def.type}" got "{val.env_var_type()}".',
causes=[src_copy],
)
)
return problems
Loading