From ed10934e36e0362eeff0da6fc069f4c6a3d53627 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 14 Nov 2025 10:49:53 -0800 Subject: [PATCH 1/2] checkpoint yamls that get created --- app/jobs/projects/deployment_job.rb | 3 +++ app/models/deployment.rb | 6 ++++++ app/models/project.rb | 1 - app/services/k8/kubectl.rb | 9 +++++++++ ...1114025053_add_kubernetes_manifests_to_deployments.rb | 5 +++++ db/schema.rb | 3 +-- spec/factories/projects.rb | 1 - spec/models/project_spec.rb | 1 - 8 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb diff --git a/app/jobs/projects/deployment_job.rb b/app/jobs/projects/deployment_job.rb index d2c7b656..9ce1b245 100644 --- a/app/jobs/projects/deployment_job.rb +++ b/app/jobs/projects/deployment_job.rb @@ -20,6 +20,9 @@ class Projects::DeploymentJob < ApplicationJob apply_config_map(project, kubectl) deploy_volumes(project, kubectl) + kubectl.register_after_apply do |yaml_content| + deployment.add_manifest(yaml_content) + end predeploy(project, kubectl, connection) # For each of the projects services deploy_services(project, kubectl) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 5ec1109b..bd982392 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -24,4 +24,10 @@ class Deployment < ApplicationRecord after_update_commit do self.build.broadcast_build end + + def add_manifest(yaml) + self.manifests ||= { 'files' => [] } + self.manifests['files'] << yaml + save! + end end diff --git a/app/models/project.rb b/app/models/project.rb index 62e0a059..6cb47e2f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,7 +8,6 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null -# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text diff --git a/app/services/k8/kubectl.rb b/app/services/k8/kubectl.rb index 1d744a24..399852e2 100644 --- a/app/services/k8/kubectl.rb +++ b/app/services/k8/kubectl.rb @@ -11,6 +11,11 @@ class K8::Kubectl raise "Kubeconfig is required" end @runner = runner + @after_apply_blocks = [] + end + + def register_after_apply(&block) + @after_apply_blocks << block end def apply_yaml(yaml_content) @@ -25,6 +30,10 @@ class K8::Kubectl runner.call(command, envs: { "KUBECONFIG" => kubeconfig_file.path }) end end + + @after_apply_blocks.each do |block| + block.call(yaml_content) + end end def call(command) diff --git a/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb b/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb new file mode 100644 index 00000000..85d17ca3 --- /dev/null +++ b/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb @@ -0,0 +1,5 @@ +class AddKubernetesManifestsToDeployments < ActiveRecord::Migration[7.2] + def change + add_column :deployments, :manifests, :jsonb + end +end diff --git a/db/schema.rb b/db/schema.rb index 8205bb51..7514b4d1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_11_03_000229) do +ActiveRecord::Schema[7.2].define(version: 2025_11_10_152921) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -414,7 +414,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_03_000229) do t.boolean "autodeploy", default: true, null: false t.string "dockerfile_path", default: "./Dockerfile", null: false t.string "docker_build_context_directory", default: ".", null: false - t.string "docker_command" t.text "predeploy_command" t.integer "status", default: 0, null: false t.datetime "created_at", null: false diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 6d530c85..df1091ad 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,7 +8,6 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null -# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 65932eb0..5e7b325b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8,7 +8,6 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null -# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text From db7eb2ad4c1c0859c82bbb369d1e67b08713a25b Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 14 Nov 2025 14:16:50 -0800 Subject: [PATCH 2/2] added kubernetes manifests section --- ...roller.js => content_toggle_controller.js} | 0 .../manifest_browser_controller.js | 86 ++++++++++++++ app/jobs/projects/deployment_job.rb | 6 +- app/models/deployment.rb | 14 ++- app/models/project.rb | 1 + app/views/clusters/edit.html.erb | 20 ++-- .../deployments/_manifest_browser.html.erb | 45 ++++++++ app/views/projects/deployments/show.html.erb | 43 ++++++- ...add_kubernetes_manifests_to_deployments.rb | 2 +- db/schema.rb | 17 +-- spec/factories/deployments.rb | 1 + spec/factories/projects.rb | 1 + spec/models/deployment_spec.rb | 105 ++++++++++++++++++ spec/models/project_spec.rb | 1 + 14 files changed, 308 insertions(+), 34 deletions(-) rename app/javascript/controllers/{kubeconfig_editor_controller.js => content_toggle_controller.js} (100%) create mode 100644 app/javascript/controllers/manifest_browser_controller.js create mode 100644 app/views/projects/deployments/_manifest_browser.html.erb create mode 100644 spec/models/deployment_spec.rb diff --git a/app/javascript/controllers/kubeconfig_editor_controller.js b/app/javascript/controllers/content_toggle_controller.js similarity index 100% rename from app/javascript/controllers/kubeconfig_editor_controller.js rename to app/javascript/controllers/content_toggle_controller.js diff --git a/app/javascript/controllers/manifest_browser_controller.js b/app/javascript/controllers/manifest_browser_controller.js new file mode 100644 index 00000000..bd1a0858 --- /dev/null +++ b/app/javascript/controllers/manifest_browser_controller.js @@ -0,0 +1,86 @@ +import { Controller } from "@hotwired/stimulus" +import { EditorView, basicSetup } from "codemirror" +import { EditorState } from "@codemirror/state" +import { yaml } from "@codemirror/lang-yaml" +import { oneDark } from "@codemirror/theme-one-dark" + +export default class extends Controller { + static targets = ["file", "content", "filename", "editor"] + + connect() { + this.setupEditor() + } + + disconnect() { + if (this.editorView) { + this.editorView.destroy() + } + } + + setupEditor() { + const initialContent = this.contentTarget.value || '' + + // Create the editor state with YAML syntax highlighting, dark theme, and read-only + const state = EditorState.create({ + doc: initialContent, + extensions: [ + basicSetup, + yaml(), + oneDark, + EditorState.readOnly.of(true), + EditorView.theme({ + "&": { + fontSize: "14px", + border: "1px solid #374151", + borderRadius: "0.5rem", + height: "450px" + }, + ".cm-content": { + padding: "12px" + }, + ".cm-scroller": { + fontFamily: "'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace", + overflow: "auto" + } + }) + ] + }) + + // Create the editor view + this.editorView = new EditorView({ + state, + parent: this.editorTarget + }) + + // Hide the original textarea + this.contentTarget.style.display = 'none' + } + + selectFile(event) { + const fileButton = event.currentTarget + const manifestKey = fileButton.dataset.manifestKey + + // Update active state + this.fileTargets.forEach(file => { + file.classList.remove("active") + }) + fileButton.classList.add("active") + + // Update content display + const content = fileButton.dataset.manifestContent + + // Update CodeMirror editor + if (this.editorView) { + this.editorView.dispatch({ + changes: { + from: 0, + to: this.editorView.state.doc.length, + insert: content + } + }) + } + + // Update filename display + this.filenameTarget.textContent = manifestKey + } +} diff --git a/app/jobs/projects/deployment_job.rb b/app/jobs/projects/deployment_job.rb index 9ce1b245..9293ac21 100644 --- a/app/jobs/projects/deployment_job.rb +++ b/app/jobs/projects/deployment_job.rb @@ -11,6 +11,9 @@ class Projects::DeploymentJob < ApplicationJob project = deployment.project connection = K8::Connection.new(project, user, allow_anonymous: true) kubectl = create_kubectl(deployment, connection) + kubectl.register_after_apply do |yaml_content| + deployment.add_manifest(yaml_content) + end # Create namespace apply_namespace(project, kubectl) @@ -20,9 +23,6 @@ class Projects::DeploymentJob < ApplicationJob apply_config_map(project, kubectl) deploy_volumes(project, kubectl) - kubectl.register_after_apply do |yaml_content| - deployment.add_manifest(yaml_content) - end predeploy(project, kubectl, connection) # For each of the projects services deploy_services(project, kubectl) diff --git a/app/models/deployment.rb b/app/models/deployment.rb index bd982392..352d44b5 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -3,6 +3,7 @@ # Table name: deployments # # id :bigint not null, primary key +# manifests :jsonb # status :integer default("in_progress"), not null # created_at :datetime not null # updated_at :datetime not null @@ -26,8 +27,17 @@ class Deployment < ApplicationRecord end def add_manifest(yaml) - self.manifests ||= { 'files' => [] } - self.manifests['files'] << yaml + manifest = YAML.safe_load(yaml) + kind = manifest["kind"]&.downcase + name = manifest.dig("metadata", "name") + manifest_key = "#{kind}/#{name}" + + self.manifests ||= {} + self.manifests[manifest_key] = yaml save! end + + def has_manifests? + manifests.keys.any? + end end diff --git a/app/models/project.rb b/app/models/project.rb index 6cb47e2f..62e0a059 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -8,6 +8,7 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null +# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text diff --git a/app/views/clusters/edit.html.erb b/app/views/clusters/edit.html.erb index 82f37158..969e4c25 100644 --- a/app/views/clusters/edit.html.erb +++ b/app/views/clusters/edit.html.erb @@ -7,25 +7,25 @@
<%= render "edit_form", cluster: @cluster %> -
+

Credentials

-

- -
+ +

Kubeconfig is hidden for security. Click edit to view and modify.

- -
-

Build Logs

+

Build Logs

+
<%= render "log_outputs/logs", loggable: @build %>
<% if @build.deployment %> -

Release Logs

-
- <%= render "log_outputs/logs", loggable: @build.deployment %> +
+

Release Logs

+
+
+ <%= render "log_outputs/logs", loggable: @build.deployment %> +
+
+ <% end %> + <% if @build.deployment&.has_manifests? %> +
+
+

Deployment Manifests

+ + +
+
+ +
+

Click view to see the Kubernetes manifests that were deployed.

+
+ +
<% end %> <% end %> diff --git a/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb b/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb index 85d17ca3..370f4e2b 100644 --- a/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb +++ b/db/migrate/20251114025053_add_kubernetes_manifests_to_deployments.rb @@ -1,5 +1,5 @@ class AddKubernetesManifestsToDeployments < ActiveRecord::Migration[7.2] def change - add_column :deployments, :manifests, :jsonb + add_column :deployments, :manifests, :jsonb, default: {} end end diff --git a/db/schema.rb b/db/schema.rb index 7514b4d1..f93febe0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_11_10_152921) do +ActiveRecord::Schema[7.2].define(version: 2025_11_14_025053) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -171,6 +171,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_10_152921) do t.integer "status", default: 0, null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.jsonb "manifests", default: {} t.index ["build_id"], name: "index_deployments_on_build_id", unique: true end @@ -425,6 +426,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_10_152921) do t.text "postdestroy_command" t.bigint "project_fork_cluster_id" t.integer "project_fork_status", default: 0 + t.string "docker_command" t.index ["cluster_id"], name: "index_projects_on_cluster_id" end @@ -445,19 +447,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_11_10_152921) do t.index ["user_id"], name: "index_providers_on_user_id" end - create_table "resource_constraints", force: :cascade do |t| - t.string "constrainable_type", null: false - t.bigint "constrainable_id", null: false - t.bigint "cpu_request" - t.bigint "cpu_limit" - t.bigint "memory_request" - t.bigint "memory_limit" - t.integer "gpu_request" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index ["constrainable_type", "constrainable_id"], name: "index_resource_constraints_on_constrainable" - end - create_table "services", force: :cascade do |t| t.bigint "project_id", null: false t.integer "service_type", null: false diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 75d564e9..f1562879 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -3,6 +3,7 @@ # Table name: deployments # # id :bigint not null, primary key +# manifests :jsonb # status :integer default("in_progress"), not null # created_at :datetime not null # updated_at :datetime not null diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index df1091ad..6d530c85 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -8,6 +8,7 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null +# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb new file mode 100644 index 00000000..8504dff3 --- /dev/null +++ b/spec/models/deployment_spec.rb @@ -0,0 +1,105 @@ +# == Schema Information +# +# Table name: deployments +# +# id :bigint not null, primary key +# manifests :jsonb +# status :integer default("in_progress"), not null +# created_at :datetime not null +# updated_at :datetime not null +# build_id :bigint not null +# +# Indexes +# +# index_deployments_on_build_id (build_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (build_id => builds.id) +# +require 'rails_helper' + +RSpec.describe Deployment, type: :model do + let(:build) { create(:build) } + let(:deployment) { create(:deployment, build: build) } + + describe '#add_manifest' do + let(:deployment_yaml) do + <<~YAML + apiVersion: apps/v1 + kind: Deployment + metadata: + name: test-app + spec: + replicas: 1 + YAML + end + + let(:service_yaml) do + <<~YAML + apiVersion: v1 + kind: Service + metadata: + name: test-service + spec: + ports: + - port: 80 + YAML + end + + let(:configmap_yaml) do + <<~YAML + apiVersion: v1 + kind: ConfigMap + metadata: + name: test-config + data: + key: value + YAML + end + + it 'stores manifest with key format kind/name' do + expect(deployment.has_manifests?).to be false + + deployment.add_manifest(deployment_yaml) + + expect(deployment.manifests['deployment/test-app']).to eq(deployment_yaml) + expect(deployment.has_manifests?).to be true + end + + it 'stores multiple manifests with different keys' do + deployment.add_manifest(deployment_yaml) + deployment.add_manifest(service_yaml) + deployment.add_manifest(configmap_yaml) + + expect(deployment.manifests['deployment/test-app']).to eq(deployment_yaml) + expect(deployment.manifests['service/test-service']).to eq(service_yaml) + expect(deployment.manifests['configmap/test-config']).to eq(configmap_yaml) + end + + it 'overwrites manifest if same kind/name is added again' do + deployment.add_manifest(deployment_yaml) + + updated_yaml = <<~YAML + apiVersion: apps/v1 + kind: Deployment + metadata: + name: test-app + spec: + replicas: 3 + YAML + + deployment.add_manifest(updated_yaml) + + expect(deployment.manifests['deployment/test-app']).to eq(updated_yaml) + expect(deployment.manifests.keys.length).to eq(1) + end + + it 'persists the changes to the database' do + deployment.add_manifest(deployment_yaml) + + deployment.reload + expect(deployment.manifests['deployment/test-app']).to eq(deployment_yaml) + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5e7b325b..65932eb0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8,6 +8,7 @@ # canine_config :jsonb # container_registry_url :string # docker_build_context_directory :string default("."), not null +# docker_command :string # dockerfile_path :string default("./Dockerfile"), not null # name :string not null # postdeploy_command :text