diff --git a/README.md b/README.md index 44798f6..bd4ce4b 100644 --- a/README.md +++ b/README.md @@ -56,17 +56,18 @@ spec: package: ghcr.io/fortra/function-pythonic:v0.0.3 ``` -## Managed Resource Dependencies +## Composed Resource Dependencies -function-pythonic automatically handles dependencies between managed resources. +function-pythonic automatically handles dependencies between composed resources. Just compose everything as if it is immediately created and the framework will delay the creation of any resources which depend on other resources which do not exist yet. In other words, it accomplishes what [function-sequencer](https://github.com/crossplane-contrib/function-sequencer) provides, but it automatically detects the dependencies. -If a resource has been composed and a dependency no longer exists due to some unexpected -condition, the observed value for that field will automatically be used. +If a resource has been created and a dependency no longer exists due to some unexpected +condition, the composition will be terminated or the observed value for that field will +be used, depending on the `unknownsFatal` settings. Take the following example: ```yaml @@ -83,9 +84,13 @@ subnet.spec.forProvider.cidrBlock = '10.0.0.0/20' If the Subnet does not yet exist, the framework will detect if the vpcId set in the Subnet is unknown, and will delay the creation of the subnet. -Once the Subnet has been created, if for some mysterious reason the vpcId passed -to the Subnet is unknown, the framework will automatically use the vpcId in the -observed Subnet. +Once the Subnet has been created, if for some unexpected reason the vpcId passed +to the Subnet is unknown, the framework will detect it and either terminate +the Composite composition or use the vpcId in the observed Subnet. The default +action taken is to fast fail by terminating the composition. This can be +overridden for all composed resource by setting the Composite `self.unknownsFatal` field +to False, or at the individual composed resource level by setting the +`Resource.unknownsFatal` field to False. ## Pythonic access of Protobuf Messages @@ -186,37 +191,39 @@ The BaseComposite also provides access to the following Crossplane Function leve | self.response | Low level direct access to the RunFunctionResponse message | | self.logger | Python logger to log messages to the running function stdout | | self.ttl | Get or set the response TTL, in seconds | -| self.autoReady | Perform auto ready processing after the compose method returns, default True | | self.credentials | The request credentials | | self.context | The response context, initialized from the request context | | self.environment | The response environment, initialized from the request context environment | | self.requireds | Request and read additional local Kubernetes resources | -| self.resources | Define and process managed resources | +| self.resources | Define and process composed resources | | self.results | Returned results on the Composite and optionally on the Claim | +| self.unknownsFatal | Terminate the composition if already created resources are assigned unknown values, default True | +| self.autoReady | Perform auto ready processing after the compose method returns, default True | -### Managed Resources +### Composed Resources -Creating and accessing managed resources is performed using the `BaseComposite.resources` field. -`BaseComposite.resources` is a dictionary of the managed resources whose key is the composition +Creating and accessing composed resources is performed using the `BaseComposite.resources` field. +`BaseComposite.resources` is a dictionary of the composed resources whose key is the composition resource name. The value returned when getting a resource from BaseComposite is the following Resource class: | Field | Description | | ----- | ----------- | | Resource(apiVersion,kind,namespace,name) | Reset the resource and set the optional parameters | -| Resource.name | The composition resource name of the managed resource | -| Resource.observed | Low level direct access to the observed managed resource | -| Resource.desired | Low level direct access to the desired managed resource | -| Resource.apiVersion | The managed resource apiVersion | -| Resource.kind | The managed resource kind | -| Resource.externalName | The managed resource external name | -| Resource.metadata | The managed resource desired metadata | +| Resource.name | The composition resource name of the composed resource | +| Resource.observed | Low level direct access to the observed composed resource | +| Resource.desired | Low level direct access to the desired composed resource | +| Resource.apiVersion | The composed resource apiVersion | +| Resource.kind | The composed resource kind | +| Resource.externalName | The composed resource external name | +| Resource.metadata | The composed resource desired metadata | | Resource.spec | The resource spec | | Resource.data | The resource data | | Resource.status | The resource status | | Resource.conditions | The resource conditions | | Resource.connection | The resource connection details | | Resource.ready | The resource ready state | +| Resource.unknownsFatal | Terminate the composition if this resource has been created and is assigned unknown values, default is Composite.unknownsFatal | ### Required Resources (AKA Extra Resources) diff --git a/crossplane/pythonic/composite.py b/crossplane/pythonic/composite.py index 99d7e6e..ee60ee1 100644 --- a/crossplane/pythonic/composite.py +++ b/crossplane/pythonic/composite.py @@ -10,8 +10,8 @@ class BaseComposite: def __init__(self, request, response, logger): - self.request = protobuf.Message(None, None, request.DESCRIPTOR, request, 'Function Request') - self.response = protobuf.Message(None, None, response.DESCRIPTOR, response) + self.request = protobuf.Message(None, 'request', request.DESCRIPTOR, request, 'Function Request') + self.response = protobuf.Message(None, 'response', response.DESCRIPTOR, response) self.logger = logger self.autoReady = True self.credentials = Credentials(self.request) @@ -20,6 +20,7 @@ def __init__(self, request, response, logger): self.requireds = Requireds(self) self.resources = Resources(self) self.results = Results(self.response) + self.unknownsFatal = True observed = self.request.observed.composite desired = self.response.desired.composite @@ -134,6 +135,7 @@ def __init__(self, composite, name): self.desired = desired.resource self.conditions = Conditions(observed) self.connection = Connection(observed) + self.unknownsFatal = None def __call__(self, apiVersion=_notset, kind=_notset, namespace=_notset, name=_notset): self.desired() @@ -333,14 +335,30 @@ class Results: def __init__(self, response): self._results = response.results - def __call__(self, message=_notset, fatal=_notset, warning=_notset, reaoson=_notset, claim=_notset): - result = Result(self._results.add()) - if fatal != _notset: - result.fatal = fatal - elif warning != _notset: - result.warning = warning - if message != _notset: - result.message = message + def info(self, message, reason=_notset, claim=_notset): + result = Result(self._results.append()) + result.info = True + result.message = message + if reason != _notset: + result.reason = reason + if claim != _notset: + result.claim = claim + return result + + def warning(self, message, reason=_notset, claim=_notset): + result = Result(self._results.append()) + result.warning = True + result.message = message + if reason != _notset: + result.reason = reason + if claim != _notset: + result.claim = claim + return result + + def fatal(self, message, reason=_notset, claim=_notset): + result = Result(self._results.append()) + result.fatal = True + result.message = message if reason != _notset: result.reason = reason if claim != _notset: @@ -348,7 +366,7 @@ def __call__(self, message=_notset, fatal=_notset, warning=_notset, reaoson=_not return result def __bool__(self): - return len(self._results) > 0 + return len(self) > 0 def __len__(self): len(self._results) @@ -364,35 +382,47 @@ def __iter__(self): class Result: - def __init(self, result=None): + def __init__(self, result=None): self._result = result def __bool__(self): return self._result is not None @property - def fatal(self): - return bool(self) and self._result == fnv1.Severity.SEVERITY_FATAL + def info(self): + return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_NORMAL - @fatal.setter - def fatal(self, fatal): + @info.setter + def info(self, info): if bool(self): - if fatal: - self._result = fnv1.Severity.SEVERITY_FATAL + if info: + self._result.severity = fnv1.Severity.SEVERITY_NORMAL else: - self._result = fnv1.Severity.SEVERITY_NORMAL + self._result.severity = fnv1.Severity.SEVERITY_UNSPECIFIED @property def warning(self): - return bool(self) and self._result == fnv1.Severity.SEVERITY_WARNING + return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_WARNING @warning.setter def warning(self, warning): if bool(self): if warning: - self._result = fnv1.Severity.SEVERITY_WARNING + self._result.severity = fnv1.Severity.SEVERITY_WARNING + else: + self._result.severity = fnv1.Severity.SEVERITY_NORMAL + + @property + def fatal(self): + return bool(self) and self._result.severity == fnv1.Severity.SEVERITY_FATAL + + @fatal.setter + def fatal(self, fatal): + if bool(self): + if fatal: + self._result.severity = fnv1.Severity.SEVERITY_FATAL else: - self._result = fnv1.Severity.SEVERITY_NORMAL + self._result.severity = fnv1.Severity.SEVERITY_NORMAL @property def message(self): diff --git a/crossplane/pythonic/function.py b/crossplane/pythonic/function.py index a1529b2..e5e6945 100644 --- a/crossplane/pythonic/function.py +++ b/crossplane/pythonic/function.py @@ -26,8 +26,9 @@ class FunctionRunner(grpcv1.FunctionRunnerService): """A FunctionRunner handles gRPC RunFunctionRequests.""" - def __init__(self): + def __init__(self, debug=False): """Create a new FunctionRunner.""" + self.debug = debug self.logger = crossplane.function.logging.get_logger() self.clazzes = {} @@ -117,12 +118,57 @@ async def RunFunction( logger.exception('Compose exception') return response - for name, resource in [entry for entry in composite.resources]: - if resource.desired._hasUnknowns: + unknownResources = [] + warningResources = [] + fatalResources = [] + for name, resource in sorted(entry for entry in composite.resources): + unknowns = resource.desired._getUnknowns + if unknowns: + unknownResources.append(name) + warning = False + fatal = False + if resource.observed: + warningResources.append(name) + warning = True + if resource.unknownsFatal: + fatalResources.append(name) + fatal = True + elif resource.unknownsFatal is None and composite.unknownsFatal: + fatalResources.append(name) + fatal = True + if self.debug: + for destination, source in sorted(unknowns.items()): + destination = self._trimFullName('response', 'desired', destination) + source = self._trimFullName('request', 'observed', source) + if fatal: + logger.error('Observed unknown assignment', destination=destination, source=source) + elif warning: + logger.warning('Observed unknown assignment', destination=destination, source=source) + else: + logger.debug('New unknown assignment', destination=destination, source=source) if resource.observed: resource.desired._patchUnknowns(resource.observed) else: del composite.resources[name] + if fatalResources: + if not self.debug: + logger.error('Observed Resources with unknown assignments', resources=fatalResources) + message = f"Observed Resources with unknown assignments: {', '.join(fatalResources)}" + composite.conditions.Unknowns(False, 'FatalUnknowns', message) + composite.results.fatal(message, 'FatalUnknowns') + return response + if warningResources: + if not self.debug: + logger.warning('Observed Resources with unknown assignments', resources=fatalResources) + message = f"Observed Resources with unknown assignments: {', '.join(warningResources)}" + composite.conditions.Unknowns(False, 'ObservedUnknowns', message) + composite.results.warning(message, 'ObservedUnknowns') + elif unknownResources: + if not self.debug: + logger.info('New Resources with unknown assignments', resources=unknownResources) + message = f"New Resources with unknown assignments: {', '.join(unknownResources)}" + composite.conditions.Unknowns(False, 'NewUnknowns', message) + composite.results.info(message, 'NewUnknowns') if composite.autoReady: for name, resource in composite.resources: @@ -133,6 +179,16 @@ async def RunFunction( logger.debug('Returning') return response + def _trimFullName(self, message, state, name): + name = name.split('.') + ix = 0 + for value in (message, state, 'resources', None, 'resource'): + if value and ix < len(value) and name[ix] == value: + del name[ix] + else: + ix += 1 + return '.'.join(name) + class Module: pass diff --git a/crossplane/pythonic/main.py b/crossplane/pythonic/main.py index c32bbe8..3ba6c5c 100644 --- a/crossplane/pythonic/main.py +++ b/crossplane/pythonic/main.py @@ -66,7 +66,7 @@ def main(): logging.configure(logging.Level.DEBUG if args.debug else logging.Level.INFO) runtime.serve( - function.FunctionRunner(), + function.FunctionRunner(args.debug), args.address, creds=runtime.load_credentials(args.tls_certs_dir), insecure=args.insecure, diff --git a/crossplane/pythonic/protobuf.py b/crossplane/pythonic/protobuf.py index 212f1b2..07d83df 100644 --- a/crossplane/pythonic/protobuf.py +++ b/crossplane/pythonic/protobuf.py @@ -131,14 +131,20 @@ def __str__(self): def __format__(self, spec='yaml'): return _formatObject(self, spec) - @property - def _fullName(self): + def _fullName(self, key=None): if self._key is not None: if self._parent is not None: - parent = self._parent._fullName - if parent: - return f"{parent}.{self._key}" - return self._key + name = self._parent._fullName(self._key) + else: + name = str(self._key) + if key is not None: + if key.isidentifier(): + name += f".{key}" + else: + name += f"['{key}']" + return name + if key is not None: + return str(key) return '' def _create_child(self, key, type=None): @@ -272,14 +278,20 @@ def __str__(self): def __format__(self, spec='yaml'): return _formatObject(self, spec) - @property - def _fullName(self): + def _fullName(self, key=None): if self._key is not None: if self._parent is not None: - parent = self._parent._fullName - if parent: - return f"{parent}.{self._key}" - return self._key + name = self._parent._fullName(self._key) + else: + name = str(self._key) + if key is not None: + if key.isidentifier(): + name += f".{key}" + else: + name += f"['{key}']" + return name + if key is not None: + return str(key) return '' def _create_child(self, key, type=None): @@ -392,14 +404,17 @@ def __str__(self): def __format__(self, spec='yaml'): return _formatObject(self, spec) - @property - def _fullName(self): + def _fullName(self, key=None): if self._key is not None: if self._parent is not None: - parent = self._parent._fullName - if parent: - return f"{parent}.{self._key}" - return self._key + name = self._parent._fullName(self._key) + else: + name = str(self._key) + if key is not None: + name += f"[{key}]" + return name + if key is not None: + return str(key) return '' def _create_child(self, key, type=None): @@ -471,7 +486,7 @@ def __init__(self, parent, key, values, type, readOnly=None): self.__dict__['_values'] = values self.__dict__['_type'] = type self.__dict__['_readOnly'] = readOnly - self.__dict__['_unknowns'] = set() + self.__dict__['_unknowns'] = {} self.__dict__['_cache'] = {} def __getattr__(self, key): @@ -481,13 +496,11 @@ def __getitem__(self, key): if key in self._cache: return self._cache[key] if key in self._unknowns: - value = Values(self, key, None, self.Type.UNKNOWN, self._readOnly) - self._cache[key] = value - return value + return self._unknowns[key] if isinstance(key, str): if not self._isMap: if not self._isUnknown: - raise ValueError('Invalid key, must be a str for maps') + raise ValueError(f"Invalid key, must be a str for maps: {key}") self.__dict__['_type'] = self.Type.MAP if self._values is None or key not in self._values: struct_value = None @@ -496,7 +509,7 @@ def __getitem__(self, key): elif isinstance(key, int): if not self._isList: if not self._isUnknown: - raise ValueError('Invalid key, must be an int for lists') + raise ValueError(f"Invalid key, must be an int for lists: {key}") self.__dict__['_type'] = self.Type.LIST if self._values is None or key >= len(self._values): struct_value = None @@ -548,12 +561,12 @@ def __contains__(self, item): def __iter__(self): if self._values is not None: if self._isMap: - for key in sorted(set(self._values) | self._unknowns): + for key in sorted(set(self._values) | set(self._unknowns.keys())): yield key, self[key] elif self._isList: for ix in range(len(self._values)): yield self[ix] - for ix in sorted(self._unknowns): + for ix in sorted(self._unknowns.keys()): if ix >= len(self._values): yield self[ix] @@ -594,14 +607,31 @@ def __str__(self): def __format__(self, spec='yaml'): return _formatObject(self, spec) - @property - def _fullName(self): + def _fullName(self, key=None): if self._key is not None: if self._parent is not None: - parent = self._parent._fullName - if parent: - return f"{parent}.{self._key}" - return str(self._key) + name = self._parent._fullName(self._key) + else: + name = str(self._key) + if key is not None: + if self._isMap: + if key.isidentifier(): + name += f".{key}" + else: + name += f"['{key}']" + elif self._isList: + name += f"[{key}]" + else: + if isinstance(key, int): + name += f"[{key}]" + else: + if key.isidentifier(): + name += f".{key}" + else: + name += f"['{key}']" + return name + if key is not None: + return str(key) return '' def _create_child(self, key, type): @@ -707,7 +737,7 @@ def __setitem__(self, key, value): else: raise ValueError('Unexpected key type') self._cache.pop(key, None) - self._unknowns.discard(key) + self._unknowns.pop(key, None) if isinstance(value, ProtobufValue): value = value._protobuf_value if value is None: @@ -732,7 +762,7 @@ def __setitem__(self, key, value): values[key].list_value.Clear() self[key](*[v for v in value]) else: - self._unknowns.add(key) + self._unknowns[key] = value if self._isMap: if key in values: del values[key] @@ -761,7 +791,7 @@ def __delitem__(self, key): if key in self._values: del self._values[key] self._cache.pop(key, None) - self._unknowns.discard(key) + self._unknowns.pop(key, None) elif isinstance(key, int): if not self._isList: if not self._isUnknown: @@ -771,12 +801,12 @@ def __delitem__(self, key): if key < len(self._values): del self._values[key] self._cache.pop(key, None) - self._unknowns.discard(key) - for ix in sorted(self._unknowns): + self._unknowns.pop(key, None) + for ix in sorted(self._unknowns.keys()): if ix > key: self._cache.pop(ix, None) - self._unknowns.add(ix - 1) - self._unknowns.disacard(ix) + self._unknowns[ix - 1] = self._unknowns[ix] + del self._unknowns[ix] for ix in reversed(range(len(self._values))): if ix not in self._unknowns: break @@ -797,21 +827,22 @@ def _isList(self): return self._type == self.Type.LIST @property - def _hasUnknowns(self): - if self._unknowns: - return True + def _getUnknowns(self): + unknowns = {} + for key, unknown in self._unknowns.items(): + unknowns[self._fullName(key)] = unknown._fullName() if self._isMap: for key, value in self: - if isinstance(value, Values) and value._hasUnknowns: - return True + if isinstance(value, Values): + unknowns.update(value._getUnknowns) elif self._isList: for value in self: - if isinstance(value, Values) and value._hasUnknowns: - return True - return False + if isinstance(value, Values): + unknowns.update(value._getUnknowns) + return unknowns def _patchUnknowns(self, patches): - for key in [key for key in self._unknowns]: + for key in [key for key in self._unknowns.keys()]: self[key] = patches[key] if self._isMap: for key, value in self: diff --git a/pyproject.toml b/pyproject.toml index e30511e..0a6d54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,8 @@ type = "virtual" path = ".venv-default" dependencies = ["ipython==9.1.0"] [tool.hatch.envs.default.scripts] -development = "python crossplane/pythonic/main.py --insecure --debug" +development = "python -m crossplane.pythonic.main --insecure --debug" +production = "python -m crossplane.pythonic.main --insecure" [tool.hatch.envs.lint] type = "virtual" diff --git a/tests/fn_cases/unknowns-fatal.yaml b/tests/fn_cases/unknowns-fatal.yaml new file mode 100644 index 0000000..fbc3aaf --- /dev/null +++ b/tests/fn_cases/unknowns-fatal.yaml @@ -0,0 +1,72 @@ +request: + observed: + composite: + resource: + metadata: + name: my-app + spec: + image: nginx + resources: + flexServer: + resource: + status: + conditions: + - type: Ready + reason: Creating + status: 'False' + lastTransitionTime: '2023-11-03T09:07:31Z' + flexServerConfig: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServerConfiguration + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + serverId: abcdef + input: + composite: | + apiVersion = 'dbforpostgresql.azure.upbound.io/v1beta1' + class Composite(BaseComposite): + def compose(self): + self.ttl = (5 * 60) + 30 + s = self.resources.flexServer(apiVersion, 'FlexibleServer') + s.spec.providerConfigRef.name = 'my-provider-cfg' + s.spec.forProvider.storageMb = 32768 + c = self.resources.flexServerConfig(apiVersion, 'FlexibleServerConfiguration') + c.spec.providerConfigRef.name = 'my-provider-cfg' + c.spec.forProvider.serverId = s.status.atProvider.id + +response: + meta: + ttl: + seconds: 330 + desired: + resources: + flexServer: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServer + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + storageMb: 32768 + flexServerConfig: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServerConfiguration + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + serverId: abcdef + conditions: + - type: Unknowns + status: 3 + reason: FatalUnknowns + message: 'Observed Resources with unknown assignments: flexServerConfig' + results: + - message: 'Observed Resources with unknown assignments: flexServerConfig' + reason: FatalUnknowns + severity: 1 diff --git a/tests/fn_cases/unknowns-info.yaml b/tests/fn_cases/unknowns-info.yaml new file mode 100644 index 0000000..7214bca --- /dev/null +++ b/tests/fn_cases/unknowns-info.yaml @@ -0,0 +1,55 @@ +request: + observed: + composite: + resource: + metadata: + name: my-app + spec: + image: nginx + resources: + flexServer: + resource: + status: + conditions: + - type: Ready + reason: Creating + status: 'False' + lastTransitionTime: '2023-11-03T09:07:31Z' + input: + composite: | + apiVersion = 'dbforpostgresql.azure.upbound.io/v1beta1' + class Composite(BaseComposite): + def compose(self): + self.unknownsFatal = False + self.ttl = (5 * 60) + 30 + s = self.resources.flexServer(apiVersion, 'FlexibleServer') + s.spec.providerConfigRef.name = 'my-provider-cfg' + s.spec.forProvider.storageMb = 32768 + c = self.resources.flexServerConfig(apiVersion, 'FlexibleServerConfiguration') + c.spec.providerConfigRef.name = 'my-provider-cfg' + c.spec.forProvider.serverId = s.status.atProvider.id + +response: + meta: + ttl: + seconds: 330 + desired: + resources: + flexServer: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServer + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + storageMb: 32768 + conditions: + - type: Unknowns + status: 3 + reason: NewUnknowns + message: 'New Resources with unknown assignments: flexServerConfig' + results: + - message: 'New Resources with unknown assignments: flexServerConfig' + reason: NewUnknowns + severity: 3 diff --git a/tests/fn_cases/unknowns-warning.yaml b/tests/fn_cases/unknowns-warning.yaml new file mode 100644 index 0000000..e770ba5 --- /dev/null +++ b/tests/fn_cases/unknowns-warning.yaml @@ -0,0 +1,73 @@ +request: + observed: + composite: + resource: + metadata: + name: my-app + spec: + image: nginx + resources: + flexServer: + resource: + status: + conditions: + - type: Ready + reason: Creating + status: 'False' + lastTransitionTime: '2023-11-03T09:07:31Z' + flexServerConfig: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServerConfiguration + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + serverId: abcdef + input: + composite: | + apiVersion = 'dbforpostgresql.azure.upbound.io/v1beta1' + class Composite(BaseComposite): + def compose(self): + self.unknownsFatal = False + self.ttl = (5 * 60) + 30 + s = self.resources.flexServer(apiVersion, 'FlexibleServer') + s.spec.providerConfigRef.name = 'my-provider-cfg' + s.spec.forProvider.storageMb = 32768 + c = self.resources.flexServerConfig(apiVersion, 'FlexibleServerConfiguration') + c.spec.providerConfigRef.name = 'my-provider-cfg' + c.spec.forProvider.serverId = s.status.atProvider.id + +response: + meta: + ttl: + seconds: 330 + desired: + resources: + flexServer: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServer + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + storageMb: 32768 + flexServerConfig: + resource: + apiVersion: dbforpostgresql.azure.upbound.io/v1beta1 + kind: FlexibleServerConfiguration + spec: + providerConfigRef: + name: my-provider-cfg + forProvider: + serverId: abcdef + conditions: + - type: Unknowns + status: 3 + reason: ObservedUnknowns + message: 'Observed Resources with unknown assignments: flexServerConfig' + results: + - message: 'Observed Resources with unknown assignments: flexServerConfig' + reason: ObservedUnknowns + severity: 2 diff --git a/tests/test_protobuf_values.py b/tests/test_protobuf_values.py index db3a612..7659ccf 100644 --- a/tests/test_protobuf_values.py +++ b/tests/test_protobuf_values.py @@ -123,7 +123,7 @@ def test_values_map(): assert 'nope' not in values assert values == values assert hash(values) == hash(values) - assert values._hasUnknowns + assert values._getUnknowns def test_values_list(): values = protobuf.List( @@ -164,7 +164,7 @@ def test_values_list(): assert protobuf.Unknown() in values assert values == values assert hash(values) == hash(values) - assert values._hasUnknowns + assert values._getUnknowns def test_create_child(): map = protobuf.Unknown() @@ -174,14 +174,14 @@ def test_create_child(): assert map.a.b == 'c' del map.a.b assert 'b' not in map.a - assert not map._hasUnknowns + assert not map._getUnknowns map.a.b = protobuf.Unknown() - assert map._hasUnknowns + assert map._getUnknowns list[0][0] = 'c' assert 'c' in list[0] assert list[0][0] == 'c' del list[0][0] assert 'c' not in list[0] - assert not list._hasUnknowns + assert not list._getUnknowns list[0][0] = protobuf.Unknown() - assert list._hasUnknowns + assert list._getUnknowns