Add HTTP gem for request proxying and enhance application layout

This commit is contained in:
Dylan Knutson
2025-01-04 20:32:27 +00:00
parent 02f40215e9
commit 304b9bd5d0
15 changed files with 3289 additions and 4 deletions

View File

@@ -56,6 +56,11 @@ services:
- GF_SERVER_HTTP_PORT=3100
- GF_USERS_ALLOW_SIGN_UP=false
- GF_LOG_LEVEL=debug
- GF_SERVER_ROOT_URL=http://localhost:3100/grafana/
- GF_SERVER_SERVE_FROM_SUB_PATH=false
- GF_AUTH_PROXY_ENABLED=true
- GF_AUTH_PROXY_HEADER_NAME=X-WEBAUTH-USER
- GF_AUTH_PROXY_HEADER_PROPERTY=username
volumes:
- devcontainer-redux-grafana-data:/var/lib/grafana

View File

@@ -116,6 +116,7 @@ gem "daemons"
gem "discard"
gem "good_job", "~> 4.6"
gem "http-cookie"
gem "http", "~> 5.2" # For proxying requests
gem "kaminari"
gem "nokogiri"
gem "pluck_each"

View File

@@ -168,6 +168,9 @@ GEM
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
rake
fugit (1.11.1)
et-orbi (~> 1, >= 1.2.11)
raabro (~> 1.4)
@@ -193,8 +196,15 @@ GEM
bigdecimal
rake (>= 13)
htmlbeautifier (1.4.3)
http (5.2.0)
addressable (~> 2.8)
base64 (~> 0.1)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
llhttp-ffi (~> 0.5.0)
http-cookie (1.0.8)
domain_name (~> 0.5)
http-form_data (2.3.0)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
io-console (0.8.0)
@@ -221,6 +231,9 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
llhttp-ffi (0.5.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
logger (1.6.4)
loofah (2.23.1)
crass (~> 1.0.2)
@@ -538,6 +551,7 @@ DEPENDENCIES
faiss
good_job (~> 4.6)
htmlbeautifier
http (~> 5.2)
http-cookie
jbuilder (~> 2.13)
kaminari

View File

@@ -1,5 +1,5 @@
rails: RAILS_ENV=development bundle exec rails s -p 3000
wp-client: RAILS_ENV=development HMR=true ./bin/webpacker-dev-server
wp-server: RAILS_ENV=development HMR=true SERVER_BUNDLE_ONLY=yes ./bin/webpacker --watch
css: RAILS_ENV=development yarn build:css[debug] --watch
css: RAILS_ENV=development yarn "build:css[debug]" --watch
prometheus_exporter: RAILS_ENV=development bundle exec prometheus_exporter --bind 0.0.0.0 --prefix redux_ --label '{"environment": "development"}'

View File

@@ -1,5 +1,5 @@
rails: RAILS_ENV=staging ./bin/rails s -p 3001
wp-client: RAILS_ENV=development HMR=true ./bin/webpacker-dev-server
wp-server: RAILS_ENV=development HMR=true SERVER_BUNDLE_ONLY=yes ./bin/webpacker --watch
css: RAILS_ENV=development yarn build:css[debug] --watch
css: RAILS_ENV=development yarn "build:css[debug]" --watch
prometheus_exporter: RAILS_ENV=staging bundle exec prometheus_exporter --bind 0.0.0.0 --prefix redux_ --label '{"environment": "staging"}'

View File

@@ -0,0 +1,84 @@
# typed: true
# frozen_string_literal: true
class Admin::ProxyController < ApplicationController
before_action :authenticate_user!
before_action :require_admin!
skip_before_action :verify_authenticity_token, only: %i[grafana prometheus]
def grafana
fullpath =
"http://grafana:3100#{request.fullpath.delete_prefix("/grafana")}"
proxy_response(fullpath, "/grafana")
end
def prometheus
fullpath = "http://prometheus:9090#{request.fullpath.delete_prefix("/prometheus")}"
proxy_response(fullpath, "/prometheus")
end
private
def require_admin!
unless current_user&.admin?
redirect_to root_path, alert: "You are not authorized to access this area"
end
end
def grafana_proxy_headers
{ "X-WEBAUTH-USER" => "admin" }.merge(proxy_headers)
end
def proxy_headers
{
"X-Forwarded-Host" => request.host_with_port,
"X-Forwarded-Proto" => request.ssl? ? "https" : "http",
"X-Forwarded-For" => request.remote_ip,
"Host" => request.host,
"Connection" => request.headers["Connection"],
"Upgrade" => request.headers["Upgrade"],
"Accept" => request.headers["Accept"],
"Cookie" => request.headers["Cookie"],
"Content-Type" => request.headers["Content-Type"],
"Content-Length" => request.headers["Content-Length"],
}.merge
end
def websocket_request?
request.headers["Connection"]&.include?("upgrade")
end
def proxy_response(fullpath, prefix)
method = request.method.downcase.to_s
if method == "post"
response = HTTP.headers(grafana_proxy_headers).send(method, fullpath, body: request.raw_post)
else
response = HTTP.headers(grafana_proxy_headers).send(method, fullpath)
end
headers = response.headers.to_h
# Handle redirects by rewriting the Location header
if response.code.in?([301, 302, 303, 307, 308]) &&
headers["Location"].present?
location = headers["Location"]
# Strip the host from absolute URLs
location = location.gsub(%r{^https?://[^/]+}, "")
# Add our prefix to relative URLs
location = "#{prefix}#{location}" if location.start_with?("/")
headers["Location"] = location
end
# Pass through the response with all headers
response_headers = headers.except("Content-Type")
render_args = {
body: response.body.to_s,
status: response.code,
content_type: headers["Content-Type"],
headers: response_headers,
}
render_args[:location] = headers["Location"] if headers["Location"]
render render_args
end
end

View File

@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
extend T::Sig
extend T::Helpers
include Pundit::Authorization
include Devise::Controllers::Helpers::ClassMethods
before_action do
if Rails.env.development? || Rails.env.staging?

View File

@@ -9,6 +9,8 @@ interface UserMenuProps {
csrfToken: string;
globalStatesPath: string;
goodJobPath: string;
grafanaPath: string;
prometheusPath: string;
}
export const UserMenu: React.FC<UserMenuProps> = ({
@@ -19,6 +21,8 @@ export const UserMenu: React.FC<UserMenuProps> = ({
csrfToken,
globalStatesPath,
goodJobPath,
grafanaPath,
prometheusPath,
}) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
@@ -102,6 +106,20 @@ export const UserMenu: React.FC<UserMenuProps> = ({
<i className="fas fa-tasks mr-2 w-5" />
<span>Jobs Queue</span>
</a>
<a
href={grafanaPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-chart-line mr-2 w-5" />
<span>Grafana</span>
</a>
<a
href={prometheusPath}
className="flex w-full items-center px-4 py-2 text-sm text-slate-700 hover:bg-slate-100"
>
<i className="fas fa-chart-bar mr-2 w-5" />
<span>Prometheus</span>
</a>
</>
)}

View File

@@ -35,7 +35,9 @@
signOutPath: destroy_user_session_path,
csrfToken: form_authenticity_token,
globalStatesPath: global_states_path,
goodJobPath: good_job_path
goodJobPath: good_job_path,
grafanaPath: grafana_path,
prometheusPath: prometheus_path
}) %>
<% else %>
<%= link_to new_user_session_path, class: "text-slate-600 hover:text-slate-900" do %>

View File

@@ -65,6 +65,8 @@ Rails.application.routes.draw do
authenticate :user, ->(user) { user.admin? } do
mount GoodJob::Engine => "jobs"
match "grafana(/*path)", to: "admin/proxy#grafana", via: :all, as: :grafana
match "prometheus(/*path)", to: "admin/proxy#prometheus", via: :all, as: :prometheus
end
scope constraints: VpnOnlyRouteConstraint.new do

View File

@@ -10,3 +10,4 @@
--suppress-payload-superclass-redefinition-for=Net::IMAP::MessageSet
--suppress-payload-superclass-redefinition-for=Net::IMAP::QuotedString
--suppress-payload-superclass-redefinition-for=Net::IMAP::RawData

3142
sorbet/rbi/gems/http@5.2.0.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# typed: strict
module Devise::Controllers::Helpers::ClassMethods
extend T::Sig
sig { returns(T.nilable(User)) }
def current_user
end
end

View File

@@ -0,0 +1,5 @@
# typed: strict
module LLHttp
class Delegate
end
end

View File

@@ -1,8 +1,10 @@
# typed: true
# frozen_string_literal: true
require "./app/lib/has_color_logger"
require "./spec/helpers/debug_helpers"
require "./spec/helpers/spec_helpers"
require "./spec/helpers/http_client_mock_helpers"
require "./spec/helpers/perform_job_helpers"
require "./spec/support/matchers/html_matchers"
require "active_support/concern"
require "active_support/core_ext/integer/time"