pre-upgrade-check 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. #!/usr/bin/env python
  2. """
  3. Pre-upgrade checks that must be run on a master before proceeding with upgrade.
  4. """
  5. # This is a script not a python module:
  6. # pylint: disable=invalid-name
  7. # NOTE: This script should not require any python libs other than what is
  8. # in the standard library.
  9. __license__ = "ASL 2.0"
  10. import json
  11. import os
  12. import subprocess
  13. import re
  14. # The maximum length of container.ports.name
  15. ALLOWED_LENGTH = 15
  16. # The valid structure of container.ports.name
  17. ALLOWED_CHARS = re.compile('^[a-z0-9][a-z0-9\\-]*[a-z0-9]$')
  18. AT_LEAST_ONE_LETTER = re.compile('[a-z]')
  19. # look at OS_PATH for the full path. Default ot 'oc'
  20. OC_PATH = os.getenv('OC_PATH', 'oc')
  21. def validate(value):
  22. """
  23. validate verifies that value matches required conventions
  24. Rules of container.ports.name validation:
  25. * must be less that 16 chars
  26. * at least one letter
  27. * only a-z0-9-
  28. * hyphens can not be leading or trailing or next to each other
  29. :Parameters:
  30. - `value`: Value to validate
  31. """
  32. if len(value) > ALLOWED_LENGTH:
  33. return False
  34. if '--' in value:
  35. return False
  36. # We search since it can be anywhere
  37. if not AT_LEAST_ONE_LETTER.search(value):
  38. return False
  39. # We match because it must start at the beginning
  40. if not ALLOWED_CHARS.match(value):
  41. return False
  42. return True
  43. def list_items(kind):
  44. """
  45. list_items returns a list of items from the api
  46. :Parameters:
  47. - `kind`: Kind of item to access
  48. """
  49. response = subprocess.check_output([OC_PATH, 'get', '--all-namespaces', '-o', 'json', kind])
  50. items = json.loads(response)
  51. return items.get("items", [])
  52. def get(obj, *paths):
  53. """
  54. Gets an object
  55. :Parameters:
  56. - `obj`: A dictionary structure
  57. - `path`: All other non-keyword arguments
  58. """
  59. ret_obj = obj
  60. for path in paths:
  61. if ret_obj.get(path, None) is None:
  62. return []
  63. ret_obj = ret_obj[path]
  64. return ret_obj
  65. # pylint: disable=too-many-arguments
  66. def pretty_print_errors(namespace, kind, item_name, container_name, invalid_label, port_name, valid):
  67. """
  68. Prints out results in human friendly way.
  69. :Parameters:
  70. - `namespace`: Namespace of the resource
  71. - `kind`: Kind of the resource
  72. - `item_name`: Name of the resource
  73. - `container_name`: Name of the container. May be "" when kind=Service.
  74. - `port_name`: Name of the port
  75. - `invalid_label`: The label of the invalid port. Port.name/targetPort
  76. - `valid`: True if the port is valid
  77. """
  78. if not valid:
  79. if len(container_name) > 0:
  80. print('%s/%s -n %s (Container="%s" %s="%s")' % (
  81. kind, item_name, namespace, container_name, invalid_label, port_name))
  82. else:
  83. print('%s/%s -n %s (%s="%s")' % (
  84. kind, item_name, namespace, invalid_label, port_name))
  85. def print_validation_header():
  86. """
  87. Prints the error header. Should run on the first error to avoid
  88. overwhelming the user.
  89. """
  90. print """\
  91. At least one port name is invalid and must be corrected before upgrading.
  92. Please update or remove any resources with invalid port names.
  93. Valid port names must:
  94. * be less that 16 characters
  95. * have at least one letter
  96. * contain only a-z0-9-
  97. * not start or end with -
  98. * not contain dashes next to each other ('--')
  99. """
  100. def main():
  101. """
  102. main is the main entry point to this script
  103. """
  104. try:
  105. # the comma at the end suppresses the newline
  106. print "Checking for oc ...",
  107. subprocess.check_output([OC_PATH, 'whoami'])
  108. print "found"
  109. except:
  110. print(
  111. 'Unable to run "%s whoami"\n'
  112. 'Please ensure OpenShift is running, and "oc" is on your system '
  113. 'path.\n'
  114. 'You can override the path with the OC_PATH environment variable.'
  115. % OC_PATH)
  116. raise SystemExit(1)
  117. # Where the magic happens
  118. first_error = True
  119. for kind, path in [
  120. ('deploymentconfigs', ("spec", "template", "spec", "containers")),
  121. ('replicationcontrollers', ("spec", "template", "spec", "containers")),
  122. ('pods', ("spec", "containers"))]:
  123. for item in list_items(kind):
  124. namespace = item["metadata"]["namespace"]
  125. item_name = item["metadata"]["name"]
  126. for container in get(item, *path):
  127. container_name = container["name"]
  128. for port in get(container, "ports"):
  129. port_name = port.get("name", None)
  130. if not port_name:
  131. # Unnamed ports are OK
  132. continue
  133. valid = validate(port_name)
  134. if not valid and first_error:
  135. first_error = False
  136. print_validation_header()
  137. pretty_print_errors(
  138. namespace, kind, item_name,
  139. container_name, "Port.name", port_name, valid)
  140. # Services follow a different flow
  141. for item in list_items('services'):
  142. namespace = item["metadata"]["namespace"]
  143. item_name = item["metadata"]["name"]
  144. for port in get(item, "spec", "ports"):
  145. port_name = port.get("targetPort", None)
  146. if isinstance(port_name, int) or port_name is None:
  147. # Integer only or unnamed ports are OK
  148. continue
  149. valid = validate(port_name)
  150. if not valid and first_error:
  151. first_error = False
  152. print_validation_header()
  153. pretty_print_errors(
  154. namespace, "services", item_name, "",
  155. "targetPort", port_name, valid)
  156. # If we had at least 1 error then exit with 1
  157. if not first_error:
  158. raise SystemExit(1)
  159. if __name__ == '__main__':
  160. main()