added docker hub project creation

This commit is contained in:
Chris Zhu
2025-02-26 23:23:20 -08:00
parent 068bb2daf1
commit bb28038368
29 changed files with 491 additions and 111 deletions

View File

@@ -7,5 +7,6 @@
- [ ] Support login without Github
- [ ] Support organization projects
- [ ] Support dockerhub deployments
- [ ] Review Dockerfile.dev to be faster
- [ ] Migrate to goodjob for queues
- [ ] Migrate to goodjob for queues
- [ ] Add skeleton loader for processes page
- [ ] Migrate to goodjob to support scheduled jobs without a ton of separate gems

View File

@@ -3,21 +3,63 @@
module Projects
class Create
extend LightService::Organizer
def self.create_params(params)
params.require(:project).permit(
:name,
:repository_url,
:branch,
:cluster_id,
:docker_build_context_directory,
:docker_command,
:dockerfile_path,
:container_registry_url
)
end
def self.call(project, params, user)
steps = [
Projects::ValidateGithubRepository,
Projects::Save
]
def self.call(
params,
user
)
project = Project.new(create_params(params))
provider = find_provider(user, params)
project_credential_provider = create_project_credential_provider(project, provider)
# Only register webhook in non-local mode
unless Rails.application.config.local_mode
steps << Projects::RegisterGithubWebhook
steps = create_steps(provider)
with(
project:,
project_credential_provider:,
params:,
user:
).reduce(*steps)
end
def self.create_project_credential_provider(project, provider)
ProjectCredentialProvider.new(
project:,
provider:,
)
end
def self.create_steps(provider)
steps = []
if provider.github?
steps << Projects::ValidateGithubRepository
end
steps << Projects::DeployLatestCommit
steps << Projects::Save
with(project:, params:, user:).reduce(*steps)
# Only register webhook in non-local mode
if !Rails.application.config.local_mode && provider.github?
steps << Projects::RegisterGithubWebhook
end
steps
end
def self.find_provider(user, params)
provider_id = params[:project][:project_credential_provider][:provider_id]
user.providers.find(provider_id)
rescue ActiveRecord::RecordNotFound
raise "Provider #{provider_id} not found"
end
end
end

View File

@@ -9,14 +9,23 @@ class Projects::DeployLatestCommit
# Fetch the latest commit from the default branch
project = context.project
current_user = context.current_user || project.account.owner
client = Octokit::Client.new(access_token: project.github_access_token)
commit = client.commits(project.repository_url).first
if project.github?
project_credential_provider = project.project_credential_provider
client = Github::Client.from_project(project)
commit = client.commits.first
build = project.builds.create!(
commit_sha: commit.sha,
commit_message: commit.commit[:message],
current_user:
)
else
build = project.builds.create!(
commit_sha: "latest",
commit_message: "Deploying from #{project.repository_url}",
current_user:
)
end
build = project.builds.create!(
commit_sha: commit.sha,
commit_message: commit.commit[:message],
current_user: current_user
)
Projects::BuildJob.perform_later(build)
end
end

View File

@@ -1,10 +1,9 @@
class Projects::RegisterGithubWebhook
extend LightService::Action
expects :project
promises :project
executed do |context|
client = Github::Client.new(context.project)
client = Github::Client.from_project(context.project)
client.register_webhook!
rescue Octokit::UnprocessableEntity => e
next context if e.message.include?("Hook already exists")

View File

@@ -1,14 +1,14 @@
class Projects::Save
extend LightService::Action
expects :project, :user
expects :project, :project_credential_provider
promises :project
executed do |context|
ActiveRecord::Base.transaction do
context.project.repository_url = context.project.repository_url.strip.downcase
context.project.save!
ProjectCredentialProvider.create!(project: context.project, provider: context.user.github_provider)
context.project_credential_provider.save!
end
rescue => e
context.fail_and_return!(e.message)

View File

@@ -1,11 +1,14 @@
class Projects::ValidateGithubRepository
extend LightService::Action
expects :project
promises :project
expects :project, :project_credential_provider
executed do |context|
client = Github::Client.new(context.project)
# 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,
repository_url: context.project.repository_url
)
unless client.repository_exists?
context.project.errors.add(:repository_url, 'does not exist')
context.fail_and_return!('Repository does not exist')

View File

@@ -0,0 +1,31 @@
class Providers::GenerateConfigJson
extend LightService::Action
expects :provider
promises :docker_config_json
executed do |context|
registry = context.provider.registry
context.docker_config_json = create_docker_json_structure(
context.provider.username,
context.provider.access_token,
registry,
)
end
def self.create_docker_json_structure(username, password, registry)
# First base64 encoding
auth_value = Base64.strict_encode64("#{username}:#{password}")
# Create the JSON structure
docker_config = {
"auths" => {
registry => {
"auth" => auth_value
}
}
}
# Second base64 encoding of the entire JSON
Base64.strict_encode64(JSON.generate(docker_config))
end
end

View File

@@ -28,6 +28,10 @@ class ProjectsController < ApplicationController
# GET /projects/new
def new
selected_provider = params[:provider] || Provider::GITHUB_PROVIDER
@providers = current_user.providers.where(provider: selected_provider)
# Temporary hack
@provider = @providers.first
@project = Project.new
# Uncomment to authorize with Pundit
@@ -36,12 +40,12 @@ class ProjectsController < ApplicationController
# GET /projects/1/edit
def edit
@client = Github::Client.new(@project)
@client = Github::Client.from_project(@project)
end
# POST /projects or /projects.json
def create
result = Projects::Create.call(Project.new(project_params), params, current_user)
result = Projects::Create.call(params, current_user)
@project = result.project
respond_to do |format|
@@ -91,15 +95,6 @@ class ProjectsController < ApplicationController
# Only allow a list of trusted parameters through.
def project_params
params.require(:project).permit(
:name,
:repository_url,
:branch,
:cluster_id,
:docker_build_context_directory,
:docker_command,
:dockerfile_path,
:container_registry_url
)
Projects::Create.create_params(params)
end
end

View File

@@ -8,13 +8,19 @@ class Projects::BuildJob < ApplicationJob
def perform(build)
project = build.project
project.project_credential_provider.used!
# If its a dockerhub deploy, we don't need to build the docker image
if project.github?
project_credential_provider = project.project_credential_provider
project_credential_provider.used!
clone_repository_and_build_docker(project, build)
clone_repository_and_build_docker(project, build)
login_to_docker(project, build)
login_to_docker(project_credential_provider, build)
push_to_dockerhub(project, build)
push_to_github_container_registry(project, build)
else
build.info("Skipping build for #{project.name} because it's a Docker Hub deploy")
end
complete_build!(build)
# TODO: Step 7: Optionally, add post-deploy tasks or slack notifications
@@ -26,7 +32,8 @@ class Projects::BuildJob < ApplicationJob
private
def project_git(project)
"https://#{project.github_username}:#{project.github_access_token}@github.com/#{project.repository_url}.git"
project_credential_provider = project.project_credential_provider
"https://#{project_credential_provider.username}:#{project_credential_provider.access_token}@github.com/#{project.repository_url}.git"
end
def git_clone(project, build, repository_path)
@@ -39,7 +46,7 @@ class Projects::BuildJob < ApplicationJob
raise BuildFailure, "Failed to clone repository: #{stderr}" unless status.success?
build.success("Repository cloned successfully.")
build.success("Repository cloned successfully to #{repository_path}.")
end
def build_docker_build_command(project, repository_path)
@@ -72,21 +79,21 @@ class Projects::BuildJob < ApplicationJob
raise BuildFailure, e.message
end
def login_to_docker(project, build)
def login_to_docker(project_credential_provider, build)
docker_login_command = %w[docker login ghcr.io --username] +
[ project.github_username, "--password", project.github_access_token ]
[ project_credential_provider.username, "--password", project_credential_provider.access_token ]
build.info("Logging into ghcr.io as #{project.github_username}", color: :yellow)
build.info("Logging into ghcr.io as #{project_credential_provider.username}", color: :yellow)
_stdout, stderr, status = Open3.capture3(*docker_login_command)
if status.success?
build.success("Logged in to Docker Hub successfully.")
build.success("Logged in to Github Container Registry successfully.")
else
build.error("Docker Hub login failed with error:\n#{stderr}")
build.error("Github Container Registry login failed with error:\n#{stderr}")
end
end
def push_to_dockerhub(project, build)
def push_to_github_container_registry(project, build)
docker_push_command = [ "docker", "push", project.container_registry_url ]
build.info("Pushing Docker image to #{docker_push_command.last}", color: :yellow)
@@ -94,7 +101,7 @@ class Projects::BuildJob < ApplicationJob
raise BuildFailure, "Docker push failed for project #{project.name} with error:\n#{stderr}" unless status.success?
build.success("Docker image pushed successfully for project #{project.name}:\n#{stdout}")
build.success("Docker image pushed to `#{project.container_registry_url}` successfully for project #{project.name}:\n#{stdout}")
end
def complete_build!(build)

View File

@@ -98,6 +98,7 @@ class Projects::DeploymentJob < ApplicationJob
end
def apply_namespace(project, kubectl)
@logger.info("Creating namespace: #{project.name}", color: :yellow)
namespace_yaml = K8::Namespace.new(project).to_yaml
kubectl.apply_yaml(namespace_yaml)
end
@@ -135,29 +136,14 @@ class Projects::DeploymentJob < ApplicationJob
end
def upload_registry_secrets(kubectl, deployment)
@logger.info("Creating registry secret for #{project.container_registry_url}", color: :yellow)
project = deployment.project
docker_config_json = create_docker_config_json(
project.github_username,
project.github_access_token,
result = Providers::GenerateConfigJson.execute(
provider: project.project_credential_provider.provider,
)
secret_yaml = K8::Secrets::RegistrySecret.new(project, docker_config_json).to_yaml
raise StandardError, result.message if result.failure?
secret_yaml = K8::Secrets::RegistrySecret.new(project, result.docker_config_json).to_yaml
kubectl.apply_yaml(secret_yaml)
end
def create_docker_config_json(username, password)
# First base64 encoding
auth_value = Base64.strict_encode64("#{username}:#{password}")
# Create the JSON structure
docker_config = {
"auths" => {
"ghcr.io" => {
"auth" => auth_value
}
}
}
# Second base64 encoding of the entire JSON
Base64.strict_encode64(JSON.generate(docker_config))
end
end

View File

@@ -1,20 +1,28 @@
class Projects::DestroyJob < ApplicationJob
def perform(project)
project.destroying!
client = K8::Client.from_cluster(project.cluster)
if (namespace = client.get_namespaces.find { |n| n.metadata.name == project.name }).present?
client.delete_namespace(namespace.metadata.name)
end
delete_namespace(project)
# Delete the github webhook for the project IF there are no more projects that refer to that repository
unless Project.where(repository_url: project.repository_url).where.not(id: project.id).exists?
# TODO: This might have overlapping repository urls across different providers.
# Need to check for provider uniqueness
unless Project.where(
repository_url: project.repository_url,
).where.not(id: project.id).exists?
remove_github_webhook(project)
end
project.destroy!
end
def delete_namespace(project)
client = K8::Client.from_cluster(project.cluster)
if (namespace = client.get_namespaces.find { |n| n.metadata.name == project.name }).present?
client.delete_namespace(namespace.metadata.name)
end
end
def remove_github_webhook(project)
client = Github::Client.new(project)
client = Github::Client.from_project(project)
client.remove_webhook!
rescue Octokit::NotFound
# If the hook is not found, do nothing

View File

@@ -47,6 +47,7 @@ class Project < ApplicationRecord
with: /\A[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]\/[a-zA-Z0-9._-]+\z/,
message: "must be in the format 'owner/repository'"
}
validates :project_credential_provider, presence: true
validate :name_is_unique_to_cluster, on: :create
after_save_commit do
@@ -62,6 +63,8 @@ class Project < ApplicationRecord
deployed: 1,
destroying: 2
}
delegate :github?, to: :project_credential_provider
delegate :docker_hub?, to: :project_credential_provider
def name_is_unique_to_cluster
if cluster.namespaces.include?(name)
@@ -97,21 +100,12 @@ class Project < ApplicationRecord
"https://github.com/#{repository_url}"
end
def github_provider
project_credential_provider&.provider || account.github_provider
end
def github_username
JSON.parse(github_provider.auth)["info"]["nickname"]
end
def github_access_token
github_provider.access_token
def provider
project_credential_provider&.provider
end
def container_registry_url
container_registry = self.attributes["container_registry_url"].presence || repository_url
"ghcr.io/#{container_registry}:latest"
project_credential_provider.container_registry_url
end
def deployable?
@@ -125,4 +119,13 @@ class Project < ApplicationRecord
def updated!
services.each(&:updated!)
end
def container_registry_url
container_registry = self.attributes["container_registry_url"].presence || repository_url
if github?
"ghcr.io/#{container_registry}:latest"
else
"docker.io/#{container_registry}:latest"
end
end
end

View File

@@ -25,6 +25,10 @@ class ProjectCredentialProvider < ApplicationRecord
validates_uniqueness_of :provider_id, scope: :project_id
delegate :used!, to: :provider
delegate :username, to: :provider
delegate :access_token, to: :provider
delegate :github?, to: :provider
delegate :docker_hub?, to: :provider
def github_username
JSON.parse(provider.auth)["info"]["nickname"]

View File

@@ -39,6 +39,18 @@ class Provider < ApplicationRecord
send("#{provider}_client")
end
def username
JSON.parse(auth)["info"]["nickname"] || JSON.parse(auth)["info"]["username"]
end
def registry
if github?
"ghcr.io"
else
"https://index.docker.io/v1/"
end
end
def expired?
expires_at? && expires_at <= Time.zone.now
end
@@ -57,6 +69,14 @@ class Provider < ApplicationRecord
end
end
def docker_hub?
provider == DOCKER_HUB_PROVIDER
end
def github?
provider == GITHUB_PROVIDER
end
def twitter_refresh_token!(token); end
def used!

View File

@@ -1,19 +1,31 @@
class Github::Client
WEBHOOK_SECRET = ENV["OMNIAUTH_GITHUB_WEBHOOK_SECRET"]
attr_accessor :client, :project
def initialize(project)
@project = project
@client = Octokit::Client.new(access_token: project.github_access_token)
attr_accessor :client, :repository_url
def self.from_project(project)
new(
access_token: project.project_credential_provider.access_token,
repository_url: project.repository_url
)
end
def commits
client.commits(project.repository_url)
end
def initialize(access_token:, repository_url:)
@client = Octokit::Client.new(access_token:)
@repository_url = repository_url
end
def repository_exists?
client.repository?(project.repository_url)
client.repository?(repository_url)
end
def register_webhook!
client.create_hook(
project.repository_url,
repository_url,
"web",
{
url: Rails.application.routes.url_helpers.inbound_webhooks_github_index_url,
@@ -33,7 +45,7 @@ class Github::Client
def remove_webhook!
if webhook_exists?
client.remove_hook(project.repository_url, webhook.id)
client.remove_hook(repository_url, webhook.id)
end
end
@@ -42,7 +54,7 @@ class Github::Client
end
def webhooks
client.hooks(project.repository_url)
client.hooks(repository_url)
end
private

View File

@@ -2,7 +2,7 @@ class Async::Github::WebhookStatusViewModel < Async::BaseViewModel
expects :project_id
def client
@client ||= Github::Client.new(project)
@client ||= Github::Client.from_project(project)
end
def project

View File

@@ -10,7 +10,7 @@
<div class="form-control mt-1 w-full max-w-sm">
<div class="card bg-base-300 shadow-xl">
<div class="card-body">
<%= render "providers/provider", provider: project.github_provider %>
<%= render "providers/provider", provider: project.project_credential_provider.provider %>
</div>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<% if provider.docker_hub? %>
<%= render "projects/create/new_form_docker_hub", project:, provider: %>
<% else %>
<%= render "projects/create/new_form_github", project:, provider: %>
<% end %>

View File

@@ -0,0 +1,72 @@
<div data-controller="new-project">
<%= form_with(model: project) do |form| %>
<h2 class="text-2xl font-bold">Create a new project from Docker Hub</h2>
<%= link_to(
"Deploy from Github instead →",
new_project_path(provider: Provider::GITHUB_PROVIDER),
class: "inline-block mt-2 underline underline-offset-4 text-blue-300 hover:text-blue-200 text-sm",
) %>
<hr class="mt-3 mb-4 border-t border-base-300" />
<%= render "shared/error_messages", resource: form.object %>
<div class="form-control mt-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Name</span>
</label>
<%= form.text_field :name, class: "input input-bordered w-full focus:outline-offset-0", value: RandomNameGenerator.generate_name %>
<label class="label">
<span class="label-text-alt">* Required</span>
</label>
</div>
<div class="form-control mt-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Cluster</span>
</label>
<%= form.collection_select :cluster_id, current_account.clusters.running, :id, :name, {}, { class: "select select-bordered" } %>
<label class="label">
<span class="label-text-alt">* Required</span>
</label>
</div>
<%= form.fields_for(:project_credential_provider) do |fields| %>
<%= fields.hidden_field :provider_id, value: provider.id %>
<% end %>
<div class="form-control mt-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Docker Hub Repository</span>
</label>
<%= form.text_field(
:repository_url,
class: "input input-bordered w-full focus:outline-offset-0",
placeholder: "accountname/repo",
) %>
</div>
<div class="form-control mt-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Docker command</span>
</label>
<%= form.text_field :docker_command, class: "input input-bordered w-full focus:outline-offset-0" %>
</div>
<div class="form-control my-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Predeploy command</span>
</label>
<%= form.text_field :predeploy_command, class: "input input-bordered w-full focus:outline-offset-0" %>
</div>
<div class="form-footer">
<%= form.button "Submit", class: "btn btn-primary" %>
<% if form.object.new_record? %>
<%= link_to t("cancel"), projects_path, class: "btn btn-secondary", data: { turbo: false } %>
<% else %>
<%= link_to t("cancel"), project_path(@project), class: "btn btn-secondary" %>
<% end %>
</div>
<% end %>
</div>

View File

@@ -1,5 +1,13 @@
<div data-controller="new-project">
<%= form_with(model: project) do |form| %>
<h2 class="text-2xl font-bold">Create a new project from Github</h2>
<%= link_to(
"Deploy from Docker Hub instead →",
new_project_path(provider: Provider::DOCKER_HUB_PROVIDER),
class: "inline-block mt-2 underline underline-offset-4 text-blue-300 hover:text-blue-200 text-sm",
) %>
<hr class="mt-3 mb-4 border-t border-base-300" />
<%= render "shared/error_messages", resource: form.object %>
<div class="form-control mt-1 w-full max-w-sm">
@@ -22,11 +30,16 @@
</label>
</div>
<%= form.fields_for(:project_credential_provider) do |fields| %>
<%= fields.hidden_field :provider_id, value: provider.id %>
<% end %>
<div class="form-control mt-1 w-full max-w-sm">
<div data-controller="github-select-repository">
<label class="label">
<span class="label-text">Github Repository</span>
</label>
<%= form.text_field(
:repository_url,
class: "input input-bordered w-full focus:outline-offset-0",
@@ -114,7 +127,7 @@
<%= form.button "Submit", class: "btn btn-primary" %>
<% if form.object.new_record? %>
<%= link_to t("cancel"), projects_path, class: "btn btn-secondary" %>
<%= link_to t("cancel"), projects_path, class: "btn btn-secondary", data: { turbo: false } %>
<% else %>
<%= link_to t("cancel"), project_path(@project), class: "btn btn-secondary" %>
<% end %>

View File

@@ -11,7 +11,9 @@
<div class="card card-bordered bg-base-100">
<div class="card-body">
<%= render "new_form", project: @project %>
<%= turbo_frame_tag "new-project" do %>
<%= render "new_form", project: @project, provider: @provider %>
<% end %>
</div>
</div>
</div>

View File

@@ -3,9 +3,9 @@
<iconify-icon icon="mdi:github" width="24" height="24"></iconify-icon>
<div>
<span class="text-sm">
Connected as <b><%= JSON.parse(provider.auth)["info"]["nickname"] %></b>
Connected as <b><%= provider.username %></b>
</span>
<% if defined?(@project) %>
<% if defined?(@project) && @project.github? %>
<div class="mt-2">
<%= render(
"shared/partials/async_renderer",

View File

@@ -1,5 +1 @@
<% if provider.provider == Provider::GITHUB_PROVIDER %>
<%= JSON.parse(provider.auth)["info"]["nickname"] %>
<% elsif provider.provider == Provider::DOCKER_HUB_PROVIDER %>
<%= JSON.parse(provider.auth)["info"]["username"] %>
<% end %>
<%= provider.username %>

View File

@@ -0,0 +1,79 @@
# spec/actions/projects/create_spec.rb
require 'rails_helper'
RSpec.describe Projects::Create do
let(:user) { create(:user) }
let(:provider) { create(:provider, :github, user:) }
let(:cluster) { create(:cluster) }
let(:params) do
ActionController::Parameters.new({
project: {
name: 'example-repo',
branch: 'main',
cluster_id: cluster.id,
docker_build_context_directory: '.',
repository_url: 'example/repo',
docker_command: 'rails s',
dockerfile_path: 'Dockerfile',
container_registry_url: '',
project_credential_provider: {
provider_id: provider.id
}
}
})
end
before do
allow(Projects::ValidateGithubRepository).to receive(:execute)
allow(Projects::RegisterGithubWebhook).to receive(:execute)
end
describe '.call' do
let(:subject) { described_class.call(params, user) }
context 'for github' do
it 'creates a project with project_credential_provider' do
expect(subject).to be_success
end
end
context 'for docker hub' do
let(:provider) { create(:provider, :docker_hub, user:) }
it 'creates a project with project_credential_provider' do
expect(subject).to be_success
end
end
end
describe '.create_steps' do
let(:subject) { described_class.create_steps(provider) }
context 'in cloud mode' do
before do
allow(Rails.application.config).to receive(:local_mode).and_return(false)
end
it 'validates with github and registers webhooks' do
expect(subject).to eq([
Projects::ValidateGithubRepository,
Projects::Save,
Projects::RegisterGithubWebhook
])
end
end
context 'in local mode' do
before do
allow(Rails.application.config).to receive(:local_mode).and_return(true)
end
it 'validates with github and registers webhooks' do
expect(subject).to eq([
Projects::ValidateGithubRepository,
Projects::Save
])
end
end
end
end

View File

@@ -0,0 +1,25 @@
# spec/actions/projects/create_spec.rb
require 'rails_helper'
class MockCommit < Struct.new(:sha, :commit)
end
class MockGithub
def commits
[ MockCommit.new(sha: "1234", commit: { message: "initial commit" }) ]
end
end
RSpec.describe Projects::DeployLatestCommit do
let(:project) { create(:project) }
let(:subject) { described_class.execute(project:) }
context 'github project' do
it 'fetches from github and creates a new build' do
expect(Github::Client).to receive(:new).and_return(MockGithub.new)
expect(Projects::BuildJob).to receive(:perform_later)
expect { subject }.to change { project.builds.count }.by(1)
end
end
end

View File

@@ -5,7 +5,7 @@ RSpec.describe Providers::Create do
describe '.call' do
context 'when the provider is dockerhub' do
let(:provider) { build(:provider, provider: Provider::DOCKER_HUB_PROVIDER) }
let(:provider) { build(:provider, :docker_hub) }
it 'creates the provider' do
subject
expect(subject).to be_success
@@ -13,7 +13,7 @@ RSpec.describe Providers::Create do
end
context 'when the provider is github' do
let(:provider) { build(:provider, provider: Provider::GITHUB_PROVIDER) }
let(:provider) { build(:provider, :github) }
context 'when the access token is valid' do
before do
allow(Octokit::Client).to receive(:new).and_return(double(user: { login: 'test_user' }, scopes: [ 'repo', 'write:packages' ]))

View File

@@ -37,5 +37,10 @@ FactoryBot.define do
branch { "main" }
dockerfile_path { "./Dockerfile" }
docker_build_context_directory { "." }
after(:build) do |project|
provider = create(:provider, :github)
ProjectCredentialProvider.new(project: project, provider: provider)
end
end
end

View File

@@ -1,3 +1,28 @@
# == Schema Information
#
# Table name: providers
#
# id :bigint not null, primary key
# access_token :string
# access_token_secret :string
# auth :text
# expires_at :datetime
# last_used_at :datetime
# provider :string
# refresh_token :string
# uid :string
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_providers_on_user_id (user_id)
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
FactoryBot.define do
factory :provider do
user
@@ -7,5 +32,14 @@ FactoryBot.define do
last_used_at { nil }
provider { "github" }
uid { "sample_uid" }
trait :docker_hub do
provider { Provider::DOCKER_HUB_PROVIDER }
auth { { "info" => { "username" => "test_user" } }.to_json }
end
trait :github do
provider { Provider::GITHUB_PROVIDER }
auth { { "info" => { "nickname" => "test_user" } }.to_json }
end
end
end

View File

@@ -0,0 +1,29 @@
# spec/actions/projects/create_spec.rb
require 'rails_helper'
RSpec.describe Projects::DestroyJob do
let(:project) { create(:project) }
let(:job) { described_class.new }
let(:subject) { job.perform(project) }
before do
allow(job).to receive(:delete_namespace)
end
context 'no shared repository urls' do
it 'deregisters webhook' do
expect(job).to receive(:remove_github_webhook)
subject
end
end
context 'there is another project with the same repository url' do
let!(:project_2) { create(:project, repository_url: project.repository_url) }
it 'does not deregister webhook' do
expect(job).not_to receive(:remove_github_webhook)
subject
end
end
end