Skip to content
Snippets Groups Projects
Verified Commit a7129ec4 authored by Dzmitry (Dima) Meshcharakou's avatar Dzmitry (Dima) Meshcharakou Committed by GitLab
Browse files

Get Google Cloud Artifact Registry artifact details

parent 1dcbd93a
No related branches found
No related tags found
1 merge request!145164Get Google Cloud Artifact Registry artifact details
Showing
with 596 additions and 125 deletions
......@@ -58,6 +58,9 @@
"GoogleCloudArtifactRegistryArtifact": [
"GoogleCloudArtifactRegistryDockerImage"
],
"GoogleCloudArtifactRegistryArtifactDetails": [
"GoogleCloudArtifactRegistryDockerImageDetails"
],
"GoogleCloudLoggingConfigurationInterface": [
"GoogleCloudLoggingConfigurationType",
"InstanceGoogleCloudLoggingConfigurationType"
......
......@@ -386,6 +386,26 @@ Whether Gitpod is enabled in application settings.
 
Returns [`Boolean`](#boolean).
 
### `Query.googleCloudArtifactRegistryRepositoryArtifact`
Details about an artifact in the Google Cloud Artifact Registry.
DETAILS:
**Introduced** in GitLab 16.10.
**Status**: Experiment.
Returns [`GoogleCloudArtifactRegistryArtifactDetails`](#googlecloudartifactregistryartifactdetails).
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querygooglecloudartifactregistryrepositoryartifactgooglecloudprojectid"></a>`googleCloudProjectId` | [`String!`](#string) | ID of the Google Cloud project. |
| <a id="querygooglecloudartifactregistryrepositoryartifactimage"></a>`image` | [`String!`](#string) | Name of the image in the Google Cloud Artifact Registry. |
| <a id="querygooglecloudartifactregistryrepositoryartifactlocation"></a>`location` | [`String!`](#string) | Location of the Artifact Registry repository. |
| <a id="querygooglecloudartifactregistryrepositoryartifactprojectpath"></a>`projectPath` | [`ID!`](#id) | Full project path. |
| <a id="querygooglecloudartifactregistryrepositoryartifactrepository"></a>`repository` | [`String!`](#string) | Repository on the Google Cloud Artifact Registry. |
### `Query.group`
 
Find a group.
......@@ -19932,20 +19952,35 @@ Represents a docker artifact of Google Cloud Artifact Registry.
 
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="googlecloudartifactregistrydockerimageartifactregistryimageurl"></a>`artifactRegistryImageUrl` | [`String!`](#string) | Google Cloud URL to access the image. |
| <a id="googlecloudartifactregistrydockerimagebuildtime"></a>`buildTime` | [`Time`](#time) | Time when the image was built. |
| <a id="googlecloudartifactregistrydockerimagedigest"></a>`digest` | [`String!`](#string) | Image's digest. |
| <a id="googlecloudartifactregistrydockerimageimage"></a>`image` | [`String!`](#string) | Image's name. |
| <a id="googlecloudartifactregistrydockerimageimagesizebytes"></a>`imageSizeBytes` | [`String`](#string) | Calculated size of the image. |
| <a id="googlecloudartifactregistrydockerimagelocation"></a>`location` | [`String!`](#string) | Location of the Artifact Registry repository. |
| <a id="googlecloudartifactregistrydockerimagemediatype"></a>`mediaType` | [`String`](#string) | Media type of the image. |
| <a id="googlecloudartifactregistrydockerimagename"></a>`name` | [`String!`](#string) | Unique image name. |
| <a id="googlecloudartifactregistrydockerimageprojectid"></a>`projectId` | [`String!`](#string) | ID of the Google Cloud project. |
| <a id="googlecloudartifactregistrydockerimagerepository"></a>`repository` | [`String!`](#string) | Repository on the Google Cloud Artifact Registry. |
| <a id="googlecloudartifactregistrydockerimagetags"></a>`tags` | [`[String!]`](#string) | Tags attached to the image. |
| <a id="googlecloudartifactregistrydockerimageupdatetime"></a>`updateTime` | [`Time`](#time) | Time when the image was last updated. |
| <a id="googlecloudartifactregistrydockerimageuploadtime"></a>`uploadTime` | [`Time`](#time) | Time when the image was uploaded. |
| <a id="googlecloudartifactregistrydockerimageuri"></a>`uri` | [`String!`](#string) | Google Cloud URI to access the image. |
### `GoogleCloudArtifactRegistryDockerImageDetails`
Represents details about docker artifact of Google Cloud Artifact Registry.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="googlecloudartifactregistrydockerimagedetailsartifactregistryimageurl"></a>`artifactRegistryImageUrl` | [`String!`](#string) | Google Cloud URL to access the image. |
| <a id="googlecloudartifactregistrydockerimagedetailsbuildtime"></a>`buildTime` | [`Time`](#time) | Time when the image was built. |
| <a id="googlecloudartifactregistrydockerimagedetailsdigest"></a>`digest` | [`String!`](#string) | Image's digest. |
| <a id="googlecloudartifactregistrydockerimagedetailsimage"></a>`image` | [`String!`](#string) | Image's name. |
| <a id="googlecloudartifactregistrydockerimagedetailsimagesizebytes"></a>`imageSizeBytes` | [`String`](#string) | Calculated size of the image. |
| <a id="googlecloudartifactregistrydockerimagedetailslocation"></a>`location` | [`String!`](#string) | Location of the Artifact Registry repository. |
| <a id="googlecloudartifactregistrydockerimagedetailsmediatype"></a>`mediaType` | [`String`](#string) | Media type of the image. |
| <a id="googlecloudartifactregistrydockerimagedetailsname"></a>`name` | [`String!`](#string) | Unique image name. |
| <a id="googlecloudartifactregistrydockerimagedetailsprojectid"></a>`projectId` | [`String!`](#string) | ID of the Google Cloud project. |
| <a id="googlecloudartifactregistrydockerimagedetailsrepository"></a>`repository` | [`String!`](#string) | Repository on the Google Cloud Artifact Registry. |
| <a id="googlecloudartifactregistrydockerimagedetailstags"></a>`tags` | [`[String!]`](#string) | Tags attached to the image. |
| <a id="googlecloudartifactregistrydockerimagedetailsupdatetime"></a>`updateTime` | [`Time`](#time) | Time when the image was last updated. |
| <a id="googlecloudartifactregistrydockerimagedetailsuploadtime"></a>`uploadTime` | [`Time`](#time) | Time when the image was uploaded. |
| <a id="googlecloudartifactregistrydockerimagedetailsuri"></a>`uri` | [`String!`](#string) | Google Cloud URI to access the image. |
 
### `GoogleCloudArtifactRegistryRepository`
 
......@@ -34270,6 +34305,14 @@ One of:
 
- [`GoogleCloudArtifactRegistryDockerImage`](#googlecloudartifactregistrydockerimage)
 
#### `GoogleCloudArtifactRegistryArtifactDetails`
Details type of Google Cloud Artifact Registry artifacts.
One of:
- [`GoogleCloudArtifactRegistryDockerImageDetails`](#googlecloudartifactregistrydockerimagedetails)
#### `Issuable`
 
Represents an issuable.
......@@ -157,6 +157,12 @@ module QueryType
null: true, description: 'Member roles available for the instance.',
resolver: ::Resolvers::MemberRoles::RolesResolver,
alpha: { milestone: '16.7' }
field :google_cloud_artifact_registry_repository_artifact,
::Types::GoogleCloud::ArtifactRegistry::ArtifactDetailsType,
null: true,
description: 'Details about an artifact in the Google Cloud Artifact Registry.',
resolver: ::Resolvers::GoogleCloud::ArtifactRegistry::ArtifactResolver,
alpha: { milestone: '16.10' }
end
def vulnerability(id:)
......
# frozen_string_literal: true
module Resolvers
module GoogleCloud
module ArtifactRegistry
class ArtifactResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type ::Types::GoogleCloud::ArtifactRegistry::ArtifactDetailsType, null: true
authorize :read_container_image
argument :google_cloud_project_id,
GraphQL::Types::String,
required: true,
description: 'ID of the Google Cloud project.'
argument :location,
GraphQL::Types::String,
required: true,
description: 'Location of the Artifact Registry repository.'
argument :repository,
GraphQL::Types::String,
required: true,
description: 'Repository on the Google Cloud Artifact Registry.'
argument :image,
GraphQL::Types::String,
required: true,
description: "Name of the image in the Google Cloud Artifact Registry."
argument :project_path,
GraphQL::Types::ID,
required: true,
description: 'Full project path.'
def ready?(google_cloud_project_id:, location:, repository:, image:, project_path:)
project_integration = find_project_integration!(project_path)
validate_on_integration(
project_integration,
field: :artifact_registry_project_id,
value: google_cloud_project_id,
argument: :googleCloudProjectId,
field_title: s_('GoogleCloudPlatformService|Google Cloud project ID')
)
validate_on_integration(
project_integration,
field: :artifact_registry_location,
value: location,
argument: :location,
field_title: s_('GoogleCloudPlatformService|Repository location')
)
validate_on_integration(
project_integration,
field: :artifact_registry_repository,
value: repository,
argument: :repository,
field_title: s_('GoogleCloudPlatformService|Repository name')
)
super
end
def resolve(google_cloud_project_id:, location:, repository:, image:, project_path:)
name = "projects/#{google_cloud_project_id}/locations/#{location}/repositories/#{repository}/" \
"dockerImages/#{image}"
response = ::GoogleCloudPlatform::ArtifactRegistry::GetDockerImageService.new(
current_user: current_user,
project: find_project!(project_path),
params: {
name: name
}
).execute
raise_resource_not_available_error!(response.message) unless response.success?
response.payload
end
private
def find_project!(project_path)
strong_memoize_with(:find_project, project_path) do
authorized_find!(project_path)
end
end
def find_project_integration!(project_path)
project = find_project!(project_path)
project_integration = project.google_cloud_platform_artifact_registry_integration
unless project_integration
message =
::GoogleCloudPlatform::ArtifactRegistry::GetDockerImageService::ERROR_RESPONSES[:no_project_integration]
.message
raise_resource_not_available_error!(message)
end
project_integration
end
override :find_object
def find_object(full_path)
Project.find_by_full_path(full_path)
end
def validate_on_integration(project_integration, field:, value:, argument:, field_title:)
return if value == project_integration.public_send(field) # rubocop:disable GitlabSecurity/PublicSend -- The `field` argument is considered safe
raise_argument_error!(
argument_error_message(
argument,
title: field_title,
integration_title: project_integration.title
)
)
end
def argument_error_message(argument, title:, integration_title:)
"`#{argument}` doesn't match #{title} of #{integration_title} integration"
end
def raise_argument_error!(message)
raise Gitlab::Graphql::Errors::ArgumentError, message
end
end
end
end
end
# frozen_string_literal: true
module Types
module GoogleCloud
module ArtifactRegistry
class ArtifactDetailsType < BaseUnion
graphql_name 'GoogleCloudArtifactRegistryArtifactDetails'
description 'Details type of Google Cloud Artifact Registry artifacts'
possible_types ::Types::GoogleCloud::ArtifactRegistry::DockerImageDetailsType
def self.resolve_type(object, _context)
case object
when Google::Cloud::ArtifactRegistry::V1::DockerImage
::Types::GoogleCloud::ArtifactRegistry::DockerImageDetailsType
else
raise ::Gitlab::Graphql::Errors::BaseError,
"Unsupported Google Cloud Artifact Registry type #{object.class.name}"
end
end
end
end
end
end
# frozen_string_literal: true
module Types
module GoogleCloud
module ArtifactRegistry
# rubocop:disable Graphql/AuthorizeTypes -- authorization happens in the service, called from the resolver
class DockerImageDetailsType < DockerImageType
graphql_name 'GoogleCloudArtifactRegistryDockerImageDetails'
description 'Represents details about docker artifact of Google Cloud Artifact Registry'
field :uri,
GraphQL::Types::String,
null: false,
description: 'Google Cloud URI to access the image.'
field :image_size_bytes,
GraphQL::Types::String,
description: 'Calculated size of the image.'
field :build_time,
Types::TimeType,
description: 'Time when the image was built.'
field :media_type,
GraphQL::Types::String,
description: 'Media type of the image.'
field :project_id,
GraphQL::Types::String,
null: false,
description: 'ID of the Google Cloud project.'
field :location,
GraphQL::Types::String,
null: false,
description: 'Location of the Artifact Registry repository.'
field :repository,
GraphQL::Types::String,
null: false,
description: 'Repository on the Google Cloud Artifact Registry.'
field :artifact_registry_image_url,
GraphQL::Types::String,
null: false,
description: 'Google Cloud URL to access the image.'
def build_time
return unless artifact.build_time
Time.at(artifact.build_time.seconds)
end
def artifact_registry_image_url
"https://#{artifact.uri}"
end
def project_id
image_name_data[:project_id]
end
def location
image_name_data[:location]
end
def repository
image_name_data[:repository]
end
end
# rubocop:enable Graphql/AuthorizeTypes
end
end
end
......@@ -29,50 +29,18 @@ class DockerImageType < BaseObject
null: false,
description: 'Unique image name.'
field :uri,
GraphQL::Types::String,
null: false,
description: 'Google Cloud URI to access the image.'
field :tags,
[GraphQL::Types::String],
description: 'Tags attached to the image.'
field :image_size_bytes,
GraphQL::Types::String,
description: 'Calculated size of the image.'
field :upload_time,
Types::TimeType,
description: 'Time when the image was uploaded.'
field :media_type,
GraphQL::Types::String,
description: 'Media type of the image.'
field :build_time,
Types::TimeType,
description: 'Time when the image was built.'
field :update_time,
Types::TimeType,
description: 'Time when the image was last updated.'
field :project_id,
GraphQL::Types::String,
null: false,
description: 'ID of the Google Cloud project.'
field :location,
GraphQL::Types::String,
null: false,
description: 'Location of the Artifact Registry repository.'
field :repository,
GraphQL::Types::String,
null: false,
description: 'Repository on the Google Cloud Artifact Registry.'
field :image,
GraphQL::Types::String,
null: false,
......@@ -83,33 +51,18 @@ class DockerImageType < BaseObject
null: false,
description: "Image's digest."
field :artifact_registry_image_url,
GraphQL::Types::String,
null: false,
description: 'Google Cloud URL to access the image.'
def upload_time
return unless artifact.upload_time
Time.at(artifact.upload_time.seconds)
end
def build_time
return unless artifact.build_time
Time.at(artifact.build_time.seconds)
end
def update_time
return unless artifact.update_time
Time.at(artifact.update_time.seconds)
end
def artifact_registry_image_url
"https://#{artifact.uri}"
end
def image
image_name_data[:image]
end
......@@ -118,18 +71,6 @@ def digest
image_name_data[:digest]
end
def project_id
image_name_data[:project_id]
end
def location
image_name_data[:location]
end
def repository
image_name_data[:repository]
end
private
def image_name_data
......
# frozen_string_literal: true
require 'spec_helper'
require 'google/cloud/artifact_registry/v1'
RSpec.describe GitlabSchema.types['GoogleCloudArtifactRegistryArtifactDetails'], feature_category: :container_registry do
describe '.resolve_type' do
let(:object) { Google::Cloud::ArtifactRegistry::V1::DockerImage.new(name: 'alpine') }
subject(:mapping) { described_class.resolve_type(object, {}) }
it { is_expected.to eq(::Types::GoogleCloud::ArtifactRegistry::DockerImageDetailsType) }
context 'with an unknown type' do
let(:object) { {} }
it 'raises the error' do
expect do
mapping
end.to raise_error(Gitlab::Graphql::Errors::BaseError, 'Unsupported Google Cloud Artifact Registry type Hash')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['GoogleCloudArtifactRegistryDockerImageDetails'], feature_category: :container_registry do
specify do
expect(described_class.description)
.to eq('Represents details about docker artifact of Google Cloud Artifact Registry')
end
it 'includes all expected fields' do
expected_fields = %w[
name uri tags image_size_bytes upload_time
media_type build_time update_time project_id
location repository image digest artifact_registry_image_url
]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
......@@ -8,11 +8,7 @@
end
it 'includes all expected fields' do
expected_fields = %w[
name uri tags image_size_bytes upload_time
media_type build_time update_time project_id
location repository image digest artifact_registry_image_url
]
expected_fields = %w[name tags upload_time update_time image digest]
expect(described_class).to include_graphql_fields(*expected_fields)
end
......
......@@ -37,7 +37,8 @@
:audit_events_instance_amazon_s3_configurations,
:member_role,
:self_managed_add_on_eligible_users,
:member_roles
:member_roles,
:google_cloud_artifact_registry_repository_artifact
]
all_expected_fields = expected_foss_fields + expected_ee_fields
......
# frozen_string_literal: true
require 'spec_helper'
require 'google/cloud/artifact_registry/v1'
RSpec.describe 'getting the google cloud docker image linked to a project', :freeze_time, feature_category: :container_registry do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:project_integration) do
create(
:google_cloud_platform_artifact_registry_integration,
project: project,
artifact_registry_repositories: 'demo'
)
end
let_it_be(:user) { project.first_owner }
let(:location) { project_integration.artifact_registry_location }
let(:google_cloud_project_id) { project_integration.artifact_registry_project_id }
let(:repository) { project_integration.artifact_registry_repository }
let(:image) { 'ruby' }
let(:digest) { 'sha256:4ca5c21b' }
let(:client_double) { instance_double('::GoogleCloudPlatform::ArtifactRegistry::Client') }
let(:uri) do
"#{location}-docker.pkg.dev/#{google_cloud_project_id}/" \
"#{project_integration.artifact_registry_repository}/#{image}@#{digest}"
end
let(:name) do
"projects/#{google_cloud_project_id}/" \
"locations/#{location}/" \
"repositories/#{repository}/" \
"dockerImages/#{image}@#{digest}"
end
let(:docker_image) do
Google::Cloud::ArtifactRegistry::V1::DockerImage.new(
name: name,
uri: uri,
tags: ['97c58898'],
image_size_bytes: 304_121_628,
media_type: 'application/vnd.docker.distribution.manifest.v2+json',
build_time: Time.now,
update_time: Time.now,
upload_time: Time.now
)
end
let(:fields) do
<<~QUERY
#{query_graphql_fragment('GoogleCloudArtifactRegistryDockerImageDetails')}
QUERY
end
let(:params) do
{
google_cloud_project_id: google_cloud_project_id,
location: location,
repository: repository,
image: "#{image}@#{digest}",
projectPath: project.full_path
}
end
let(:query) do
graphql_query_for(
'googleCloudArtifactRegistryRepositoryArtifact', params, fields
)
end
let(:artifact_response) do
graphql_data_at(:google_cloud_artifact_registry_repository_artifact)
end
subject(:request) { post_graphql(query, current_user: user) }
before do
stub_saas_features(google_cloud_support: true)
allow(::GoogleCloudPlatform::ArtifactRegistry::Client).to receive(:new)
.with(
project_integration: project_integration,
user: user,
artifact_registry_location: location,
artifact_registry_repository: repository
).and_return(client_double)
allow(client_double).to receive(:docker_image).with(name: name).and_return(docker_image)
end
shared_examples 'returning the expected response' do
it 'returns the proper response' do
request
expect(artifact_response).to eq({
'name' => docker_image.name,
'uri' => docker_image.uri,
'tags' => docker_image.tags,
'imageSizeBytes' => docker_image.image_size_bytes.to_s,
'mediaType' => docker_image.media_type,
'buildTime' => Time.now.iso8601,
'updateTime' => Time.now.iso8601,
'uploadTime' => Time.now.iso8601,
'projectId' => google_cloud_project_id,
'location' => location,
'repository' => repository,
'image' => image,
'digest' => digest,
'artifactRegistryImageUrl' => "https://#{uri}"
})
end
end
shared_examples 'returning a blank response' do
it 'returns a blank response' do
subject
expect(artifact_response).to be_blank
end
end
it_behaves_like 'a working graphql query' do
before do
request
end
end
it 'matches the JSON schema' do
request
expect(artifact_response).to match_schema('graphql/google_cloud/artifact_registry/docker_image_details')
end
it_behaves_like 'returning the expected response'
context 'when an user does not have required permissions' do
let(:user) { create(:user).tap { |user| project.add_guest(user) } }
it_behaves_like 'returning a blank response'
end
context 'when google artifact registry feature is unavailable' do
before do
stub_saas_features(google_cloud_support: false)
end
it_behaves_like 'returning a blank response'
end
context 'when gcp_artifact_registry FF is disabled' do
before do
stub_feature_flags(gcp_artifact_registry: false)
end
it_behaves_like 'returning a blank response'
end
context 'when Google Cloud Artifact Registry integration is not present' do
before do
project_integration.destroy!
end
it_behaves_like 'returning a blank response'
end
context 'when Google Cloud Artifact Registry integration is inactive' do
before do
project_integration.update_column(:active, false)
end
it_behaves_like 'returning a blank response'
end
context 'with invalid arguments' do
using RSpec::Parameterized::TableSyntax
# rubocop:disable Layout/LineLength -- The table rows are more readable without line breaks
where(:argument, :error_message) do
:location | "`location` doesn't match Repository location of Google Cloud Artifact Registry"
:repository | "`repository` doesn't match Repository name of Google Cloud Artifact Registry"
:google_cloud_project_id | "`googleCloudProjectId` doesn't match Google Cloud project ID of Google Cloud Artifact Registry"
end
# rubocop:enable Layout/LineLength
with_them do
let(params[:argument]) { 'invalid' }
before do
request
end
it 'returns the error' do
expect_graphql_errors_to_include(error_message)
end
end
end
end
......@@ -110,19 +110,11 @@
'artifacts' => {
'nodes' => [{
'name' => docker_image.name,
'uri' => docker_image.uri,
'tags' => docker_image.tags,
'imageSizeBytes' => docker_image.image_size_bytes.to_s,
'mediaType' => docker_image.media_type,
'buildTime' => Time.now.iso8601,
'updateTime' => Time.now.iso8601,
'uploadTime' => Time.now.iso8601,
'projectId' => project_integration.artifact_registry_project_id,
'location' => project_integration.artifact_registry_location,
'repository' => project_integration.artifact_registry_repository,
'updateTime' => Time.now.iso8601,
'image' => image,
'digest' => digest,
'artifactRegistryImageUrl' => "https://#{docker_image.uri}"
'digest' => digest
}],
'pageInfo' => {
'endCursor' => end_cursor,
......
......@@ -2,80 +2,39 @@
"type": "object",
"required": [
"name",
"uri",
"tags",
"imageSizeBytes",
"uploadTime",
"mediaType",
"buildTime",
"updateTime",
"projectId",
"location",
"repository",
"image",
"digest",
"artifactRegistryImageUrl"
"digest"
],
"properties": {
"name": {
"type": "string"
},
"uri": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"imageSizeBytes": {
"type": [
"string",
"null"
]
},
"uploadTime": {
"type": [
"string",
"null"
]
},
"mediaType": {
"type": [
"string",
"null"
]
},
"buildTime": {
"type": [
"string",
"null"
]
},
"updateTime": {
"type": [
"string",
"null"
]
},
"projectId": {
"type": "string"
},
"location": {
"type": "string"
},
"repository": {
"type": "string"
},
"image": {
"type": "string"
},
"digest": {
"type": "string"
},
"artifactRegistryUrl": {
"type": "string"
}
}
}
{
"type": "object",
"allOf": [
{
"$ref": "./docker_image.json"
}
],
"required": [
"uri",
"imageSizeBytes",
"buildTime",
"mediaType",
"projectId",
"location",
"repository",
"artifactRegistryImageUrl"
],
"properties": {
"uri": {
"type": "string"
},
"imageSizeBytes": {
"type": [
"string",
"null"
]
},
"buildTime": {
"type": [
"string",
"null"
]
},
"mediaType": {
"type": [
"string",
"null"
]
},
"projectId": {
"type": "string"
},
"location": {
"type": "string"
},
"repository": {
"type": "string"
},
"artifactRegistryUrl": {
"type": "string"
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment