mirror of
https://github.com/czhu12/canine.git
synced 2025-12-21 10:49:49 -06:00
added specs for project forks and yaml templating
This commit is contained in:
@@ -1,44 +1,10 @@
|
|||||||
class ProjectForks::Create
|
class ProjectForks::Create
|
||||||
extend LightService::Action
|
extend LightService::Organizer
|
||||||
|
|
||||||
expects :parent_project, :pull_request
|
def self.call(parent_project:, pull_request:)
|
||||||
promises :project_fork
|
with(parent_project:, pull_request:).reduce(
|
||||||
|
ProjectForks::ForkProject,
|
||||||
executed do |context|
|
ProjectForks::InitializeFromCanineConfig
|
||||||
parent_project = context.parent_project
|
)
|
||||||
pull_request = context.pull_request
|
|
||||||
child_project = parent_project.dup
|
|
||||||
child_project.branch = pull_request.branch
|
|
||||||
child_project.name = "#{parent_project.name}-#{pull_request.number}"
|
|
||||||
child_project.cluster_id = parent_project.project_fork_cluster_id
|
|
||||||
# Duplicate the project_credential_provider
|
|
||||||
child_project_credential_provider = parent_project.project_credential_provider.dup
|
|
||||||
child_project_credential_provider.project = child_project
|
|
||||||
|
|
||||||
# Duplicate the services
|
|
||||||
new_services = parent_project.services.map do |service|
|
|
||||||
new_service = service.dup
|
|
||||||
new_service.allow_public_networking = false
|
|
||||||
new_service.project = child_project
|
|
||||||
new_service
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
context.project_fork = ProjectFork.new(
|
|
||||||
child_project:,
|
|
||||||
parent_project:,
|
|
||||||
external_id: pull_request.id,
|
|
||||||
number: pull_request.number,
|
|
||||||
title: pull_request.title,
|
|
||||||
url: pull_request.url,
|
|
||||||
user: pull_request.user,
|
|
||||||
)
|
|
||||||
child_project.save!
|
|
||||||
child_project_credential_provider.save!
|
|
||||||
new_services.each(&:save!)
|
|
||||||
context.project_fork.save!
|
|
||||||
end
|
|
||||||
rescue StandardError => e
|
|
||||||
context.fail_and_return!("Failed to create project fork: #{e.message}")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
46
app/actions/project_forks/fork_project.rb
Normal file
46
app/actions/project_forks/fork_project.rb
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
class ProjectForks::ForkProject
|
||||||
|
extend LightService::Action
|
||||||
|
|
||||||
|
expects :parent_project, :pull_request
|
||||||
|
promises :project_fork
|
||||||
|
|
||||||
|
executed do |context|
|
||||||
|
parent_project = context.parent_project
|
||||||
|
pull_request = context.pull_request
|
||||||
|
child_project = parent_project.dup
|
||||||
|
child_project.branch = pull_request.branch
|
||||||
|
child_project.name = "#{parent_project.name}-#{pull_request.number}"
|
||||||
|
child_project.cluster_id = parent_project.project_fork_cluster_id
|
||||||
|
# Duplicate the project_credential_provider
|
||||||
|
child_project_credential_provider = parent_project.project_credential_provider.dup
|
||||||
|
child_project_credential_provider.project = child_project
|
||||||
|
|
||||||
|
# Duplicate the services
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
context.project_fork = ProjectFork.new(
|
||||||
|
child_project:,
|
||||||
|
parent_project:,
|
||||||
|
external_id: pull_request.id,
|
||||||
|
number: pull_request.number,
|
||||||
|
title: pull_request.title,
|
||||||
|
url: pull_request.url,
|
||||||
|
user: pull_request.user,
|
||||||
|
)
|
||||||
|
child_project.save!
|
||||||
|
child_project_credential_provider.save!
|
||||||
|
context.project_fork.save!
|
||||||
|
|
||||||
|
# Fetch and store canine config if it exists
|
||||||
|
client = Git::Client.from_project(child_project)
|
||||||
|
file = client.get_file('.canine.yml', pull_request.branch)
|
||||||
|
|
||||||
|
if file.present?
|
||||||
|
# Parse and store the config
|
||||||
|
canine_config = CanineConfig::Definition.parse(file.content, parent_project, pull_request)
|
||||||
|
context.project_fork.update!(canine_config: canine_config.to_hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
context.fail_and_return!("Failed to create project fork: #{e.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,12 +1,26 @@
|
|||||||
class ProjectForks::CreateFromCanineConfig
|
class ProjectForks::InitializeFromCanineConfig
|
||||||
extend LightService::Action
|
extend LightService::Action
|
||||||
expects :canine_config, :project
|
expects :project_fork
|
||||||
|
|
||||||
executed do |context|
|
executed do |context|
|
||||||
context.canine_config.services.each do |service_config|
|
# Skip this action if no canine_config is stored
|
||||||
|
next if context.project_fork.canine_config.blank?
|
||||||
|
|
||||||
|
config_data = context.project_fork.canine_config
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
service.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
context.canine_config.environment_variables.each do |environment_variable|
|
# Create environment variables from the stored config
|
||||||
|
config_data['environment_variables']&.each do |env_var|
|
||||||
|
context.project_fork.child_project.environment_variables.create!(
|
||||||
|
name: env_var['name'],
|
||||||
|
value: env_var['value']
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Table name: project_forks
|
# Table name: project_forks
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
|
# canine_config :jsonb
|
||||||
# clean_up_command :text
|
# clean_up_command :text
|
||||||
# number :string not null
|
# number :string not null
|
||||||
# title :string not null
|
# title :string not null
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
class CanineConfig::Definition
|
class CanineConfig::Definition
|
||||||
attr_reader :definition
|
attr_reader :definition
|
||||||
|
|
||||||
def initialize(yaml_path, base_project, pull_request)
|
def self.parse(yaml_content, base_project, pull_request)
|
||||||
context = {
|
context = build_context(base_project, pull_request)
|
||||||
|
|
||||||
|
parsed_content = if yaml_content.include?('<%')
|
||||||
|
erb = ERB.new(yaml_content)
|
||||||
|
context_binding = binding
|
||||||
|
context.each do |key, value|
|
||||||
|
context_binding.local_variable_set(key, value)
|
||||||
|
end
|
||||||
|
erb.result(context_binding)
|
||||||
|
else
|
||||||
|
yaml_content
|
||||||
|
end
|
||||||
|
|
||||||
|
new(YAML.safe_load(parsed_content))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.build_context(base_project, pull_request)
|
||||||
|
{
|
||||||
"cluster_id": base_project.project_fork_cluster_id,
|
"cluster_id": base_project.project_fork_cluster_id,
|
||||||
"cluster_name": base_project.project_fork_cluster.name,
|
"cluster_name": base_project.project_fork_cluster.name,
|
||||||
"project_name": "#{base_project.name}-#{pull_request.number}",
|
"project_name": "#{base_project.name}-#{pull_request.number}",
|
||||||
@@ -11,19 +28,10 @@ class CanineConfig::Definition
|
|||||||
"branch_name": pull_request.branch,
|
"branch_name": pull_request.branch,
|
||||||
"username": pull_request.user
|
"username": pull_request.user
|
||||||
}
|
}
|
||||||
|
end
|
||||||
|
|
||||||
content = if yaml_path.to_s.end_with?('.erb')
|
def initialize(definition)
|
||||||
erb = ERB.new(File.read(yaml_path))
|
@definition = definition
|
||||||
context_binding = binding
|
|
||||||
context.each do |key, value|
|
|
||||||
context_binding.local_variable_set(key, value)
|
|
||||||
end
|
|
||||||
erb.result(context_binding)
|
|
||||||
else
|
|
||||||
File.read(yaml_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
@definition = YAML.load(content)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def services
|
def services
|
||||||
@@ -38,4 +46,8 @@ class CanineConfig::Definition
|
|||||||
EnvironmentVariable.new(name: env['name'], value: env['value'])
|
EnvironmentVariable.new(name: env['name'], value: env['value'])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_hash
|
||||||
|
@definition
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
1
app/services/git/common/file.rb
Normal file
1
app/services/git/common/file.rb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
class Git::Common::File < Struct.new(:path, :content, :branch); end
|
||||||
@@ -81,6 +81,15 @@ class Git::Github::Client < Git::Client
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_file(file_path, branch)
|
||||||
|
contents = client.contents(repository_url, path: file_path, ref: branch)
|
||||||
|
return nil if contents.nil?
|
||||||
|
|
||||||
|
Git::Common::File.new(file_path, Base64.decode64(contents.content), branch)
|
||||||
|
rescue Octokit::NotFound
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def webhook_secret
|
def webhook_secret
|
||||||
|
|||||||
@@ -109,4 +109,12 @@ class Git::Gitlab::Client < Git::Client
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_file(file_path, branch)
|
||||||
|
response = HTTParty.get(
|
||||||
|
"#{GITLAB_API_BASE}/projects/#{encoded_url}/repository/files/#{URI.encode_www_form_component(file_path)}/raw?ref=#{branch}",
|
||||||
|
headers: { "Authorization" => "Bearer #{access_token}" }
|
||||||
|
)
|
||||||
|
response.success? ? response.body : nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class CreateProjectForks < ActiveRecord::Migration[7.2]
|
|||||||
t.string :title, null: false
|
t.string :title, null: false
|
||||||
t.string :url, null: false
|
t.string :url, null: false
|
||||||
t.string :user, null: false
|
t.string :user, null: false
|
||||||
|
t.jsonb :canine_config, default: {}
|
||||||
|
|
||||||
t.text :clean_up_command
|
t.text :clean_up_command
|
||||||
|
|
||||||
|
|||||||
1
db/schema.rb
generated
1
db/schema.rb
generated
@@ -239,6 +239,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_06_29_164951) do
|
|||||||
t.string "title", null: false
|
t.string "title", null: false
|
||||||
t.string "url", null: false
|
t.string "url", null: false
|
||||||
t.string "user", null: false
|
t.string "user", null: false
|
||||||
|
t.jsonb "canine_config", default: {}
|
||||||
t.text "clean_up_command"
|
t.text "clean_up_command"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
|||||||
@@ -1,23 +1,56 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ProjectForks::Create do
|
RSpec.describe ProjectForks::Create do
|
||||||
class MockPr < Struct.new(:id, :title, :number, :branch); end
|
let(:account) { create(:account) }
|
||||||
|
let(:fork_cluster) { create(:cluster, account:) }
|
||||||
let(:base_project) { create(:project) }
|
let(:parent_project) { create(:project, project_fork_cluster_id: fork_cluster.id, account:) }
|
||||||
let!(:service_1) { create(:service, project: base_project) }
|
let!(:service_1) { create(:service, project: parent_project) }
|
||||||
let!(:service_2) { create(:service, project: base_project) }
|
let!(:service_2) { create(:service, project: parent_project) }
|
||||||
let(:pull_request) { MockPr.new(id: "1", title: "Fake title", number: "7301", branch: "feature/test") }
|
let(:pull_request) do
|
||||||
|
Git::Common::PullRequest.new(
|
||||||
it 'can create a project fork from a base project' do
|
id: "1",
|
||||||
result = described_class.execute(base_project:, pull_request:)
|
title: "Fake title",
|
||||||
expect(result).to be_success
|
number: "7301",
|
||||||
base_project.reload
|
branch: "feature/test",
|
||||||
expect(base_project.forks.count).to eq(1)
|
user: "testuser",
|
||||||
|
url: "https://github.com/test/repo/pull/7301",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
let(:git_client) { instance_double(Git::Github::Client) }
|
||||||
|
|
||||||
it 'clones over services, project credential providers' do
|
context 'with a simple yaml file' do
|
||||||
result = described_class.execute(base_project:, pull_request:)
|
before do
|
||||||
result.
|
allow(Git::Client).to receive(:from_project).and_return(git_client)
|
||||||
expect(base_project.forks.first.new_project.services.count).to eq(2)
|
allow(git_client).to receive(:get_file).with('.canine.yml', 'feature/test').and_return(
|
||||||
|
Git::Common::File.new(
|
||||||
|
'.canine.yml',
|
||||||
|
File.read(Rails.root.join('spec', 'resources', 'canine_config', 'example_1.yaml')),
|
||||||
|
'feature/test'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can create a project fork from a base project' do
|
||||||
|
result = described_class.call(parent_project:, pull_request:)
|
||||||
|
expect(result).to be_success
|
||||||
|
parent_project.reload
|
||||||
|
expect(parent_project.forks.count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
context 'with a yaml file with erb' do
|
||||||
|
before do
|
||||||
|
allow(Git::Client).to receive(:from_project).and_return(git_client)
|
||||||
|
allow(git_client).to receive(:get_file).with('.canine.yml', 'feature/test').and_return(
|
||||||
|
Git::Common::File.new(
|
||||||
|
'.canine.yml',
|
||||||
|
File.read(Rails.root.join('spec', 'resources', 'canine_config', 'example_2.yaml.erb')),
|
||||||
|
'feature/test'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can create a project fork from a base project' do
|
||||||
|
expect { described_class.call(parent_project:, pull_request:) }.to change { parent_project.forks.count }.by(1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
122
spec/actions/project_forks/fork_project_spec.rb
Normal file
122
spec/actions/project_forks/fork_project_spec.rb
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ProjectForks::ForkProject do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:cluster) { create(:cluster, account:) }
|
||||||
|
let(:parent_project) { create(:project, account:, cluster:, project_fork_cluster_id: cluster.id) }
|
||||||
|
let(:provider) { create(:provider, :github, user: account.owner) }
|
||||||
|
let!(:project_credential_provider) { create(:project_credential_provider, project: parent_project, provider:) }
|
||||||
|
let(:pull_request) do
|
||||||
|
Git::Common::PullRequest.new(
|
||||||
|
id: "123",
|
||||||
|
title: "Test PR",
|
||||||
|
number: "42",
|
||||||
|
branch: "feature/test",
|
||||||
|
user: "testuser",
|
||||||
|
url: "https://github.com/test/repo/pull/42"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
let(:git_client) { instance_double(Git::Github::Client) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Mock Git client for all tests to avoid authentication issues
|
||||||
|
allow(Git::Client).to receive(:from_project).and_return(git_client)
|
||||||
|
allow(git_client).to receive(:get_file).with('.canine.yml', 'feature/test').and_return(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
context 'when successful' do
|
||||||
|
let(:result) { described_class.execute(parent_project:, pull_request:) }
|
||||||
|
|
||||||
|
it 'creates a project fork' do
|
||||||
|
expect { result }.to change { ProjectFork.count }.by(1)
|
||||||
|
expect(result).to be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a child project with correct attributes' do
|
||||||
|
result
|
||||||
|
project_fork = result.project_fork
|
||||||
|
child_project = project_fork.child_project
|
||||||
|
|
||||||
|
expect(child_project.name).to eq("#{parent_project.name}-42")
|
||||||
|
expect(child_project.branch).to eq("feature/test")
|
||||||
|
expect(child_project.cluster_id).to eq(parent_project.project_fork_cluster_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'duplicates the project credential provider' do
|
||||||
|
expect { result }.to change { ProjectCredentialProvider.count }.by(1)
|
||||||
|
|
||||||
|
child_project = result.project_fork.child_project
|
||||||
|
expect(child_project.project_credential_provider).to be_present
|
||||||
|
expect(child_project.project_credential_provider.provider).to eq(provider)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets project fork attributes correctly' do
|
||||||
|
result
|
||||||
|
project_fork = result.project_fork
|
||||||
|
|
||||||
|
expect(project_fork.parent_project).to eq(parent_project)
|
||||||
|
expect(project_fork.external_id).to eq("123")
|
||||||
|
expect(project_fork.number).to eq("42")
|
||||||
|
expect(project_fork.title).to eq("Test PR")
|
||||||
|
expect(project_fork.url).to eq("https://github.com/test/repo/pull/42")
|
||||||
|
expect(project_fork.user).to eq("testuser")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes project_fork in the context' do
|
||||||
|
expect(result.project_fork).to be_a(ProjectFork)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with canine config file' do
|
||||||
|
let(:canine_config_content) do
|
||||||
|
Git::Common::File.new(
|
||||||
|
'.canine.yml',
|
||||||
|
File.read(Rails.root.join('spec', 'resources', 'canine_config', 'example_1.yaml')),
|
||||||
|
'feature/test'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(git_client).to receive(:get_file).with('.canine.yml', 'feature/test').and_return(canine_config_content)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches and stores the canine config' do
|
||||||
|
result = described_class.execute(parent_project:, pull_request:)
|
||||||
|
|
||||||
|
expect(result).to be_success
|
||||||
|
project_fork = result.project_fork
|
||||||
|
|
||||||
|
expect(project_fork.canine_config).to be_present
|
||||||
|
expect(project_fork.canine_config['services']).to be_an(Array)
|
||||||
|
expect(project_fork.canine_config['services'].first['name']).to eq('web')
|
||||||
|
expect(project_fork.canine_config['environment_variables']).to be_an(Array)
|
||||||
|
expect(project_fork.canine_config['environment_variables'].first['name']).to eq('DATABASE_URL')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without canine config file' do
|
||||||
|
# Git client is already mocked in the parent before block
|
||||||
|
|
||||||
|
it 'still creates the project fork successfully' do
|
||||||
|
result = described_class.execute(parent_project:, pull_request:)
|
||||||
|
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(result.project_fork.canine_config).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when transaction fails' do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(ProjectFork).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns failure context' do
|
||||||
|
result = described_class.execute(parent_project:, pull_request:)
|
||||||
|
|
||||||
|
expect(result).to be_failure
|
||||||
|
expect(result.message).to include("Failed to create project fork")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
107
spec/actions/project_forks/initialize_from_canine_config_spec.rb
Normal file
107
spec/actions/project_forks/initialize_from_canine_config_spec.rb
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ProjectForks::InitializeFromCanineConfig do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:cluster) { create(:cluster, account:) }
|
||||||
|
let(:parent_project) { create(:project, account:, cluster:) }
|
||||||
|
let(:child_project) { create(:project, account:, cluster:) }
|
||||||
|
let(:project_fork) { create(:project_fork, parent_project:, child_project:) }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
context 'when project fork has canine config' do
|
||||||
|
let(:canine_config) do
|
||||||
|
{
|
||||||
|
'services' => [
|
||||||
|
{
|
||||||
|
'name' => 'web',
|
||||||
|
'container_port' => 6379,
|
||||||
|
'service_type' => 'web_service'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'worker',
|
||||||
|
'container_port' => 5432,
|
||||||
|
'service_type' => 'background_service'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'environment_variables' => [
|
||||||
|
{
|
||||||
|
'name' => 'DATABASE_URL',
|
||||||
|
'value' => 'postgres://localhost/test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'REDIS_URL',
|
||||||
|
'value' => 'redis://localhost:6379'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
project_fork.update!(canine_config:)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates services from the config' do
|
||||||
|
expect {
|
||||||
|
described_class.execute(project_fork:)
|
||||||
|
}.to change { child_project.services.count }.by(2)
|
||||||
|
|
||||||
|
services = child_project.services.order(:name)
|
||||||
|
|
||||||
|
expect(services[0].name).to eq('web')
|
||||||
|
expect(services[0].container_port).to eq(6379)
|
||||||
|
expect(services[0].service_type).to eq('web_service')
|
||||||
|
|
||||||
|
expect(services[1].name).to eq('worker')
|
||||||
|
expect(services[1].container_port).to eq(5432)
|
||||||
|
expect(services[1].service_type).to eq('background_service')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates environment variables from the config' do
|
||||||
|
expect {
|
||||||
|
described_class.execute(project_fork:)
|
||||||
|
}.to change { child_project.environment_variables.count }.by(2)
|
||||||
|
|
||||||
|
env_vars = child_project.environment_variables.order(:name)
|
||||||
|
|
||||||
|
expect(env_vars[0].name).to eq('DATABASE_URL')
|
||||||
|
expect(env_vars[0].value).to eq('postgres://localhost/test')
|
||||||
|
|
||||||
|
expect(env_vars[1].name).to eq('REDIS_URL')
|
||||||
|
expect(env_vars[1].value).to eq('redis://localhost:6379')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns success context' do
|
||||||
|
result = described_class.execute(project_fork:)
|
||||||
|
expect(result).to be_success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project fork has empty canine config' do
|
||||||
|
before do
|
||||||
|
project_fork.update!(canine_config: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips and returns success' do
|
||||||
|
result = described_class.execute(project_fork:)
|
||||||
|
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(child_project.services.count).to eq(0)
|
||||||
|
expect(child_project.environment_variables.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project fork has no canine config' do
|
||||||
|
before do
|
||||||
|
project_fork.update!(canine_config: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips and returns success' do
|
||||||
|
result = described_class.execute(project_fork:)
|
||||||
|
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(child_project.services.count).to eq(0)
|
||||||
|
expect(child_project.environment_variables.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
# Table name: project_forks
|
# Table name: project_forks
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
|
# canine_config :jsonb
|
||||||
# clean_up_command :text
|
# clean_up_command :text
|
||||||
# number :string not null
|
# number :string not null
|
||||||
# title :string not null
|
# title :string not null
|
||||||
|
|||||||
@@ -113,21 +113,21 @@ RSpec.describe Project, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe 'forks' do
|
describe 'forks' do
|
||||||
let(:base_project) { create(:project) }
|
let(:parent_project) { create(:project) }
|
||||||
let!(:project_fork) { create(:project_fork, base_project:, new_project: project) }
|
let!(:project_fork) { create(:project_fork, parent_project:, child_project: project) }
|
||||||
|
|
||||||
it 'can determine if a project can fork' do
|
it 'can determine if a project can fork' do
|
||||||
expect(base_project.can_fork?).to be_falsey
|
expect(parent_project.can_fork?).to be_falsey
|
||||||
expect(project.can_fork?).to be_falsey
|
expect(project.can_fork?).to be_falsey
|
||||||
|
|
||||||
base_project.project_fork_status = :manually_create
|
parent_project.project_fork_status = :manually_create
|
||||||
base_project.save!
|
parent_project.save!
|
||||||
expect(base_project.can_fork?).to be_truthy
|
expect(parent_project.can_fork?).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can tell if a project is a preview project' do
|
it 'can tell if a project is a preview project' do
|
||||||
expect(base_project.preview?).to be_falsey
|
expect(parent_project.forked?).to be_falsey
|
||||||
expect(project.preview?).to be_truthy
|
expect(project.forked?).to be_truthy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
7
spec/resources/canine_config/example_1.yaml
Normal file
7
spec/resources/canine_config/example_1.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
- name: web
|
||||||
|
container_port: 6379
|
||||||
|
service_type: web_service
|
||||||
|
environment_variables:
|
||||||
|
- name: DATABASE_URL
|
||||||
|
value: postgres://localhost/test
|
||||||
11
spec/resources/canine_config/example_2.yaml.erb
Normal file
11
spec/resources/canine_config/example_2.yaml.erb
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
- name: "<%= project_name %>"
|
||||||
|
container_port: 3000
|
||||||
|
service_type: "web_service"
|
||||||
|
environment_variables:
|
||||||
|
- name: "DATABASE_URL"
|
||||||
|
value: "redis://redis.<%= cluster_name %>.svc.local/<%= number %>"
|
||||||
|
- name: "BRANCH"
|
||||||
|
value: "<%= branch_name %>"
|
||||||
|
- name: "USER"
|
||||||
|
value: "<%= username %>"
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe CanineConfig::Definition do
|
RSpec.describe CanineConfig::Definition do
|
||||||
let(:yaml_path) { Rails.root.join('resources', 'canine_config', 'example_1.yaml.erb') }
|
|
||||||
let(:account) { create(:account) }
|
let(:account) { create(:account) }
|
||||||
let(:cluster) { create(:cluster, account:) }
|
let(:cluster) { create(:cluster, account:, name: 'test-cluster') }
|
||||||
let(:base_project) { create(:project, project_fork_cluster_id: cluster.id, account:) }
|
let(:base_project) { create(:project, project_fork_cluster_id: cluster.id, account:, name: 'test-project') }
|
||||||
let(:pull_request) do
|
let(:pull_request) do
|
||||||
Git::Common::PullRequest.new(
|
Git::Common::PullRequest.new(
|
||||||
number: 42,
|
number: 42,
|
||||||
@@ -14,25 +13,220 @@ RSpec.describe CanineConfig::Definition do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
subject(:definition) { described_class.new(yaml_path, base_project, pull_request) }
|
describe '.parse' do
|
||||||
|
context 'with plain YAML content' do
|
||||||
|
let(:yaml_content) do
|
||||||
|
<<~YAML
|
||||||
|
services:
|
||||||
|
- name: "web"
|
||||||
|
container_port: 3000
|
||||||
|
service_type: "web_service"
|
||||||
|
environment_variables:
|
||||||
|
- name: "API_KEY"
|
||||||
|
value: "test-key"
|
||||||
|
YAML
|
||||||
|
end
|
||||||
|
|
||||||
describe '#environment_variables' do
|
it 'parses YAML content and returns a Definition instance' do
|
||||||
it 'returns environment variables from the definition' do
|
definition = described_class.parse(yaml_content, base_project, pull_request)
|
||||||
env_vars = definition.environment_variables
|
|
||||||
|
|
||||||
expect(env_vars.count).to eq(1)
|
expect(definition).to be_a(CanineConfig::Definition)
|
||||||
expect(env_vars.first.name).to eq('DATABASE_URL')
|
expect(definition.to_hash).to include(
|
||||||
expect(env_vars.first.value).to eq('redis://redis.cluster.svc.local/42')
|
'services' => [
|
||||||
|
{
|
||||||
|
'name' => 'web',
|
||||||
|
'container_port' => 3000,
|
||||||
|
'service_type' => 'web_service'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'environment_variables' => [
|
||||||
|
{
|
||||||
|
'name' => 'API_KEY',
|
||||||
|
'value' => 'test-key'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with ERB template content' do
|
||||||
|
let(:yaml_content) do
|
||||||
|
<<~YAML
|
||||||
|
services:
|
||||||
|
- name: "<%= project_name %>"
|
||||||
|
container_port: 3000
|
||||||
|
service_type: "web_service"
|
||||||
|
environment_variables:
|
||||||
|
- name: "DATABASE_URL"
|
||||||
|
value: "redis://redis.<%= cluster_name %>.svc.local/<%= number %>"
|
||||||
|
- name: "BRANCH"
|
||||||
|
value: "<%= branch_name %>"
|
||||||
|
- name: "USER"
|
||||||
|
value: "<%= username %>"
|
||||||
|
YAML
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'interpolates ERB variables with context values' do
|
||||||
|
definition = described_class.parse(yaml_content, base_project, pull_request)
|
||||||
|
|
||||||
|
expect(definition.to_hash).to include(
|
||||||
|
'services' => [
|
||||||
|
{
|
||||||
|
'name' => 'test-project-42',
|
||||||
|
'container_port' => 3000,
|
||||||
|
'service_type' => 'web_service'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'environment_variables' => [
|
||||||
|
{
|
||||||
|
'name' => 'DATABASE_URL',
|
||||||
|
'value' => 'redis://redis.test-cluster.svc.local/42'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'BRANCH',
|
||||||
|
'value' => 'feature/test'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'USER',
|
||||||
|
'value' => 'testuser'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid YAML' do
|
||||||
|
let(:yaml_content) { "invalid: yaml: content:" }
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
expect {
|
||||||
|
described_class.parse(yaml_content, base_project, pull_request)
|
||||||
|
}.to raise_error(Psych::SyntaxError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.build_context' do
|
||||||
|
it 'returns a hash with all required context variables' do
|
||||||
|
context = described_class.build_context(base_project, pull_request)
|
||||||
|
|
||||||
|
expect(context).to eq({
|
||||||
|
cluster_id: cluster.id,
|
||||||
|
cluster_name: 'test-cluster',
|
||||||
|
project_name: 'test-project-42',
|
||||||
|
number: 42,
|
||||||
|
title: 'Test PR',
|
||||||
|
branch_name: 'feature/test',
|
||||||
|
username: 'testuser'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#initialize' do
|
||||||
|
let(:definition_hash) do
|
||||||
|
{
|
||||||
|
'services' => [
|
||||||
|
{ 'name' => 'web', 'container_port' => 3000 }
|
||||||
|
],
|
||||||
|
'environment_variables' => [
|
||||||
|
{ 'name' => 'API_KEY', 'value' => 'secret' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'stores the definition hash' do
|
||||||
|
definition = described_class.new(definition_hash)
|
||||||
|
expect(definition.definition).to eq(definition_hash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#services' do
|
describe '#services' do
|
||||||
it 'returns services from the definition' do
|
let(:definition_hash) do
|
||||||
|
{
|
||||||
|
'services' => [
|
||||||
|
{
|
||||||
|
'name' => 'web',
|
||||||
|
'container_port' => 3000,
|
||||||
|
'service_type' => 'web_service',
|
||||||
|
'extra_field' => 'should_be_filtered'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name' => 'worker',
|
||||||
|
'container_port' => 4000,
|
||||||
|
'service_type' => 'background_service'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:definition) { described_class.new(definition_hash) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Service).to receive(:permitted_params).and_return(
|
||||||
|
{ 'name' => 'web', 'container_port' => 3000, 'service_type' => 'web_service' },
|
||||||
|
{ 'name' => 'worker', 'container_port' => 4000, 'service_type' => 'background_service' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array of Service objects' do
|
||||||
services = definition.services
|
services = definition.services
|
||||||
|
|
||||||
expect(services.count).to eq(1)
|
expect(services).to be_an(Array)
|
||||||
expect(services.first.name).to eq('service_1')
|
expect(services.length).to eq(2)
|
||||||
expect(services.first.container_port).to eq(8080)
|
expect(services).to all(be_a(Service))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'filters service parameters through Service.permitted_params' do
|
||||||
|
expect(Service).to receive(:permitted_params).exactly(2).times
|
||||||
|
definition.services
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#environment_variables' do
|
||||||
|
let(:definition_hash) do
|
||||||
|
{
|
||||||
|
'environment_variables' => [
|
||||||
|
{ 'name' => 'DATABASE_URL', 'value' => 'postgres://localhost/test' },
|
||||||
|
{ 'name' => 'REDIS_URL', 'value' => 'redis://localhost:6379' },
|
||||||
|
{ 'name' => 'SECRET_KEY', 'value' => 'abc123' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:definition) { described_class.new(definition_hash) }
|
||||||
|
|
||||||
|
it 'returns an array of EnvironmentVariable objects' do
|
||||||
|
env_vars = definition.environment_variables
|
||||||
|
|
||||||
|
expect(env_vars).to be_an(Array)
|
||||||
|
expect(env_vars.length).to eq(3)
|
||||||
|
expect(env_vars).to all(be_a(EnvironmentVariable))
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates EnvironmentVariable objects with correct attributes' do
|
||||||
|
env_vars = definition.environment_variables
|
||||||
|
|
||||||
|
expect(env_vars[0].name).to eq('DATABASE_URL')
|
||||||
|
expect(env_vars[0].value).to eq('postgres://localhost/test')
|
||||||
|
|
||||||
|
expect(env_vars[1].name).to eq('REDIS_URL')
|
||||||
|
expect(env_vars[1].value).to eq('redis://localhost:6379')
|
||||||
|
|
||||||
|
expect(env_vars[2].name).to eq('SECRET_KEY')
|
||||||
|
expect(env_vars[2].value).to eq('abc123')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#to_hash' do
|
||||||
|
let(:definition_hash) do
|
||||||
|
{
|
||||||
|
'services' => [ { 'name' => 'web' } ],
|
||||||
|
'environment_variables' => [ { 'name' => 'KEY', 'value' => 'value' } ],
|
||||||
|
'extra_field' => 'extra_value'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:definition) { described_class.new(definition_hash) }
|
||||||
|
|
||||||
|
it 'returns the original definition hash' do
|
||||||
|
expect(definition.to_hash).to eq(definition_hash)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user