warp_engine 0.2.0: pluggable storage adapter and publish notifications
Two seams the hosts needed, both backward compatible.
Storage: artifacts are served through WarpEngine::Storage.adapter instead of
raw filesystem calls. The default :local adapter keeps the previous behaviour
byte for byte, including the path traversal guard. A host can now set
config.storage_adapter to any object answering file?/directory?/locate and
serve builds from an object store - FileService and /api/download both honour
a Location.redirect, so a signing adapter turns them into redirects.
DownloadService#create still returns an absolute path (nil when missing) for
existing callers; #locate is the new entry point that can also return a
redirect. Ingestion (upload, extraction, file manager) stays local for now.
Publish: PublishService emits ActiveSupport::Notifications
("warp_engine.publish") with platform/name/version/software/release, so hosts
can react to a new build without hanging callbacks on the models.
WarpEngine.instruments_publish? lets a host feature-detect and keep its
fallback for older engine versions.
This commit is contained in:
@@ -3,6 +3,7 @@ require "apipie-rails"
|
||||
|
||||
require "warp_engine/version"
|
||||
require "warp_engine/configuration"
|
||||
require "warp_engine/storage"
|
||||
|
||||
module WarpEngine
|
||||
# A tábláink prefix nélküliek (softwares, releases, ...) — az isolate_namespace
|
||||
@@ -23,6 +24,18 @@ module WarpEngine
|
||||
def self.woodpecker_configured?
|
||||
config.woodpecker_url.present? && config.woodpecker_api_token.present?
|
||||
end
|
||||
|
||||
# A host innen tudja, hogy a publikálás ActiveSupport::Notifications-t szór
|
||||
# ("warp_engine.publish"), és nem kell modell-callbackre kapaszkodnia.
|
||||
# Régebbi engine-verziókon a metódus nem létezik, ezért a hívó oldalon
|
||||
# respond_to?-val kérdezendő.
|
||||
def self.instruments_publish?
|
||||
true
|
||||
end
|
||||
|
||||
def self.storage
|
||||
Storage.adapter
|
||||
end
|
||||
end
|
||||
|
||||
require "warp_engine/engine"
|
||||
|
||||
@@ -9,6 +9,12 @@ module WarpEngine
|
||||
# :database — only WarpEngine::ApplicationToken is accepted, the shared secret is not
|
||||
# application_token_owner_class: class name of the mandatory token owner
|
||||
# (e.g. "AdminUser"); nil makes :database mode reject every request.
|
||||
# storage_adapter: where build artifacts are served from.
|
||||
# :local (default) — the local filesystem under file_container_path,
|
||||
# byte for byte the previous behaviour;
|
||||
# any object — must answer file?/directory?/locate, see
|
||||
# WarpEngine::Storage. Serving only: uploads and
|
||||
# archive extraction still write to the local disk.
|
||||
# max_upload_size: file size cap in bytes for /build/upload (and the admin file manager).
|
||||
# enforce_software_ownership: when true, a DB token may only upload/publish
|
||||
# its own owner's softwares (unrestricted tokens are exempt).
|
||||
@@ -37,7 +43,8 @@ module WarpEngine
|
||||
:woodpecker_url,
|
||||
:woodpecker_api_token,
|
||||
:woodpecker_repo_owner,
|
||||
:image_owners
|
||||
:image_owners,
|
||||
:storage_adapter
|
||||
|
||||
def initialize
|
||||
@file_container_path = ENV.fetch("FILE_CONTAINER_PATH", "/softwares")
|
||||
@@ -55,6 +62,7 @@ module WarpEngine
|
||||
@woodpecker_api_token = ENV["WOODPECKER_API_TOKEN"]
|
||||
@woodpecker_repo_owner = ENV["WOODPECKER_REPO_OWNER"]
|
||||
@image_owners = []
|
||||
@storage_adapter = :local
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
module WarpEngine
|
||||
# Where the artifacts physically live.
|
||||
#
|
||||
# Until now every path in the engine was a local filesystem path. This module
|
||||
# is the seam a host needs to serve builds from somewhere else (an object
|
||||
# store behind a CDN, for example) without patching the engine.
|
||||
#
|
||||
# The default adapter is :local and behaves exactly as before - same paths,
|
||||
# same traversal protection, same File.file? checks.
|
||||
#
|
||||
# A custom adapter is any object answering to this contract:
|
||||
#
|
||||
# file?(relative_path) -> true/false
|
||||
# directory?(relative_path) -> true/false
|
||||
# locate(relative_path, filename: nil, expires_in: nil) -> Location
|
||||
#
|
||||
# It is set on the configuration:
|
||||
#
|
||||
# c.storage_adapter = MyObjectStore.new # or :local (default)
|
||||
#
|
||||
# NOTE: ingestion (build/upload, archive extraction, the admin file manager)
|
||||
# still writes to the local filesystem. A remote adapter therefore needs its
|
||||
# own upload path today; the serving side is what this seam covers.
|
||||
module Storage
|
||||
Location = Struct.new(:kind, :path, :url, keyword_init: true) do
|
||||
def file? = kind == :file
|
||||
def redirect? = kind == :redirect
|
||||
|
||||
def self.file(path) = new(kind: :file, path: path)
|
||||
def self.redirect(url) = new(kind: :redirect, url: url)
|
||||
end
|
||||
|
||||
# The local filesystem, rooted at config.file_container_path.
|
||||
class LocalAdapter
|
||||
def base_path
|
||||
Pathname.new(WarpEngine.config.file_container_path)
|
||||
end
|
||||
|
||||
def absolute_path(relative_path)
|
||||
base_path.join(relative_path.to_s)
|
||||
end
|
||||
|
||||
def file?(relative_path)
|
||||
path = absolute_path(relative_path)
|
||||
File.file?(path) && inside_base?(path)
|
||||
end
|
||||
|
||||
def directory?(relative_path)
|
||||
path = absolute_path(relative_path)
|
||||
File.directory?(path) && inside_base?(path)
|
||||
end
|
||||
|
||||
# expires_in is part of the contract for signing adapters; the local
|
||||
# filesystem has nothing to sign, so it is ignored here.
|
||||
def locate(relative_path, filename: nil, expires_in: nil)
|
||||
Location.file(absolute_path(relative_path).to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Path traversal guard: the resolved path must stay under the container.
|
||||
def inside_base?(path)
|
||||
root = base_path.realpath.to_s
|
||||
Pathname.new(path).realpath.to_s.start_with?(root)
|
||||
rescue Errno::ENOENT
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def adapter
|
||||
configured = WarpEngine.config.storage_adapter
|
||||
|
||||
case configured
|
||||
when nil, :local, "local" then local_adapter
|
||||
else configured
|
||||
end
|
||||
end
|
||||
|
||||
def local? = adapter.is_a?(LocalAdapter)
|
||||
|
||||
def local_adapter
|
||||
@local_adapter ||= LocalAdapter.new
|
||||
end
|
||||
|
||||
# Tests and hosts that swap the configuration at runtime.
|
||||
def reset!
|
||||
@local_adapter = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,3 @@
|
||||
module WarpEngine
|
||||
VERSION = "0.1.0"
|
||||
VERSION = "0.2.0"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user