mirror of
https://github.com/czhu12/canine.git
synced 2025-12-21 10:49:49 -06:00
custom container registry implementation
This commit is contained in:
@@ -5,15 +5,15 @@ module Providers
|
||||
extend LightService::Organizer
|
||||
|
||||
def self.call(provider)
|
||||
if provider.provider == Provider::GITHUB_PROVIDER
|
||||
if provider.github?
|
||||
with(provider:).reduce(
|
||||
Providers::CreateGithubProvider,
|
||||
)
|
||||
elsif provider.provider == Provider::DOCKER_HUB_PROVIDER
|
||||
elsif provider.container_registry?
|
||||
with(provider:).reduce(
|
||||
Providers::CreateDockerImageProvider,
|
||||
)
|
||||
elsif provider.provider == Provider::GITLAB_PROVIDER
|
||||
elsif provider.gitlab?
|
||||
with(provider:).reduce(
|
||||
Providers::CreateGitlabProvider,
|
||||
)
|
||||
|
||||
@@ -5,11 +5,29 @@ class Providers::CreateDockerImageProvider
|
||||
promises :provider
|
||||
|
||||
executed do |context|
|
||||
provider = context.provider
|
||||
|
||||
# Test the container registry credentials
|
||||
begin
|
||||
DockerCli.with_registry_auth(
|
||||
registry_url: provider.registry_url,
|
||||
username: provider.username_param,
|
||||
password: provider.access_token
|
||||
) do
|
||||
# If we get here, authentication was successful
|
||||
Rails.logger.info("Container registry authentication successful")
|
||||
end
|
||||
rescue DockerCli::AuthenticationError => e
|
||||
context.provider.errors.add(:access_token, "Invalid credentials: #{e.message}")
|
||||
context.fail_and_return!(e.message)
|
||||
end
|
||||
|
||||
context.provider.auth = {
|
||||
info: {
|
||||
username: context.provider.username_param
|
||||
}
|
||||
}.to_json
|
||||
|
||||
if context.provider.save
|
||||
context.provider = context.provider
|
||||
else
|
||||
|
||||
@@ -26,6 +26,6 @@ class ProvidersController < ApplicationController
|
||||
end
|
||||
private
|
||||
def provider_params
|
||||
params.require(:provider).permit(:provider, :username_param, :access_token)
|
||||
params.require(:provider).permit(:provider, :username_param, :access_token, :registry_url)
|
||||
end
|
||||
end
|
||||
|
||||
46
app/javascript/controllers/registry_selector_controller.js
Normal file
46
app/javascript/controllers/registry_selector_controller.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["urlInput", "buttons"]
|
||||
|
||||
static values = {
|
||||
registries: Object
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.registriesValue = {
|
||||
dockerhub: "https://index.docker.io/v1/",
|
||||
ghcr: "ghcr.io",
|
||||
gcr: "gcr.io",
|
||||
ecr: "ecr.amazonaws.com",
|
||||
acr: "azurecr.io"
|
||||
}
|
||||
|
||||
// Disable URL field by default
|
||||
this.urlInputTarget.disabled = true
|
||||
this.urlInputTarget.classList.add('bg-base-200')
|
||||
}
|
||||
|
||||
selectRegistry(event) {
|
||||
const registry = event.currentTarget.dataset.registry
|
||||
|
||||
if (registry === "other") {
|
||||
this.urlInputTarget.value = ""
|
||||
this.urlInputTarget.disabled = false
|
||||
this.urlInputTarget.classList.remove('bg-base-200')
|
||||
this.urlInputTarget.focus()
|
||||
} else if (this.registriesValue[registry]) {
|
||||
this.urlInputTarget.value = this.registriesValue[registry]
|
||||
this.urlInputTarget.disabled = true
|
||||
this.urlInputTarget.classList.add('bg-base-200')
|
||||
}
|
||||
|
||||
// Update active button state
|
||||
this.element.querySelectorAll('[data-action="click->registry-selector#selectRegistry"]').forEach(btn => {
|
||||
btn.classList.remove('btn-active', 'btn-primary')
|
||||
btn.classList.add('btn-outline')
|
||||
})
|
||||
event.currentTarget.classList.remove('btn-outline')
|
||||
event.currentTarget.classList.add('btn-active', 'btn-primary')
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ class Projects::BuildJob < ApplicationJob
|
||||
def perform(build)
|
||||
project = build.project
|
||||
# If its a dockerhub deploy, we don't need to build the docker image
|
||||
if project.docker_hub?
|
||||
if project.container_registry?
|
||||
build.info("Skipping build for #{project.name} because it's a Docker Hub deploy")
|
||||
else
|
||||
project_credential_provider = project.project_credential_provider
|
||||
|
||||
@@ -83,7 +83,7 @@ class Project < ApplicationRecord
|
||||
manually_create: 1
|
||||
}, prefix: :forks
|
||||
delegate :git?, :github?, :gitlab?, to: :project_credential_provider
|
||||
delegate :docker_hub?, to: :project_credential_provider
|
||||
delegate :container_registry?, to: :project_credential_provider
|
||||
|
||||
def project_fork_cluster_id_is_owned_by_account
|
||||
if project_fork_cluster_id.present? && !account.clusters.exists?(id: project_fork_cluster_id)
|
||||
@@ -165,7 +165,7 @@ class Project < ApplicationRecord
|
||||
|
||||
def container_registry_url
|
||||
container_registry = self.attributes["container_registry_url"].presence || repository_url
|
||||
tag = docker_hub? ? 'latest' : branch.gsub('/', '-') # Docker Hub uses latest, others use branch name
|
||||
tag = container_registry? ? 'latest' : branch.gsub('/', '-') # Docker Hub uses latest, others use branch name
|
||||
|
||||
if github?
|
||||
"ghcr.io/#{container_registry}:#{tag}"
|
||||
|
||||
@@ -28,7 +28,7 @@ class ProjectCredentialProvider < ApplicationRecord
|
||||
delegate :username, to: :provider
|
||||
delegate :access_token, to: :provider
|
||||
delegate :github?, to: :provider
|
||||
delegate :docker_hub?, to: :provider
|
||||
delegate :container_registry?, to: :provider
|
||||
delegate :gitlab?, to: :provider
|
||||
delegate :git?, to: :provider
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# last_used_at :datetime
|
||||
# provider :string
|
||||
# refresh_token :string
|
||||
# registry_url :string
|
||||
# uid :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -26,16 +27,16 @@
|
||||
class Provider < ApplicationRecord
|
||||
attr_accessor :username_param
|
||||
GITHUB_PROVIDER = "github"
|
||||
DOCKER_HUB_PROVIDER = "docker_hub"
|
||||
CUSTOM_REGISTRY_PROVIDER = "container_registry"
|
||||
GITLAB_PROVIDER = "gitlab"
|
||||
GIT_TYPE = "git"
|
||||
DOCKER_TYPE = "docker"
|
||||
REGISTRY_TYPE = "registry"
|
||||
PROVIDER_TYPES = {
|
||||
GIT_TYPE => [ GITHUB_PROVIDER, GITLAB_PROVIDER ],
|
||||
DOCKER_TYPE => [ DOCKER_HUB_PROVIDER ]
|
||||
REGISTRY_TYPE => [ CUSTOM_REGISTRY_PROVIDER ]
|
||||
}
|
||||
|
||||
AVAILABLE_PROVIDERS = [ GITHUB_PROVIDER, DOCKER_HUB_PROVIDER, GITLAB_PROVIDER ].freeze
|
||||
AVAILABLE_PROVIDERS = [ GITHUB_PROVIDER, GITLAB_PROVIDER, CUSTOM_REGISTRY_PROVIDER ].freeze
|
||||
|
||||
belongs_to :user
|
||||
|
||||
@@ -83,8 +84,8 @@ class Provider < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def docker_hub?
|
||||
provider == DOCKER_HUB_PROVIDER
|
||||
def container_registry?
|
||||
provider == CUSTOM_REGISTRY_PROVIDER
|
||||
end
|
||||
|
||||
def github?
|
||||
|
||||
71
app/services/docker_cli.rb
Normal file
71
app/services/docker_cli.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
class DockerCli
|
||||
class AuthenticationError < StandardError; end
|
||||
|
||||
def self.with_registry_auth(registry_url:, username:, password:, &block)
|
||||
normalized_registry = normalize_registry_url(registry_url)
|
||||
|
||||
# Login to registry
|
||||
login_success = login(normalized_registry, username, password)
|
||||
unless login_success
|
||||
raise AuthenticationError, "Failed to authenticate with registry: #{normalized_registry}"
|
||||
end
|
||||
|
||||
# Execute the block
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
# Always logout, even if block raises an exception
|
||||
logout(normalized_registry)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.login(registry, username, password)
|
||||
docker_login_command = [
|
||||
"docker", "login", registry,
|
||||
"--username", username,
|
||||
"--password-stdin"
|
||||
]
|
||||
|
||||
stdout, stderr, status = Open3.capture3(*docker_login_command, stdin_data: password)
|
||||
|
||||
if status.success?
|
||||
Rails.logger.info("Successfully logged in to #{registry}")
|
||||
true
|
||||
else
|
||||
Rails.logger.error("Docker login failed for #{registry}: #{stderr}")
|
||||
false
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Docker login error: #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def self.logout(registry)
|
||||
stdout, stderr, status = Open3.capture3("docker", "logout", registry)
|
||||
|
||||
if status.success?
|
||||
Rails.logger.info("Successfully logged out from #{registry}")
|
||||
else
|
||||
Rails.logger.warn("Docker logout failed for #{registry}: #{stderr}")
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Docker logout error: #{e.message}")
|
||||
end
|
||||
|
||||
def self.normalize_registry_url(url)
|
||||
return "docker.io" if url.blank?
|
||||
|
||||
# Remove protocol if present
|
||||
url = url.sub(/^https?:\/\//, '')
|
||||
# Remove trailing slashes and paths
|
||||
url = url.sub(/\/.*$/, '')
|
||||
# Handle Docker Hub special cases
|
||||
if url.include?('docker.io') || url.include?('index.docker.io')
|
||||
'docker.io'
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if project.docker_hub? %>
|
||||
<%= render "projects/update/edit_form_docker_hub", project: %>
|
||||
<% if project.container_registry? %>
|
||||
<%= render "projects/update/edit_form_container_registry", project: %>
|
||||
<% else %>
|
||||
<%= render "projects/update/edit_form_git", project: %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<% if selected_provider_type == Provider::GIT_TYPE && selectable_providers.empty? %>
|
||||
<%= render "projects/create/missing_git_credentials", project: %>
|
||||
<% elsif selected_provider_type == Provider::DOCKER_TYPE && selectable_providers.empty? %>
|
||||
<%= render "projects/create/missing_docker_hub_credentials", project: %>
|
||||
<%= render "projects/create/missing_container_registry_credentials", project: %>
|
||||
<% elsif selected_provider_type == Provider::DOCKER_TYPE %>
|
||||
<%= render "projects/create/new_form_docker_hub", project:, provider:, selectable_providers: %>
|
||||
<%= render "projects/create/new_form_container_registry", project:, provider:, selectable_providers: %>
|
||||
<% else %>
|
||||
<%= render "projects/create/new_form_git", project:, provider:, selectable_providers: %>
|
||||
<% end %>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<%= link_to(
|
||||
"Deploy from Docker Hub instead →",
|
||||
new_project_path(provider: Provider::DOCKER_HUB_PROVIDER),
|
||||
"Deploy from container registry instead →",
|
||||
new_project_path(provider_type: Provider::REGISTRY_TYPE),
|
||||
class: "inline-block mt-2 underline underline-offset-4 text-blue-300 hover:text-blue-200 text-sm",
|
||||
) %>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div data-controller="new-project">
|
||||
<%= form_with(model: project, data: { turbo: false }) do |form| %>
|
||||
<h2 class="text-2xl font-bold">Create a new project from Docker Hub</h2>
|
||||
<h2 class="text-2xl font-bold">Create a new project from a container registry</h2>
|
||||
<%= link_to(
|
||||
"Deploy from Git repository instead →",
|
||||
new_project_path(provider: Provider::GITHUB_PROVIDER),
|
||||
@@ -22,8 +22,6 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-footer">
|
||||
<%= form.button "Submit", class: "btn btn-primary" %>
|
||||
|
||||
@@ -1,7 +1,42 @@
|
||||
<%= form_with model: provider do |form| %>
|
||||
<%= render "shared/error_messages", resource: form.object %>
|
||||
<%= form.hidden_field :provider, value: params[:provider_type] %>
|
||||
<% if params[:provider_type] == Provider::DOCKER_HUB_PROVIDER || form.object.provider == Provider::DOCKER_HUB_PROVIDER %>
|
||||
<% if params[:provider_type] == Provider::CUSTOM_REGISTRY_PROVIDER || form.object.provider == Provider::CUSTOM_REGISTRY_PROVIDER %>
|
||||
<div data-controller="registry-selector">
|
||||
<div class="form-control mt-1 w-full max-w-sm">
|
||||
<label class="label">
|
||||
<span class="label-text">Select Registry</span>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2 mb-3" data-registry-selector-target="buttons">
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="dockerhub">
|
||||
Docker Hub
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="ghcr">
|
||||
GitHub (ghcr.io)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="gcr">
|
||||
Google (gcr.io)
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="ecr">
|
||||
AWS ECR
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="acr">
|
||||
Azure ACR
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" data-action="click->registry-selector#selectRegistry" data-registry="other">
|
||||
Other
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-1 w-full max-w-sm">
|
||||
<label class="label">
|
||||
<span class="label-text">Registry URL</span>
|
||||
</label>
|
||||
<%= form.text_field :registry_url, class: "input input-bordered", required: true, data: { "registry-selector-target": "urlInput" } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mt-1 w-full max-w-sm">
|
||||
<label class="label">
|
||||
<span class="label-text">Username</span>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<hr class="mt-3 mb-4 border-t border-base-300" />
|
||||
<%= turbo_frame_tag "provider" do %>
|
||||
<%= render "providers/index" %>
|
||||
<%= link_to "+ Github Credentials", new_provider_path(provider_type: "github"), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "+ Dockerhub Credentials", new_provider_path(provider_type: "docker_hub"), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "+ Gitlab Credentials", new_provider_path(provider_type: "gitlab"), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "+ Github Credentials", new_provider_path(provider_type: Provider::GITHUB_PROVIDER), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "+ Gitlab Credentials", new_provider_path(provider_type: Provider::GITLAB_PROVIDER), class: "btn btn-primary btn-sm" %>
|
||||
<%= link_to "+ Container Registry Credentials", new_provider_path(provider_type: Provider::CUSTOM_REGISTRY_PROVIDER), class: "btn btn-primary btn-sm" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -6,10 +6,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-2 mb-4">
|
||||
<% if params[:provider_type] == Provider::DOCKER_HUB_PROVIDER %>
|
||||
<%= link_to "Find your Docker Hub token →", "https://app.docker.com/settings/personal-access-tokens", target: "_blank", class: "text-sm text-gray-500 underline" %>
|
||||
<div class=" mt-2">
|
||||
Make sure you select the <code class="mx-2 bg-white text-black px-2 py-1 rounded-md">Read & Write</code> permission if you want to deploy a private project.
|
||||
<% if params[:provider_type] == Provider::CUSTOM_REGISTRY_PROVIDER %>
|
||||
<div class="text-sm text-gray-500">
|
||||
Select your container registry provider or enter a custom URL.
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
Make sure you have the <code class="mx-2 bg-white text-black px-2 py-1 rounded-md">Read & Write</code> permission if you want to deploy a private project.
|
||||
</div>
|
||||
<% elsif params[:provider_type] == Provider::GITHUB_PROVIDER %>
|
||||
<%= link_to "Find your Github token →", "https://github.com/settings/tokens", target: "_blank", class: "text-sm text-gray-500 " %>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddRegistryUrlToProviders < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :providers, :registry_url, :string
|
||||
end
|
||||
end
|
||||
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_07_19_160150) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_07_29_215525) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -280,6 +280,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_07_19_160150) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "last_used_at"
|
||||
t.string "registry_url"
|
||||
t.index ["user_id"], name: "index_providers_on_user_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ RSpec.describe Projects::Create do
|
||||
end
|
||||
|
||||
context 'for docker hub' do
|
||||
let(:provider) { create(:provider, :docker_hub, user:) }
|
||||
let(:provider) { create(:provider, :container_registry, user:) }
|
||||
|
||||
it 'creates a project with project_credential_provider' do
|
||||
expect(subject).to be_success
|
||||
|
||||
@@ -5,7 +5,7 @@ RSpec.describe Providers::Create do
|
||||
|
||||
describe '.call' do
|
||||
context 'when the provider is dockerhub' do
|
||||
let(:provider) { build(:provider, :docker_hub) }
|
||||
let(:provider) { build(:provider, :container_registry) }
|
||||
it 'creates the provider' do
|
||||
subject
|
||||
expect(subject).to be_success
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Providers::GenerateConfigJson do
|
||||
let(:provider) { build(:provider, :docker_hub) }
|
||||
let(:provider) { build(:provider, :container_registry) }
|
||||
|
||||
subject(:context) { described_class.execute(provider: provider) }
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ FactoryBot.define do
|
||||
end
|
||||
end
|
||||
|
||||
trait :docker_hub do
|
||||
trait :container_registry do
|
||||
after(:build) do |project|
|
||||
provider = create(:provider, :docker_hub)
|
||||
provider = create(:provider, :container_registry)
|
||||
project.project_credential_provider = build(:project_credential_provider, project: project, provider: provider)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
# last_used_at :datetime
|
||||
# provider :string
|
||||
# refresh_token :string
|
||||
# registry_url :string
|
||||
# uid :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
@@ -32,9 +33,10 @@ FactoryBot.define do
|
||||
last_used_at { nil }
|
||||
provider { "github" }
|
||||
uid { "sample_uid" }
|
||||
trait :docker_hub do
|
||||
provider { Provider::DOCKER_HUB_PROVIDER }
|
||||
trait :container_registry do
|
||||
provider { Provider::CUSTOM_REGISTRY_PROVIDER }
|
||||
auth { { "info" => { "username" => "test_user" } }.to_json }
|
||||
registry_url { "docker.io" }
|
||||
end
|
||||
|
||||
trait :github do
|
||||
|
||||
@@ -156,7 +156,7 @@ RSpec.describe Project, type: :model do
|
||||
end
|
||||
|
||||
it 'uses latest tag for Docker Hub' do
|
||||
docker_project = create(:project, :docker_hub, repository_url: 'owner/repo', branch: 'feature/test')
|
||||
docker_project = create(:project, :container_registry, repository_url: 'owner/repo', branch: 'feature/test')
|
||||
expect(docker_project.container_registry_url).to eq('docker.io/owner/repo:latest')
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user