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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,4 @@ crossplane/pythonic/__version__.py
pocs/
pythonic-packages/
tests/protobuf/pytest_pb2*
scripts/.aws-credentials
40 changes: 36 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,39 @@ kind: Function
metadata:
name: function-pythonic
spec:
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
```

### Crossplane V1
When running function-pythonic in Crossplane V1, the `--crossplane-v1` command line
option should be specified. This requires using a Crossplane DeploymentRuntimeConfig.
```yaml
apiVersion: pkg.crossplane.io/v1
kind: Function
metadata:
name: function-pythonic
spec:
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
runtimeConfigRef:
name: function-pythonic
--
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: function-pythonic
spec:
deploymentTemplate:
spec:
selector: {}
template:
spec:
containers:
- name: package-runtime
args:
- --debug
- --crossplane-v1
```

## Composed Resource Dependencies

function-pythonic automatically handles dependencies between composed resources.
Expand Down Expand Up @@ -204,9 +235,10 @@ The BaseComposite class provides the following fields for manipulating the Compo
| self.status | Map | The composite desired and observed status, read from observed if not in desired |
| self.conditions | Conditions | The composite desired and observed conditions, read from observed if not in desired |
| self.results | Results | Returned results applied to the Composite and optionally on the Claim |
| self.connectionSecret | Map | The name, namespace, and resourceName to use when generating the connection secret in Crossplane v2 |
| self.connection | Map | The composite desired connection detials |
| self.connection.observed | Map | The composite observed connection detials |
| self.ready | Boolean | The composite desired ready state |
| self.observed.connection | Map | The composite observed connection detials |

The BaseComposite also provides access to the following Crossplane Function level features:

Expand Down Expand Up @@ -253,7 +285,7 @@ Resource class:
| Resource.usages | Boolean | Generate Crossplane Usages for this resource, default is Composite.autoReady |
| Resource.autoReady | Boolean | Perform auto ready processing on this resource, default is Composite.autoReady |

### Required Resources (AKA Extra Resources)
### Required Resources

Creating and accessing required resources is performed using the `BaseComposite.requireds` field.
`BaseComposite.requireds` is a dictionary of the required resources whose key is the required
Expand Down Expand Up @@ -523,7 +555,7 @@ kind: Function
metadata:
name: function-pythonic
spec:
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.2.1
package: xpkg.upbound.io/crossplane-contrib/function-pythonic:v0.3.0
runtimeConfigRef:
name: function-pythonic
---
Expand Down
153 changes: 153 additions & 0 deletions crossplane/pythonic/auto_ready.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@


def process(composite):
for name, resource in composite.resources:
if resource.observed:
if resource.autoReady or (resource.autoReady is None and composite.autoReady):
if resource.ready is None:
if _checks.get((resource.apiVersion, resource.kind), _check_default).ready(resource):
resource.ready = True


class ConditionReady:
def ready(self, resource):
return bool(resource.conditions.Ready.status)

_checks = {}
_check_default = ConditionReady()

class Check:
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if hasattr(cls, 'apiVersion'):
_checks[(cls.apiVersion, cls.__name__)] = cls()

def ready(self, resource):
raise NotImplementedError()

class AlwaysReady(Check):
def ready(self, resource):
return True


class ClusterRole(AlwaysReady):
apiVersion = 'rbac.authorization.k8s.io/v1'

class ClusterRoleBinding(AlwaysReady):
apiVersion = 'rbac.authorization.k8s.io/v1'

class ConfigMap(AlwaysReady):
apiVersion = 'v1'

class CronJob(Check):
apiVersion = 'batch/v1'
def ready(self, resource):
if resource.observed.spec.suspend and len(resource.observed.spec.suspend):
return True
if not resource.status.lastScheduleTime:
return False
if resource.status.active:
return True
if not resource.status.lastSuccessfulTime:
return False
return str(resource.status.lastSuccessfulTime) >= str(resource.status.lastScheduleTime)

class DaemonSet(Check):
apiVersion = 'apps/v1'
def ready(self, resource):
if not resource.status.desiredNumberScheduled:
return False
scheduled = resource.status.desiredNumberScheduled
return (scheduled == resource.status.numberReady and
scheduled == resource.status.updatedNumberScheduled and
scheduled == resource.status.numberAvailable
)

class Deployment(Check):
apiVersion = 'apps/v1'
def ready(self, resource):
replicas = resource.observed.spec.replicas or 1
if resource.status.updatedReplicas != replicas or resource.status.availableReplicas != replicas:
return False
return bool(resource.conditions.Available.status)

class HorizontalPodAutoscaler(Check):
apiVersion = 'autoscaling/v2'
def ready(self, resource):
for type in ('FailedGetScale', 'FailedUpdateScale', 'FailedGetResourceMetric', 'InvalidSelector'):
if resource.conditions[type].status:
return False
for type in ('ScalingActive', 'ScalingLimited'):
if resource.conditions[type].status:
return True
return False

class Ingress(Check):
apiVersion = 'networking.k8s.io/v1'
def ready(self, resource):
return len(resource.status.loadBalancer.ingress) > 0

class Job(Check):
apiVersion = 'batch/v1'
def ready(self, resource):
for type in ('Failed', 'Suspended'):
if resource.conditions[type].status:
return False
return bool(resource.conditions.Complete.status)

class Namespace(AlwaysReady):
apiVersion = 'v1'

class PersistentVolumeClaim(Check):
apiVersion = 'v1'
def ready(self, resource):
return resource.status.phase == 'Bound'

class Pod(Check):
apiVersion = 'v1'
def ready(self, resource):
if resource.status.phase == 'Succeeded':
return True
if resource.status.phase == 'Running':
if resource.observed.spec.restartPolicy == 'Always':
if resource.conditions.Ready.status:
return True
return False

class ReplicaSet(Check):
apiVersion = 'v1'
def ready(self, resource):
if int(resource.status.observedGeneration) < int(resource.observed.metadata.generation):
return False
if resource.conditions.ReplicaFailure.status:
return False
return int(resource.status.availableReplicas) >= int(resource.observed.spec.replicas or 1)

class Role(AlwaysReady):
apiVersion = 'rbac.authorization.k8s.io/v1'

class RoleBinding(AlwaysReady):
apiVersion = 'rbac.authorization.k8s.io/v1'

class Secret(AlwaysReady):
apiVersion = 'v1'

class Service(Check):
apiVersion = 'v1'
def ready(self, resource):
if resource.observed.spec.type != 'LoadBalancer':
return True
return len(resource.status.loadBalancer.ingress) > 0

class ServiceAccount(AlwaysReady):
apiVersion = 'v1'

class StatefulSet(Check):
apiVersion = 'apps/v1'
def ready(self, resource):
replicas = resource.observed.spec.replicas or 1
return (resource.status.readyReplicas == replicas and
resource.status.currentReplicas == replicas and
resource.status.currentRevision == resource.status.updateRevision
)
5 changes: 5 additions & 0 deletions crossplane/pythonic/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ def add_function_arguments(cls, parser):
action='store_true',
help='Allow oversized protobuf messages',
)
parser.add_argument(
'--crossplane-v1',
action='store_true',
help='Enable Crossplane V1 compatibility mode',
)

def __init__(self, args):
self.args = args
Expand Down
Loading
Loading