Skip to content

Commit 8740644

Browse files
author
Patrick J. McNerthney
committed
Rework how self.connection and resource.connection are implemented
and add support for step parameters.
1 parent 8ad70c0 commit 8740644

File tree

10 files changed

+228
-48
lines changed

10 files changed

+228
-48
lines changed

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ The BaseComposite class provides the following fields for manipulating the Compo
203203
| self.spec | Map | The composite observed spec |
204204
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
205205
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
206-
| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired |
207206
| self.events | Events | Returned events against the Composite and optionally on the Claim |
207+
| self.connection | Connection | The composite desired and observed connection detials, read from observed if not in desired |
208208
| self.ready | Boolean | The composite desired ready state |
209209

210210
The BaseComposite also provides access to the following Crossplane Function level features:
@@ -214,6 +214,7 @@ The BaseComposite also provides access to the following Crossplane Function leve
214214
| self.request | Message | Low level direct access to the RunFunctionRequest message |
215215
| self.response | Message | Low level direct access to the RunFunctionResponse message |
216216
| self.logger | Logger | Python logger to log messages to the running function stdout |
217+
| self.parameters | Map | The configured step parameters |
217218
| self.ttl | Integer | Get or set the response TTL, in seconds |
218219
| self.credentials | Credentials | The request credentials |
219220
| self.context | Map | The response context, initialized from the request context |
@@ -552,6 +553,41 @@ Secrets can also be used in an identical manner as ConfigMaps by enabling the
552553
`--packages-secrets` command line option. Secrets permissions need to be
553554
added to the above RBAC configuration.
554555

556+
## Step Parameters
557+
558+
Step specific parameters can be configured to be used by the composite
559+
implementation. This is useful when setting the composite to the python class.
560+
For example:
561+
```yaml
562+
apiVersion: v1
563+
kind: ConfigMap
564+
metadata:
565+
namespace: crossplane-system
566+
name: example-pythonic
567+
labels:
568+
function-pythonic.package: example.pythonic
569+
data:
570+
features.py: |
571+
from crossplane.pythonic import BaseComposite
572+
class GreetingComposite(BaseComposite):
573+
def compose(self):
574+
cm = self.resources.ConfigMap('v1', 'ConfigMap')
575+
cm.data.greeting = f"Hello, {self.parameters.who}!"
576+
```
577+
```yaml
578+
...
579+
- step: pythonic
580+
functionRef:
581+
name: function-pythonic
582+
input:
583+
apiVersion: pythonic.fn.fortra.com/v1alpha1
584+
kind: Composite
585+
parameters:
586+
who: World
587+
composite: example.pythonic.features.GreetingComposite
588+
...
589+
```
590+
555591
## Filing System Packages
556592

557593
Composition Composite implementations can be coded in a stand alone python files

crossplane/pythonic/composite.py

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
class BaseComposite:
13-
def __init__(self, request, logger):
13+
def __init__(self, request, single_use, logger):
1414
self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
1515
response = fnv1.RunFunctionResponse(
1616
meta=fnv1.ResponseMeta(
@@ -24,6 +24,10 @@ def __init__(self, request, logger):
2424
)
2525
self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
2626
self.logger = logger
27+
if single_use:
28+
self.parameters = self.request.observed.composite.resource.spec.parameters
29+
else:
30+
self.parameters = self.request.input.parameters
2731
self.credentials = Credentials(self.request)
2832
self.context = self.response.context
2933
self.environment = self.context['apiextensions.crossplane.io/environment']
@@ -43,7 +47,6 @@ def __init__(self, request, logger):
4347
self.spec = self.observed.spec
4448
self.status = self.desired.status
4549
self.conditions = Conditions(observed, self.response)
46-
self.connection = Connection(observed, desired)
4750
self.events = Events(self.response)
4851

4952
@property
@@ -66,6 +69,16 @@ def ttl(self, ttl):
6669
else:
6770
raise ValueError('ttl must be an int or float')
6871

72+
@property
73+
def connection(self):
74+
return self.response.desired.composite.connection_details
75+
76+
@connection.setter
77+
def connection(self, connection):
78+
self.response.desired.composite.connection_details()
79+
for key, value in connection:
80+
self.response.desired.composite.connection_details[key] = value
81+
6982
@property
7083
def ready(self):
7184
ready = self.desired._parent.ready
@@ -189,7 +202,7 @@ def __init__(self, composite, name):
189202
self.observed = observed.resource
190203
self.desired = desired.resource
191204
self.conditions = Conditions(observed)
192-
self.connection = Connection(observed)
205+
self.connection = observed.connection_details
193206
self.unknownsFatal = None
194207
self.autoReady = None
195208
self.usages = None
@@ -554,38 +567,6 @@ def _find_condition(self, create=False):
554567
return self._conditions._response.conditions.append(condition)
555568

556569

557-
class Connection:
558-
def __init__(self, observed, desired=None):
559-
self.__dict__['_observed'] = observed
560-
self.__dict__['_desired'] = desired
561-
562-
def __bool__(self):
563-
if self._desired is not None and len(self._desired.connection_details) > 0:
564-
return True
565-
if self._observed is not None and len(self._observed.connection_details) > 0:
566-
return True
567-
return False
568-
569-
def __getattr__(self, key):
570-
return self[key]
571-
572-
def __getitem__(self, key):
573-
value = None
574-
if self._desired is not None and key in self._desired.connection_details:
575-
value = self._desired.connection_details[key]
576-
if value is None and key in self._observed.connection_details:
577-
value = self._observed.connection_details[key]
578-
return value
579-
580-
def __setattr__(self, key, value):
581-
self[key] = value
582-
583-
def __setitem__(self, key, value):
584-
if self._desired is None:
585-
raise ValueError('Connection is read only')
586-
self._desired.connection_details[key] = value
587-
588-
589570
class Events:
590571
def __init__(self, response):
591572
self._results = response.results

crossplane/pythonic/function.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ async def run_function(self, request):
4747
if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
4848
if 'spec' not in composite or 'composite' not in composite['spec']:
4949
return self.fatal(request, logger, 'Missing spec "composite"')
50+
single_use = True
5051
composite = composite['spec']['composite']
5152
else:
5253
if 'composite' not in request.input:
5354
return self.fatal(request, logger, 'Missing input "composite"')
55+
single_use = False
5456
composite = request.input['composite']
5557

5658
# Ideally this is something the Function API provides
@@ -94,7 +96,7 @@ async def run_function(self, request):
9496
self.clazzes[composite] = clazz
9597

9698
try:
97-
composite = clazz(request, logger)
99+
composite = clazz(request, single_use, logger)
98100
except Exception as e:
99101
return self.fatal(request, logger, 'Instantiate', e)
100102

crossplane/pythonic/protobuf.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,12 @@ def __setitem__(self, key, message):
336336
message = message._value
337337
elif isinstance(message, Value):
338338
message = message._raw
339-
if self._field.type == self._field.TYPE_BYTES and isinstance(message, str):
340-
message = message.encode('utf-8')
341-
self._messages[key] = message
339+
if message is _Unknown:
340+
self._messages.pop(key, None)
341+
else:
342+
if self._field.type == self._field.TYPE_BYTES and isinstance(message, str):
343+
message = message.encode('utf-8')
344+
self._messages[key] = message
342345
self._cache.pop(key, None)
343346

344347
def __delattr__(self, key):
@@ -477,13 +480,18 @@ def __setitem__(self, key, message):
477480
message = message._value
478481
elif isinstance(message, Value):
479482
message = message._raw
480-
if self._field.type == self._field.TYPE_BYTES and isinstance(message, str):
481-
message = message.encode('utf-8')
482-
if key >= len(self._messages):
483-
self._messages.append(message)
483+
if message is _Unknown:
484+
if key < len(self._messages):
485+
self._messages.pop(key)
486+
self._cache.clear()
484487
else:
485-
self._messages[key] = message
486-
self._cache.pop(key, None)
488+
if self._field.type == self._field.TYPE_BYTES and isinstance(message, str):
489+
message = message.encode('utf-8')
490+
if key >= len(self._messages):
491+
self._messages.append(message)
492+
else:
493+
self._messages[key] = message
494+
self._cache.pop(key, None)
487495

488496
def __delitem__(self, key):
489497
if self._readOnly:

package/composite-definition.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ spec:
2020
spec:
2121
type: object
2222
properties:
23+
parameters:
24+
type: object
25+
x-kubernetes-preserve-unknown-fields: true
2326
composite:
2427
type: string
2528
description: 'A Python module that defines a class with the signature: class Composite(BaseComposite)'

package/input-definition.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ spec:
2929
step:
3030
type: string
3131
description: Optional step name used in logging
32+
parameters:
33+
type: object
34+
x-kubernetes-preserve-unknown-fields: true
3235
composite:
3336
type: string
3437
description: 'A Python module that defines a class with the signature: class Composite(BaseComposite)'

tests/fn_cases/connection.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
request:
2+
observed:
3+
resources:
4+
AccessKey:
5+
connection_details:
6+
username: access-key-username
7+
password: access-key-password
8+
input:
9+
step: pytest
10+
parameters:
11+
suffix: test
12+
composite: |
13+
class Test(BaseComposite):
14+
def compose(self):
15+
user = self.resources.User('iam.aws.crossplane.io/v1beta1', 'User')
16+
user.externalName = 'pmcnerthney-test'
17+
user.spec.forProvider = {}
18+
key = self.resources.AccessKey('iam.aws.crossplane.io/v1beta1', 'AccessKey')
19+
key.spec.forProvider.userName = user.externalName
20+
key.spec.writeConnectionSecretToRef.namespace = 'default'
21+
key.spec.writeConnectionSecretToRef.name = 'accesskey-test'
22+
self.connection.constant = 'constant-test'
23+
self.connection.parameter = self.parameters.suffix
24+
for name, value in key.connection:
25+
self.connection[f"{name}-{self.parameters.suffix}"] = value
26+
self.connection.client_secret = key.connection.client_secret
27+
28+
response:
29+
desired:
30+
composite:
31+
connection_details:
32+
constant: constant-test
33+
parameter: test
34+
password-test: access-key-password
35+
username-test: access-key-username
36+
resources:
37+
User:
38+
resource:
39+
apiVersion: iam.aws.crossplane.io/v1beta1
40+
kind: User
41+
metadata:
42+
annotations:
43+
crossplane.io/external-name: pmcnerthney-test
44+
spec:
45+
forProvider: {}
46+
AccessKey:
47+
resource:
48+
apiVersion: iam.aws.crossplane.io/v1beta1
49+
kind: AccessKey
50+
spec:
51+
forProvider:
52+
userName: pmcnerthney-test
53+
writeConnectionSecretToRef:
54+
namespace: default
55+
name: accesskey-test

tests/fn_cases/instantiate-exception.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ request:
33
step: pytest
44
composite: |
55
class InstantiateException(BaseComposite):
6-
def __init__(self, request, logger):
7-
super(InstantiateException, self).__init__(request, logger)
6+
def __init__(self, request, single_use, logger):
7+
super(InstantiateException, self).__init__(request, single_use, logger)
88
foo = bar
99
1010
response:

tests/fn_cases/parameters.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
request:
2+
input:
3+
step: pytest
4+
parameters:
5+
hello: world
6+
composite: |
7+
class Composite(BaseComposite):
8+
def compose(self):
9+
cm = self.resources.ConfigMap('v1', 'ConfigMap')
10+
cm.data = self.parameters
11+
12+
response:
13+
desired:
14+
resources:
15+
ConfigMap:
16+
resource:
17+
apiVersion: v1
18+
kind: ConfigMap
19+
data:
20+
hello: world
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
request:
2+
observed:
3+
composite:
4+
resource:
5+
metadata:
6+
namespace: default
7+
name: usages-test
8+
resources:
9+
VPC:
10+
resource:
11+
metadata:
12+
name: pytest-0123456
13+
status:
14+
atProvider:
15+
vpcId: vpc-0123456789
16+
SubnetA:
17+
resource:
18+
metadata:
19+
name: pytest-1234567
20+
input:
21+
step: pytest
22+
composite: |
23+
class UsagesComposite(BaseComposite):
24+
def compose(self):
25+
vpc = self.resources.VPC('ec2.aws.crossplane.io/v1beta1', 'VPC')
26+
vpc.spec.forProvider.region = 'us-east-1'
27+
vpc.spec.forProvider.cidrBlock = '10.0.0.0/16'
28+
subnet = self.resources.SubnetA('ec2.aws.crossplane.io/v1beta1', 'Subnet')
29+
subnet.usages = True
30+
subnet.spec.forProvider.region = 'us-east-1'
31+
subnet.spec.forProvider.vpcId = vpc.status.atProvider.vpcId
32+
subnet.spec.forProvider.availabilityZone = 'us-east-1a'
33+
subnet.spec.forProvider.cidrBlock = '10.0.0.0/20'
34+
35+
response:
36+
desired:
37+
resources:
38+
VPC:
39+
resource:
40+
apiVersion: ec2.aws.crossplane.io/v1beta1
41+
kind: VPC
42+
spec:
43+
forProvider:
44+
region: us-east-1
45+
cidrBlock: 10.0.0.0/16
46+
SubnetA:
47+
resource:
48+
apiVersion: ec2.aws.crossplane.io/v1beta1
49+
kind: Subnet
50+
spec:
51+
forProvider:
52+
region: us-east-1
53+
vpcId: vpc-0123456789
54+
availabilityZone: us-east-1a
55+
cidrBlock: 10.0.0.0/20
56+
SubnetA_VPC_pytest-0123456:
57+
resource:
58+
apiVersion: protection.crossplane.io/v1beta1
59+
kind: Usage
60+
spec:
61+
reason: spec.forProvider.vpcId = status.atProvider.vpcId
62+
replayDeletion: true
63+
by:
64+
apiVersion: ec2.aws.crossplane.io/v1beta1
65+
kind: Subnet
66+
resourceRef:
67+
name: pytest-1234567
68+
of:
69+
apiVersion: ec2.aws.crossplane.io/v1beta1
70+
kind: VPC
71+
resourceRef:
72+
name: pytest-0123456

0 commit comments

Comments
 (0)