mirror of
https://github.com/czhu12/canine.git
synced 2026-01-06 11:40:44 -06:00
Merge branch 'main' of github.com:czhu12/canine
This commit is contained in:
@@ -7,9 +7,6 @@
|
||||
- [ ] allow public network access flag is not currently doing anything
|
||||
|
||||
* I want a way to “stop” the processes, can maybe do this with a replicas=0 setting
|
||||
* All the times need to show relative times, not absolute. It’s too hard to understand absolute times.
|
||||
* Whenever something is pushed, and deployed, we need to kill all one off containers because they are no longer running the correct source code
|
||||
* “Pending” should have some kind of active spinner animation just for the feels
|
||||
* Rebulid metrics tabs so it works for both clusters & pods
|
||||
https://overcast.blog/zero-downtime-deployments-with-kubernetes-a-full-guide-71019397b924?gi=95ab85c45634
|
||||
* Team mates features
|
||||
|
||||
@@ -13,13 +13,12 @@ class Clusters::InstallAcmeIssuer
|
||||
cluster.info("Acme issuer is already installed")
|
||||
rescue Cli::CommandFailedError => e
|
||||
cluster.info("Acme issuer not detected, installing...")
|
||||
ingress_yaml = K8::Shared::AcmeIssuer.new(cluster.user.email).to_yaml
|
||||
ingress_yaml = K8::Shared::AcmeIssuer.new(cluster.account.owner.email).to_yaml
|
||||
kubectl.apply_yaml(ingress_yaml)
|
||||
cluster.info("Acme issuer installed")
|
||||
end
|
||||
rescue StandardError => e
|
||||
cluster.failed!
|
||||
cluster.info("Acme issuer failed to install")
|
||||
context.fail!("Script failed with exit code #{exit_status.exitstatus}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ class Projects::DeployLatestCommit
|
||||
executed do |context|
|
||||
# Fetch the latest commit from the default branch
|
||||
project = context.project
|
||||
client = Octokit::Client.new(access_token: project.user.github_access_token)
|
||||
client = Octokit::Client.new(access_token: project.account.github_access_token)
|
||||
commit = client.commits(project.repository_url).first
|
||||
|
||||
build = Build.create!(
|
||||
|
||||
@@ -6,7 +6,7 @@ class Projects::RegisterGithubWebhook
|
||||
promises :project
|
||||
|
||||
executed do |context|
|
||||
client = Octokit::Client.new(access_token: context.project.user.github_access_token)
|
||||
client = Octokit::Client.new(access_token: context.project.account.github_access_token)
|
||||
client.create_hook(
|
||||
context.project.repository_url,
|
||||
"web",
|
||||
|
||||
@@ -5,7 +5,7 @@ class Projects::ValidateGithubRepository
|
||||
promises :project
|
||||
|
||||
executed do |context|
|
||||
client = Octokit::Client.new(access_token: context.project.user.github_access_token)
|
||||
client = Octokit::Client.new(access_token: context.project.account.github_access_token)
|
||||
unless client.repository?(context.project.repository_url)
|
||||
context.project.errors.add(:repository_url, 'does not exist')
|
||||
context.fail_and_return!('Repository does not exist')
|
||||
|
||||
7
app/controllers/accounts_controller.rb
Normal file
7
app/controllers/accounts_controller.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class AccountsController < ApplicationController
|
||||
def switch
|
||||
@account = current_user.accounts.find(params[:id])
|
||||
session[:account_id] = @account.id
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,7 @@ class AddOnsController < ApplicationController
|
||||
|
||||
# GET /add_ons
|
||||
def index
|
||||
@pagy, @add_ons = pagy(AddOn.all)
|
||||
@pagy, @add_ons = pagy(current_account.add_ons)
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @add_ons
|
||||
@@ -71,7 +71,7 @@ class AddOnsController < ApplicationController
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_add_on
|
||||
@add_on = current_user.add_ons.find(params[:id])
|
||||
@add_on = current_account.add_ons.find(params[:id])
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @add_on
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include ActionView::Helpers::DateHelper
|
||||
impersonates :user
|
||||
include Pundit::Authorization
|
||||
include Pagy::Backend
|
||||
@@ -13,6 +14,20 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
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"
|
||||
else
|
||||
"Never"
|
||||
end
|
||||
end
|
||||
helper_method :time_ago
|
||||
|
||||
def configure_permitted_parameters
|
||||
devise_parameter_sanitizer.permit(:sign_up, keys: [ :name ])
|
||||
devise_parameter_sanitizer.permit(:account_update, keys: [ :name, :avatar ])
|
||||
|
||||
@@ -4,7 +4,7 @@ class ClustersController < ApplicationController
|
||||
# GET /clusters
|
||||
def index
|
||||
sortable_column = params[:sort] || "created_at"
|
||||
@pagy, @clusters = pagy(current_user.clusters.order(sortable_column => "asc"))
|
||||
@pagy, @clusters = pagy(current_account.clusters.order(sortable_column => "asc"))
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @clusters
|
||||
@@ -44,7 +44,7 @@ class ClustersController < ApplicationController
|
||||
|
||||
# POST /clusters or /clusters.json
|
||||
def create
|
||||
@cluster = current_user.clusters.new(cluster_params)
|
||||
@cluster = current_account.clusters.new(cluster_params)
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @cluster
|
||||
@@ -88,7 +88,7 @@ class ClustersController < ApplicationController
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_cluster
|
||||
@cluster = current_user.clusters.find(params[:id])
|
||||
@cluster = current_account.clusters.find(params[:id])
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @cluster
|
||||
|
||||
@@ -4,6 +4,6 @@ class Projects::BaseController < ApplicationController
|
||||
|
||||
private
|
||||
def set_project
|
||||
@project = current_user.projects.find(params[:project_id])
|
||||
@project = current_account.projects.find(params[:project_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,6 @@ class Projects::ProcessesController < Projects::BaseController
|
||||
end
|
||||
|
||||
def set_cluster
|
||||
@cluster = current_user.clusters.find(params[:cluster_id])
|
||||
@cluster = current_account.clusters.find(params[:cluster_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ class ProjectsController < ApplicationController
|
||||
# GET /projects
|
||||
def index
|
||||
sortable_column = params[:sort] || "created_at"
|
||||
@pagy, @projects = pagy(current_user.projects.order(sortable_column => "asc"))
|
||||
@pagy, @projects = pagy(current_account.projects.order(sortable_column => "asc"))
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @projects
|
||||
@@ -79,7 +79,7 @@ class ProjectsController < ApplicationController
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_project
|
||||
@project = current_user.projects.find(params[:id])
|
||||
@project = current_account.projects.find(params[:id])
|
||||
|
||||
# Uncomment to authorize with Pundit
|
||||
# authorize @project
|
||||
|
||||
@@ -35,6 +35,7 @@ module Users
|
||||
redirect_to edit_user_registration_path
|
||||
else
|
||||
sign_in_and_redirect user, event: :authentication
|
||||
session[:account_id] = user.accounts.first.id
|
||||
set_flash_message :notice, :success, kind: kind
|
||||
end
|
||||
end
|
||||
@@ -77,11 +78,19 @@ module Users
|
||||
end
|
||||
|
||||
def create_user
|
||||
User.create(
|
||||
email: auth.info.email,
|
||||
# name: auth.info.name,
|
||||
password: Devise.friendly_token[0, 20]
|
||||
)
|
||||
ActiveRecord::Base.transaction do
|
||||
user = User.create!(
|
||||
email: auth.info.email,
|
||||
# name: auth.info.name,
|
||||
password: Devise.friendly_token[0, 20]
|
||||
)
|
||||
account = Account.create!(
|
||||
owner: user,
|
||||
name: "#{auth.info.name || auth.info.email.split("@").first}'s Account"
|
||||
)
|
||||
AccountUser.create!(account: account, user: user)
|
||||
user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,6 +14,6 @@ export default class extends Controller {
|
||||
event.currentTarget.classList.add('ring', 'ring-primary')
|
||||
// Show Input
|
||||
this.element.querySelectorAll('.card-form').forEach(form => form.classList.add('hidden'))
|
||||
this.element.querySelector(`#card-${event.currentTarget.dataset.cardName}`)?.classList.remove("hidden");
|
||||
this.element.querySelectorAll(`.card-${event.currentTarget.dataset.cardName}`).forEach(form => form.classList.remove("hidden"));
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class Projects::BuildJob < ApplicationJob
|
||||
private
|
||||
|
||||
def project_git(project)
|
||||
"https://#{project.user.github_username}:#{project.user.github_access_token}@github.com/#{project.repository_url}.git"
|
||||
"https://#{project.account.github_username}:#{project.account.github_access_token}@github.com/#{project.repository_url}.git"
|
||||
end
|
||||
|
||||
def git_clone(project, build, repository_path)
|
||||
@@ -72,9 +72,9 @@ class Projects::BuildJob < ApplicationJob
|
||||
|
||||
def login_to_docker(project, build)
|
||||
docker_login_command = %w[docker login ghcr.io --username] +
|
||||
[ project.user.github_username, "--password", project.user.github_access_token ]
|
||||
[ project.account.github_username, "--password", project.account.github_access_token ]
|
||||
|
||||
build.info "Logging into ghcr.io as #{project.user.github_username}"
|
||||
build.info "Logging into ghcr.io as #{project.account.github_username}"
|
||||
_stdout, stderr, status = Open3.capture3(*docker_login_command)
|
||||
|
||||
if status.success?
|
||||
|
||||
@@ -63,11 +63,17 @@ class Projects::DeploymentJob < ApplicationJob
|
||||
elsif service.web_service?
|
||||
apply_deployment(service, kubectl)
|
||||
apply_service(service, kubectl)
|
||||
if service.domains.any?
|
||||
if service.domains.any? && service.allow_public_networking?
|
||||
apply_ingress(service, kubectl)
|
||||
end
|
||||
restart_deployment(service, kubectl)
|
||||
end
|
||||
# Kill all one off containers
|
||||
kill_one_off_containers(service, kubectl)
|
||||
end
|
||||
|
||||
def kill_one_off_containers(service, kubectl)
|
||||
kubectl.call("-n #{service.project.name} delete pods -l oneoff=true")
|
||||
end
|
||||
|
||||
def apply_namespace(project, kubectl)
|
||||
@@ -90,7 +96,7 @@ class Projects::DeploymentJob < ApplicationJob
|
||||
|
||||
def upload_registry_secrets(kubectl, deployment)
|
||||
project = deployment.project
|
||||
docker_config_json = create_docker_config_json(project.user.github_username, project.user.github_access_token)
|
||||
docker_config_json = create_docker_config_json(project.account.github_username, project.account.github_access_token)
|
||||
secret_yaml = K8::Secrets::RegistrySecret.new(project, docker_config_json).to_yaml
|
||||
kubectl.apply_yaml(secret_yaml)
|
||||
end
|
||||
|
||||
23
app/madmin/resources/account_resource.rb
Normal file
23
app/madmin/resources/account_resource.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
class AccountResource < Madmin::Resource
|
||||
# Associations
|
||||
attribute :owner
|
||||
attribute :users
|
||||
attribute :clusters
|
||||
attribute :projects
|
||||
attribute :services
|
||||
attribute :add_ons
|
||||
|
||||
# Uncomment this to customize the display name of records in the admin area.
|
||||
# def self.display_name(record)
|
||||
# record.name
|
||||
# end
|
||||
|
||||
# Uncomment this to customize the default sort column and direction.
|
||||
# def self.default_sort_column
|
||||
# "created_at"
|
||||
# end
|
||||
#
|
||||
# def self.default_sort_direction
|
||||
# "desc"
|
||||
# end
|
||||
end
|
||||
@@ -11,6 +11,7 @@ class AddOnResource < Madmin::Resource
|
||||
# Associations
|
||||
attribute :log_output
|
||||
attribute :cluster
|
||||
attribute :account
|
||||
|
||||
# Uncomment this to customize the display name of records in the admin area.
|
||||
# def self.display_name(record)
|
||||
|
||||
@@ -9,7 +9,7 @@ class ClusterResource < Madmin::Resource
|
||||
|
||||
# Associations
|
||||
attribute :log_output
|
||||
attribute :user
|
||||
attribute :account
|
||||
attribute :projects
|
||||
attribute :add_ons
|
||||
attribute :domains
|
||||
|
||||
@@ -15,7 +15,7 @@ class ProjectResource < Madmin::Resource
|
||||
|
||||
# Associations
|
||||
attribute :cluster
|
||||
attribute :user
|
||||
attribute :account
|
||||
attribute :services
|
||||
attribute :environment_variables
|
||||
attribute :builds
|
||||
|
||||
@@ -19,7 +19,6 @@ class UserResource < Madmin::Resource
|
||||
attribute :clusters
|
||||
attribute :projects
|
||||
attribute :services
|
||||
attribute :docker_hub_credential
|
||||
attribute :add_ons
|
||||
|
||||
# Uncomment this to customize the display name of records in the admin area.
|
||||
|
||||
40
app/models/account.rb
Normal file
40
app/models/account.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: accounts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# owner_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_accounts_on_owner_id (owner_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (owner_id => users.id)
|
||||
#
|
||||
class Account < ApplicationRecord
|
||||
belongs_to :owner, class_name: "User"
|
||||
has_many :account_users, dependent: :destroy
|
||||
has_many :users, through: :account_users
|
||||
|
||||
has_many :clusters, dependent: :destroy
|
||||
has_many :projects, through: :clusters
|
||||
has_many :add_ons, through: :clusters
|
||||
has_many :services, through: :projects
|
||||
|
||||
def github_username
|
||||
JSON.parse(github_account.auth)["info"]["nickname"]
|
||||
end
|
||||
|
||||
def github_access_token
|
||||
github_account.access_token
|
||||
end
|
||||
|
||||
def github_account
|
||||
@_github_account ||= owner.providers.find_by(provider: "github")
|
||||
end
|
||||
end
|
||||
24
app/models/account_user.rb
Normal file
24
app/models/account_user.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_users_on_account_id (account_id)
|
||||
# index_account_users_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
class AccountUser < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :account
|
||||
end
|
||||
@@ -27,6 +27,7 @@ class AddOn < ApplicationRecord
|
||||
}.freeze
|
||||
include Loggable
|
||||
belongs_to :cluster
|
||||
has_one :account, through: :cluster
|
||||
enum :status, {installing: 0, installed: 1, uninstalling: 2, uninstalled: 3, failed: 4}
|
||||
validates :chart_type, presence: true
|
||||
validate :chart_type_exists
|
||||
|
||||
@@ -8,20 +8,21 @@
|
||||
# status :integer default("initializing"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# user_id :bigint not null
|
||||
# account_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_clusters_on_user_id (user_id)
|
||||
# index_clusters_on_account_id (account_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (user_id => users.id)
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
#
|
||||
class Cluster < ApplicationRecord
|
||||
include Loggable
|
||||
broadcasts_refreshes
|
||||
belongs_to :user
|
||||
belongs_to :account
|
||||
|
||||
has_many :projects, dependent: :destroy
|
||||
has_many :add_ons, dependent: :destroy
|
||||
has_many :domains, through: :projects
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
class Project < ApplicationRecord
|
||||
broadcasts_refreshes
|
||||
belongs_to :cluster
|
||||
has_one :user, through: :cluster
|
||||
has_one :account, through: :cluster
|
||||
has_many :services, dependent: :destroy
|
||||
has_many :environment_variables, dependent: :destroy
|
||||
has_many :builds, dependent: :destroy
|
||||
@@ -48,6 +48,10 @@ class Project < ApplicationRecord
|
||||
deployments.order(created_at: :desc).where(status: :completed).first
|
||||
end
|
||||
|
||||
def last_build
|
||||
builds.order(created_at: :desc).first
|
||||
end
|
||||
|
||||
def last_deployment
|
||||
deployments.order(created_at: :desc).first
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
# name :string not null
|
||||
# replicas :integer default(1)
|
||||
# service_type :integer not null
|
||||
# status :integer
|
||||
# status :integer default("healthy")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# project_id :bigint not null
|
||||
|
||||
@@ -28,26 +28,16 @@ class User < ApplicationRecord
|
||||
has_one_attached :avatar
|
||||
has_person_name
|
||||
|
||||
has_many :account_users, dependent: :destroy
|
||||
has_many :accounts, through: :account_users
|
||||
|
||||
has_many :providers, dependent: :destroy
|
||||
has_many :clusters, through: :accounts
|
||||
has_many :projects, through: :accounts
|
||||
has_many :add_ons, through: :accounts
|
||||
has_many :services, through: :accounts
|
||||
|
||||
# has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
|
||||
# has_many :notification_mentions, as: :record, dependent: :destroy, class_name: "Noticed::Event"
|
||||
has_many :providers
|
||||
|
||||
has_many :clusters, dependent: :destroy
|
||||
has_many :projects, through: :clusters
|
||||
has_many :services, through: :projects
|
||||
has_one :docker_hub_credential, dependent: :destroy
|
||||
has_many :add_ons, through: :clusters
|
||||
|
||||
|
||||
def github_username
|
||||
JSON.parse(github_account.auth)["info"]["nickname"]
|
||||
end
|
||||
|
||||
def github_access_token
|
||||
github_account.access_token
|
||||
end
|
||||
|
||||
def github_account
|
||||
@_github_account ||= providers.find_by(provider: "github")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<%= form.label :cluster_id %>
|
||||
<%= form.collection_select :cluster_id, current_user.clusters, :id, :name, {}, { class: "select select-bordered" } %>
|
||||
<%= form.collection_select :cluster_id, current_account.clusters, :id, :name, {}, { class: "select select-bordered" } %>
|
||||
</div>
|
||||
|
||||
<div data-controller="new-add-ons">
|
||||
|
||||
@@ -4,6 +4,20 @@
|
||||
<img src="/images/logo-full.png" class="h-[40px] hidden lg:block" />
|
||||
<span class="sr-only"><%= Rails.configuration.application_name %></span>
|
||||
<% end %>
|
||||
<% if current_user.accounts.count > 1 %>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="mx-auto dropdown">
|
||||
<button type="button" class="btn w-full"><%= current_account.name %></button>
|
||||
<ul tabindex="0" class="menu dropdown-content w-52 rounded-box bg-base-100 p-2 shadow" role="menu">
|
||||
<%= current_user.accounts.each do |account| %>
|
||||
<li>
|
||||
<%= link_to account.name, switch_account_path(account) %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div data-simplebar="init" class="h-[calc(100vh-64px)] lg:h-[calc(100vh-230px)] simplebar-scrollable-y">
|
||||
<div class="simplebar-wrapper">
|
||||
@@ -23,7 +37,7 @@
|
||||
<% end %>
|
||||
</summary>
|
||||
<ul>
|
||||
<% current_user.projects.each do |project| %>
|
||||
<% current_account.projects.each do |project| %>
|
||||
<li>
|
||||
<%= link_to root_projects_path(project), class: "hover:bg-base-content/15 #{'active' if current_page?(root_projects_path(project))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -54,7 +68,7 @@
|
||||
<% end %>
|
||||
</summary>
|
||||
<ul>
|
||||
<% current_user.clusters.each do |cluster| %>
|
||||
<% current_account.clusters.each do |cluster| %>
|
||||
<li>
|
||||
<%= link_to cluster_path(cluster), class: "hover:bg-base-content/15 #{'active' if current_page?(cluster_path(cluster))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -85,7 +99,7 @@
|
||||
<% end %>
|
||||
</summary>
|
||||
<ul>
|
||||
<% current_user.add_ons.each do |add_on| %>
|
||||
<% current_account.add_ons.each do |add_on| %>
|
||||
<li>
|
||||
<%= link_to add_on_path(add_on), class: "hover:bg-base-content/15 #{'active' if current_page?(add_on_path(add_on))}" do %>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<td>
|
||||
<% if project.last_deployment_at %>
|
||||
<div>
|
||||
<div class="text-sm"><%= "#{distance_of_time_in_words(project.last_deployment_at, Time.current)} ago" %></div>
|
||||
<div role="tooltip" data-tip="<%= project.last_deployment_at.strftime("%B %d, %Y %I:%M %p") %>" class="tooltip">
|
||||
<div class="text-sm"><%= time_ago(project.last_deployment_at) %></div>
|
||||
</div>
|
||||
<div class="text-xs italic"><%= project.last_deployment.build.commit_message %></div>
|
||||
</div>
|
||||
<% else %>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<label class="label">
|
||||
<span class="label-text">Cluster</span>
|
||||
</label>
|
||||
<%= form.collection_select :cluster_id, current_user.clusters.running, :id, :name, {}, { class: "select select-bordered" } %>
|
||||
<%= form.collection_select :cluster_id, current_account.clusters.running, :id, :name, {}, { class: "select select-bordered" } %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">* Required</span>
|
||||
</label>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<% if project.last_deployment&.in_progress? %>
|
||||
<% if project.last_build&.in_progress? %>
|
||||
<div aria-label="Badge" class="badge border-0 bg-warning/10 font-medium capitalize text-warning">
|
||||
Building <iconify-icon class="ml-1 animate-spin" icon="lucide:loader-circle"></iconify-icon>
|
||||
</div>
|
||||
<% elsif project.last_deployment&.in_progress? %>
|
||||
<div aria-label="Badge" class="badge border-0 bg-warning/10 font-medium capitalize text-warning">
|
||||
Deploying <iconify-icon class="ml-1 animate-spin" icon="lucide:loader-circle"></iconify-icon>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,9 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm capitalize"><%= build.created_at.strftime("%B %d, %Y %H:%M") %></div>
|
||||
<div role="tooltip" data-tip="<%= build.created_at.strftime("%B %d, %Y %H:%M %p") %>" class="tooltip">
|
||||
<div class="text-sm capitalize"><%= distance_of_time_in_words(build.created_at, Time.current) %> ago</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text-sm capitalize">
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<div class="mt-5">
|
||||
<div aria-label="Card" class="card card-bordered bg-base-100">
|
||||
<div class="card-body">
|
||||
<% if current_user.clusters.empty? %>
|
||||
<% if current_account.clusters.empty? %>
|
||||
<div class="text-center">
|
||||
<p class="mb-4 h3">Create your first Cluster to get started</p>
|
||||
<%= link_to t("scaffold.new.title", model: "Cluster"), new_cluster_path, class: "btn btn-primary" %>
|
||||
|
||||
@@ -30,14 +30,24 @@
|
||||
<label class="label mt-1">
|
||||
<span class="label-text cursor-pointer">Allow public networking</span>
|
||||
<%= form.check_box :allow_public_networking, class: "checkbox" %>
|
||||
<span class="label-text-alt">Checking this allows your service to be accessible from the public internet</span>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if service.web_service? || service.background_service? %>
|
||||
<div>
|
||||
<h2 class="text-lg my-2 mt-4">Resources</h2>
|
||||
<div class="form-control form-group">
|
||||
<%= form.label :replicas %>
|
||||
<%= form.number_field :replicas, class: "input input-bordered", placeholder: "1" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-footer">
|
||||
<%= form.submit class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if service.web_service? %>
|
||||
<% if service.web_service? && service.allow_public_networking? %>
|
||||
<div class="my-8">
|
||||
<h2 class="text-lg my-2">Networking</h2>
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<%= service.status %>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
Last checked <%= time_ago_in_words(service.last_health_checked_at) %> ago
|
||||
<% if service.last_health_checked_at %>
|
||||
Last checked <%= time_ago(service.last_health_checked_at) %>
|
||||
<% else %>
|
||||
Never checked
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="card-web_service" class="card-form hidden">
|
||||
<div class="card-form hidden card-web_service">
|
||||
<h3 class="text-lg font-bold">Networking</h3>
|
||||
<div class="form-group">
|
||||
<%= form.label :container_port %>
|
||||
@@ -84,15 +84,24 @@
|
||||
<%= form.label :healthcheck_url %>
|
||||
<%= form.text_field :healthcheck_url, class: "input input-bordered", placeholder: "/health" %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<div class="form-control rounded-lg bg-base-200 p-2 px-4 max-w-xs">
|
||||
<label class="label mt-1">
|
||||
<span class="label-text cursor-pointer">Allow public networking</span>
|
||||
<%= form.check_box :allow_public_networking, class: "toggle toggle-primary" %>
|
||||
<span class="label-text-alt">Checking this allows your service to be accessible from the public internet</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="card-cron_job" class="card-form hidden">
|
||||
<div class="card-form hidden card-web_service card-background_service">
|
||||
<h3 class="text-lg font-bold">Resources</h3>
|
||||
<div class="form-group">
|
||||
<%= form.label :replicas %>
|
||||
<%= form.number_field :replicas, class: "input input-bordered", placeholder: "1" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-form hidden card-cron_job">
|
||||
<div class="form-group">
|
||||
<h3 class="text-lg font-bold">Cron job</h3>
|
||||
<%= form.fields_for :cron_schedule do |cron_schedule_form| %>
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
<div class="ml-auto">
|
||||
<% if current_user.present? %>
|
||||
<div class="dropdown ml-auto dropdown-end dropdown-bottom">
|
||||
<label tabindex="0" class="btn btn-ghost rounded-btn px-1.5 hover:bg-base-content/20">
|
||||
<div aria-label="Avatar photo" class="avatar">
|
||||
<div class="mask mask-squircle" style="width: 30px; height: 30px">
|
||||
<%= image_tag avatar_path(current_user, size: 40) %>
|
||||
</div>
|
||||
<div class="dropdown ml-auto dropdown-end dropdown-bottom">
|
||||
<label tabindex="0" class="btn btn-ghost rounded-btn px-1.5 hover:bg-base-content/20">
|
||||
<div aria-label="Avatar photo" class="avatar">
|
||||
<div class="mask mask-squircle" style="width: 30px; height: 30px">
|
||||
<%= image_tag avatar_path(current_user, size: 40) %>
|
||||
</div>
|
||||
</label>
|
||||
<ul class="menu dropdown-content mt-4 w-52 rounded-box bg-base-100 p-2 shadow">
|
||||
</div>
|
||||
</label>
|
||||
<ul class="menu dropdown-content mt-4 w-52 rounded-box bg-base-100 p-2 shadow">
|
||||
<li>
|
||||
<div>
|
||||
<%# <%= link_to t(".connected_accounts"), user_connected_accounts_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' if Devise.omniauth_configs.any? %>
|
||||
<%# <%= link_to t(".accounts"), accounts_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' %>
|
||||
<%#= link_to t(".api_tokens"), api_tokens_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' if Jumpstart.config.payments_enabled? %>
|
||||
<%= link_to edit_user_registration_path do %>
|
||||
<iconify-icon icon="lucide:user" height="16"></iconify-icon>
|
||||
My Profile
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% if current_user.admin? && respond_to?(:madmin_root_path) %>
|
||||
<li>
|
||||
<div>
|
||||
<%# <%= link_to t(".connected_accounts"), user_connected_accounts_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' if Devise.omniauth_configs.any? %>
|
||||
<%# <%= link_to t(".accounts"), accounts_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' %>
|
||||
<%#= link_to t(".api_tokens"), api_tokens_path, class: 'text-gray-800 bg-white hover:bg-primary-50 dark:text-gray-50 dark:bg-gray-800 dark:hover:bg-primary-800 transition ease-in-out duration-200 whitespace-nowrap no-underline block px-6 py-3' if Jumpstart.config.payments_enabled? %>
|
||||
<%= link_to edit_user_registration_path do %>
|
||||
<%= link_to admin_root_path do %>
|
||||
<iconify-icon icon="lucide:user" height="16"></iconify-icon>
|
||||
My Profile
|
||||
Dashboard
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% if current_user.admin? && respond_to?(:madmin_root_path) %>
|
||||
<li>
|
||||
<div>
|
||||
<%= link_to admin_root_path do %>
|
||||
<iconify-icon icon="lucide:user" height="16"></iconify-icon>
|
||||
Dashboard
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
<%= link_to admin_sidekiq_web_path do %>
|
||||
<iconify-icon icon="lucide:user" height="16"></iconify-icon>
|
||||
Sidekiq
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
<hr class="-mx-2 my-1 border-base-content/10">
|
||||
</li>
|
||||
<li>
|
||||
<div class="text-error">
|
||||
<%= link_to destroy_user_session_path, method: :delete do %>
|
||||
<iconify-icon icon="lucide:log-out" height="16"></iconify-icon>
|
||||
Logout
|
||||
<div>
|
||||
<%= link_to admin_sidekiq_web_path do %>
|
||||
<iconify-icon icon="lucide:user" height="16"></iconify-icon>
|
||||
Sidekiq
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center gap-2">
|
||||
<%= link_to new_user_session_path, class: "btn btn-primary" do %>
|
||||
Login
|
||||
<% end %>
|
||||
<%= link_to new_user_registration_path, class: "btn btn-primary" do %>
|
||||
Sign Up
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<hr class="-mx-2 my-1 border-base-content/10">
|
||||
<li>
|
||||
<div class="text-error">
|
||||
<%= link_to destroy_user_session_path, method: :delete do %>
|
||||
<iconify-icon icon="lucide:log-out" height="16"></iconify-icon>
|
||||
Logout
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,11 @@ require "sidekiq/web"
|
||||
|
||||
Rails.application.routes.draw do
|
||||
draw :madmin
|
||||
resources :accounts, only: [] do
|
||||
member do
|
||||
get :switch
|
||||
end
|
||||
end
|
||||
namespace :inbound_webhooks do
|
||||
resources :github, controller: :github, only: [ :create ]
|
||||
end
|
||||
|
||||
10
db/migrate/20240925205148_create_accounts.rb
Normal file
10
db/migrate/20240925205148_create_accounts.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class CreateAccounts < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :accounts do |t|
|
||||
t.references :owner, foreign_key: { to_table: :users }, index: true
|
||||
t.string :name, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
10
db/migrate/20240925205149_create_account_users.rb
Normal file
10
db/migrate/20240925205149_create_account_users.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
class CreateAccountUsers < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
create_table :account_users do |t|
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.references :account, null: false, foreign_key: true
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ class CreateClusters < ActiveRecord::Migration[7.2]
|
||||
create_table :clusters do |t|
|
||||
t.string :name, null: false
|
||||
t.jsonb :kubeconfig, null: false, default: {}
|
||||
t.references :user, null: false, foreign_key: true
|
||||
t.references :account, null: false, foreign_key: true
|
||||
t.integer :status, null: false, default: 0
|
||||
|
||||
t.timestamps
|
||||
|
||||
@@ -3,8 +3,14 @@ class CreateServices < ActiveRecord::Migration[7.2]
|
||||
create_table :services do |t|
|
||||
t.references :project, null: false, foreign_key: true
|
||||
t.integer :service_type, null: false
|
||||
t.string :command, null: false
|
||||
t.string :command
|
||||
t.string :name, null: false
|
||||
t.integer :replicas, default: 1
|
||||
t.string :healthcheck_url
|
||||
t.boolean :allow_public_networking, default: false
|
||||
t.integer :status, default: 0
|
||||
t.datetime :last_health_checked_at
|
||||
t.integer :container_port, default: 3000
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
class AddContainerPortToServices < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :services, :container_port, :integer, default: 3000
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class RemoveNullConstraintOnServices < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
change_column_null :services, :command, true
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
class AddHealthcheckUrlToServices < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :services, :healthcheck_url, :string
|
||||
add_column :services, :allow_public_networking, :boolean, default: false
|
||||
add_column :services, :status, :integer
|
||||
add_column :services, :last_health_checked_at, :datetime
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
class AddReplicasToServices < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :services, :replicas, :integer, default: 1
|
||||
end
|
||||
end
|
||||
38
db/schema.rb
generated
38
db/schema.rb
generated
@@ -10,10 +10,27 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_17_220734) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_10_09_172011) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "account_users", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_account_users_on_account_id"
|
||||
t.index ["user_id"], name: "index_account_users_on_user_id"
|
||||
end
|
||||
|
||||
create_table "accounts", force: :cascade do |t|
|
||||
t.bigint "owner_id"
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["owner_id"], name: "index_accounts_on_owner_id"
|
||||
end
|
||||
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -78,11 +95,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_220734) do
|
||||
create_table "clusters", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.jsonb "kubeconfig", default: {}, null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.integer "status", default: 0, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["user_id"], name: "index_clusters_on_user_id"
|
||||
t.index ["account_id"], name: "index_clusters_on_account_id"
|
||||
end
|
||||
|
||||
create_table "cron_schedules", force: :cascade do |t|
|
||||
@@ -222,14 +239,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_220734) do
|
||||
t.integer "service_type", null: false
|
||||
t.string "command"
|
||||
t.string "name", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "container_port", default: 3000
|
||||
t.integer "replicas", default: 1
|
||||
t.string "healthcheck_url"
|
||||
t.boolean "allow_public_networking", default: false
|
||||
t.integer "status"
|
||||
t.integer "status", default: 0
|
||||
t.datetime "last_health_checked_at"
|
||||
t.integer "replicas", default: 1
|
||||
t.integer "container_port", default: 3000
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["project_id"], name: "index_services_on_project_id"
|
||||
end
|
||||
|
||||
@@ -249,11 +266,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_17_220734) do
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "account_users", "accounts"
|
||||
add_foreign_key "account_users", "users"
|
||||
add_foreign_key "accounts", "users", column: "owner_id"
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "add_ons", "clusters"
|
||||
add_foreign_key "builds", "projects"
|
||||
add_foreign_key "clusters", "users"
|
||||
add_foreign_key "clusters", "accounts"
|
||||
add_foreign_key "cron_schedules", "services"
|
||||
add_foreign_key "deployments", "builds"
|
||||
add_foreign_key "environment_variables", "projects"
|
||||
|
||||
@@ -6,7 +6,12 @@ metadata:
|
||||
labels:
|
||||
app: <%= service.name %>
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: <%= service.replicas %>
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: <%= service.name %>
|
||||
@@ -24,5 +29,19 @@ spec:
|
||||
name: <%= project.name %>
|
||||
ports:
|
||||
- containerPort: <%= service.container_port %>
|
||||
<% if service.healthcheck_url.present? %>
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: <%= service.healthcheck_url %>
|
||||
port: <%= service.container_port %>
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: <%= service.healthcheck_url %>
|
||||
port: <%= service.container_port %>
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
<% end %>
|
||||
imagePullSecrets:
|
||||
- name: dockerconfigjson-github-com
|
||||
|
||||
28
test/fixtures/account_users.yml
vendored
Normal file
28
test/fixtures/account_users.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_users_on_account_id (account_id)
|
||||
# index_account_users_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
|
||||
one:
|
||||
user: one
|
||||
account: one
|
||||
|
||||
two:
|
||||
user: two
|
||||
account: two
|
||||
24
test/fixtures/accounts.yml
vendored
Normal file
24
test/fixtures/accounts.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: accounts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# owner_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_accounts_on_owner_id (owner_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (owner_id => users.id)
|
||||
#
|
||||
|
||||
one:
|
||||
owner_id:
|
||||
|
||||
two:
|
||||
owner_id:
|
||||
25
test/models/account_test.rb
Normal file
25
test/models/account_test.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: accounts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# owner_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_accounts_on_owner_id (owner_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (owner_id => users.id)
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class AccountTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
27
test/models/account_user_test.rb
Normal file
27
test/models/account_user_test.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# user_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_account_users_on_account_id (account_id)
|
||||
# index_account_users_on_user_id (user_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
# fk_rails_... (user_id => users.id)
|
||||
#
|
||||
require "test_helper"
|
||||
|
||||
class AccountUserTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
Reference in New Issue
Block a user