From fc4d4a62a779d5b98733bce26279e764469a2e42 Mon Sep 17 00:00:00 2001 From: Zakir Dzhamaliddinov Date: Fri, 13 Dec 2024 16:03:22 +0300 Subject: [PATCH] Security: switch to using image's digest sha256 value on promotion --- CHANGELOG.md | 4 ++++ lib/command/base.rb | 11 +++++++++++ lib/command/deploy_image.rb | 12 ++++++++---- lib/command/promote_app_from_upstream.rb | 2 +- spec/command/deploy_image_spec.rb | 11 +++++++++++ spec/command/promote_app_from_upstream_spec.rb | 3 +++ 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3e190e..9708e95b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ _Please add entries here for your pull requests that have not yet been released. - Fixed issue where app cannot be deleted because one of the workloads has a volumeset in-use. [PR 245](https://github.com/shakacode/control-plane-flow/pull/245) by [Zakir Dzhamaliddinov](https://github.com/zzaakiirr). +### Changed + +- Providing digest (SHA256 value) for image link on promotion from upstream. [PR 247](https://github.com/shakacode/control-plane-flow/pull/247) by [Zakir Dzhamaliddinov](https://github.com/zzaakiirr). + ## [4.0.0] - 2024-08-21 ### Fixed diff --git a/lib/command/base.rb b/lib/command/base.rb index dd05997b..4ec25766 100644 --- a/lib/command/base.rb +++ b/lib/command/base.rb @@ -442,6 +442,17 @@ def self.add_app_identity_option(required: false) } } end + + def self.use_digest_ref_option(required: false) + { + name: :use_digest_ref, + params: { + desc: "Uses the Docker image's digest as its reference.", + type: :boolean, + required: required + } + } + end # rubocop:enable Metrics/MethodLength def self.all_options diff --git a/lib/command/deploy_image.rb b/lib/command/deploy_image.rb index 485a48d8..22713014 100644 --- a/lib/command/deploy_image.rb +++ b/lib/command/deploy_image.rb @@ -5,7 +5,8 @@ class DeployImage < Base NAME = "deploy-image" OPTIONS = [ app_option(required: true), - run_release_phase_option + run_release_phase_option, + use_digest_ref_option ].freeze DESCRIPTION = "Deploys the latest image to app workloads, and runs a release script (optional)" LONG_DESCRIPTION = <<~DESC @@ -15,22 +16,25 @@ class DeployImage < Base - If the release script exits with a non-zero code, the command will stop executing and also exit with a non-zero code DESC - def call # rubocop:disable Metrics/MethodLength + def call # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity run_release_script if config.options[:run_release_phase] deployed_endpoints = {} image = cp.latest_image - if cp.fetch_image_details(image).nil? + image_details = cp.fetch_image_details(image) + if image_details.nil? raise "Image '#{image}' does not exist in the Docker repository on Control Plane " \ "(see https://console.cpln.io/console/org/#{config.org}/repository/#{config.app}). " \ "Use `cpflow build-image` first." end + image = "#{image_details['name']}@#{image_details['digest']}" if config.options[:use_digest_ref] + config[:app_workloads].each do |workload| workload_data = cp.fetch_workload!(workload) workload_data.dig("spec", "containers").each do |container| - next unless container["image"].match?(%r{^/org/#{config.org}/image/#{config.app}:}) + next unless container["image"].match?(%r{^/org/#{config.org}/image/#{config.app}}) container_name = container["name"] step("Deploying image '#{image}' for workload '#{container_name}'") do diff --git a/lib/command/promote_app_from_upstream.rb b/lib/command/promote_app_from_upstream.rb index 7db2e868..4e6df1f7 100644 --- a/lib/command/promote_app_from_upstream.rb +++ b/lib/command/promote_app_from_upstream.rb @@ -32,7 +32,7 @@ def copy_image_from_upstream def deploy_image args = [] args.push("--run-release-phase") if config.current[:release_script] - run_cpflow_command("deploy-image", "-a", config.app, *args) + run_cpflow_command("deploy-image", "-a", config.app, "--use-digest-ref", *args) end end end diff --git a/spec/command/deploy_image_spec.rb b/spec/command/deploy_image_spec.rb index b17bd5f8..1f7c0756 100644 --- a/spec/command/deploy_image_spec.rb +++ b/spec/command/deploy_image_spec.rb @@ -47,6 +47,17 @@ end end + context "with --use-digest-ref option" do + let!(:app) { dummy_test_app("rails-non-app-image", create_if_not_exists: true) } + + it "deploys latest image with digest reference", :slow do + result = run_cpflow_command("deploy-image", "-a", app, "--use-digest-ref") + + expect(result[:status]).to eq(0) + expect(result[:stderr]).to match(/Deploying image '#{app}:\d+@sha256:[a-fA-F0-9]{64}'/) + end + end + context "when 'release_script' is not defined" do let!(:app) { dummy_test_app("nothing") } diff --git a/spec/command/promote_app_from_upstream_spec.rb b/spec/command/promote_app_from_upstream_spec.rb index fd6ae9c3..fccdaa10 100644 --- a/spec/command/promote_app_from_upstream_spec.rb +++ b/spec/command/promote_app_from_upstream_spec.rb @@ -30,6 +30,7 @@ expect(result[:stderr]).to match(%r{Pulling image from '.+?/#{upstream_app}:1'}) expect(result[:stderr]).to match(%r{Pushing image to '.+?/#{app}:1'}) expect(result[:stderr]).not_to include("Running release script") + expect(result[:stderr]).to match(/Deploying image '#{app}:1@sha256:[a-fA-F0-9]{64}'/) expect(result[:stderr]).to match(%r{rails: https://rails-.+?.cpln.app}) end end @@ -64,6 +65,7 @@ expect(result[:stderr]).to match(%r{Pushing image to '.+?/#{app}:1'}) expect(result[:stderr]).to include("Running release script") expect(result[:stderr]).to include("Failed to run release script") + expect(result[:stderr]).not_to include("Deploying image") expect(result[:stderr]).not_to match(%r{rails: https://rails-.+?.cpln.app}) end end @@ -98,6 +100,7 @@ expect(result[:stderr]).to match(%r{Pushing image to '.+?/#{app}:1'}) expect(result[:stderr]).to include("Running release script") expect(result[:stderr]).to include("Finished running release script") + expect(result[:stderr]).to match(/Deploying image '#{app}:1@sha256:[a-fA-F0-9]{64}'/) expect(result[:stderr]).to match(%r{rails: https://rails-.+?.cpln.app}) end end