From 0469866de461ef7273c66abf8bed1a42382512b2 Mon Sep 17 00:00:00 2001 From: Aaron Huntsman Date: Mon, 8 Dec 2025 15:18:23 -0600 Subject: [PATCH 1/2] Supply Chain Attestations now support Geo replication Adds implementation for replication of Supply Chain Attestations, including blob replication for the predicate file and signed attestation. Changelog: changed EE: true --- app/models/supply_chain/attestation.rb | 4 +- .../000_inflections.rb | 1 + doc/api/graphql/reference/_index.md | 1 + ee/app/models/ee/supply_chain/attestation.rb | 59 +++++++ .../geo/supply_chain_attestation_registry.rb | 18 +++ .../geo/supply_chain_attestation_state.rb | 12 ++ .../supply_chain_attestation_replicator.rb | 25 +++ .../secondary/registry_consistency_worker.rb | 3 +- ...attestation_force_primary_checksumming.yml | 10 ++ ...o_supply_chain_attestation_replication.yml | 10 ++ ee/lib/gitlab/geo.rb | 3 +- .../geo/supply_chain_attestation_registry.rb | 42 +++++ .../geo/supply_chain_attestation_states.rb | 15 ++ .../factories/supply_chain/attestations.rb | 37 +++++ .../ee/supply_chain/attestation_spec.rb | 152 ++++++++++++++++++ .../supply_chain_attestation_registry_spec.rb | 13 ++ .../supply_chain_attestation_state_spec.rb | 16 ++ ...upply_chain_attestation_replicator_spec.rb | 9 ++ .../registry_consistency_worker_spec.rb | 3 + locale/gitlab.pot | 6 + spec/support/helpers/stub_object_storage.rb | 8 + 21 files changed, 444 insertions(+), 3 deletions(-) create mode 100644 ee/app/models/ee/supply_chain/attestation.rb create mode 100644 ee/app/models/geo/supply_chain_attestation_registry.rb create mode 100644 ee/app/models/geo/supply_chain_attestation_state.rb create mode 100644 ee/app/replicators/geo/supply_chain_attestation_replicator.rb create mode 100644 ee/config/feature_flags/ops/geo_supply_chain_attestation_force_primary_checksumming.yml create mode 100644 ee/config/feature_flags/ops/geo_supply_chain_attestation_replication.yml create mode 100644 ee/spec/factories/geo/supply_chain_attestation_registry.rb create mode 100644 ee/spec/factories/geo/supply_chain_attestation_states.rb create mode 100644 ee/spec/factories/supply_chain/attestations.rb create mode 100644 ee/spec/models/ee/supply_chain/attestation_spec.rb create mode 100644 ee/spec/models/geo/supply_chain_attestation_registry_spec.rb create mode 100644 ee/spec/models/geo/supply_chain_attestation_state_spec.rb create mode 100644 ee/spec/replicators/geo/supply_chain_attestation_replicator_spec.rb diff --git a/app/models/supply_chain/attestation.rb b/app/models/supply_chain/attestation.rb index daaf5ce128d382..2962fd2de2e6e6 100644 --- a/app/models/supply_chain/attestation.rb +++ b/app/models/supply_chain/attestation.rb @@ -15,7 +15,7 @@ class Attestation < ::ApplicationRecord has_internal_id :iid, scope: :project - validates :project_id, presence: true + validates :project, presence: true validates :file, presence: true, unless: :error? validates :predicate_kind, presence: true validates :predicate_type, presence: true @@ -49,3 +49,5 @@ def self.find_provenance(project:, subject_digest:) end end end + +SupplyChain::Attestation.prepend_mod diff --git a/config/initializers_before_autoloader/000_inflections.rb b/config/initializers_before_autoloader/000_inflections.rb index 32aafec1e5ece8..a693443b7ec432 100644 --- a/config/initializers_before_autoloader/000_inflections.rb +++ b/config/initializers_before_autoloader/000_inflections.rb @@ -39,6 +39,7 @@ project_repository_registry project_statistics snippet_repository_registry + supply_chain_attestation_registry system_note_metadata terraform_state_version_registry vulnerabilities_feedback diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index e874a33df65a7c..6352057e94eaf7 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -52444,6 +52444,7 @@ Geo registry class. | `PROJECT_REPOSITORY_REGISTRY` | Geo::ProjectRepositoryRegistry registry class. | | `PROJECT_WIKI_REPOSITORY_REGISTRY` | Geo::ProjectWikiRepositoryRegistry registry class. | | `SNIPPET_REPOSITORY_REGISTRY` | Geo::SnippetRepositoryRegistry registry class. | +| `SUPPLY_CHAIN_ATTESTATION_REGISTRY` | Geo::SupplyChainAttestationRegistry registry class. | | `TERRAFORM_STATE_VERSION_REGISTRY` | Geo::TerraformStateVersionRegistry registry class. | | `UPLOAD_REGISTRY` | Geo::UploadRegistry registry class. | diff --git a/ee/app/models/ee/supply_chain/attestation.rb b/ee/app/models/ee/supply_chain/attestation.rb new file mode 100644 index 00000000000000..dd7a6bfc810a20 --- /dev/null +++ b/ee/app/models/ee/supply_chain/attestation.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module EE + module SupplyChain + module Attestation + extend ActiveSupport::Concern + + prepended do + include ::Geo::ReplicableModel + include ::Geo::VerifiableModel + + delegate(*::Geo::VerificationState::VERIFICATION_METHODS, to: :supply_chain_attestation_state) + + with_replicator Geo::SupplyChainAttestationReplicator + + has_one :supply_chain_attestation_state, autosave: false, inverse_of: :supply_chain_attestation, + class_name: 'Geo::SupplyChainAttestationState' + + scope :project_id_in, ->(ids) { where(project_id: ids) } + + scope :with_verification_state, ->(state) { + joins(:supply_chain_attestation_state) + .where(supply_chain_attestation_states: { verification_state: verification_state_value(state) }) + } + + def verification_state_object + supply_chain_attestation_state + end + end + + class_methods do + extend ::Gitlab::Utils::Override + + override :verification_state_model_key + def verification_state_model_key + :supply_chain_attestation_id + end + + override :verification_state_table_class + def verification_state_table_class + ::Geo::SupplyChainAttestationState + end + + # @return [ActiveRecord::Relation] scope observing selective sync settings + # of the given node + override :selective_sync_scope + def selective_sync_scope(node, **_params) + return all unless node.selective_sync? + + project_id_in(::Project.selective_sync_scope(node)) + end + end + + def supply_chain_attestation_state + super || build_supply_chain_attestation_state + end + end + end +end diff --git a/ee/app/models/geo/supply_chain_attestation_registry.rb b/ee/app/models/geo/supply_chain_attestation_registry.rb new file mode 100644 index 00000000000000..64a3ce36f46f5d --- /dev/null +++ b/ee/app/models/geo/supply_chain_attestation_registry.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Geo + class SupplyChainAttestationRegistry < Geo::BaseRegistry + include ::Geo::ReplicableRegistry + include ::Geo::VerifiableRegistry + + belongs_to :supply_chain_attestation, class_name: 'SupplyChain::Attestation' + + def self.model_class + ::SupplyChain::Attestation + end + + def self.model_foreign_key + :supply_chain_attestation_id + end + end +end diff --git a/ee/app/models/geo/supply_chain_attestation_state.rb b/ee/app/models/geo/supply_chain_attestation_state.rb new file mode 100644 index 00000000000000..9249dcb0048c20 --- /dev/null +++ b/ee/app/models/geo/supply_chain_attestation_state.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Geo + class SupplyChainAttestationState < ApplicationRecord + include ::Geo::VerificationStateDefinition + + belongs_to :supply_chain_attestation, inverse_of: :supply_chain_attestation_state, + class_name: 'SupplyChain::Attestation' + + validates :verification_state, :supply_chain_attestation, presence: true + end +end diff --git a/ee/app/replicators/geo/supply_chain_attestation_replicator.rb b/ee/app/replicators/geo/supply_chain_attestation_replicator.rb new file mode 100644 index 00000000000000..1474c41438f854 --- /dev/null +++ b/ee/app/replicators/geo/supply_chain_attestation_replicator.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Geo + class SupplyChainAttestationReplicator < Gitlab::Geo::Replicator + include ::Geo::BlobReplicatorStrategy + + def self.model + ::SupplyChain::Attestation + end + + # @return [String] human-readable title. + def self.replicable_title + s_('Geo|Supply Chain Attestation') + end + + # @return [String] pluralized human-readable title. + def self.replicable_title_plural + s_('Geo|Supply Chain Attestations') + end + + def carrierwave_uploader + model_record.file + end + end +end diff --git a/ee/app/workers/geo/secondary/registry_consistency_worker.rb b/ee/app/workers/geo/secondary/registry_consistency_worker.rb index 619f4468b23f7e..291d888f59f509 100644 --- a/ee/app/workers/geo/secondary/registry_consistency_worker.rb +++ b/ee/app/workers/geo/secondary/registry_consistency_worker.rb @@ -36,7 +36,8 @@ class RegistryConsistencyWorker Geo::DependencyProxyManifestRegistry, Geo::ProjectWikiRepositoryRegistry, Geo::ProjectRepositoryRegistry, - Geo::PackagesNugetSymbolRegistry + Geo::PackagesNugetSymbolRegistry, + Geo::SupplyChainAttestationRegistry ].freeze BATCH_SIZE = 10000 diff --git a/ee/config/feature_flags/ops/geo_supply_chain_attestation_force_primary_checksumming.yml b/ee/config/feature_flags/ops/geo_supply_chain_attestation_force_primary_checksumming.yml new file mode 100644 index 00000000000000..277f38ad6a3769 --- /dev/null +++ b/ee/config/feature_flags/ops/geo_supply_chain_attestation_force_primary_checksumming.yml @@ -0,0 +1,10 @@ +--- +name: geo_supply_chain_attestation_force_primary_checksumming +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/571772 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/215606 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/579710 +milestone: '18.7' +group: group::geo +type: ops +default_enabled: false diff --git a/ee/config/feature_flags/ops/geo_supply_chain_attestation_replication.yml b/ee/config/feature_flags/ops/geo_supply_chain_attestation_replication.yml new file mode 100644 index 00000000000000..057e1d5a3f906b --- /dev/null +++ b/ee/config/feature_flags/ops/geo_supply_chain_attestation_replication.yml @@ -0,0 +1,10 @@ +--- +name: geo_supply_chain_attestation_replication +description: +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/571772 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/215606 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/579391 +milestone: '18.7' +group: group::geo +type: ops +default_enabled: false diff --git a/ee/lib/gitlab/geo.rb b/ee/lib/gitlab/geo.rb index 4fbab4c1ad4a98..67a7c70690d488 100644 --- a/ee/lib/gitlab/geo.rb +++ b/ee/lib/gitlab/geo.rb @@ -43,7 +43,8 @@ module Geo ::Geo::SnippetRepositoryReplicator, ::Geo::TerraformStateVersionReplicator, ::Geo::UploadReplicator, - ::Geo::PackagesNugetSymbolReplicator + ::Geo::PackagesNugetSymbolReplicator, + ::Geo::SupplyChainAttestationReplicator ].freeze # We "regenerate" an 1hour valid JWT every 30 minutes, resulting in diff --git a/ee/spec/factories/geo/supply_chain_attestation_registry.rb b/ee/spec/factories/geo/supply_chain_attestation_registry.rb new file mode 100644 index 00000000000000..85bc9a5dd760bf --- /dev/null +++ b/ee/spec/factories/geo/supply_chain_attestation_registry.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :geo_supply_chain_attestation_registry, class: 'Geo::SupplyChainAttestationRegistry' do + supply_chain_attestation # This association should have data, like a file or repository + state { Geo::SupplyChainAttestationRegistry.state_value(:pending) } + + trait :synced do + state { Geo::SupplyChainAttestationRegistry.state_value(:synced) } + last_synced_at { 5.days.ago } + end + + trait :failed do + state { Geo::SupplyChainAttestationRegistry.state_value(:failed) } + last_synced_at { 1.day.ago } + retry_count { 2 } + retry_at { 2.hours.from_now } + last_sync_failure { 'Random error' } + end + + trait :started do + state { Geo::SupplyChainAttestationRegistry.state_value(:started) } + last_synced_at { 1.day.ago } + retry_count { 0 } + end + + trait :verification_succeeded do + synced + verification_checksum { 'e079a831cab27bcda7d81cd9b48296d0c3dd92ef' } + verification_state { Geo::SupplyChainAttestationRegistry.verification_state_value(:verification_succeeded) } + verified_at { 5.days.ago } + end + + trait :verification_failed do + synced + verification_failure { 'Could not calculate the checksum' } + verification_state { Geo::SupplyChainAttestationRegistry.verification_state_value(:verification_failed) } + verification_retry_count { 1 } + verification_retry_at { 2.hours.from_now } + end + end +end diff --git a/ee/spec/factories/geo/supply_chain_attestation_states.rb b/ee/spec/factories/geo/supply_chain_attestation_states.rb new file mode 100644 index 00000000000000..31189183d5e9e7 --- /dev/null +++ b/ee/spec/factories/geo/supply_chain_attestation_states.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :geo_supply_chain_attestation_state, class: 'Geo::SupplyChainAttestationState' do + supply_chain_attestation factory: :supply_chain_attestation + + trait :checksummed do + verification_checksum { 'abc' } + end + + trait :checksum_failure do + verification_failure { 'Could not calculate the checksum' } + end + end +end diff --git a/ee/spec/factories/supply_chain/attestations.rb b/ee/spec/factories/supply_chain/attestations.rb new file mode 100644 index 00000000000000..516016301b37a2 --- /dev/null +++ b/ee/spec/factories/supply_chain/attestations.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +FactoryBot.modify do + factory :supply_chain_attestation do + trait :verification_succeeded do + verification_checksum { 'abc' } + verification_state { SupplyChain::Attestation.verification_state_value(:verification_succeeded) } + + after(:create) do |instance, _| + instance.verification_failure = nil + instance.verification_state = ::SupplyChain::Attestation.verification_state_value(:verification_started) + instance.supply_chain_attestation_state.supply_chain_attestation = instance + instance.verification_succeeded! + end + end + + trait :verification_failed do + verification_failure { 'Could not calculate the checksum' } + verification_state { SupplyChain::Attestation.verification_state_value(:verification_failed) } + + # + # Geo::VerifiableReplicator#after_verifiable_update tries to verify + # the replicable async and marks it as verification started when the + # model record is created/updated. + # + after(:create) do |instance, evaluator| + instance.verification_failure = evaluator.verification_failure + instance.supply_chain_attestation_state.supply_chain_attestation = instance + instance.verification_failed! + end + end + + trait :remote_store do + file_store { SupplyChain::AttestationUploader::Store::REMOTE } + end + end +end diff --git a/ee/spec/models/ee/supply_chain/attestation_spec.rb b/ee/spec/models/ee/supply_chain/attestation_spec.rb new file mode 100644 index 00000000000000..220dc7c698be46 --- /dev/null +++ b/ee/spec/models/ee/supply_chain/attestation_spec.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SupplyChain::Attestation, feature_category: :geo_replication do + include_examples 'a verifiable model for verification state' do + let(:verifiable_model_record) { build(:supply_chain_attestation) } + let(:unverifiable_model_record) { nil } + end + + describe 'scopes' do + describe '.project_id_in' do + let_it_be(:project) { create(:project) } + let_it_be(:other_project) { create(:project) } + let_it_be(:supply_chain_attestation) { create(:supply_chain_attestation, project:) } + let_it_be(:other_supply_chain_attestation) { create(:supply_chain_attestation, project: other_project) } + + subject { described_class.project_id_in([project.id]) } + + it { is_expected.to contain_exactly(supply_chain_attestation) } + end + end + + describe '.replicables_for_current_secondary' do + include ::EE::GeoHelpers + + subject(:replicables) { described_class.replicables_for_current_secondary(1..described_class.last.id) } + + context 'for replication' do + let_it_be(:secondary) { create(:geo_node) } + let_it_be(:supply_chain_attestation) { create(:supply_chain_attestation) } + + before do + stub_current_geo_node(secondary) + end + + it { is_expected.to be_an(ActiveRecord::Relation).and include(supply_chain_attestation) } + end + + context 'for object storage' do + before do + stub_current_geo_node(secondary) + stub_supply_chain_attestation_object_storage + end + + let_it_be(:local_stored) { create(:supply_chain_attestation) } + # Cannot use let_it_be because it depends on stub_supply_chain_attestation_object_storage + let!(:object_stored) { create(:supply_chain_attestation, :object_storage) } + + context 'with sync object storage enabled' do + let_it_be(:secondary) { create(:geo_node, sync_object_storage: true) } + + it { is_expected.to include(local_stored, object_stored) } + end + + context 'with sync object storage disabled' do + let_it_be(:secondary) { create(:geo_node, sync_object_storage: false) } + + it { is_expected.to include(local_stored).and exclude(object_stored) } + end + end + + context 'for selective sync' do + # Create an attestation owned by a project on shard foo + let_it_be(:project_on_shard_foo) { create_project_on_shard('foo') } + + let_it_be(:supply_chain_attestation_on_shard_foo) do + create(:supply_chain_attestation, project: project_on_shard_foo) + end + + # Create an attestation owned by a project on shard bar + let_it_be(:project_on_shard_bar) { create_project_on_shard('bar') } + + let_it_be(:supply_chain_attestation_on_shard_bar) do + create(:supply_chain_attestation, project: project_on_shard_bar) + end + + # Create an attestation owned by a particular namespace, and create + # another attestation owned via a nested group. + let_it_be(:root_group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: root_group) } + let_it_be(:project_in_root_group) { create(:project, group: root_group) } + let_it_be(:project_in_subgroup) { create(:project, group: subgroup) } + + let_it_be(:supply_chain_attestation_in_root_group) do + create(:supply_chain_attestation, project: project_in_root_group) + end + + let_it_be(:supply_chain_attestation_in_subgroup) do + create(:supply_chain_attestation, project: project_in_subgroup) + end + + before do + stub_current_geo_node(secondary) + end + + context 'without selective sync' do + let_it_be(:secondary) { create(:geo_node) } + + it 'does not exclude any records' do + is_expected.to include( + supply_chain_attestation_on_shard_foo, + supply_chain_attestation_on_shard_bar, + supply_chain_attestation_in_root_group, + supply_chain_attestation_in_subgroup + ) + end + end + + context 'with selective sync by shard' do + let_it_be(:secondary) { create(:geo_node, selective_sync_type: 'shards', selective_sync_shards: ['foo']) } + + it 'includes records on a selected shard' do + is_expected.to include(supply_chain_attestation_on_shard_foo) + .and exclude(supply_chain_attestation_on_shard_bar) + end + end + + context 'with selective sync by namespace' do + context 'with sync object storage enabled' do + let_it_be(:secondary) { create(:geo_node, selective_sync_type: 'namespaces', namespaces: [root_group]) } + + it 'includes records owned by projects on a selected namespace' do + is_expected.to include(supply_chain_attestation_in_root_group, supply_chain_attestation_in_subgroup) + .and exclude(supply_chain_attestation_on_shard_foo, supply_chain_attestation_on_shard_bar) + end + end + + # The most complex permutation + context 'with sync object storage disabled' do + let_it_be(:secondary) do + create(:geo_node, selective_sync_type: 'namespaces', namespaces: [root_group], sync_object_storage: false) + end + + it 'includes only locally stored records owned by projects on a selected namespace' do + is_expected.to include(supply_chain_attestation_in_root_group, supply_chain_attestation_in_subgroup) + .and exclude(supply_chain_attestation_on_shard_foo, supply_chain_attestation_on_shard_bar) + end + + context 'with object stored records' do + before do + supply_chain_attestation_in_root_group.update_column(:file_store, ObjectStorage::Store::REMOTE) + supply_chain_attestation_in_subgroup.update_column(:file_store, ObjectStorage::Store::REMOTE) + end + + it { is_expected.to exclude(supply_chain_attestation_in_root_group, supply_chain_attestation_in_subgroup) } + end + end + end + end + end +end diff --git a/ee/spec/models/geo/supply_chain_attestation_registry_spec.rb b/ee/spec/models/geo/supply_chain_attestation_registry_spec.rb new file mode 100644 index 00000000000000..9101a5c1125257 --- /dev/null +++ b/ee/spec/models/geo/supply_chain_attestation_registry_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Geo::SupplyChainAttestationRegistry, :geo, feature_category: :geo_replication do + let_it_be(:registry) { build(:geo_supply_chain_attestation_registry) } + + specify 'factory is valid' do + expect(registry).to be_valid + end + + include_examples 'a Geo framework registry' +end diff --git a/ee/spec/models/geo/supply_chain_attestation_state_spec.rb b/ee/spec/models/geo/supply_chain_attestation_state_spec.rb new file mode 100644 index 00000000000000..9f978f31b200b2 --- /dev/null +++ b/ee/spec/models/geo/supply_chain_attestation_state_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Geo::SupplyChainAttestationState, :geo, feature_category: :geo_replication do + it { is_expected.to be_a ::Geo::VerificationStateDefinition } + + describe 'associations' do + it { is_expected.to belong_to(:supply_chain_attestation).class_name('::SupplyChain::Attestation') } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:supply_chain_attestation) } + it { is_expected.to validate_presence_of(:verification_state) } + end +end diff --git a/ee/spec/replicators/geo/supply_chain_attestation_replicator_spec.rb b/ee/spec/replicators/geo/supply_chain_attestation_replicator_spec.rb new file mode 100644 index 00000000000000..90837d05469ca2 --- /dev/null +++ b/ee/spec/replicators/geo/supply_chain_attestation_replicator_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Geo::SupplyChainAttestationReplicator, feature_category: :geo_replication do + let(:model_record) { create(:supply_chain_attestation) } + + include_examples 'a blob replicator' +end diff --git a/ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb b/ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb index 8fc396eca30766..499f00ff371789 100644 --- a/ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb +++ b/ee/spec/workers/geo/secondary/registry_consistency_worker_spec.rb @@ -94,6 +94,7 @@ project_wiki_repository = create(:project_wiki_repository, project: project) design_management_repository = create(:design_management_repository, project: project) nuget_symbol = create(:nuget_symbol) + supply_chain_attestation = create(:supply_chain_attestation) expect(Geo::ContainerRepositoryRegistry.where(container_repository_id: container_repository.id).count).to eq(0) expect(Geo::DesignManagementRepositoryRegistry.where(design_management_repository_id: design_management_repository.id).count).to eq(0) @@ -112,6 +113,7 @@ expect(Geo::ProjectWikiRepositoryRegistry.where(project_wiki_repository: project_wiki_repository.id).count).to eq(0) expect(Geo::ProjectRepositoryRegistry.where(project_id: project.id).count).to eq(0) expect(Geo::PackagesNugetSymbolRegistry.where(packages_nuget_symbol_id: nuget_symbol.id).count).to eq(0) + expect(Geo::SupplyChainAttestationRegistry.where(supply_chain_attestation_id: supply_chain_attestation.id).count).to eq(0) subject.perform @@ -131,6 +133,7 @@ expect(Geo::ProjectWikiRepositoryRegistry.where(project_wiki_repository: project_wiki_repository.id).count).to eq(1) expect(Geo::ProjectRepositoryRegistry.where(project_id: project.id).count).to eq(1) expect(Geo::PackagesNugetSymbolRegistry.where(packages_nuget_symbol_id: nuget_symbol.id).count).to eq(1) + expect(Geo::SupplyChainAttestationRegistry.where(supply_chain_attestation_id: supply_chain_attestation.id).count).to eq(1) end context 'when the current Geo node is disabled or primary' do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d98ac57d3a531d..ef2005d080b7a4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -30745,6 +30745,12 @@ msgstr "" msgid "Geo|Successfully recalculated checksum for %{name}." msgstr "" +msgid "Geo|Supply Chain Attestation" +msgstr "" + +msgid "Geo|Supply Chain Attestations" +msgstr "" + msgid "Geo|Sync failed" msgstr "" diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index a2e45bd3dd3491..f65903d969d5dd 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -154,6 +154,14 @@ def stub_pages_object_storage(uploader = described_class, **params) ) end + def stub_supply_chain_attestation_object_storage(**params) + stub_object_storage_uploader( + config: Gitlab.config.uploads.object_store, + uploader: ::SupplyChain::AttestationUploader, + **params + ) + end + def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id") stub_request(:post, %r{\A#{endpoint}tmp/uploads/[%A-Za-z0-9-]*\?uploads\z}) .to_return status: 200, body: <<-EOS.strip_heredoc -- GitLab From b7c915b25f29a24e6e139c5ccfa31e8dfe443143 Mon Sep 17 00:00:00 2001 From: Aaron Huntsman Date: Tue, 16 Dec 2025 05:59:00 -0600 Subject: [PATCH 2/2] Update fixture schemas --- .../object_schemas/geo_node_usage.json | 40 ++++++++++ .../public_api/v4/geo_node_status.json | 78 +++++++++++++++++++ .../public_api/v4/geo_site_status.json | 78 +++++++++++++++++++ .../types/geo/registry_class_enum_spec.rb | 1 + 4 files changed, 197 insertions(+) diff --git a/ee/config/metrics/object_schemas/geo_node_usage.json b/ee/config/metrics/object_schemas/geo_node_usage.json index 50055acd10fe5c..9224a630cf879f 100644 --- a/ee/config/metrics/object_schemas/geo_node_usage.json +++ b/ee/config/metrics/object_schemas/geo_node_usage.json @@ -667,6 +667,46 @@ "description": "Snippet repositories verified count", "type": "number" }, + "supply_chain_attestations_checksum_failed_count": { + "description": "Supply chain attestations checksum failed count", + "type": "number" + }, + "supply_chain_attestations_checksum_total_count": { + "description": "Supply chain attestations checksum total count", + "type": "number" + }, + "supply_chain_attestations_checksummed_count": { + "description": "Supply chain attestations checksummed count", + "type": "number" + }, + "supply_chain_attestations_count": { + "description": "Supply chain attestations count", + "type": "number" + }, + "supply_chain_attestations_failed_count": { + "description": "Supply chain attestations failed count", + "type": "number" + }, + "supply_chain_attestations_registry_count": { + "description": "Supply chain attestations registry count", + "type": "number" + }, + "supply_chain_attestations_synced_count": { + "description": "Supply chain attestations synced count", + "type": "number" + }, + "supply_chain_attestations_verification_failed_count": { + "description": "Supply chain attestations verification failed count", + "type": "number" + }, + "supply_chain_attestations_verification_total_count": { + "description": "Supply chain attestations verification total count", + "type": "number" + }, + "supply_chain_attestations_verified_count": { + "description": "Supply chain attestations verified count", + "type": "number" + }, "terraform_state_versions_checksum_failed_count": { "description": "Terraform state versions checksum failed count", "type": "number" diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json b/ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json index 4e9404736c99a8..db9ab52cdc7b87 100644 --- a/ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json +++ b/ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json @@ -140,6 +140,18 @@ "pages_deployments_verification_total_count", "pages_deployments_verified_count", "pages_deployments_verified_in_percentage", + "supply_chain_attestations_count", + "supply_chain_attestations_checksum_total_count", + "supply_chain_attestations_checksummed_count", + "supply_chain_attestations_checksum_failed_count", + "supply_chain_attestations_synced_count", + "supply_chain_attestations_failed_count", + "supply_chain_attestations_registry_count", + "supply_chain_attestations_verification_total_count", + "supply_chain_attestations_verified_count", + "supply_chain_attestations_verification_failed_count", + "supply_chain_attestations_synced_in_percentage", + "supply_chain_attestations_verified_in_percentage", "terraform_state_versions_count", "terraform_state_versions_checksum_failed_count", "terraform_state_versions_checksum_total_count", @@ -997,6 +1009,72 @@ "pages_deployments_verified_in_percentage": { "type": "string" }, + "supply_chain_attestations_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksummed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksum_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksum_total_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_registry_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_synced_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_synced_in_percentage": { + "type": "string" + }, + "supply_chain_attestations_verification_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verification_total_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verified_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verified_in_percentage": { + "type": "string" + }, "terraform_state_versions_count": { "type": [ "integer", diff --git a/ee/spec/fixtures/api/schemas/public_api/v4/geo_site_status.json b/ee/spec/fixtures/api/schemas/public_api/v4/geo_site_status.json index 1c46d05923719c..88952b771d83cf 100644 --- a/ee/spec/fixtures/api/schemas/public_api/v4/geo_site_status.json +++ b/ee/spec/fixtures/api/schemas/public_api/v4/geo_site_status.json @@ -128,6 +128,18 @@ "packages_nuget_symbols_verification_failed_count", "packages_nuget_symbols_synced_in_percentage", "packages_nuget_symbols_verified_in_percentage", + "supply_chain_attestations_count", + "supply_chain_attestations_checksum_total_count", + "supply_chain_attestations_checksummed_count", + "supply_chain_attestations_checksum_failed_count", + "supply_chain_attestations_synced_count", + "supply_chain_attestations_failed_count", + "supply_chain_attestations_registry_count", + "supply_chain_attestations_verification_total_count", + "supply_chain_attestations_verified_count", + "supply_chain_attestations_verification_failed_count", + "supply_chain_attestations_synced_in_percentage", + "supply_chain_attestations_verified_in_percentage", "pages_deployments_count", "pages_deployments_checksum_failed_count", "pages_deployments_checksum_total_count", @@ -997,6 +1009,72 @@ "pages_deployments_verified_in_percentage": { "type": "string" }, + "supply_chain_attestations_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksummed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksum_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_checksum_total_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_registry_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_synced_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_synced_in_percentage": { + "type": "string" + }, + "supply_chain_attestations_verification_failed_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verification_total_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verified_count": { + "type": [ + "integer", + "null" + ] + }, + "supply_chain_attestations_verified_in_percentage": { + "type": "string" + }, "terraform_state_versions_count": { "type": [ "integer", diff --git a/ee/spec/graphql/types/geo/registry_class_enum_spec.rb b/ee/spec/graphql/types/geo/registry_class_enum_spec.rb index abcde84f7a876c..74fdc39dbf750c 100644 --- a/ee/spec/graphql/types/geo/registry_class_enum_spec.rb +++ b/ee/spec/graphql/types/geo/registry_class_enum_spec.rb @@ -23,6 +23,7 @@ PROJECT_REPOSITORY_REGISTRY GROUP_WIKI_REPOSITORY_REGISTRY PACKAGES_NUGET_SYMBOL_REGISTRY + SUPPLY_CHAIN_ATTESTATION_REGISTRY ] end -- GitLab