diff --git a/app/models/supply_chain/attestation.rb b/app/models/supply_chain/attestation.rb
index daaf5ce128d3826a46457b637418db5d6cf6923b..2962fd2de2e6e6cb9da6e0d8c452e79ed7ee1394 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 32aafec1e5ece884eaf4bb2f4810d9963f9b6d0a..a693443b7ec432651f33e57fc6e0b97cc3b4d0cd 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 e874a33df65a7c7930ee5b9063ae39f95ffac653..6352057e94eaf719f19c24ac8a1b7b033a8cde0a 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 0000000000000000000000000000000000000000..dd7a6bfc810a20adcaa32ad60fcdd41746c65418
--- /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 0000000000000000000000000000000000000000..64a3ce36f46f5d0754d4630a0750ea4d71fbd41b
--- /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 0000000000000000000000000000000000000000..9249dcb0048c206ce62edeb435b31447229e3ecc
--- /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 0000000000000000000000000000000000000000..1474c41438f85403bc5c87d6970fa4673dc476bd
--- /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 619f4468b23f7e59ebbbfe4cd57595c83345ee15..291d888f59f509a9d2d621c19c2a6a7cb90909ee 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 0000000000000000000000000000000000000000..277f38ad6a3769ae27d511c2b2dfca93a2932919
--- /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 0000000000000000000000000000000000000000..057e1d5a3f906bafec393cd16c15f2dcd9daba36
--- /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/config/metrics/object_schemas/geo_node_usage.json b/ee/config/metrics/object_schemas/geo_node_usage.json
index 50055acd10fe5c67ebe041037a3ee15bbd4b53e3..9224a630cf879ffcea9047d37378b485b32a7149 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/lib/gitlab/geo.rb b/ee/lib/gitlab/geo.rb
index 4fbab4c1ad4a98ba3a9aa84980406dd587e3b43e..67a7c70690d48889381fba39269c17a3a397d457 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 0000000000000000000000000000000000000000..85bc9a5dd760bfc67d8d2efa2ae04ddcdb2433ce
--- /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 0000000000000000000000000000000000000000..31189183d5e9e763af2b193be0d5e6e6be8a8de2
--- /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 0000000000000000000000000000000000000000..516016301b37a2f85c5424be06bc230b5f00e7d6
--- /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/fixtures/api/schemas/public_api/v4/geo_node_status.json b/ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json
index 4e9404736c99a8e3d9e41fcb645ce478f222c2c8..db9ab52cdc7b87ee11c4b33b9f197713025c312d 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 1c46d05923719ca6ea064b51b924d5942a852306..88952b771d83cf3d4732689d88ec205b62a27783 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 abcde84f7a876cebde3fae8a917f1138a6ffe51c..74fdc39dbf750cf8bd17a3797b9671e219a8db5e 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
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 0000000000000000000000000000000000000000..220dc7c698be465670b7cd403e07263de435d56c
--- /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 0000000000000000000000000000000000000000..9101a5c1125257f8b7ce3255ce7fa6ca15f6a505
--- /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 0000000000000000000000000000000000000000..9f978f31b200b28719f51cb6b4ee239ce2fefabe
--- /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 0000000000000000000000000000000000000000..90837d05469ca2c156d56b54647193bc8eca5364
--- /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 8fc396eca3076620cc0b187c1ff41ae612351c2b..499f00ff371789c0dc9b07d259a5c58b7a24ec97 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 d98ac57d3a531d6a55357bc5b2df12590e391d7e..ef2005d080b7a4d668786cb5d6d2ae8764a3795c 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 a2e45bd3dd3491c782b4abf84991e99238d87911..f65903d969d5dd687a8c01fded0a293cfa018063 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