dependabot-docker 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/dependabot/docker/file_fetcher.rb +42 -0
- data/lib/dependabot/docker/file_parser.rb +165 -0
- data/lib/dependabot/docker/file_updater.rb +135 -0
- data/lib/dependabot/docker/metadata_finder.rb +20 -0
- data/lib/dependabot/docker/update_checker.rb +291 -0
- data/lib/dependabot/docker/utils/credentials_finder.rb +65 -0
- metadata +176 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f91e012344a707c90832f405af7e3c63084bd319b93c0ae9181867357de02e36
|
|
4
|
+
data.tar.gz: 0ec7fcb7029b52d69b9664f3b67aff22cbee2f9dfd71263c7041ee24dea59a15
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f628cee07f0b8346d582ff23496607187bcbbf0dc174798b4d811f1410637c3442888dd41b530d23639e80477c83869272a7ba4543768e43cd93f10b72fce4ca
|
|
7
|
+
data.tar.gz: 52732d1c2c1273a91a8377ae551c9fecae807f460e883f7ab930c3f874dbf86365d4398770a5d7f71889c0e37c13af39bfadee3e6871901bbcfa459dc02cc98f
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/file_fetchers"
|
|
4
|
+
require "dependabot/file_fetchers/base"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Docker
|
|
8
|
+
class FileFetcher < Dependabot::FileFetchers::Base
|
|
9
|
+
def self.required_files_in?(filenames)
|
|
10
|
+
filenames.any? { |f| f.match?(/dockerfile/i) }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.required_files_message
|
|
14
|
+
"Repo must contain a Dockerfile."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def fetch_files
|
|
20
|
+
fetched_files = []
|
|
21
|
+
fetched_files += dockerfiles
|
|
22
|
+
|
|
23
|
+
return fetched_files if fetched_files.any?
|
|
24
|
+
|
|
25
|
+
raise(
|
|
26
|
+
Dependabot::DependencyFileNotFound,
|
|
27
|
+
File.join(directory, "Dockerfile")
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def dockerfiles
|
|
32
|
+
@dockerfiles ||=
|
|
33
|
+
repo_contents(raise_errors: false).
|
|
34
|
+
select { |f| f.type == "file" && f.name.match?(/dockerfile/i) }.
|
|
35
|
+
map { |f| fetch_file_from_host(f.name) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Dependabot::FileFetchers.
|
|
42
|
+
register("docker", Dependabot::Docker::FileFetcher)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "docker_registry2"
|
|
4
|
+
|
|
5
|
+
require "dependabot/dependency"
|
|
6
|
+
require "dependabot/file_parsers"
|
|
7
|
+
require "dependabot/file_parsers/base"
|
|
8
|
+
require "dependabot/errors"
|
|
9
|
+
require "dependabot/docker/utils/credentials_finder"
|
|
10
|
+
|
|
11
|
+
module Dependabot
|
|
12
|
+
module Docker
|
|
13
|
+
class FileParser < Dependabot::FileParsers::Base
|
|
14
|
+
require "dependabot/file_parsers/base/dependency_set"
|
|
15
|
+
|
|
16
|
+
# Detials of Docker regular expressions is at
|
|
17
|
+
# https://github.com/docker/distribution/blob/master/reference/regexp.go
|
|
18
|
+
DOMAIN_COMPONENT =
|
|
19
|
+
/(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/.freeze
|
|
20
|
+
DOMAIN = /(?:#{DOMAIN_COMPONENT}(?:\.#{DOMAIN_COMPONENT})+)/.freeze
|
|
21
|
+
REGISTRY = /(?<registry>#{DOMAIN}(?::[0-9]+)?)/.freeze
|
|
22
|
+
|
|
23
|
+
NAME_COMPONENT = /(?:[a-z0-9]+(?:(?:[._]|__|[-]*)[a-z0-9]+)*)/.freeze
|
|
24
|
+
IMAGE = %r{(?<image>#{NAME_COMPONENT}(?:/#{NAME_COMPONENT})*)}.freeze
|
|
25
|
+
|
|
26
|
+
FROM = /[Ff][Rr][Oo][Mm]/.freeze
|
|
27
|
+
TAG = /:(?<tag>[\w][\w.-]{0,127})/.freeze
|
|
28
|
+
DIGEST = /@(?<digest>[^\s]+)/.freeze
|
|
29
|
+
NAME = /\s+AS\s+(?<name>[a-zA-Z0-9_-]+)/.freeze
|
|
30
|
+
FROM_LINE =
|
|
31
|
+
%r{^#{FROM}\s+(#{REGISTRY}/)?#{IMAGE}#{TAG}?#{DIGEST}?#{NAME}?}.freeze
|
|
32
|
+
|
|
33
|
+
AWS_ECR_URL = /dkr\.ecr\.(?<region>[^.]+).amazonaws\.com/.freeze
|
|
34
|
+
|
|
35
|
+
def parse
|
|
36
|
+
dependency_set = DependencySet.new
|
|
37
|
+
|
|
38
|
+
dockerfiles.each do |dockerfile|
|
|
39
|
+
dockerfile.content.each_line do |line|
|
|
40
|
+
next unless FROM_LINE.match?(line)
|
|
41
|
+
|
|
42
|
+
parsed_from_line = FROM_LINE.match(line).named_captures
|
|
43
|
+
|
|
44
|
+
version = version_from(parsed_from_line)
|
|
45
|
+
next unless version
|
|
46
|
+
|
|
47
|
+
dependency_set << Dependency.new(
|
|
48
|
+
name: parsed_from_line.fetch("image"),
|
|
49
|
+
version: version,
|
|
50
|
+
package_manager: "docker",
|
|
51
|
+
requirements: [
|
|
52
|
+
requirement: nil,
|
|
53
|
+
groups: [],
|
|
54
|
+
file: dockerfile.name,
|
|
55
|
+
source: source_from(parsed_from_line)
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
dependency_set.dependencies
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def dockerfiles
|
|
67
|
+
# The Docker file fetcher only fetches Dockerfiles, so no need to
|
|
68
|
+
# filter here
|
|
69
|
+
dependency_files
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def version_from(parsed_from_line)
|
|
73
|
+
return parsed_from_line.fetch("tag") if parsed_from_line.fetch("tag")
|
|
74
|
+
|
|
75
|
+
version_from_digest(
|
|
76
|
+
registry: parsed_from_line.fetch("registry"),
|
|
77
|
+
image: parsed_from_line.fetch("image"),
|
|
78
|
+
digest: parsed_from_line.fetch("digest")
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def source_from(parsed_from_line)
|
|
83
|
+
source = {}
|
|
84
|
+
|
|
85
|
+
if parsed_from_line.fetch("registry")
|
|
86
|
+
source[:registry] = parsed_from_line.fetch("registry")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if parsed_from_line.fetch("tag")
|
|
90
|
+
source[:tag] = parsed_from_line.fetch("tag")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if parsed_from_line.fetch("digest")
|
|
94
|
+
source[:digest] = parsed_from_line.fetch("digest")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
source
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def version_from_digest(registry:, image:, digest:)
|
|
101
|
+
return unless digest
|
|
102
|
+
|
|
103
|
+
repo = docker_repo_name(image, registry)
|
|
104
|
+
registry_client = docker_registry_client(registry)
|
|
105
|
+
registry_client.tags(repo).fetch("tags").find do |tag|
|
|
106
|
+
digest == registry_client.digest(repo, tag)
|
|
107
|
+
rescue DockerRegistry2::NotFound
|
|
108
|
+
# Shouldn't happen, but it does. Example of existing tag with
|
|
109
|
+
# no manifest is "library/python", "2-windowsservercore".
|
|
110
|
+
false
|
|
111
|
+
end
|
|
112
|
+
rescue DockerRegistry2::RegistryAuthenticationException,
|
|
113
|
+
RestClient::Forbidden
|
|
114
|
+
raise if standard_registry?(registry)
|
|
115
|
+
|
|
116
|
+
raise PrivateSourceAuthenticationFailure, registry
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def docker_repo_name(image, registry)
|
|
120
|
+
return image unless standard_registry?(registry)
|
|
121
|
+
return image unless image.split("/").count < 2
|
|
122
|
+
|
|
123
|
+
"library/#{image}"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def docker_registry_client(registry)
|
|
127
|
+
if registry
|
|
128
|
+
credentials = registry_credentials(registry)
|
|
129
|
+
|
|
130
|
+
DockerRegistry2::Registry.new(
|
|
131
|
+
"https://#{registry}",
|
|
132
|
+
user: credentials&.fetch("username"),
|
|
133
|
+
password: credentials&.fetch("password")
|
|
134
|
+
)
|
|
135
|
+
else
|
|
136
|
+
DockerRegistry2::Registry.new("https://registry.hub.docker.com")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def registry_credentials(registry_url)
|
|
141
|
+
credentials_finder.credentials_for_registry(registry_url)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def credentials_finder
|
|
145
|
+
@credentials_finder ||= Utils::CredentialsFinder.new(credentials)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def standard_registry?(registry)
|
|
149
|
+
return true if registry.nil?
|
|
150
|
+
|
|
151
|
+
registry == "registry.hub.docker.com"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_required_files
|
|
155
|
+
# Just check if there are any files at all.
|
|
156
|
+
return if dependency_files.any?
|
|
157
|
+
|
|
158
|
+
raise "No Dockerfile!"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
Dependabot::FileParsers.
|
|
165
|
+
register("docker", Dependabot::Docker::FileParser)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/file_updaters"
|
|
4
|
+
require "dependabot/file_updaters/base"
|
|
5
|
+
require "dependabot/errors"
|
|
6
|
+
|
|
7
|
+
module Dependabot
|
|
8
|
+
module Docker
|
|
9
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
|
10
|
+
FROM_REGEX = /[Ff][Rr][Oo][Mm]/.freeze
|
|
11
|
+
|
|
12
|
+
def self.updated_files_regex
|
|
13
|
+
[/dockerfile/]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def updated_dependency_files
|
|
17
|
+
updated_files = []
|
|
18
|
+
|
|
19
|
+
dependency_files.each do |file|
|
|
20
|
+
next unless requirement_changed?(file, dependency)
|
|
21
|
+
|
|
22
|
+
updated_files <<
|
|
23
|
+
updated_file(
|
|
24
|
+
file: file,
|
|
25
|
+
content: updated_dockerfile_content(file)
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
updated_files.reject! { |f| dependency_files.include?(f) }
|
|
30
|
+
raise "No files changed!" if updated_files.none?
|
|
31
|
+
|
|
32
|
+
updated_files
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def dependency
|
|
38
|
+
# Dockerfiles will only ever be updating a single dependency
|
|
39
|
+
dependencies.first
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check_required_files
|
|
43
|
+
# Just check if there are any files at all.
|
|
44
|
+
return if dependency_files.any?
|
|
45
|
+
|
|
46
|
+
raise "No Dockerfile!"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def updated_dockerfile_content(file)
|
|
50
|
+
updated_content =
|
|
51
|
+
if specified_with_digest?(file)
|
|
52
|
+
update_digest_and_tag(file)
|
|
53
|
+
else
|
|
54
|
+
update_tag(file)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise "Expected content to change!" if updated_content == file.content
|
|
58
|
+
|
|
59
|
+
updated_content
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def update_digest_and_tag(file)
|
|
63
|
+
old_declaration_regex = /^#{FROM_REGEX}\s+.*@#{old_digest(file)}/
|
|
64
|
+
|
|
65
|
+
file.content.gsub(old_declaration_regex) do |old_dec|
|
|
66
|
+
old_dec.
|
|
67
|
+
gsub("@#{old_digest(file)}", "@#{new_digest(file)}").
|
|
68
|
+
gsub(":#{dependency.previous_version}",
|
|
69
|
+
":#{dependency.version}")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def update_tag(file)
|
|
74
|
+
return unless old_tag(file)
|
|
75
|
+
|
|
76
|
+
old_declaration =
|
|
77
|
+
if private_registry_url(file) then "#{private_registry_url(file)}/"
|
|
78
|
+
else ""
|
|
79
|
+
end
|
|
80
|
+
old_declaration += "#{dependency.name}:#{old_tag(file)}"
|
|
81
|
+
escaped_declaration = Regexp.escape(old_declaration)
|
|
82
|
+
|
|
83
|
+
old_declaration_regex = /^#{FROM_REGEX}\s+#{escaped_declaration}/
|
|
84
|
+
|
|
85
|
+
file.content.gsub(old_declaration_regex) do |old_dec|
|
|
86
|
+
old_dec.gsub(":#{old_tag(file)}", ":#{new_tag(file)}")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def specified_with_digest?(file)
|
|
91
|
+
dependency.
|
|
92
|
+
requirements.
|
|
93
|
+
find { |r| r[:file] == file.name }.
|
|
94
|
+
fetch(:source)[:digest]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def new_digest(file)
|
|
98
|
+
return unless specified_with_digest?(file)
|
|
99
|
+
|
|
100
|
+
dependency.requirements.
|
|
101
|
+
find { |r| r[:file] == file.name }.
|
|
102
|
+
fetch(:source).fetch(:digest)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def old_digest(file)
|
|
106
|
+
return unless specified_with_digest?(file)
|
|
107
|
+
|
|
108
|
+
dependency.previous_requirements.
|
|
109
|
+
find { |r| r[:file] == file.name }.
|
|
110
|
+
fetch(:source).fetch(:digest)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def new_tag(file)
|
|
114
|
+
dependency.requirements.
|
|
115
|
+
find { |r| r[:file] == file.name }.
|
|
116
|
+
fetch(:source)[:tag]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def old_tag(file)
|
|
120
|
+
dependency.previous_requirements.
|
|
121
|
+
find { |r| r[:file] == file.name }.
|
|
122
|
+
fetch(:source)[:tag]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def private_registry_url(file)
|
|
126
|
+
dependency.requirements.
|
|
127
|
+
find { |r| r[:file] == file.name }.
|
|
128
|
+
fetch(:source)[:registry]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Dependabot::FileUpdaters.
|
|
135
|
+
register("docker", Dependabot::Docker::FileUpdater)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dependabot/metadata_finders"
|
|
4
|
+
require "dependabot/metadata_finders/base"
|
|
5
|
+
|
|
6
|
+
module Dependabot
|
|
7
|
+
module Docker
|
|
8
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def look_up_source
|
|
12
|
+
# TODO: Find a way to add links to PRs
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Dependabot::MetadataFinders.
|
|
20
|
+
register("docker", Dependabot::Docker::MetadataFinder)
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "docker_registry2"
|
|
4
|
+
|
|
5
|
+
require "dependabot/update_checkers"
|
|
6
|
+
require "dependabot/update_checkers/base"
|
|
7
|
+
require "dependabot/errors"
|
|
8
|
+
require "dependabot/docker/utils/credentials_finder"
|
|
9
|
+
|
|
10
|
+
module Dependabot
|
|
11
|
+
module Docker
|
|
12
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
|
13
|
+
VERSION_REGEX = /(?<version>[0-9]+(?:\.[a-zA-Z0-9]+)*)/.freeze
|
|
14
|
+
VERSION_WITH_SUFFIX =
|
|
15
|
+
/^#{VERSION_REGEX}(?<affix>-[a-z0-9.\-]+)?$/.freeze
|
|
16
|
+
VERSION_WITH_PREFIX =
|
|
17
|
+
/^(?<affix>[a-z0-9.\-]+-)?#{VERSION_REGEX}$/.freeze
|
|
18
|
+
NAME_WITH_VERSION =
|
|
19
|
+
/#{VERSION_WITH_PREFIX}|#{VERSION_WITH_SUFFIX}/.freeze
|
|
20
|
+
|
|
21
|
+
def latest_version
|
|
22
|
+
@latest_version ||= fetch_latest_version
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def latest_resolvable_version
|
|
26
|
+
# Resolvability isn't an issue for Docker containers.
|
|
27
|
+
latest_version
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def latest_resolvable_version_with_no_unlock
|
|
31
|
+
# No concept of "unlocking" for Docker containers
|
|
32
|
+
dependency.version
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def updated_requirements
|
|
36
|
+
dependency.requirements.map do |req|
|
|
37
|
+
updated_source = req.fetch(:source).dup
|
|
38
|
+
updated_source[:digest] = updated_digest if req[:source][:digest]
|
|
39
|
+
updated_source[:tag] = latest_version if req[:source][:tag]
|
|
40
|
+
|
|
41
|
+
req.merge(source: updated_source)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def latest_version_resolvable_with_full_unlock?
|
|
48
|
+
# Full unlock checks aren't relevant for Dockerfiles
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def updated_dependencies_after_full_unlock
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def version_can_update?(*)
|
|
57
|
+
!version_up_to_date?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def version_up_to_date?
|
|
61
|
+
# If the tag isn't up-to-date then we can definitely update
|
|
62
|
+
return false if version_tag_up_to_date? == false
|
|
63
|
+
|
|
64
|
+
# Otherwise, if the Dockerfile specifies a digest check that that is
|
|
65
|
+
# up-to-date
|
|
66
|
+
digest_up_to_date?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def version_tag_up_to_date?
|
|
70
|
+
return unless dependency.version.match?(NAME_WITH_VERSION)
|
|
71
|
+
|
|
72
|
+
old_v = numeric_version_from(dependency.version)
|
|
73
|
+
latest_v = numeric_version_from(latest_version)
|
|
74
|
+
|
|
75
|
+
return true if version_class.new(latest_v) <= version_class.new(old_v)
|
|
76
|
+
|
|
77
|
+
# Check the precision of the potentially higher tag is the same as the
|
|
78
|
+
# one it would replace. In the event that it's not the same, check the
|
|
79
|
+
# digests are also unequal. Avoids 'updating' ruby-2 -> ruby-2.5.1
|
|
80
|
+
return false if old_v.split(".").count == latest_v.split(".").count
|
|
81
|
+
|
|
82
|
+
digest_of(dependency.version) == digest_of(latest_version)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def digest_up_to_date?
|
|
86
|
+
dependency.requirements.all? do |req|
|
|
87
|
+
next true unless req.fetch(:source)[:digest]
|
|
88
|
+
|
|
89
|
+
req.fetch(:source).fetch(:digest) == digest_of(dependency.version)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Note: It's important that this *always* returns a version (even if
|
|
94
|
+
# it's the existing one) as it is what we later check the digest of.
|
|
95
|
+
def fetch_latest_version
|
|
96
|
+
unless dependency.version.match?(NAME_WITH_VERSION)
|
|
97
|
+
return dependency.version
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Prune out any downgrade tags before checking for pre-releases
|
|
101
|
+
# (which requires a call to the registry for each tag, so can be slow)
|
|
102
|
+
candidate_tags = comparable_tags_from_registry
|
|
103
|
+
non_downgrade_tags = remove_version_downgrades(candidate_tags)
|
|
104
|
+
candidate_tags = non_downgrade_tags if non_downgrade_tags.any?
|
|
105
|
+
|
|
106
|
+
wants_prerelease = prerelease?(dependency.version)
|
|
107
|
+
candidate_tags =
|
|
108
|
+
candidate_tags.
|
|
109
|
+
reject { |tag| prerelease?(tag) && !wants_prerelease }.
|
|
110
|
+
reject do |tag|
|
|
111
|
+
version = version_class.new(numeric_version_from(tag))
|
|
112
|
+
ignore_reqs.any? { |r| r.satisfied_by?(version) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
latest_tag =
|
|
116
|
+
candidate_tags.
|
|
117
|
+
max_by { |tag| version_class.new(numeric_version_from(tag)) }
|
|
118
|
+
|
|
119
|
+
latest_tag || dependency.version
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def comparable_tags_from_registry
|
|
123
|
+
original_affix = affix_of(dependency.version)
|
|
124
|
+
|
|
125
|
+
tags_from_registry.
|
|
126
|
+
select { |tag| tag.match?(NAME_WITH_VERSION) }.
|
|
127
|
+
select { |tag| affix_of(tag) == original_affix }.
|
|
128
|
+
reject { |tag| commit_sha_suffix?(tag) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def remove_version_downgrades(candidate_tags)
|
|
132
|
+
candidate_tags.select do |tag|
|
|
133
|
+
version_class.new(numeric_version_from(tag)) >=
|
|
134
|
+
version_class.new(numeric_version_from(dependency.version))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def commit_sha_suffix?(tag)
|
|
139
|
+
# Some people suffix their versions with commit SHAs. Dependabot
|
|
140
|
+
# can't order on those but will try to, so instead we should exclude
|
|
141
|
+
# them (unless there's a `latest` version pushed to the registry, in
|
|
142
|
+
# which case we'll use that to find the latest version)
|
|
143
|
+
return false unless tag.match?(/(^|\-)[0-9a-f]{7,}$/)
|
|
144
|
+
|
|
145
|
+
!tag.match?(/(^|\-)20[0-1]\d{5}$/)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def version_of_latest_tag
|
|
149
|
+
return unless latest_digest
|
|
150
|
+
|
|
151
|
+
tags_from_registry.
|
|
152
|
+
select { |tag| canonical_version?(tag) }.
|
|
153
|
+
select { |t| digest_of(t) == latest_digest }.
|
|
154
|
+
map { |t| version_class.new(numeric_version_from(t)) }.
|
|
155
|
+
max
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def canonical_version?(tag)
|
|
159
|
+
return false unless numeric_version_from(tag)
|
|
160
|
+
return true if tag == numeric_version_from(tag)
|
|
161
|
+
|
|
162
|
+
# .NET tags are suffixed with -sdk. There may be other cases we need
|
|
163
|
+
# to consider in future, too.
|
|
164
|
+
tag == numeric_version_from(tag) + "-sdk"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def updated_digest
|
|
168
|
+
@updated_digest ||=
|
|
169
|
+
begin
|
|
170
|
+
docker_registry_client.digest(docker_repo_name, latest_version)
|
|
171
|
+
rescue RestClient::Exceptions::Timeout
|
|
172
|
+
attempt ||= 1
|
|
173
|
+
attempt += 1
|
|
174
|
+
raise if attempt > 3
|
|
175
|
+
|
|
176
|
+
retry
|
|
177
|
+
end
|
|
178
|
+
rescue DockerRegistry2::RegistryAuthenticationException,
|
|
179
|
+
RestClient::Forbidden
|
|
180
|
+
raise PrivateSourceAuthenticationFailure, registry_hostname
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def tags_from_registry
|
|
184
|
+
@tags_from_registry ||=
|
|
185
|
+
begin
|
|
186
|
+
docker_registry_client.tags(docker_repo_name).fetch("tags")
|
|
187
|
+
rescue RestClient::Exceptions::Timeout
|
|
188
|
+
attempt ||= 1
|
|
189
|
+
attempt += 1
|
|
190
|
+
raise if attempt > 3
|
|
191
|
+
|
|
192
|
+
retry
|
|
193
|
+
end
|
|
194
|
+
rescue DockerRegistry2::RegistryAuthenticationException,
|
|
195
|
+
RestClient::Forbidden
|
|
196
|
+
raise PrivateSourceAuthenticationFailure, registry_hostname
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def latest_digest
|
|
200
|
+
return unless tags_from_registry.include?("latest")
|
|
201
|
+
|
|
202
|
+
digest_of("latest")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def digest_of(tag)
|
|
206
|
+
@digests ||= {}
|
|
207
|
+
return @digests[tag] if @digests.key?(tag)
|
|
208
|
+
|
|
209
|
+
@digests[tag] =
|
|
210
|
+
begin
|
|
211
|
+
docker_registry_client.digest(docker_repo_name, tag)
|
|
212
|
+
rescue *transient_docker_errors => e
|
|
213
|
+
attempt ||= 1
|
|
214
|
+
attempt += 1
|
|
215
|
+
return if attempt > 3 && e.is_a?(DockerRegistry2::NotFound)
|
|
216
|
+
raise if attempt > 3
|
|
217
|
+
|
|
218
|
+
retry
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def transient_docker_errors
|
|
223
|
+
[RestClient::Exceptions::Timeout, DockerRegistry2::NotFound]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def affix_of(tag)
|
|
227
|
+
tag.match(NAME_WITH_VERSION).named_captures.fetch("affix")
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def prerelease?(tag)
|
|
231
|
+
return true if numeric_version_from(tag).match?(/[a-zA-Z]/)
|
|
232
|
+
|
|
233
|
+
# If we're dealing with a numeric version we can compare it against
|
|
234
|
+
# the digest for the `latest` tag.
|
|
235
|
+
return false unless numeric_version_from(tag)
|
|
236
|
+
return false unless latest_digest
|
|
237
|
+
return false unless version_of_latest_tag
|
|
238
|
+
|
|
239
|
+
version_class.new(numeric_version_from(tag)) > version_of_latest_tag
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def numeric_version_from(tag)
|
|
243
|
+
return unless tag.match?(NAME_WITH_VERSION)
|
|
244
|
+
|
|
245
|
+
tag.match(NAME_WITH_VERSION).named_captures.fetch("version")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def registry_hostname
|
|
249
|
+
dependency.requirements.first[:source][:registry] ||
|
|
250
|
+
"registry.hub.docker.com"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def using_dockerhub?
|
|
254
|
+
registry_hostname == "registry.hub.docker.com"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def registry_credentials
|
|
258
|
+
credentials_finder.credentials_for_registry(registry_hostname)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def credentials_finder
|
|
262
|
+
@credentials_finder ||= Utils::CredentialsFinder.new(credentials)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def docker_repo_name
|
|
266
|
+
return dependency.name unless using_dockerhub?
|
|
267
|
+
return dependency.name unless dependency.name.split("/").count < 2
|
|
268
|
+
|
|
269
|
+
"library/#{dependency.name}"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def docker_registry_client
|
|
273
|
+
@docker_registry_client ||=
|
|
274
|
+
DockerRegistry2::Registry.new(
|
|
275
|
+
"https://#{registry_hostname}",
|
|
276
|
+
user: registry_credentials&.fetch("username"),
|
|
277
|
+
password: registry_credentials&.fetch("password")
|
|
278
|
+
)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def ignore_reqs
|
|
282
|
+
# Note: we use Gem::Requirement here because ignore conditions will
|
|
283
|
+
# be passed as Ruby ranges
|
|
284
|
+
ignored_versions.map { |req| Gem::Requirement.new(req.split(",")) }
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
Dependabot::UpdateCheckers.
|
|
291
|
+
register("docker", Dependabot::Docker::UpdateChecker)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "aws-sdk-ecr"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
require "dependabot/errors"
|
|
7
|
+
|
|
8
|
+
module Dependabot
|
|
9
|
+
module Docker
|
|
10
|
+
module Utils
|
|
11
|
+
class CredentialsFinder
|
|
12
|
+
AWS_ECR_URL = /dkr\.ecr\.(?<region>[^.]+).amazonaws\.com/.freeze
|
|
13
|
+
|
|
14
|
+
def initialize(credentials)
|
|
15
|
+
@credentials = credentials
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def credentials_for_registry(registry_hostname)
|
|
19
|
+
registry_details =
|
|
20
|
+
credentials.
|
|
21
|
+
select { |cred| cred["type"] == "docker_registry" }.
|
|
22
|
+
find { |cred| cred.fetch("registry") == registry_hostname }
|
|
23
|
+
return unless registry_details
|
|
24
|
+
return registry_details unless registry_hostname.match?(AWS_ECR_URL)
|
|
25
|
+
|
|
26
|
+
build_aws_credentials(registry_details)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :credentials
|
|
32
|
+
|
|
33
|
+
def build_aws_credentials(registry_details)
|
|
34
|
+
# If credentials have been generated from AWS we can just return them
|
|
35
|
+
return registry_details if registry_details.fetch("username") == "AWS"
|
|
36
|
+
|
|
37
|
+
# Otherwise, we need to use the provided Access Key ID and secret to
|
|
38
|
+
# generate a temporary username and password
|
|
39
|
+
aws_credentials = Aws::Credentials.new(
|
|
40
|
+
registry_details.fetch("username"),
|
|
41
|
+
registry_details.fetch("password")
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
registry_hostname = registry_details.fetch("registry")
|
|
45
|
+
region = registry_hostname.match(AWS_ECR_URL).
|
|
46
|
+
named_captures.fetch("region")
|
|
47
|
+
|
|
48
|
+
@authorization_tokens ||= {}
|
|
49
|
+
@authorization_tokens[registry_hostname] ||=
|
|
50
|
+
Aws::ECR::Client.new(region: region, credentials: aws_credentials).
|
|
51
|
+
get_authorization_token.authorization_data.first.
|
|
52
|
+
authorization_token
|
|
53
|
+
|
|
54
|
+
username, password =
|
|
55
|
+
Base64.decode64(@authorization_tokens[registry_hostname]).split(":")
|
|
56
|
+
|
|
57
|
+
registry_details.merge("username" => username, "password" => password)
|
|
58
|
+
rescue Aws::Errors::MissingCredentialsError,
|
|
59
|
+
Aws::ECR::Errors::UnrecognizedClientException
|
|
60
|
+
raise PrivateSourceAuthenticationFailure, registry_hostname
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dependabot-docker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.77.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dependabot
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-12-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dependabot-core
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.77.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.77.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: byebug
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '12'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '12'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.8'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.8'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec-its
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.2'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.2'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec_junit_formatter
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0.4'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0.4'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0.61'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0.61'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: vcr
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '4.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '4.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: webmock
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '3.4'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '3.4'
|
|
139
|
+
description: Automated dependency management for Ruby, JavaScript, Python, PHP, Elixir,
|
|
140
|
+
Rust, Java, .NET, Elm and Go
|
|
141
|
+
email: support@dependabot.com
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- lib/dependabot/docker/file_fetcher.rb
|
|
147
|
+
- lib/dependabot/docker/file_parser.rb
|
|
148
|
+
- lib/dependabot/docker/file_updater.rb
|
|
149
|
+
- lib/dependabot/docker/metadata_finder.rb
|
|
150
|
+
- lib/dependabot/docker/update_checker.rb
|
|
151
|
+
- lib/dependabot/docker/utils/credentials_finder.rb
|
|
152
|
+
homepage: https://github.com/dependabot/dependabot-core
|
|
153
|
+
licenses:
|
|
154
|
+
- Nonstandard
|
|
155
|
+
metadata: {}
|
|
156
|
+
post_install_message:
|
|
157
|
+
rdoc_options: []
|
|
158
|
+
require_paths:
|
|
159
|
+
- lib
|
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - ">="
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: 2.5.0
|
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: 2.5.0
|
|
170
|
+
requirements: []
|
|
171
|
+
rubyforge_project:
|
|
172
|
+
rubygems_version: 2.7.6
|
|
173
|
+
signing_key:
|
|
174
|
+
specification_version: 4
|
|
175
|
+
summary: Terraform support for dependabot-core
|
|
176
|
+
test_files: []
|