remove portainer

This commit is contained in:
Celina Lopez
2025-08-29 11:21:10 -07:00
parent 2cb38f7c2e
commit dd8029f30a
24 changed files with 1 additions and 708 deletions

View File

@@ -1,16 +0,0 @@
class Portainer::Authenticate
extend LightService::Action
expects :stack_manager, :user, :auth_code
expects :username, default: nil
executed do |context|
stack_manager = context.stack_manager
access_token = Portainer::Client.authenticate(
auth_code: context.auth_code,
username: context.username,
provider_url: stack_manager.provider_url
)
context.user.providers.find_or_initialize_by(provider: "portainer").update!(access_token:)
end
end

View File

@@ -1,16 +0,0 @@
class Portainer::Kubeconfig
extend LightService::Action
expects :cluster, :user
promises :kubeconfig
executed do |context|
portainer_url = context.user.accounts.first.stack_manager.provider_url
portainer_client = Portainer::Client.new(portainer_url, context.user.portainer_jwt)
context.kubeconfig = portainer_client.get("/api/kubernetes/config?ids[]=#{context.cluster.external_id}")
rescue Portainer::Client::UnauthorizedError
context.fail_and_return!("Current user is unauthorized: #{context.user.email}")
rescue Portainer::Client::PermissionDeniedError
context.fail_and_return!("Current user is not authorized to access this cluster: #{context.user.email}")
end
end

View File

@@ -1,22 +0,0 @@
class Portainer::SyncClusters
extend LightService::Action
expects :user, :account
promises :clusters
executed do |context|
portainer_url = context.account.stack_manager.provider_url
portainer_client = Portainer::Client.new(portainer_url, context.user.portainer_jwt)
response = portainer_client.get("/api/endpoints")
clusters = []
response.each do |cluster|
clusters << context.account.clusters.create!(name: cluster["Name"], external_id: cluster["Id"])
end
context.clusters = clusters
rescue Portainer::Client::UnauthorizedError
context.fail_and_return!("Current user is unauthorized: #{context.user.email}")
rescue Portainer::Client::PermissionDeniedError
context.fail_and_return!("Current user is not authorized to access this cluster: #{context.user.email}")
end
end

View File

@@ -30,33 +30,4 @@ class Local::PagesController < ApplicationController
flash[:error] = "Invalid personal access token"
redirect_to github_token_path
end
def portainer_configuration
end
def update_portainer_configuration
stack_manager = current_account.stack_manager || current_account.build_stack_manager
stack_manager.update!(provider_url: params[:provider_url])
result = Portainer::Authenticate.execute(stack_manager:, user: current_user, auth_code: params[:password], username: params[:username])
if result.success?
flash[:notice] = "The Portainer configuration has been updated"
else
flash[:error] = result.message
end
redirect_to root_path
end
def github_oauth
result = Portainer::Authenticate.execute(
stack_manager: current_account.stack_manager,
user: current_user,
auth_code: params[:code]
)
if result.success?
flash[:notice] = "The Portainer configuration has been updated"
else
flash[:error] = result.message
end
redirect_to root_path
end
end

View File

@@ -1,7 +0,0 @@
class Clusters::SyncClustersJob < ApplicationJob
queue_as :default
def perform(user, account)
Portainer::SyncClusters.execute(account:, user:)
end
end

View File

@@ -29,7 +29,6 @@ class Provider < ApplicationRecord
GITHUB_PROVIDER = "github"
CUSTOM_REGISTRY_PROVIDER = "container_registry"
GITLAB_PROVIDER = "gitlab"
PORTAINER_PROVIDER = "portainer"
GIT_TYPE = "git"
REGISTRY_TYPE = "registry"
PROVIDER_TYPES = {

View File

@@ -48,11 +48,6 @@ class User < ApplicationRecord
providers.find_by(provider: "github")
end
def portainer_jwt
return @portainer_jwt if @portainer_jwt
@portainer_jwt = providers.find_by(provider: "portainer")&.access_token
end
private
def downcase_email

View File

@@ -78,8 +78,6 @@ module K8
def build_kubeconfig(kubeconfig_string)
if kubeconfig_string.is_a?(String)
load_kubeconfig(kubeconfig_string)
elsif kubeconfig_string.nil?
K8Stack.fetch_kubeconfig(@connection.cluster, @connection.user)
else
kubeconfig_string
end

View File

@@ -6,11 +6,6 @@ class K8::Connection
end
def kubeconfig
# If the cluster has a kubeconfig, use it.
if cluster.kubeconfig.present?
cluster.kubeconfig
else
K8Stack.fetch_kubeconfig(cluster, user)
end
cluster.kubeconfig
end
end

View File

@@ -1,15 +0,0 @@
class K8Stack
# The stack class is just to make sure that we don't hard couple to portainer
def self.fetch_kubeconfig(cluster, user)
stack_manager = user.accounts.first.stack_manager
if stack_manager&.portainer?
portainer_jwt = user.portainer_jwt
portainer_url = stack_manager.provider_url
raise "No Portainer JWT found" if portainer_jwt.blank?
raise "No Portainer URL found" if portainer_url.blank?
Portainer::Client.new(portainer_url, portainer_jwt).get_kubernetes_config
else
raise "Unsupported Kubernetes provider: #{stack_manager&.stack_manager_type}"
end
end
end

View File

@@ -1,79 +0,0 @@
# frozen_string_literal: true
require 'httparty'
module Portainer
class Client
attr_reader :jwt, :provider_url
include HTTParty
default_options.update(verify: false)
class UnauthorizedError < StandardError; end
class PermissionDeniedError < StandardError; end
def initialize(provider_url, jwt)
@jwt = jwt
@provider_url = provider_url
end
def get_kubernetes_config
fetch_wrapper do
self.class.get(
"#{provider_url}/api/kubernetes/config",
headers: headers
)
end
end
def self.authenticate(auth_code:, username: nil, provider_url:)
response = if username.present?
post(
"#{provider_url}/api/auth",
headers: { 'Content-Type' => 'application/json' },
body: {
username: username,
password: auth_code
}.to_json
)
else
post(
"#{provider_url}/api/auth/oauth/validate",
headers: { 'Content-Type' => 'application/json' },
body: { code: auth_code }.to_json
)
end
response.parsed_response['jwt'] if response.success?
end
def get(path)
fetch_wrapper do
self.class.get("#{provider_url}#{path}", headers:)
end
end
private
def headers
@headers ||= {
'Authorization' => "Bearer #{jwt}",
'Content-Type' => 'application/json'
}
end
def fetch_wrapper(&block)
response = yield
raise UnauthorizedError, "Unauthorized to access Portainer" if response.code == 401
raise PermissionDeniedError, "Permission denied to access Portainer" if response.code == 403
if response.success?
response.parsed_response
else
raise "Failed to fetch from Portainer: #{response.code} #{response.body}"
end
end
end
end

View File

@@ -16,14 +16,6 @@
<span class="hidden sm:inline">New Cluster</span>
</button>
<% end %>
<% if current_account.stack_manager&.portainer? %>
<%= link_to sync_clusters_path do %>
<button class="btn btn-outline btn-sm">
<iconify-icon icon="lucide:refresh-ccw" height="16"></iconify-icon>
<span class="hidden sm:inline">Sync Clusters</span>
</button>
<% end %>
<% end %>
</div>
<%= tag.div id: ("clusters" if @pagy.pages == 1) do %>

View File

@@ -1,33 +0,0 @@
<div class="space-y-4">
<h4 class="text-lg font-bold">Enter your Portainer URL below.</h4>
<%= form_with url: portainer_configuration_path, model: current_account.stack_manager, method: :put do |form| %>
<div class="form-group">
<%= form.label :provider_url, "Portainer URL" %>
<%= form.text_field :provider_url, class: "input input-bordered w-full max-w-xs", placeholder: "http://portainer.portainer.svc.cluster.local:9000" %>
<label class="label">
<span class="label-text-alt">All data is saved locally and never sent to any external servers.</span>
</label>
</div>
<h4 class="text-lg font-bold my-4">Login to Portainer</h4>
<div class="form-group" data-controller="toggle-password">
<%= form.label :username, "Username" %>
<%= form.text_field :username, class: "input input-bordered w-full max-w-xs" %>
</div>
<div class="form-group" data-controller="toggle-password">
<%= form.label :password, "Password" %>
<%= form.text_field :password, type: "password", class: "input input-bordered w-full max-w-xs", data: { toggle_password_target: "input" } %>
<button type="button" class="btn btn-outline" data-action="toggle-password#toggle">
<iconify-icon icon="mdi:eye"></iconify-icon>
</button>
</div>
<div class="text-center"> or </div>
<div>
<%= link_to "Connect via Github", Git::Github::UrlHelper.authorize_url, class: "btn btn-primary" %>
</div>
<div class="form-footer">
<%= form.submit "Save", class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@@ -118,8 +118,6 @@ Rails.application.routes.draw do
if Rails.application.config.local_mode
get "/github_token", to: "local/pages#github_token"
put "/github_token", to: "local/pages#update_github_token"
get "/portainer_configuration", to: "local/pages#portainer_configuration"
put "/portainer_configuration", to: "local/pages#update_portainer_configuration"
get "/github_oauth", to: "local/pages#github_oauth"
root to: "projects#index"
else

View File

@@ -1,51 +0,0 @@
# frozen_string_literal: true
require 'httparty'
require 'yaml'
require 'tempfile'
PORTAINER_URL = "https://portainer.portainer.svc.cluster.local:9443"
namespace :portainer do
desc 'Run Portainer task'
task run: :environment do
jwt = Portainer::Client.authenticate(
provider_url: PORTAINER_URL,
username: 'admin',
auth_code: ENV['PORTAINER_PASSWORD']
)
if jwt.present?
puts "JWT: #{jwt}"
# Get Kubernetes config
config_response = Portainer::Client.new(PORTAINER_URL, jwt).get_kubernetes_config
if config_response.present?
config_yaml = config_response.to_yaml
# Save to temp file
temp_file = Tempfile.new([ 'kubeconfig', '.yaml' ])
temp_file.write(config_yaml)
temp_file.close
puts "Kubeconfig saved to: #{temp_file.path}"
# Run kubectl command with the temp kubeconfig
output = `KUBECONFIG=#{temp_file.path} kubectl get pods 2>&1`
puts "\nKubectl output:"
puts output
# Clean up temp file
temp_file.unlink
puts "\nSUCCESSFULLY REACHED CLUSTER VIA PORTAINER"
else
puts "Error getting config: #{config_response.body}"
end
else
puts "Error: #{response.code}"
puts response.body
end
end
end

View File

@@ -1,15 +0,0 @@
require 'rails_helper'
require 'support/shared_contexts/with_portainer'
RSpec.describe Portainer::Authenticate do
include_context 'with portainer'
let(:user) { create(:user) }
let(:stack_manager) { create(:stack_manager) }
let(:auth_code) { 'auth_code' }
it 'can authenticate with portainer' do
result = described_class.execute(user:, stack_manager:, auth_code:)
expect(result).to be_success
expect(user.providers.first.access_token).to eql('jwt')
end
end

View File

@@ -1,18 +0,0 @@
require 'rails_helper'
require 'support/shared_contexts/with_portainer'
RSpec.describe Portainer::Kubeconfig do
let(:account) { create(:account) }
let(:cluster) { create(:cluster, account:) }
let!(:provider) { create(:provider, :portainer, user: account.owner) }
let!(:stack_manager) { create(:stack_manager, account:) }
context 'gets kubeconfig from portainer' do
include_context 'with portainer'
it 'can get kubeconfig from portainer' do
result = described_class.execute(cluster:, user: account.owner)
expect(result).to be_success
expect(result.kubeconfig).to eql(JSON.parse(File.read(Rails.root.join(*%w[spec resources portainer kubeconfig.json]))))
end
end
end

View File

@@ -1,19 +0,0 @@
require 'rails_helper'
require 'support/shared_contexts/with_portainer'
RSpec.describe Portainer::SyncClusters do
let(:account) { create(:account) }
let!(:provider) { create(:provider, :portainer, user: account.owner) }
let!(:stack_manager) { create(:stack_manager, account:) }
context 'syncs clusters from portainer' do
include_context 'with portainer'
it 'can sync clusters from portainer' do
result = described_class.execute(user: account.owner, account:)
expect(result).to be_success
expect(result.clusters.count).to eql(2)
expect(result.clusters.first.name).to eql('local')
expect(result.clusters.last.name).to eql('testing-production')
end
end
end

View File

@@ -1,3 +0,0 @@
{
"jwt": "jwt"
}

View File

@@ -1,259 +0,0 @@
[
{
"Id": 1,
"Name": "local",
"Type": 5,
"ContainerEngine": "",
"URL": "https://kubernetes.default.svc",
"GroupId": 1,
"PublicURL": "",
"Gpus": [],
"TLSConfig": {
"TLS": true,
"TLSSkipVerify": true
},
"AzureCredentials": {
"ApplicationID": "",
"TenantID": "",
"AuthenticationKey": ""
},
"TagIds": [],
"Status": 1,
"Snapshots": [],
"UserAccessPolicies": {
"2": {
"RoleId": 4
}
},
"TeamAccessPolicies": {},
"EdgeKey": "",
"ComposeSyntaxMaxVersion": "3.9",
"SecuritySettings": {
"allowBindMountsForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowVolumeBrowserForRegularUsers": false,
"allowHostNamespaceForRegularUsers": true,
"allowDeviceMappingForRegularUsers": true,
"allowStackManagementForRegularUsers": true,
"allowContainerCapabilitiesForRegularUsers": true,
"allowSysctlSettingForRegularUsers": true,
"enableHostManagementFeatures": false
},
"LastCheckInDate": 0,
"QueryDate": 0,
"Heartbeat": false,
"Edge": {
"AsyncMode": false,
"PingInterval": 0,
"SnapshotInterval": 0,
"CommandInterval": 0
},
"AuthorizedUsers": null,
"AuthorizedTeams": null,
"Tags": null,
"StatusMessage": {
"summary": "",
"detail": "",
"operation": "",
"operationStatus": "",
"warnings": null
},
"CloudProvider": null,
"Kubernetes": {
"Snapshots": [
{
"Time": 1756319913,
"KubernetesVersion": "v1.31.1",
"NodeCount": 2,
"TotalCPU": 4,
"TotalMemory": 8210763776,
"DiagnosticsData": null,
"PerformanceMetrics": {
"CPUUsage": 17,
"MemoryUsage": 133,
"NetworkUsage": 133383
}
}
],
"Configuration": {
"UseLoadBalancer": false,
"UseServerMetrics": true,
"EnableResourceOverCommit": true,
"ResourceOverCommitPercentage": 0,
"StorageClasses": [
{
"Name": "do-block-storage",
"AccessModes": ["RWO"],
"Provisioner": "dobs.csi.digitalocean.com",
"AllowVolumeExpansion": true
}
],
"IngressClasses": [
{
"Name": "nginx",
"Type": "nginx",
"Blocked": false,
"BlockedNamespaces": null
}
],
"RestrictDefaultNamespace": false,
"IngressAvailabilityPerNamespace": false,
"AllowNoneIngressClass": false,
"RestrictSecrets": false,
"RestrictStandardUserIngressW": false
},
"Flags": {
"IsServerMetricsDetected": true,
"IsServerIngressClassDetected": true,
"IsServerStorageDetected": true
}
},
"PostInitMigrations": {
"MigrateIngresses": false,
"MigrateGPUs": false,
"MigrateGateKeeper": false,
"MigrateSecretOwners": false
},
"EdgeCheckinInterval": 5,
"Agent": {},
"LocalTimeZone": "",
"ChangeWindow": {
"Enabled": false,
"StartTime": "",
"EndTime": ""
},
"DeploymentOptions": null,
"EnableImageNotification": false,
"EnableGPUManagement": false
},
{
"Id": 2,
"Name": "testing-production",
"Type": 6,
"ContainerEngine": "",
"URL": "137.184",
"GroupId": 1,
"PublicURL": "",
"Gpus": null,
"TLSConfig": {
"TLS": true,
"TLSSkipVerify": true
},
"AzureCredentials": {
"ApplicationID": "",
"TenantID": "",
"AuthenticationKey": ""
},
"TagIds": [],
"Status": 1,
"Snapshots": [],
"UserAccessPolicies": {},
"TeamAccessPolicies": {},
"EdgeKey": "",
"ComposeSyntaxMaxVersion": "3.9",
"SecuritySettings": {
"allowBindMountsForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowVolumeBrowserForRegularUsers": false,
"allowHostNamespaceForRegularUsers": true,
"allowDeviceMappingForRegularUsers": true,
"allowStackManagementForRegularUsers": true,
"allowContainerCapabilitiesForRegularUsers": true,
"allowSysctlSettingForRegularUsers": true,
"enableHostManagementFeatures": false
},
"LastCheckInDate": 0,
"QueryDate": 0,
"Heartbeat": false,
"Edge": {
"AsyncMode": false,
"PingInterval": 0,
"SnapshotInterval": 0,
"CommandInterval": 0
},
"AuthorizedUsers": null,
"AuthorizedTeams": null,
"Tags": null,
"StatusMessage": {
"summary": "",
"detail": "",
"operation": "",
"operationStatus": "",
"warnings": null
},
"CloudProvider": {
"Provider": "kubeconfig",
"Name": "KubeConfig",
"URL": "",
"Region": "",
"Size": null,
"NodeCount": 0,
"CPU": null,
"RAM": null,
"HDD": null,
"NetworkID": null,
"CredentialID": 1,
"ResourceGroup": "",
"Tier": "",
"PoolName": "",
"DNSPrefix": "",
"KubernetesVersion": "",
"AmiType": null,
"InstanceType": null,
"NodeVolumeSize": null,
"AddonWithArgs": null,
"NodeIPs": null,
"OfflineInstall": false
},
"Kubernetes": {
"Snapshots": [
{
"Time": 1756319913,
"KubernetesVersion": "",
"NodeCount": 0,
"TotalCPU": 0,
"TotalMemory": 0,
"DiagnosticsData": null,
"PerformanceMetrics": null
}
],
"Configuration": {
"UseLoadBalancer": false,
"UseServerMetrics": false,
"EnableResourceOverCommit": true,
"ResourceOverCommitPercentage": 20,
"StorageClasses": [],
"IngressClasses": [],
"RestrictDefaultNamespace": false,
"IngressAvailabilityPerNamespace": false,
"AllowNoneIngressClass": false,
"RestrictSecrets": false,
"RestrictStandardUserIngressW": false
},
"Flags": {
"IsServerMetricsDetected": true,
"IsServerIngressClassDetected": true,
"IsServerStorageDetected": true
}
},
"PostInitMigrations": {
"MigrateIngresses": false,
"MigrateGPUs": false,
"MigrateGateKeeper": false,
"MigrateSecretOwners": false
},
"EdgeCheckinInterval": 5,
"Agent": {
"Version": "2.27.9"
},
"LocalTimeZone": "",
"ChangeWindow": {
"Enabled": false,
"StartTime": "",
"EndTime": ""
},
"DeploymentOptions": null,
"EnableImageNotification": false,
"EnableGPUManagement": false
}
]

View File

@@ -1,46 +0,0 @@
{
"kind": "Config",
"apiVersion": "v1",
"preferences": {},
"clusters": [
{
"name": "portainer-cluster-local",
"cluster": {
"server": "https://portainer.portainer.svc.cluster.local:9000/api/endpoints/1/kubernetes",
"insecure-skip-tls-verify": true
}
},
{
"name": "portainer-cluster-testing-production",
"cluster": {
"server": "https://portainer.portainer.svc.cluster.local:9000/api/endpoints/2/kubernetes",
"insecure-skip-tls-verify": true
}
}
],
"users": [
{
"name": "portainer-sa-user-9ec6cb2d-de79-402b-aff3-e4e8e40ae0da-4",
"user": {
"token": "sample_token"
}
}
],
"contexts": [
{
"name": "portainer-ctx-local",
"context": {
"cluster": "portainer-cluster-local",
"user": "portainer-sa-user-9ec6cb2d-de79-402b-aff3-e4e8e40ae0da-4"
}
},
{
"name": "portainer-ctx-testing-production",
"context": {
"cluster": "portainer-cluster-testing-production",
"user": "portainer-sa-user-9ec6cb2d-de79-402b-aff3-e4e8e40ae0da-4"
}
}
],
"current-context": "portainer-ctx-local"
}

View File

@@ -1,5 +1,4 @@
require 'rails_helper'
require 'support/shared_contexts/with_portainer'
RSpec.describe K8::Connection do
let!(:user) { create(:user) }
let(:connection) { described_class.new(cluster, user) }
@@ -12,21 +11,4 @@ RSpec.describe K8::Connection do
end
end
end
describe 'using the K8Stack' do
context 'kubernetes provider is portainer' do
include_context 'with portainer'
let!(:cluster) { create(:cluster, kubeconfig: nil) }
let(:account) { create(:account, owner: user) }
before do
create(:provider, provider: 'portainer', access_token: 'jwt', user:)
create(:stack_manager, account:)
end
it 'returns the kubeconfig' do
expect(connection.kubeconfig).to eq(JSON.parse(File.read(Rails.root.join(*%w[spec resources portainer kubeconfig.json]))))
end
end
end
end

View File

@@ -1,20 +0,0 @@
require 'rails_helper'
require 'support/shared_contexts/with_portainer'
RSpec.describe K8Stack do
describe '.fetch_kubeconfig' do
include_context 'with portainer'
context 'when the stack manager is portainer' do
let(:account) { create(:account) }
let!(:provider) { create(:provider, :portainer, user: account.owner) }
let!(:stack_manager) { create(:stack_manager, :portainer, account:) }
let(:cluster) { create(:cluster, account:) }
subject { described_class.fetch_kubeconfig(cluster, account.owner) }
it 'fetches the kubeconfig' do
expect(subject).to eql(JSON.parse(File.read(Rails.root.join(*%w[spec resources portainer kubeconfig.json]))))
end
end
end
end

View File

@@ -1,18 +0,0 @@
require 'rails_helper'
RSpec.shared_context 'with portainer' do
before do
headers = { 'Content-Type' => 'application/json' }
WebMock.stub_request(:any, %r{/api/kubernetes/config}).to_return(
status: 200, body: File.read(Rails.root.join(*%w[spec resources portainer kubeconfig.json])), headers:
)
WebMock.stub_request(:any, %r{/api/endpoints}).to_return(
status: 200, body: File.read(Rails.root.join(*%w[spec resources portainer endpoints.json])), headers:
)
WebMock.stub_request(:any, %r{/api/auth/oauth/validate}).to_return(
status: 200, body: File.read(Rails.root.join(*%w[spec resources portainer authenticate.json])), headers:
)
WebMock.stub_request(:any, %r{/api/auth}).to_return(
status: 200, body: File.read(Rails.root.join(*%w[spec resources portainer authenticate.json])), headers:
)
end
end