mirror of
https://github.com/czhu12/canine.git
synced 2025-12-21 10:49:49 -06:00
added docker hub project creation
This commit is contained in:
5
TODO.md
5
TODO.md
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
31
app/actions/providers/generate_config_json.rb
Normal file
31
app/actions/providers/generate_config_json.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
5
app/views/projects/_new_form.html.erb
Normal file
5
app/views/projects/_new_form.html.erb
Normal 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 %>
|
||||
72
app/views/projects/create/_new_form_docker_hub.html.erb
Normal file
72
app/views/projects/create/_new_form_docker_hub.html.erb
Normal 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>
|
||||
@@ -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 %>
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
79
spec/actions/projects/create_spec.rb
Normal file
79
spec/actions/projects/create_spec.rb
Normal 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
|
||||
25
spec/actions/projects/deploy_latest_commit_spec.rb
Normal file
25
spec/actions/projects/deploy_latest_commit_spec.rb
Normal 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
|
||||
@@ -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' ]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
29
spec/jobs/projects/destroy_job_spec.rb
Normal file
29
spec/jobs/projects/destroy_job_spec.rb
Normal 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
|
||||
Reference in New Issue
Block a user