mirror of
https://github.com/czhu12/canine.git
synced 2026-01-06 03:30:16 -06:00
Merge pull request #206 from czhu12/chriszhu__flesh_out_previews
Preview apps & better pod logs
This commit is contained in:
1
TODO.md
1
TODO.md
@@ -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`
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
9
db/migrate/20250719160150_fix_project_commands.rb
Normal file
9
db/migrate/20250719160150_fix_project_commands.rb
Normal 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
11
db/schema.rb
generated
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
26
spec/resources/canine_config/example_with_domains.yaml
Normal file
26
spec/resources/canine_config/example_with_domains.yaml
Normal 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
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user