dependabot-docker 0.77.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|