diff --git a/.env.test b/.env.test index 9ef2c8a0..60eb6f07 100644 --- a/.env.test +++ b/.env.test @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2539bb92..e07679de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile.dev b/Dockerfile.dev index 1db8b555..533c05a5 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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 diff --git a/TODO.md b/TODO.md index dcb36727..c47e25cc 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/app/actions/local/create_default_user.rb b/app/actions/local/create_default_user.rb new file mode 100644 index 00000000..0512d279 --- /dev/null +++ b/app/actions/local/create_default_user.rb @@ -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 \ No newline at end of file diff --git a/app/actions/projects/create.rb b/app/actions/projects/create.rb index a8b6177a..9e3e3157 100644 --- a/app/actions/projects/create.rb +++ b/app/actions/projects/create.rb @@ -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 diff --git a/app/actions/projects/local/check_docker_exists.rb b/app/actions/projects/local/check_docker_exists.rb new file mode 100644 index 00000000..1d8f0c14 --- /dev/null +++ b/app/actions/projects/local/check_docker_exists.rb @@ -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 \ No newline at end of file diff --git a/app/actions/projects/local/check_github_connectivity.rb b/app/actions/projects/local/check_github_connectivity.rb new file mode 100644 index 00000000..bc3d8280 --- /dev/null +++ b/app/actions/projects/local/check_github_connectivity.rb @@ -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 diff --git a/app/actions/projects/local/create.rb b/app/actions/projects/local/create.rb new file mode 100644 index 00000000..fbb9972a --- /dev/null +++ b/app/actions/projects/local/create.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0b98df9a..2f7b8a72 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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" diff --git a/app/controllers/clusters_controller.rb b/app/controllers/clusters_controller.rb index 35e5d72d..bd574ea1 100644 --- a/app/controllers/clusters_controller.rb +++ b/app/controllers/clusters_controller.rb @@ -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? diff --git a/app/controllers/local/pages_controller.rb b/app/controllers/local/pages_controller.rb new file mode 100644 index 00000000..45c1e547 --- /dev/null +++ b/app/controllers/local/pages_controller.rb @@ -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 diff --git a/app/helpers/local/authentication.rb b/app/helpers/local/authentication.rb new file mode 100644 index 00000000..529f0db0 --- /dev/null +++ b/app/helpers/local/authentication.rb @@ -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 \ No newline at end of file diff --git a/app/views/clusters/_status.html.erb b/app/views/clusters/_status.html.erb index c788e30a..6f748e68 100644 --- a/app/views/clusters/_status.html.erb +++ b/app/views/clusters/_status.html.erb @@ -10,6 +10,13 @@ "error" end %> -
- <%= cluster.status %> +
+
+ <%= cluster.status %> +
+ <% if cluster.status == "failed" %> +
+ <%= button_to "Retry installation", retry_install_cluster_path(cluster), class: "btn btn-primary btn-outline btn-xs" %> +
+ <% end %>
\ No newline at end of file diff --git a/app/views/local/pages/github_token.html.erb b/app/views/local/pages/github_token.html.erb new file mode 100644 index 00000000..7e4210fb --- /dev/null +++ b/app/views/local/pages/github_token.html.erb @@ -0,0 +1,16 @@ +
+

Please go to Github.com and enter your personal access token below.

+<%= link_to "https://github.com/settings/tokens", target: "_blank", class: "btn btn-primary btn-outline" do %> + Go to Github +<% end %> +<%= form_with url: github_token_path, method: :put do |form| %> +
+ <%= form.label :github_token, "Personal access token" %> + <%= form.text_field :github_token, class: "input input-bordered w-full max-w-xs" %> +
+ +<% end %> +
+ diff --git a/app/views/projects/_new_form.erb b/app/views/projects/_new_form.erb index 1c7a0e4a..68740947 100644 --- a/app/views/projects/_new_form.erb +++ b/app/views/projects/_new_form.erb @@ -22,9 +22,13 @@
- <%= 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", + ) %> @@ -40,12 +44,14 @@
-
- -
+ <% unless Rails.application.config.local_mode %> +
+ +
+ <% end %>