mirror of
https://github.com/czhu12/canine.git
synced 2026-01-06 03:30:16 -06:00
portainer round 4
This commit is contained in:
@@ -1,7 +1,24 @@
|
||||
module Accounts
|
||||
class StackManagersController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
skip_before_action :authenticate_user!, only: [ :verify_url ]
|
||||
skip_before_action :authenticate_user!, only: [ :verify_url, :authenticated ]
|
||||
|
||||
def authenticated
|
||||
stack_manager = current_account&.stack_manager
|
||||
|
||||
if stack_manager.nil? || current_user&.portainer_jwt.nil?
|
||||
head :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
portainer_client = Portainer::Client.new(stack_manager.provider_url, current_user.portainer_jwt)
|
||||
|
||||
if portainer_client.authenticated?
|
||||
head :ok
|
||||
else
|
||||
head :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def index
|
||||
redirect_to stack_manager_path
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { Turbo } from "@hotwired/turbo-rails"
|
||||
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.checkAuthentication()
|
||||
}
|
||||
|
||||
async checkAuthentication() {
|
||||
try {
|
||||
const response = await fetch('/stack_manager/authenticated', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
await this.logout()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Authentication check failed:', error)
|
||||
await this.logout()
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
||||
|
||||
const response = await fetch('/users/sign_out', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'Accept': 'text/vnd.turbo-stream.html, text/html, application/xhtml+xml',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
Turbo.visit('/users/sign_in', { action: 'replace' })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error)
|
||||
window.location.href = '/users/sign_in'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ class Cluster < ApplicationRecord
|
||||
def external?
|
||||
external_id.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_build_cloud_record!(attributes)
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_providers_on_external_id (external_id) UNIQUE
|
||||
# index_providers_on_user_id (user_id)
|
||||
# index_providers_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
@@ -42,7 +41,6 @@ class Provider < ApplicationRecord
|
||||
|
||||
AVAILABLE_PROVIDERS = [ GITHUB_PROVIDER, GITLAB_PROVIDER, CUSTOM_REGISTRY_PROVIDER ].freeze
|
||||
validates :registry_url, presence: true, if: :container_registry?
|
||||
validates :external_id, uniqueness: true, allow_nil: true
|
||||
scope :has_container_registry, -> { where(provider: [ GITHUB_PROVIDER, GITLAB_PROVIDER, CUSTOM_REGISTRY_PROVIDER ]) }
|
||||
|
||||
belongs_to :user
|
||||
|
||||
@@ -28,6 +28,8 @@ class StackManager < ApplicationRecord
|
||||
validates_presence_of :account, :provider_url, :stack_manager_type
|
||||
validates_uniqueness_of :account
|
||||
|
||||
before_validation :strip_trailing_slash_from_provider_url
|
||||
|
||||
def requires_reauthentication?
|
||||
access_token.blank?
|
||||
end
|
||||
@@ -37,4 +39,18 @@ class StackManager < ApplicationRecord
|
||||
@_stack ||= Portainer::Stack.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def strip_trailing_slash_from_provider_url
|
||||
return if provider_url.blank?
|
||||
|
||||
uri = URI.parse(provider_url)
|
||||
uri.path = ""
|
||||
uri.query = nil
|
||||
uri.fragment = nil
|
||||
self.provider_url = uri.to_s
|
||||
rescue URI::InvalidURIError
|
||||
# Leave provider_url unchanged if it's invalid
|
||||
end
|
||||
end
|
||||
|
||||
@@ -27,6 +27,9 @@ class K8::Stateless::Ingress < K8::Base
|
||||
|
||||
def self.ip_address(client)
|
||||
service = client.get_services.find { |s| s['metadata']['name'] == 'ingress-nginx-controller' }
|
||||
if service.nil?
|
||||
raise "Ingress-nginx-controller service not installed"
|
||||
end
|
||||
service.status.loadBalancer.ingress[0].ip
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
<span class="sr-only"><%= Rails.configuration.application_name %></span>
|
||||
<% end %>
|
||||
|
||||
<% if current_account.stack_manager.present? %>
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render "devise/sessions/portainer_badge", stack_manager: current_account.stack_manager %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center justify-center">
|
||||
<details class="mx-auto dropdown">
|
||||
<summary class="btn m-1"><%= current_account.name %> <iconify-icon icon="lucide:chevron-down"></iconify-icon></summary>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<%= render 'shared/head' %>
|
||||
</head>
|
||||
|
||||
<body data-controller="theme" data-theme-preference-value="dark">
|
||||
<body data-controller="theme<%= ' stack-manager-authentication' if current_account&.stack_manager&.present? %>" data-theme-preference-value="dark">
|
||||
<div class="size-full">
|
||||
<div class="flex">
|
||||
<%= render 'layouts/sidebar' %>
|
||||
|
||||
@@ -28,6 +28,7 @@ Rails.application.routes.draw do
|
||||
resource :stack_manager, only: %i[show new create edit update destroy], controller: 'accounts/stack_managers' do
|
||||
collection do
|
||||
post :verify_url
|
||||
get :authenticated
|
||||
end
|
||||
end
|
||||
namespace :inbound_webhooks do
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class RemoveUniquenessFromExternalIdOnProviders < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
remove_index :providers, :external_id
|
||||
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_09_14_175112) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_10_02_214647) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -424,7 +424,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_09_14_175112) do
|
||||
t.datetime "last_used_at"
|
||||
t.string "registry_url"
|
||||
t.string "external_id"
|
||||
t.index ["external_id"], name: "index_providers_on_external_id", unique: true
|
||||
t.index ["user_id"], name: "index_providers_on_user_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_providers_on_external_id (external_id) UNIQUE
|
||||
# index_providers_on_user_id (user_id)
|
||||
# index_providers_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
|
||||
109
spec/models/stack_manager_spec.rb
Normal file
109
spec/models/stack_manager_spec.rb
Normal file
@@ -0,0 +1,109 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: stack_managers
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# access_token :string
|
||||
# provider_url :string not null
|
||||
# stack_manager_type :integer default("portainer"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_stack_managers_on_account_id (account_id) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe StackManager, type: :model do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe 'provider_url normalization' do
|
||||
it 'removes trailing slash from provider_url' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com/',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com')
|
||||
end
|
||||
|
||||
it 'does not modify provider_url without trailing slash' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com')
|
||||
end
|
||||
|
||||
it 'removes path from provider_url' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com/api/',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com')
|
||||
end
|
||||
|
||||
it 'preserves port number when removing path' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com:9443/',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com:9443')
|
||||
end
|
||||
|
||||
it 'preserves port number without path' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com:9443',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com:9443')
|
||||
end
|
||||
|
||||
it 'removes query parameters from provider_url' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.example.com/?key=value',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.example.com')
|
||||
end
|
||||
|
||||
it 'removes path and fragment from provider_url' do
|
||||
stack_manager = StackManager.create(
|
||||
account: account,
|
||||
provider_url: 'https://portainer.portainer.svc.cluster.local:9443/#!/home',
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
expect(stack_manager.provider_url).to eq('https://portainer.portainer.svc.cluster.local:9443')
|
||||
end
|
||||
|
||||
it 'leaves invalid URLs unchanged' do
|
||||
invalid_url = 'not a valid url/'
|
||||
stack_manager = StackManager.new(
|
||||
account: account,
|
||||
provider_url: invalid_url,
|
||||
stack_manager_type: :portainer
|
||||
)
|
||||
|
||||
stack_manager.valid?
|
||||
expect(stack_manager.provider_url).to eq(invalid_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user