Merge pull request #206 from czhu12/chriszhu__flesh_out_previews

Preview apps & better pod logs
This commit is contained in:
Chris Zhu
2025-07-19 11:26:38 -07:00
committed by GitHub
21 changed files with 142 additions and 63 deletions

View File

@@ -17,4 +17,3 @@
- [ ] Pull request preview apps
- [ ] Update vocabulary on landing page
- [ ] Metrics page needs to load even if the cluster is down so that memory outages can be tracked
- [ ] Docker uploads to the same registry even for different branch builds. It probably needs to go to `image:branch`

View File

@@ -40,10 +40,10 @@ class ProjectForks::ForkProject
# Parse and store the config
canine_config = CanineConfig::Definition.parse(file.content, parent_project, pull_request)
child_project.canine_config = canine_config.to_hash
child_project.predeploy_script = canine_config.predeploy_script
child_project.postdeploy_script = canine_config.postdeploy_script
child_project.predestroy_script = canine_config.predestroy_script
child_project.postdestroy_script = canine_config.postdestroy_script
child_project.predeploy_command = canine_config.predeploy_command
child_project.postdeploy_command = canine_config.postdeploy_command
child_project.predestroy_command = canine_config.predestroy_command
child_project.postdestroy_command = canine_config.postdestroy_command
end
context.project_fork.save!
end

View File

@@ -7,19 +7,19 @@ class ProjectForks::InitializeFromCanineConfig
next if context.project_fork.child_project.canine_config.blank?
config_data = context.project_fork.child_project.canine_config
definition = CanineConfig::Definition.new(config_data)
# Create services from the stored config
config_data['services']&.each do |service_config|
params = Service.permitted_params(ActionController::Parameters.new(service: service_config))
service = context.project_fork.child_project.services.build(params)
# Create services from the definition
definition.services.each do |service|
service.project = context.project_fork.child_project
service.save!
end
# Create environment variables from the stored config
config_data['environment_variables']&.each do |env_var|
# Create environment variables from the definition
definition.environment_variables.each do |env_var|
context.project_fork.child_project.environment_variables.create!(
name: env_var['name'],
value: env_var['value']
name: env_var.name,
value: env_var.value
)
end
end

View File

@@ -6,6 +6,7 @@ class AddOns::ProcessesController < AddOns::BaseController
def show
client = K8::Client.new(@add_on.cluster.kubeconfig)
@logs = client.get_pod_log(params[:id], @add_on.name)
@pod_events = client.get_pod_events(params[:id], @add_on.name)
rescue Kubeclient::ResourceNotFoundError
flash[:alert] = "Pod #{params[:id]} not found"
redirect_to add_on_processes_path(@add_on)

View File

@@ -14,6 +14,7 @@ class Projects::ProcessesController < Projects::BaseController
def show
client = K8::Client.new(@project.cluster.kubeconfig)
@logs = client.get_pod_log(params[:id], @project.name)
@pod_events = client.get_pod_events(params[:id], @project.name)
respond_to do |format|
format.html

View File

@@ -31,6 +31,7 @@ class Projects::DeploymentJob < ApplicationJob
deployment.completed!
project.deployed!
postdeploy(project, kubectl)
rescue StandardError => e
@logger.error("Deployment failed: #{e.message}")
puts e.full_message
@@ -53,19 +54,27 @@ class Projects::DeploymentJob < ApplicationJob
end
end
def predeploy(project, kubectl)
return unless project.predeploy_command.present?
@logger.info("Running predeploy command: `#{project.predeploy_command}`...", color: :yellow)
command = K8::Stateless::Command.new(project)
def _run_command(command, kubectl, project, type)
@logger.info("Running command: `#{command}`...", color: :yellow)
command = K8::Stateless::Command.new(project, type, command)
command_yaml = command.to_yaml
command.delete_if_exists!
kubectl.apply_yaml(command_yaml)
# Wait for the predeploy to finish
command.wait_for_completion
# Get logs f
end
def predeploy(project, kubectl)
return unless project.predeploy_command.present?
_run_command(project.predeploy_command, kubectl, project, 'predeploy')
end
def postdeploy(project, kubectl)
return unless project.postdeploy_command.present?
_run_command(project.postdeploy_command, kubectl, project, 'postdeploy')
end
def create_kubectl(deployment, kubeconfig)
runner = Cli::RunAndLog.new(deployment)

View File

@@ -11,11 +11,10 @@
# docker_command :string
# dockerfile_path :string default("./Dockerfile"), not null
# name :string not null
# postdeploy_script :text
# postdestroy_script :text
# predeploy_command :string
# predeploy_script :text
# predestroy_script :text
# postdeploy_command :text
# postdestroy_command :text
# predeploy_command :text
# predestroy_command :text
# project_fork_status :integer default("disabled")
# repository_url :string not null
# status :integer default("creating"), not null

View File

@@ -34,26 +34,38 @@ class CanineConfig::Definition
@definition = definition
end
def predeploy_script
def predeploy_command
definition.dig('scripts', 'predeploy')
end
def postdeploy_script
def postdeploy_command
definition.dig('scripts', 'postdeploy')
end
def predestroy_script
def predestroy_command
definition.dig('scripts', 'predestroy')
end
def postdestroy_script
def postdestroy_command
definition.dig('scripts', 'postdestroy')
end
def services
definition['services'].map do |service|
params = Service.permitted_params(ActionController::Parameters.new(service:))
Service.new(params)
service_instance = Service.new(params)
# Handle domains if present and service is a web_service
if service['service_type'] == 'web_service' && service['domains'].present?
# Ensure allow_public_networking is true when domains are specified
service_instance.allow_public_networking = true
service['domains'].each do |domain_name|
service_instance.domains.build(domain_name: domain_name)
end
end
service_instance
end
end

View File

@@ -11,6 +11,7 @@ module K8
:get_endpoints,
:get_namespaces,
:delete_namespace,
:get_events,
to: :client
)
@@ -42,7 +43,7 @@ module K8
end
def pods_for_namespace(namespace)
@client.get_pods(namespace: namespace)
@client.get_pods(namespace:)
end
def pods_for_service(service_name, namespace)
@@ -66,6 +67,13 @@ module K8
@kubeconfig["current-context"]
end
def get_pod_events(pod_name, namespace)
get_events(
namespace: namespace,
field_selector: "involvedObject.name=#{pod_name},involvedObject.kind=Pod"
)
end
private
def load_kubeconfig(kubeconfig_string)

View File

@@ -1,20 +1,18 @@
class K8::Stateless::Command < K8::Base
attr_accessor :project
attr_accessor :project, :type, :command
def initialize(project)
def initialize(project, type, command)
@project = project
@type = type
@command = command
end
def kubectl
@kubectl ||= K8::Kubectl.new(project.cluster.kubeconfig)
end
def command
project.predeploy_command
end
def name
"#{project.name}-predeployment"
"#{project.name}-#{type}"
end
def namespace

View File

@@ -1,3 +1,3 @@
<%= add_on_layout(@add_on) do %>
<%= render "log_outputs/pod_logs", logs: @logs, back_path: add_on_processes_path(@add_on) %>
<%= render "log_outputs/pod_logs", logs: @logs, pod_events: @pod_events, back_path: add_on_processes_path(@add_on) %>
<% end %>

View File

@@ -11,4 +11,19 @@
<pre class="text-sm font-mono whitespace-pre-wrap" id="logs"><%= ansi_to_tailwind(logs.force_encoding("UTF-8")).html_safe || "No logs yet..." %></pre>
</div>
</div>
<div class="flex space-x-2 items-center mt-8">
<h4 class="text-lg font-medium">Start Up Logs</h4>
</div>
<div class="bg-gray-900 text-gray-100 rounded-lg shadow-lg mt-4">
<div class="overflow-auto h-48 bg-gray-800 p-2 rounded">
<pre class="text-sm font-mono whitespace-pre-wrap" id="startup-logs"><%
event_logs = pod_events.map do |event|
timestamp_str = event.lastTimestamp || event.firstTimestamp || event.metadata.creationTimestamp
timestamp = Time.parse(timestamp_str)
"[#{timestamp.strftime('%Y-%m-%d %H:%M:%S')}] #{event.reason}: #{event.message}"
end.join("\n")
%><%= ansi_to_tailwind(event_logs.force_encoding("UTF-8")).html_safe %></pre>
</div>
</div>
</div>

View File

@@ -65,7 +65,7 @@
</div>
</div>
<% else %>
<div role="tooltip" data-tip="Please add a service to your project to deploy" class="tooltip tooltip-secondary">
<div role="tooltip" data-tip="Please add a service to your project to deploy" class="tooltip tooltip-secondary tooltip-left">
<%= button_to "#", class: "btn btn-primary m-1", disabled: true do %>
Deploy
<% end %>

View File

@@ -1,3 +1,3 @@
<%= project_layout(@project) do %>
<%= render "log_outputs/pod_logs", logs: @logs, back_path: project_processes_path(@project) %>
<%= render "log_outputs/pod_logs", logs: @logs, pod_events: @pod_events, back_path: project_processes_path(@project) %>
<% end %>

View File

@@ -0,0 +1,9 @@
class FixProjectCommands < ActiveRecord::Migration[7.2]
def change
remove_column :projects, :predeploy_script, :text
change_column :projects, :predeploy_command, :text
rename_column :projects, :postdeploy_script, :postdeploy_command
rename_column :projects, :predestroy_script, :predestroy_command
rename_column :projects, :postdestroy_script, :postdestroy_command
end
end

11
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_06_29_164951) do
ActiveRecord::Schema[7.2].define(version: 2025_07_19_160150) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -254,16 +254,15 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_29_164951) do
t.string "dockerfile_path", default: "./Dockerfile", null: false
t.string "docker_build_context_directory", default: ".", null: false
t.string "docker_command"
t.string "predeploy_command"
t.text "predeploy_command"
t.integer "status", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "container_registry_url"
t.jsonb "canine_config", default: {}
t.text "predeploy_script"
t.text "postdeploy_script"
t.text "predestroy_script"
t.text "postdestroy_script"
t.text "postdeploy_command"
t.text "predestroy_command"
t.text "postdestroy_command"
t.bigint "project_fork_cluster_id"
t.integer "project_fork_status", default: 0
t.index ["cluster_id"], name: "index_projects_on_cluster_id"

View File

@@ -93,10 +93,10 @@ RSpec.describe ProjectForks::ForkProject do
expect(child_project.canine_config['services'].first['name']).to eq('web')
expect(child_project.canine_config['environment_variables']).to be_an(Array)
expect(child_project.canine_config['environment_variables'].first['name']).to eq('DATABASE_URL')
expect(child_project.predeploy_script).to eq('echo "Pre deploy script"')
expect(child_project.postdeploy_script).to eq('echo "Post deploy script"')
expect(child_project.predestroy_script).to eq('echo "Pre destroy script"')
expect(child_project.postdestroy_script).to eq('echo "Post destroy script"')
expect(child_project.predeploy_command).to eq('echo "Pre deploy script"')
expect(child_project.postdeploy_command).to eq('echo "Post deploy script"')
expect(child_project.predestroy_command).to eq('echo "Pre destroy script"')
expect(child_project.postdestroy_command).to eq('echo "Post destroy script"')
end
end

View File

@@ -11,11 +11,10 @@
# docker_command :string
# dockerfile_path :string default("./Dockerfile"), not null
# name :string not null
# postdeploy_script :text
# postdestroy_script :text
# predeploy_command :string
# predeploy_script :text
# predestroy_script :text
# postdeploy_command :text
# postdestroy_command :text
# predeploy_command :text
# predestroy_command :text
# project_fork_status :integer default("disabled")
# repository_url :string not null
# status :integer default("creating"), not null

View File

@@ -11,11 +11,10 @@
# docker_command :string
# dockerfile_path :string default("./Dockerfile"), not null
# name :string not null
# postdeploy_script :text
# postdestroy_script :text
# predeploy_command :string
# predeploy_script :text
# predestroy_script :text
# postdeploy_command :text
# postdestroy_command :text
# predeploy_command :text
# predestroy_command :text
# project_fork_status :integer default("disabled")
# repository_url :string not null
# status :integer default("creating"), not null

View File

@@ -0,0 +1,26 @@
scripts:
predeploy: echo "Pre deploy script"
postdeploy: echo "Post deploy script"
predestroy: echo "Pre destroy script"
postdestroy: echo "Post destroy script"
services:
- name: web
container_port: 3000
service_type: web_service
domains:
- example.com
- www.example.com
- api.example.com
- name: backend
container_port: 8080
service_type: web_service
domains:
- backend.example.com
- name: worker
container_port: 6379
service_type: background_service
environment_variables:
- name: DATABASE_URL
value: postgres://localhost/test
- name: REDIS_URL
value: redis://localhost:6379

View File

@@ -21,6 +21,9 @@ RSpec.describe CanineConfig::Definition do
- name: "web"
container_port: 3000
service_type: "web_service"
domains:
- example.com
- www.example.com
environment_variables:
- name: "API_KEY"
value: "test-key"
@@ -36,7 +39,8 @@ RSpec.describe CanineConfig::Definition do
{
'name' => 'web',
'container_port' => 3000,
'service_type' => 'web_service'
'service_type' => 'web_service',
'domains' => [ 'example.com', 'www.example.com' ]
}
],
'environment_variables' => [
@@ -148,7 +152,8 @@ RSpec.describe CanineConfig::Definition do
'name' => 'web',
'container_port' => 3000,
'service_type' => 'web_service',
'extra_field' => 'should_be_filtered'
'extra_field' => 'should_be_filtered',
'domains' => [ 'example.com', 'www.example.com' ]
},
{
'name' => 'worker',