<NME> registry.rb
<BEF> #
# Copyright:: Copyright (c) 2016 GitLab Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require_relative 'nginx.rb'

module Registry
  class << self
    def parse_variables
      # first registry_external_url
      parse_registry_external_url
      # Enable the registry automatically if appropriate
      auto_enable
      # before this gitlab_rails[registry_path] needs to be parsed
      parse_registry
      # parsing the registry notifications
      parse_registry_notifications
    end

    ##
    # If registry_external_url is not set, we're using Let's Encrypt integration,
    # and the registry isn't explicitly disabled, we automatically enable the registry
    def auto_enable
      # If this is not nil, then it has been explicitly set and we should honor that
      return unless Gitlab['registry']['enable'].nil?

      # If registry_external_url is not nil, it has been explicitly set, and we should honor that
      return unless Gitlab['registry_external_url'].nil?

      # If Let's Encrypt is not enabled, we don't want to run
      return unless Gitlab['letsencrypt']['enable']
      Gitlab['gitlab_rails']['registry_host'] = uri.host
      Gitlab['registry_nginx']['listen_port'] ||= uri.port
      Gitlab['registry_nginx']['proxy_set_headers'] ||= Hash.new
      Gitlab['registry_nginx']['proxy_set_headers']['Host'] ||= "#{uri.host}:#{uri.port}"

      case uri.scheme
      when "http"
      Gitlab['registry_nginx']['listen_port'] ||= '5050'
      Gitlab['registry_nginx']['redirect_http_to_https'] = false
      Gitlab['registry_nginx']['fqdn'] = uri.host
      Gitlab['gitlab_rails']['registry_port'] ||= Gitlab['registry_nginx']['listen_port']
      set_ssl(uri.to_s)
      parse_defaults(uri)
    end

    def parse_secrets
      Gitlab['registry']['http_secret'] ||= SecretsHelper.generate_hex(64)
      gitlab_registry_crt, gitlab_registry_key = Registry.generate_registry_keypair
      Gitlab['registry']['internal_certificate'] ||= gitlab_registry_crt
      Gitlab['registry']['internal_key'] ||= gitlab_registry_key
    end

    def parse_registry_external_url
      return unless Gitlab['registry_external_url']

      uri = URI(Gitlab['registry_external_url'].to_s)

      raise "GitLab Container Registry external URL must include a schema and FQDN, e.g. https://registry.example.com/" unless uri.host

      Gitlab['registry']['enable'] = true if Gitlab['registry']['enable'].nil?
      parse_defaults(uri)

      set_ssl(uri.to_s)
      LetsEncryptHelper.add_service_alt_name("registry")
    end

    def parse_defaults(uri)
      Gitlab['gitlab_rails']['registry_enabled'] = true if Gitlab['registry']['enable']
      Gitlab['gitlab_rails']['registry_host'] = uri.host
      Gitlab['registry_nginx']['listen_port'] ||= uri.port
      Gitlab['registry']['registry_http_addr'] ||= "127.0.0.1:5000"
      Gitlab['registry']['registry_http_addr'].gsub(/^https?\:\/\/(www.)?/, '')
      Gitlab['registry']['token_realm'] ||= Gitlab['external_url']
      Gitlab['gitlab_rails']['registry_api_url'] ||= "http://#{Gitlab['registry']['registry_http_addr']}"
    end

    def set_ssl(url)
      uri = URI(url)

      case uri.scheme
      when "http"
        Gitlab['registry_nginx']['https'] ||= false
        Nginx.parse_proxy_headers('registry_nginx', false)
      when "https"
        Gitlab['registry_nginx']['https'] ||= true
        Gitlab['registry_nginx']['ssl_certificate'] ||= "/etc/gitlab/ssl/#{uri.host}.crt"
        Gitlab['registry_nginx']['ssl_certificate_key'] ||= "/etc/gitlab/ssl/#{uri.host}.key"

        Nginx.parse_proxy_headers('registry_nginx', true)
      else
        raise "Unsupported GitLab Registry external URL scheme: #{uri.scheme}"
      end

      raise "Unsupported GitLab Registry external URL path: #{uri.path}" unless ["", "/"].include?(uri.path)

      # Docker versions before 1.13 will fail to authenticate/push with the
      # registry if Registry URL contained :80 or :443. So, we don't set the
      # port in gitlab.yml.
      Gitlab['gitlab_rails']['registry_port'] = uri.port unless [80, 443].include?(uri.port)
    end

    def parse_registry
      return unless Gitlab['registry']['enable']

      Gitlab['gitlab_rails']['registry_path'] = "#{Gitlab['gitlab_rails']['shared_path']}/registry" if Gitlab['gitlab_rails']['registry_path'].nil?
      Gitlab['registry']['storage_delete_enabled'] = true if Gitlab['registry']['storage_delete_enabled'].nil?
      Gitlab['registry']['health_storagedriver_enabled'] = true if Gitlab['registry']['health_storagedriver_enabled'].nil?

      Gitlab['registry']['storage'] ||= {
        'filesystem' => { 'rootdirectory' => Gitlab['gitlab_rails']['registry_path'] }
      }

      Gitlab['registry']['storage']['cache'] ||= { 'blobdescriptor' => 'inmemory' }
      Gitlab['registry']['storage']['delete'] ||= { 'enabled' => Gitlab['registry']['storage_delete_enabled'] }
    end

    def parse_registry_notifications
      return unless Gitlab['registry']['notifications']

      user_configuration = Gitlab['registry']
      gitlab_configuration = Gitlab['node']['registry']

      # Use the registry defaults configured by the user but use the defaults from gitlab if they were not set
      user_configuration['default_notifications_timeout'] ||= gitlab_configuration['default_notifications_timeout']
      user_configuration['default_notifications_threshold'] ||= gitlab_configuration['default_notifications_threshold']
      user_configuration['default_notifications_backoff'] ||=  gitlab_configuration['default_notifications_backoff']
      user_configuration['default_notifications_headers'] ||=  gitlab_configuration['default_notifications_headers']

      Gitlab['registry']['notifications'].each do |endpoint|
        # Get the values from default if they are not set
        endpoint['timeout'] ||= user_configuration['default_notifications_timeout']
        endpoint['threshold'] ||= user_configuration['default_notifications_threshold']
        endpoint['backoff'] ||= user_configuration['default_notifications_backoff']

        # And merge the default headers with the ones specific to this endpoint
        endpoint['headers'] = user_configuration['default_notifications_headers'].merge(endpoint['headers'] || {})

        # As we set notification secret in gitlab.rb for registry we don't have to make user
        # fill it in separately for GitLab application
        Gitlab['gitlab_rails']['registry_notification_secret'] ||= endpoint['headers']['Authorization'].last if endpoint['name'] == 'geo_event'
      end
    end

    def generate_registry_keypair
      key, cert = SecretsHelper.generate_keypair(
        bits: 4096,
        subject: "/C=USA/O=GitLab/OU=Container/CN=Registry",
        validity: 365 * 10 # ten years from now
      )

      [cert.to_pem, key.to_pem]
    end
  end
end

<MSG> Exclude standard ports from Host header

Docker registry does not behave properly when standard ports are included.

Closes #1449

<DFF> @@ -45,7 +45,7 @@ module Registry
       Gitlab['gitlab_rails']['registry_host'] = uri.host
       Gitlab['registry_nginx']['listen_port'] ||= uri.port
       Gitlab['registry_nginx']['proxy_set_headers'] ||= Hash.new
-      Gitlab['registry_nginx']['proxy_set_headers']['Host'] ||= "#{uri.host}:#{uri.port}"
+      Gitlab['registry_nginx']['proxy_set_headers']['Host'] ||= Nginx.generate_host_header(uri)
 
       case uri.scheme
       when "http"

{'commit': 'diff --git a/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb b/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb\nindex 1a702e82a..d7689e9fd 100644\n--- a/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb\n+++ b/files/gitlab-cookbooks/gitlab/libraries/gitlab_rails.rb\n@@ -45,7 +45,7 @@ module GitlabRails\n       Gitlab[\'gitlab_rails\'][\'gitlab_host\'] = uri.host\n       Gitlab[\'gitlab_rails\'][\'gitlab_email_from\'] ||= "gitlab@#{uri.host}"\n       Gitlab[\'nginx\'][\'proxy_set_headers\'] ||= Hash.new\n-      Gitlab[\'nginx\'][\'proxy_set_headers\'][\'Host\'] ||= "#{uri.host}:#{uri.port}"\n+      Gitlab[\'nginx\'][\'proxy_set_headers\'][\'Host\'] ||= Nginx.generate_host_header(uri)\n \n       case uri.scheme\n       when "http"\ndiff --git a/files/gitlab-cookbooks/gitlab/libraries/nginx.rb b/files/gitlab-cookbooks/gitlab/libraries/nginx.rb\nindex d3c13ec0c..29f85acec 100644\n--- a/files/gitlab-cookbooks/gitlab/libraries/nginx.rb\n+++ b/files/gitlab-cookbooks/gitlab/libraries/nginx.rb\n@@ -76,5 +76,18 @@ module Nginx\n \n       Gitlab[app][\'proxy_set_headers\'] = default_from_attributes\n     end\n+\n+    def generate_host_header(uri)\n+      header = uri.host.dup\n+\n+      case uri.scheme\n+      when "http"\n+        header << ":#{uri.port}" if uri.port != 80\n+      when "https"\n+        header << ":#{uri.port}" if uri.port != 443\n+      end\n+\n+      header\n+    end\n   end\n end\ndiff --git a/files/gitlab-cookbooks/gitlab/libraries/registry.rb b/files/gitlab-cookbooks/gitlab/libraries/registry.rb\nindex df1e4ad62..ea1dbbe63 100644\n--- a/files/gitlab-cookbooks/gitlab/libraries/registry.rb\n+++ b/files/gitlab-cookbooks/gitlab/libraries/registry.rb\n@@ -45,7 +45,7 @@ module Registry\n       Gitlab[\'gitlab_rails\'][\'registry_host\'] = uri.host\n       Gitlab[\'registry_nginx\'][\'listen_port\'] ||= uri.port\n       Gitlab[\'registry_nginx\'][\'proxy_set_headers\'] ||= Hash.new\n-      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'][\'Host\'] ||= "#{uri.host}:#{uri.port}"\n+      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'][\'Host\'] ||= Nginx.generate_host_header(uri)\n \n       case uri.scheme\n       when "http"\n', 'commit_subject': 'Exclude standard ports from Host header', 'repo_name': 'gitlabhq/omnibus-gitlab', 'committer': {'name': 'Stan Hu', 'email': '1b75cc461044b78a759f097fe82a64cb6f58b890@gmail.com', 'time_sec': '1469483413', 'tz_offset': '-420', 'date': {'seconds': '1469483413'}}, 'commit_tree': 'd863d42b913d542ac30cc976f9e3d2a746135b66', 'commit_message': 'Exclude standard ports from Host header\n\nDocker registry does not behave properly when standard ports are included.\n\nCloses #1449\n', 'language_name': 'Ruby', 'license': 'apache-2.0', 'hunks': '@@ -45,7 +45,7 @@ module Registry\n       Gitlab[\'gitlab_rails\'][\'registry_host\'] = uri.host\n       Gitlab[\'registry_nginx\'][\'listen_port\'] ||= uri.port\n       Gitlab[\'registry_nginx\'][\'proxy_set_headers\'] ||= Hash.new\n-      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'][\'Host\'] ||= "#{uri.host}:#{uri.port}"\n+      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'][\'Host\'] ||= Nginx.generate_host_header(uri)\n \n       case uri.scheme\n       when "http"\n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/files/gitlab-cookbooks/gitlab/libraries/registry.rb', 'tgt_file': 'b/files/gitlab-cookbooks/gitlab/libraries/registry.rb', 'file_extension': '.rb', 'before_file': '#\n# Copyright:: Copyright (c) 2016 GitLab Inc.\n# License:: Apache License, Version 2.0\n#\n# Licensed under the Apache License, Version 2.0 (the "License");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an "AS IS" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\nrequire_relative \'nginx.rb\'\n\nmodule Registry\n  class << self\n    def parse_variables\n      # first registry_external_url\n      parse_registry_external_url\n      # Enable the registry automatically if appropriate\n      auto_enable\n      # before this gitlab_rails[registry_path] needs to be parsed\n      parse_registry\n      # parsing the registry notifications\n      parse_registry_notifications\n    end\n\n    ##\n    # If registry_external_url is not set, we\'re using Let\'s Encrypt integration,\n    # and the registry isn\'t explicitly disabled, we automatically enable the registry\n    def auto_enable\n      # If this is not nil, then it has been explicitly set and we should honor that\n      return unless Gitlab[\'registry\'][\'enable\'].nil?\n\n      # If registry_external_url is not nil, it has been explicitly set, and we should honor that\n      return unless Gitlab[\'registry_external_url\'].nil?\n\n      # If Let\'s Encrypt is not enabled, we don\'t want to run\n      return unless Gitlab[\'letsencrypt\'][\'enable\']\n      Gitlab[\'gitlab_rails\'][\'registry_host\'] = uri.host\n      Gitlab[\'registry_nginx\'][\'listen_port\'] ||= uri.port\n      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'] ||= Hash.new\n      Gitlab[\'registry_nginx\'][\'proxy_set_headers\'][\'Host\'] ||= "#{uri.host}:#{uri.port}"\n\n      case uri.scheme\n      when "http"\n      Gitlab[\'registry_nginx\'][\'listen_port\'] ||= \'5050\'\n      Gitlab[\'registry_nginx\'][\'redirect_http_to_https\'] = false\n      Gitlab[\'registry_nginx\'][\'fqdn\'] = uri.host\n      Gitlab[\'gitlab_rails\'][\'registry_port\'] ||= Gitlab[\'registry_nginx\'][\'listen_port\']\n      set_ssl(uri.to_s)\n      parse_defaults(uri)\n    end\n\n    def parse_secrets\n      Gitlab[\'registry\'][\'http_secret\'] ||= SecretsHelper.generate_hex(64)\n      gitlab_registry_crt, gitlab_registry_key = Registry.generate_registry_keypair\n      Gitlab[\'registry\'][\'internal_certificate\'] ||= gitlab_registry_crt\n      Gitlab[\'registry\'][\'internal_key\'] ||= gitlab_registry_key\n    end\n\n    def parse_registry_external_url\n      return unless Gitlab[\'registry_external_url\']\n\n      uri = URI(Gitlab[\'registry_external_url\'].to_s)\n\n      raise "GitLab Container Registry external URL must include a schema and FQDN, e.g. https://registry.example.com/" unless uri.host\n\n      Gitlab[\'registry\'][\'enable\'] = true if Gitlab[\'registry\'][\'enable\'].nil?\n      parse_defaults(uri)\n\n      set_ssl(uri.to_s)\n      LetsEncryptHelper.add_service_alt_name("registry")\n    end\n\n    def parse_defaults(uri)\n      Gitlab[\'gitlab_rails\'][\'registry_enabled\'] = true if Gitlab[\'registry\'][\'enable\']\n      Gitlab[\'gitlab_rails\'][\'registry_host\'] = uri.host\n      Gitlab[\'registry_nginx\'][\'listen_port\'] ||= uri.port\n      Gitlab[\'registry\'][\'registry_http_addr\'] ||= "127.0.0.1:5000"\n      Gitlab[\'registry\'][\'registry_http_addr\'].gsub(/^https?\\:\\/\\/(www.)?/, \'\')\n      Gitlab[\'registry\'][\'token_realm\'] ||= Gitlab[\'external_url\']\n      Gitlab[\'gitlab_rails\'][\'registry_api_url\'] ||= "http://#{Gitlab[\'registry\'][\'registry_http_addr\']}"\n    end\n\n    def set_ssl(url)\n      uri = URI(url)\n\n      case uri.scheme\n      when "http"\n        Gitlab[\'registry_nginx\'][\'https\'] ||= false\n        Nginx.parse_proxy_headers(\'registry_nginx\', false)\n      when "https"\n        Gitlab[\'registry_nginx\'][\'https\'] ||= true\n        Gitlab[\'registry_nginx\'][\'ssl_certificate\'] ||= "/etc/gitlab/ssl/#{uri.host}.crt"\n        Gitlab[\'registry_nginx\'][\'ssl_certificate_key\'] ||= "/etc/gitlab/ssl/#{uri.host}.key"\n\n        Nginx.parse_proxy_headers(\'registry_nginx\', true)\n      else\n        raise "Unsupported GitLab Registry external URL scheme: #{uri.scheme}"\n      end\n\n      raise "Unsupported GitLab Registry external URL path: #{uri.path}" unless ["", "/"].include?(uri.path)\n\n      # Docker versions before 1.13 will fail to authenticate/push with the\n      # registry if Registry URL contained :80 or :443. So, we don\'t set the\n      # port in gitlab.yml.\n      Gitlab[\'gitlab_rails\'][\'registry_port\'] = uri.port unless [80, 443].include?(uri.port)\n    end\n\n    def parse_registry\n      return unless Gitlab[\'registry\'][\'enable\']\n\n      Gitlab[\'gitlab_rails\'][\'registry_path\'] = "#{Gitlab[\'gitlab_rails\'][\'shared_path\']}/registry" if Gitlab[\'gitlab_rails\'][\'registry_path\'].nil?\n      Gitlab[\'registry\'][\'storage_delete_enabled\'] = true if Gitlab[\'registry\'][\'storage_delete_enabled\'].nil?\n      Gitlab[\'registry\'][\'health_storagedriver_enabled\'] = true if Gitlab[\'registry\'][\'health_storagedriver_enabled\'].nil?\n\n      Gitlab[\'registry\'][\'storage\'] ||= {\n        \'filesystem\' => { \'rootdirectory\' => Gitlab[\'gitlab_rails\'][\'registry_path\'] }\n      }\n\n      Gitlab[\'registry\'][\'storage\'][\'cache\'] ||= { \'blobdescriptor\' => \'inmemory\' }\n      Gitlab[\'registry\'][\'storage\'][\'delete\'] ||= { \'enabled\' => Gitlab[\'registry\'][\'storage_delete_enabled\'] }\n    end\n\n    def parse_registry_notifications\n      return unless Gitlab[\'registry\'][\'notifications\']\n\n      user_configuration = Gitlab[\'registry\']\n      gitlab_configuration = Gitlab[\'node\'][\'registry\']\n\n      # Use the registry defaults configured by the user but use the defaults from gitlab if they were not set\n      user_configuration[\'default_notifications_timeout\'] ||= gitlab_configuration[\'default_notifications_timeout\']\n      user_configuration[\'default_notifications_threshold\'] ||= gitlab_configuration[\'default_notifications_threshold\']\n      user_configuration[\'default_notifications_backoff\'] ||=  gitlab_configuration[\'default_notifications_backoff\']\n      user_configuration[\'default_notifications_headers\'] ||=  gitlab_configuration[\'default_notifications_headers\']\n\n      Gitlab[\'registry\'][\'notifications\'].each do |endpoint|\n        # Get the values from default if they are not set\n        endpoint[\'timeout\'] ||= user_configuration[\'default_notifications_timeout\']\n        endpoint[\'threshold\'] ||= user_configuration[\'default_notifications_threshold\']\n        endpoint[\'backoff\'] ||= user_configuration[\'default_notifications_backoff\']\n\n        # And merge the default headers with the ones specific to this endpoint\n        endpoint[\'headers\'] = user_configuration[\'default_notifications_headers\'].merge(endpoint[\'headers\'] || {})\n\n        # As we set notification secret in gitlab.rb for registry we don\'t have to make user\n        # fill it in separately for GitLab application\n        Gitlab[\'gitlab_rails\'][\'registry_notification_secret\'] ||= endpoint[\'headers\'][\'Authorization\'].last if endpoint[\'name\'] == \'geo_event\'\n      end\n    end\n\n    def generate_registry_keypair\n      key, cert = SecretsHelper.generate_keypair(\n        bits: 4096,\n        subject: "/C=USA/O=GitLab/OU=Container/CN=Registry",\n        validity: 365 * 10 # ten years from now\n      )\n\n      [cert.to_pem, key.to_pem]\n    end\n  end\nend\n', 'message': 'Exclude standard ports from Host header\n\nDocker registry does not behave properly when standard ports are included.\n\nCloses #1449\n', 'author_name': 'Stan Hu', 'lang': 'rb'}