Implement Geo support of Supply Chain Attestations behind FF
What does this MR do and why?
Adds implementation for Geo replication of SupplyChain::Attestation records, using blob replication strategy for the file field. Also implements selective sync by organization.
Note: The predicate_file field was added to the model but is not used and intended to be removed (discussion here).
Adds feature flags for safe rollout:
-
geo_supply_chain_attestation_replication- controls replication functionality -
geo_supply_chain_attestation_checksumming- controls verification on primary
How to set up and validate locally
Prerequisites
Click to expand
1. **Set up Geo with GDK** - Follow the GDK Geo setup guide to configure a primary and secondary Geo instance - Enable object storage for Merge Request Diffs - Ensure both instances are running properly 2. **Enable Geo replication for attestations + organization features** - Run these Rails commands on your primary GDK instance in Rails console: ```ruby Feature.enable(:geo_supply_chain_attestation_replication) Feature.enable(:geo_supply_chain_attestation_force_primary_checksumming) Feature.enable_percentage_of_time(:allow_organization_creation, 100) Feature.enable_percentage_of_time(:organization_switching, 100) Feature.enable_percentage_of_time(:ui_for_organizations, 100) ``` 3. **Create test organizations** - Run these Rails commands to create test organizations with projects: ```ruby # Create first organization with owner org1 = Organizations::Organization.create!(name: 'Test Org 1', path: 'test-org-1', visibility_level: Organizations::Organization::PUBLIC) Organizations::OrganizationUser.create_organization_record_for(User.first.id, org1.id) # Create second organization with owner
org2 = Organizations::Organization.create!(name: 'Test Org 2', path: 'test-org-2', visibility_level: Organizations::Organization::PUBLIC)
Organizations::OrganizationUser.create_organization_record_for(User.first.id, org2.id)
```
- Assign projects to new organizations
- Find two projects that have pipelines with job artifacts:
Ci::JobArtifact.pluck(:project_id).uniq! - Reassign these projects to the newly created organizations, and create sample attestation files for their job artifacts:
# Replace with actual project, organization ids new_project_orgs = [ {project_id: 1, org_id: 1000}, {project_id: 6, org_id: 1001} ] new_project_orgs.each do |v| Project.find(v[:project_id]).namespace.update!(organization: v[:org_id]) Project.find(v[:project_id]).update!(organization: v[:org_id]) end new_project_orgs.map { |v| v[:project_id] }.each do |project_id| Project.find(project_id).job_artifacts.each do |artifact| attestation = SupplyChain::Attestation.new( project_id: project_id, build_id: artifact.job_id, predicate_kind: :provenance, predicate_type: 'https://slsa.dev/provenance/v1', subject_digest: Digest::SHA256.hexdigest(artifact.id.to_s) ) file = Tempfile.new(['attestation', '.json']) begin file.write({sample: (0...1024).map { (65 + rand(26)).chr }.join}.to_json) attestation.file = file attestation.save! ensure file.close file.unlink end end
-
Create a personal access token
- Follow the personal access token documentation
- Make sure to select the
apiandadmin_modescopes - Save the token for use in the API requests
Verify Replication
Click to expand
On primary node:
-
Verify the files were uploaded to object storage in the
uploadsbucket. -
Verify the attestation was created and verification started:
attestation.reload attestation.supply_chain_attestation_state # Should show verification_state as 'pending' or 'verification_succeeded'
On the Secondary Node:
-
Check that the registry entry was created:
Geo::SupplyChainAttestationRegistry.count # Should be > 0 registry = Geo::SupplyChainAttestationRegistry.last registry.supply_chain_attestation_id # Should match the attestation ID from primary -
Trigger replication:
Geo::SupplyChainAttestationReplicator.new(model_record_id: attestation.id).execute -
Verify the attestation file was replicated:
registry.reload registry.state # Should be 'synced' registry.verification_state # Should be 'verification_succeeded' -
Verify the file was replicated in object storage on the secondary.
Primary Site Selective Checksumming by Organizations - Testing Steps
Click to expand
-
In the primary GDK site:
gdk switch 580365-implement-geo-support-of-supply-chain-attestations-behind-ff -
In the secondary GDK site:
gdk switch 580365-implement-geo-support-of-supply-chain-attestations-behind-ff -
In the primary GDK site, open Rails console:
bin/rails c -
Enable the FF:
Feature.enable(:org_mover_extend_selective_sync_to_primary_checksumming) -
Enable the FF:
Feature.enable(:geo_selective_sync_by_organizations) -
Get your current configuration:
# Get your personal access token export PRIVATE_TOKEN="your_personal_access_token" # List all Geo sites to get the site ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites" | jq # Store the site ID of the primary node export SITE_ID=1 # Replace with your primary site ID # Output organization objects for their IDs bin/rails runner "pp Organizations::Organization.all" # Store an organization ID for testing export ORG_ID=1000 # Replace with your organization ID -
Enable selective checksumming by organization:
# Enable selective checksumming by organization and select the specific organization curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "organizations", "selective_sync_organization_ids": ['$ORG_ID'] }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" -
Verify the configuration:
# Get the updated Geo site configuration and confirm that selective_sync_type is "organizations" and # organization_ids contains your organization ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" | jq -
Wait a few minutes and verify the secondary site status
# Get the updated site status and confirm that supply_chain_attestations_checksummed_count # matches the number of supply chain attestations that belong to your organization ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq -
Test with multiple organizations:
export ORG_ID2=1001 # Replace with another organization ID # Update to include multiple organizations curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "organizations", "selective_sync_organization_ids": ['$ORG_ID','$ORG_ID2'] }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Verify the update curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" | jq # Wait a few minutes and verify the Geo site status curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq -
Disable selective sync:
# Reset back to no selective sync curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "" }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Verify the update curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Wait a few minutes and verify the Geo site status curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq # In the primary GDK site, disable the FF: bin/rails runner "pp Feature.disable(:geo_selective_sync_by_organizations)"
Secondary Site Selective Sync by Organizations - Testing Steps
Click to expand
-
In the primary GDK site:
gdk switch 580365-implement-geo-support-of-supply-chain-attestations-behind-ff -
In the secondary GDK site:
gdk switch 580365-implement-geo-support-of-supply-chain-attestations-behind-ff -
In the primary GDK site, open Rails console:
bin/rails c -
Enable the FF:
Feature.enable(:geo_selective_sync_by_organizations) -
Get your current configuration:
# Get your personal access token export PRIVATE_TOKEN="your_personal_access_token" # List all Geo sites to get the site ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites" | jq # Store the site ID of the secondary node export SITE_ID=2 # Replace with your secondary site ID # Output organization objects for their IDs bin/rails runner "pp Organizations::Organization.all" # Store an organization ID for testing export ORG_ID=1000 # Replace with your organization ID -
Enable selective sync by organization:
# Enable selective sync by organization and select the specific organization curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "organizations", "selective_sync_organization_ids": ['$ORG_ID'] }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" -
Verify the configuration:
# Get the updated Geo site configuration and confirm that selective_sync_type is "organizations" and # organization_ids contains your organization ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" | jq -
Wait a few minutes and verify the secondary site status
# Get the updated site status and confirm that supply_chain_attestations_checksummed_count # matches the number of supply chain attestations that belong to your organization ID curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq -
Test with multiple organizations:
export ORG_ID2=1001 # Replace with another organization ID # Update to include multiple organizations curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "organizations", "selective_sync_organization_ids": ['$ORG_ID','$ORG_ID2'] }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Verify the update curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" | jq # Wait a few minutes and verify the Geo site status curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq -
Disable selective sync:
# Reset back to no selective sync curl --request PUT \ --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" \ --header "Content-Type: application/json" \ --data '{ "selective_sync_type": "" }' \ "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Verify the update curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID" # Wait a few minutes and verify the Geo site status curl --header "PRIVATE-TOKEN: $PRIVATE_TOKEN" "http://localhost:3000/api/v4/geo_sites/$SITE_ID/status" | jq # In the primary GDK site, disable the FF: bin/rails runner "pp Feature.disable(:org_mover_extend_selective_sync_to_primary_checksumming)" bin/rails runner "pp Feature.disable(:geo_selective_sync_by_organizations)"
Database Queries
-
SupplyChain::Attestation.replicables_for_current_secondary(1..10000)-
Raw SQL
Click to expand
SELECT "slsa_attestations".* FROM "slsa_attestations" WHERE "slsa_attestations"."id" BETWEEN 1 AND 10000 AND "slsa_attestations"."project_id" IN ( SELECT "projects"."id" FROM "projects" WHERE "projects"."namespace_id" IN ( SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."organization_id" IN ( SELECT "geo_node_organization_links"."organization_id" FROM "geo_node_organization_links" WHERE "geo_node_organization_links"."geo_node_id" = 1 ) ) ) -
Query Plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/47455/commands/143483
-
-
SupplyChain::Attestation.pluck_verifiable_ids_in_range(1..10000)-
Raw SQL
Click to expand
SELECT "slsa_attestations"."id" FROM "slsa_attestations" WHERE "slsa_attestations"."id" BETWEEN 1 AND 10000 AND "slsa_attestations"."project_id" IN ( SELECT "projects"."id" FROM "projects" WHERE "projects"."namespace_id" IN ( SELECT "namespaces"."id" FROM "namespaces" WHERE "namespaces"."organization_id" IN ( SELECT "geo_node_organization_links"."organization_id" FROM "geo_node_organization_links" WHERE "geo_node_organization_links"."geo_node_id" = 1 ) ) ) -
Query Plan: https://console.postgres.ai/gitlab/gitlab-production-main/sessions/47455/commands/143484
-
Related to #580365 (closed)