1# -*- coding: utf-8 -*- #
2# Copyright 2018 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Wraps a Cloud Run Condition messages, making fields easier to access."""
16
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import unicode_literals
20
21import collections
22
23
24_SEVERITY_ERROR = 'Error'
25_SEVERITY_WARNING = 'Warning'
26
27
28def GetNonTerminalMessages(conditions, ignore_retry=False):
29  """Get messages for non-terminal subconditions.
30
31  Only show a message for some non-terminal subconditions:
32  - if severity == warning
33  - if message is provided
34  Non-terminal subconditions that aren't warnings are effectively neutral,
35  so messages for these aren't included unless provided.
36
37  Args:
38    conditions: Conditions
39    ignore_retry: bool, if True, ignores the "Retry" condition
40
41  Returns:
42    list(str) messages of non-terminal subconditions
43  """
44  messages = []
45  for c in conditions.NonTerminalSubconditions():
46    if ignore_retry and c == 'Retry':
47      continue
48    if conditions[c]['severity'] == _SEVERITY_WARNING:
49      messages.append('{}: {}'.format(
50          c, conditions[c]['message'] or 'Unknown Warning.'))
51    elif conditions[c]['message']:
52      messages.append('{}: {}'.format(c, conditions[c]['message']))
53  return messages
54
55
56class Conditions(collections.Mapping):
57  """Represents the status Conditions of a resource in a dict-like way.
58
59  Resource means a Cloud Run resource, e.g: Configuration.
60
61  The conditions of a resource describe error, warning, and completion states of
62  the last set of operations on the resource. True is success, False is failure,
63  and "Unknown" is an operation in progress.
64
65  The special "ready condition" describes the overall success state of the
66  (last operation on) the resource.
67
68  Other conditions may be "terminal", in which case they are required to be True
69  for overall success of the operation, and being False indicates failure.
70
71  If a condition has a severity of "info" or "warning" in the API, it's not
72  terminal.
73
74  More info: https://github.com/knative/serving/blob/master/docs/spec/errors.md
75
76  Note, status field of conditions is converted to boolean type.
77  """
78
79  def __init__(
80      self, conditions, ready_condition=None,
81      observed_generation=None, generation=None):
82    """Constructor.
83
84    Args:
85      conditions: A list of objects of condition_class.
86      ready_condition: str, The one condition type that indicates it is ready.
87      observed_generation: The observedGeneration field of the status object
88      generation: The generation of the object. Incremented every time a user
89        changes the object directly.
90    """
91    self._conditions = {}
92    for cond in conditions:
93      status = None  # Unset or Unknown
94      if cond.status.lower() == 'true':
95        status = True
96      elif cond.status.lower() == 'false':
97        status = False
98      self._conditions[cond.type] = {
99          'severity': cond.severity,
100          'reason': cond.reason,
101          'message': cond.message,
102          'lastTransitionTime': cond.lastTransitionTime,
103          'status': status
104      }
105    self._ready_condition = ready_condition
106    self._fresh = (observed_generation is None or
107                   (observed_generation == generation))
108
109  def __getitem__(self, key):
110    """Implements evaluation of `self[key]`."""
111    return self._conditions[key]
112
113  def __contains__(self, item):
114    """Implements evaluation of `item in self`."""
115    return any(cond_type == item for cond_type in self._conditions)
116
117  def __len__(self):
118    """Implements evaluation of `len(self)`."""
119    return len(self._conditions)
120
121  def __iter__(self):
122    """Returns a generator yielding the condition types."""
123    for cond_type in self._conditions:
124      yield cond_type
125
126  def TerminalSubconditions(self):
127    """Yields keys of the conditions which if all True, Ready should be true."""
128    for k in self:
129      if (k != self._ready_condition and
130          (not self[k]['severity'] or self[k]['severity'] == _SEVERITY_ERROR)):
131        yield k
132
133  def NonTerminalSubconditions(self):
134    """Yields keys of the conditions which do not directly affect Ready."""
135    for k in self:
136      if (k != self._ready_condition and self[k]['severity'] and
137          self[k]['severity'] != _SEVERITY_ERROR):
138        yield k
139
140  def TerminalCondition(self):
141    return self._ready_condition
142
143  def DescriptiveMessage(self):
144    """Descriptive message about what's happened to the last user operation."""
145    if (self._ready_condition and
146        self._ready_condition in self and
147        self[self._ready_condition]['message']):
148      return self[self._ready_condition]['message']
149    return None
150
151  def IsTerminal(self):
152    """True if the resource has finished the last operation, for good or ill.
153
154    conditions are considered terminal if and only if the ready condition is
155    either true or false.
156
157    Returns:
158      A bool representing if terminal.
159    """
160    if not self._ready_condition:
161      raise NotImplementedError()
162    if not self._fresh:
163      return False
164    if self._ready_condition not in self._conditions:
165      return False
166    return self._conditions[self._ready_condition]['status'] is not None
167
168  def IsReady(self):
169    """Return True if the resource has succeeded its current operation."""
170    if not self.IsTerminal():
171      return False
172    return self._conditions[self._ready_condition]['status']
173
174  def IsFailed(self):
175    """"Return True if the resource has failed its current operation."""
176    return self.IsTerminal() and not self.IsReady()
177
178  def IsFresh(self):
179    return self._fresh
180