diff --git a/Dockerfile b/Dockerfile index 6ba96e07..9e046056 100755 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update -qq --fix-missing RUN apt-get install -y curl # Get node -RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - +RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - RUN apt-get update && apt-get install -y nodejs apt-transport-https RUN apt-get install -y xvfb nano build-essential libpq-dev wget postgresql-client postgresql-contrib diff --git a/app/assets/stylesheets/extensions/compilation.scss b/app/assets/stylesheets/extensions/compilation.scss index a93463b6..aaecba9b 100755 --- a/app/assets/stylesheets/extensions/compilation.scss +++ b/app/assets/stylesheets/extensions/compilation.scss @@ -36,7 +36,7 @@ top: 0; font-size: smaller; margin-bottom: 1.5em; - .compilation-complete { + .compilation-pending { border-radius: 5px; border: 1px solid $notice_primary_green; background-color: $notice_secondary_green; @@ -52,6 +52,10 @@ border: 1px solid $notice_primary_red; background-color: $notice_secondary_red; } + .compilation-complete { + border: none; + background-color: white; + } } .compilation-notice { diff --git a/app/controllers/extensions_controller.rb b/app/controllers/extensions_controller.rb index 7261f6f5..e5328a83 100755 --- a/app/controllers/extensions_controller.rb +++ b/app/controllers/extensions_controller.rb @@ -47,7 +47,6 @@ def new @repo_names = Marshal.load(@repo_names) else FetchAccessibleReposWorker.perform_async(current_user.id) - # puts '******* FetchAccessibleReposWorker *********' end @extension = Extension.new @@ -60,15 +59,23 @@ def new # def create eparams = params.require(:extension).permit(:name, :description, :github_url, :github_url_short, :tmp_source_file, :tag_tokens, :version, compatible_platforms: []) - create_extension = CreateExtension.new(eparams, current_user) - @extension = create_extension.process! - if @extension.errors.none? - redirect_to owner_scoped_extension_url(@extension), notice: t("extension.created") + candidate = Extension.new(eparams) + + if candidate.valid? + create_context = CreateExtension.call(params: eparams, candidate: candidate, user: current_user) + + if create_context.success? + @extension = create_context.extension + redirect_to owner_scoped_extension_url(@extension), notice: t("extension.created") + return + end else - @repo_names = current_user.octokit.repos.map { |r| r.to_h.slice(:full_name, :name, :description) } rescue [] - render :new + flash[:alert] = candidate.errors.full_messages.join("; ") end + @extension = Extension.new + @repo_names = current_user.octokit.repos.map { |r| r.to_h.slice(:full_name, :name, :description) } rescue [] + render :new end # @@ -169,14 +176,14 @@ def update @extension.update_attributes(extension_edit_params) key = if extension_edit_params.key?(:up_for_adoption) - if extension_edit_params[:up_for_adoption] == 'true' - 'adoption.up' - else - 'adoption.down' - end - else - 'extension.updated' - end + if extension_edit_params[:up_for_adoption] == 'true' + 'adoption.up' + else + 'adoption.down' + end + else + 'extension.updated' + end redirect_to owner_scoped_extension_url(@extension), notice: t(key, name: @extension.name) end @@ -328,19 +335,33 @@ def report redirect_to owner_scoped_extension_url(@extension), notice: t("extension.reported", extension: @extension.name) end + # + # GET /extensions/:extension/sync_repo + # + # Recompiles the asset + # def sync_repo - extension = Extension.with_owner_and_lowercase_name(owner_name: params[:username], lowercase_name: params[:id]) - authorize! extension - CompileExtension.call(extension: extension) - redirect_to owner_scoped_extension_url(@extension), notice: t("extension.syncing_in_progress") + @extension = Extension.with_owner_and_lowercase_name(owner_name: params[:username], lowercase_name: params[:id]) + authorize! @extension + compile_context = CompileExtension.call(extension: @extension) + if compile_context.success? + flash[:notice] = t("extension.syncing_in_progress") + else + flash[:error] = t("extension.failed_to_compile", extension: @extension.name) + end + redirect_to owner_scoped_extension_url(@extension) end + # + # GET /extensions/:extension/sync_status + # + # Returns the status of a recompile + # def sync_status @extension = Extension.find_by(id: params[:id]) redis_pool.with do |redis| - @job_ids = JSON.parse( redis.get("compile.extension;#{params[:id]};status") || "{}" ) + @status = JSON.parse( redis.get("compile.extension;#{params[:id]};status") || "{}" ) end - respond_to do |format| format.js end @@ -379,9 +400,9 @@ def webhook CollectExtensionMetadataWorker.perform_async(@extension.id, []) end when "watch" - ExtractExtensionStargazersWorker.perform_async(@extension.id) + ExtractExtensionStargazers.call(@extension) when "member" - ExtractExtensionCollaboratorsWorker.perform_async(@extension.id) + ExtractExtensionCollaborators.call(@extension) else Rails.logger.info '*** Github Error: unidentified event type' end diff --git a/app/helpers/extensions_helper.rb b/app/helpers/extensions_helper.rb index 8f6649fd..8ae7003f 100755 --- a/app/helpers/extensions_helper.rb +++ b/app/helpers/extensions_helper.rb @@ -187,6 +187,14 @@ def to_asset_definition(version, format: "yaml") resp end + # + # Given extentsion returns compilation errors + # + # @param extension [Extension] + # @param version [ExtensionVersion] optional + # + # @return [String] compilation errors + # def compilation_errors(extension, version=nil) errors = [] if extension.compilation_error.present? @@ -200,53 +208,32 @@ def compilation_errors(extension, version=nil) errors end - def compilation_status(extension, job_ids) - job_count = @job_ids.length - pending, retrying, complete, failed = 0.0, 0.0, 0.0, 0.0 - jobs_failed, jobs_retrying = [], [] - - @job_ids.each do |job, key| + def compilation_status(extension, status) + worker_status = Sidekiq::Status::status( status['job_id'] ) + html = '' + klass = '' + case worker_status + when :queued, :working + html += content_tag('div', raw("Compiling Extension: #{status['task']}"), class: 'compilation-notice') + klass = 'compilation-pending' - status = Sidekiq::Status::status(key) + when :retrying + html += content_tag("div", raw("Retrying Compilations: #{status['worker']}"), class: 'compilation-notice') + klass = 'compilation-retry' - case status - when :queued, :working - pending += 1 - when :retrying - retrying += 1 - jobs_retrying << job - when :complete - complete += 1 - # puts status - when :failed, :interrupted - failed += 1 - jobs_failed << job - else - end - end - - percent_complete = ((complete / job_count) * 100).round(2) + when :complete + redis_pool.with do |redis| + redis.del("compile.extension;#{extension.id};status") + end + klass = 'compilation-complete' - html = '' - klass = 'compilation-complete' - if failed > 0 - html += content_tag("div", raw("Compilations failed: #{jobs_failed.join(', ')}"), class: 'compilation-notice') - klass = 'compilation-failed' - end - if retrying > 0 - html += content_tag("div", raw("Retrying Compilations: #{jobs_retrying.join(', ')}"), class: 'compilation-notice') - klass = 'compilation-retry' - end - html += content_tag('div', raw("Compiling Extension % #{percent_complete} Complete"), class: 'compilation-notice') + when :failed, :interrupted + html += content_tag("div", raw("Compilations failed: #{status['worker']}"), class: 'compilation-notice') + klass = 'compilation-failed' - if percent_complete == 100 - redis_pool.with do |redis| - redis.del("compile.extension;#{extension.id};status") - end end - content_tag('div', raw(html), class: klass ) - + end # diff --git a/app/interactors/compile_extension.rb b/app/interactors/compile_extension.rb index 7ff22ac8..08a02e49 100755 --- a/app/interactors/compile_extension.rb +++ b/app/interactors/compile_extension.rb @@ -5,39 +5,38 @@ class CompileExtension include Interactor - # The required context attributes: delegate :extension, to: :context def call + + extension.update_column(:compilation_error, '') + + # test credentials before going to background job + begin + extension.octokit.user + rescue Octokit::Unauthorized + message = "Octokit Error: Credentials are bad on this asset." + extension.update_column(:compilation_error, message) + context.fail!(error: message) + end + + CompileExtensionStatusClear.call(extension: extension) + if extension.hosted? - compile_hosted_extension(extension) - else - CompileExtensionStatusClear.call(extension: extension) - CompileExtensionStatus.call( - extension: extension, - worker: 'ExtractExtensionParentWorker', - job_id: ExtractExtensionParentWorker.perform_async(extension.id) + extension: extension, + task: 'Set Up Hosted Extension Compilation', + worker: 'SetupCompileHostedExtension', + job_id: SetupCompileHostedExtensionWorker.perform_async(extension.id) ) - + else CompileExtensionStatus.call( - extension: extension, - worker: 'SyncExtensionRepoWorker', - job_id: SyncExtensionRepoWorker.perform_async(extension.id) + extension: extension, + task: 'Set Up Github Extension Compilation', + worker: 'SetupCompileGithubExtension', + job_id: SetupCompileGithubExtensionWorker.perform_async(extension.id) ) end end - private - - def redis_pool - REDIS_POOL - end - - def compile_hosted_extension(extension) - extension.extension_versions.each do |version| - next unless version.source_file.attached? - version.source_file.blob.analyze_later - end - end end diff --git a/app/interactors/compile_extension_status.rb b/app/interactors/compile_extension_status.rb index d4558c7d..7fe91cce 100755 --- a/app/interactors/compile_extension_status.rb +++ b/app/interactors/compile_extension_status.rb @@ -2,15 +2,21 @@ class CompileExtensionStatus include Interactor # The required context attributes: - delegate :extension, to: :context - delegate :worker, to: :context - delegate :job_id, to: :context + delegate :extension, to: :context + delegate :task, to: :context + delegate :worker, to: :context + delegate :job_id, to: :context + def call redis_pool.with do |redis| status = redis.get("compile.extension;#{extension.id};status") status = status.present? ? JSON.parse( status ) : {} - status[worker] = job_id + status['task'] = task.present? ? task : '' + if job_id.present? + status['worker'] = worker + status['job_id'] = job_id + end redis.set("compile.extension;#{extension.id};status", status.to_json) end end diff --git a/app/interactors/compile_github_extension_version_config.rb b/app/interactors/compile_github_extension_version_config.rb index eb431042..b51b7ba1 100755 --- a/app/interactors/compile_github_extension_version_config.rb +++ b/app/interactors/compile_github_extension_version_config.rb @@ -3,73 +3,70 @@ # # See the example in ./compile_hosted_extension_version_config.rb. -require 'fetch_remote_sha' - class CompileGithubExtensionVersionConfig include Interactor - # The required context attributes: - delegate :version, to: :context - delegate :system_command_runner, to: :context + delegate :extension, to: :context + delegate :version, to: :context + delegate :command_run, to: :context + delegate :config_hash, to: :context def call - config_hash = fetch_bonsai_config(system_command_runner) + context.command_run = CmdAtPath.new(extension.repo_path) + + fetch_bonsai_config if config_hash['builds'].present? version.update_column(:config, config_hash) else context.fail!(error: "Bonsai configuration has no 'builds' section") end - - config_hash['builds'] = compile_builds(version, config_hash['builds']) - context.data_hash = config_hash + context.config_hash['builds'] = compile_builds(config_hash['builds']) + rescue => error raise if Interactor::Failure === error # Don't trap context.fail! calls - context.fail!(error: "could not compile the Bonsai configuration file: #{error}") + context.fail!(error: "Could not compile the Bonsai configuration file: #{error}") end private - def fetch_bonsai_config(cmd_runner) + def fetch_bonsai_config name_switches = Extension::CONFIG_FILE_NAMES .map {|name| "-name '#{name}'"} .join(" -o ") find_command = "find . -maxdepth 1 #{name_switches}" - path = cmd_runner - .cmd(find_command) - .split("\n") - .first + path = command_run.cmd(find_command).split("\n").first + + unless path.present? + context.fail!(error: 'Cannot find a Bonsai configuration file') + end - context.fail!(error: 'cannot find a Bonsai configuration file') unless path.present? + body = command_run.cmd("cat '#{path}'").encode(Encoding.find('UTF-8'), {invalid: :replace, undef: :replace, replace: ''}) - body = cmd_runner.cmd("cat '#{path}'").encode(Encoding.find('UTF-8'), {invalid: :replace, undef: :replace, replace: ''}) begin - config_hash = YAML.load(body.to_s) + context.config_hash = YAML.load(body.to_s) rescue => error - context.fail!(error: "cannot parse the Bonsai configuration file: #{error.message}") + context.fail!(error: "Cannot parse the Bonsai configuration file: #{error.message}") end context.fail!(error: "Bonsai configuration is invalid") unless config_hash.is_a?(Hash) - config_hash end - def compile_builds(version, build_configs) - github_asset_data_hashes = gather_github_release_asset_data_hashes(version) + def compile_builds(build_configs) + github_asset_data_hashes = gather_github_release_asset_data_hashes github_asset_data_hashes_lut = Array.wrap(github_asset_data_hashes) .group_by { |h| h[:name] } .transform_values(&:first) Array.wrap(build_configs).each_with_index.map { |build_config, idx| - Thread.new do - compiled_config = compile_build_hash(build_config, idx + 1, github_asset_data_hashes_lut, version) - build_config.merge compiled_config - end - }.map(&:value) + compiled_config = compile_build_hash(build_config, idx + 1, github_asset_data_hashes_lut) + build_config.merge compiled_config + } end - def gather_github_release_asset_data_hashes(version) + def gather_github_release_asset_data_hashes releases_data = version.octokit .releases(version.github_repo) .find { |h| h[:tag_name] == version.version } @@ -77,21 +74,21 @@ def gather_github_release_asset_data_hashes(version) Array.wrap(releases_data[:assets]) end - def compile_build_hash(build_config, num, github_asset_data_hashes_lut, version) + def compile_build_hash(build_config, num, github_asset_data_hashes_lut) - context.fail!(error: "build ##{num} is malformed (perhaps missing indentation)") unless build_config.is_a?(Hash) + context.fail!(error: "Build ##{num} is malformed (perhaps missing indentation)") unless build_config.is_a?(Hash) src_sha_filename = build_config['sha_filename'] context.fail!(error: "build ##{num} is missing a 'sha_filename' value") unless src_sha_filename.present? src_asset_filename = build_config['asset_filename'] - context.fail!(error: "build ##{num} is missing an 'asset_filename' value") unless src_asset_filename.present? + context.fail!(error: "Build ##{num} is missing an 'asset_filename' value") unless src_asset_filename.present? compiled_sha_filename = version.interpolate_variables(src_sha_filename) - context.fail!(error: "build ##{num} 'sha_filename' value could not be interpolated") unless compiled_sha_filename.present? + context.fail!(error: "Build ##{num} 'sha_filename' value could not be interpolated") unless compiled_sha_filename.present? compiled_asset_filename = version.interpolate_variables(src_asset_filename) - context.fail!(error: "build ##{num} 'asset_filename' value could not be interpolated") unless compiled_asset_filename.present? + context.fail!(error: "Build ##{num} 'asset_filename' value could not be interpolated") unless compiled_asset_filename.present? asset_filename = File.basename(compiled_asset_filename) file_download_url = github_download_url(compiled_asset_filename, github_asset_data_hashes_lut) @@ -109,13 +106,14 @@ def compile_build_hash(build_config, num, github_asset_data_hashes_lut, version) def read_sha_file(compiled_sha_filename, asset_filename, github_asset_data_hashes_lut) sha_download_url = github_download_url(compiled_sha_filename, github_asset_data_hashes_lut) - result = FetchRemoteSha.call( + + result = FetchRemoteSha.call( sha_download_url: sha_download_url, asset_filename: asset_filename ) result.tap do |sha_result| - context.fail!(error: "cannot extract the SHA for #{asset_filename}") unless sha_result.sha.present? + context.fail!(error: "Cannot extract the SHA for #{asset_filename}") unless sha_result.sha.present? end end @@ -132,7 +130,6 @@ def github_download_url(filename, github_asset_data_hashes_lut) return asset_data[:browser_download_url] end end - context.fail!(error: "missing GitHub release asset for #{filename}") - return '' + context.fail!(error: "Missing GitHub release asset for #{filename}") end -end +end \ No newline at end of file diff --git a/app/interactors/compile_github_overrides.rb b/app/interactors/compile_github_overrides.rb new file mode 100755 index 00000000..e863a186 --- /dev/null +++ b/app/interactors/compile_github_overrides.rb @@ -0,0 +1,91 @@ +class CompileGithubOverrides + include Interactor + + delegate :extension, to: :context + delegate :version, to: :context + + def call + + version_overrides = version.config["overrides"].nil? ? {} : version.config["overrides"][0] + extension_overrides = extension.config_overrides + + # readme overrides + if version_overrides.present? && version_overrides["readme_url"].present? + override_readme(version_overrides["readme_url"]) + elsif extension_overrides["readme_url"].present? + override_readme(extension_overrides["readme_url"]) + end + + end + + private + + def override_readme(readme_url) + + message = [] + + begin + url = URI.parse(readme_url) + rescue URI::Error => error + context.fail!( error: "URI error: #{readme_url} - #{error.message}") + end + + readme_ext = File.extname(url.path).gsub(".", "") + unless ExtensionVersion::MARKDOWN_EXTENSIONS.include?(readme_ext) + message << "#{version.version} override readme_url is not a valid markdown file." + end + + # resconstruct Github url to get file, not html + # https://github.com/jspaleta/sensu-plugins-redis/blob/master/README.md + # should translate to + # https://raw.githubusercontent.com/jspaleta/sensu-plugins-redis/master/README.md + + if ['github.com', 'www.github.com'].include?(url.host) + url.host = "raw.githubusercontent.com" + url.path.gsub!('/blob', '') + end + + # get file contents + begin + file = url.open + rescue OpenURI::HTTPError => error + status = error.io.status + message << "#{version.version} #{url.path} file read error: #{status[0]} - #{status[1]}" + compilation_error = ([version.compilation_error] + message).compact + version.update_column(:compilation_error, compilation_error.join('; ')) + context.fail!(error: compilation_error.join('; ')) + end + + readme = file.read + + if readme.include?('!DOCTYPE html') + message << "#{version.version} override readme is not valid markdown." + end + + begin + filter = HTML::Pipeline.new [ + HTML::Pipeline::MarkdownFilter, + ], {gfm: true} + filter.call(readme) + rescue + message << "#{version.version} override readme is not valid markdown." + end + + if message.present? + compilation_error =([version.compilation_error] + message).compact + if compilation_error.present? + version.update_column(:compilation_error, compilation_error.join('; ')) + context.fail!(error: compilation_error.join('; ')) + end + else + + readme = readme.encode(Encoding.find('UTF-8'), {invalid: :replace, undef: :replace, replace: ''}) + + version.update_columns( + readme: readme, + readme_extension: readme_ext + ) + end + end + +end \ No newline at end of file diff --git a/app/interactors/set_up_hosted_extension.rb b/app/interactors/compile_new_hosted_extension.rb similarity index 64% rename from app/interactors/set_up_hosted_extension.rb rename to app/interactors/compile_new_hosted_extension.rb index 0fbf9388..00528558 100755 --- a/app/interactors/set_up_hosted_extension.rb +++ b/app/interactors/compile_new_hosted_extension.rb @@ -1,43 +1,34 @@ -# Call this service after creating a Sensu-hosted extension. -# This service sets up all of the version configure, etc -# for the new extension. - -class SetUpHostedExtension - include Interactor - - # The required context attributes: - delegate :extension, to: :context - delegate :version_name, to: :context - - def call - extension_version = extension.extension_versions.create!(version: version_name) - - if extension.tmp_source_file.attached? - # Transfer the temporarily-staged attachment to the extension_version. - extension_version.source_file.attach(extension.tmp_source_file.blob) - - - # ActiveStorage blob checksum is a base64-encoded MD5 digest of the blob’s data - # and is unique. A new blob is created when a file is updated. - last_commit_sha = extension_version.source_file.blob.checksum - extension_version.update_columns(last_commit_sha: last_commit_sha, last_commit_at: DateTime.now) - - extension.tmp_source_file.detach - - attachment = extension_version.source_file.attachment - if attachment.analyzed? - extension_version.after_attachment_analysis(attachment, attachment.metadata) - else - #:nocov: - CompileExtensionStatus.call( - extension: extension, - worker: 'ActiveStorageAnalyzeBlob', - job_id: attachment.analyze_later - ) - - #:nocov: - end - end - end - -end +class CompileNewHostedExtension + include Interactor + + delegate :extension, to: :context + delegate :version_name, to: :context + + def call + extension_version = extension.extension_versions.create!(version: version_name) + + if extension.tmp_source_file.attached? + # Transfer the temporarily-staged attachment to the extension_version. + extension_version.source_file.attach(extension.tmp_source_file.blob) + + + # ActiveStorage blob checksum is a base64-encoded MD5 digest of the blob’s data + # and is unique. A new blob is created when a file is updated. + last_commit_sha = extension_version.source_file.blob.checksum + extension_version.update_columns(last_commit_sha: last_commit_sha, last_commit_at: DateTime.now) + + extension.tmp_source_file.detach + + attachment = extension_version.source_file.attachment + if attachment.analyzed? + extension_version.after_attachment_analysis(attachment, attachment.metadata) + else + #:nocov: + attachment.analyze + #:nocov: + end + end + + end + +end \ No newline at end of file diff --git a/app/interactors/create_extension.rb b/app/interactors/create_extension.rb new file mode 100755 index 00000000..b4289ff2 --- /dev/null +++ b/app/interactors/create_extension.rb @@ -0,0 +1,74 @@ +class CreateExtension + include Interactor + + delegate :params, to: :context + delegate :candidate, to: :context + delegate :user, to: :context + + def call + + if candidate.hosted? + candidate.owner = User.host_organization + candidate.owner_name = ENV['HOST_ORGANIZATION'] + else + + # test credentials before going to background job + begin + owner.octokit.user + rescue Octokit::Unauthorized + message = "Octokit Error: Credentials were denied by Github." + flash[:alert] = message + context.fail!(error: message) + end + + fetch_context = FetchGithubInfo.call(extension: candidate, owner: user) + candidate.owner = user + candidate.owner_name = fetch_context.owner_name + candidate.github_organization = fetch_context.github_organization + end + + # A disabled extension is available for re-use, but it must first be re-enabled. + unless candidate.hosted? + existing = Extension.unscoped.where(enabled: false, github_url: candidate.github_url).first + if existing.present? + existing.update_attribute(:enabled, true) + # TODO Does this need to validated and/or recompiled? + context.extension = existing + return + end + end + + validate_context = ValidateNewExtension.call(extension: candidate, owner: user) + + if validate_context.success? + candidate = validate_context.extension + + candidate.save + + CompileExtensionStatusClear.call(extension: candidate) + + if candidate.hosted? + version_name = params[:version] || '0.0.1' + CompileExtensionStatus.call( + extension: candidate, + task: 'Set Up Hosted Compilation', + worker: 'SetupHostedExtensionWorker', + job_id: SetupHostedExtensionWorker.perform_async(candidate.id, version_name) + ) + else + compatible_platforms = params[:compatible_platforms] || [] + compatible_platforms.select!{ |p| p.present? } + + CompileExtensionStatus.call( + extension: candidate, + task: 'Set Up Github Compilation', + worker: 'SetupGithubExtensionWorker', + job_id: SetupGithubExtensionWorker.perform_async(candidate.id, compatible_platforms) + ) + end + end + + context.extension = candidate + end + +end \ No newline at end of file diff --git a/app/interactors/destroy_assets.rb b/app/interactors/destroy_assets.rb index 4eec492d..08465123 100755 --- a/app/interactors/destroy_assets.rb +++ b/app/interactors/destroy_assets.rb @@ -9,8 +9,6 @@ class DestroyAssets def call context.fail!(error: "It does not store assets for master version") if version.version == 'master' - - puts "Destroying assets for #{version.version} on S3" version.release_assets.each do |release_asset| @@ -19,15 +17,10 @@ def call object_exists = context.s3_bucket.object(key).exists? begin - if object_exists - context.s3_bucket.object(key).delete - puts "S3 destroyed: #{key}" - else - puts "Already deleted from S3: #{key}" - end + context.s3_bucket.object(key).delete if object_exists release_asset.destroy rescue Aws::S3::Errors::ServiceError => error - puts "****** S3 error: #{error.code} - #{error.message}" + context.error = "S3 error: #{error.code} - #{error.message}" next end diff --git a/app/interactors/ensure_github_user_and_account.rb b/app/interactors/ensure_github_user_and_account.rb new file mode 100755 index 00000000..e8238b54 --- /dev/null +++ b/app/interactors/ensure_github_user_and_account.rb @@ -0,0 +1,47 @@ +class EnsureGithubUserAndAccount + include Interactor + + delegate :github_user, to: :context + delegate :account, to: :context + + def call + + account = Account.find_or_initialize_by( + username: github_user[:login], + provider: "github" + ) + + github_user[:name] ||= "Unknown Name" + + account.user = User.unscoped.find_or_create_by(id: account.user_id).tap do |a| + a.first_name = first_name + a.last_name = last_name + a.email = github_user[:email] + a.avatar_url = github_user[:avatar_url] + # enabled: true # only if you want to reactivate inactive accounts + end + + account.save(validate: false) + + context.account = account + + end + + private + + # "John Clark Smith" = first_name: "John Clark", last_name: "Smith" + def first_name + if github_user[:name].split.count > 1 + github_user[:name].split[0..-2].join(' ') + else + github_user[:name] + end + end + + def last_name + if github_user[:name].split.count > 1 + github_user[:name].split.last + end + end + +end \ No newline at end of file diff --git a/app/interactors/ensure_updated_version.rb b/app/interactors/ensure_updated_version.rb new file mode 100755 index 00000000..88b857db --- /dev/null +++ b/app/interactors/ensure_updated_version.rb @@ -0,0 +1,33 @@ +class EnsureUpdatedVersion + include Interactor + + delegate :extension, to: :context + delegate :tag_name, to: :context + delegate :readme_body, to: :context + delegate :readme_ext, to: :context + delegate :error_message, to: :context + delegate :command_run, to: :context + + def call + context.command_run = CmdAtPath.new(extension.repo_path) + + yml_line_count = command_run.cmd("find . -name '*.yml' -o -name '*.yaml' -print0 | xargs -0 wc -l")&.split("\n")&.last || "" + rb_line_count = command_run.cmd("find . -name '*.rb' -print0 | xargs -0 wc -l")&.split("\n")&.last || "" + + yml_line_count = yml_line_count.strip.to_i + rb_line_count = rb_line_count.strip.to_i + + version = extension.extension_versions.find_or_create_by(version: tag_name) + + version.update_columns( + readme: readme_body, + readme_extension: readme_ext, + yml_line_count: yml_line_count, + rb_line_count: rb_line_count + ) + + context.version = version + + end + +end \ No newline at end of file diff --git a/app/interactors/extract_extension_basic_metadata.rb b/app/interactors/extract_extension_basic_metadata.rb new file mode 100755 index 00000000..ba14735a --- /dev/null +++ b/app/interactors/extract_extension_basic_metadata.rb @@ -0,0 +1,18 @@ +class ExtractExtensionBasicMetadata + include Interactor + + delegate :extension, to: :context + + def call + begin + repo = extension.octokit.repo(extension.github_repo) + extension.update_columns( + name: repo[:full_name], + issues_url: "https://github.com/#{extension.github_repo}/issues" + ) + rescue Octokit::NotFound + context.fail! + end + end + +end \ No newline at end of file diff --git a/app/interactors/extract_extension_collaborators.rb b/app/interactors/extract_extension_collaborators.rb new file mode 100755 index 00000000..38cf3a36 --- /dev/null +++ b/app/interactors/extract_extension_collaborators.rb @@ -0,0 +1,49 @@ +class ExtractExtensionCollaborators + include Interactor + + delegate :extension, to: :context + + def call + + begin + 1.step do |page| + contributors = extension.octokit.contributors(extension.github_repo, nil, page: page) + break if contributors.blank? + process_contributors(contributors) + end + rescue Octokit::NotFound + # context.fail! + end + + begin + 1.step do |page| + collaborators = extension.octokit.collaborators(extension.github_repo, page: page) + break if collaborators.blank? + process_contributors(collaborators) + end + rescue Octokit::NotFound + # context.fail! + end + + end + + private + + def process_contributors(contributors) + contributors.each do |contributor| + begin + collaborator = extension.octokit.user(contributor[:login]) + unless extension.collaborators.includes_user(collaborator[:login]).present? + ActiveRecord::Base.transaction do + context = EnsureGithubUserAndAccount.call(github_user: collaborator) + Collaborator.create(user: context.account.user, resourceable: extension) + end + end + + rescue Octokit::NotFound + # context.fail! + end + end + end + +end \ No newline at end of file diff --git a/app/interactors/extract_extension_license.rb b/app/interactors/extract_extension_license.rb new file mode 100755 index 00000000..1a40ea64 --- /dev/null +++ b/app/interactors/extract_extension_license.rb @@ -0,0 +1,32 @@ +class ExtractExtensionLicense + include Interactor + + delegate :extension, to: :context + + def call + + begin + repo = extension.octokit.repo(extension.github_repo, accept: "application/vnd.github.drax-preview+json") + + if repo[:license] + begin + license = extension.octokit.license(repo[:license][:key], accept: "application/vnd.github.drax-preview+json") + rescue + license = { + name: repo[:license][:name], + body: "" + } + end + extension.update_columns( + license_name: license[:name], + license_text: license[:body] + ) + end + + rescue Octokit::NotFound + context.fail! + end + + end + +end \ No newline at end of file diff --git a/app/workers/extract_extension_parent_worker.rb b/app/interactors/extract_extension_parent.rb similarity index 54% rename from app/workers/extract_extension_parent_worker.rb rename to app/interactors/extract_extension_parent.rb index 62c68736..add3ec2f 100755 --- a/app/workers/extract_extension_parent_worker.rb +++ b/app/interactors/extract_extension_parent.rb @@ -1,31 +1,31 @@ -class ExtractExtensionParentWorker < ApplicationWorker - - def perform(extension_id) - @extension = Extension.find(extension_id) - repo = octokit.repo(@extension.github_repo) +class ExtractExtensionParent + include Interactor + + delegate :extension, to: :context + + def call + repo = extension.octokit.repo(extension.github_repo) repo_parent = repo[:parent] + + # if parent exists on github if repo_parent.present? parent_name = repo_parent[:name] parent_owner_name = repo_parent[:owner][:login] parent_html_url = repo_parent[:html_url] - @extension.update( + extension.update( parent_name: parent_name, parent_owner_name: parent_owner_name, parent_html_url: parent_html_url ) + # if parent exists on bonsai parent = Extension.find_by(owner_name: parent_owner_name, lowercase_name: parent_name) - @extension.update(parent: parent) if parent.present? + extension.update(parent: parent) if parent.present? end - end - - private + end - def octokit - @octokit ||= @extension.octokit - end end \ No newline at end of file diff --git a/app/interactors/extract_extension_stargazers.rb b/app/interactors/extract_extension_stargazers.rb new file mode 100755 index 00000000..dbf7a70a --- /dev/null +++ b/app/interactors/extract_extension_stargazers.rb @@ -0,0 +1,39 @@ +class ExtractExtensionStargazers + include Interactor + + delegate :extension, to: :context + + def call + + begin + 1.step do |page| + stargazers = extension.octokit.stargazers(extension.github_repo, page: page, per_page: 100) + break if stargazers.blank? + process_stargazers(stargazers) + end + rescue Octokit::NotFound + # context.fail! + end + + end + + private + + def process_stargazers(stargazers) + stargazers.each do |stargazer| + begin + github_stargazer = extension.octokit.user(stargazer[:login]) + unless extension.extension_followers.includes_user(github_stargazer[:login]).present? + ActiveRecord::Base.transaction do + context = EnsureGithubUserAndAccount.call(github_user: github_stargazer) + extension.extension_followers.create(user: context.account.user) + end + end + + rescue Octokit::NotFound + # context.fail! + end + end + end + +end \ No newline at end of file diff --git a/app/interactors/fetch_github_info.rb b/app/interactors/fetch_github_info.rb new file mode 100755 index 00000000..3e4c2c4a --- /dev/null +++ b/app/interactors/fetch_github_info.rb @@ -0,0 +1,30 @@ +# Call this service before creating a GitHub-hosted extension. +# This service gathers information from Github for the new extension. + +class FetchGithubInfo + include Interactor + + delegate :extension, to: :context + delegate :owner, to: :context + + def call + repo_info = owner.octokit.repo(extension.github_repo) + org = repo_info[:organization] + + context.github_organization = if org + GithubOrganization.where(github_id: org[:id]).first_or_create!( + name: org[:login], + avatar_url: org[:avatar_url] + ) + else + nil + end + + context.owner_name = if org + org[:login] + else + owner.username + end + end + +end \ No newline at end of file diff --git a/app/interactors/fetch_readme_at_version.rb b/app/interactors/fetch_readme_at_version.rb new file mode 100755 index 00000000..b4fc171d --- /dev/null +++ b/app/interactors/fetch_readme_at_version.rb @@ -0,0 +1,38 @@ +class FetchReadmeAtVersion + include Interactor + + delegate :extension, to: :context + delegate :tag_name, to: :context + delegate :command_run, to: :context + delegate :file_extension, to: :context + delegate :body, to: :context + + def call + + context.command_run = CmdAtPath.new(extension.repo_path) + filename = command_run.cmd("ls README*") + + if filename.present? + filename = filename.split("\n").first + extract_file_extension(filename) + context.body = command_run.cmd("cat '#{filename}'") + context.body = body.encode(Encoding.find('UTF-8'), {invalid: :replace, undef: :replace, replace: ''}) + else + context.error = "There is no README file for the #{context.tag_name} #{I18n.t('nouns.extension')}." + end + + end + + private + + def extract_file_extension(filename) + match = filename.match(/\.[a-zA-Z0-9]+$/) + context.file_extension = if match.present? + match[0].gsub(".", "") + else + "txt" + end + end + +end + diff --git a/app/interactors/find_or_create_release_asset.rb b/app/interactors/find_or_create_release_asset.rb index 51c20724..c171ca65 100755 --- a/app/interactors/find_or_create_release_asset.rb +++ b/app/interactors/find_or_create_release_asset.rb @@ -47,8 +47,6 @@ def create_release_asset(version, build) end def update_release_asset(release_asset, version, build) - #puts "******************** UPDATE RELEASE ASSET #{build['asset_sha']}" - #puts "******************** ASSET URL #{build['asset_url']}" release_asset.update( viable: build['viable'], filter: Array.wrap(build['filter']), diff --git a/app/interactors/notify_moderators_of_new_extension.rb b/app/interactors/notify_moderators_of_new_extension.rb new file mode 100755 index 00000000..a31338d8 --- /dev/null +++ b/app/interactors/notify_moderators_of_new_extension.rb @@ -0,0 +1,12 @@ +class NotifyModeratorsOfNewExtension + include Interactor + + delegate :extension, to: :context + + def call + User.moderator.each do |user| + ExtensionMailer.delay.notify_moderator_of_new(extension.id, user.id) + end + end + +end \ No newline at end of file diff --git a/app/interactors/persist_assets.rb b/app/interactors/persist_assets.rb index a82e4485..58cf0911 100755 --- a/app/interactors/persist_assets.rb +++ b/app/interactors/persist_assets.rb @@ -11,13 +11,11 @@ def call context.fail!(error: "Do not store assets for master version") if version.version == 'master' context.fail!(error: "#{version.version} has no config") if version.config.blank? context.fail!(error: "#{version.version} has no builds in config") if version.config['builds'].blank? - - puts "Copying assets for #{version.version} to S3" version.config['builds'].each do |build| if build['asset_url'].blank? - puts "**** Error in Source URL: #{build['asset_url']}" + context.error = "Error in Source URL: #{build['asset_url']}" next end @@ -29,7 +27,7 @@ def call #puts "******** URI #{release_asset.source_asset_url}" url = URI(release_asset.source_asset_url) rescue URI::Error => error - puts "******** URI error: #{release_asset.source_asset_url} - #{error.message}" + context.error = "URI error: #{release_asset.source_asset_url} - #{error.message}" next end @@ -42,9 +40,8 @@ def call # to update files in case they were changed. begin context.s3_bucket.object(key).delete - puts "Removed file from S3: #{key}" rescue Aws::S3::Errors::ServiceError => error - puts "****** S3 error: #{error.code} - #{error.message}" + context.error = "S3 error: #{error.code} - #{error.message}" end end @@ -54,17 +51,16 @@ def call rescue OpenURI::HTTPError => error status = error.io.status[0] message = error.io.status[1] - puts "****** file read error: #{status} - #{message}" + context.error = "file read error: #{status} - #{message}" next end begin context.s3_bucket.object(key).put(body: file, acl: 'public-read') - puts "File saved to S3: #{key}" uri = URI(context.s3_bucket.object(key).public_url) last_modified = context.s3_bucket.object(key).last_modified rescue Aws::S3::Errors::ServiceError => error - puts "****** S3 error: #{error.code} - #{error.message}" + context.error = "S3 error: #{error.code} - #{error.message}" next end @@ -74,7 +70,6 @@ def call # remove the bucket from the path if returned in that format # note that this does not change the host uri.path.gsub!(/#{ENV['AWS_S3_ASSETS_BUCKET']}\//, '') - puts "******** Updating vanity_url: #{uri.to_s}" end release_asset.update_columns(vanity_url: uri.to_s, last_modified: last_modified) diff --git a/app/interactors/scan_version_files.rb b/app/interactors/scan_version_files.rb new file mode 100755 index 00000000..a78f3899 --- /dev/null +++ b/app/interactors/scan_version_files.rb @@ -0,0 +1,91 @@ +class ScanVersionFiles + include Interactor + + delegate :extension, to: :context + delegate :version, to: :context + delegate :command_run, to: :context + + def call + context.command_run = CmdAtPath.new(extension.repo_path) + + # remove existing content items + version.extension_version_content_items.delete_all + + scan_config_yml_file + scan_yml_files + scan_class_dirs + + end + + private + + def scan_config_yml_file + + compilation_context = CompileGithubExtensionVersionConfig.call( + extension: extension, + version: version + ) + if compilation_context.success? && compilation_context.config_hash.present? + version.update_columns( + config: compilation_context.config_hash, + compilation_error: nil + ) + elsif compilation_context.error.present? + version.update_column(:compilation_error, compilation_context.error) + else + message = "Compile Github Extension Version #{version.version} Config failed." + version.update_column(:compilation_error, message) + context.fail!(error: message) + end + end + + def scan_yml_files + yml_files = command_run.cmd("find . -name '*.yml' -o -name '*.yaml'").split("\n") + + yml_files.each do |path| + + body = command_run.cmd("cat '#{path}'") + path = path.gsub("./", "") + + type = if body["MiqReport"] + "Report" + elsif body["MiqPolicySet"] + "Policy" + elsif body["MiqAlert"] + "Alert" + elsif body["dialog_tabs"] + "Dialog" + elsif body["MiqWidget"] + "Widget" + elsif body["CustomButtonSet"] + "Button Set" + end + + next if type.nil? + + version.extension_version_content_items.create( + path: path, + name: path.gsub(/.+\//, ""), + item_type: type, + github_url: extension.github_url + "/blob/#{version.version}/#{CGI.escape(path)}" + ) + + end + end + + def scan_class_dirs + dirs = command_run.cmd("find . -name '*.class'").split("\n") + + dirs.each do |path| + version.extension_version_content_items.create!( + path: path, + name: path.gsub(/.+\//, ""), + item_type: "Class", + github_url: extension.github_url + "/blob/#{version.version}/#{path}" + ) + end + end + + +end + \ No newline at end of file diff --git a/app/workers/setup_extension_web_hooks_worker.rb b/app/interactors/set_up_extension_web_hooks.rb similarity index 51% rename from app/workers/setup_extension_web_hooks_worker.rb rename to app/interactors/set_up_extension_web_hooks.rb index 6b19ed73..c14aadb4 100755 --- a/app/workers/setup_extension_web_hooks_worker.rb +++ b/app/interactors/set_up_extension_web_hooks.rb @@ -1,23 +1,26 @@ -class SetupExtensionWebHooksWorker < ApplicationWorker - - def perform(extension_id) - @extension = Extension.find(extension_id) - @octokit = @extension.octokit - - begin - @octokit.create_hook( - @extension.github_repo, "web", - { - url: Rails.application.routes.url_helpers.webhook_extension_url(@extension, username: @extension.owner_name), - content_type: "json" - }, - { - events: ["release", "watch", "member"], - active: true - } - ) - rescue Octokit::UnprocessableEntity - # Do nothing and continue if the hook already exists - end - end -end +class SetUpExtensionWebHooks + include Interactor + + delegate :extension, to: :context + + def call + begin + extension.octokit.create_hook( + extension.github_repo, + "web", + { + url: Rails.application.routes.url_helpers.webhook_extension_url(extension, username: extension.owner_name), + content_type: "json" + }, + { + events: ["release", "watch", "member"], + active: true + } + ) + rescue Octokit::UnprocessableEntity + # Do nothing and continue if the hook already exists + end + + end + +end \ No newline at end of file diff --git a/app/interactors/set_up_github_extension.rb b/app/interactors/set_up_github_extension.rb deleted file mode 100755 index a276fa92..00000000 --- a/app/interactors/set_up_github_extension.rb +++ /dev/null @@ -1,46 +0,0 @@ -# Call this service after creating a GitHub-hosted extension. -# This service sets up all of the information, webhooks, email notifications, etc -# for the new extension. - -class SetUpGithubExtension - include Interactor - - # The required context attributes: - delegate :extension, to: :context - delegate :octokit, to: :context - delegate :compatible_platforms, to: :context - - def call - platforms = compatible_platforms.select { |p| !p.strip.blank? } - CompileExtensionStatus.call( - extension: extension, - worker: 'CollectExtensionMetadataWorker', - job_id: CollectExtensionMetadataWorker.perform_async(extension.id, platforms) - ) - CompileExtensionStatus.call( - extension: extension, - worker: 'SetupExtensionWebHooksWorker', - job_id: SetupExtensionWebHooksWorker.perform_async(extension.id) - ) - CompileExtensionStatus.call( - extension: extension, - worker: 'NotifyModeratorsOfNewExtensionWorker', - job_id: NotifyModeratorsOfNewExtensionWorker.perform_async(extension.id) - ) - end - - def self.gather_github_info(extension, octokit, owner) - repo_info = octokit.repo(extension.github_repo) - org = repo_info[:organization] - - github_organization = if org - GithubOrganization.where(github_id: org[:id]).first_or_create!( - name: org[:login], - avatar_url: org[:avatar_url] - ) - end - owner_name = org ? org[:login] : owner.username - return github_organization, owner_name - end - -end diff --git a/app/interactors/sync_extension_contents_at_versions.rb b/app/interactors/sync_extension_contents_at_versions.rb new file mode 100755 index 00000000..812f700b --- /dev/null +++ b/app/interactors/sync_extension_contents_at_versions.rb @@ -0,0 +1,164 @@ +class SyncExtensionContentsAtVersions + include Interactor + + delegate :extension, to: :context + delegate :tag_names, to: :context + delegate :compatible_platforms, to: :context + delegate :release_infos_by_tag_name, to: :context + delegate :errored_tag_names, to: :context + delegate :command_run, to: :context + + def call + context.errored_tag_names = [] + context.command_run = CmdAtPath.new(extension.repo_path) + tag_names.each do |tag_name| + + if semver?(tag_name) + release_info = release_infos_by_tag_name[tag_name].to_h + CompileExtensionStatus.call( + extension: extension, + task: "Sync Extension Version #{tag_name}", + ) + + sync_extension_version(tag_name, release_info) + tally_commits if tag_name == "master" + + end # if semver? + + end # versions.each + + end + + private + + def semver?(tag_name) + begin + tag = SemverNormalizer.call(tag_name) + Semverse::Version.new(tag) + rescue Semverse::InvalidVersionFormat => error + unless errored_tag_names.include?(tag_name) + context.errored_tag_names << tag_name + compilation_error = [extension.compilation_error, error.message] + extension.update_column(:compilation_error, compilation_error.compact.join('; ')) + context.error = compilation_error.compact.join('; ') + message = "#{extension.lowercase_name} release is invalid: #{context.error}" + end + return false + end + true + end + + def sync_extension_version(tag_name, release_info) + checkout_correct_tag(tag_name) + readme_context = FetchReadmeAtVersion.call( + extension: extension, + tag_name: tag_name, + ) + + version_context = EnsureUpdatedVersion.call( + extension: extension, + tag_name: tag_name, + readme_body: readme_context.body, + readme_ext: readme_context.file_extension + ) + + set_compatible_platforms(version_context.version) + set_last_commit(version_context.version) + set_commit_count(version_context.version) + + scan_files_context = ScanVersionFiles.call( + extension: extension, + version: version_context.version + ) + + sync_release_info(version_context.version, release_info) + + override_context = CompileGithubOverrides.call( + extension: extension, + version: version_context.version + ) + + persist_context = PersistAssets.call( + version: version_context.version + ) + + update_annotations(version_context.version) + + end + + def set_compatible_platforms(version) + unless version.supported_platforms.any? + version.supported_platform_ids = compatible_platforms + end + rescue PG::UniqueViolation + end + + def set_last_commit(version) + commit = command_run.cmd("git log -1") + # no git log for this release + return if commit.blank? + + commit.gsub!(/^Merge: [^\n]+\n/, "") + sha, author, date = *commit.split("\n") + + message = commit.split("\n\n").last + # Empty repo; no commits + return if message.blank? + + message = message.gsub("\n", " ").strip + sha = sha.gsub("commit ", "") + date = Time.parse(date.gsub("Date:", "").strip) + version.update_columns( + last_commit_sha: sha, + last_commit_at: date, + last_commit_string: message, + last_commit_url: version.extension.github_url + "/commit/#{sha}" + ) + end + + def set_commit_count(version) + version.update_column(:commit_count, command_run.cmd("git shortlog | grep -E '^[ ]+\\w+' | wc -l").strip.to_i) + end + + def sync_release_info(version, release_info) + version.update_column(:release_notes, release_info['body']) + end + + def update_annotations(version) + config_hash = version.config + if config_hash['annotations'].present? + prefix = ExtensionVersion::ANNOTATION_PREFIX + updated_annotations = {} + config_hash['annotations'].each do |key, value| + key = "#{prefix}.#{key}" unless key.start_with?(prefix) + updated_annotations[key] = value + end + config_hash['annotations'] = updated_annotations + version.update_columns( + config: config_hash, + annotations: updated_annotations + ) + end + end + + def checkout_correct_tag(tag_name) + command_run.cmd("git checkout #{tag_name}") + command_run.cmd("git pull origin #{tag_name}") + end + + def tally_commits + commits = command_run.cmd("git --no-pager log --format='%H|%ad'") + + commits.split("\n").each do |c| + sha, date = c.split("|") + + CommitSha.transaction do + if !CommitSha.where(sha: sha).first + CommitSha.create(sha: sha) + DailyMetric.increment(@extension.commit_daily_metric_key, 1, date.to_date) + end + end + end + end + +end \ No newline at end of file diff --git a/app/interactors/sync_extension_repo.rb b/app/interactors/sync_extension_repo.rb new file mode 100755 index 00000000..1cd7c085 --- /dev/null +++ b/app/interactors/sync_extension_repo.rb @@ -0,0 +1,60 @@ + +class SyncExtensionRepo + include Interactor + + delegate :extension, to: :context + delegate :compatible_platforms, to: :context + + def call + + context.compatible_platforms ||= [] + + # clear previous error messages + extension.update_column(:compilation_error, '') + + begin + releases = extension.octokit.releases(extension.github_repo) + rescue Octokit::NotFound + message = 'Compile Github Extension failed as octokit failed to connect.' + extension.update_column(:compilation_error, message) + context.fail!(error: message) + end + + clone_repo + tag_names = extract_tag_names_from_releases(releases) + destroy_unreleased_versions(tag_names) + + if tag_names.blank? + message = 'Compile Github Extension failed as no releases were found.' + extension.update_column(:compilation_error, message) + context.fail!(error: message) + end + + release_infos_by_tag_name = releases.group_by {|release| release[:tag_name]}.transform_values {|arr| arr.first.to_h} + + SyncExtensionContentsAtVersions.call( + extension: extension, + tag_names: tag_names, + compatible_platforms: compatible_platforms, + release_infos_by_tag_name: release_infos_by_tag_name + ) + + end + + private + + def clone_repo + # Must clear any old repo as git will not clone to a non-empty directory + FileUtils.rm_rf(Dir["#{extension.repo_path}"]) + system("git clone #{extension.github_url} #{extension.repo_path}") + end + + def extract_tag_names_from_releases(releases) + releases.map { |r| r[:tag_name] } + end + + def destroy_unreleased_versions(tag_names) + extension.extension_versions.where.not(version: tag_names).destroy_all + end + +end \ No newline at end of file diff --git a/app/interactors/validate_new_extension.rb b/app/interactors/validate_new_extension.rb new file mode 100755 index 00000000..ba4a80ca --- /dev/null +++ b/app/interactors/validate_new_extension.rb @@ -0,0 +1,60 @@ +# Call this service before saving a new extension. +# This service validates the new extension. + +class ValidateNewExtension + include Interactor + + delegate :extension, to: :context + delegate :owner, to: :context + + def call + + context.fail!(error: I18n.t("extension.tmp_source_file_error")) unless extension.valid? + + if extension.hosted? + validate_tmp_source_file + else + validate_repo_collaborator + validate_config_file + end + + end + + def validate_tmp_source_file + unless extension.tmp_source_file.attached? + extension.errors.add(:tmp_source_file, I18n.t("extension.tmp_source_file_error")) + context.fail!(error: I18n.t("extension.tmp_source_file_error")) + end + end + + def validate_repo_collaborator + begin + valid = owner.octokit.collaborator?(extension.github_repo, owner.github_account.username) + rescue ArgumentError, Octokit::Unauthorized, Octokit::Forbidden + valid = false + end + + unless valid + extension.errors.add(:github_url, I18n.t("extension.github_url_format_error")) + context.fail!(error: I18n.t("extension.github_url_format_error")) + end + end + + def validate_config_file + config_file_names = Extension::CONFIG_FILE_NAMES + begin + repo_top_level_file_names = owner.octokit.contents(extension.github_repo).map { |h| h[:name] } + valid = (repo_top_level_file_names & config_file_names).present? + rescue ArgumentError, Octokit::Unauthorized, Octokit::Forbidden + valid = false + end + + unless valid + allowed_config_file_names = config_file_names.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ') + message = I18n.t("extension.missing_config_file", allowed_config_file_names: allowed_config_file_names) + extension.errors.add(:github_url, message) + context.fail!(error: message) + end + end + +end \ No newline at end of file diff --git a/app/models/collaborator.rb b/app/models/collaborator.rb index 82e1cc37..c10f8497 100755 --- a/app/models/collaborator.rb +++ b/app/models/collaborator.rb @@ -14,9 +14,35 @@ class Collaborator < ApplicationRecord # Associations # -------------------- attr_accessor :user_ids + class << self + # + # Returns the ineligible users for collaboration for a given resource. + # + def ineligible_collaborators_for(resource) + [resource.collaborator_users, resource.owner].flatten + end + + # + # Returns the ineligible users for ownership for a given resource. + # + def ineligible_owners_for(resource) + [resource.owner] + end + + # + # returns records for user + # + def includes_user(username) + user_ids = Account.for_provider.with_username(username).pluck(:user_id) + where(user_id: user_ids) + end + + end # class << self + def resourceable_with_unscoped resourceable_type.constantize.unscoped { resourceable_without_unscoped } end + # alias_method_chain :resourceable, :unscoped # @@ -31,17 +57,4 @@ def transfer_ownership end end - # - # Returns the ineligible users for collaboration for a given resource. - # - def self.ineligible_collaborators_for(resource) - [resource.collaborator_users, resource.owner].flatten - end - - # - # Returns the ineligible users for ownership for a given resource. - # - def self.ineligible_owners_for(resource) - [resource.owner] - end end diff --git a/app/models/extension_follower.rb b/app/models/extension_follower.rb index b83fd870..b1335805 100755 --- a/app/models/extension_follower.rb +++ b/app/models/extension_follower.rb @@ -9,4 +9,16 @@ class ExtensionFollower < ApplicationRecord validates :extension, presence: true validates :user, presence: true validates :extension_id, uniqueness: { scope: :user_id } + + class << self + + # + # returns records for user + # + def includes_user(username) + user_ids = Account.for_provider.with_username(username).pluck(:user_id) + where(user_id: user_ids) + end + + end # class << self end diff --git a/app/models/extension_version_content_item.rb b/app/models/extension_version_content_item.rb index c8538d2a..49f052cf 100755 --- a/app/models/extension_version_content_item.rb +++ b/app/models/extension_version_content_item.rb @@ -1,3 +1,4 @@ class ExtensionVersionContentItem < ApplicationRecord belongs_to :extension_version, required: false + end diff --git a/app/services/add_extension_collaborator.rb b/app/services/add_extension_collaborator.rb deleted file mode 100755 index 3ec4a92a..00000000 --- a/app/services/add_extension_collaborator.rb +++ /dev/null @@ -1,13 +0,0 @@ -class AddExtensionCollaborator - def initialize(extension, github_user) - @extension = extension - @github_user = github_user - end - - def process! - ActiveRecord::Base.transaction do - user, account = EnsureGithubUserAndAccount.new(@github_user).process! - Collaborator.create(user: user, resourceable: @extension) - end - end -end diff --git a/app/services/create_extension.rb b/app/services/create_extension.rb deleted file mode 100755 index c70a786b..00000000 --- a/app/services/create_extension.rb +++ /dev/null @@ -1,98 +0,0 @@ -class CreateExtension - - def initialize(params, user) - @params = params - @compatible_platforms = params[:compatible_platforms] || [] - @version_name = params[:version].presence || '0.0.1' - @user = user - @octokit = user.octokit - end - - def process! - candidate = Extension.new(@params) do |extension| - if extension.hosted? - extension.owner = User.host_organization - extension.owner_name = ENV['HOST_ORGANIZATION'] - else - extension.github_organization, extension.owner_name = SetUpGithubExtension.gather_github_info(extension, @octokit, @user) - extension.owner = @user - end - end - - if validate(candidate, @octokit, @user) - candidate.save - postprocess(candidate, @octokit, @compatible_platforms, @version_name) - elsif !candidate.hosted? - existing = Extension.unscoped.where(enabled: false, github_url: candidate.github_url).first - if existing - # A disabled extension is available for re-use, but it must first be re-enabled. - existing.update_attribute(:enabled, true) - return existing - end - end - - return candidate - end - - private - - def postprocess(extension, octokit, compatible_platforms, version_name) - CompileExtensionStatusClear.call(extension: extension) - if extension.hosted? - SetUpHostedExtension.call(extension: extension, version_name: version_name) - else - SetUpGithubExtension.call(extension: extension, octokit: octokit, compatible_platforms: compatible_platforms) - end - end - - def validate(extension, octokit, user) - return false if !extension.valid? - - if extension.hosted? - extension.tmp_source_file.attached? - else - repo_valid?(extension, octokit, user) - end - end - - def repo_valid?(extension, octokit, user) - validatate_repo_collaborator(extension, octokit, user) && - validate_config_file(extension, octokit) - end - - def validatate_repo_collaborator(extension, octokit, user) - begin - result = octokit.collaborator?(extension.github_repo, user.github_account.username) - rescue ArgumentError, Octokit::Unauthorized, Octokit::Forbidden - result = false - end - - if !result - extension.errors.add(:github_url, I18n.t("extension.github_url_format_error")) - end - - result - end - - def validate_config_file(extension, octokit) - config_file_names = Extension::CONFIG_FILE_NAMES - - begin - repo_top_level_file_names = octokit.contents(extension.github_repo).map { |h| h[:name] } - top_level_config_file_names = repo_top_level_file_names & config_file_names - result = top_level_config_file_names.any? - rescue ArgumentError, Octokit::Unauthorized, Octokit::Forbidden - result = false - end - - if !result - allowed_config_file_names_str = config_file_names.to_sentence(last_word_connector: ', or ', - two_words_connector: ' or ') - extension.errors.add(:github_url, - I18n.t("extension.missing_config_file", - allowed_config_file_names: allowed_config_file_names_str)) - end - - result - end -end diff --git a/app/services/ensure_github_user_and_account.rb b/app/services/ensure_github_user_and_account.rb deleted file mode 100755 index c3326e6a..00000000 --- a/app/services/ensure_github_user_and_account.rb +++ /dev/null @@ -1,42 +0,0 @@ -class EnsureGithubUserAndAccount - def initialize(github_user) - @github_user = github_user - end - - def process! - # NOTE: This is very similar to User.find_or_create_from_github_oauth, - # except we're using the user object obtained from the GitHub API rather - # than OAuth. Also, we must save the account without OAuth fields. We - # ought to come back to this and DRY it up... - - account = Account.where( - username: @github_user[:login], - provider: "github" - ).first_or_initialize - - @github_user[:name] ||= "Unknown Name" - - if @github_user[:name].include?(" ") - split = @github_user[:name].split(" ") - first_name = split[0] - last_name = split[1] - else - first_name = @github_user[:name] - last_name = nil - end - - if !account.user_id or !User.unscoped.where(id: account.user_id).first - account.user ||= User.new( - first_name: first_name, - last_name: last_name, - email: @github_user[:email], - avatar_url: @github_user[:avatar_url], - ) - account.user.save(validate: false) - end - - account.save(validate: false) - - return account.user, account - end -end diff --git a/app/views/extensions/directory.html.erb b/app/views/extensions/directory.html.erb index f65e8dc0..5d959473 100755 --- a/app/views/extensions/directory.html.erb +++ b/app/views/extensions/directory.html.erb @@ -50,7 +50,7 @@
Assets are shareable, reusable packages that make it easy to deploy Sensu plugins. You can use assets to provide the plugins, libraries, and runtimes you need to automate your monitoring workflows. Log in with your GitHub account to share your assets on Bonsai.
+Assets are shareable, reusable packages that make it easy to deploy Sensu plugins. You can use assets to provide the plugins, libraries, and runtimes you need to automate your monitoring workflows. Log in with your GitHub account to <%= link_to 'share your assets', new_extension_path, rel: 'nofollow' %> on Bonsai.