diff --git a/docker/lib/dependabot/common/file_parser_helper.rb b/docker/lib/dependabot/common/file_parser_helper.rb new file mode 100644 index 00000000000..454aac9cb33 --- /dev/null +++ b/docker/lib/dependabot/common/file_parser_helper.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "docker_registry2" + +require "dependabot/dependency" +require "dependabot/errors" +require "dependabot/docker/utils/credentials_finder" + +module Dependabot + module Docker + module FileParserHelper + private + + def version_from(parsed_info) + return parsed_info.fetch("tag") if parsed_info.fetch("tag") + + version_from_digest( + registry: parsed_info.fetch("registry"), + image: parsed_info.fetch("image"), + digest: parsed_info.fetch("digest") + ) + end + + def source_from(parsed_info) + source = {} + + %w(registry tag digest).each do |part| + value = parsed_info.fetch(part) + source[part.to_sym] = value if value + end + + source + end + + def version_from_digest(registry:, image:, digest:) + return unless digest + + repo = docker_repo_name(image, registry) + client = docker_registry_client(registry) + client.tags(repo, auto_paginate: true).fetch("tags").find do |tag| + digest == client.digest(repo, tag) + rescue DockerRegistry2::NotFound + # Shouldn't happen, but it does. Example of existing tag with + # no manifest is "library/python", "2-windowsservercore". + false + end + rescue DockerRegistry2::RegistryAuthenticationException, + RestClient::Forbidden + raise if standard_registry?(registry) + + raise PrivateSourceAuthenticationFailure, registry + end + + def docker_repo_name(image, registry) + return image unless standard_registry?(registry) + return image unless image.split("/").count < 2 + + "library/#{image}" + end + + def docker_registry_client(registry) + if registry + credentials = registry_credentials(registry) + + DockerRegistry2::Registry.new( + "https://#{registry}", + user: credentials&.fetch("username", nil), + password: credentials&.fetch("password", nil) + ) + else + DockerRegistry2::Registry.new("https://registry.hub.docker.com") + end + end + + def registry_credentials(registry_url) + credentials_finder.credentials_for_registry(registry_url) + end + + def credentials_finder + @credentials_finder ||= Utils::CredentialsFinder.new(credentials) + end + + def standard_registry?(registry) + return true if registry.nil? + + registry == "registry.hub.docker.com" + end + end + end +end diff --git a/docker/lib/dependabot/common/file_updater_helper.rb b/docker/lib/dependabot/common/file_updater_helper.rb new file mode 100644 index 00000000000..6e4d89a6283 --- /dev/null +++ b/docker/lib/dependabot/common/file_updater_helper.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require "dependabot/file_updaters" +require "dependabot/file_updaters/base" +require "dependabot/errors" + +module Dependabot + module Docker + module FileUpdaterHelper + private + + def update_digest_and_tag(file) + old_declaration_regex = digest_and_tag_regex(old_digest(file)) + + file.content.gsub(old_declaration_regex) do |old_dec| + old_dec. + gsub("@#{old_digest(file)}", "@#{new_digest(file)}"). + gsub(":#{dependency.previous_version}", + ":#{dependency.version}") + end + end + + def update_tag(file) + return unless old_tag(file) + + old_declaration = + if private_registry_url(file) then "#{private_registry_url(file)}/" + else "" + end + old_declaration += "#{dependency.name}:#{old_tag(file)}" + + old_declaration_regex = tag_regex(old_declaration) + + file.content.gsub(old_declaration_regex) do |old_dec| + old_dec.gsub(":#{old_tag(file)}", ":#{new_tag(file)}") + end + end + + def fetch_file_source(file, reqs) + reqs. + find { |req| req[:file] == file.name }. + fetch(:source) + end + + def fetch_property_in_file_source(file, reqs, property) + fetch_file_source(file, reqs).fetch(property) + end + + def specified_with_digest?(file) + fetch_file_source(file, dependency.requirements)[:digest] + end + + def new_digest(file) + return unless specified_with_digest?(file) + + fetch_property_in_file_source(file, dependency.requirements, :digest) + end + + def old_digest(file) + return unless specified_with_digest?(file) + + fetch_property_in_file_source( + file, + dependency.previous_requirements, + :digest + ) + end + + def digest(file, reqs) + return unless specified_with_digest?(file) + + fetch_property_in_file_source(file, reqs, :digest) + end + + def new_tag(file) + fetch_property_in_file_source(file, dependency.requirements, :tag) + end + + def old_tag(file) + fetch_property_in_file_source( + file, + dependency.previous_requirements, + :tag + ) + end + + def private_registry_url(file) + fetch_file_source(file, dependency.requirements)[:registry] + end + end + end +end diff --git a/docker/lib/dependabot/docker.rb b/docker/lib/dependabot/docker.rb index 430a0ed51d7..5411d80cee0 100644 --- a/docker/lib/dependabot/docker.rb +++ b/docker/lib/dependabot/docker.rb @@ -10,6 +10,8 @@ require "dependabot/docker/requirement" require "dependabot/docker/version" +require_relative "docker_compose" + require "dependabot/pull_request_creator/labeler" Dependabot::PullRequestCreator::Labeler. register_label_details("docker", name: "docker", colour: "21ceff") diff --git a/docker/lib/dependabot/docker/file_parser.rb b/docker/lib/dependabot/docker/file_parser.rb index 13d3b0b99df..8b6cf7e8467 100644 --- a/docker/lib/dependabot/docker/file_parser.rb +++ b/docker/lib/dependabot/docker/file_parser.rb @@ -1,18 +1,16 @@ # frozen_string_literal: true -require "docker_registry2" - -require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" -require "dependabot/errors" -require "dependabot/docker/utils/credentials_finder" +require "dependabot/common/file_parser_helper" module Dependabot module Docker class FileParser < Dependabot::FileParsers::Base require "dependabot/file_parsers/base/dependency_set" + include Dependabot::Docker::FileParserHelper + # Details of Docker regular expressions is at # https://github.com/docker/distribution/blob/master/reference/regexp.go DOMAIN_COMPONENT = @@ -72,88 +70,6 @@ def dockerfiles dependency_files end - def version_from(parsed_from_line) - return parsed_from_line.fetch("tag") if parsed_from_line.fetch("tag") - - version_from_digest( - registry: parsed_from_line.fetch("registry"), - image: parsed_from_line.fetch("image"), - digest: parsed_from_line.fetch("digest") - ) - end - - def source_from(parsed_from_line) - source = {} - - if parsed_from_line.fetch("registry") - source[:registry] = parsed_from_line.fetch("registry") - end - - if parsed_from_line.fetch("tag") - source[:tag] = parsed_from_line.fetch("tag") - end - - if parsed_from_line.fetch("digest") - source[:digest] = parsed_from_line.fetch("digest") - end - - source - end - - def version_from_digest(registry:, image:, digest:) - return unless digest - - repo = docker_repo_name(image, registry) - client = docker_registry_client(registry) - client.tags(repo, auto_paginate: true).fetch("tags").find do |tag| - digest == client.digest(repo, tag) - rescue DockerRegistry2::NotFound - # Shouldn't happen, but it does. Example of existing tag with - # no manifest is "library/python", "2-windowsservercore". - false - end - rescue DockerRegistry2::RegistryAuthenticationException, - RestClient::Forbidden - raise if standard_registry?(registry) - - raise PrivateSourceAuthenticationFailure, registry - end - - def docker_repo_name(image, registry) - return image unless standard_registry?(registry) - return image unless image.split("/").count < 2 - - "library/#{image}" - end - - def docker_registry_client(registry) - if registry - credentials = registry_credentials(registry) - - DockerRegistry2::Registry.new( - "https://#{registry}", - user: credentials&.fetch("username", nil), - password: credentials&.fetch("password", nil) - ) - else - DockerRegistry2::Registry.new("https://registry.hub.docker.com") - end - end - - def registry_credentials(registry_url) - credentials_finder.credentials_for_registry(registry_url) - end - - def credentials_finder - @credentials_finder ||= Utils::CredentialsFinder.new(credentials) - end - - def standard_registry?(registry) - return true if registry.nil? - - registry == "registry.hub.docker.com" - end - def check_required_files # Just check if there are any files at all. return if dependency_files.any? diff --git a/docker/lib/dependabot/docker/file_updater.rb b/docker/lib/dependabot/docker/file_updater.rb index 30054001c54..6c5924cae3a 100644 --- a/docker/lib/dependabot/docker/file_updater.rb +++ b/docker/lib/dependabot/docker/file_updater.rb @@ -3,10 +3,13 @@ require "dependabot/file_updaters" require "dependabot/file_updaters/base" require "dependabot/errors" +require "dependabot/common/file_updater_helper" module Dependabot module Docker class FileUpdater < Dependabot::FileUpdaters::Base + include Dependabot::Docker::FileUpdaterHelper + FROM_REGEX = /FROM/i.freeze def self.updated_files_regex @@ -59,74 +62,14 @@ def updated_dockerfile_content(file) updated_content end - def update_digest_and_tag(file) - old_declaration_regex = /^#{FROM_REGEX}\s+.*@#{old_digest(file)}/ - - file.content.gsub(old_declaration_regex) do |old_dec| - old_dec. - gsub("@#{old_digest(file)}", "@#{new_digest(file)}"). - gsub(":#{dependency.previous_version}", - ":#{dependency.version}") - end + def digest_and_tag_regex(digest) + /^#{FROM_REGEX}\s+.*@#{digest}/ end - def update_tag(file) - return unless old_tag(file) - - old_declaration = - if private_registry_url(file) then "#{private_registry_url(file)}/" - else "" - end - old_declaration += "#{dependency.name}:#{old_tag(file)}" - escaped_declaration = Regexp.escape(old_declaration) - - old_declaration_regex = - %r{^#{FROM_REGEX}\s+(docker\.io/)?#{escaped_declaration}(?=\s|$)} - - file.content.gsub(old_declaration_regex) do |old_dec| - old_dec.gsub(":#{old_tag(file)}", ":#{new_tag(file)}") - end - end - - def specified_with_digest?(file) - dependency. - requirements. - find { |r| r[:file] == file.name }. - fetch(:source)[:digest] - end - - def new_digest(file) - return unless specified_with_digest?(file) - - dependency.requirements. - find { |r| r[:file] == file.name }. - fetch(:source).fetch(:digest) - end - - def old_digest(file) - return unless specified_with_digest?(file) - - dependency.previous_requirements. - find { |r| r[:file] == file.name }. - fetch(:source).fetch(:digest) - end - - def new_tag(file) - dependency.requirements. - find { |r| r[:file] == file.name }. - fetch(:source)[:tag] - end - - def old_tag(file) - dependency.previous_requirements. - find { |r| r[:file] == file.name }. - fetch(:source)[:tag] - end + def tag_regex(declaration) + escaped_declaration = Regexp.escape(declaration) - def private_registry_url(file) - dependency.requirements. - find { |r| r[:file] == file.name }. - fetch(:source)[:registry] + %r{^#{FROM_REGEX}\s+(docker\.io/)?#{escaped_declaration}(?=\s|$)} end end end diff --git a/docker/lib/dependabot/docker/update_checker.rb b/docker/lib/dependabot/docker/update_checker.rb index 0358e09d66e..df29b9f5acf 100644 --- a/docker/lib/dependabot/docker/update_checker.rb +++ b/docker/lib/dependabot/docker/update_checker.rb @@ -53,7 +53,7 @@ def updated_requirements private def latest_version_resolvable_with_full_unlock? - # Full unlock checks aren't relevant for Dockerfiles + # Full unlock checks aren't relevant for Dockerfiles/docker-compose.yml false end @@ -69,8 +69,8 @@ def version_up_to_date? # If the tag isn't up-to-date then we can definitely update return false if version_tag_up_to_date? == false - # Otherwise, if the Dockerfile specifies a digest check that that is - # up-to-date + # Otherwise, if the Dockerfile/docker-compose.yml specifies a digest + # check that that is up-to-date digest_up_to_date? end diff --git a/docker/lib/dependabot/docker_compose.rb b/docker/lib/dependabot/docker_compose.rb new file mode 100644 index 00000000000..75abfae0baa --- /dev/null +++ b/docker/lib/dependabot/docker_compose.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# These all need to be required so the various classes can be registered in a +# lookup table of package manager names to concrete classes. +require "dependabot/docker_compose/file_fetcher" +require "dependabot/docker_compose/file_parser" +require "dependabot/docker_compose/update_checker" +require "dependabot/docker_compose/file_updater" +require "dependabot/docker_compose/metadata_finder" +require "dependabot/docker_compose/requirement" +require "dependabot/docker_compose/version" + +require "dependabot/pull_request_creator/labeler" +Dependabot::PullRequestCreator::Labeler. + register_label_details("docker_compose", name: "docker", colour: "21ceff") + +require "dependabot/dependency" +Dependabot::Dependency.register_production_check( + "docker_compose", + ->(_) { true } +) diff --git a/docker/lib/dependabot/docker_compose/file_fetcher.rb b/docker/lib/dependabot/docker_compose/file_fetcher.rb new file mode 100644 index 00000000000..aff6e22838c --- /dev/null +++ b/docker/lib/dependabot/docker_compose/file_fetcher.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "dependabot/file_fetchers" +require "dependabot/file_fetchers/base" + +module Dependabot + module DockerCompose + class FileFetcher < Dependabot::FileFetchers::Base + FILENAME_REGEX = /docker-compose(?>\.override)?\.yml/i.freeze + + def self.required_files_in?(filenames) + filenames.any? { |f| f.match?(FILENAME_REGEX) } + end + + def self.required_files_message + "Repo must contain a docker-compose.yaml file." + end + + private + + def fetch_files + fetched_files = [] + fetched_files += correctly_encoded_docker_compose_files + + return fetched_files if fetched_files.any? + + if incorrectly_encoded_docker_compose_files.none? + raise( + Dependabot::DependencyFileNotFound, + File.join(directory, "docker-compose.yml") + ) + else + raise( + Dependabot::DependencyFileNotParseable, + incorrectly_encoded_docker_compose_files.first.path + ) + end + end + + def docker_compose_files + @docker_compose_files ||= + repo_contents(raise_errors: false). + select { |f| f.type == "file" && f.name.match?(FILENAME_REGEX) }. + map { |f| fetch_file_from_host(f.name) } + end + + def correctly_encoded_docker_compose_files + docker_compose_files.select { |f| f.content.valid_encoding? } + end + + def incorrectly_encoded_docker_compose_files + docker_compose_files.reject { |f| f.content.valid_encoding? } + end + end + end +end + +Dependabot::FileFetchers.register( + "docker_compose", + Dependabot::DockerCompose::FileFetcher +) diff --git a/docker/lib/dependabot/docker_compose/file_parser.rb b/docker/lib/dependabot/docker_compose/file_parser.rb new file mode 100644 index 00000000000..8e69630c76b --- /dev/null +++ b/docker/lib/dependabot/docker_compose/file_parser.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "yaml" + +require "dependabot/file_parsers" +require "dependabot/file_parsers/base" +require "dependabot/common/file_parser_helper" + +module Dependabot + module DockerCompose + class FileParser < Dependabot::FileParsers::Base + require "dependabot/file_parsers/base/dependency_set" + + include Dependabot::Docker::FileParserHelper + + # Details of Docker regular expressions is at + # https://github.com/docker/distribution/blob/master/reference/regexp.go + DOMAIN_COMPONENT = + /(?:[[:alnum:]]|[[:alnum:]][[[:alnum:]]-]*[[:alnum:]])/.freeze + DOMAIN = /(?:#{DOMAIN_COMPONENT}(?:\.#{DOMAIN_COMPONENT})+)/.freeze + REGISTRY = /(?#{DOMAIN}(?::\d+)?)/.freeze + + NAME_COMPONENT = /(?:[a-z\d]+(?:(?:[._]|__|[-]*)[a-z\d]+)*)/.freeze + IMAGE = %r{(?#{NAME_COMPONENT}(?:/#{NAME_COMPONENT})*)}.freeze + + TAG = /:(?[\w][\w.-]{0,127})/.freeze + DIGEST = /@(?[^\s]+)/.freeze + NAME = /\s+AS\s+(?[\w-]+)/.freeze + FROM_IMAGE = + %r{^(#{REGISTRY}/)?#{IMAGE}#{TAG}?#{DIGEST}?#{NAME}?}.freeze + + def parse + dependency_set = DependencySet.new + + composefiles.each do |composefile| + yaml = YAML.safe_load(composefile.content) + yaml["services"].each do |_, service| + parsed_from_image = + FROM_IMAGE.match(service["image"]).named_captures + if parsed_from_image["registry"] == "docker.io" + parsed_from_image["registry"] = nil + end + + version = version_from(parsed_from_image) + next unless version + + dependency_set << Dependency.new( + name: parsed_from_image["image"], + version: version, + package_manager: "docker_compose", + requirements: [ + requirement: nil, + groups: [], + file: composefile.name, + source: source_from(parsed_from_image) + ] + ) + end + end + + dependency_set.dependencies + end + + private + + def composefiles + # The DockerCompose file fetcher only fetches docker-compose.yml files, + # so no need to filter here + dependency_files + end + + def check_required_files + # Just check if there are any files at all. + return if dependency_files.any? + + raise "No docker-compose.yml file!" + end + end + end +end + +Dependabot::FileParsers.register( + "docker_compose", + Dependabot::DockerCompose::FileParser +) diff --git a/docker/lib/dependabot/docker_compose/file_updater.rb b/docker/lib/dependabot/docker_compose/file_updater.rb new file mode 100644 index 00000000000..a1bde5dcdbf --- /dev/null +++ b/docker/lib/dependabot/docker_compose/file_updater.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require "dependabot/file_updaters" +require "dependabot/file_updaters/base" +require "dependabot/errors" +require "dependabot/common/file_updater_helper" + +module Dependabot + module DockerCompose + class FileUpdater < Dependabot::FileUpdaters::Base + include Dependabot::Docker::FileUpdaterHelper + + IMAGE_REGEX = /image:\s*/.freeze + + def self.updated_files_regex + [/docker-compose\.yml/i] + end + + def updated_dependency_files + updated_files = [] + + dependency_files.each do |file| + next unless requirement_changed?(file, dependency) + + updated_files << + updated_file( + file: file, + content: updated_dockercompose_file_content(file) + ) + end + + updated_files.reject! { |f| dependency_files.include?(f) } + raise "No files changed!" if updated_files.none? + + updated_files + end + + private + + def dependency + # docker-compose.yml files will only ever be updating + # a single dependency + dependencies.first + end + + def check_required_files + # Just check if there are any files at all. + return if dependency_files.any? + + raise "No docker-compose.yml file!" + end + + def updated_dockercompose_file_content(file) + updated_content = + if specified_with_digest?(file) + update_digest_and_tag(file) + else + update_tag(file) + end + + raise "Expected content to change!" if updated_content == file.content + + updated_content + end + + def digest_and_tag_regex(digest) + /^\s*#{IMAGE_REGEX}\s+.*@#{digest}/ + end + + def tag_regex(declaration) + escaped_declaration = Regexp.escape(declaration) + + %r{^\s*#{IMAGE_REGEX}\s+(docker\.io/)?#{escaped_declaration}(?=\s|$)} + end + end + end +end + +Dependabot::FileUpdaters.register( + "docker_compose", + Dependabot::DockerCompose::FileUpdater +) diff --git a/docker/lib/dependabot/docker_compose/metadata_finder.rb b/docker/lib/dependabot/docker_compose/metadata_finder.rb new file mode 100644 index 00000000000..e551e22ee0d --- /dev/null +++ b/docker/lib/dependabot/docker_compose/metadata_finder.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require "dependabot/metadata_finders" +require "dependabot/metadata_finders/base" +require "dependabot/docker/metadata_finder" + +Dependabot::MetadataFinders. + register("docker_compose", Dependabot::Docker::MetadataFinder) diff --git a/docker/lib/dependabot/docker_compose/requirement.rb b/docker/lib/dependabot/docker_compose/requirement.rb new file mode 100644 index 00000000000..c281cc7338c --- /dev/null +++ b/docker/lib/dependabot/docker_compose/requirement.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "dependabot/utils" +require "dependabot/docker/requirement" + +Dependabot::Utils. + register_requirement_class("docker_compose", Dependabot::Docker::Requirement) diff --git a/docker/lib/dependabot/docker_compose/update_checker.rb b/docker/lib/dependabot/docker_compose/update_checker.rb new file mode 100644 index 00000000000..867dd959eba --- /dev/null +++ b/docker/lib/dependabot/docker_compose/update_checker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require "dependabot/update_checkers" +require "dependabot/update_checkers/base" +require "dependabot/docker_compose/version" +require "dependabot/docker_compose/requirement" +require "dependabot/docker/update_checker" + +module Dependabot + module DockerCompose + class UpdateChecker < Dependabot::Docker::UpdateChecker + end + end +end + +Dependabot::UpdateCheckers.register( + "docker_compose", + Dependabot::DockerCompose::UpdateChecker +) diff --git a/docker/lib/dependabot/docker_compose/version.rb b/docker/lib/dependabot/docker_compose/version.rb new file mode 100644 index 00000000000..b771cfdf107 --- /dev/null +++ b/docker/lib/dependabot/docker_compose/version.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require "dependabot/utils" +require "dependabot/docker/version" + +Dependabot::Utils. + register_version_class("docker_compose", Dependabot::Docker::Version) diff --git a/docker/spec/dependabot/docker/common/shared_examples_for_docker_update_checkers.rb b/docker/spec/dependabot/docker/common/shared_examples_for_docker_update_checkers.rb new file mode 100644 index 00000000000..283f8043381 --- /dev/null +++ b/docker/spec/dependabot/docker/common/shared_examples_for_docker_update_checkers.rb @@ -0,0 +1,652 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency" +require "dependabot/update_checkers/base" +require_common_spec "update_checkers/shared_examples_for_update_checkers" + +RSpec.shared_examples "a Docker update checker" do + it_behaves_like "an update checker" + + let(:checker) do + described_class.new( + dependency: dependency, + dependency_files: [], + credentials: credentials, + ignored_versions: ignored_versions + ) + end + let(:ignored_versions) { [] } + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: version, + requirements: [{ + requirement: nil, + groups: [], + file: file_name, + source: source + }], + package_manager: package_manager + ) + end + let(:dependency_name) { "ubuntu" } + let(:version) { "17.04" } + let(:source) { { tag: version } } + let(:repo_url) { "https://registry.hub.docker.com/v2/library/ubuntu/" } + let(:registry_tags) { fixture("docker", "registry_tags", tags_fixture_name) } + let(:tags_fixture_name) { "ubuntu_no_latest.json" } + + before do + auth_url = "https://auth.docker.io/token?service=registry.docker.io" + stub_request(:get, auth_url). + and_return(status: 200, body: { token: "token" }.to_json) + + stub_request(:get, repo_url + "tags/list"). + and_return(status: 200, body: registry_tags) + end + + describe "#can_update?" do + subject { checker.can_update?(requirements_to_unlock: :own) } + + context "given an outdated dependency" do + let(:version) { "17.04" } + it { is_expected.to be_truthy } + end + + context "given an up-to-date dependency" do + let(:version) { "17.10" } + it { is_expected.to be_falsey } + end + + context "given a purely numeric version" do + let(:version) { "1234567890" } + it { is_expected.to be_truthy } + end + + context "given a non-numeric version" do + let(:version) { "artful" } + it { is_expected.to be_falsey } + + context "and a digest" do + let(:source) { { digest: "old_digest" } } + let(:headers_response) do + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + end + + before do + stub_request(:head, repo_url + "manifests/artful"). + and_return(status: 200, headers: JSON.parse(headers_response)) + end + + context "that is out-of-date" do + let(:source) { { digest: "old_digest" } } + it { is_expected.to be_truthy } + + context "but the response doesn't include a new digest" do + let(:headers_response) do + fixture( + "docker", + "registry_manifest_headers", + "ubuntu_17.10.json" + ).gsub(/^\s*"docker_content_digest.*?,/m, "") + end + + it { is_expected.to be_falsey } + end + end + + context "that is up-to-date" do + let(:source) do + { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86ca97"\ + "eba880ebf600d68608" + } + end + + it { is_expected.to be_falsey } + end + end + end + + context "when the 'latest' version is just a more precise one" do + let(:dependency_name) { "python" } + let(:version) { "3.6" } + let(:tags_fixture_name) { "python.json" } + let(:headers_response) do + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + end + let(:repo_url) { "https://registry.hub.docker.com/v2/library/python/" } + + before do + stub_request(:get, repo_url + "tags/list"). + and_return(status: 200, body: registry_tags) + stub_request(:head, repo_url + "manifests/3.6"). + and_return(status: 200, headers: JSON.parse(headers_response)) + stub_request(:head, repo_url + "manifests/3.6.3"). + and_return(status: 200, headers: JSON.parse(headers_response)) + end + + it { is_expected.to be_falsey } + end + end + + describe "#latest_version" do + subject { checker.latest_version } + + it { is_expected.to eq("17.10") } + + context "when the dependency has a non-numeric version" do + let(:version) { "artful" } + it { is_expected.to eq("artful") } + + context "that starts with a number" do + let(:version) { "309403913c7f0848e6616446edec909b55d53571" } + it { is_expected.to eq("309403913c7f0848e6616446edec909b55d53571") } + end + end + + context "when versions at different specificities look equal" do + let(:dependency_name) { "ruby" } + let(:version) { "2.4.0-slim" } + let(:tags_fixture_name) { "ruby_25.json" } + before do + tags_url = "https://registry.hub.docker.com/v2/library/ruby/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("2.5.0-slim") } + end + + context "when the latest version is being ignored" do + let(:ignored_versions) { [">= 17.10"] } + it { is_expected.to eq("17.04") } + end + + context "when there are also date-like versions" do + let(:tags_fixture_name) { "windows-servercore.json" } + let(:version) { "10.0.16299.1087" } + + it { is_expected.to eq("10.0.18362.175") } + + context "and we're using one" do + let(:version) { "1803" } + it { is_expected.to eq("1903") } + end + end + + context "when there is a latest tag" do + let(:tags_fixture_name) { "ubuntu.json" } + let(:headers_response) do + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + end + let(:version) { "12.10" } + + before do + stub_request(:head, repo_url + "manifests/17.10"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response) + ) + + # Stub the latest version to return a different digest + ["17.04", "latest"].each do |version| + stub_request(:head, repo_url + "manifests/#{version}"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) + ) + end + end + + it { is_expected.to eq("17.04") } + end + + context "when the dependency's version has a prefix" do + let(:version) { "artful-20170826" } + it { is_expected.to eq("artful-20170916") } + end + + context "when the dependency's version starts with a 'v'" do + let(:version) { "v1.5.0" } + let(:tags_fixture_name) { "kube_state_metrics.json" } + it { is_expected.to eq("v1.6.0") } + end + + context "when the dependency has SHA suffices that should be ignored" do + let(:tags_fixture_name) { "sha_suffices.json" } + let(:version) { "7.2-0.1" } + it { is_expected.to eq("7.2-0.3.1") } + + context "for an older version of the prefix" do + let(:version) { "7.1-0.1" } + it { is_expected.to eq("7.1-0.3.1") } + end + end + + context "when the docker registry times out" do + before do + stub_request(:get, repo_url + "tags/list"). + to_raise(RestClient::Exceptions::OpenTimeout).then. + to_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("17.10") } + + context "every time" do + before do + stub_request(:get, repo_url + "tags/list"). + to_raise(RestClient::Exceptions::OpenTimeout) + end + + it "raises" do + expect { checker.latest_version }. + to raise_error(RestClient::Exceptions::OpenTimeout) + end + + context "for a private registry" do + let(:dependency_name) { "ubuntu" } + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: version, + requirements: [{ + requirement: nil, + groups: [], + file: file_name, + source: { registry: "registry-host.io:5000" } + }], + package_manager: package_manager + ) + end + let(:repo_url) { "https://registry-host.io:5000/v2/ubuntu/" } + let(:tags_fixture_name) { "ubuntu_no_latest.json" } + + it "raises" do + expect { checker.latest_version }. + to raise_error(Dependabot::PrivateSourceTimedOut) + end + end + end + end + + context "when the dependency's version has a suffix" do + let(:dependency_name) { "ruby" } + let(:version) { "2.4.0-slim" } + let(:tags_fixture_name) { "ruby.json" } + before do + tags_url = "https://registry.hub.docker.com/v2/library/ruby/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("2.4.2-slim") } + end + + context "when the dependency's version has a prefix and a suffix" do + let(:dependency_name) { "adoptopenjdk/openjdk11" } + let(:version) { "jdk-11.0.2.7-alpine-slim" } + let(:tags_fixture_name) { "openjdk11.json" } + let(:repo_url) do + "https://registry.hub.docker.com/v2/adoptopenjdk/openjdk11/" + end + let(:headers_response) do + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + end + before do + stub_request(:get, repo_url + "tags/list"). + and_return(status: 200, body: registry_tags) + + stub_request(:head, repo_url + "manifests/#{version}"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response) + ) + + # Stub the latest version to return a different digest + ["jdk-11.0.2.9", "latest"].each do |version| + stub_request(:head, repo_url + "manifests/#{version}"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) + ) + end + + # Stub an oddly-formatted version to come back as a pre-release + stub_request(:head, repo_url + "manifests/jdk-11.28"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response.gsub("3ea1ca1", "11171a2")) + ) + end + + it { is_expected.to eq("jdk-11.0.2.9-alpine-slim") } + end + + context "when the dependency has a namespace" do + let(:dependency_name) { "moj/ruby" } + let(:tags_fixture_name) { "ruby.json" } + before do + tags_url = "https://registry.hub.docker.com/v2/moj/ruby/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("2.4.2") } + + context "and dockerhub 401s" do + before do + tags_url = "https://registry.hub.docker.com/v2/moj/ruby/tags/list" + stub_request(:get, tags_url). + and_return( + status: 401, + body: "", + headers: { "www_authenticate" => "basic 123" } + ) + end + + it "raises a to PrivateSourceAuthenticationFailure error" do + error_class = Dependabot::PrivateSourceAuthenticationFailure + expect { checker.latest_version }. + to raise_error(error_class) do |error| + expect(error.source).to eq("registry.hub.docker.com") + end + end + end + end + + context "when the latest version is a pre-release" do + let(:dependency_name) { "python" } + let(:version) { "3.5" } + let(:tags_fixture_name) { "python.json" } + before do + tags_url = "https://registry.hub.docker.com/v2/library/python/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("3.6.3") } + + context "and the current version is a pre-release" do + let(:version) { "3.7.0a1" } + it { is_expected.to eq("3.7.0a2") } + end + end + + context "when the latest tag points to an older version" do + let(:tags_fixture_name) { "dotnet.json" } + let(:headers_response) do + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + end + let(:version) { "2.0-sdk" } + let(:latest_versions) { %w(2-sdk 2.1-sdk 2.1.401-sdk) } + + before do + stub_request(:head, repo_url + "manifests/2.2-sdk"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response) + ) + + # Stub the latest version to return a different digest + [*latest_versions, "latest"].each do |version| + stub_request(:head, repo_url + "manifests/#{version}"). + and_return( + status: 200, + body: "", + headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) + ) + end + end + + it { is_expected.to eq("2.1.401-sdk") } + + context "and a suffix" do + let(:version) { "2.0-runtime" } + it { is_expected.to eq("2.1.3-runtime") } + end + + context "with a paginated response" do + let(:pagination_headers) do + fixture("docker", "registry_pagination_headers", "next_link.json") + end + let(:end_pagination_headers) do + fixture("docker", "registry_pagination_headers", "no_next_link.json") + end + before do + stub_request(:get, repo_url + "tags/list"). + and_return( + status: 200, + body: fixture("docker", "registry_tags", "dotnet_page_1.json"), + headers: JSON.parse(pagination_headers) + ) + last = "ukD72mdD/mC8b5xV3susmJzzaTgp3hKwR9nRUW1yZZ6dLc5kfZtKLT2ICo63"\ + "WYvt2jq2VyIS3LWB%2Bo9HjGuiYQ6hARJz1jTFdW4jEMKPIg4kRwXypd7HXj"\ + "/SnA9iMm3YvNsd4LmPQrO4fpYZgnZZ8rzIIYqex6%2B3A3/mKcTsNKkKDV9V"\ + "R3ic6RJjYFCMOEk5/eqsfLaCDYEbtCNoxE2fBDwlzIl/W14f/F%2Bb%2BtQR"\ + "Gh3eUKE9nBJpVvAfibAEs215m4ePJm%2BNuVktVjHOYlRG3U03ekr1T7CPD1"\ + "Q%2B65wVYi0y2nCIl1/V40nkgG2WX5viYDxUuk3nEdnf55GUocnt38sDZzqB"\ + "nyglM9jvbxBzlO8=" + stub_request(:get, repo_url + "tags/list?last=#{last}"). + and_return( + status: 200, + body: fixture("docker", "registry_tags", "dotnet_page_2.json"), + headers: JSON.parse(end_pagination_headers) + ) + end + + it { is_expected.to eq("2.1.401-sdk") } + end + + context "when the latest tag 404s" do + before do + stub_request(:head, repo_url + "manifests/latest"). + to_return(status: 404).then. + to_return( + status: 200, + body: "", + headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) + ) + end + + it { is_expected.to eq("2.1.401-sdk") } + + context "every time" do + before do + stub_request(:head, repo_url + "manifests/latest"). + to_return(status: 404) + end + + it { is_expected.to eq("2.2-sdk") } + end + end + end + + context "when the dependency's version has a suffix with periods" do + let(:dependency_name) { "python" } + let(:version) { "3.6.2-alpine3.6" } + let(:tags_fixture_name) { "python.json" } + before do + tags_url = "https://registry.hub.docker.com/v2/library/python/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("3.6.3-alpine3.6") } + end + + context "when the dependency has a private registry" do + let(:dependency_name) { "ubuntu" } + let(:dependency) do + Dependabot::Dependency.new( + name: dependency_name, + version: version, + requirements: [{ + requirement: nil, + groups: [], + file: file_name, + source: { registry: "registry-host.io:5000" } + }], + package_manager: package_manager + ) + end + let(:tags_fixture_name) { "ubuntu_no_latest.json" } + + context "without authentication credentials" do + before do + tags_url = "https://registry-host.io:5000/v2/ubuntu/tags/list" + stub_request(:get, tags_url). + and_return( + status: 401, + body: "", + headers: { "www_authenticate" => "basic 123" } + ) + end + + it "raises a to PrivateSourceAuthenticationFailure error" do + error_class = Dependabot::PrivateSourceAuthenticationFailure + expect { checker.latest_version }. + to raise_error(error_class) do |error| + expect(error.source).to eq("registry-host.io:5000") + end + end + end + + context "with authentication credentials" do + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }, { + "type" => "docker_registry", + "registry" => "registry-host.io:5000", + "username" => "grey", + "password" => "pa55word" + }] + end + + before do + tags_url = "https://registry-host.io:5000/v2/ubuntu/tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + it { is_expected.to eq("17.10") } + + context "that don't have a username or password" do + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }, { + "type" => "docker_registry", + "registry" => "registry-host.io:5000" + }] + end + + it { is_expected.to eq("17.10") } + end + end + end + end + + describe "#latest_resolvable_version" do + subject { checker.latest_resolvable_version } + + before { allow(checker).to receive(:latest_version).and_return("delegate") } + it { is_expected.to eq("delegate") } + end + + describe "#updated_requirements" do + subject { checker.updated_requirements } + + context "when specified with a tag" do + let(:source) { { tag: version } } + + it "updates the tag" do + expect(checker.updated_requirements). + to eq( + [{ + requirement: nil, + groups: [], + file: file_name, + source: { tag: "17.10" } + }] + ) + end + end + + context "when specified with a digest" do + let(:source) { { digest: "old_digest" } } + + before do + new_headers = + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + stub_request(:head, repo_url + "manifests/17.10"). + and_return(status: 200, body: "", headers: JSON.parse(new_headers)) + end + + it "updates the digest" do + expect(checker.updated_requirements). + to eq( + [{ + requirement: nil, + groups: [], + file: file_name, + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608" + } + }] + ) + end + end + + context "when specified with a digest and a tag" do + let(:source) { { digest: "old_digest", tag: "17.04" } } + + before do + new_headers = + fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") + stub_request(:head, repo_url + "manifests/17.10"). + and_return(status: 200, body: "", headers: JSON.parse(new_headers)) + end + + it "updates the tag and the digest" do + expect(checker.updated_requirements). + to eq( + [{ + requirement: nil, + groups: [], + file: file_name, + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608", + tag: "17.10" + } + }] + ) + end + end + end +end diff --git a/docker/spec/dependabot/docker/file_fetcher_spec.rb b/docker/spec/dependabot/docker/file_fetcher_spec.rb index 6dfdbddf6e3..ff397bc016a 100644 --- a/docker/spec/dependabot/docker/file_fetcher_spec.rb +++ b/docker/spec/dependabot/docker/file_fetcher_spec.rb @@ -50,7 +50,9 @@ ) end - let(:dockerfile_fixture) { fixture("github", "contents_dockerfile.json") } + let(:dockerfile_fixture) do + fixture("github", "contents_dockerfile.json") + end it "fetches the Dockerfile" do expect(file_fetcher_instance.files.count).to eq(1) @@ -93,8 +95,12 @@ ) end - let(:dockerfile_fixture) { fixture("github", "contents_dockerfile.json") } - let(:dockerfile_2_fixture) { fixture("github", "contents_dockerfile.json") } + let(:dockerfile_fixture) do + fixture("github", "contents_dockerfile.json") + end + let(:dockerfile_2_fixture) do + fixture("github", "contents_dockerfile.json") + end it "fetches both Dockerfiles" do expect(file_fetcher_instance.files.count).to eq(2) diff --git a/docker/spec/dependabot/docker/update_checker_spec.rb b/docker/spec/dependabot/docker/update_checker_spec.rb index b813a4ab4f4..13fdcc8c6d3 100644 --- a/docker/spec/dependabot/docker/update_checker_spec.rb +++ b/docker/spec/dependabot/docker/update_checker_spec.rb @@ -1,652 +1,12 @@ # frozen_string_literal: true require "spec_helper" -require "dependabot/dependency" require "dependabot/docker/update_checker" -require_common_spec "update_checkers/shared_examples_for_update_checkers" +require "dependabot/docker/common/shared_examples_for_docker_update_checkers.rb" RSpec.describe Dependabot::Docker::UpdateChecker do - it_behaves_like "an update checker" + it_behaves_like "a Docker update checker" - let(:checker) do - described_class.new( - dependency: dependency, - dependency_files: [], - credentials: credentials, - ignored_versions: ignored_versions - ) - end - let(:ignored_versions) { [] } - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: version, - requirements: [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: source - }], - package_manager: "docker" - ) - end - let(:dependency_name) { "ubuntu" } - let(:version) { "17.04" } - let(:source) { { tag: version } } - let(:repo_url) { "https://registry.hub.docker.com/v2/library/ubuntu/" } - let(:registry_tags) { fixture("docker", "registry_tags", tags_fixture_name) } - let(:tags_fixture_name) { "ubuntu_no_latest.json" } - - before do - auth_url = "https://auth.docker.io/token?service=registry.docker.io" - stub_request(:get, auth_url). - and_return(status: 200, body: { token: "token" }.to_json) - - stub_request(:get, repo_url + "tags/list"). - and_return(status: 200, body: registry_tags) - end - - describe "#can_update?" do - subject { checker.can_update?(requirements_to_unlock: :own) } - - context "given an outdated dependency" do - let(:version) { "17.04" } - it { is_expected.to be_truthy } - end - - context "given an up-to-date dependency" do - let(:version) { "17.10" } - it { is_expected.to be_falsey } - end - - context "given a purely numeric version" do - let(:version) { "1234567890" } - it { is_expected.to be_truthy } - end - - context "given a non-numeric version" do - let(:version) { "artful" } - it { is_expected.to be_falsey } - - context "and a digest" do - let(:source) { { digest: "old_digest" } } - let(:headers_response) do - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - end - - before do - stub_request(:head, repo_url + "manifests/artful"). - and_return(status: 200, headers: JSON.parse(headers_response)) - end - - context "that is out-of-date" do - let(:source) { { digest: "old_digest" } } - it { is_expected.to be_truthy } - - context "but the response doesn't include a new digest" do - let(:headers_response) do - fixture( - "docker", - "registry_manifest_headers", - "ubuntu_17.10.json" - ).gsub(/^\s*"docker_content_digest.*?,/m, "") - end - - it { is_expected.to be_falsey } - end - end - - context "that is up-to-date" do - let(:source) do - { - digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86ca97"\ - "eba880ebf600d68608" - } - end - - it { is_expected.to be_falsey } - end - end - end - - context "when the 'latest' version is just a more precise one" do - let(:dependency_name) { "python" } - let(:version) { "3.6" } - let(:tags_fixture_name) { "python.json" } - let(:headers_response) do - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - end - let(:repo_url) { "https://registry.hub.docker.com/v2/library/python/" } - - before do - stub_request(:get, repo_url + "tags/list"). - and_return(status: 200, body: registry_tags) - stub_request(:head, repo_url + "manifests/3.6"). - and_return(status: 200, headers: JSON.parse(headers_response)) - stub_request(:head, repo_url + "manifests/3.6.3"). - and_return(status: 200, headers: JSON.parse(headers_response)) - end - - it { is_expected.to be_falsey } - end - end - - describe "#latest_version" do - subject { checker.latest_version } - - it { is_expected.to eq("17.10") } - - context "when the dependency has a non-numeric version" do - let(:version) { "artful" } - it { is_expected.to eq("artful") } - - context "that starts with a number" do - let(:version) { "309403913c7f0848e6616446edec909b55d53571" } - it { is_expected.to eq("309403913c7f0848e6616446edec909b55d53571") } - end - end - - context "when versions at different specificities look equal" do - let(:dependency_name) { "ruby" } - let(:version) { "2.4.0-slim" } - let(:tags_fixture_name) { "ruby_25.json" } - before do - tags_url = "https://registry.hub.docker.com/v2/library/ruby/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("2.5.0-slim") } - end - - context "when the latest version is being ignored" do - let(:ignored_versions) { [">= 17.10"] } - it { is_expected.to eq("17.04") } - end - - context "when there are also date-like versions" do - let(:tags_fixture_name) { "windows-servercore.json" } - let(:version) { "10.0.16299.1087" } - - it { is_expected.to eq("10.0.18362.175") } - - context "and we're using one" do - let(:version) { "1803" } - it { is_expected.to eq("1903") } - end - end - - context "when there is a latest tag" do - let(:tags_fixture_name) { "ubuntu.json" } - let(:headers_response) do - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - end - let(:version) { "12.10" } - - before do - stub_request(:head, repo_url + "manifests/17.10"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response) - ) - - # Stub the latest version to return a different digest - ["17.04", "latest"].each do |version| - stub_request(:head, repo_url + "manifests/#{version}"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) - ) - end - end - - it { is_expected.to eq("17.04") } - end - - context "when the dependency's version has a prefix" do - let(:version) { "artful-20170826" } - it { is_expected.to eq("artful-20170916") } - end - - context "when the dependency's version starts with a 'v'" do - let(:version) { "v1.5.0" } - let(:tags_fixture_name) { "kube_state_metrics.json" } - it { is_expected.to eq("v1.6.0") } - end - - context "when the dependency has SHA suffices that should be ignored" do - let(:tags_fixture_name) { "sha_suffices.json" } - let(:version) { "7.2-0.1" } - it { is_expected.to eq("7.2-0.3.1") } - - context "for an older version of the prefix" do - let(:version) { "7.1-0.1" } - it { is_expected.to eq("7.1-0.3.1") } - end - end - - context "when the docker registry times out" do - before do - stub_request(:get, repo_url + "tags/list"). - to_raise(RestClient::Exceptions::OpenTimeout).then. - to_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("17.10") } - - context "every time" do - before do - stub_request(:get, repo_url + "tags/list"). - to_raise(RestClient::Exceptions::OpenTimeout) - end - - it "raises" do - expect { checker.latest_version }. - to raise_error(RestClient::Exceptions::OpenTimeout) - end - - context "for a private registry" do - let(:dependency_name) { "ubuntu" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: version, - requirements: [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: { registry: "registry-host.io:5000" } - }], - package_manager: "docker" - ) - end - let(:repo_url) { "https://registry-host.io:5000/v2/ubuntu/" } - let(:tags_fixture_name) { "ubuntu_no_latest.json" } - - it "raises" do - expect { checker.latest_version }. - to raise_error(Dependabot::PrivateSourceTimedOut) - end - end - end - end - - context "when the dependency's version has a suffix" do - let(:dependency_name) { "ruby" } - let(:version) { "2.4.0-slim" } - let(:tags_fixture_name) { "ruby.json" } - before do - tags_url = "https://registry.hub.docker.com/v2/library/ruby/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("2.4.2-slim") } - end - - context "when the dependency's version has a prefix and a suffix" do - let(:dependency_name) { "adoptopenjdk/openjdk11" } - let(:version) { "jdk-11.0.2.7-alpine-slim" } - let(:tags_fixture_name) { "openjdk11.json" } - let(:repo_url) do - "https://registry.hub.docker.com/v2/adoptopenjdk/openjdk11/" - end - let(:headers_response) do - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - end - before do - stub_request(:get, repo_url + "tags/list"). - and_return(status: 200, body: registry_tags) - - stub_request(:head, repo_url + "manifests/#{version}"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response) - ) - - # Stub the latest version to return a different digest - ["jdk-11.0.2.9", "latest"].each do |version| - stub_request(:head, repo_url + "manifests/#{version}"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) - ) - end - - # Stub an oddly-formatted version to come back as a pre-release - stub_request(:head, repo_url + "manifests/jdk-11.28"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response.gsub("3ea1ca1", "11171a2")) - ) - end - - it { is_expected.to eq("jdk-11.0.2.9-alpine-slim") } - end - - context "when the dependency has a namespace" do - let(:dependency_name) { "moj/ruby" } - let(:tags_fixture_name) { "ruby.json" } - before do - tags_url = "https://registry.hub.docker.com/v2/moj/ruby/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("2.4.2") } - - context "and dockerhub 401s" do - before do - tags_url = "https://registry.hub.docker.com/v2/moj/ruby/tags/list" - stub_request(:get, tags_url). - and_return( - status: 401, - body: "", - headers: { "www_authenticate" => "basic 123" } - ) - end - - it "raises a to PrivateSourceAuthenticationFailure error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { checker.latest_version }. - to raise_error(error_class) do |error| - expect(error.source).to eq("registry.hub.docker.com") - end - end - end - end - - context "when the latest version is a pre-release" do - let(:dependency_name) { "python" } - let(:version) { "3.5" } - let(:tags_fixture_name) { "python.json" } - before do - tags_url = "https://registry.hub.docker.com/v2/library/python/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("3.6.3") } - - context "and the current version is a pre-release" do - let(:version) { "3.7.0a1" } - it { is_expected.to eq("3.7.0a2") } - end - end - - context "when the latest tag points to an older version" do - let(:tags_fixture_name) { "dotnet.json" } - let(:headers_response) do - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - end - let(:version) { "2.0-sdk" } - let(:latest_versions) { %w(2-sdk 2.1-sdk 2.1.401-sdk) } - - before do - stub_request(:head, repo_url + "manifests/2.2-sdk"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response) - ) - - # Stub the latest version to return a different digest - [*latest_versions, "latest"].each do |version| - stub_request(:head, repo_url + "manifests/#{version}"). - and_return( - status: 200, - body: "", - headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) - ) - end - end - - it { is_expected.to eq("2.1.401-sdk") } - - context "and a suffix" do - let(:version) { "2.0-runtime" } - it { is_expected.to eq("2.1.3-runtime") } - end - - context "with a paginated response" do - let(:pagination_headers) do - fixture("docker", "registry_pagination_headers", "next_link.json") - end - let(:end_pagination_headers) do - fixture("docker", "registry_pagination_headers", "no_next_link.json") - end - before do - stub_request(:get, repo_url + "tags/list"). - and_return( - status: 200, - body: fixture("docker", "registry_tags", "dotnet_page_1.json"), - headers: JSON.parse(pagination_headers) - ) - last = "ukD72mdD/mC8b5xV3susmJzzaTgp3hKwR9nRUW1yZZ6dLc5kfZtKLT2ICo63"\ - "WYvt2jq2VyIS3LWB%2Bo9HjGuiYQ6hARJz1jTFdW4jEMKPIg4kRwXypd7HXj"\ - "/SnA9iMm3YvNsd4LmPQrO4fpYZgnZZ8rzIIYqex6%2B3A3/mKcTsNKkKDV9V"\ - "R3ic6RJjYFCMOEk5/eqsfLaCDYEbtCNoxE2fBDwlzIl/W14f/F%2Bb%2BtQR"\ - "Gh3eUKE9nBJpVvAfibAEs215m4ePJm%2BNuVktVjHOYlRG3U03ekr1T7CPD1"\ - "Q%2B65wVYi0y2nCIl1/V40nkgG2WX5viYDxUuk3nEdnf55GUocnt38sDZzqB"\ - "nyglM9jvbxBzlO8=" - stub_request(:get, repo_url + "tags/list?last=#{last}"). - and_return( - status: 200, - body: fixture("docker", "registry_tags", "dotnet_page_2.json"), - headers: JSON.parse(end_pagination_headers) - ) - end - - it { is_expected.to eq("2.1.401-sdk") } - end - - context "when the latest tag 404s" do - before do - stub_request(:head, repo_url + "manifests/latest"). - to_return(status: 404).then. - to_return( - status: 200, - body: "", - headers: JSON.parse(headers_response.gsub("3ea1ca1", "4da71a2")) - ) - end - - it { is_expected.to eq("2.1.401-sdk") } - - context "every time" do - before do - stub_request(:head, repo_url + "manifests/latest"). - to_return(status: 404) - end - - it { is_expected.to eq("2.2-sdk") } - end - end - end - - context "when the dependency's version has a suffix with periods" do - let(:dependency_name) { "python" } - let(:version) { "3.6.2-alpine3.6" } - let(:tags_fixture_name) { "python.json" } - before do - tags_url = "https://registry.hub.docker.com/v2/library/python/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("3.6.3-alpine3.6") } - end - - context "when the dependency has a private registry" do - let(:dependency_name) { "ubuntu" } - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: version, - requirements: [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: { registry: "registry-host.io:5000" } - }], - package_manager: "docker" - ) - end - let(:tags_fixture_name) { "ubuntu_no_latest.json" } - - context "without authentication credentials" do - before do - tags_url = "https://registry-host.io:5000/v2/ubuntu/tags/list" - stub_request(:get, tags_url). - and_return( - status: 401, - body: "", - headers: { "www_authenticate" => "basic 123" } - ) - end - - it "raises a to PrivateSourceAuthenticationFailure error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { checker.latest_version }. - to raise_error(error_class) do |error| - expect(error.source).to eq("registry-host.io:5000") - end - end - end - - context "with authentication credentials" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "docker_registry", - "registry" => "registry-host.io:5000", - "username" => "grey", - "password" => "pa55word" - }] - end - - before do - tags_url = "https://registry-host.io:5000/v2/ubuntu/tags/list" - stub_request(:get, tags_url). - and_return(status: 200, body: registry_tags) - end - - it { is_expected.to eq("17.10") } - - context "that don't have a username or password" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "docker_registry", - "registry" => "registry-host.io:5000" - }] - end - - it { is_expected.to eq("17.10") } - end - end - end - end - - describe "#latest_resolvable_version" do - subject { checker.latest_resolvable_version } - - before { allow(checker).to receive(:latest_version).and_return("delegate") } - it { is_expected.to eq("delegate") } - end - - describe "#updated_requirements" do - subject { checker.updated_requirements } - - context "when specified with a tag" do - let(:source) { { tag: version } } - - it "updates the tag" do - expect(checker.updated_requirements). - to eq( - [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: { tag: "17.10" } - }] - ) - end - end - - context "when specified with a digest" do - let(:source) { { digest: "old_digest" } } - - before do - new_headers = - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - stub_request(:head, repo_url + "manifests/17.10"). - and_return(status: 200, body: "", headers: JSON.parse(new_headers)) - end - - it "updates the digest" do - expect(checker.updated_requirements). - to eq( - [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: { - digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ - "ca97eba880ebf600d68608" - } - }] - ) - end - end - - context "when specified with a digest and a tag" do - let(:source) { { digest: "old_digest", tag: "17.04" } } - - before do - new_headers = - fixture("docker", "registry_manifest_headers", "ubuntu_17.10.json") - stub_request(:head, repo_url + "manifests/17.10"). - and_return(status: 200, body: "", headers: JSON.parse(new_headers)) - end - - it "updates the tag and the digest" do - expect(checker.updated_requirements). - to eq( - [{ - requirement: nil, - groups: [], - file: "Dockerfile", - source: { - digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ - "ca97eba880ebf600d68608", - tag: "17.10" - } - }] - ) - end - end - end + let(:package_manager) { "docker" } + let(:file_name) { "Dockerfile" } end diff --git a/docker/spec/dependabot/docker_compose/file_fetcher_spec.rb b/docker/spec/dependabot/docker_compose/file_fetcher_spec.rb new file mode 100644 index 00000000000..f5e13eb5efd --- /dev/null +++ b/docker/spec/dependabot/docker_compose/file_fetcher_spec.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/docker_compose/file_fetcher" +require_common_spec "file_fetchers/shared_examples_for_file_fetchers" + +RSpec.describe Dependabot::DockerCompose::FileFetcher do + it_behaves_like "a dependency file fetcher" + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: directory + ) + end + let(:file_fetcher_instance) do + described_class.new(source: source, credentials: credentials) + end + let(:directory) { "/" } + let(:github_url) { "https://github.com/gitapi/" } + let(:url) { github_url + "repos/gocardless/bump/contents/" } + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + + before { allow(file_fetcher_instance).to receive(:commit).and_return("sha") } + + context "with a docker-compose.yml file" do + before do + stub_request(:get, url + "?ref=sha"). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: fixture("github", "contents_docker_repo.json"), + headers: { "content-type" => "application/json" } + ) + + stub_request(:get, File.join(url, "docker-compose.yml?ref=sha")). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: composefile_fixture, + headers: { "content-type" => "application/json" } + ) + end + + let(:composefile_fixture) do + fixture("github", "contents_docker-compose.json") + end + + it "fetches the docker-compose.yml file" do + expect(file_fetcher_instance.files.count).to eq(1) + expect(file_fetcher_instance.files.map(&:name)). + to match_array(%w(docker-compose.yml)) + end + + context "that has an invalid encoding" do + let(:composefile_fixture) { fixture("github", "contents_image.json") } + + it "raises a helpful error" do + expect { file_fetcher_instance.files }. + to raise_error(Dependabot::DependencyFileNotParseable) + end + end + end + + context "with docker-compose.yml override file" do + before do + stub_request(:get, url + "?ref=sha"). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: fixture("github", "contents_docker_repo_multiple.json"), + headers: { "content-type" => "application/json" } + ) + stub_request(:get, File.join(url, "docker-compose.yml?ref=sha")). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: composefile_fixture, + headers: { "content-type" => "application/json" } + ) + stub_request(:get, File.join(url, "docker-compose.override.yml?ref=sha")). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: composefile_2_fixture, + headers: { "content-type" => "application/json" } + ) + end + + let(:composefile_fixture) do + fixture("github", "contents_docker-compose.json") + end + let(:composefile_2_fixture) do + fixture("github", "contents_docker-compose.json") + end + + it "fetches both docker-compose.yml files" do + expect(file_fetcher_instance.files.count).to eq(2) + expect(file_fetcher_instance.files.map(&:name)). + to match_array(%w(docker-compose.yml docker-compose.override.yml)) + end + + context "one of which has an invalid encoding" do + let(:composefile_2_fixture) { fixture("github", "contents_image.json") } + + it "fetches the first docker-compose.yml file, "\ + "and ignores the invalid one" do + expect(file_fetcher_instance.files.count).to eq(1) + expect(file_fetcher_instance.files.map(&:name)). + to match_array(%w(docker-compose.yml)) + end + end + end + + context "with a custom named docker-compose.yml file" do + before do + stub_request(:get, url + "?ref=sha"). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: fixture("github", "contents_docker_repo_custom.json"), + headers: { "content-type" => "application/json" } + ) + + stub_request(:get, File.join(url, "docker-compose.override.yml?ref=sha")). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 200, + body: fixture("github", "contents_docker-compose.json"), + headers: { "content-type" => "application/json" } + ) + end + + it "fetches the docker-compose.override.yml file" do + expect(file_fetcher_instance.files.count).to eq(1) + expect(file_fetcher_instance.files.map(&:name)). + to match_array(%w(docker-compose.override.yml)) + end + end + + context "with a directory that doesn't exist" do + let(:directory) { "/non/existant" } + + before do + stub_request(:get, url + "non/existant?ref=sha"). + with(headers: { "Authorization" => "token token" }). + to_return( + status: 404, + body: fixture("github", "not_found.json"), + headers: { "content-type" => "application/json" } + ) + end + + it "raises a helpful error" do + expect { file_fetcher_instance.files }. + to raise_error(Dependabot::DependencyFileNotFound) + end + end +end diff --git a/docker/spec/dependabot/docker_compose/file_parser_spec.rb b/docker/spec/dependabot/docker_compose/file_parser_spec.rb new file mode 100644 index 00000000000..b395f4fc971 --- /dev/null +++ b/docker/spec/dependabot/docker_compose/file_parser_spec.rb @@ -0,0 +1,591 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_file" +require "dependabot/source" +require "dependabot/docker_compose/file_parser" +require_common_spec "file_parsers/shared_examples_for_file_parsers" + +RSpec.describe Dependabot::DockerCompose::FileParser do + it_behaves_like "a dependency file parser" + + let(:files) { [composefile] } + let(:composefile) do + Dependabot::DependencyFile.new( + name: "docker-compose.yml", + content: composefile_body + ) + end + let(:composefile_body) do + fixture("docker_compose", "composefiles", composefile_fixture_name) + end + let(:composefile_fixture_name) { "tag" } + let(:parser) { described_class.new(dependency_files: files, source: source) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end + + describe "parse" do + subject(:dependencies) { parser.parse } + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + context "with no tag or digest" do + let(:composefile_fixture_name) { "bare" } + its(:length) { is_expected.to eq(0) } + end + + context "with a namespace" do + let(:composefile_fixture_name) { "namespace" } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("my-fork/ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + + context "with a non-numeric version" do + let(:composefile_fixture_name) { "non_numeric_version" } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "artful" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("ubuntu") + expect(dependency.version).to eq("artful") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + + context "with a digest" do + let(:composefile_fixture_name) { "digest" } + let(:registry_tags) { fixture("docker", "registry_tags", "ubuntu.json") } + let(:digest_headers) do + JSON.parse( + fixture("docker", "registry_manifest_headers", "ubuntu_12.04.5.json") + ) + end + + let(:repo_url) { "https://registry.hub.docker.com/v2/library/ubuntu/" } + + before do + auth_url = "https://auth.docker.io/token?service=registry.docker.io" + stub_request(:get, auth_url). + and_return(status: 200, body: { token: "token" }.to_json) + + tags_url = repo_url + "tags/list" + stub_request(:get, tags_url). + and_return(status: 200, body: registry_tags) + end + + context "that doesn't match any tags" do + let(:registry_tags) do + fixture("docker", "registry_tags", "small_ubuntu.json") + end + before { digest_headers["docker_content_digest"] = "nomatch" } + + before do + ubuntu_url = "https://registry.hub.docker.com/v2/library/ubuntu/" + stub_request(:head, /#{Regexp.quote(ubuntu_url)}manifests/). + and_return(status: 200, body: "", headers: digest_headers) + end + + its(:length) { is_expected.to eq(0) } + end + + context "that matches a tag" do + before do + stub_request(:head, repo_url + "manifests/10.04"). + and_return(status: 404) + + stub_request(:head, repo_url + "manifests/12.04.5"). + and_return(status: 200, body: "", headers: digest_headers) + end + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8d"\ + "fc38288cf73aa07485005" + } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("ubuntu") + expect(dependency.version).to eq("12.04.5") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + context "for a private registry" do + let(:composefile_fixture_name) { "private_digest" } + let(:repo_url) { "https://registry-host.io:5000/v2/myreg/ubuntu/" } + + context "without no/bad authentication credentials" do + before do + tags_url = repo_url + "tags/list" + stub_request(:get, tags_url). + and_return( + status: 401, + body: "", + headers: { "www_authenticate" => "basic 123" } + ) + end + + it "raises a PrivateSourceAuthenticationFailure error" do + error_class = Dependabot::PrivateSourceAuthenticationFailure + expect { parser.parse }. + to raise_error(error_class) do |error| + expect(error.source).to eq("registry-host.io:5000") + end + end + end + + context "with good authentication credentials" do + let(:parser) do + described_class.new( + dependency_files: files, + credentials: credentials, + source: source + ) + end + let(:credentials) do + [{ + "type" => "docker_registry", + "registry" => "registry-host.io:5000", + "username" => "grey", + "password" => "pa55word" + }] + end + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76"\ + "e4c8dfc38288cf73aa07485005" + } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("myreg/ubuntu") + expect(dependency.version).to eq("12.04.5") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + context "that don't include a username and password" do + let(:credentials) do + [{ + "type" => "docker_registry", + "registry" => "registry-host.io:5000" + }] + end + + its(:length) { is_expected.to eq(1) } + end + end + + context "that uses Amazon ECR" do + let(:composefile_fixture_name) { "private_ecr_digest" } + let(:repo_url) do + "https://695729449481.dkr.ecr.eu-west-2.amazonaws.com/v2/"\ + "docker-php/" + end + + context "without credentials" do + before do + tags_url = repo_url + "tags/list" + stub_request(:get, tags_url). + and_return( + status: 401, + body: "", + headers: { "www_authenticate" => "basic 123" } + ) + end + + it "raises a PrivateSourceAuthenticationFailure error" do + error_class = Dependabot::PrivateSourceAuthenticationFailure + expect { parser.parse }. + to raise_error(error_class) do |error| + expect(error.source). + to eq("695729449481.dkr.ecr.eu-west-2.amazonaws.com") + end + end + end + + context "with credentials" do + let(:parser) do + described_class.new( + dependency_files: files, + credentials: credentials, + source: source + ) + end + + let(:credentials) do + [{ + "type" => "docker_registry", + "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", + "username" => "grey", + "password" => "pa55word" + }] + end + + context "that are invalid" do + before do + stub_request( + :post, + "https://api.ecr.eu-west-2.amazonaws.com/" + ).and_return( + status: 403, + body: fixture("docker", "ecr_responses", "invalid_token") + ) + end + + it "raises a PrivateSourceAuthenticationFailure error" do + error_class = Dependabot::PrivateSourceAuthenticationFailure + expect { parser.parse }. + to raise_error(error_class) do |error| + expect(error.source). + to eq("695729449481.dkr.ecr.eu-west-2.amazonaws.com") + end + end + end + + context "that are valid" do + before do + stub_request( + :post, + "https://api.ecr.eu-west-2.amazonaws.com/" + ).and_return( + status: 200, + body: fixture("docker", "ecr_responses", "auth_data") + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: + "695729449481.dkr.ecr.eu-west-2.amazonaws.com", + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76"\ + "e4c8dfc38288cf73aa07485005" + } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("docker-php") + expect(dependency.version).to eq("12.04.5") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + end + end + end + end + end + + context "with multiple services" do + let(:composefile_fixture_name) { "multiple" } + + its(:length) { is_expected.to eq(2) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + describe "the second dependency" do + subject(:dependency) { dependencies.last } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "3.6.3" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("python") + expect(dependency.version).to eq("3.6.3") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + context "that are identical" do + let(:composefile_fixture_name) { "multiple_identical" } + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "10-alpine" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("node") + expect(dependency.version).to eq("10-alpine") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + end + + context "with a v1 dockerhub reference and a tag" do + let(:composefile_fixture_name) { "v1_tag" } + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("myreg/ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + + context "with a private registry and a tag" do + let(:composefile_fixture_name) { "private_tag" } + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + tag: "17.04" + } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("myreg/ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + context "when the registry has no port" do + let(:composefile_fixture_name) { "private_no_port" } + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "aws-account-id.dkr.ecr.region.amazonaws.com", + tag: "17.04" + } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("myreg/ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + end + + context "with a non-standard filename" do + let(:composefile) do + Dependabot::DependencyFile.new( + name: "custom-name", + content: composefile_body + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "custom-name", + source: { tag: "17.04" } + }] + end + + its(:requirements) { is_expected.to eq(expected_requirements) } + end + end + + context "with multiple composefiles" do + let(:files) { [composefile, dockefile2] } + let(:dockefile2) do + Dependabot::DependencyFile.new( + name: "custom-name", + content: composefile_body2 + ) + end + let(:composefile_body2) do + fixture("docker_compose", "composefiles", "namespace") + end + + its(:length) { is_expected.to eq(2) } + + describe "the first dependency" do + subject(:dependency) { dependencies.first } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + + describe "the second dependency" do + subject(:dependency) { dependencies.last } + let(:expected_requirements) do + [{ + requirement: nil, + groups: [], + file: "custom-name", + source: { tag: "17.04" } + }] + end + + it "has the right details" do + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("my-fork/ubuntu") + expect(dependency.version).to eq("17.04") + expect(dependency.requirements).to eq(expected_requirements) + end + end + end + end +end diff --git a/docker/spec/dependabot/docker_compose/file_updater_spec.rb b/docker/spec/dependabot/docker_compose/file_updater_spec.rb new file mode 100644 index 00000000000..c0bd065397f --- /dev/null +++ b/docker/spec/dependabot/docker_compose/file_updater_spec.rb @@ -0,0 +1,477 @@ +# frozen_string_literal: true + +require "spec_helper" +require "yaml" +require "dependabot/dependency" +require "dependabot/dependency_file" +require "dependabot/source" +require "dependabot/docker_compose/file_updater" +require_common_spec "file_updaters/shared_examples_for_file_updaters" + +RSpec.describe Dependabot::DockerCompose::FileUpdater do + it_behaves_like "a dependency file updater" + + let(:updater) do + described_class.new( + dependency_files: files, + dependencies: [dependency], + credentials: credentials + ) + end + let(:files) { [dockerfile] } + let(:credentials) do + [{ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }] + end + let(:dockerfile) do + Dependabot::DependencyFile.new( + content: dockerfile_body, + name: "docker-compose.yml" + ) + end + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "multiple") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "ubuntu", + version: "17.10", + previous_version: "17.04", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.10" } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }], + package_manager: "docker" + ) + end + + describe "#updated_dependency_files" do + subject(:updated_files) { updater.updated_dependency_files } + + it "returns DependencyFile objects" do + updated_files.each { |f| expect(f).to be_a(Dependabot::DependencyFile) } + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + let(:yaml_content) do + YAML.safe_load updated_dockerfile.content + end + + its(:content) { is_expected.to include "image: ubuntu:17.10\n" } + its(:content) { is_expected.to include "image: python:3.6.3\n" } + it "should contain the expected YAML content" do + expect(yaml_content).to eq( + "version" => "2", + "services" => { + "interpreter" => { "image" => "python:3.6.3" }, + "os" => { "image" => "ubuntu:17.10" } + } + ) + end + end + + context "when multiple identical lines need to be updated" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "multiple_identical") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "node", + version: "10.9-alpine", + previous_version: "10-alpine", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "10.9-alpine" } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "10-alpine" } + }], + package_manager: "docker" + ) + end + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) { is_expected.to include "image: node:10.9-alpine\n" } + its(:content) { is_expected.to include "node-2:" } + end + end + + context "when the dependency has a namespace" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "namespace") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "my-fork/ubuntu", + version: "17.10", + previous_version: "17.04", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.10" } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }], + package_manager: "docker" + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) { is_expected.to include "image: my-fork/ubuntu:17.10\n" } + its(:content) do + is_expected.to include "command: [/bin/echo, 'Hello world']" + end + end + end + + context "when the dependency is from a private registry" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "private_tag") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "myreg/ubuntu", + version: "17.10", + previous_version: "17.04", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + tag: "17.10" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + tag: "17.04" + } + }], + package_manager: "docker" + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) do + is_expected. + to include("image: registry-host.io:5000/myreg/ubuntu:17.10\n") + end + its(:content) do + is_expected.to include "command: [/bin/echo, 'Hello world']" + end + end + end + + context "when the dependency is docker-compose.yml using the v1 API" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "v1_tag") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "myreg/ubuntu", + version: "17.10", + previous_version: "17.04", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.10" } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { tag: "17.04" } + }], + package_manager: "docker" + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) do + is_expected. + to include("image: docker.io/myreg/ubuntu:17.10\n") + end + its(:content) do + is_expected.to include "command: [/bin/echo, 'Hello world']" + end + end + end + + context "when the dependency has a digest" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "digest") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "ubuntu", + version: "17.10", + previous_version: "12.04.5", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8"\ + "dfc38288cf73aa07485005" + } + }], + package_manager: "docker" + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) do + is_expected.to include "image: ubuntu@sha256:3ea1ca1aa" + end + its(:content) do + is_expected.to include "command: [/bin/echo, 'Hello world']" + end + + context "when the dockerfile has a tag as well as a digest" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "digest_and_tag") + end + + its(:content) do + is_expected.to include "image: ubuntu:17.10@sha256:3ea1ca1aa" + end + end + end + + context "when the dependency has a private registry" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "private_digest") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "myreg/ubuntu", + version: "17.10", + previous_version: "17.10", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + registry: "registry-host.io:5000", + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8"\ + "dfc38288cf73aa07485005" + } + }], + package_manager: "docker" + ) + end + + its(:length) { is_expected.to eq(1) } + + describe "the updated docker-compose.yml" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "docker-compose.yml" } + end + + its(:content) do + is_expected.to include("image: registry-host.io:5000/"\ + "myreg/ubuntu@sha256:3ea1ca1aa") + end + its(:content) do + is_expected.to include "command: [/bin/echo, 'Hello world']" + end + end + end + end + + context "when multiple dockerfiles to be updated" do + let(:files) { [dockerfile, dockefile2] } + let(:dockefile2) do + Dependabot::DependencyFile.new( + name: "custom-name", + content: dockerfile_body2 + ) + end + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "digest") + end + let(:dockerfile_body2) do + fixture("docker_compose", "composefiles", "digest_and_tag") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "ubuntu", + version: "17.10", + previous_version: "12.04.5", + requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608" + } + }, { + requirement: nil, + groups: [], + file: "custom-name", + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608", + tag: "17.10" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "docker-compose.yml", + source: { + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8"\ + "dfc38288cf73aa07485005" + } + }, { + requirement: nil, + groups: [], + file: "custom-name", + source: { + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8"\ + "dfc38288cf73aa07485005", + tag: "12.04.5" + } + }], + package_manager: "docker" + ) + end + + describe "the updated docker-compose.yml" do + subject { updated_files.find { |f| f.name == "docker-compose.yml" } } + its(:content) do + is_expected.to include "image: ubuntu@sha256:3ea1ca1aa" + end + end + + describe "the updated custom-name file" do + subject { updated_files.find { |f| f.name == "custom-name" } } + + its(:content) do + is_expected.to include "image: ubuntu:17.10@sha256:3ea1ca1aa" + end + end + + context "when only one needs updating" do + let(:dockerfile_body) do + fixture("docker_compose", "composefiles", "bare") + end + + let(:dependency) do + Dependabot::Dependency.new( + name: "ubuntu", + version: "17.10", + previous_version: "12.04.5", + requirements: [{ + requirement: nil, + groups: [], + file: "custom-name", + source: { + digest: "sha256:3ea1ca1aa8483a38081750953ad75046e6cc9f6b86"\ + "ca97eba880ebf600d68608" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "custom-name", + source: { + digest: "sha256:18305429afa14ea462f810146ba44d4363ae76e4c8"\ + "dfc38288cf73aa07485005" + } + }], + package_manager: "docker" + ) + end + + describe "the updated custom-name file" do + subject { updated_files.find { |f| f.name == "custom-name" } } + + its(:content) do + is_expected.to include "image: ubuntu:17.10@sha256:3ea1ca1aa" + end + end + end + end + end +end diff --git a/docker/spec/dependabot/docker_compose/update_checker_spec.rb b/docker/spec/dependabot/docker_compose/update_checker_spec.rb new file mode 100644 index 00000000000..68887043c1b --- /dev/null +++ b/docker/spec/dependabot/docker_compose/update_checker_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/docker_compose/update_checker" +require "dependabot/docker/common/shared_examples_for_docker_update_checkers.rb" + +RSpec.describe Dependabot::DockerCompose::UpdateChecker do + it_behaves_like "a Docker update checker" + + let(:package_manager) { "docker_compose" } + let(:file_name) { "docker-compose.yml" } +end diff --git a/docker/spec/dependabot/docker_compose_spec.rb b/docker/spec/dependabot/docker_compose_spec.rb new file mode 100644 index 00000000000..e0c3dc81fc5 --- /dev/null +++ b/docker/spec/dependabot/docker_compose_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/docker_compose" +require_common_spec "shared_examples_for_autoloading" + +RSpec.describe Dependabot::DockerCompose do + it_behaves_like "it registers the required classes", "docker_compose" +end diff --git a/docker/spec/fixtures/docker_compose/composefiles/bare b/docker/spec/fixtures/docker_compose/composefiles/bare new file mode 100644 index 00000000000..066287521d7 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/bare @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: ubuntu + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/digest b/docker/spec/fixtures/docker_compose/composefiles/digest new file mode 100644 index 00000000000..93b585ced9a --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/digest @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: ubuntu@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/digest_and_tag b/docker/spec/fixtures/docker_compose/composefiles/digest_and_tag new file mode 100644 index 00000000000..9e3aeb08829 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/digest_and_tag @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: ubuntu:12.04.5@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/multiple b/docker/spec/fixtures/docker_compose/composefiles/multiple new file mode 100644 index 00000000000..fb7e79760bd --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/multiple @@ -0,0 +1,6 @@ +version: '2' +services: + os: + image: ubuntu:17.04 + interpreter: + image: python:3.6.3 diff --git a/docker/spec/fixtures/docker_compose/composefiles/multiple_identical b/docker/spec/fixtures/docker_compose/composefiles/multiple_identical new file mode 100644 index 00000000000..b7b8ead64e6 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/multiple_identical @@ -0,0 +1,6 @@ +version: '2' +services: + node-1: + image: node:10-alpine + node-2: + image: node:10-alpine diff --git a/docker/spec/fixtures/docker_compose/composefiles/namespace b/docker/spec/fixtures/docker_compose/composefiles/namespace new file mode 100644 index 00000000000..c1484ab7fc3 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/namespace @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: my-fork/ubuntu:17.04 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/non_numeric_version b/docker/spec/fixtures/docker_compose/composefiles/non_numeric_version new file mode 100644 index 00000000000..97e312804ad --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/non_numeric_version @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: ubuntu:artful + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/private_digest b/docker/spec/fixtures/docker_compose/composefiles/private_digest new file mode 100644 index 00000000000..2017b08be5e --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/private_digest @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: registry-host.io:5000/myreg/ubuntu@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/private_ecr_digest b/docker/spec/fixtures/docker_compose/composefiles/private_ecr_digest new file mode 100644 index 00000000000..112545b53a1 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/private_ecr_digest @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: 695729449481.dkr.ecr.eu-west-2.amazonaws.com/docker-php@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/private_no_port b/docker/spec/fixtures/docker_compose/composefiles/private_no_port new file mode 100644 index 00000000000..ca01a020d25 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/private_no_port @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: aws-account-id.dkr.ecr.region.amazonaws.com/myreg/ubuntu:17.04 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/private_tag b/docker/spec/fixtures/docker_compose/composefiles/private_tag new file mode 100644 index 00000000000..374514ea03c --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/private_tag @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: registry-host.io:5000/myreg/ubuntu:17.04 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/tag b/docker/spec/fixtures/docker_compose/composefiles/tag new file mode 100644 index 00000000000..dce865acc7b --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/tag @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: ubuntu:17.04 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/docker_compose/composefiles/v1_tag b/docker/spec/fixtures/docker_compose/composefiles/v1_tag new file mode 100644 index 00000000000..e8d93a02354 --- /dev/null +++ b/docker/spec/fixtures/docker_compose/composefiles/v1_tag @@ -0,0 +1,5 @@ +version: '2' +services: + hello_world: + image: docker.io/myreg/ubuntu:17.04 + command: [/bin/echo, 'Hello world'] diff --git a/docker/spec/fixtures/github/contents_docker-compose.json b/docker/spec/fixtures/github/contents_docker-compose.json new file mode 100644 index 00000000000..0faed57f5aa --- /dev/null +++ b/docker/spec/fixtures/github/contents_docker-compose.json @@ -0,0 +1,18 @@ +{ + "name": "docker-compose.yml", + "path": "docker-compose.yml", + "sha": "311f9743315183cc6751313cb251cfeb3de45c1a", + "size": 4927, + "url": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "html_url": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml", + "git_url": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "download_url": "https://github.com/raw/dependabot/dependabot-core/master/docker-compose.yml", + "type": "file", + "content": "RlJPTSB1YnVudHU6MTguMDQKCiMjIyBTWVNURU0gREVQRU5ERU5DSUVTCgoj\nIEV2ZXJ5dGhpbmcgZnJvbSBgbWFrZWAgb253YXJkcyBpbiBhcHQtZ2V0IGlu\nc3RhbGwgaXMgb25seSBpbnN0YWxsZWQgdG8gZW5zdXJlCiMgUHl0aG9uIHN1\ncHBvcnQgd29ya3Mgd2l0aCBhbGwgcGFja2FnZXMgKHdoaWNoIG1heSByZXF1\naXJlIHNwZWNpZmljIGxpYnJhcmllcwojIGF0IGluc3RhbGwgdGltZSkuCkVO\nViBERUJJQU5fRlJPTlRFTkQgbm9uaW50ZXJhY3RpdmUKUlVOIGFwdC1nZXQg\ndXBkYXRlIFwKICAgICYmIGFwdC1nZXQgdXBncmFkZSAteSBcCiAgICAmJiBh\ncHQtZ2V0IGluc3RhbGwgLXkgLS1uby1pbnN0YWxsLXJlY29tbWVuZHMgXAog\nICAgICBidWlsZC1lc3NlbnRpYWwgXAogICAgICBkaXJtbmdyIFwKICAgICAg\nZ2l0IFwKICAgICAgZ251cGcyIFwKICAgICAgY3VybCBcCiAgICAgIHdnZXQg\nXAogICAgICB6bGliMWctZGV2IFwKICAgICAgbGlibHptYS1kZXYgXAogICAg\nICB0emRhdGEgXAogICAgICB6aXAgXAogICAgICB1bnppcCBcCiAgICAgIGxv\nY2FsZXMgXAogICAgICBvcGVuc3NoLWNsaWVudCBcCiAgICAgIG1ha2UgXAog\nICAgICBsaWJzc2wtZGV2IFwKICAgICAgbGliYnoyLWRldiBcCiAgICAgIGxp\nYnJlYWRsaW5lLWRldiBcCiAgICAgIGxpYnNxbGl0ZTMtZGV2IFwKICAgICAg\nbGx2bSBcCiAgICAgIGxpYm5jdXJzZXM1LWRldiBcCiAgICAgIGxpYm5jdXJz\nZXN3NS1kZXYgXAogICAgICBsaWJteXNxbGNsaWVudC1kZXYgXAogICAgICB4\nei11dGlscyBcCiAgICAgIHRrLWRldiBcCiAgICAmJiBsb2NhbGUtZ2VuIGVu\nX1VTLlVURi04CkVOViBMQ19BTEwgZW5fVVMuVVRGLTgKCgojIyMgUlVCWQoK\nIyBJbnN0YWxsIFJ1YnkgMi41LCB1cGRhdGUgUnVieUdlbXMsIGFuZCBpbnN0\nYWxsIEJ1bmRsZXIKUlVOIGFwdC1rZXkgYWR2IC0ta2V5c2VydmVyIGtleXNl\ncnZlci51YnVudHUuY29tIC0tcmVjdi1rZXlzIEMzMTczQUE2IFwKICAgICYm\nIGVjaG8gImRlYiBodHRwOi8vcHBhLmxhdW5jaHBhZC5uZXQvYnJpZ2h0Ym94\nL3J1YnktbmcvdWJ1bnR1IGJpb25pYyBtYWluIiA+IC9ldGMvYXB0L3NvdXJj\nZXMubGlzdC5kL2JyaWdodGJveC5saXN0IFwKICAgICYmIGFwdC1nZXQgdXBk\nYXRlIFwKICAgICYmIGFwdC1nZXQgaW5zdGFsbCAteSBydWJ5Mi41IHJ1Ynky\nLjUtZGV2IFwKICAgICYmIGdlbSB1cGRhdGUgLS1zeXN0ZW0gMi43LjcgXAog\nICAgJiYgZ2VtIGluc3RhbGwgLS1uby1yaSAtLW5vLXJkb2MgYnVuZGxlciAt\ndiAxLjE2LjQKCgojIyMgUFlUSE9OCgojIEluc3RhbGwgUHl0aG9uIDIuNyBh\nbmQgMy42IHdpdGggcHllbnYuIFVzaW5nIHB5ZW52IGxldHMgdXMgc3VwcG9y\ndCBtdWx0aXBsZSBQeXRob25zCkVOViBQWUVOVl9ST09UPS91c3IvbG9jYWwv\nLnB5ZW52IFwKICAgIFBBVEg9Ii91c3IvbG9jYWwvLnB5ZW52L2JpbjokUEFU\nSCIKUlVOIGdpdCBjbG9uZSBodHRwczovL2dpdGh1Yi5jb20vcHllbnYvcHll\nbnYuZ2l0IC91c3IvbG9jYWwvLnB5ZW52IFwKICAgICYmIGNkIC91c3IvbG9j\nYWwvLnB5ZW52ICYmIGdpdCBjaGVja291dCB2MS4yLjcgJiYgY2QgLSBcCiAg\nICAmJiBweWVudiBpbnN0YWxsIDMuNi41IFwKICAgICYmIHB5ZW52IGluc3Rh\nbGwgMi43LjE1IFwKICAgICYmIHB5ZW52IGdsb2JhbCAzLjYuNQoKCiMjIyBK\nQVZBU0NSSVBUCgojIEluc3RhbGwgTm9kZSA4LjAgYW5kIFlhcm4KUlVOIGN1\ncmwgLXNMIGh0dHBzOi8vZGViLm5vZGVzb3VyY2UuY29tL3NldHVwXzgueCB8\nIGJhc2ggLSBcCiAgICAmJiBhcHQtZ2V0IGluc3RhbGwgLXkgbm9kZWpzIFwK\nICAgICYmIGN1cmwgLXNTIGh0dHBzOi8vZGwueWFybnBrZy5jb20vZGViaWFu\nL3B1YmtleS5ncGcgfCBhcHQta2V5IGFkZCAtIFwKICAgICYmIGVjaG8gImRl\nYiBodHRwczovL2RsLnlhcm5wa2cuY29tL2RlYmlhbi8gc3RhYmxlIG1haW4i\nIHwgdGVlIC9ldGMvYXB0L3NvdXJjZXMubGlzdC5kL3lhcm4ubGlzdCBcCiAg\nICAmJiBhcHQtZ2V0IHVwZGF0ZSAmJiBhcHQtZ2V0IGluc3RhbGwgLXkgeWFy\nbgoKCiMjIyBFTE0KCiMgSW5zdGFsbCBFbG0gMC4xOApFTlYgUEFUSD0iJFBB\nVEg6L25vZGVfbW9kdWxlcy8uYmluIgpSVU4gbnBtIGluc3RhbGwgZWxtQDAu\nMTguMAoKCiMjIyBQSFAKCiMgSW5zdGFsbCBQSFAgNy4yIGFuZCBDb21wb3Nl\ncgpSVU4gZWNobyAiZGViIGh0dHA6Ly9wcGEubGF1bmNocGFkLm5ldC9vbmRy\nZWovcGhwL3VidW50dSBiaW9uaWMgbWFpbiIgPj4gL2V0Yy9hcHQvc291cmNl\ncy5saXN0LmQvb25kcmVqLXBocC5saXN0IFwKICAgICYmIGVjaG8gImRlYi1z\ncmMgaHR0cDovL3BwYS5sYXVuY2hwYWQubmV0L29uZHJlai9waHAvdWJ1bnR1\nIGJpb25pYyBtYWluIiA+PiAvZXRjL2FwdC9zb3VyY2VzLmxpc3QuZC9vbmRy\nZWotcGhwLmxpc3QgXAogICAgJiYgYXB0LWtleSBhZHYgLS1rZXlzZXJ2ZXIg\na2V5c2VydmVyLnVidW50dS5jb20gLS1yZWN2LWtleXMgNEY0RUEwQUFFNTI2\nN0E2QyBcCiAgICAmJiBhcHQtZ2V0IHVwZGF0ZSBcCiAgICAmJiBhcHQtZ2V0\nIGluc3RhbGwgLXkgcGhwNy4yIHBocDcuMi14bWwgcGhwNy4yLWpzb24gcGhw\nNy4yLXppcCBwaHA3LjItbWJzdHJpbmcgcGhwNy4yLWludGwgcGhwNy4yLWNv\nbW1vbiBwaHA3LjItZ2V0dGV4dCBwaHA3LjItY3VybCBwaHAteGRlYnVnIHBo\ncDcuMi1iY21hdGggcGhwLWdtcCBwaHA3LjItaW1hZ2ljayBwaHA3LjItZ2Qg\ncGhwNy4yLXJlZGlzIHBocDcuMi1zb2FwIHBocDcuMi1sZGFwIFwKICAgICYm\nIGN1cmwgLXNTIGh0dHBzOi8vZ2V0Y29tcG9zZXIub3JnL2luc3RhbGxlciB8\nIHBocCBcCiAgICAmJiBtdiBjb21wb3Nlci5waGFyIC91c3IvbG9jYWwvYmlu\nL2NvbXBvc2VyCgoKIyMjIEdPCgojIEluc3RhbGwgR28gYW5kIGRlcApSVU4g\nY3VybCAtTyBodHRwczovL2RsLmdvb2dsZS5jb20vZ28vZ28xLjEwLjMubGlu\ndXgtYW1kNjQudGFyLmd6IFwKICAgICYmIHRhciB4dmYgZ28xLjEwLjMubGlu\ndXgtYW1kNjQudGFyLmd6IFwKICAgICYmIHdnZXQgaHR0cHM6Ly9naXRodWIu\nY29tL2dvbGFuZy9kZXAvcmVsZWFzZXMvZG93bmxvYWQvdjAuNS4wL2RlcC1s\naW51eC1hbWQ2NCBcCiAgICAmJiBtdiBkZXAtbGludXgtYW1kNjQgZ28vYmlu\nL2RlcCBcCiAgICAmJiBjaG1vZCAreCBnby9iaW4vZGVwIFwKICAgICYmIG12\nIGdvIC9yb290CkVOViBQQVRIPS9yb290L2dvL2JpbjokUEFUSAoKCiMjIyBF\nTElYSVIKCiMgSW5zdGFsbCBFcmxhbmcsIEVsaXhpciBhbmQgSGV4CkVOViBQ\nQVRIPSIkUEFUSDovdXNyL2xvY2FsL2VsaXhpci9iaW4iClJVTiB3Z2V0IGh0\ndHBzOi8vcGFja2FnZXMuZXJsYW5nLXNvbHV0aW9ucy5jb20vZXJsYW5nLXNv\nbHV0aW9uc18xLjBfYWxsLmRlYiBcCiAgICAmJiBkcGtnIC1pIGVybGFuZy1z\nb2x1dGlvbnNfMS4wX2FsbC5kZWIgXAogICAgJiYgYXB0LWdldCB1cGRhdGUg\nXAogICAgJiYgYXB0LWdldCBpbnN0YWxsIC15IGVzbC1lcmxhbmcgXAogICAg\nJiYgd2dldCBodHRwczovL2dpdGh1Yi5jb20vZWxpeGlyLWxhbmcvZWxpeGly\nL3JlbGVhc2VzL2Rvd25sb2FkL3YxLjcuMi9QcmVjb21waWxlZC56aXAgXAog\nICAgJiYgdW56aXAgLWQgL3Vzci9sb2NhbC9lbGl4aXIgLXggUHJlY29tcGls\nZWQuemlwIFwKICAgICYmIHJtIC1mIFByZWNvbXBpbGVkLnppcCBcCiAgICAm\nJiBtaXggbG9jYWwuaGV4IC0tZm9yY2UKCgojIyMgUlVTVAoKIyBJbnN0YWxs\nIFJ1c3QKRU5WIFJVU1RVUF9IT01FPS9vcHQvcnVzdCBcCiAgICBQQVRIPSIk\ne1BBVEh9Oi9vcHQvcnVzdC9iaW4iClJVTiBleHBvcnQgQ0FSR09fSE9NRT0v\nb3B0L3J1c3QgOyBjdXJsIGh0dHBzOi8vc2gucnVzdHVwLnJzIC1zU2YgfCBz\naCAtcyAtLSAteQoKCiMjIyBKQVZBLCBHUk9PVlkgQU5EIEdSQURMRQoKIyBJ\nbnN0YWxsIEphdmEsIEdyb292eSBhbmQgR3JhZGxlClJVTiBlY2hvICJvcmFj\nbGUtamF2YTgtaW5zdGFsbGVyIHNoYXJlZC9hY2NlcHRlZC1vcmFjbGUtbGlj\nZW5zZS12MS0xIHNlbGVjdCB0cnVlIiB8IGRlYmNvbmYtc2V0LXNlbGVjdGlv\nbnMgXAogICAgJiYgZWNobyAiZGViIGh0dHA6Ly9wcGEubGF1bmNocGFkLm5l\ndC93ZWJ1cGQ4dGVhbS9qYXZhL3VidW50dSBiaW9uaWMgbWFpbiIgPiAvZXRj\nL2FwdC9zb3VyY2VzLmxpc3QuZC93ZWJ1cGQ4dGVhbS1qYXZhLXRydXN0eS5s\naXN0IFwKICAgICYmIGFwdC1rZXkgYWR2IC0ta2V5c2VydmVyIGtleXNlcnZl\nci51YnVudHUuY29tIC0tcmVjdi1rZXlzIEVFQTE0ODg2IFwKICAgICYmIGFw\ndC1nZXQgdXBkYXRlIFwKICAgICYmIGFwdC1nZXQgaW5zdGFsbCAteSBvcmFj\nbGUtamF2YTgtaW5zdGFsbGVyIG9yYWNsZS1qYXZhOC1zZXQtZGVmYXVsdCBc\nCiAgICAmJiBjZCAvdG1wIFwKICAgICYmIHdnZXQgaHR0cDovL2RsLmJpbnRy\nYXkuY29tL2dyb292eS9tYXZlbi9hcGFjaGUtZ3Jvb3Z5LWJpbmFyeS0yLjUu\nMi56aXAgXAogICAgJiYgdW56aXAgYXBhY2hlLWdyb292eS1iaW5hcnktMi41\nLjIuemlwIFwKICAgICYmIG12IGdyb292eS0yLjUuMiAvdXNyL2xvY2FsL2dy\nb292eSBcCiAgICAmJiBybSAtZiBhcGFjaGUtZ3Jvb3Z5LWJpbmFyeS0yLjUu\nMi56aXAgXAogICAgJiYgY2QgL3RtcCBcCiAgICAmJiB3Z2V0IGh0dHBzOi8v\nc2VydmljZXMuZ3JhZGxlLm9yZy9kaXN0cmlidXRpb25zL2dyYWRsZS00Ljkt\nYmluLnppcCBcCiAgICAmJiB1bnppcCBncmFkbGUtNC45LWJpbi56aXAgXAog\nICAgJiYgbXYgZ3JhZGxlLTQuOSAvdXNyL2xvY2FsL2dyYWRsZSBcCiAgICAm\nJiBybSAtZiBncmFkbGUtNC45LWJpbi56aXAKRU5WIEpBVkFfSE9NRT0vdXNy\nL2xpYi9qdm0vamF2YS04LW9yYWNsZSBcCiAgICBHUk9PVllfSE9NRT0vdXNy\nL2xvY2FsL2dyb292eSBcCiAgICBHUkFETEVfSE9NRT0vdXNyL2xvY2FsL2dy\nYWRsZSBcCiAgICBQQVRIPS91c3IvbG9jYWwvZ3Jvb3Z5L2Jpbi86L3Vzci9s\nb2NhbC9ncmFkbGUvYmluOiRQQVRICg==\n", + "encoding": "base64", + "_links": { + "self": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "git": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "html": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml" + } +} diff --git a/docker/spec/fixtures/github/contents_docker_repo.json b/docker/spec/fixtures/github/contents_docker_repo.json index 5eea29e2e0d..af0288d5fc0 100644 --- a/docker/spec/fixtures/github/contents_docker_repo.json +++ b/docker/spec/fixtures/github/contents_docker_repo.json @@ -111,6 +111,22 @@ "html": "https://github.com/dependabot/dependabot-core/blob/master/Dockerfile" } }, + { + "name": "docker-compose.yml", + "path": "docker-compose.yml", + "sha": "311f9743315183cc6751313cb251cfeb3de45c1a", + "size": 4927, + "url": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "html_url": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml", + "git_url": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "download_url": "https://github.com/raw/dependabot/dependabot-core/master/docker-compose.yml", + "type": "file", + "_links": { + "self": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "git": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "html": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml" + } + }, { "name": "Gemfile", "path": "Gemfile", diff --git a/docker/spec/fixtures/github/contents_docker_repo_custom.json b/docker/spec/fixtures/github/contents_docker_repo_custom.json index 68f8605e945..d963f665d70 100644 --- a/docker/spec/fixtures/github/contents_docker_repo_custom.json +++ b/docker/spec/fixtures/github/contents_docker_repo_custom.json @@ -111,6 +111,22 @@ "html": "https://github.com/dependabot/dependabot-core/blob/master/Dockerfile-base" } }, + { + "name": "docker-compose.override.yml", + "path": "docker-compose.override.yml", + "sha": "311f9743315183cc6751313cb251cfeb3de45c1a", + "size": 4927, + "url": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.override.yml?ref=master", + "html_url": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.override.yml", + "git_url": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "download_url": "https://github.com/raw/dependabot/dependabot-core/master/docker-compose.override.yml", + "type": "file", + "_links": { + "self": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.override.yml?ref=master", + "git": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "html": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.override.yml" + } + }, { "name": "Gemfile", "path": "Gemfile", diff --git a/docker/spec/fixtures/github/contents_docker_repo_multiple.json b/docker/spec/fixtures/github/contents_docker_repo_multiple.json index a34a38a0298..6ed043c9853 100644 --- a/docker/spec/fixtures/github/contents_docker_repo_multiple.json +++ b/docker/spec/fixtures/github/contents_docker_repo_multiple.json @@ -127,6 +127,38 @@ "html": "https://github.com/dependabot/dependabot-core/blob/master/Dockerfile-base" } }, + { + "name": "docker-compose.yml", + "path": "docker-compose.yml", + "sha": "311f9743315183cc6751313cb251cfeb3de45c1a", + "size": 4927, + "url": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "html_url": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml", + "git_url": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "download_url": "https://github.com/raw/dependabot/dependabot-core/master/docker-compose.yml", + "type": "file", + "_links": { + "self": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.yml?ref=master", + "git": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "html": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.yml" + } + }, + { + "name": "docker-compose.override.yml", + "path": "docker-compose.override.yml", + "sha": "311f9743315183cc6751313cb251cfeb3de45c1a", + "size": 4927, + "url": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.override.yml?ref=master", + "html_url": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.override.yml", + "git_url": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "download_url": "https://github.com/raw/dependabot/dependabot-core/master/docker-compose.override.yml", + "type": "file", + "_links": { + "self": "https://github.com/gitapi/repos/dependabot/dependabot-core/contents/docker-compose.override.yml?ref=master", + "git": "https://github.com/gitapi/repos/dependabot/dependabot-core/git/blobs/311f9743315183cc6751313cb251cfeb3de45c1a", + "html": "https://github.com/dependabot/dependabot-core/blob/master/docker-compose.override.yml" + } + }, { "name": "Gemfile", "path": "Gemfile",