crossplane-function-pythonic 0.0.9.post0__tar.gz → 0.0.11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/.gitignore +1 -0
  2. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/PKG-INFO +5 -5
  3. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/README.md +4 -4
  4. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/composite.py +53 -6
  5. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/function.py +80 -75
  6. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/main.py +6 -1
  7. crossplane_function_pythonic-0.0.11/crossplane/pythonic/packages.py +148 -0
  8. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/protobuf.py +67 -43
  9. crossplane_function_pythonic-0.0.11/examples/aks-cluster/README.md +32 -0
  10. crossplane_function_pythonic-0.0.11/examples/aks-cluster/aks/kubernetescluster.py +29 -0
  11. crossplane_function_pythonic-0.0.11/examples/aks-cluster/aks/resourcegroup.py +14 -0
  12. crossplane_function_pythonic-0.0.11/examples/aks-cluster/cluster-function-pythonic.yaml +63 -0
  13. crossplane_function_pythonic-0.0.11/examples/aks-cluster/composition.yaml +24 -0
  14. crossplane_function_pythonic-0.0.11/examples/aks-cluster/definition.yaml +43 -0
  15. crossplane_function_pythonic-0.0.11/examples/aks-cluster/functions.yaml +10 -0
  16. crossplane_function_pythonic-0.0.11/examples/aks-cluster/install.sh +13 -0
  17. crossplane_function_pythonic-0.0.11/examples/aks-cluster/kustomization.yaml +20 -0
  18. crossplane_function_pythonic-0.0.11/examples/aks-cluster/providers.yaml +14 -0
  19. crossplane_function_pythonic-0.0.11/examples/aks-cluster/render.sh +9 -0
  20. crossplane_function_pythonic-0.0.11/examples/aks-cluster/xr.yaml +15 -0
  21. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/functions.yaml +1 -1
  22. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/function.yaml +1 -1
  23. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/composition.yaml +2 -2
  24. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/functions.yaml +1 -1
  25. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/pyproject.toml +4 -2
  26. crossplane_function_pythonic-0.0.9.post0/crossplane/pythonic/packages.py +0 -157
  27. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/LICENSE +0 -0
  28. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/crossplane/pythonic/__init__.py +0 -0
  29. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/.dev/functions.yaml +0 -0
  30. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/composition-v2.yaml +0 -0
  31. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/composition.yaml +0 -0
  32. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/definition.yaml +0 -0
  33. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/render-v2.sh +0 -0
  34. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/render.sh +0 -0
  35. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/eks-cluster/xr.yaml +0 -0
  36. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/README.md +0 -0
  37. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/composition.yaml +0 -0
  38. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/definition.yaml +0 -0
  39. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/kustomization.yaml +0 -0
  40. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/runtime.yaml +0 -0
  41. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/vcluster.py +0 -0
  42. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/filing-system/vcluster.yaml +0 -0
  43. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/composition.yaml +0 -0
  44. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/functions.yaml +0 -0
  45. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/render.sh +0 -0
  46. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/xr.yaml +0 -0
  47. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/conditions/xrd.yaml +0 -0
  48. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/composition.yaml +0 -0
  49. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/environmentConfigs.yaml +0 -0
  50. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/functions.yaml +0 -0
  51. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/render.sh +0 -0
  52. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/xr.yaml +0 -0
  53. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/context/xrd.yaml +0 -0
  54. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/composition.yaml +0 -0
  55. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/extraResources.yaml +0 -0
  56. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/functions.yaml +0 -0
  57. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/render.sh +0 -0
  58. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/extra-resources/xr.yaml +0 -0
  59. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/composition.yaml +0 -0
  60. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/functions.yaml +0 -0
  61. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/render.sh +0 -0
  62. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/fromYaml/xr.yaml +0 -0
  63. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/composition.yaml +0 -0
  64. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/functions.yaml +0 -0
  65. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/observed.yaml +0 -0
  66. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/render.sh +0 -0
  67. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getComposedResource/xr.yaml +0 -0
  68. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/composition.yaml +0 -0
  69. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/functions.yaml +0 -0
  70. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/render.sh +0 -0
  71. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getCompositeResource/xr.yaml +0 -0
  72. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/composition.yaml +0 -0
  73. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/functions.yaml +0 -0
  74. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/observed.yaml +0 -0
  75. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/render.sh +0 -0
  76. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/getResourceCondition/xr.yaml +0 -0
  77. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/composition.yaml +0 -0
  78. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/functions.yaml +0 -0
  79. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/render.sh +0 -0
  80. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/include/xr.yaml +0 -0
  81. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/composition.yaml +0 -0
  82. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/functions.yaml +0 -0
  83. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/render.sh +0 -0
  84. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/functions/toYaml/xr.yaml +0 -0
  85. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/composition.yaml +0 -0
  86. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/functions.yaml +0 -0
  87. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/render.sh +0 -0
  88. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/inline/xr.yaml +0 -0
  89. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/composition-real.yaml +0 -0
  90. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/composition-wrapper.yaml +0 -0
  91. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/functions.yaml +0 -0
  92. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/render.sh +0 -0
  93. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/function-go-templating/recursive/xr.yaml +0 -0
  94. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/definition.yaml +0 -0
  95. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/functions.yaml +0 -0
  96. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/render.sh +0 -0
  97. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/get-started-app/xr.yaml +0 -0
  98. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/composition.yaml +0 -0
  99. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/kustomization.yaml +0 -0
  100. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/render.sh +0 -0
  101. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/run-function.sh +0 -0
  102. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/vcluster.py +0 -0
  103. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/xr.yaml +0 -0
  104. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/helm-copy-secret/xrd.yaml +0 -0
  105. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/functions.yaml +0 -0
  106. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/render.sh +0 -0
  107. {crossplane_function_pythonic-0.0.9.post0 → crossplane_function_pythonic-0.0.11}/examples/single-purpose/xr.yaml +0 -0
@@ -214,3 +214,4 @@ __marimo__/
214
214
  crossplane/pythonic/__version__.py
215
215
  pocs/
216
216
  pythonic-packages/
217
+ tests/protobuf/pytest_pb2*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crossplane-function-pythonic
3
- Version: 0.0.9.post0
3
+ Version: 0.0.11
4
4
  Summary: A Python centric Crossplane Function
5
5
  Project-URL: Documentation, https://github.com/fortra/function-pythonic#readme
6
6
  Project-URL: Issues, https://github.com/fortra/function-pythonic/issues
@@ -81,7 +81,7 @@ kind: Function
81
81
  metadata:
82
82
  name: function-pythonic
83
83
  spec:
84
- package: ghcr.io/fortra/function-pythonic:v0.0.9
84
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
85
85
  ```
86
86
  ## Composed Resource Dependencies
87
87
 
@@ -386,11 +386,11 @@ metadata:
386
386
  annotations:
387
387
  render.crossplane.io/runtime: Development
388
388
  spec:
389
- package: ghcr.io/fortra/function-pythonic:v0.0.9
389
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
390
390
  ```
391
391
  In one terminal session, run function-pythonic:
392
392
  ```shell
393
- $ function-pythonic --insecure --debug
393
+ $ function-pythonic --insecure --debug --render-unknowns
394
394
  [2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
395
395
  ```
396
396
  In another terminal session, render the Composite:
@@ -488,7 +488,7 @@ kind: Function
488
488
  metadata:
489
489
  name: function-pythonic
490
490
  spec:
491
- package: ghcr.io/fortra/function-pythonic:v0.0.9
491
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
492
492
  runtimeConfigRef:
493
493
  name: function-pythonic
494
494
  ---
@@ -57,7 +57,7 @@ kind: Function
57
57
  metadata:
58
58
  name: function-pythonic
59
59
  spec:
60
- package: ghcr.io/fortra/function-pythonic:v0.0.9
60
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
61
61
  ```
62
62
  ## Composed Resource Dependencies
63
63
 
@@ -362,11 +362,11 @@ metadata:
362
362
  annotations:
363
363
  render.crossplane.io/runtime: Development
364
364
  spec:
365
- package: ghcr.io/fortra/function-pythonic:v0.0.9
365
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
366
366
  ```
367
367
  In one terminal session, run function-pythonic:
368
368
  ```shell
369
- $ function-pythonic --insecure --debug
369
+ $ function-pythonic --insecure --debug --render-unknowns
370
370
  [2025-08-21 15:32:37.966] grpc._cython.cygrpc [DEBUG ] Using AsyncIOEngine.POLLER as I/O engine
371
371
  ```
372
372
  In another terminal session, render the Composite:
@@ -464,7 +464,7 @@ kind: Function
464
464
  metadata:
465
465
  name: function-pythonic
466
466
  spec:
467
- package: ghcr.io/fortra/function-pythonic:v0.0.9
467
+ package: ghcr.io/fortra/function-pythonic:v0.0.10
468
468
  runtimeConfigRef:
469
469
  name: function-pythonic
470
470
  ---
@@ -1,5 +1,6 @@
1
1
 
2
2
  import datetime
3
+ from google.protobuf.duration_pb2 import Duration
3
4
  from crossplane.function.proto.v1 import run_function_pb2 as fnv1
4
5
 
5
6
  from . import protobuf
@@ -9,8 +10,18 @@ _notset = object()
9
10
 
10
11
 
11
12
  class BaseComposite:
12
- def __init__(self, request, response, logger):
13
+ def __init__(self, request, logger):
13
14
  self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request')
15
+ response = fnv1.RunFunctionResponse(
16
+ meta=fnv1.ResponseMeta(
17
+ tag=request.meta.tag,
18
+ ttl=Duration(
19
+ seconds=60,
20
+ ),
21
+ ),
22
+ desired=request.desired,
23
+ context=request.context,
24
+ )
14
25
  self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response)
15
26
  self.logger = logger
16
27
  self.credentials = Credentials(self.request)
@@ -36,11 +47,23 @@ class BaseComposite:
36
47
 
37
48
  @property
38
49
  def ttl(self):
50
+ if self.response.meta.ttl.nanos:
51
+ return float(self.response.meta.ttl.seconds) + (float(self.response.meta.ttl.nanos) / 1000000000.0)
39
52
  return self.response.meta.ttl.seconds
40
53
 
41
54
  @ttl.setter
42
55
  def ttl(self, ttl):
43
- self.response.meta.ttl.seconds = ttl
56
+ if isinstance(ttl, int):
57
+ self.response.meta.ttl.seconds = ttl
58
+ self.response.meta.ttl.nanos = 0
59
+ elif isinstance(ttl, float):
60
+ self.response.meta.ttl.seconds = int(ttl)
61
+ if ttl.is_integer():
62
+ self.response.meta.ttl.nanos = 0
63
+ else:
64
+ self.response.meta.ttl.nanos = int((ttl - self.response.meta.ttl.seconds) * 1000000000)
65
+ else:
66
+ raise ValueError('ttl must be an int or float')
44
67
 
45
68
  @property
46
69
  def ready(self):
@@ -73,22 +96,46 @@ class Credentials:
73
96
  return self[key]
74
97
 
75
98
  def __getitem__(self, key):
76
- return self._request.credentials[key].credentials_data.data
99
+ return Credential(self._request.credentials[key])
77
100
 
78
101
  def __bool__(self):
79
- return bool(_request.credentials)
102
+ return bool(self._request.credentials)
80
103
 
81
104
  def __len__(self):
82
105
  return len(self._request.credentials)
83
106
 
84
107
  def __contains__(self, key):
85
- return key in _request.credentials
108
+ return key in self._request.credentials
86
109
 
87
110
  def __iter__(self):
88
111
  for key, resource in self._request.credentials:
89
112
  yield key, self[key]
90
113
 
91
114
 
115
+ class Credential:
116
+ def __init__(self, credential):
117
+ self.__dict__['_credential'] = credential
118
+
119
+ def __getattr__(self, key):
120
+ return self[key]
121
+
122
+ def __getitem__(self, key):
123
+ return self._credential.credential_data.data[key]
124
+
125
+ def __bool__(self):
126
+ return bool(self._credential.credential_data.data)
127
+
128
+ def __len__(self):
129
+ return len(self._credential.credential_data.data)
130
+
131
+ def __contains__(self, key):
132
+ return key in self._credential.credential_data.data
133
+
134
+ def __iter__(self):
135
+ for key, resource in self._credential.credential_data.data:
136
+ yield key, self[key]
137
+
138
+
92
139
  class Resources:
93
140
  def __init__(self, composite):
94
141
  self.__dict__['_composite'] = composite
@@ -587,7 +634,7 @@ class Events:
587
634
  def __getitem__(self, key):
588
635
  if key >= len(self._results):
589
636
  return Event()
590
- return Event(self._results[ix])
637
+ return Event(self._results[key])
591
638
 
592
639
  def __iter__(self):
593
640
  for ix in range(len(self._results)):
@@ -1,38 +1,26 @@
1
1
  """A Crossplane composition function."""
2
2
 
3
3
  import asyncio
4
- import base64
5
- import builtins
6
4
  import importlib
7
5
  import inspect
8
6
  import logging
9
7
  import sys
10
8
 
11
9
  import grpc
12
- import crossplane.function.response
13
10
  from crossplane.function.proto.v1 import run_function_pb2 as fnv1
14
11
  from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1
15
12
  from .. import pythonic
16
13
 
17
- builtins.BaseComposite = pythonic.BaseComposite
18
- builtins.append = pythonic.append
19
- builtins.Map = pythonic.Map
20
- builtins.List = pythonic.List
21
- builtins.Unknown = pythonic.Unknown
22
- builtins.Yaml = pythonic.Yaml
23
- builtins.Json = pythonic.Json
24
- builtins.B64Encode = pythonic.B64Encode
25
- builtins.B64Decode = pythonic.B64Decode
26
-
27
14
  logger = logging.getLogger(__name__)
28
15
 
29
16
 
30
17
  class FunctionRunner(grpcv1.FunctionRunnerService):
31
18
  """A FunctionRunner handles gRPC RunFunctionRequests."""
32
19
 
33
- def __init__(self, debug=False):
20
+ def __init__(self, debug=False, renderUnknowns=False):
34
21
  """Create a new FunctionRunner."""
35
22
  self.debug = debug
23
+ self.renderUnknowns = renderUnknowns
36
24
  self.clazzes = {}
37
25
 
38
26
  def invalidate_module(self, module):
@@ -46,9 +34,8 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
46
34
  ) -> fnv1.RunFunctionResponse:
47
35
  try:
48
36
  return await self.run_function(request)
49
- except:
50
- logger.exception('Exception thrown in run fuction')
51
- raise
37
+ except Exception as e:
38
+ return self.fatal(request, logger, 'RunFunction', e)
52
39
 
53
40
  async def run_function(self, request):
54
41
  composite = request.observed.composite.resource
@@ -56,27 +43,22 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
56
43
  name.append(composite['kind'])
57
44
  name.append(composite['metadata']['name'])
58
45
  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
-
65
- response = crossplane.function.response.to(request)
66
46
 
67
47
  if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
68
- if 'composite' not in composite['spec']:
69
- logger.error('Missing spec "composite"')
70
- crossplane.function.response.fatal(response, 'Missing spec "composite"')
71
- return response
48
+ if 'spec' not in composite or 'composite' not in composite['spec']:
49
+ return self.fatal(request, logger, 'Missing spec "composite"')
72
50
  composite = composite['spec']['composite']
73
51
  else:
74
52
  if 'composite' not in request.input:
75
- logger.error('Missing input "composite"')
76
- crossplane.function.response.fatal(response, 'Missing input "composite"')
77
- return response
53
+ return self.fatal(request, logger, 'Missing input "composite"')
78
54
  composite = request.input['composite']
79
55
 
56
+ # Ideally this is something the Function API provides
57
+ if 'step' in request.input:
58
+ step = request.input['step']
59
+ else:
60
+ step = str(hash(composite))
61
+
80
62
  clazz = self.clazzes.get(composite)
81
63
  if not clazz:
82
64
  if '\n' in composite:
@@ -84,81 +66,67 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
84
66
  try:
85
67
  exec(composite, module.__dict__)
86
68
  except Exception as e:
87
- logger.exception('Exec exception')
88
- crossplane.function.response.fatal(response, f"Exec exception: {e}")
89
- return response
69
+ return self.fatal(request, logger, 'Exec', e)
90
70
  for field in dir(module):
91
71
  value = getattr(module, field)
92
- if inspect.isclass(value) and issubclass(value, BaseComposite):
72
+ if inspect.isclass(value) and issubclass(value, pythonic.BaseComposite) and value != pythonic.BaseComposite:
93
73
  if clazz:
94
- logger.error('Composite script has multiple BaseComposite classes')
95
- crossplane.function.response.fatal(response, 'Composite script has multiple BaseComposite classes')
96
- return response
74
+ return self.fatal(request, logger, 'Composite script has multiple BaseComposite classes')
97
75
  clazz = value
98
76
  if not clazz:
99
- logger.error('Composite script does not have have a BaseComposite class')
100
- crossplane.function.response.fatal(response, 'Composite script does have have a BaseComposite class')
101
- return response
77
+ return self.fatal(request, logger, 'Composite script does not have a BaseComposite class')
102
78
  else:
103
79
  composite = composite.rsplit('.', 1)
104
80
  if len(composite) == 1:
105
- logger.error(f"Composite class name does not include module: {composite[0]}")
106
- crossplane.function.response.fatal(response, f"Composite class name does not include module: {composite[0]}")
107
- return response
81
+ return self.fatal(request, logger, f"Composite class name does not include module: {composite[0]}")
108
82
  try:
109
83
  module = importlib.import_module(composite[0])
110
84
  except Exception as e:
111
- logger.error(str(e))
112
- crossplane.function.response.fatal(response, f"Import module exception: {e}")
113
- return response
85
+ return self.fatal(request, logger, 'Import module', e)
114
86
  clazz = getattr(module, composite[1], None)
115
87
  if not clazz:
116
- logger.error(f"{composite[0]} did not define: {composite[1]}")
117
- crossplane.function.response.fatal(response, f"{composite[0]} did not define: {composite[1]}")
118
- return response
88
+ return self.fatal(request, logger, f"{composite[0]} does not define: {composite[1]}")
119
89
  composite = '.'.join(composite)
120
90
  if not inspect.isclass(clazz):
121
- logger.error(f"{composite} is not a class")
122
- crossplane.function.response.fatal(response, f"{composite} is not a class")
123
- return response
124
- if not issubclass(clazz, BaseComposite):
125
- logger.error(f"{composite} is not a subclass of BaseComposite")
126
- crossplane.function.response.fatal(response, f"{composite} is not a subclass of BaseComposite")
127
- return response
91
+ return self.fatal(request, logger, f"{composite} is not a class")
92
+ if not issubclass(clazz, pythonic.BaseComposite):
93
+ return self.fatal(request, logger, f"{composite} is not a subclass of BaseComposite")
128
94
  self.clazzes[composite] = clazz
129
95
 
130
96
  try:
131
- composite = clazz(request, response, logger)
97
+ composite = clazz(request, logger)
132
98
  except Exception as e:
133
- logger.exception('Instatiate exception')
134
- crossplane.function.response.fatal(response, f"Instatiate exception: {e}")
135
- return response
99
+ return self.fatal(request, logger, 'Instantiate', e)
100
+
101
+ step = composite.context._pythonic[step]
102
+ iteration = (step.iteration or 0) + 1
103
+ step.iteration = iteration
104
+ composite.context.iteration = iteration
105
+ logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
136
106
 
137
107
  try:
138
108
  result = composite.compose()
139
109
  if asyncio.iscoroutine(result):
140
110
  await result
141
111
  except Exception as e:
142
- logger.exception('Compose exception')
143
- crossplane.function.response.fatal(response, f"Compose exception: {e}")
144
- return response
112
+ return self.fatal(request, logger, 'Compose', e)
145
113
 
146
114
  requested = []
147
115
  for name, required in composite.requireds:
148
116
  if required.apiVersion and required.kind:
149
- r = Map(apiVersion=required.apiVersion, kind=required.kind)
117
+ r = pythonic.Map(apiVersion=required.apiVersion, kind=required.kind)
150
118
  if required.namespace:
151
119
  r.namespace = required.namespace
152
120
  if required.matchName:
153
121
  r.matchName = required.matchName
154
122
  for key, value in required.matchLabels:
155
123
  r.matchLabels[key] = value
156
- if r != composite.context._requireds[name]:
157
- composite.context._requireds[name] = r
124
+ if r != step.requireds[name]:
125
+ step.requireds[name] = r
158
126
  requested.append(name)
159
127
  if requested:
160
128
  logger.info(f"Requireds requested: {','.join(requested)}")
161
- return response
129
+ return composite.response._message
162
130
 
163
131
  unknownResources = []
164
132
  warningResources = []
@@ -187,6 +155,8 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
187
155
  logger.debug(f'Desired unknown: {destination} = {source}')
188
156
  if resource.observed:
189
157
  resource.desired._patchUnknowns(resource.observed)
158
+ elif self.renderUnknowns:
159
+ resource.desired._renderUnknowns(self.trimFullName)
190
160
  else:
191
161
  del composite.resources[name]
192
162
 
@@ -227,7 +197,29 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
227
197
  resource.ready = True
228
198
 
229
199
  logger.info('Completed compose')
230
- return response
200
+ return composite.response._message
201
+
202
+ def fatal(self, request, logger, message, exception=None):
203
+ if exception:
204
+ message += ' exceptiion'
205
+ logger.exception(message)
206
+ m = str(exception)
207
+ if not m:
208
+ m = exception.__class__.__name__
209
+ message += ': ' + m
210
+ else:
211
+ logger.error(message)
212
+ return fnv1.RunFunctionResponse(
213
+ meta=fnv1.ResponseMeta(
214
+ tag=request.meta.tag,
215
+ ),
216
+ results=[
217
+ fnv1.Result(
218
+ severity=fnv1.SEVERITY_FATAL,
219
+ message=message,
220
+ )
221
+ ]
222
+ )
231
223
 
232
224
  def trimFullName(self, name):
233
225
  name = name.split('.')
@@ -236,10 +228,15 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
236
228
  ('request', 'extra_resources', None, 'items', 'resource'),
237
229
  ('response', 'desired', 'resources', None, 'resource'),
238
230
  ):
239
- if len(values) <= len(name):
240
- for ix, value in enumerate(values):
241
- if value and value != name[ix] and not name[ix].startswith(f"{value}["):
242
- break
231
+ if len(values) < len(name):
232
+ ix = 0
233
+ for iv, value in enumerate(values):
234
+ if value:
235
+ if value != name[ix]:
236
+ if not name[ix].startswith(f"{values[iv]}[") or iv+1 >= len(values) or values[iv+1]:
237
+ break
238
+ continue
239
+ ix += 1
243
240
  else:
244
241
  ix = 0
245
242
  for value in values:
@@ -251,7 +248,6 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
251
248
  del name[ix]
252
249
  else:
253
250
  name[ix] = name[ix][len(value):]
254
- ix += 1
255
251
  else:
256
252
  ix += 1
257
253
  break
@@ -268,4 +264,13 @@ def ordinal(ix):
268
264
 
269
265
 
270
266
  class Module:
271
- pass
267
+ def __init__(self):
268
+ self.BaseComposite = pythonic.BaseComposite
269
+ self.append = pythonic.append
270
+ self.Map = pythonic.Map
271
+ self.List = pythonic.List
272
+ self.Unknown = pythonic.Unknown
273
+ self.Yaml = pythonic.Yaml
274
+ self.Json = pythonic.Json
275
+ self.B64Encode = pythonic.B64Encode
276
+ self.B64Decode = pythonic.B64Decode
@@ -92,6 +92,11 @@ class Main:
92
92
  action='store_true',
93
93
  help='Allow oversized protobuf messages'
94
94
  )
95
+ parser.add_argument(
96
+ '--render-unknowns',
97
+ action='store_true',
98
+ help='Render resources with unknowns, useful during local develomment'
99
+ )
95
100
  args = parser.parse_args()
96
101
  if not args.tls_certs_dir and not args.insecure:
97
102
  print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
@@ -117,7 +122,7 @@ class Main:
117
122
  api_implementation._c_module.SetAllowOversizeProtos(True)
118
123
 
119
124
  grpc.aio.init_grpc_aio()
120
- grpc_runner = function.FunctionRunner(args.debug)
125
+ grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns)
121
126
  grpc_server = grpc.aio.server()
122
127
  grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
123
128
  if args.insecure:
@@ -0,0 +1,148 @@
1
+
2
+ import base64
3
+ import logging
4
+ import pathlib
5
+ import sys
6
+
7
+ import kopf
8
+
9
+
10
+ GRPC_SERVER = None
11
+ GRPC_RUNNER = None
12
+ PACKAGES_DIR = None
13
+ PACKAGE_LABEL = {'function-pythonic.package': kopf.PRESENT}
14
+
15
+
16
+ def operator(grpc_server, grpc_runner, packages_secrets, packages_namespaces, packages_dir):
17
+ logging.getLogger('kopf.objects').setLevel(logging.INFO)
18
+ global GRPC_SERVER, GRPC_RUNNER, PACKAGES_DIR
19
+ GRPC_SERVER = grpc_server
20
+ GRPC_RUNNER = grpc_runner
21
+ PACKAGES_DIR = pathlib.Path(packages_dir).expanduser().resolve()
22
+ sys.path.insert(0, str(PACKAGES_DIR))
23
+ if packages_secrets:
24
+ kopf.on.create('', 'v1', 'secrets', labels=PACKAGE_LABEL)(create)
25
+ kopf.on.resume('', 'v1', 'secrets', labels=PACKAGE_LABEL)(create)
26
+ kopf.on.update('', 'v1', 'secrets', labels=PACKAGE_LABEL)(update)
27
+ kopf.on.delete('', 'v1', 'secrets', labels=PACKAGE_LABEL)(delete)
28
+ return kopf.operator(
29
+ standalone=True,
30
+ clusterwide=not packages_namespaces,
31
+ namespaces=packages_namespaces,
32
+ )
33
+
34
+
35
+ @kopf.on.startup()
36
+ async def startup(settings, **_):
37
+ settings.scanning.disabled = True
38
+
39
+
40
+ @kopf.on.cleanup()
41
+ async def cleanup(**_):
42
+ await GRPC_SERVER.stop(5)
43
+
44
+
45
+ @kopf.on.create('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
46
+ @kopf.on.resume('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
47
+ async def create(body, logger, **_):
48
+ package_dir = get_package_dir(body, logger)
49
+ if package_dir:
50
+ secret = body['kind'] == 'Secret'
51
+ for name, text in body.get('data', {}).items():
52
+ package_file_write(package_dir, name, secret, text, 'Created', logger)
53
+
54
+
55
+ @kopf.on.update('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
56
+ async def update(body, old, logger, **_):
57
+ old_package_dir = get_package_dir(old)
58
+ if old_package_dir:
59
+ old_data = old.get('data', {})
60
+ else:
61
+ old_data = {}
62
+ old_names = set(old_data.keys())
63
+ package_dir = get_package_dir(body, logger)
64
+ if package_dir:
65
+ secret = body['kind'] == 'Secret'
66
+ for name, text in body.get('data', {}).items():
67
+ if package_dir == old_package_dir and text == old_data.get(name, None):
68
+ action = 'Unchanged'
69
+ else:
70
+ action = 'Updated' if package_dir == old_package_dir and name in old_names else 'Created'
71
+ package_file_write(package_dir, name, secret, text, action, logger)
72
+ if package_dir == old_package_dir:
73
+ old_names.discard(name)
74
+ if old_package_dir:
75
+ for name in old_names:
76
+ package_file_unlink(old_package_dir, name, 'Removed', logger)
77
+
78
+
79
+ @kopf.on.delete('', 'v1', 'configmaps', labels=PACKAGE_LABEL)
80
+ async def delete(old, logger, **_):
81
+ package_dir = get_package_dir(old)
82
+ if package_dir:
83
+ for name in old.get('data', {}).keys():
84
+ package_file_unlink(package_dir, name, 'Deleted', logger)
85
+
86
+
87
+ def get_package_dir(body, logger=None):
88
+ package = body.get('metadata', {}).get('labels', {}).get('function-pythonic.package', None)
89
+ if package is None:
90
+ if logger:
91
+ logger.error('function-pythonic.package label is missing')
92
+ return None
93
+ package_dir = PACKAGES_DIR
94
+ if package:
95
+ for segment in package.split('.'):
96
+ if not segment.isidentifier():
97
+ if logger:
98
+ logger.error('Package has invalid package name: %s', package)
99
+ return None
100
+ package_dir = package_dir / segment
101
+ return package_dir
102
+
103
+
104
+ def package_file_write(package_dir, name, secret, text, action, logger):
105
+ package_file = package_dir / name
106
+ if action != 'Unchanged':
107
+ package_file.parent.mkdir(parents=True, exist_ok=True)
108
+ if secret:
109
+ package_file.write_bytes(base64.b64decode(text.encode('utf-8')))
110
+ else:
111
+ package_file.write_text(text)
112
+ module, name = package_file_name(package_file)
113
+ if module:
114
+ if action != 'Unchanged':
115
+ GRPC_RUNNER.invalidate_module(name)
116
+ logger.info(f"{action} module: {name}")
117
+ else:
118
+ logger.info(f"{action} file: {name}")
119
+
120
+
121
+ def package_file_unlink(package_dir, name, action, logger):
122
+ package_file = package_dir / name
123
+ package_file.unlink(missing_ok=True)
124
+ module, name = package_file_name(package_file)
125
+ if module:
126
+ GRPC_RUNNER.invalidate_module(name)
127
+ logger.info(f"{action} module: {name}")
128
+ else:
129
+ logger.info(f"{action} file: {name}")
130
+ package_dir = package_file.parent
131
+ while (
132
+ package_dir.is_relative_to(PACKAGES_DIR)
133
+ and package_dir.is_dir()
134
+ and not list(package_dir.iterdir())
135
+ ):
136
+ package_dir.rmdir()
137
+ module = str(package_dir.relative_to(PACKAGES_DIR)).replace('/', '.')
138
+ if module != '.':
139
+ GRPC_RUNNER.invalidate_module(module)
140
+ logger.info(f"{action} package: {module}")
141
+ package_dir = package_dir.parent
142
+
143
+
144
+ def package_file_name(package_file):
145
+ name = str(package_file.relative_to(PACKAGES_DIR))
146
+ if name.endswith('.py'):
147
+ return True, name[:-3].replace('/', '.')
148
+ return False, name