Skip to content
This repository was archived by the owner on Dec 10, 2025. It is now read-only.

Commit 3dd6a42

Browse files
author
Patrick J. McNerthney
committed
Allow for multiple function-pythonic steps in a single composition.
1 parent 4c459f8 commit 3dd6a42

22 files changed

+108
-90
lines changed

crossplane/pythonic/function.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ async def run_function(self, request):
5656
name.append(composite['kind'])
5757
name.append(composite['metadata']['name'])
5858
logger = logging.getLogger('.'.join(name))
59-
if 'iteration' in request.context:
60-
request.context['iteration'] = request.context['iteration'] + 1
61-
else:
62-
request.context['iteration'] = 1
63-
logger.debug(f"Starting compose, {ordinal(request.context['iteration'])} pass")
64-
6559
response = crossplane.function.response.to(request)
6660

6761
if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
@@ -77,6 +71,12 @@ async def run_function(self, request):
7771
return response
7872
composite = request.input['composite']
7973

74+
# Ideally this is something the Function API provides
75+
if 'step' in request.input:
76+
step = request.input['step']
77+
else:
78+
step = str(hash(composite))
79+
8080
clazz = self.clazzes.get(composite)
8181
if not clazz:
8282
if '\n' in composite:
@@ -134,6 +134,12 @@ async def run_function(self, request):
134134
crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
135135
return response
136136

137+
step = composite.context._pythonic[step]
138+
iteration = (step.iteration or 0) + 1
139+
step.iteration = iteration
140+
composite.context.iteration = iteration
141+
logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
142+
137143
try:
138144
result = composite.compose()
139145
if asyncio.iscoroutine(result):
@@ -153,8 +159,8 @@ async def run_function(self, request):
153159
r.matchName = required.matchName
154160
for key, value in required.matchLabels:
155161
r.matchLabels[key] = value
156-
if r != composite.context._requireds[name]:
157-
composite.context._requireds[name] = r
162+
if r != step.requireds[name]:
163+
step.requireds[name] = r
158164
requested.append(name)
159165
if requested:
160166
logger.info(f"Requireds requested: {','.join(requested)}")

crossplane/pythonic/packages.py

Lines changed: 60 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -46,112 +46,104 @@ async def cleanup(**_):
4646
@kopf.on.create('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
4747
@kopf.on.resume('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
4848
async def create(body, logger, **_):
49-
package_dir, package = get_package_dir(body)
49+
package_dir = get_package_dir(body, logger)
5050
if package_dir:
51-
package_dir.mkdir(parents=True, exist_ok=True)
5251
secret = body['kind'] == 'Secret'
53-
invalidate = False
5452
for name, text in body.get('data', {}).items():
55-
package_file = package_dir / name
56-
if secret:
57-
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
58-
else:
59-
package_file.write_text(text)
60-
if package_file.suffixes == ['.py']:
61-
module = '.'.join(package + [package_file.stem])
62-
GRPC_RUNNER.invalidate_module(module)
63-
logger.info(f"Created module: {module}")
64-
else:
65-
logger.info(f"Created file: {'/'.join(package + [name])}")
53+
package_file_write(package_dir, name, secret, text, 'Created', logger)
6654

6755

6856
@kopf.on.update('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
6957
async def update(body, old, logger, **_):
70-
old_package_dir, old_package = get_package_dir(old)
58+
old_package_dir = get_package_dir(old)
7159
if old_package_dir:
7260
old_data = old.get('data', {})
7361
else:
7462
old_data = {}
7563
old_names = set(old_data.keys())
76-
package_dir, package = get_package_dir(body, logger)
64+
package_dir = get_package_dir(body, logger)
7765
if package_dir:
78-
package_dir.mkdir(parents=True, exist_ok=True)
7966
secret = body['kind'] == 'Secret'
8067
for name, text in body.get('data', {}).items():
81-
package_file = package_dir / name
8268
if package_dir == old_package_dir and text == old_data.get(name, None):
8369
action = 'Unchanged'
8470
else:
85-
if secret:
86-
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
87-
else:
88-
package_file.write_text(text)
8971
action = 'Updated' if package_dir == old_package_dir and name in old_names else 'Created'
90-
if package_file.suffixes == ['.py']:
91-
module = '.'.join(package + [package_file.stem])
92-
if action != 'Unchanged':
93-
GRPC_RUNNER.invalidate_module(module)
94-
logger.info(f"{action} module: {module}")
95-
else:
96-
logger.info(f"{action} file: {'/'.join(package + [name])}")
72+
package_file_write(package_dir, name, secret, text, action, logger)
9773
if package_dir == old_package_dir:
9874
old_names.discard(name)
9975
if old_package_dir:
10076
for name in old_names:
101-
package_file = old_package_dir / name
102-
package_file.unlink(missing_ok=True)
103-
if package_file.suffixes == ['.py']:
104-
module = '.'.join(old_package + [package_file.stem])
105-
GRPC_RUNNER.invalidate_module(module)
106-
logger.info(f"Removed module: {module}")
107-
else:
108-
logger.info(f"Removed file: {'/'.join(old_package + [name])}")
109-
while old_package and old_package_dir.is_dir() and not list(old_package_dir.iterdir()):
110-
old_package_dir.rmdir()
111-
module = '.'.join(old_package)
112-
GRPC_RUNNER.invalidate_module(module)
113-
logger.info(f"Removed package: {module}")
114-
old_package_dir = old_package_dir.parent
115-
old_package.pop()
77+
package_file_unlink(old_package_dir, name, 'Removed', logger)
11678

11779

11880
@kopf.on.delete('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
11981
async def delete(old, logger, **_):
120-
package_dir, package = get_package_dir(old)
82+
package_dir = get_package_dir(old)
12183
if package_dir:
12284
for name in old.get('data', {}).keys():
123-
package_file = package_dir / name
124-
package_file.unlink(missing_ok=True)
125-
if package_file.suffixes == ['.py']:
126-
module = '.'.join(package + [package_file.stem])
127-
GRPC_RUNNER.invalidate_module(module)
128-
logger.info(f"Deleted module: {module}")
129-
else:
130-
logger.info(f"Deleted file: {'/'.join(package + [name])}")
131-
while package and package_dir.is_dir() and not list(package_dir.iterdir()):
132-
package_dir.rmdir()
133-
module = '.'.join(package)
134-
GRPC_RUNNER.invalidate_module(module)
135-
logger.info(f"Deleted package: {module}")
136-
package_dir = package_dir.parent
137-
package.pop()
85+
package_file_unlink(package_dir, name, 'Deleted', logger)
13886

13987

14088
def get_package_dir(body, logger=None):
14189
package = body.get('metadata', {}).get('labels', {}).get('function-pythonic.package', None)
14290
if package is None:
14391
if logger:
14492
logger.error('function-pythonic.package label is missing')
145-
return None, None
93+
return None
14694
package_dir = PACKAGES_DIR
147-
if package == '':
148-
package = []
149-
else:
150-
package = package.split('.')
151-
for segment in package:
95+
if package:
96+
for segment in package.split('.'):
15297
if not segment.isidentifier():
15398
if logger:
15499
logger.error('Package has invalid package name: %s', package)
155-
return None, None
100+
return None
156101
package_dir = package_dir / segment
157-
return package_dir, package
102+
return package_dir
103+
104+
105+
def package_file_write(package_dir, name, secret, text, action, logger):
106+
package_file = package_dir / name
107+
if action != 'Unchanged':
108+
package_file.parent.mkdir(parents=True, exist_ok=True)
109+
if secret:
110+
package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
111+
else:
112+
package_file.write_text(text)
113+
module, name = package_file_name(package_file)
114+
if module:
115+
if action != 'Unchanged':
116+
GRPC_RUNNER.invalidate_module(name)
117+
logger.info(f"{action} module: {name}")
118+
else:
119+
logger.info(f"{action} file: {name}")
120+
121+
122+
def package_file_unlink(package_dir, name, action, logger):
123+
package_file = package_dir / name
124+
package_file.unlink(missing_ok=True)
125+
module, name = package_file_name(package_file)
126+
if module:
127+
GRPC_RUNNER.invalidate_module(name)
128+
logger.info(f"{action} module: {name}")
129+
else:
130+
logger.info(f"{action} file: {name}")
131+
package_dir = package_file.parent
132+
while (
133+
package_dir.is_relative_to(PACKAGES_DIR)
134+
and package_dir.is_dir()
135+
and not list(package_dir.iterdir())
136+
):
137+
package_dir.rmdir()
138+
module = str(package_dir.relative_to(PACKAGES_DIR)).replace('/', '.')
139+
if module != '.':
140+
GRPC_RUNNER.invalidate_module(module)
141+
logger.info(f"{action} package: {module}")
142+
package_dir = package_dir.parent
143+
144+
145+
def package_file_name(package_file):
146+
name = str(package_file.relative_to(PACKAGES_DIR))
147+
if name.endswith('.py'):
148+
return True, name[:-3].replace('/', '.')
149+
return False, name
File renamed without changes.

examples/aks-cluster/cluster-function-pythonic.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ kind: Function
33
metadata:
44
name: function-pythonic
55
spec:
6-
package: ghcr.io/fortra/function-pythonic:v0.0.8
6+
package: ghcr.io/fortra/function-pythonic:v0.0.10
77
runtimeConfigRef:
88
name: function-pythonic
99
---

examples/aks-cluster/composition.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ spec:
1414
input:
1515
apiVersion: pythonic.fn.fortra.com/v1alpha1
1616
kind: Composite
17-
composite: resourcegroup.ResourceGroupComposite
17+
composite: aks.resourcegroup.ResourceGroupComposite
1818
- step: create-aks-cluster
1919
functionRef:
2020
name: function-pythonic
2121
input:
22-
apiVersion: pythonic.fn.fortra.com/v1alpha1
22+
apiVersion: paks.ythonic.fn.fortra.com/v1alpha1
2323
kind: Composite
24-
composite: kubernetescluster.KubernetesClusterComposite
24+
composite: aks.kubernetescluster.KubernetesClusterComposite

examples/aks-cluster/kustomization.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ configMapGenerator:
1010
name: pythonic-aks
1111
options:
1212
labels:
13-
function-pythonic.package: ''
13+
function-pythonic.package: 'aks'
1414
files:
15-
- kubernetescluster.py
16-
- resourcegroup.py
15+
- aks/kubernetescluster.py
16+
- aks/resourcegroup.py
1717

1818
resources:
1919
- composition.yaml

examples/get-started-app/composition.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ spec:
1919
def compose(self):
2020
labels = {'example.crossplane.io/app': self.metadata.name}
2121
22-
d = self.resources.deployment('apps/v1', 'Deployment', self.metadata.namespace)
22+
d = self.resources.deployment('apps/v1', 'Deployment')
2323
d.metadata.labels = labels
2424
d.spec.replicas = 2
2525
d.spec.selector.matchLabels = labels
@@ -29,7 +29,7 @@ spec:
2929
d.spec.template.spec.containers[0].ports[0].containerPort = 80
3030
d.ready = d.conditions.Available.status
3131
32-
s = self.resources.service('v1', 'Service', self.metadata.namespace)
32+
s = self.resources.service('v1', 'Service')
3333
s.metadata.labels = labels
3434
s.spec.selector = labels
3535
s.spec.ports[0].protocol = 'TCP'

tests/fn_cases/buckets.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
request:
22
input:
3+
step: pytest
34
composite: |
45
class BucketComposite(BaseComposite):
56
def compose(self):

tests/fn_cases/clazz.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
request:
22
input:
3+
step: pytest
34
composite: tests.fn_cases.clazz.TestComposite
45

56
response:

0 commit comments

Comments
 (0)