mirror of
https://github.com/czhu12/canine.git
synced 2026-01-06 11:40:44 -06:00
added generic git client rewrite to support both github and gitlab
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.vscode/
|
||||
coverage/
|
||||
workdir/
|
||||
.DS_Store
|
||||
|
||||
@@ -43,15 +43,15 @@ module Projects
|
||||
|
||||
def self.create_steps(provider)
|
||||
steps = []
|
||||
if provider.github?
|
||||
steps << Projects::ValidateGithubRepository
|
||||
if provider.git?
|
||||
steps << Projects::ValidateGitRepository
|
||||
end
|
||||
|
||||
steps << Projects::Save
|
||||
|
||||
# Only register webhook in non-local mode
|
||||
if !Rails.application.config.local_mode && provider.github?
|
||||
steps << Projects::RegisterGithubWebhook
|
||||
if !Rails.application.config.local_mode && provider.git?
|
||||
steps << Projects::RegisterGitWebhook
|
||||
end
|
||||
steps
|
||||
end
|
||||
|
||||
@@ -12,8 +12,8 @@ class Projects::DeployLatestCommit
|
||||
current_user = context.current_user || project.account.owner
|
||||
if project.github?
|
||||
project_credential_provider = project.project_credential_provider
|
||||
client = Github::Client.from_project(project)
|
||||
commit = client.commits.first
|
||||
client = Git::Client.from_project(project)
|
||||
commit = client.commits(project.branch).first
|
||||
build = project.builds.create!(
|
||||
commit_sha: commit.sha,
|
||||
commit_message: commit.commit[:message],
|
||||
|
||||
12
app/actions/projects/register_git_webhook.rb
Normal file
12
app/actions/projects/register_git_webhook.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
class Projects::RegisterGitWebhook
|
||||
extend LightService::Action
|
||||
expects :project
|
||||
|
||||
executed do |context|
|
||||
client = Git::Client.from_project(context.project)
|
||||
client.register_webhook!
|
||||
rescue StandardError => e
|
||||
context.project.errors.add(:repository_url, "Failed to create webhook: #{e.message}")
|
||||
context.fail_and_return!("Failed to create webhook: #{e.message}")
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
class Projects::RegisterGithubWebhook
|
||||
extend LightService::Action
|
||||
expects :project
|
||||
|
||||
executed do |context|
|
||||
client = Github::Client.from_project(context.project)
|
||||
client.register_webhook!
|
||||
rescue Octokit::UnprocessableEntity => e
|
||||
next context if e.message.include?("Hook already exists")
|
||||
context.fail_and_return!("Failed to create webhook")
|
||||
end
|
||||
end
|
||||
@@ -1,12 +1,12 @@
|
||||
class Projects::ValidateGithubRepository
|
||||
class Projects::ValidateGitRepository
|
||||
extend LightService::Action
|
||||
|
||||
expects :project, :project_credential_provider
|
||||
|
||||
executed do |context|
|
||||
# The project is not created yet, so we can't call Github::Client.from_project
|
||||
client = Github::Client.new(
|
||||
access_token: context.project_credential_provider.access_token,
|
||||
client = Git::Client.from_provider(
|
||||
provider: context.project_credential_provider.provider,
|
||||
repository_url: context.project.repository_url
|
||||
)
|
||||
unless client.repository_exists?
|
||||
25
app/controllers/inbound_webhooks/gitlab_controller.rb
Normal file
25
app/controllers/inbound_webhooks/gitlab_controller.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module InboundWebhooks
|
||||
class GithubController < ApplicationController
|
||||
before_action :verify_event
|
||||
|
||||
def create
|
||||
# Save webhook to database
|
||||
record = InboundWebhook.create(body: payload)
|
||||
|
||||
# Queue webhook for processing
|
||||
InboundWebhooks::GithubJob.perform_later(record, current_user:)
|
||||
|
||||
# Tell service we received the webhook successfully
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_event
|
||||
payload = request.body.read
|
||||
# TODO: Verify the event was sent from the service
|
||||
# Render `head :bad_request` if verification fails
|
||||
secret = Gitlab::Client::GITLAB_WEBHOOK_SECRET
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -37,7 +37,7 @@ class ProjectsController < ApplicationController
|
||||
|
||||
# GET /projects/1/edit
|
||||
def edit
|
||||
@client = Github::Client.from_project(@project)
|
||||
@client = Git::Client.from_project(@project)
|
||||
end
|
||||
|
||||
# POST /projects or /projects.json
|
||||
|
||||
39
app/jobs/inbound_webhooks/gitlab_job.rb
Normal file
39
app/jobs/inbound_webhooks/gitlab_job.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
module InboundWebhooks
|
||||
class GitlabJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(inbound_webhook, current_user: nil)
|
||||
inbound_webhook.processing!
|
||||
|
||||
# Process webhook
|
||||
# Determine the project
|
||||
# Trigger a docker build & docker deploy if auto deploy is on for the project
|
||||
body = JSON.parse(inbound_webhook.body)
|
||||
process_webhook(body, current_user:)
|
||||
|
||||
inbound_webhook.processed!
|
||||
|
||||
# Or mark as failed and re-enqueue the job
|
||||
# inbound_webhook.failed!
|
||||
end
|
||||
|
||||
def process_webhook(body, current_user:)
|
||||
projects = Project.where(
|
||||
"LOWER(repository_url) = ?",
|
||||
body["repository"]["full_name"].downcase,
|
||||
).where(
|
||||
"LOWER(branch) = ?",
|
||||
branch.downcase,
|
||||
).where(autodeploy: true)
|
||||
projects.each do |project|
|
||||
# Trigger a docker build & docker deploy
|
||||
build = project.builds.create!(
|
||||
current_user:,
|
||||
commit_sha: body["head_commit"]["id"],
|
||||
commit_message: body["head_commit"]["message"]
|
||||
)
|
||||
Projects::BuildJob.perform_later(build)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -21,7 +21,7 @@ class Projects::DestroyJob < ApplicationJob
|
||||
end
|
||||
|
||||
def remove_github_webhook(project)
|
||||
client = Github::Client.from_project(project)
|
||||
client = Git::Client.from_project(project)
|
||||
client.remove_webhook!
|
||||
rescue Octokit::NotFound
|
||||
# If the hook is not found, do nothing
|
||||
|
||||
@@ -63,7 +63,7 @@ class Project < ApplicationRecord
|
||||
deployed: 1,
|
||||
destroying: 2
|
||||
}
|
||||
delegate :github?, :gitlab?, to: :project_credential_provider
|
||||
delegate :git?, :github?, :gitlab?, to: :project_credential_provider
|
||||
delegate :docker_hub?, to: :project_credential_provider
|
||||
|
||||
def name_is_unique_to_cluster
|
||||
|
||||
@@ -30,6 +30,7 @@ class ProjectCredentialProvider < ApplicationRecord
|
||||
delegate :github?, to: :provider
|
||||
delegate :docker_hub?, to: :provider
|
||||
delegate :gitlab?, to: :provider
|
||||
delegate :git?, to: :provider
|
||||
|
||||
def github_username
|
||||
JSON.parse(provider.auth)["info"]["nickname"]
|
||||
|
||||
@@ -51,6 +51,10 @@ class Provider < ApplicationRecord
|
||||
JSON.parse(auth)["info"]["nickname"] || JSON.parse(auth)["info"]["username"]
|
||||
end
|
||||
|
||||
def git?
|
||||
github? || gitlab?
|
||||
end
|
||||
|
||||
def registry
|
||||
if github?
|
||||
"ghcr.io"
|
||||
|
||||
21
app/services/git/client.rb
Normal file
21
app/services/git/client.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Git::Client
|
||||
def self.from_provider(provider:, repository_url:)
|
||||
if provider.github?
|
||||
Git::Github::Client.new(access_token: provider.access_token, repository_url:)
|
||||
elsif provider.gitlab?
|
||||
Git::Gitlab::Client.new(access_token:provider.access_token, repository_url:)
|
||||
else
|
||||
raise "Unsupported Git provider: #{provider}"
|
||||
end
|
||||
end
|
||||
|
||||
def self.from_project(project)
|
||||
if project.project_credential_provider.provider.github?
|
||||
Git::Github::Client.from_project(project)
|
||||
elsif project.project_credential_provider.provider.gitlab?
|
||||
Git::Gitlab::Client.from_project(project)
|
||||
else
|
||||
raise "Unsupported Git provider: #{project.project_credential_provider.provider}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
class Github::Client
|
||||
class Git::Github::Client < Git::Client
|
||||
WEBHOOK_SECRET = ENV["OMNIAUTH_GITHUB_WEBHOOK_SECRET"]
|
||||
|
||||
attr_accessor :client, :repository_url
|
||||
@@ -10,8 +10,8 @@ class Github::Client
|
||||
)
|
||||
end
|
||||
|
||||
def commits
|
||||
client.commits(repository_url)
|
||||
def commits(branch)
|
||||
client.commits(repository_url, branch)
|
||||
end
|
||||
|
||||
def initialize(access_token:, repository_url:)
|
||||
@@ -31,6 +31,10 @@ class Github::Client
|
||||
end
|
||||
|
||||
def register_webhook!
|
||||
if webhook_exists?
|
||||
return
|
||||
end
|
||||
|
||||
client.create_hook(
|
||||
repository_url,
|
||||
"web",
|
||||
96
app/services/git/gitlab/client.rb
Normal file
96
app/services/git/gitlab/client.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
class Git::Gitlab::Client < Git::Client
|
||||
GITLAB_API_BASE = "https://gitlab.com/api/v4"
|
||||
GITLAB_WEBHOOK_SECRET = ENV["GITLAB_WEBHOOK_SECRET"]
|
||||
attr_accessor :access_token, :repository_url
|
||||
|
||||
def self.from_project(project)
|
||||
raise "Project is not a GitLab project" unless project.project_credential_provider.provider.gitlab?
|
||||
new(
|
||||
access_token: project.project_credential_provider.access_token,
|
||||
repository_url: project.repository_url
|
||||
)
|
||||
end
|
||||
|
||||
def initialize(access_token:, repository_url:)
|
||||
@access_token = access_token
|
||||
@repository_url = repository_url
|
||||
end
|
||||
|
||||
def repository_exists?
|
||||
repository.present?
|
||||
end
|
||||
|
||||
def commits(branch)
|
||||
HTTParty.get(
|
||||
"#{GITLAB_API_BASE}/projects/#{encoded_url}/repository/commits?ref=#{branch}",
|
||||
headers: { "Authorization" => "Bearer #{access_token}" }
|
||||
)
|
||||
end
|
||||
|
||||
def can_write_webhooks?
|
||||
true
|
||||
end
|
||||
|
||||
def register_webhook!
|
||||
if webhook_exists?
|
||||
return
|
||||
end
|
||||
response = HTTParty.post(
|
||||
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks",
|
||||
headers: { "Authorization" => "Bearer #{access_token}", "Content-Type" => "application/json" },
|
||||
body: {
|
||||
url: Rails.application.routes.url_helpers.inbound_webhooks_gitlab_index_url,
|
||||
name: "canine-webhook",
|
||||
push_events: true,
|
||||
enable_ssl_verification: true,
|
||||
token: GITLAB_WEBHOOK_SECRET
|
||||
}.to_json
|
||||
)
|
||||
unless response.success?
|
||||
raise "Failed to register webhook: #{response.body}"
|
||||
end
|
||||
response.parsed_response
|
||||
end
|
||||
|
||||
def webhooks
|
||||
response = HTTParty.get(
|
||||
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks",
|
||||
headers: { "Authorization" => "Bearer #{access_token}" },
|
||||
format: :json
|
||||
)
|
||||
end
|
||||
|
||||
def encoded_url
|
||||
URI.encode_www_form_component(repository_url)
|
||||
end
|
||||
|
||||
def repository
|
||||
@repository ||= begin
|
||||
project_response = HTTParty.get(
|
||||
"#{GITLAB_API_BASE}/projects/#{encoded_url}",
|
||||
headers: { "Authorization" => "Bearer #{access_token}" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def access_token
|
||||
@access_token
|
||||
end
|
||||
|
||||
def webhook_exists?
|
||||
webhook.present?
|
||||
end
|
||||
|
||||
def webhook
|
||||
webhooks.find { |h| h['url'].include?(Rails.application.routes.url_helpers.inbound_webhooks_gitlab_index_path) }
|
||||
end
|
||||
|
||||
def remove_webhook!
|
||||
if webhook_exists?
|
||||
HTTParty.delete(
|
||||
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks/#{webhook['id']}",
|
||||
headers: { "Authorization" => "Bearer #{access_token}" }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ class Async::Github::WebhookStatusViewModel < Async::BaseViewModel
|
||||
expects :project_id
|
||||
|
||||
def client
|
||||
@client ||= Github::Client.from_project(project)
|
||||
@client ||= Git::Client.from_project(project)
|
||||
end
|
||||
|
||||
def project
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if project.docker_hub? %>
|
||||
<%= render "projects/update/edit_form_docker_hub", project: %>
|
||||
<% else %>
|
||||
<%= render "projects/update/edit_form_github", project: %>
|
||||
<%= render "projects/update/edit_form_git", project: %>
|
||||
<% end %>
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
<% end %>
|
||||
<div class="text-sm">
|
||||
<%= link_to project.full_repository_url, target: "_blank" do %>
|
||||
<% if project.github? %>
|
||||
<iconify-icon icon="lucide:github"></iconify-icon>
|
||||
<% if project.git? %>
|
||||
<% if project.github? %>
|
||||
<iconify-icon icon="lucide:github"></iconify-icon>
|
||||
<% elsif project.gitlab? %>
|
||||
<iconify-icon icon="lucide:gitlab"></iconify-icon>
|
||||
<% end %>
|
||||
<span class="underline mr-2"><%= project.repository_url %></span>
|
||||
<iconify-icon icon="lucide:git-branch"></iconify-icon>
|
||||
<span class="underline"><%= project.branch %></span>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
</label>
|
||||
<%= form.text_field :container_registry_url, class: "input input-bordered w-full focus:outline-offset-0", value: form.object.attributes["container_registry_url"] %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">If this is left blank, Github Container Registry will be used</span>
|
||||
<span class="label-text-alt">If this is left blank, <%= project.github? ? "Github" : "Gitlab" %> Container Registry will be used</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<div class="flex items-start gap-2">
|
||||
<% if provider.github? %>
|
||||
<iconify-icon icon="mdi:github" width="24" height="24"></iconify-icon>
|
||||
<% elsif provider.gitlab? %>
|
||||
<iconify-icon icon="mdi:gitlab" width="24" height="24"></iconify-icon>
|
||||
<% else %>
|
||||
<iconify-icon icon="mdi:docker" width="24" height="24"></iconify-icon>
|
||||
<% end %>
|
||||
@@ -9,7 +11,7 @@
|
||||
<span class="text-sm">
|
||||
Connected as <b><%= provider.username %></b>
|
||||
</span>
|
||||
<% if defined?(@project) && @project.github? %>
|
||||
<% if defined?(@project) && @project.git? %>
|
||||
<div class="mt-2">
|
||||
<%= render(
|
||||
"shared/partials/async_renderer",
|
||||
|
||||
@@ -17,6 +17,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
namespace :inbound_webhooks do
|
||||
resources :github, controller: :github, only: [ :create ]
|
||||
resources :gitlab, controller: :gitlab, only: [ :create ]
|
||||
end
|
||||
get "/privacy", to: "static#privacy"
|
||||
get "/terms", to: "static#terms"
|
||||
|
||||
Reference in New Issue
Block a user