# typed: ignore require 'kube-dsl' require 'kuby/cert-manager' module Kuby module Plugins module RailsApp class Plugin < ::Kuby::Plugin depends_on :kubernetes, '>= 1.20' extend ::KubeDSL::ValueFields WEB_ROLE = 'web'.freeze DEFAULT_HOSTNAME = 'localhost'.freeze MASTER_KEY_VAR = 'RAILS_MASTER_KEY'.freeze ENV_SECRETS = [MASTER_KEY_VAR].freeze ENV_EXCLUDE = ['RAILS_ENV'].freeze DEFAULT_ASSET_URL = '/assets'.freeze DEFAULT_PACKS_URL = '/packs'.freeze DEFAULT_ASSET_PATH = './public'.freeze value_field :root, default: '.' value_fields :hostname, :tls_enabled, :manage_database, :replicas value_fields :asset_url, :packs_url, :asset_path alias_method :manage_database?, :manage_database def initialize(environment) @environment = environment @tls_enabled = true @replicas = 1 @manage_database = true @hostname = DEFAULT_HOSTNAME @asset_url = DEFAULT_ASSET_URL @packs_url = DEFAULT_PACKS_URL @asset_path = DEFAULT_ASSET_PATH end def configure(&block) instance_eval(&block) if block end def after_configuration context = self if manage_database? && @database = Database.get(self) @database.plugin.instance_eval(&@database_block) if @database_block environment.kubernetes.plugins[@database.plugin_name] = @database.plugin environment.docker do insert :rewrite_db_config, RewriteDbConfig.new, after: :copy_phase end @database.plugin.configure_pod_spec(deployment.spec.template.spec) end environment.kubernetes.add_plugin(:nginx_ingress) environment.kubernetes.add_plugin(:rails_assets) do asset_url context.asset_url packs_url context.packs_url asset_path context.asset_path end if @tls_enabled context = self environment.kubernetes.add_plugin(:cert_manager) do email context.environment.docker.credentials.email end end end def database_host database.plugin.host end def before_deploy(manifest) # Make sure plugin has been configured. If not, do nothing. if cert_manager = environment.kubernetes.plugin(:cert_manager) cert_manager.annotate_ingress(ingress) if tls_enabled end image_with_tag = "#{docker.image.image_url}:#{kubernetes.tag || Kuby::Docker::LATEST_TAG}" if assets = environment.kubernetes.plugin(:rails_assets) assets.configure_ingress(ingress, hostname) end spec = self deployment do metadata do annotations do add( 'getkuby.io/dockerfile-checksum', spec.environment.docker.image.dockerfile.checksum ) end end spec do template do spec do container(:web) do image image_with_tag end if spec.manage_database? init_container(:create_db) do image image_with_tag end init_container(:migrate_db) do image image_with_tag end end end end end end end def database(&block) if block @database_block = block elsif environment.configured? @database end end def service(&block) spec = self @service ||= KubeDSL.service do metadata do name "#{spec.selector_app}-svc" namespace spec.namespace.metadata.name labels do add :app, spec.selector_app add :role, spec.role end end spec do type 'NodePort' selector do add :app, spec.selector_app add :role, spec.role end port do name 'http' port spec.docker.webserver_phase.port protocol 'TCP' target_port 'http' end end end @service.instance_eval(&block) if block @service end def service_account(&block) spec = self @service_account ||= KubeDSL.service_account do metadata do name "#{spec.selector_app}-sa" namespace spec.namespace.metadata.name labels do add :app, spec.selector_app add :role, spec.role end end end @service_account.instance_eval(&block) if block @service_account end def config_map(&block) spec = self @config_map ||= KubeDSL.config_map do metadata do name "#{spec.selector_app}-config" namespace spec.namespace.metadata.name end data do ENV.each_pair do |key, val| include_key = key.start_with?('RAILS_') && !ENV_SECRETS.include?(key) && !ENV_EXCLUDE.include?(key) if include_key add key.to_sym, val end end end end @config_map.instance_eval(&block) if block @config_map end alias_method :env, :config_map def app_secrets(&block) spec = self @app_secrets ||= KubeDSL.secret do metadata do name "#{spec.selector_app}-secrets" namespace spec.namespace.metadata.name end type 'Opaque' data do add MASTER_KEY_VAR.to_sym, spec.master_key end end @app_secrets.instance_eval(&block) if block @app_secrets end def deployment(&block) kube_spec = self @deployment ||= KubeDSL.deployment do metadata do name "#{kube_spec.selector_app}-#{kube_spec.role}" namespace kube_spec.namespace.metadata.name labels do add :app, kube_spec.selector_app add :role, kube_spec.role end end spec do replicas kube_spec.replicas selector do match_labels do add :app, kube_spec.selector_app add :role, kube_spec.role end end strategy do type 'RollingUpdate' rolling_update do max_surge '25%' max_unavailable 0 end end template do metadata do labels do add :app, kube_spec.selector_app add :role, kube_spec.role end end spec do container(:web) do name "#{kube_spec.selector_app}-#{kube_spec.role}" image_pull_policy 'IfNotPresent' port do container_port kube_spec.docker.webserver_phase.port name 'http' protocol 'TCP' end env_from do config_map_ref do name kube_spec.config_map.metadata.name end end env_from do secret_ref do name kube_spec.app_secrets.metadata.name end end readiness_probe do success_threshold 1 failure_threshold 2 initial_delay_seconds 15 period_seconds 3 timeout_seconds 1 http_get do path '/healthz' port kube_spec.docker.webserver_phase.port scheme 'HTTP' end end end if kube_spec.manage_database? init_container(:create_db) do name "#{kube_spec.selector_app}-create-db" command %w(bundle exec rake kuby:rails_app:db:bootstrap) env_from do config_map_ref do name kube_spec.config_map.metadata.name end end env_from do secret_ref do name kube_spec.app_secrets.metadata.name end end end init_container(:migrate_db) do name "#{kube_spec.selector_app}-migrate-db" command %w(bundle exec rake kuby:rails_app:db:migrate) env_from do config_map_ref do name kube_spec.config_map.metadata.name end end env_from do secret_ref do name kube_spec.app_secrets.metadata.name end end end end image_pull_secret do name kube_spec.environment.kubernetes.registry_secret.metadata.name end restart_policy 'Always' service_account_name kube_spec.service_account.metadata.name end end end end @deployment.instance_eval(&block) if block @deployment end def ingress(&block) spec = self tls_enabled = @tls_enabled @ingress ||= KubeDSL::DSL::Networking::V1::Ingress.new do metadata do name "#{spec.selector_app}-ingress" namespace spec.namespace.metadata.name annotations do add :'kubernetes.io/ingress.class', 'nginx' end end spec do rule do host spec.hostname http do path do path '/' path_type 'Prefix' backend do service do name spec.service.metadata.name port do name spec.service.spec.ports.first.name end end end end end end if tls_enabled tls do secret_name "#{spec.selector_app}-tls" hosts [spec.hostname] end end end end @ingress.instance_eval(&block) if block @ingress end def resources @resources ||= [ service, service_account, config_map, app_secrets, deployment, *database&.plugin&.resources, ingress ] end def selector_app environment.kubernetes.selector_app end def role WEB_ROLE end def docker environment.docker end def kubernetes environment.kubernetes end def namespace environment.kubernetes.namespace end def master_key @master_key ||= ENV[MASTER_KEY_VAR] || begin master_key_path = File.join(root, 'config', 'master.key') File.read(master_key_path).strip if File.exist?(master_key_path) end end end end end end