From db9c838a039e8f87c10700b507fab645517cfe29 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 2 Aug 2025 09:41:49 -0700 Subject: [PATCH] custom container registry implementation --- app/actions/providers/create.rb | 6 +- .../providers/create_docker_image_provider.rb | 18 +++++ app/controllers/providers_controller.rb | 2 +- .../registry_selector_controller.js | 46 ++++++++++++ app/jobs/projects/build_job.rb | 2 +- app/models/project.rb | 4 +- app/models/project_credential_provider.rb | 2 +- app/models/provider.rb | 13 ++-- app/services/docker_cli.rb | 71 +++++++++++++++++++ app/views/projects/_edit_form.html.erb | 4 +- app/views/projects/_new_form.html.erb | 4 +- ...g_container_registry_credentials.html.erb} | 0 .../create/_missing_git_credentials.html.erb | 4 +- ... => _new_form_container_registry.html.erb} | 2 +- ...=> _edit_form_container_registry.html.erb} | 2 - app/views/providers/_form.html.erb | 37 +++++++++- app/views/providers/index.html.erb | 6 +- app/views/providers/new.html.erb | 10 +-- ...729215525_add_registry_url_to_providers.rb | 5 ++ db/schema.rb | 3 +- spec/actions/projects/create_spec.rb | 2 +- spec/actions/providers/create_spec.rb | 2 +- .../providers/generate_config_json_spec.rb | 2 +- spec/factories/projects.rb | 4 +- spec/factories/providers.rb | 6 +- spec/models/project_spec.rb | 2 +- 26 files changed, 219 insertions(+), 40 deletions(-) create mode 100644 app/javascript/controllers/registry_selector_controller.js create mode 100644 app/services/docker_cli.rb rename app/views/projects/create/{_missing_docker_hub_credentials.html.erb => _missing_container_registry_credentials.html.erb} (100%) rename app/views/projects/create/{_new_form_docker_hub.html.erb => _new_form_container_registry.html.erb} (97%) rename app/views/projects/update/{_edit_form_docker_hub.html.erb => _edit_form_container_registry.html.erb} (99%) create mode 100644 db/migrate/20250729215525_add_registry_url_to_providers.rb diff --git a/app/actions/providers/create.rb b/app/actions/providers/create.rb index 858db114..cc447b6f 100644 --- a/app/actions/providers/create.rb +++ b/app/actions/providers/create.rb @@ -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, ) diff --git a/app/actions/providers/create_docker_image_provider.rb b/app/actions/providers/create_docker_image_provider.rb index d66375b5..f0ee6b01 100644 --- a/app/actions/providers/create_docker_image_provider.rb +++ b/app/actions/providers/create_docker_image_provider.rb @@ -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 diff --git a/app/controllers/providers_controller.rb b/app/controllers/providers_controller.rb index 5c60e508..00d6cc74 100644 --- a/app/controllers/providers_controller.rb +++ b/app/controllers/providers_controller.rb @@ -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 diff --git a/app/javascript/controllers/registry_selector_controller.js b/app/javascript/controllers/registry_selector_controller.js new file mode 100644 index 00000000..0feb6d35 --- /dev/null +++ b/app/javascript/controllers/registry_selector_controller.js @@ -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') + } +} \ No newline at end of file diff --git a/app/jobs/projects/build_job.rb b/app/jobs/projects/build_job.rb index b47a5c22..98b78746 100644 --- a/app/jobs/projects/build_job.rb +++ b/app/jobs/projects/build_job.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index bb1ed549..46634095 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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}" diff --git a/app/models/project_credential_provider.rb b/app/models/project_credential_provider.rb index 15006306..54a27bd4 100644 --- a/app/models/project_credential_provider.rb +++ b/app/models/project_credential_provider.rb @@ -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 diff --git a/app/models/provider.rb b/app/models/provider.rb index e459dc5a..bb015eed 100644 --- a/app/models/provider.rb +++ b/app/models/provider.rb @@ -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? diff --git a/app/services/docker_cli.rb b/app/services/docker_cli.rb new file mode 100644 index 00000000..4272ae0b --- /dev/null +++ b/app/services/docker_cli.rb @@ -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 \ No newline at end of file diff --git a/app/views/projects/_edit_form.html.erb b/app/views/projects/_edit_form.html.erb index 591deed3..75ba41e8 100644 --- a/app/views/projects/_edit_form.html.erb +++ b/app/views/projects/_edit_form.html.erb @@ -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 %> diff --git a/app/views/projects/_new_form.html.erb b/app/views/projects/_new_form.html.erb index 53493dcc..84152982 100644 --- a/app/views/projects/_new_form.html.erb +++ b/app/views/projects/_new_form.html.erb @@ -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 %> diff --git a/app/views/projects/create/_missing_docker_hub_credentials.html.erb b/app/views/projects/create/_missing_container_registry_credentials.html.erb similarity index 100% rename from app/views/projects/create/_missing_docker_hub_credentials.html.erb rename to app/views/projects/create/_missing_container_registry_credentials.html.erb diff --git a/app/views/projects/create/_missing_git_credentials.html.erb b/app/views/projects/create/_missing_git_credentials.html.erb index be43e5ac..f87fd93a 100644 --- a/app/views/projects/create/_missing_git_credentials.html.erb +++ b/app/views/projects/create/_missing_git_credentials.html.erb @@ -19,8 +19,8 @@
<%= 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", ) %>
diff --git a/app/views/projects/create/_new_form_docker_hub.html.erb b/app/views/projects/create/_new_form_container_registry.html.erb similarity index 97% rename from app/views/projects/create/_new_form_docker_hub.html.erb rename to app/views/projects/create/_new_form_container_registry.html.erb index 36500989..a9868374 100644 --- a/app/views/projects/create/_new_form_docker_hub.html.erb +++ b/app/views/projects/create/_new_form_container_registry.html.erb @@ -1,6 +1,6 @@
<%= form_with(model: project, data: { turbo: false }) do |form| %> -

Create a new project from Docker Hub

+

Create a new project from a container registry

<%= link_to( "Deploy from Git repository instead →", new_project_path(provider: Provider::GITHUB_PROVIDER), diff --git a/app/views/projects/update/_edit_form_docker_hub.html.erb b/app/views/projects/update/_edit_form_container_registry.html.erb similarity index 99% rename from app/views/projects/update/_edit_form_docker_hub.html.erb rename to app/views/projects/update/_edit_form_container_registry.html.erb index 6c59e7ec..405676d4 100644 --- a/app/views/projects/update/_edit_form_docker_hub.html.erb +++ b/app/views/projects/update/_edit_form_container_registry.html.erb @@ -22,8 +22,6 @@ <% end %>
- -