Kaynağa Gözat

Merge pull request #7197 from miminar/allowed_registries_for_import

configure imagePolicyConfig:allowedRegistriesForImport
Scott Dodson 6 yıl önce
ebeveyn
işleme
08433069b0

+ 2 - 0
inventory/hosts.example

@@ -101,6 +101,8 @@ debug_level=2
 # Configure imagePolicyConfig in the master config
 # Configure imagePolicyConfig in the master config
 # See: https://docs.openshift.org/latest/admin_guide/image_policy.html
 # See: https://docs.openshift.org/latest/admin_guide/image_policy.html
 #openshift_master_image_policy_config={"maxImagesBulkImportedPerRepository": 3, "disableScheduledImport": true}
 #openshift_master_image_policy_config={"maxImagesBulkImportedPerRepository": 3, "disableScheduledImport": true}
+# This setting overrides allowedRegistriesForImport in openshift_master_image_policy_config. By default, all registries are allowed.
+#openshift_master_image_policy_allowed_registries_for_import=["docker.io", "*.docker.io", "*.redhat.com", "gcr.io", "quay.io", "registry.centos.org", "registry.redhat.io", "*.amazonaws.com"]
 
 
 # Configure master API rate limits for external clients
 # Configure master API rate limits for external clients
 #openshift_master_external_ratelimit_qps=200
 #openshift_master_external_ratelimit_qps=200

+ 112 - 0
roles/lib_utils/action_plugins/sanity_checks.py

@@ -2,10 +2,14 @@
 Ansible action plugin to ensure inventory variables are set
 Ansible action plugin to ensure inventory variables are set
 appropriately and no conflicting options have been provided.
 appropriately and no conflicting options have been provided.
 """
 """
+import json
 import re
 import re
 
 
 from ansible.plugins.action import ActionBase
 from ansible.plugins.action import ActionBase
 from ansible import errors
 from ansible import errors
+# pylint: disable=import-error,no-name-in-module
+from ansible.module_utils.six.moves.urllib.parse import urlparse
+
 
 
 # Valid values for openshift_deployment_type
 # Valid values for openshift_deployment_type
 VALID_DEPLOYMENT_TYPES = ('origin', 'openshift-enterprise')
 VALID_DEPLOYMENT_TYPES = ('origin', 'openshift-enterprise')
@@ -43,6 +47,9 @@ STORAGE_KIND_TUPLE = (
     'openshift_prometheus_alertmanager_storage_kind',
     'openshift_prometheus_alertmanager_storage_kind',
     'openshift_prometheus_storage_kind')
     'openshift_prometheus_storage_kind')
 
 
+IMAGE_POLICY_CONFIG_VAR = "openshift_master_image_policy_config"
+ALLOWED_REGISTRIES_VAR = "openshift_master_image_policy_allowed_registries_for_import"
+
 REMOVED_VARIABLES = (
 REMOVED_VARIABLES = (
     # TODO(michaelgugino): Remove these in 3.11
     # TODO(michaelgugino): Remove these in 3.11
     ('openshift_metrics_image_prefix', 'openshift_metrics_<component>_image'),
     ('openshift_metrics_image_prefix', 'openshift_metrics_<component>_image'),
@@ -157,6 +164,79 @@ class ActionModule(ActionBase):
             raise errors.AnsibleModuleError(msg)
             raise errors.AnsibleModuleError(msg)
         return openshift_deployment_type
         return openshift_deployment_type
 
 
+    def get_allowed_registries(self, hostvars, host):
+        """Returns a list of configured allowedRegistriesForImport as a list of patterns"""
+        allowed_registries_for_import = self.template_var(hostvars, host, ALLOWED_REGISTRIES_VAR)
+        if allowed_registries_for_import is None:
+            image_policy_config = self.template_var(hostvars, host, IMAGE_POLICY_CONFIG_VAR)
+            if not image_policy_config:
+                return image_policy_config
+
+            if isinstance(image_policy_config, str):
+                try:
+                    image_policy_config = json.loads(image_policy_config)
+                except Exception:
+                    raise errors.AnsibleModuleError(
+                        "{} is not a valid json string".format(IMAGE_POLICY_CONFIG_VAR))
+
+            if not isinstance(image_policy_config, dict):
+                raise errors.AnsibleModuleError(
+                    "expected dictionary for {}, not {}".format(
+                        IMAGE_POLICY_CONFIG_VAR, type(image_policy_config)))
+
+            detailed = image_policy_config.get("allowedRegistriesForImport", None)
+            if not detailed:
+                return detailed
+
+            if not isinstance(detailed, list):
+                raise errors.AnsibleModuleError("expected list for {}['{}'], not {}".format(
+                    IMAGE_POLICY_CONFIG_VAR, "allowedRegistriesForImport",
+                    type(allowed_registries_for_import)))
+
+            try:
+                return [i["domainName"] for i in detailed]
+            except Exception:
+                raise errors.AnsibleModuleError(
+                    "each item of allowedRegistriesForImport must be a dictionary with 'domainName' key")
+
+        if not isinstance(allowed_registries_for_import, list):
+            raise errors.AnsibleModuleError("expected list for {}, not {}".format(
+                IMAGE_POLICY_CONFIG_VAR, type(allowed_registries_for_import)))
+
+        return allowed_registries_for_import
+
+    def check_whitelisted_registries(self, hostvars, host):
+        """Ensure defined registries are whitelisted"""
+        allowed = self.get_allowed_registries(hostvars, host)
+        if allowed is None:
+            return
+
+        unmatched_registries = []
+        for regvar in (
+                "oreg_url_master", "oreg_url_node", "oreg_url"
+                "openshift_cockpit_deployer_prefix",
+                "openshift_metrics_image_prefix",
+                "openshift_logging_image_prefix",
+                "openshift_service_catalog_image_prefix",
+                "openshift_docker_insecure_registries"):
+            value = self.template_var(hostvars, host, regvar)
+            if not value:
+                continue
+            if isinstance(value, list):
+                registries = value
+            else:
+                registries = [value]
+
+            for reg in registries:
+                if not any(is_registry_match(reg, pat) for pat in allowed):
+                    unmatched_registries.append((regvar, reg))
+
+        if unmatched_registries:
+            registry_list = ", ".join(["{}:{}".format(n, v) for n, v in unmatched_registries])
+            raise errors.AnsibleModuleError(
+                "registry hostnames of the following image prefixes are not whitelisted by image"
+                " policy configuration: {}".format(registry_list))
+
     def check_python_version(self, hostvars, host, distro):
     def check_python_version(self, hostvars, host, distro):
         """Ensure python version is 3 for Fedora and python 2 for others"""
         """Ensure python version is 3 for Fedora and python 2 for others"""
         ansible_python = self.template_var(hostvars, host, 'ansible_python')
         ansible_python = self.template_var(hostvars, host, 'ansible_python')
@@ -311,6 +391,7 @@ class ActionModule(ActionBase):
         """Execute the hostvars validations against host"""
         """Execute the hostvars validations against host"""
         distro = self.template_var(hostvars, host, 'ansible_distribution')
         distro = self.template_var(hostvars, host, 'ansible_distribution')
         odt = self.check_openshift_deployment_type(hostvars, host)
         odt = self.check_openshift_deployment_type(hostvars, host)
+        self.check_whitelisted_registries(hostvars, host)
         self.check_python_version(hostvars, host, distro)
         self.check_python_version(hostvars, host, distro)
         self.check_image_tag_format(hostvars, host, odt)
         self.check_image_tag_format(hostvars, host, odt)
         self.network_plugin_check(hostvars, host)
         self.network_plugin_check(hostvars, host)
@@ -363,3 +444,34 @@ class ActionModule(ActionBase):
         result["msg"] = "Sanity Checks passed"
         result["msg"] = "Sanity Checks passed"
 
 
         return result
         return result
+
+
+def is_registry_match(item, pattern):
+    """returns True if the registry matches the given whitelist pattern
+
+    Unlike in OpenShift, the comparison is done solely on hostname part
+    (excluding the port part) since the latter is much more difficult due to
+    vague definition of port defaulting based on insecure flag. Moreover, most
+    of the registries will be listed without the port and insecure flag.
+    """
+    item = "schema://" + item.split('://', 1)[-1]
+    return is_match(urlparse(item).hostname, pattern.rsplit(':', 1)[0])
+
+
+# taken from https://leetcode.com/problems/wildcard-matching/discuss/17845/python-dp-solution
+# (the same source as for openshift/origin/pkg/util/strings/wildcard.go)
+def is_match(item, pattern):
+    """implements DP algorithm for string matching"""
+    length = len(item)
+    if len(pattern) - pattern.count('*') > length:
+        return False
+    matches = [True] + [False] * length
+    for i in pattern:
+        if i != '*':
+            for index in reversed(range(length)):
+                matches[index + 1] = matches[index] and (i == item[index] or i == '?')
+        else:
+            for index in range(1, length + 1):
+                matches[index] = matches[index - 1] or matches[index]
+        matches[0] = matches[0] and i == '*'
+    return matches[-1]

+ 48 - 0
roles/lib_utils/test/test_sanity_checks.py

@@ -0,0 +1,48 @@
+'''
+ Unit tests for wildcard
+'''
+import os
+import sys
+
+MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'action_plugins'))
+sys.path.insert(0, MODULE_PATH)
+
+# pylint: disable=import-error,wrong-import-position,missing-docstring
+from sanity_checks import is_registry_match   # noqa: E402
+
+
+def test_is_registry_match():
+    '''
+     Test for is_registry_match
+    '''
+    pat_allowall = "*"
+    pat_docker = "docker.io"
+    pat_subdomain = "*.example.com"
+    pat_matchport = "registry:80"
+
+    assert is_registry_match("docker.io/repo/my", pat_allowall)
+    assert is_registry_match("example.com:4000/repo/my", pat_allowall)
+    assert is_registry_match("172.192.222.10:4000/a/b/c", pat_allowall)
+    assert is_registry_match("https://registry.com", pat_allowall)
+    assert is_registry_match("example.com/openshift3/ose-${component}:${version}", pat_allowall)
+
+    assert is_registry_match("docker.io/repo/my", pat_docker)
+    assert is_registry_match("docker.io:443/repo/my", pat_docker)
+    assert is_registry_match("docker.io/openshift3/ose-${component}:${version}", pat_allowall)
+    assert not is_registry_match("example.com:4000/repo/my", pat_docker)
+    assert not is_registry_match("index.docker.io/a/b/c", pat_docker)
+    assert not is_registry_match("https://registry.com", pat_docker)
+    assert not is_registry_match("example.com/openshift3/ose-${component}:${version}", pat_docker)
+
+    assert is_registry_match("apps.foo.example.com/prefix", pat_subdomain)
+    assert is_registry_match("sub.example.com:80", pat_subdomain)
+    assert not is_registry_match("https://example.com:443/prefix", pat_subdomain)
+    assert not is_registry_match("docker.io/library/my", pat_subdomain)
+    assert not is_registry_match("https://hello.example.bar", pat_subdomain)
+
+    assert is_registry_match("registry:80/prefix", pat_matchport)
+    assert is_registry_match("registry/myapp", pat_matchport)
+    assert is_registry_match("registry:443/myap", pat_matchport)
+    assert not is_registry_match("https://example.com:443/prefix", pat_matchport)
+    assert not is_registry_match("docker.io/library/my", pat_matchport)
+    assert not is_registry_match("https://hello.registry/myapp", pat_matchport)

+ 29 - 0
roles/openshift_facts/library/openshift_facts.py

@@ -492,6 +492,34 @@ def set_nodename(facts):
     return facts
     return facts
 
 
 
 
+def make_allowed_registries(registry_list):
+    """ turns a list of wildcard registries to allowedRegistriesForImport json setting """
+    return {
+        "allowedRegistriesForImport": [
+            {'domainName': reg} if isinstance(reg, str) else reg for reg in registry_list
+        ]
+    }
+
+
+def set_allowed_registries(facts):
+    """ override allowedRegistriesForImport in imagePolicyConfig """
+    if 'master' in facts:
+        image_policy = {}
+        overriden = False
+        if facts['master'].get('image_policy_config', None):
+            image_policy = facts['master']['image_policy_config']
+            overriden = True
+
+        overrides = facts['master'].get('image_policy_allowed_registries_for_import', None)
+        if overrides:
+            image_policy = merge_facts(image_policy, make_allowed_registries(overrides), None)
+            overriden = True
+
+        if overriden:
+            facts['master']['image_policy_config'] = image_policy
+    return facts
+
+
 def format_url(use_ssl, hostname, port, path=''):
 def format_url(use_ssl, hostname, port, path=''):
     """ Format url based on ssl flag, hostname, port and path
     """ Format url based on ssl flag, hostname, port and path
 
 
@@ -1045,6 +1073,7 @@ class OpenShiftFacts(object):
         facts = set_builddefaults_facts(facts)
         facts = set_builddefaults_facts(facts)
         facts = set_buildoverrides_facts(facts)
         facts = set_buildoverrides_facts(facts)
         facts = set_nodename(facts)
         facts = set_nodename(facts)
+        facts = set_allowed_registries(facts)
         return dict(openshift=facts)
         return dict(openshift=facts)
 
 
     def get_defaults(self, roles):
     def get_defaults(self, roles):

Dosya farkı çok büyük olduğundan ihmal edildi
+ 6 - 1
roles/openshift_hosted/tasks/registry.yml


+ 1 - 0
roles/openshift_master_facts/tasks/main.yml

@@ -51,6 +51,7 @@
       admission_plugin_config: "{{openshift_master_admission_plugin_config }}"
       admission_plugin_config: "{{openshift_master_admission_plugin_config }}"
       kube_admission_plugin_config: "{{openshift_master_kube_admission_plugin_config | default(None) }}"  # deprecated, merged with admission_plugin_config
       kube_admission_plugin_config: "{{openshift_master_kube_admission_plugin_config | default(None) }}"  # deprecated, merged with admission_plugin_config
       image_policy_config: "{{ openshift_master_image_policy_config | default(None) }}"
       image_policy_config: "{{ openshift_master_image_policy_config | default(None) }}"
+      image_policy_allowed_registries_for_import: "{{ openshift_master_image_policy_allowed_registries_for_import | default(None) }}"
 
 
 - name: Determine if scheduler config present
 - name: Determine if scheduler config present
   stat:
   stat: