Skip to content

Commit

Permalink
refactor oci image processing to store media types
Browse files Browse the repository at this point in the history
  • Loading branch information
ezekg committed Nov 14, 2024
1 parent 19c6359 commit c673614
Show file tree
Hide file tree
Showing 27 changed files with 629 additions and 149 deletions.
5 changes: 4 additions & 1 deletion app/controllers/api/v1/release_artifacts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ def show
return render jsonapi: artifact if
!artifact.downloadable? || prefers?('no-download')

download = artifact.download!(ttl: release_artifact_query[:ttl])
download = artifact.download!(
filename: params.fetch(:filename) { artifact.filename },
ttl: release_artifact_query[:ttl],
)

BroadcastEventService.call(
# NOTE(ezekg) The `release.downloaded` event is for backwards compat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ class Npm::PackageMetadataController < Api::V1::BaseController
before_action :set_package

def show
authorize! package,
to: :show?
authorize! package

artifacts = authorized_scope(package.artifacts.tarballs).order_by_version
.where_assoc_exists(:manifest) # must exist
Expand Down
28 changes: 16 additions & 12 deletions app/controllers/api/v1/release_engines/oci/blobs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,32 @@ class Oci::BlobsController < Api::V1::BaseController
before_action :scope_to_current_account!
before_action :require_active_subscription!
before_action :authenticate_with_token
before_action :set_artifact
before_action :set_package

def show
authorize! artifact
authorize! package

redirect_to vanity_v1_account_release_artifact_url(artifact.account, artifact, filename: artifact.filename, host: request.host),
descriptor = package.descriptors.find_by!(content_digest: params[:digest])
authorize! descriptor.artifact

redirect_to vanity_v1_account_release_artifact_url(current_account, descriptor.artifact, filename: descriptor.content_path, host: request.host),
status: :see_other
end

private

attr_reader :artifact

def set_artifact
scoped_artifacts = authorized_scope(current_account.release_artifacts.blobs)
.for_package(params[:namespace])
attr_reader :package

# see: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests
algorithm, encoded = params[:digest].split(':', 2)
def set_package
scoped_packages = authorized_scope(current_account.release_packages.oci)
.where_assoc_exists(
%i[releases artifacts manifest], # must exist
)

Current.resource = @artifact = scoped_artifacts.find_by!(
filename: "blobs/#{algorithm}/#{encoded}",
@package = Current.resource = FindByAliasService.call(
scoped_packages,
id: params[:package],
aliases: :key,
)
end
end
Expand Down
30 changes: 19 additions & 11 deletions app/controllers/api/v1/release_engines/oci/manifests_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,39 @@ class Oci::ManifestsController < Api::V1::BaseController
before_action :scope_to_current_account!
before_action :require_active_subscription!
before_action :authenticate_with_token
before_action :set_artifact
before_action :set_package

def show
authorize! artifact,
to: :show?
authorize! package

manifest = artifact.manifest
manifest = package.manifests.find_by_reference!(params[:reference])
authorize! manifest.artifact

# for etag support
return unless
stale?(manifest, cache_control: { max_age: 1.day, private: true })

# docker is very particular about content types
response.content_type = manifest.content_type

render body: manifest.content
end

private

attr_reader :artifact
attr_reader :package

def set_package
scoped_packages = authorized_scope(current_account.release_packages.oci)
.where_assoc_exists(
%i[releases artifacts manifest], # must exist
)

def set_artifact
Current.resource = @artifact = authorized_scope(current_account.release_artifacts.tarballs)
.for_package(params[:namespace])
.for_release(params[:reference])
.order_by_version
.first!
@package = Current.resource = FindByAliasService.call(
scoped_packages,
id: params[:package],
aliases: :key,
)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ def show

def set_artifact
scoped_artifacts = authorized_scope(current_account.release_artifacts)
.for_product(params[:product_id])
.for_package(params[:package_id])
.for_release(params[:release_id])
.for_product(params[:product])
.for_package(params[:package])
.for_release(params[:release])

@artifact = Current.resource = FindByAliasService.call(
scoped_artifacts.order_by_version,
id: params[:id],
id: params[:artifact],
aliases: :filename,
reorder: false,
)
Expand Down
18 changes: 18 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def render_forbidden(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :forbidden, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -111,6 +113,8 @@ def render_unauthorized(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :unauthorized, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -134,6 +138,8 @@ def render_unprocessable_entity(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :unprocessable_entity, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -157,6 +163,8 @@ def render_not_found(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :not_found, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -181,6 +189,8 @@ def render_bad_request(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :bad_request, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -204,6 +214,8 @@ def render_conflict(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :conflict, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -227,6 +239,8 @@ def render_payment_required(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :payment_required, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -250,6 +264,8 @@ def render_internal_server_error(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :internal_server_error, json: {
meta: { id: request.request_id },
errors: [{
Expand All @@ -273,6 +289,8 @@ def render_service_unavailable(**kwargs)

respond_to do |format|
format.any {
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)

render status: :service_unavailable, json: {
meta: { id: request.request_id },
errors: [{
Expand Down
21 changes: 14 additions & 7 deletions app/controllers/concerns/rendering.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ module Base
include ActionController::MimeResponds

# overload render method to automatically set content type
def render(args, ...)
case args
in jsonapi:
def render(options, ...)
mime_type, * = Mime::Type.parse(response.content_type.to_s) rescue nil

# skip if we've already set content type
unless mime_type.nil?
return super
end

case options
in jsonapi: _
response.content_type = Mime::Type.lookup_by_extension(:jsonapi)
in json:
in json: _
response.content_type = Mime::Type.lookup_by_extension(:json)
in body:
in body: _
response.content_type = Mime::Type.lookup_by_extension(:binary)
in html:
in html: _
response.content_type = Mime::Type.lookup_by_extension(:html)
in gz:
in gz: _
response.content_type = Mime::Type.lookup_by_extension(:gzip)
else
# leave as-is
Expand Down
1 change: 1 addition & 0 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Account < ApplicationRecord
has_many :release_engines, dependent: :destroy_async
has_many :release_packages, dependent: :destroy_async
has_many :release_manifests, dependent: :destroy_async
has_many :release_descriptors, dependent: :destroy_async
has_many :release_platforms, dependent: :destroy_async
has_many :release_arches, dependent: :destroy_async
has_many :release_filetypes, dependent: :destroy_async
Expand Down
33 changes: 17 additions & 16 deletions app/models/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ def owned = where(bearer: proxy_association.owner)
end

# TODO(ezekg) Should deleting queue up a cancelable background job?
has_many :webhook_endpoints, dependent: :destroy_async
has_many :webhook_events, dependent: :destroy_async
has_many :entitlements, dependent: :destroy_async
has_many :groups, dependent: :destroy_async
has_many :products, dependent: :destroy_async
has_many :policies, dependent: :destroy_async
has_many :licenses, dependent: :destroy_async
has_many :license_users, dependent: :destroy_async
has_many :machines, dependent: :destroy_async
has_many :machine_processes, dependent: :destroy_async
has_many :machine_components, dependent: :destroy_async
has_many :users, dependent: :destroy_async
has_many :releases, dependent: :destroy_async
has_many :release_artifacts, dependent: :destroy_async
has_many :release_manifests, dependent: :destroy_async
has_many :release_packages, dependent: :destroy_async
has_many :webhook_endpoints, dependent: :destroy_async
has_many :webhook_events, dependent: :destroy_async
has_many :entitlements, dependent: :destroy_async
has_many :groups, dependent: :destroy_async
has_many :products, dependent: :destroy_async
has_many :policies, dependent: :destroy_async
has_many :licenses, dependent: :destroy_async
has_many :license_users, dependent: :destroy_async
has_many :machines, dependent: :destroy_async
has_many :machine_processes, dependent: :destroy_async
has_many :machine_components, dependent: :destroy_async
has_many :users, dependent: :destroy_async
has_many :releases, dependent: :destroy_async
has_many :release_artifacts, dependent: :destroy_async
has_many :release_manifests, dependent: :destroy_async
has_many :release_packages, dependent: :destroy_async
has_many :release_descriptors, dependent: :destroy_async

has_account
has_role :environment
Expand Down
8 changes: 6 additions & 2 deletions app/models/release.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,16 @@ class Release < ApplicationRecord
class_name: 'ReleaseUploadLink',
inverse_of: :release,
dependent: :delete_all
has_many :artifacts,
class_name: 'ReleaseArtifact',
inverse_of: :release,
dependent: :delete_all
has_many :manifests,
class_name: 'ReleaseManifest',
inverse_of: :release,
dependent: :delete_all
has_many :artifacts,
class_name: 'ReleaseArtifact',
has_many :descriptors,
class_name: 'ReleaseDescriptor',
inverse_of: :release,
dependent: :delete_all
has_many :filetypes,
Expand Down
15 changes: 10 additions & 5 deletions app/models/release_artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ class ReleaseArtifact < ApplicationRecord
foreign_key: :release_artifact_id,
inverse_of: :artifact,
dependent: :delete
has_many :descriptors,
class_name: 'ReleaseDescriptor',
foreign_key: :release_artifact_id,
inverse_of: :artifact,
dependent: :delete_all
has_one :channel,
through: :release
has_one :product,
Expand Down Expand Up @@ -452,16 +457,16 @@ class ReleaseArtifact < ApplicationRecord

scope :gems, -> { for_filetype(:gem) }
scope :tarballs, -> { for_filetypes(:tgz, :tar, :'tar.gz') }
scope :blobs, -> { where("release_artifacts.filename LIKE 'blobs/%'") } # prefixes are fast

delegate :version, :semver, :channel, :tag,
:tag?, :licensed?, :open?, :closed?,
allow_nil: true,
to: :release

def key = "artifacts/#{account_id}/#{release_id}/#{filename}"
def presigner = Aws::S3::Presigner.new(client:)
def key_for(path) = "artifacts/#{account_id}/#{release_id}/#{path}"
def key = key_for(filename)

def presigner = Aws::S3::Presigner.new(client:)
def client
case backend
when 'S3'
Expand Down Expand Up @@ -512,8 +517,8 @@ def redirect_url?
redirect_url.present?
end

def download!(ttl: 1.hour)
self.redirect_url = presigner.presigned_url(:get_object, bucket:, key:, expires_in: ttl&.to_i)
def download!(filename: self.filename, ttl: 1.hour)
self.redirect_url = presigner.presigned_url(:get_object, bucket:, key: key_for(filename), expires_in: ttl&.to_i)

release.download_links.create!(url: redirect_url, ttl:, account:)
end
Expand Down
35 changes: 35 additions & 0 deletions app/models/release_descriptor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

class ReleaseDescriptor < ApplicationRecord
include Keygen::PortableClass
include Environmental
include Accountable

belongs_to :artifact,
class_name: 'ReleaseArtifact',
foreign_key: :release_artifact_id,
inverse_of: :descriptors
belongs_to :release,
inverse_of: :descriptors
has_one :product,
through: :release
has_one :package,
through: :release
has_one :engine,
through: :package

has_environment default: -> { artifact&.environment_id }
has_account default: -> { artifact&.account_id }

validates :artifact,
scope: { by: :account_id }

validates :release,
scope: { by: :account_id }

def client = artifact.client
def bucket = artifact.bucket
def key = artifact.key_for(content_path)

def download!(**) = artifact.download!(**, filename: content_path)
end
Loading

0 comments on commit c673614

Please sign in to comment.