summaryrefslogtreecommitdiff
path: root/lib/container_registry/base_client.rb
blob: bb9422ae048dc1fd72d032559e606be6659d06a1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# frozen_string_literal: true

require 'faraday'
require 'faraday_middleware'
require 'digest'

module ContainerRegistry
  class BaseClient
    DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
    DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
    OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
    CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'

    ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
    ACCEPTED_TYPES_RAW = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE, DOCKER_DISTRIBUTION_MANIFEST_LIST_V2_TYPE].freeze

    RETRY_EXCEPTIONS = [Faraday::Request::Retry::DEFAULT_EXCEPTIONS, Faraday::ConnectionFailed].flatten.freeze
    RETRY_OPTIONS = {
      max: 1,
      interval: 5,
      exceptions: RETRY_EXCEPTIONS
    }.freeze

    ERROR_CALLBACK_OPTIONS = {
      callback: -> (env, exception) do
        Gitlab::ErrorTracking.log_exception(
          exception,
          class: name,
          url: env[:url]
        )
      end
    }.freeze

    # Taken from: FaradayMiddleware::FollowRedirects
    REDIRECT_CODES = Set.new [301, 302, 303, 307]

    class << self
      private

      def with_dummy_client(return_value_if_disabled: nil, token_config: { type: :full_access_token, path: nil })
        registry_config = Gitlab.config.registry
        unless registry_config.enabled && registry_config.api_url.present?
          return return_value_if_disabled
        end

        yield new(registry_config.api_url, token: token_from(token_config))
      end

      def token_from(config)
        case config[:type]
        when :full_access_token
          Auth::ContainerRegistryAuthenticationService.access_token([], [])
        when :nested_repositories_token
          return unless config[:path]

          Auth::ContainerRegistryAuthenticationService.pull_nested_repositories_access_token(config[:path])
        end
      end
    end

    def initialize(base_uri, options = {})
      @base_uri = base_uri
      @options = options
    end

    private

    def faraday(timeout_enabled: true)
      @faraday ||= faraday_base(timeout_enabled: timeout_enabled) do |conn|
        initialize_connection(conn, @options, &method(:configure_connection))
      end
    end

    def faraday_base(timeout_enabled: true, &block)
      request_options = timeout_enabled ? Gitlab::HTTP::DEFAULT_TIMEOUT_OPTIONS : nil

      Faraday.new(
        @base_uri,
        headers: { user_agent: "GitLab/#{Gitlab::VERSION}" },
        request: request_options,
        &block
      )
    end

    def initialize_connection(conn, options)
      conn.request :json

      if options[:user] && options[:password]
        conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
      elsif options[:token]
        conn.request(:authorization, :bearer, options[:token].to_s)
      end

      yield(conn) if block_given?

      conn.request(:retry, RETRY_OPTIONS)
      conn.request(:gitlab_error_callback, ERROR_CALLBACK_OPTIONS)
      conn.adapter :net_http
    end

    def response_body(response, allow_redirect: false)
      if allow_redirect && REDIRECT_CODES.include?(response.status)
        response = redirect_response(response.headers['location'])
      end

      response.body if response && response.success?
    end

    def redirect_response(location)
      return unless location

      uri = URI(@base_uri).merge(location)
      raise ArgumentError, "Invalid scheme for #{location}" unless %w[http https].include?(uri.scheme)

      faraday_redirect.get(uri)
    end

    def configure_connection(conn)
      conn.headers['Accept'] = ACCEPTED_TYPES

      conn.response :json, content_type: 'application/json'
      conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws'
      conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json'
      conn.response :json, content_type: DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE
      conn.response :json, content_type: OCI_MANIFEST_V1_TYPE
    end

    # Create a new request to make sure the Authorization header is not inserted
    # via the Faraday middleware
    def faraday_redirect
      @faraday_redirect ||= faraday_base do |conn|
        conn.request :json

        conn.request(:retry, RETRY_OPTIONS)
        conn.request(:gitlab_error_callback, ERROR_CALLBACK_OPTIONS)
        conn.adapter :net_http
      end
    end

    def delete_if_exists(path)
      result = faraday.delete(path)

      result.success? || result.status == 404
    end
  end
end