Add artifact registry client foundation and repository fetch

What does this MR do and why?

Add artifact registry client foundation and repository fetch

This is step 1 in the plan: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/monolith/2026-07-02-ar-ruby-client.md#step-1-client-foundation-and-repository-fetch

References

Screenshots or screen recordings

Before After

How to set up and validate locally

You'll need to ensure you have setup the caproni Artifact Registry dev rig.

To test the AR client, if you don't have the rails monolith edit mode setup, then you can run the rails console via the gdk, the default for the base url is currently localhost:8080, so the validation steps will work.

To test via caproni, setup the monolith edit mode with ./gitlab checked out to 605077-ar-ruby-client-step-1.

Update the config/gitlab.yml file under development with the below

artifact_registry:
    api_url: http://artifact-registry.artifact-registry.svc.cluster.local:8080
  1. Bring up the caproni AR rig: cd to the artifact-registry dir and run caproni up.
  2. Run caproni update to get the latest AR, then caproni run (with the monolith in edit mode on this branch).
  3. Check all pods are running: ar_pods and gl_pods.
  4. Seed a repository (namespace gitlab-org, repo test) from the AR rig dir:
    scripts/seed-namespace.sh
  5. Pre-flight the backend directly from your host, which isolates caproni issues from Rails:
    curl -s -H "Authorization: Bearer local-dev-not-a-secret" \
      http://localhost:8080/api/v1/gitlab-org/repositories/test | jq

Expected:

gitlab git:(605077-ar-ruby-client-step-1) curl -s -H "Authorization: Bearer local-dev-not-a-secret" \
  http://localhost:8080/api/v1/gitlab-org/repositories/test | jq
{
  "id": "f1455977-160a-4d86-93ab-5d7df2c84774",
  "name": "test",
  "format": "container",
  "kind": "hosted",
  "visibility": "public",
  "description": null,
  "artifacts_count": 0,
  "downloads_count": 0,
  "size_bytes": 0,
  "created_at": "2026-07-13T04:29:13Z",
  "last_updated_at": null,
  "created_by": null,
  "updated_by": null
}
  1. Open a Rails console in the edit-mode monolith. From the rig's gitlab/ directory:
    .gitlab/caproni/exec.sh bin/rails console

OR

if running from the gdk, launch the console gdk rails console

  1. Call through the client. The token is injected via a stand-in TokenExchange:
    token  = 'local-dev-not-a-secret'
    
    token_exchange = Class.new(ArtifactRegistry::TokenExchange) do
      define_method(:token_for) { |_user, _slug| token }
    end.new
    
    client = ArtifactRegistry::Client.new(current_user: nil, token_exchange: token_exchange)
    
    repo = client.repository(slug: 'gitlab-org', name: 'test')
    [repo.class, repo&.name, repo&.format, repo&.kind, repo&.visibility]
    # => [ArtifactRegistry::Repository, "test", "container", "hosted", "public"]
    
    client.repository(slug: 'gitlab-org', name: 'nope') # => nil (404 path)

Expected:

--------------------------------------------------------------------------------
 Ruby:         ruby 3.3.11 (2026-03-26 revision 1f2d15125a) [arm64-darwin25]
 GitLab:       19.2.0-pre (40098566a17) EE
 GitLab Shell: Unknown
 PostgreSQL:   17.8
--------------------------------------------------------------------------------
Loading development environment (Rails 7.2.3.1)
[1] pry(main)> token  = 'local-dev-not-a-secret'
=> "local-dev-not-a-secret"
[2] pry(main)> token_exchange = Class.new(ArtifactRegistry::TokenExchange) do
[2] pry(main)> token_exchange = Class.new(ArtifactRegistry::TokenExchange) do
  define_method(:token_for) { |_user, _slug| token }
=> #<#<Class:0x000000014b6cb230>:0x000000014932a2e0>
[3] pry(main)> client = ArtifactRegistry::Client.new(current_user: nil, token_exchange: token_exchange)
=> #<ArtifactRegistry::Client:0x000000014a365c90
 @base_url=nil,
 @current_user=nil,
 @token_exchange=#<#<Class:0x000000014b6cb230>:0x000000014932a2e0>>
[4] pry(main)> repo = client.repository(slug: 'gitlab-org', name: 'test')
=> #<ArtifactRegistry::Repository:0x000000014a1e93d0
 @attributes=
  {"id"=>"f1455977-160a-4d86-93ab-5d7df2c84774",
   "name"=>"test",
   "format"=>"container",
   "kind"=>"hosted",
   "visibility"=>"public",
   "description"=>nil,
   "artifacts_count"=>0,
   "downloads_count"=>0,
   "size_bytes"=>0,
   "created_at"=>"2026-07-13T04:29:13Z",
   "last_updated_at"=>nil,
   "created_by"=>nil,
   "updated_by"=>nil}>
[5] pry(main)> [repo.class, repo&.name, repo&.format, repo&.kind, repo&.visibility]
=> [ArtifactRegistry::Repository, "test", "container", "hosted", "public"]
[6] pry(main)> client.repository(slug: 'gitlab-org', name: 'nope')
=> nil
  1. Negative auth check. The default TokenExchange returns nil, so a client with no injected exchange raises before any request is made:
    ArtifactRegistry::Client.new(current_user: nil).repository(slug: 'gitlab-org', name: 'test')
    # => raises ArtifactRegistry::Client::AuthorizationError

Expected:

[8] pry(main)> ArtifactRegistry::Client.new(current_user: nil).repository(slug: 'gitlab-org', name: 'test')
ArtifactRegistry::Client::AuthorizationError: No Artifact Registry credential was obtained for the current user
from /Users/fionamccawley/GitLab/caproni-demo/artifact-registry/gitlab/ee/lib/artifact_registry/client.rb:107:in `raise_missing_credential'
[9] pry(main)>

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Related to #605077 (closed)

Edited by Fiona McCawley

Merge request reports

Loading
Loading