portainer round 4

This commit is contained in:
Chris
2025-10-03 12:12:53 +01:00
parent 127362c1ea
commit aa81ef9bc8
13 changed files with 213 additions and 9 deletions

View File

@@ -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

View File

@@ -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'
}
}
}

View File

@@ -69,6 +69,7 @@ class Cluster < ApplicationRecord
def external?
external_id.present?
end
private
def create_build_cloud_record!(attributes)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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' %>

View File

@@ -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

View File

@@ -0,0 +1,5 @@
class RemoveUniquenessFromExternalIdOnProviders < ActiveRecord::Migration[7.2]
def change
remove_index :providers, :external_id
end
end

3
db/schema.rb generated
View File

@@ -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

View File

@@ -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
#

View 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