added local mode

This commit is contained in:
Chris Zhu
2024-11-08 20:50:20 -08:00
parent e9818a1dbf
commit 5ae231dcf2
23 changed files with 347 additions and 30 deletions

View File

@@ -2,3 +2,4 @@ APP_HOST=canine.example.com
OMNIAUTH_GITHUB_WEBHOOK_SECRET=1234567890
OMNIAUTH_GITHUB_PUBLIC_KEY=1234567890
OMNIAUTH_GITHUB_PRIVATE_KEY=1234567890
LOCAL_MODE=true

View File

@@ -20,14 +20,30 @@ RUN apt-get update -qq && \
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
BUNDLE_WITHOUT="development" \
DOCKER_ENV="1"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
# Install packages needed to build gems and kubernetes tools
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libpq-dev pkg-config && \
apt-get install --no-install-recommends -y build-essential git libpq-dev pkg-config \
curl \
ca-certificates \
gnupg && \
# Install Docker CLI
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list && \
apt-get update && \
apt-get install -y docker-ce-cli && \
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
chmod +x kubectl && \
mv kubectl /usr/local/bin/ && \
# Install helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash && \
# Cleanup
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install application gems

View File

@@ -16,6 +16,16 @@ RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
# Make the Ruby binary available on the PATH
ENV PATH="/opt/rubies/ruby-3.3.0/bin:/usr/local/bundle/bin:${PATH}"
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
gnupg && \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian bullseye stable" > /etc/apt/sources.list.d/docker.list && \
apt-get update && \
apt-get install -y docker-ce-cli && \
rm -rf /var/lib/apt/lists/*
ARG NODE_VERSION=20.11.0
ARG YARN_VERSION=1.22.21
ENV BINDING="0.0.0.0" \
@@ -35,6 +45,12 @@ RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz
# App dependencies
RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends imagemagick libvips libvips-dev libvips-tools poppler-utils libpq-dev postgresql-client && \
curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \
chmod +x ./kubectl && \
mv ./kubectl /usr/local/bin/kubectl && \
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 && \
chmod +x get_helm.sh && \
./get_helm.sh && \
rm -rf /var/lib/apt/lists/* /var/cache/apt
# App

View File

@@ -2,6 +2,13 @@
- [ ] Onboarding flow (connect github)
- [ ] Allow connecting to github separately from the app, not just on sign up
- [ ] Make a single user mode
- [x] LOCAL_MODE=true
- [x] Automatically create a user if they don't exist
- [x] use http basic auth
- [ ] How to clone from github?
- [ ] How to build a docker image?
- [ ] How to upload docker image to github container registry?
- [ ] I want a way to “stop” the processes, can maybe do this with a replicas=0 setting
- [ ] Rebulid metrics tabs so it works for both clusters & pods
https://overcast.blog/zero-downtime-deployments-with-kubernetes-a-full-guide-71019397b924?gi=95ab85c45634

View File

@@ -0,0 +1,17 @@
class Local::CreateDefaultUser
extend LightService::Action
promises :user, :account
executed do |context|
ActiveRecord::Base.transaction do
context.user = User.first || User.new
context.user.email = "#{ENV.fetch("USERNAME", SecureRandom.uuid)}@example.com"
context.user.password = ENV.fetch("PASSWORD", "password")
context.user.password_confirmation = ENV.fetch("PASSWORD", "password")
context.user.save!
context.account = context.user.accounts.first || context.user.accounts.create!(name: "Default")
AccountUser.find_or_create_by!(account: context.account, user: context.user)
end
end
end

View File

@@ -5,12 +5,19 @@ module Projects
extend LightService::Organizer
def self.call(project, params)
with(project:, params:).reduce(
steps = [
Projects::ValidateGithubRepository,
Projects::Save,
Projects::RegisterGithubWebhook,
Projects::DeployLatestCommit
)
Projects::Save
]
# Only register webhook in non-local mode
unless Rails.application.config.local_mode
steps << Projects::RegisterGithubWebhook
end
steps << Projects::DeployLatestCommit
with(project:, params:).reduce(*steps)
end
end
end

View File

@@ -0,0 +1,38 @@
class Projects::Local::CheckDockerExists
extend LightService::Action
expects :checklist
promises :checklist
executed do |context|
# Check if docker binary exists in PATH
context.checklist << "Checking docker binary exists..."
unless system('which docker > /dev/null 2>&1')
context.fail!("Docker is not installed or not in PATH")
return
end
# Check if current user can run docker commands
context.checklist << "Checking current user can run docker commands..."
unless system('docker info > /dev/null 2>&1')
context.fail!("Cannot access Docker. Please ensure the current user has proper permissions")
return
end
# Get Docker version and check if it meets minimum requirements
context.checklist << "Checking Docker version..."
version_output = `docker version --format '{{.Server.Version}}' 2>/dev/null`
if $?.success?
current_version = Gem::Version.new(version_output.strip)
minimum_version = Gem::Version.new('20.10.0') # Adjust minimum version as needed
if current_version < minimum_version
context.fail!("Docker version #{current_version} is below minimum required version #{minimum_version}")
return
end
else
context.fail!("Unable to determine Docker version")
return
end
end
end

View File

@@ -0,0 +1,40 @@
class Projects::Local::CheckGithubConnectivity
extend LightService::Action
expects :checklist
promises :checklist
executed do |context|
path = context.project.repository_url
# Check if directory exists
context.checklist << "Checking project directory exists..."
unless Dir.exist?(path)
context.fail!("Project directory does not exist: #{path}")
next context
end
# Check if it's a git repository
context.checklist << "Checking if it's a git repository..."
unless system("git -C #{path} rev-parse --git-dir > /dev/null 2>&1")
context.fail!("Not a valid git repository: #{path}")
next context
end
# Check if remote origin is GitHub
context.checklist << "Checking if remote origin is GitHub..."
remote_url = `git -C #{path} config --get remote.origin.url`.strip
unless remote_url.include?('github.com')
context.fail!("Remote origin is not GitHub: #{remote_url}")
next context
end
# Test SSH connectivity to GitHub (timeout after 5 seconds)
context.checklist << "Checking SSH connectivity to GitHub..."
ssh_test = system('ssh -o BatchMode=yes -o ConnectTimeout=5 -T git@github.com 2>&1')
unless ssh_test
context.fail!("SSH connectivity to GitHub failed")
next context
end
end
end

View File

@@ -0,0 +1,11 @@
class Projects::Local::Create
extend LightService::Organizer
def call
with(project:, params:, checklist: []).reduce(
Projects::Local::CheckDockerExists,
Projects::Local::CheckGithubConnectivity,
Projects::Save
)
end
end

View File

@@ -8,20 +8,23 @@ class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :authenticate_user!
if Rails.application.config.local_mode
include Local::Authentication
else
before_action :authenticate_user!
end
layout :determine_layout
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
protected
def current_account
return nil unless user_signed_in?
@current_account ||= current_user.accounts.find_by(id: session[:account_id]) || current_user.accounts.first
end
helper_method :current_account
def time_ago(t)
if t.present?
"#{time_ago_in_words(t)} ago"

View File

@@ -2,8 +2,8 @@ class ClustersController < ApplicationController
before_action :set_cluster, only: [
:show, :edit, :update, :destroy,
:test_connection, :download_kubeconfig, :logs, :download_yaml,
:retry_install
]
skip_before_action :authenticate_user!, only: [:new]
# GET /clusters
def index
@@ -33,6 +33,11 @@ class ClustersController < ApplicationController
def logs
end
def retry_install
InstallClusterJob.perform_later(@cluster)
redirect_to @cluster, notice: "Retrying installation for cluster..."
end
def test_connection
client = K8::Client.new(@cluster.kubeconfig)
if client.can_connect?

View File

@@ -0,0 +1,24 @@
class Local::PagesController < ApplicationController
skip_before_action :set_github_token_if_not_exists
def github_token
end
def update_github_token
client = Octokit::Client.new(access_token: params[:github_token])
provider = current_user.providers.find_or_initialize_by(provider: "github")
provider.update!(access_token: params[:github_token])
username = client.user[:login]
provider.auth = {
info: {
nickname: username
}
}.to_json
provider.save!
flash[:notice] = "Your Github account (#{username}) has been connected"
redirect_to root_path
rescue Octokit::Unauthorized
flash[:error] = "Invalid personal access token"
redirect_to github_token_path
end
end

View File

@@ -0,0 +1,43 @@
module Local::Authentication
extend ActiveSupport::Concern
included do
before_action :authenticate_user!
before_action :set_github_token_if_not_exists
end
def set_github_token_if_not_exists
if current_user.providers.empty?
# Redirect to github
redirect_to github_token_path
end
end
def current_user
@current_user
end
def current_account
@current_account
end
def authenticate_user!
if User.count.zero?
Local::CreateDefaultUser.execute
end
if ENV["USERNAME"] && ENV["PASSWORD"]
authenticate_or_request_with_http_basic do |username, password|
@current_user = User.find_by!(email: "#{username}@example.com")
@current_account = @current_user.accounts.first
end
else
@current_user = User.first
@current_account = @current_user.accounts.first
end
rescue StandardError => e
Rails.logger.error "Error authenticating user: #{e.message}"
# Logout http basic auth
request_http_basic_authentication
end
end

View File

@@ -10,6 +10,13 @@
"error"
end
%>
<div aria-label="Badge" class="badge border-0 bg-<%= badge_color %>/10 font-medium capitalize text-<%= badge_color %>">
<%= cluster.status %>
<div class="flex items-center justify-between">
<div aria-label="Badge" class="badge border-0 bg-<%= badge_color %>/10 font-medium capitalize text-<%= badge_color %>">
<%= cluster.status %>
</div>
<% if cluster.status == "failed" %>
<div>
<%= button_to "Retry installation", retry_install_cluster_path(cluster), class: "btn btn-primary btn-outline btn-xs" %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,16 @@
<div class="space-y-4">
<h4 class="text-lg font-bold">Please go to Github.com and enter your personal access token below.</h4>
<%= link_to "https://github.com/settings/tokens", target: "_blank", class: "btn btn-primary btn-outline" do %>
<iconify-icon icon="mdi:github"></iconify-icon> Go to Github
<% end %>
<%= form_with url: github_token_path, method: :put do |form| %>
<div class="form-group">
<%= form.label :github_token, "Personal access token" %>
<%= form.text_field :github_token, class: "input input-bordered w-full max-w-xs" %>
</div>
<div class="form-footer">
<%= form.submit "Save", class: "btn btn-primary" %>
</div>
<% end %>
</div>

View File

@@ -22,9 +22,13 @@
<div class="form-control mt-1 w-full max-w-xs">
<label class="label">
<span class="label-text">Repository path</span>
<span class="label-text">Github Repository</span>
</label>
<%= form.text_field :repository_url, class: "input input-bordered w-full focus:outline-offset-0", placeholder: "accountname/repo" %>
<%= form.text_field(
:repository_url,
class: "input input-bordered w-full focus:outline-offset-0",
placeholder: "accountname/repo",
) %>
<label class="label">
<span class="label-text-alt">* Required</span>
</label>
@@ -40,12 +44,14 @@
</label>
</div>
<div class="form-control w-full max-w-xs rounded-lg bg-base-200 p-2 px-4">
<label class="label mt-1">
<span class="label-text cursor-pointer">Autodeploy</span>
<%= form.check_box :autodeploy, class: "checkbox" %>
</label>
</div>
<% unless Rails.application.config.local_mode %>
<div class="form-control w-full max-w-xs rounded-lg bg-base-200 p-2 px-4">
<label class="label mt-1">
<span class="label-text cursor-pointer">Autodeploy</span>
<%= form.check_box :autodeploy, class: "checkbox" %>
</label>
</div>
<% end %>
<div class="form-control mt-1 w-full max-w-xs">
<label class="label">

View File

@@ -1,6 +1,6 @@
<% flash.each do |msg_type, message| %>
<div>
<div class="rounded-none alert <%= msg_type == 'notice' ? 'alert-success' : 'alert-error' %> alert-dismissible fade show" role="alert">
<div class="!rounded-none alert <%= msg_type == 'notice' ? 'alert-success' : 'alert-error' %> alert-dismissible fade show" role="alert">
<%= message %>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>

View File

@@ -1,12 +1,16 @@
#!/bin/bash -e
# Enable jemalloc for reduced memory usage and latency.
echo "Executing pre-entrypoint script..."
if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then
export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)"
fi
# If running the rails server then create or migrate existing database
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then
# If running the rails server or bin/dev then create or migrate existing database
echo "Checking if database needs to be prepared..."
if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ] || [ "${1}" == "./bin/dev" ]; then
echo "Preparing database..."
./bin/rails db:create
./bin/rails db:prepare
fi

View File

@@ -8,6 +8,7 @@ Bundler.require(*Rails.groups)
module Canine
class Application < Rails::Application
config.local_mode = ENV['LOCAL_MODE'] == 'true'
config.active_job.queue_adapter = :sidekiq
config.application_name = Rails.application.class.module_parent_name
# Initialize configuration defaults for originally generated Rails version.

View File

@@ -19,9 +19,21 @@ default: &default
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
docker: &docker
adapter: postgresql
encoding: unicode
pool: 5
username: postgres
password: password
host: postgres
development:
<% if ENV["DOCKER_ENV"] %>
<<: *docker
<% else %>
<<: *default
<% end %>
database: canine_development
# The specified database role being used to connect to PostgreSQL.

View File

@@ -53,9 +53,10 @@ Rails.application.routes.draw do
get :download_yaml
get :logs
end
resource :metrics, only: [ :show ], module: :clusters
resource :metrics, only: [ :show ], module: :clustero
member do
post :test_connection
post :retry_install
end
end
authenticate :user, lambda { |u| u.admin? } do
@@ -83,5 +84,11 @@ end
get "manifest" => "rails/pwa#manifest", as: :pwa_manifest
# Public marketing homepage
root to: "static#index"
if Rails.application.config.local_mode
get "/github_token", to: "local/pages#github_token"
put "/github_token", to: "local/pages#update_github_token"
root to: "projects#index"
else
root to: "static#index"
end
end

View File

@@ -3,7 +3,9 @@ services:
postgres:
image: postgres:16
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_MULTIPLE_DATABASES=canine_production,canine_development
ports:
- "5432:5432"
volumes:
@@ -29,14 +31,14 @@ services:
- "3000:3000"
- "3200:3200"
environment:
- PGHOST=postgres
- PGUSER=postgres
- PGPASSWORD=password
- DATABASE_URL=postgres://postgres:password@postgres:5432
- REDIS_URL=redis://redis:6379
- PORT=3000
- LOCAL_MODE=true
volumes:
- .:/rails
- node_modules:/rails/node_modules
- "/var/run/docker.sock:/var/run/docker.sock"
volumes:
postgres:

34
install.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
set -e
mkdir -p ~/.canine/src
git clone https://github.com/czhu12/canine.git ~/.canine/src
cd ~/.canine/src
# Check if /var/run/docker.sock exists
if [ ! -S /var/run/docker.sock ]; then
echo "Docker socket not found. Please check your Docker installation."
exit 1
fi
# Get the port that the user wants to use, or just default to 3000 if they press enter
read -p "Enter the port number to use (default: 3000):" port
if [ -z "$port" ]; then
port=3000
fi
# Get the username that the user wants to use, or just default to none
read -p "Enter the username to use (default: none):" username
if [ -z "$username" ]; then
username=none
fi
# Get the password that the user wants to use, or just default to none
read -p "Enter the password to use (default: none):" password
if [ -z "$password" ]; then
password=none
fi
# Run docker compose with PORT environment variable
docker-compose up -d PORT=$port USERNAME=$username PASSWORD=$password
# Open browser to http://localhost:$port
open http://localhost:$port