Repository: UffizziCloud/uffizzi Branch: develop Commit: a57ce80acba3 Files: 713 Total size: 857.5 KB Directory structure: gitextract_iyyaf1qh/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── helm-release.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Gemfile ├── INSTALL.md ├── LICENSE ├── Makefile ├── README.md ├── Rakefile ├── app/ │ ├── assets/ │ │ ├── config/ │ │ │ └── manifest.js │ │ ├── fonts/ │ │ │ └── mtiFontTrackingCode.js │ │ ├── images/ │ │ │ └── .keep │ │ └── stylesheets/ │ │ └── application.css │ ├── channels/ │ │ └── application_cable/ │ │ ├── channel.rb │ │ └── connection.rb │ ├── controllers/ │ │ ├── application_controller.rb │ │ └── concerns/ │ │ └── .keep │ ├── helpers/ │ │ └── application_helper.rb │ ├── javascript/ │ │ ├── channels/ │ │ │ ├── consumer.js │ │ │ └── index.js │ │ └── packs/ │ │ └── application.js │ ├── jobs/ │ │ └── application_job.rb │ ├── mailers/ │ │ └── application_mailer.rb │ ├── models/ │ │ ├── application_record.rb │ │ └── concerns/ │ │ └── .keep │ └── views/ │ └── layouts/ │ ├── application.html.erb │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin/ │ ├── rails │ ├── rake │ ├── setup │ └── yarn ├── charts/ │ └── uffizzi-app/ │ ├── Chart.yaml │ ├── README.md │ ├── templates/ │ │ ├── configmap-common.yaml │ │ ├── configmap-sidekiq.yaml │ │ ├── configmap-web.yaml │ │ ├── secret-web.yaml │ │ ├── sidekiq-deployment.yaml │ │ ├── web-deployment.yaml │ │ ├── web-ingress.yaml │ │ └── web-service.yaml │ └── values.yaml ├── ci/ │ ├── github-actions/ │ │ └── README.md │ └── gitlab/ │ └── README.md ├── config/ │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── credentials.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments/ │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers/ │ │ ├── application_controller_renderer.rb │ │ ├── assets.rb │ │ ├── backtrace_silencers.rb │ │ ├── content_security_policy.rb │ │ ├── cookies_serializer.rb │ │ ├── filter_parameter_logging.rb │ │ ├── health_check.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── sidekiq.rb │ │ └── wrap_parameters.rb │ ├── locales/ │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ ├── secrets.yml │ ├── settings.yml │ ├── sidekiq.yml │ └── storage.yml ├── config.ru ├── core/ │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── Gemfile │ ├── LICENSE │ ├── Makefile │ ├── README.md │ ├── Rakefile │ ├── app/ │ │ ├── assets/ │ │ │ ├── config/ │ │ │ │ └── uffizzi_core_manifest.js │ │ │ ├── images/ │ │ │ │ └── uffizzi_core/ │ │ │ │ └── .keep │ │ │ └── stylesheets/ │ │ │ └── uffizzi_core/ │ │ │ └── application.css │ │ ├── clients/ │ │ │ └── uffizzi_core/ │ │ │ ├── amazon_registry_client.rb │ │ │ ├── azure_registry_client/ │ │ │ │ └── request_result.rb │ │ │ ├── azure_registry_client.rb │ │ │ ├── container_registry_request_decorator.rb │ │ │ ├── controller_client/ │ │ │ │ └── request_result.rb │ │ │ ├── controller_client.rb │ │ │ ├── docker_hub_client/ │ │ │ │ ├── not_authorized_error.rb │ │ │ │ └── request_result.rb │ │ │ ├── docker_hub_client.rb │ │ │ ├── docker_registry_client/ │ │ │ │ └── request_result.rb │ │ │ ├── docker_registry_client.rb │ │ │ ├── github_container_registry_client/ │ │ │ │ └── request_result.rb │ │ │ ├── github_container_registry_client.rb │ │ │ ├── google_registry_client/ │ │ │ │ └── request_result.rb │ │ │ └── google_registry_client.rb │ │ ├── contexts/ │ │ │ └── uffizzi_core/ │ │ │ ├── account_context.rb │ │ │ ├── base_context.rb │ │ │ ├── project/ │ │ │ │ └── cluster_context.rb │ │ │ ├── project_context.rb │ │ │ └── webhooks_context.rb │ │ ├── controller_modules/ │ │ │ └── uffizzi_core/ │ │ │ └── api/ │ │ │ └── cli/ │ │ │ └── v1/ │ │ │ ├── accounts_controller_module.rb │ │ │ ├── projects/ │ │ │ │ ├── clusters_controller_module.rb │ │ │ │ └── deployments_controller_module.rb │ │ │ └── projects_controller_module.rb │ │ ├── controllers/ │ │ │ ├── concerns/ │ │ │ │ └── uffizzi_core/ │ │ │ │ ├── auth_management.rb │ │ │ │ ├── authorization_concern.rb │ │ │ │ └── dependency_injection_concern.rb │ │ │ └── uffizzi_core/ │ │ │ ├── api/ │ │ │ │ └── cli/ │ │ │ │ └── v1/ │ │ │ │ ├── accounts/ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ ├── clusters_controller.rb │ │ │ │ │ ├── credentials_controller.rb │ │ │ │ │ └── projects_controller.rb │ │ │ │ ├── accounts_controller.rb │ │ │ │ ├── application_controller.rb │ │ │ │ ├── ci/ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ └── sessions_controller.rb │ │ │ │ ├── projects/ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ ├── clusters/ │ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ │ └── ingresses_controller.rb │ │ │ │ │ ├── clusters_controller.rb │ │ │ │ │ ├── compose_files_controller.rb │ │ │ │ │ ├── deployments/ │ │ │ │ │ │ ├── activity_items_controller.rb │ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ │ ├── containers/ │ │ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ │ │ └── logs_controller.rb │ │ │ │ │ │ ├── containers_controller.rb │ │ │ │ │ │ └── events_controller.rb │ │ │ │ │ ├── deployments_controller.rb │ │ │ │ │ └── secrets_controller.rb │ │ │ │ ├── projects_controller.rb │ │ │ │ └── sessions_controller.rb │ │ │ └── application_controller.rb │ │ ├── errors/ │ │ │ └── uffizzi_core/ │ │ │ ├── cluster_scale_error.rb │ │ │ ├── compose_file/ │ │ │ │ ├── build_error.rb │ │ │ │ ├── credential_error.rb │ │ │ │ ├── parse_error.rb │ │ │ │ └── secrets_error.rb │ │ │ ├── compose_file_error.rb │ │ │ ├── container_registry_error.rb │ │ │ ├── deployment/ │ │ │ │ └── image_pull_error.rb │ │ │ ├── deployment_not_found_error.rb │ │ │ └── registry_not_supported_error.rb │ │ ├── forms/ │ │ │ └── uffizzi_core/ │ │ │ ├── api/ │ │ │ │ └── cli/ │ │ │ │ └── v1/ │ │ │ │ ├── account/ │ │ │ │ │ └── credential/ │ │ │ │ │ ├── check_credential_form.rb │ │ │ │ │ ├── create_form.rb │ │ │ │ │ └── update_form.rb │ │ │ │ ├── cluster/ │ │ │ │ │ ├── create_form.rb │ │ │ │ │ └── sync_form.rb │ │ │ │ ├── compose_file/ │ │ │ │ │ ├── check_credentials_form.rb │ │ │ │ │ ├── cli_form.rb │ │ │ │ │ ├── create_form.rb │ │ │ │ │ ├── template_form.rb │ │ │ │ │ └── update_form.rb │ │ │ │ ├── config_file/ │ │ │ │ │ └── create_form.rb │ │ │ │ ├── deployment/ │ │ │ │ │ ├── create_form.rb │ │ │ │ │ └── update_form.rb │ │ │ │ ├── project/ │ │ │ │ │ ├── create_form.rb │ │ │ │ │ └── update_form.rb │ │ │ │ ├── secret/ │ │ │ │ │ └── bulk_assign_form.rb │ │ │ │ ├── session_create_form.rb │ │ │ │ └── template/ │ │ │ │ └── create_form.rb │ │ │ ├── application_form.rb │ │ │ ├── application_form_without_active_record.rb │ │ │ └── mass_assignment_control_concern.rb │ │ ├── helpers/ │ │ │ └── uffizzi_core/ │ │ │ └── application_helper.rb │ │ ├── jobs/ │ │ │ └── uffizzi_core/ │ │ │ ├── account/ │ │ │ │ ├── create_credential_job.rb │ │ │ │ └── update_credential_job.rb │ │ │ ├── activity_item/ │ │ │ │ └── docker/ │ │ │ │ └── update_digest_job.rb │ │ │ ├── application_job.rb │ │ │ ├── cluster/ │ │ │ │ ├── delete_job.rb │ │ │ │ ├── deploy_job.rb │ │ │ │ ├── manage_deploying_job.rb │ │ │ │ ├── manage_scaling_down_job.rb │ │ │ │ └── manage_scaling_up_job.rb │ │ │ ├── config_file/ │ │ │ │ └── apply_job.rb │ │ │ └── deployment/ │ │ │ ├── create_credential_job.rb │ │ │ ├── create_credentials_job.rb │ │ │ ├── create_job.rb │ │ │ ├── delete_credential_job.rb │ │ │ ├── delete_job.rb │ │ │ ├── deploy_containers_job.rb │ │ │ ├── manage_deploy_activity_item_job.rb │ │ │ └── update_credential_job.rb │ │ ├── lib/ │ │ │ └── uffizzi_core/ │ │ │ ├── concerns/ │ │ │ │ └── models/ │ │ │ │ ├── account.rb │ │ │ │ ├── activity_item.rb │ │ │ │ ├── cluster.rb │ │ │ │ ├── comment.rb │ │ │ │ ├── compose_file.rb │ │ │ │ ├── config_file.rb │ │ │ │ ├── container.rb │ │ │ │ ├── container_config_file.rb │ │ │ │ ├── container_host_volume_file.rb │ │ │ │ ├── coupon.rb │ │ │ │ ├── credential.rb │ │ │ │ ├── deployment.rb │ │ │ │ ├── deployment_event.rb │ │ │ │ ├── event.rb │ │ │ │ ├── host_volume_file.rb │ │ │ │ ├── kubernetes_distribution.rb │ │ │ │ ├── membership.rb │ │ │ │ ├── payment.rb │ │ │ │ ├── price.rb │ │ │ │ ├── product.rb │ │ │ │ ├── project.rb │ │ │ │ ├── rating.rb │ │ │ │ ├── repo.rb │ │ │ │ ├── role.rb │ │ │ │ ├── secret.rb │ │ │ │ ├── template.rb │ │ │ │ ├── user.rb │ │ │ │ └── user_project.rb │ │ │ └── rbac/ │ │ │ └── user_access_service.rb │ │ ├── mailers/ │ │ │ └── uffizzi_core/ │ │ │ └── application_mailer.rb │ │ ├── models/ │ │ │ ├── concerns/ │ │ │ │ └── uffizzi_core/ │ │ │ │ ├── hashid_concern.rb │ │ │ │ └── state_machine_concern.rb │ │ │ └── uffizzi_core/ │ │ │ ├── account.rb │ │ │ ├── activity_item/ │ │ │ │ ├── docker.rb │ │ │ │ ├── github.rb │ │ │ │ └── memory_limit.rb │ │ │ ├── activity_item.rb │ │ │ ├── application_record.rb │ │ │ ├── cluster.rb │ │ │ ├── comment.rb │ │ │ ├── compose_file.rb │ │ │ ├── config_file.rb │ │ │ ├── container.rb │ │ │ ├── container_config_file.rb │ │ │ ├── container_host_volume_file.rb │ │ │ ├── continuous_preview.rb │ │ │ ├── coupon.rb │ │ │ ├── credential/ │ │ │ │ ├── amazon.rb │ │ │ │ ├── azure.rb │ │ │ │ ├── docker_hub.rb │ │ │ │ ├── docker_registry.rb │ │ │ │ ├── github_container_registry.rb │ │ │ │ └── google.rb │ │ │ ├── credential.rb │ │ │ ├── database.rb │ │ │ ├── database_offering.rb │ │ │ ├── deployment.rb │ │ │ ├── deployment_event.rb │ │ │ ├── event.rb │ │ │ ├── host_volume_file.rb │ │ │ ├── kubernetes_distribution.rb │ │ │ ├── membership.rb │ │ │ ├── payment.rb │ │ │ ├── price.rb │ │ │ ├── product.rb │ │ │ ├── project.rb │ │ │ ├── rating.rb │ │ │ ├── repo/ │ │ │ │ ├── amazon.rb │ │ │ │ ├── azure.rb │ │ │ │ ├── docker_hub.rb │ │ │ │ ├── docker_registry.rb │ │ │ │ ├── github_container_registry.rb │ │ │ │ └── google.rb │ │ │ ├── repo.rb │ │ │ ├── role.rb │ │ │ ├── secret.rb │ │ │ ├── template.rb │ │ │ ├── user.rb │ │ │ └── user_project.rb │ │ ├── policies/ │ │ │ └── uffizzi_core/ │ │ │ ├── api/ │ │ │ │ └── cli/ │ │ │ │ └── v1/ │ │ │ │ ├── accounts/ │ │ │ │ │ ├── clusters_policy.rb │ │ │ │ │ ├── credentials_policy.rb │ │ │ │ │ └── projects_policy.rb │ │ │ │ ├── accounts_policy.rb │ │ │ │ ├── projects/ │ │ │ │ │ ├── clusters/ │ │ │ │ │ │ └── ingresses_policy.rb │ │ │ │ │ ├── clusters_policy.rb │ │ │ │ │ ├── compose_files_policy.rb │ │ │ │ │ ├── deployments/ │ │ │ │ │ │ ├── activity_items_policy.rb │ │ │ │ │ │ ├── containers_policy.rb │ │ │ │ │ │ └── events_policy.rb │ │ │ │ │ ├── deployments_policy.rb │ │ │ │ │ └── secrets_policy.rb │ │ │ │ └── projects_policy.rb │ │ │ └── application_policy.rb │ │ ├── repositories/ │ │ │ └── uffizzi_core/ │ │ │ ├── account_repo.rb │ │ │ ├── activity_item_repo.rb │ │ │ ├── basic_order_repo.rb │ │ │ ├── cluster_repo.rb │ │ │ ├── comment_repo.rb │ │ │ ├── compose_file_repo.rb │ │ │ ├── config_file_repo.rb │ │ │ ├── container_repo.rb │ │ │ ├── credential_repo.rb │ │ │ ├── deployment_repo.rb │ │ │ ├── event_repo.rb │ │ │ ├── host_volume_file_repo.rb │ │ │ ├── membership_repo.rb │ │ │ ├── price_repo.rb │ │ │ ├── product_repo.rb │ │ │ ├── project_repo.rb │ │ │ ├── repo_repo.rb │ │ │ ├── template_repo.rb │ │ │ ├── usage_repo.rb │ │ │ └── user_repo.rb │ │ ├── responders/ │ │ │ └── uffizzi_core/ │ │ │ └── json_responder.rb │ │ ├── serializers/ │ │ │ └── uffizzi_core/ │ │ │ ├── api/ │ │ │ │ └── cli/ │ │ │ │ └── v1/ │ │ │ │ ├── account_serializer.rb │ │ │ │ ├── accounts/ │ │ │ │ │ ├── cluster_serializer/ │ │ │ │ │ │ └── project_serializer.rb │ │ │ │ │ ├── cluster_serializer.rb │ │ │ │ │ ├── credential_serializer.rb │ │ │ │ │ └── project_serializer.rb │ │ │ │ ├── project_serializer/ │ │ │ │ │ ├── account_serializer.rb │ │ │ │ │ ├── compose_file_serializer.rb │ │ │ │ │ └── deployment_serializer.rb │ │ │ │ ├── project_serializer.rb │ │ │ │ ├── projects/ │ │ │ │ │ ├── cluster_serializer.rb │ │ │ │ │ ├── compose_file_serializer.rb │ │ │ │ │ ├── deployment_serializer/ │ │ │ │ │ │ ├── container_serializer.rb │ │ │ │ │ │ └── user_serializer.rb │ │ │ │ │ ├── deployment_serializer.rb │ │ │ │ │ ├── deployments/ │ │ │ │ │ │ ├── activity_item_serializer.rb │ │ │ │ │ │ ├── container_serializer/ │ │ │ │ │ │ │ ├── container_config_file_serializer/ │ │ │ │ │ │ │ │ └── config_file_serializer.rb │ │ │ │ │ │ │ └── container_config_file_serializer.rb │ │ │ │ │ │ └── container_serializer.rb │ │ │ │ │ ├── deployments_serializer/ │ │ │ │ │ │ └── user_serializer.rb │ │ │ │ │ ├── deployments_serializer.rb │ │ │ │ │ ├── secret_serializer.rb │ │ │ │ │ └── short_cluster_serializer.rb │ │ │ │ ├── short_project_serializer.rb │ │ │ │ ├── user_serializer/ │ │ │ │ │ └── account_serializer.rb │ │ │ │ └── user_serializer.rb │ │ │ ├── base_serializer.rb │ │ │ └── controller/ │ │ │ ├── apply_config_file/ │ │ │ │ └── config_file_serializer.rb │ │ │ ├── create_cluster/ │ │ │ │ └── cluster_serializer.rb │ │ │ ├── create_credential/ │ │ │ │ └── credential_serializer.rb │ │ │ ├── create_deployment/ │ │ │ │ └── deployment_serializer.rb │ │ │ ├── deploy_containers/ │ │ │ │ ├── compose_file_serializer.rb │ │ │ │ ├── container_serializer/ │ │ │ │ │ ├── container_config_file_serializer/ │ │ │ │ │ │ └── config_file_serializer.rb │ │ │ │ │ ├── container_config_file_serializer.rb │ │ │ │ │ └── container_host_volume_file_serializer.rb │ │ │ │ ├── container_serializer.rb │ │ │ │ ├── credential_serializer.rb │ │ │ │ └── host_volume_file_serializer.rb │ │ │ └── update_cluster/ │ │ │ └── cluster_serializer.rb │ │ ├── services/ │ │ │ └── uffizzi_core/ │ │ │ ├── account_service.rb │ │ │ ├── activity_item_service.rb │ │ │ ├── ci_service.rb │ │ │ ├── cluster_service.rb │ │ │ ├── compose_file/ │ │ │ │ ├── builders/ │ │ │ │ │ ├── container_builder_service.rb │ │ │ │ │ ├── container_config_files_builder_service.rb │ │ │ │ │ ├── container_host_volume_files_builder_service.rb │ │ │ │ │ ├── docker_repo_builder_service.rb │ │ │ │ │ ├── template_builder_service.rb │ │ │ │ │ └── variables_builder_service.rb │ │ │ │ ├── config_files_service.rb │ │ │ │ ├── config_option_service.rb │ │ │ │ ├── container_service.rb │ │ │ │ ├── dependencies_service.rb │ │ │ │ ├── errors_service.rb │ │ │ │ ├── github_dependencies_service.rb │ │ │ │ ├── host_volume_files_service.rb │ │ │ │ ├── parsers/ │ │ │ │ │ ├── configs_parser_service.rb │ │ │ │ │ ├── continuous_preview_parser_service.rb │ │ │ │ │ ├── ingress_parser_service.rb │ │ │ │ │ ├── named_volumes_parser_service.rb │ │ │ │ │ ├── secrets_parser_service.rb │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── command_parser_service.rb │ │ │ │ │ │ ├── configs_parser_service.rb │ │ │ │ │ │ ├── deploy_parser_service.rb │ │ │ │ │ │ ├── entrypoint_parser_service.rb │ │ │ │ │ │ ├── env_file_parser_service.rb │ │ │ │ │ │ ├── environment_parser_service.rb │ │ │ │ │ │ ├── healthcheck_parser_service.rb │ │ │ │ │ │ ├── image_parser_service.rb │ │ │ │ │ │ ├── secrets_parser_service.rb │ │ │ │ │ │ └── volumes_parser_service.rb │ │ │ │ │ ├── services_parser_service.rb │ │ │ │ │ └── variables_parser_service.rb │ │ │ │ └── template_service.rb │ │ │ ├── compose_file_service.rb │ │ │ ├── container_registry/ │ │ │ │ ├── amazon_service.rb │ │ │ │ ├── azure_service.rb │ │ │ │ ├── docker_hub_service.rb │ │ │ │ ├── docker_registry_service.rb │ │ │ │ ├── github_container_registry_service.rb │ │ │ │ └── google_service.rb │ │ │ ├── container_registry_service.rb │ │ │ ├── container_service.rb │ │ │ ├── controller_service.rb │ │ │ ├── controller_settings_service.rb │ │ │ ├── deployment/ │ │ │ │ ├── domain_service.rb │ │ │ │ └── memory_service.rb │ │ │ ├── deployment_service.rb │ │ │ ├── logs_service.rb │ │ │ ├── manage_activity_items_service.rb │ │ │ ├── project_service.rb │ │ │ ├── repo_service.rb │ │ │ ├── response_service.rb │ │ │ ├── template/ │ │ │ │ └── memory_service.rb │ │ │ ├── token_service.rb │ │ │ ├── user_access_service.rb │ │ │ └── user_generator_service.rb │ │ ├── utils/ │ │ │ └── uffizzi_core/ │ │ │ └── converters.rb │ │ └── validators/ │ │ └── uffizzi_core/ │ │ ├── email_validator.rb │ │ ├── environment_variable_list_validator.rb │ │ └── image_command_args_validator.rb │ ├── bin/ │ │ └── rails │ ├── config/ │ │ ├── initializers/ │ │ │ ├── rswag_api.rb │ │ │ ├── rswag_ui.rb │ │ │ └── swagger_yard.rb │ │ ├── locales/ │ │ │ ├── en.activerecord.yml │ │ │ └── en.yml │ │ └── routes.rb │ ├── db/ │ │ ├── migrate/ │ │ │ ├── 20220218121438_create_uffizzi_core_tables.rb │ │ │ ├── 20220309110201_remove_secrets_from_projects.rb │ │ │ ├── 20220310110150_create_project_secrets.rb │ │ │ ├── 20220325113342_add_name_to_uffizzi_containers.rb │ │ │ ├── 20220329123323_rename_project_secrets_to_secrets.rb │ │ │ ├── 20220329124542_add_resource_to_secrets.rb │ │ │ ├── 20220329143241_remove_project_ref_from_secrets.rb │ │ │ ├── 20220419074956_add_health_check_to_containers.rb │ │ │ ├── 20220422151523_add_volumes_to_uffizzi_core_containers.rb │ │ │ ├── 20220525113412_rename_name_to_uffizzi_containers.rb │ │ │ ├── 20220704135629_add_disabled_at_to_deployments.rb │ │ │ ├── 20220805164628_add_metadata_to_deployment.rb │ │ │ ├── 20220901110752_create_host_volume_files.rb │ │ │ ├── 20220901165313_create_container_host_volume_files.rb │ │ │ ├── 20220927113647_add_additional_subdomains_to_containers.rb │ │ │ ├── 20230111000000_add_state_to_memberships.rb │ │ │ ├── 20230306142513_add_last_deploy_at_to_deployments.rb │ │ │ ├── 20230406154451_add_full_image_name_to_container.rb │ │ │ ├── 20230531135739_create_deployment_events.rb │ │ │ ├── 20230613101901_create_clusters.rb │ │ │ ├── 20230711101901_add_host_to_clusters.rb │ │ │ ├── 20230810140316_add_source_to_uffizzi_core_clusters.rb │ │ │ ├── 20230824150022_update_name_constraint_to_projects.rb │ │ │ ├── 20231009162719_create_uffizzi_core_kubernetes_distributions.rb │ │ │ ├── 20231009182412_add_kubernetes_distribution_id_to_uffizzi_core_clusters.rb │ │ │ ├── 20240301200235_add_node_selector_to_cluster.rb │ │ │ └── 20240314170113_delete_node_selector_from_cluster.rb │ │ └── seeds.rb │ ├── lib/ │ │ ├── tasks/ │ │ │ └── uffizzi_core_tasks.rake │ │ ├── uffizzi_core/ │ │ │ ├── engine.rb │ │ │ └── version.rb │ │ └── uffizzi_core.rb │ ├── swagger/ │ │ └── v1/ │ │ └── swagger.json │ ├── test/ │ │ ├── controllers/ │ │ │ └── uffizzi_core/ │ │ │ └── api/ │ │ │ └── cli/ │ │ │ └── v1/ │ │ │ ├── accounts/ │ │ │ │ ├── clusters_controller_test.rb │ │ │ │ ├── credentials_controller_test.rb │ │ │ │ └── projects_controller_test.rb │ │ │ ├── accounts_controller_test.rb │ │ │ ├── projects/ │ │ │ │ ├── clusters/ │ │ │ │ │ └── ingresses_controller_test.rb │ │ │ │ ├── clusters_controller_test.rb │ │ │ │ ├── compose_files_controller_test.rb │ │ │ │ ├── deployments/ │ │ │ │ │ ├── activity_items_controller_test.rb │ │ │ │ │ ├── containers/ │ │ │ │ │ │ └── logs_controller_test.rb │ │ │ │ │ ├── containers_controller_test.rb │ │ │ │ │ └── events_controller_test.rb │ │ │ │ ├── deployments_controller/ │ │ │ │ │ ├── create_test.rb │ │ │ │ │ ├── deploy_containers_test.rb │ │ │ │ │ ├── destroy_test.rb │ │ │ │ │ └── update_test.rb │ │ │ │ └── secrets_controller_test.rb │ │ │ ├── projects_controller_test.rb │ │ │ └── sessions_controller_test.rb │ │ ├── dummy/ │ │ │ ├── Rakefile │ │ │ ├── app/ │ │ │ │ ├── assets/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── manifest.js │ │ │ │ │ ├── images/ │ │ │ │ │ │ └── .keep │ │ │ │ │ └── stylesheets/ │ │ │ │ │ └── application.css │ │ │ │ ├── channels/ │ │ │ │ │ └── application_cable/ │ │ │ │ │ ├── channel.rb │ │ │ │ │ └── connection.rb │ │ │ │ ├── controllers/ │ │ │ │ │ ├── application_controller.rb │ │ │ │ │ └── concerns/ │ │ │ │ │ └── .keep │ │ │ │ ├── helpers/ │ │ │ │ │ └── application_helper.rb │ │ │ │ ├── javascript/ │ │ │ │ │ └── packs/ │ │ │ │ │ └── application.js │ │ │ │ ├── jobs/ │ │ │ │ │ └── application_job.rb │ │ │ │ ├── mailers/ │ │ │ │ │ └── application_mailer.rb │ │ │ │ ├── models/ │ │ │ │ │ ├── application_record.rb │ │ │ │ │ └── concerns/ │ │ │ │ │ └── .keep │ │ │ │ └── views/ │ │ │ │ └── layouts/ │ │ │ │ ├── application.html.erb │ │ │ │ ├── mailer.html.erb │ │ │ │ └── mailer.text.erb │ │ │ ├── bin/ │ │ │ │ ├── rails │ │ │ │ ├── rake │ │ │ │ ├── setup │ │ │ │ └── spring │ │ │ ├── config/ │ │ │ │ ├── application.rb │ │ │ │ ├── boot.rb │ │ │ │ ├── cable.yml │ │ │ │ ├── database.yml │ │ │ │ ├── environment.rb │ │ │ │ ├── environments/ │ │ │ │ │ ├── development.rb │ │ │ │ │ ├── production.rb │ │ │ │ │ └── test.rb │ │ │ │ ├── initializers/ │ │ │ │ │ ├── application_controller_renderer.rb │ │ │ │ │ ├── assets.rb │ │ │ │ │ ├── backtrace_silencers.rb │ │ │ │ │ ├── config.rb │ │ │ │ │ ├── content_security_policy.rb │ │ │ │ │ ├── cookies_serializer.rb │ │ │ │ │ ├── filter_parameter_logging.rb │ │ │ │ │ ├── inflections.rb │ │ │ │ │ ├── mime_types.rb │ │ │ │ │ ├── octokit.rb │ │ │ │ │ ├── permissions_policy.rb │ │ │ │ │ └── wrap_parameters.rb │ │ │ │ ├── locales/ │ │ │ │ │ └── en.yml │ │ │ │ ├── puma.rb │ │ │ │ ├── routes.rb │ │ │ │ ├── settings.yml │ │ │ │ ├── spring.rb │ │ │ │ └── storage.yml │ │ │ ├── config.ru │ │ │ ├── db/ │ │ │ │ └── schema.rb │ │ │ ├── lib/ │ │ │ │ └── assets/ │ │ │ │ └── .keep │ │ │ ├── log/ │ │ │ │ └── .keep │ │ │ └── public/ │ │ │ ├── 404.html │ │ │ ├── 422.html │ │ │ └── 500.html │ │ ├── factories/ │ │ │ ├── account.rb │ │ │ ├── activity_item.rb │ │ │ ├── cluster.rb │ │ │ ├── comments.rb │ │ │ ├── compose_files.rb │ │ │ ├── config_files.rb │ │ │ ├── container_config_files.rb │ │ │ ├── container_host_volume_files.rb │ │ │ ├── containers.rb │ │ │ ├── credentials.rb │ │ │ ├── deployment.rb │ │ │ ├── host_volume_files.rb │ │ │ ├── kubernetes_distribution.rb │ │ │ ├── membership.rb │ │ │ ├── payments.rb │ │ │ ├── project.rb │ │ │ ├── ratings.rb │ │ │ ├── repos.rb │ │ │ ├── secrets.rb │ │ │ ├── sequences.rb │ │ │ ├── templates.rb │ │ │ ├── user.rb │ │ │ └── user_project.rb │ │ ├── fixtures/ │ │ │ └── files/ │ │ │ ├── cluster/ │ │ │ │ └── manifest.yml │ │ │ ├── compose_dependencies/ │ │ │ │ └── configs/ │ │ │ │ └── vote_conf.json │ │ │ ├── compose_files/ │ │ │ │ ├── azure_services/ │ │ │ │ │ └── nginx.yml │ │ │ │ ├── boolean_option.yml │ │ │ │ ├── compose_empty.yml │ │ │ │ ├── compose_memory.yml │ │ │ │ ├── compose_vote_app_github.yml │ │ │ │ ├── compose_with_continuous_preview.yml │ │ │ │ ├── compose_with_only_line.yml │ │ │ │ ├── compose_with_syntax_error.yml │ │ │ │ ├── compose_with_volumes.yml │ │ │ │ ├── compose_without_image.yml │ │ │ │ ├── compose_without_services.yml │ │ │ │ ├── config_file_dependencies.yml │ │ │ │ ├── dockerhub_services/ │ │ │ │ │ ├── account_custom_image.yml │ │ │ │ │ ├── nginx.yml │ │ │ │ │ ├── nginx_auto_deploy_off.yml │ │ │ │ │ ├── nginx_config_files.yml │ │ │ │ │ ├── nginx_config_invalid_ingress_service.yml │ │ │ │ │ ├── nginx_configs_long_syntax.yml │ │ │ │ │ ├── nginx_configs_long_syntax_without_target.yml │ │ │ │ │ ├── nginx_configs_short_syntax.yml │ │ │ │ │ ├── nginx_configs_short_syntax_invalid_path.yml │ │ │ │ │ ├── nginx_configs_short_syntax_unknown_config.yml │ │ │ │ │ ├── nginx_cp_delete_after_integer.yml │ │ │ │ │ ├── nginx_cp_invalid_delete_after_hours.yml │ │ │ │ │ ├── nginx_cp_invalid_delete_after_max.yml │ │ │ │ │ ├── nginx_cp_invalid_delete_after_min.yml │ │ │ │ │ ├── nginx_cp_invalid_delete_after_postfix.yml │ │ │ │ │ ├── nginx_env_file.yml │ │ │ │ │ ├── nginx_env_file_duplicates.yml │ │ │ │ │ ├── nginx_env_file_empty.yml │ │ │ │ │ ├── nginx_env_file_empty_in_array.yml │ │ │ │ │ ├── nginx_envs.yml │ │ │ │ │ ├── nginx_ingress_port_non_integer.yml │ │ │ │ │ ├── nginx_invalid_deploy_auto.yml │ │ │ │ │ ├── nginx_invalid_memory.yml │ │ │ │ │ ├── nginx_invalid_memory_min_value.yml │ │ │ │ │ ├── nginx_invalid_memory_postfix.yml │ │ │ │ │ ├── nginx_invalid_memory_type.yml │ │ │ │ │ ├── nginx_invalid_port.yml │ │ │ │ │ ├── nginx_uffizzi_ingress.yml │ │ │ │ │ ├── nginx_with_continuous_preview_without_x-uffizzi.yml │ │ │ │ │ ├── nginx_with_ingress_without_x-uffizzi.yml │ │ │ │ │ ├── nginx_without_ingress.yml │ │ │ │ │ ├── nginx_without_ingress_port.yml │ │ │ │ │ ├── nginx_without_ingress_service.yml │ │ │ │ │ ├── nginx_without_tag.yml │ │ │ │ │ ├── postgres_secrets.yml │ │ │ │ │ ├── postgres_secrets_duplicates.yml │ │ │ │ │ ├── postgres_secrets_unknown.yml │ │ │ │ │ ├── postgres_secrets_without_external.yml │ │ │ │ │ ├── postgres_secrets_without_name.yml │ │ │ │ │ ├── volumes_anonymous.yml │ │ │ │ │ └── volumes_named.yml │ │ │ │ ├── github_services/ │ │ │ │ │ └── hello_world.yml │ │ │ │ ├── google_services/ │ │ │ │ │ ├── cloudsql.yml │ │ │ │ │ ├── cloudsql_entrypoint.yml │ │ │ │ │ └── nginx.yml │ │ │ │ ├── healthcheck/ │ │ │ │ │ ├── array_command_success.yml │ │ │ │ │ ├── disabled_healthcheck.yml │ │ │ │ │ ├── healthcheck_with_disable.yml │ │ │ │ │ ├── invalid_command.yml │ │ │ │ │ ├── invalid_interval.yml │ │ │ │ │ ├── invalid_options.yml │ │ │ │ │ ├── invalid_retries.yml │ │ │ │ │ └── string_command_success.yml │ │ │ │ └── invalid_service.yml │ │ │ ├── controller/ │ │ │ │ ├── cluster_asleep.json │ │ │ │ ├── cluster_awake.json │ │ │ │ ├── cluster_not_ready.json │ │ │ │ ├── cluster_ready.json │ │ │ │ ├── deployment_containers.json │ │ │ │ ├── deployment_containers_with_error.json │ │ │ │ ├── deployments.json │ │ │ │ ├── ingresses.json │ │ │ │ └── logs.json │ │ │ ├── dockerhub/ │ │ │ │ ├── digest.json │ │ │ │ ├── login_fail.json │ │ │ │ ├── repository.json │ │ │ │ └── webhooks/ │ │ │ │ └── push/ │ │ │ │ └── event_data.json │ │ │ ├── github/ │ │ │ │ └── compose_files/ │ │ │ │ └── hello_world_compose_github_container_registry.json │ │ │ ├── test-compose-full.yml │ │ │ ├── test-compose-success-jfrog.yml │ │ │ ├── test-compose-success-without-dependencies.yml │ │ │ ├── test-compose-success.yml │ │ │ ├── test-compose-without-images.yml │ │ │ ├── uffizzi-compose-amazon.yml │ │ │ ├── uffizzi-compose-azure.yml │ │ │ ├── uffizzi-compose-docker-registry-anonymous.yml │ │ │ ├── uffizzi-compose-dockerhub.yml │ │ │ ├── uffizzi-compose-ghcr.yml │ │ │ ├── uffizzi-compose-google.yml │ │ │ ├── uffizzi-compose-invalid-service-name.yml │ │ │ ├── uffizzi-compose-vote-app-docker-with-memory-request.yml │ │ │ ├── uffizzi-compose-vote-app-docker.yml │ │ │ ├── uffizzi-compose-vote-app-with-command-as-string.yml │ │ │ ├── uffizzi-compose-with-host-volumes.yml │ │ │ └── uffizzi-compose-with_alias.yml │ │ ├── services/ │ │ │ ├── activity_item_service_test.rb │ │ │ ├── compose_file_service_test.rb │ │ │ ├── deployment_service_test.rb │ │ │ ├── google_service_test.rb │ │ │ ├── image_parser_service_test.rb │ │ │ ├── manage_activity_items_service_test.rb │ │ │ └── user_generator_service_test.rb │ │ ├── support/ │ │ │ ├── azure_registry_stub_support.rb │ │ │ ├── controller_stub_support.rb │ │ │ ├── docker_hub_stub_support.rb │ │ │ ├── docker_registry_stub_support.rb │ │ │ ├── fixture_support.rb │ │ │ ├── github_container_registry_support.rb │ │ │ ├── google_registry_stub_support.rb │ │ │ └── stub_support.rb │ │ ├── test_helper.rb │ │ └── uffizzi_core_test.rb │ └── uffizzi_core.gemspec ├── db/ │ ├── migrate/ │ │ ├── 20220219114713_create_uffizzi_core_tables.uffizzi_core.rb │ │ ├── 20220317112742_remove_secrets_from_projects.uffizzi_core.rb │ │ ├── 20220317112743_create_project_secrets.uffizzi_core.rb │ │ ├── 20220325113623_add_name_to_uffizzi_containers.uffizzi_core.rb │ │ ├── 20220412133606_rename_project_secrets_to_secrets.uffizzi_core.rb │ │ ├── 20220412133607_add_resource_to_secrets.uffizzi_core.rb │ │ ├── 20220412133608_remove_project_ref_from_secrets.uffizzi_core.rb │ │ ├── 20220420103952_add_health_check_to_containers.uffizzi_core.rb │ │ ├── 20220527135654_rename_name_to_uffizzi_containers.uffizzi_core.rb │ │ ├── 20220617184754_add_volumes_to_uffizzi_core_containers.uffizzi_core.rb │ │ ├── 20220708093405_add_disabled_at_to_deployments.uffizzi_core.rb │ │ ├── 20220817140346_add_metadata_to_deployment.uffizzi_core.rb │ │ ├── 20220901110752_create_host_volume_files.rb │ │ ├── 20220901165313_create_container_host_volume_files.rb │ │ ├── 20220927113647_add_additional_subdomains_to_containers.rb │ │ ├── 20230111000000_add_state_to_memberships.rb │ │ ├── 20230306142805_add_last_deploy_at_to_deployments.uffizzi_core.rb │ │ ├── 20230406154547_add_full_image_name_to_container.uffizzi_core.rb │ │ ├── 20230531135739_create_deployment_events.rb │ │ ├── 20230613110517_create_clusters.uffizzi_core.rb │ │ ├── 20230711101901_add_host_to_clusters.rb │ │ ├── 20230824150022_update_name_constraint_to_projects.rb │ │ ├── 20231009103942_add_source_to_uffizzi_core_clusters.uffizzi_core.rb │ │ ├── 20231009163516_create_uffizzi_core_kubernetes_distributions.uffizzi_core.rb │ │ ├── 20231009201239_add_kubernetes_distribution_id_to_uffizzi_core_clusters.uffizzi_core.rb │ │ ├── 20240301200916_add_node_selector_to_cluster.uffizzi_core.rb │ │ └── 20240314170425_delete_node_selector_from_cluster.uffizzi_core.rb │ ├── schema.rb │ └── seeds.rb ├── docker-compose.yml ├── docs/ │ ├── continuous-previews.md │ └── quickstart-guide.md ├── lib/ │ ├── assets/ │ │ └── .keep │ └── tasks/ │ └── .keep ├── log/ │ └── .keep ├── public/ │ ├── 404.html │ ├── 422.html │ ├── 500.html │ └── robots.txt ├── storage/ │ └── .keep ├── test/ │ ├── application_system_test_case.rb │ ├── channels/ │ │ └── application_cable/ │ │ └── connection_test.rb │ ├── controllers/ │ │ └── .keep │ ├── fixtures/ │ │ ├── .keep │ │ └── files/ │ │ └── .keep │ ├── helpers/ │ │ └── .keep │ ├── integration/ │ │ └── .keep │ ├── mailers/ │ │ └── .keep │ ├── models/ │ │ └── .keep │ ├── system/ │ │ └── .keep │ └── test_helper.rb ├── tmp/ │ └── .keep ├── uffizzi-compose-example.yml └── vendor/ └── .keep ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: Bug report labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Configuration:** - Browser [e.g. chrome, safari] - Version [e.g. 22] - Open Source, CLI, Cloud, Docker Desktop Extension **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: community-new assignees: '' --- **Tell us about your request** A clear and concise description of what you want to happen or the change you would like to see **Which service(s) is this request for?** Let us know which product(s) you want this for - Open Source or Cloud solution or both? **Tell us about the problem you're trying to solve. What are you trying to do, and why is it hard?** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Are you currently working around the issue?** A clear and concise description of any alternative solutions or features you've considered or are using today. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/helm-release.yml ================================================ name: Release Helm Charts on: push: branches: - main jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 - name: Configure Git run: | git config user.name "$GITHUB_ACTOR" git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Run chart-releaser uses: helm/chart-releaser-action@v1.3.0 env: CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" ================================================ FILE: .github/workflows/main.yml ================================================ name: Test, Lint, Build, and Publish Image on: push: branches: - qa - develop - main pull_request: types: [opened,synchronize,reopened] concurrency: group: ${{ github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.7.5 bundler-cache: true - name: Run rubocop run: bundle exec rubocop test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Create env file uses: SpicyPizza/create-envfile@v1.3 - name: Run tests run: docker-compose run --rm core bash -c "bundle install && bundle exec rails db:create && bundle exec rails test" build-and-push-some-branches: runs-on: ubuntu-latest needs: - lint - test if: ${{ github.ref_name == 'main' || github.ref_name == 'qa' || github.ref_name == 'develop' || github.event_name == 'pull_request' }} steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Docker metadata id: meta uses: docker/metadata-action@v3 with: images: uffizzi/app tags: | type=raw,value=latest,enable=${{ github.ref_name == 'main' }} type=ref,event=branch,enable=${{ github.ref_name == 'qa' || github.ref_name == 'develop' }} type=ref,event=pr - name: Build and Push Image to Docker Hub uses: docker/build-push-action@v2 with: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - name: Update Docker Hub Description for Default Branch uses: peter-evans/dockerhub-description@v2.4.3 if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} repository: uffizzi/app notify: needs: - lint - test - build-and-push-some-branches if: ${{ always() }} runs-on: ubuntu-latest steps: - uses: technote-space/workflow-conclusion-action@v2 - uses: 8398a7/action-slack@v3 with: status: ${{ env.WORKFLOW_CONCLUSION }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} if: ${{ (github.ref_name == 'main' && env.WORKFLOW_CONCLUSION == 'success') || ((github.ref_name == 'main' || github.ref_name == 'qa' || github.ref_name == 'develop') && env.WORKFLOW_CONCLUSION == 'failure') }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Uffizzi Core Release on: push: tags: - core_v* jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Setup ruby uses: ruby/setup-ruby@v1 with: ruby-version: 2.5.0 - name: Create env file uses: SpicyPizza/create-envfile@v1.3 - name: Release Gem run: docker compose run --rm core bash -c "make release_gem" env: RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v2 with: context: core push: true tags: uffizzi/core:${{ github.ref_name }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files for more about ignoring files. # # If you find yourself ignoring temporary files generated by your text editor # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile '~/.gitignore_global' # Ignore bundler config. /.bundle # Ignore the default SQLite database. /db/*.sqlite3 /db/*.sqlite3-journal /db/*.sqlite3-* # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/ !/tmp/pids/.keep # Ignore uploaded files in development. /storage/* !/storage/.keep /public/assets .byebug_history # Ignore master key for decrypting credentials and more. /config/master.key /credentials/*.json .env # Ignore (intellij) idea config directory /.idea ================================================ FILE: .rubocop.yml ================================================ require: - rubocop-minitest AllCops: NewCops: disable SuggestExtensions: false Exclude: - core/test/dummy/**/* - db/schema.rb - vendor/**/* Naming/AccessorMethodName: Description: Check the naming of accessor methods for get_/set_. Enabled: false Naming/MethodParameterName: Enabled: false Style/Alias: Description: 'Use alias_method instead of alias.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' Enabled: false Style/ArrayJoin: Description: 'Use Array#join instead of Array#*.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#array-join' Enabled: false Style/AsciiComments: Description: 'Use only ascii symbols in comments.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-comments' Enabled: false Naming/AsciiIdentifiers: Description: 'Use only ascii symbols in identifiers.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#english-identifiers' Enabled: false Style/Attr: Description: 'Checks for uses of Module#attr.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr' Enabled: false Metrics/BlockNesting: Description: 'Avoid excessive block nesting' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count' Enabled: false Style/CaseEquality: Description: 'Avoid explicit use of the case equality operator(===).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' Enabled: false Style/CharacterLiteral: Description: 'Checks for uses of character literals.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-character-literals' Enabled: false Style/ClassAndModuleChildren: Description: 'Checks style of children classes and modules.' Enabled: false Metrics/ClassLength: Description: 'Avoid classes longer than 100 lines of code.' Enabled: false Metrics/ModuleLength: Description: 'Avoid modules longer than 100 lines of code.' Enabled: false Style/ClassVars: Description: 'Avoid the use of class variables.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-class-vars' Enabled: false Style/CollectionMethods: Enabled: true PreferredMethods: find: detect inject: reduce collect: map find_all: select Style/ColonMethodCall: Description: 'Do not use :: for method call.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#double-colons' Enabled: false Style/CommentAnnotation: Description: >- Checks formatting of special comments (TODO, FIXME, OPTIMIZE, HACK, REVIEW). StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#annotate-keywords' Enabled: false Style/PreferredHashMethods: Description: 'Checks use of `has_key?` and `has_value?` Hash methods.' StyleGuide: '#hash-key' Enabled: false Style/Documentation: Description: 'Document classes and non-namespace modules.' Enabled: false Style/DoubleNegation: Description: 'Checks for uses of double negation (!!).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-bang-bang' Enabled: false Style/EachWithObject: Description: 'Prefer `each_with_object` over `inject` or `reduce`.' Enabled: false Style/EmptyLiteral: Description: 'Prefer literals to Array.new/Hash.new/String.new.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' Enabled: false Style/Encoding: Enabled: false Style/EvenOdd: Description: 'Favor the use of Fixnum#even? && Fixnum#odd?' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' Enabled: false Naming/FileName: Description: 'Use snake_case for source file names.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' Enabled: false Style/FrozenStringLiteralComment: Description: >- Add the frozen_string_literal comment to the top of files to help transition from Ruby 2.3.0 to Ruby 3.0. Enabled: true Style/FormatString: Description: 'Enforce the use of Kernel#sprintf, Kernel#format or String#%.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#sprintf' Enabled: false Style/GlobalVars: Description: 'Do not introduce global variables.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#instance-vars' Reference: 'http://www.zenspider.com/Languages/Ruby/QuickRef.html' Enabled: false Style/GuardClause: Description: 'Check for conditionals that can be replaced with guard clauses' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' Enabled: false Style/IfUnlessModifier: Description: >- Favor modifier if/unless usage when you have a single-line body. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#if-as-a-modifier' Enabled: false Style/IfWithSemicolon: Description: 'Do not use if x; .... Use the ternary operator instead.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-semicolon-ifs' Enabled: false Style/InlineComment: Description: 'Avoid inline comments.' Enabled: false Style/Lambda: Description: 'Use the new lambda literal syntax for single-line blocks.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#lambda-multi-line' Enabled: false Style/LambdaCall: Description: 'Use lambda.call(...) instead of lambda.(...).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc-call' Enabled: false Style/LineEndConcatenation: Description: >- Use \ instead of + or << to concatenate two string literals at line end. Enabled: false Metrics/AbcSize: Description: >- A calculated magnitude based on number of assignments, branches, and conditions. Enabled: false Metrics/BlockLength: CountComments: true Max: 50 IgnoredMethods: [] Exclude: - test/**/* - app/repositories/**/* - config/environments/**/* - core/test/**/* - core/app/repositories/**/* - core/config/routes.rb Metrics/CyclomaticComplexity: Description: >- A complexity metric that is strongly correlated to the number of test cases needed to validate a method. Enabled: false Metrics/MethodLength: Description: 'Avoid methods longer than 10 lines of code.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods' Enabled: false Style/MixinUsage: Enabled: true Exclude: - test/test_helper.rb Style/ModuleFunction: Description: 'Checks for usage of `extend self` in modules.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#module-function' Enabled: false Style/MultilineBlockChain: Description: 'Avoid multi-line chains of blocks.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' Enabled: false Style/NegatedIf: Description: >- Favor unless over if for negative conditions (or control flow or). StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#unless-for-negatives' Enabled: false Style/NegatedWhile: Description: 'Favor until over while for negative conditions.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#until-for-negatives' Enabled: false Style/Next: Description: 'Use `next` to skip iteration instead of a condition at the end.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-conditionals' Enabled: false Style/NilComparison: Description: 'Prefer x.nil? to x == nil.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods' Enabled: false Style/Not: Description: 'Use ! instead of not.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not' Enabled: false Style/NumericLiterals: Description: >- Add underscores to large numeric literals to improve their readability. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscores-in-numerics' Enabled: false Style/OneLineConditional: Description: >- Favor the ternary operator(?:) over if/then/else/end constructs. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator' Enabled: false Naming/BinaryOperatorParameterName: Description: 'When defining binary operators, name the argument other.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg' Enabled: false Metrics/ParameterLists: Description: 'Avoid parameter lists longer than three or four parameters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params' Enabled: false Style/PercentLiteralDelimiters: Description: 'Use `%`-literal delimiters consistently' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-literal-braces' Enabled: false Style/PerlBackrefs: Description: 'Avoid Perl-style regex back references.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' Enabled: false Naming/PredicateName: Description: 'Check the names of predicate methods.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' ForbiddenPrefixes: - is_ Exclude: - spec/**/* Style/Proc: Description: 'Use proc instead of Proc.new.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#proc' Enabled: false Style/RaiseArgs: Description: 'Checks the arguments passed to raise/fail.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#exception-class-messages' Enabled: false Style/RegexpLiteral: Description: 'Use / or %r around regular expressions.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-r' Enabled: false Style/Sample: Description: >- Use `sample` instead of `shuffle.first`, `shuffle.last`, and `shuffle[Fixnum]`. Reference: 'https://github.com/JuanitoFatas/fast-ruby#arrayshufflefirst-vs-arraysample-code' Enabled: false Style/SelfAssignment: Description: >- Checks for places where self-assignment shorthand should have been used. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#self-assignment' Enabled: false Style/SingleLineBlockParams: Description: 'Enforces the names of some block params.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#reduce-blocks' Enabled: false Style/SingleLineMethods: Description: 'Avoid single-line methods.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-single-line-methods' Enabled: false Style/SignalException: Description: 'Checks for proper usage of fail and raise.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#fail-method' Enabled: false Style/SpecialGlobalVars: Description: 'Avoid Perl-style global variables.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-cryptic-perlisms' Enabled: false Style/StringLiterals: Description: 'Checks if uses of quotes match the configured preference.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-string-literals' EnforcedStyle: single_quotes Enabled: true Style/StructInheritance: Enabled: true Style/SymbolArray: EnforcedStyle: brackets Style/SymbolProc: Enabled: true Style/TrailingCommaInArguments: Description: 'Checks for trailing comma in argument lists.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' EnforcedStyleForMultiline: comma SupportedStylesForMultiline: - comma - consistent_comma - no_comma Enabled: true Style/TrailingCommaInArrayLiteral: Description: 'Checks for trailing comma in array literals.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' EnforcedStyleForMultiline: comma SupportedStylesForMultiline: - comma - consistent_comma - no_comma Enabled: true Style/TrailingCommaInHashLiteral: Description: 'Checks for trailing comma in hash literals.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' EnforcedStyleForMultiline: comma SupportedStylesForMultiline: - comma - consistent_comma - no_comma Enabled: true Style/TrivialAccessors: Description: 'Prefer attr_* methods to trivial readers/writers.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family' Enabled: false Style/VariableInterpolation: Description: >- Don't interpolate global, instance and class variables directly in strings. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#curlies-interpolate' Enabled: false Style/WhenThen: Description: 'Use when x then ... for one-line cases.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#one-line-cases' Enabled: false Style/WhileUntilModifier: Description: >- Favor modifier while/until usage when you have a single-line body. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#while-as-a-modifier' Enabled: false Style/WordArray: Description: 'Use %w or %W for arrays of words.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-w' EnforcedStyle: brackets Style/MethodCallWithArgsParentheses: Description: 'Use parentheses for method calls with arguments.' Enabled: true IgnoredMethods: - require - require_relative - raise - head - render - respond_with - puts Exclude: - Gemfile - test/test_helper.rb - db/migrate/**/* - core/uffizzi_core.gemspec - core/db/migrate/**/* Layout/ParameterAlignment: Description: 'Here we check if the parameters on a multi-line method call or definition are aligned.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-double-indent' Enabled: false Layout/ConditionPosition: Description: >- Checks for condition placed in a confusing position relative to the keyword. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#same-line-condition' Enabled: false Layout/DotPosition: Description: 'Checks the position of the dot in multi-line method calls.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#consistent-multi-line-chains' EnforcedStyle: leading Layout/ExtraSpacing: Description: 'Do not use unnecessary spacing.' Enabled: true Layout/MultilineOperationIndentation: Description: >- Checks indentation of binary operations that span more than one line. Enabled: true EnforcedStyle: indented Layout/MultilineMethodCallIndentation: Description: >- Checks indentation of method calls with the dot operator that span more than one line. Enabled: true EnforcedStyle: indented Layout/InitialIndentation: Description: >- Checks the indentation of the first non-blank non-comment line in a file. Enabled: false Layout/EndAlignment: Description: >- Checks whether the end keywords are aligned properly. EnforcedStyleAlignWith: variable Layout/LineLength: Description: 'Limit lines to 140 characters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' Max: 140 Lint/AmbiguousOperator: Description: >- Checks for ambiguous operators in the first argument of a method invocation without parentheses. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-as-args' Enabled: false Lint/AmbiguousRegexpLiteral: Description: >- Checks for ambiguous regexp literals in the first argument of a method invocation without parenthesis. Enabled: false Lint/AssignmentInCondition: Description: "Don't use assignment in conditions." StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#safe-assignment-in-condition' Enabled: false Lint/CircularArgumentReference: Description: "Don't refer to the keyword argument in the default value." Enabled: false Lint/DeprecatedClassMethods: Description: 'Check for deprecated class method calls.' Enabled: false Lint/DuplicateHashKey: Description: 'Check for duplicate keys in hash literals.' Enabled: false Lint/EachWithObjectArgument: Description: 'Check for immutable argument given to each_with_object.' Enabled: false Lint/ElseLayout: Description: 'Check for odd code arrangement in an else block.' Enabled: false Lint/FlipFlop: Description: 'Checks for flip flops' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-flip-flops' Enabled: false Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: false Lint/SuppressedException: Description: "Don't suppress exception." StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' Enabled: false Lint/LiteralAsCondition: Description: 'Checks of literals used in conditions.' Enabled: false Lint/LiteralInInterpolation: Description: 'Checks for literals used in interpolation.' Enabled: false Lint/Loop: Description: >- Use Kernel#loop with break rather than begin/end/until or begin/end/while for post-loop tests. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#loop-with-break' Enabled: false Lint/NestedMethodDefinition: Description: 'Do not use nested method definitions.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-methods' Enabled: false Lint/NonLocalExitFromIterator: Description: 'Do not use return in iterator to cause non-local exit.' Enabled: false Lint/ParenthesesAsGroupedExpression: Description: >- Checks for method calls with a space before the opening parenthesis. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' Enabled: false Lint/RedundantCopDisableDirective: Description: >- Checks for rubocop:disable comments that can be removed. Note: this cop is not disabled when disabling all cops. It must be explicitly disabled. Enabled: false Lint/RequireParentheses: Description: >- Use parentheses in the method call to avoid confusion about precedence. Enabled: false Lint/UnderscorePrefixedVariableName: Description: 'Do not use prefix `_` for a variable that is used.' Enabled: false Lint/Void: Description: 'Possible use of operator/literal/variable in void context.' Enabled: false Gemspec/DateAssignment: Enabled: true Gemspec/RequireMFA: Enabled: false Layout/LineEndStringConcatenationIndentation: Enabled: true Layout/SpaceBeforeBrackets: Enabled: true Lint/AmbiguousAssignment: Enabled: true Lint/AmbiguousOperatorPrecedence: Enabled: true Lint/AmbiguousRange: Enabled: true Lint/DeprecatedConstants: Enabled: true Lint/DuplicateBranch: Enabled: true Lint/DuplicateRegexpCharacterClassElement: Enabled: true Lint/EmptyBlock: Enabled: false Lint/EmptyClass: Enabled: true Lint/EmptyInPattern: Enabled: true Lint/IncompatibleIoSelectWithFiberScheduler: Enabled: true Lint/LambdaWithoutLiteralBlock: Enabled: true Lint/NoReturnInBeginEndBlocks: Enabled: true Lint/NumberedParameterAssignment: Enabled: true Lint/OrAssignmentToConstant: Enabled: true Lint/RedundantDirGlobSort: Enabled: true Lint/RequireRelativeSelfPath: Enabled: true Lint/SymbolConversion: Enabled: true Lint/ToEnumArguments: Enabled: true Lint/TripleQuotes: Enabled: true Lint/UnexpectedBlockArity: Enabled: true Lint/UnmodifiedReduceAccumulator: Enabled: true Lint/UselessRuby2Keywords: Enabled: true Naming/BlockForwarding: Enabled: true Security/IoMethods: Enabled: true Style/ArgumentsForwarding: Enabled: true Style/CollectionCompact: Enabled: true Style/DocumentDynamicEvalDefinition: Enabled: true Style/EndlessMethod: Enabled: true Style/FileRead: Enabled: true Style/FileWrite: Enabled: true Style/HashConversion: Enabled: true Style/HashExcept: Enabled: true Style/IfWithBooleanLiteralBranches: Enabled: true Style/InPatternThen: Enabled: true Style/MapToHash: Enabled: true Style/MultilineInPatternThen: Enabled: true Style/NegatedIfElseCondition: Enabled: true Style/NilLambda: Enabled: true Style/NumberedParameters: Enabled: true Style/NumberedParametersLimit: Enabled: true Style/OpenStructUse: Enabled: false Style/OptionalBooleanParameter: Enabled: false Style/QuotedSymbols: Enabled: true Style/RedundantArgument: Enabled: true Style/RedundantSelfAssignmentBranch: Enabled: true Style/SelectByRegexp: Enabled: true Style/StringChars: Enabled: true Style/SwapValues: Enabled: true Minitest/AssertInDelta: Enabled: true Minitest/AssertionInLifecycleHook: Enabled: true Minitest/AssertKindOf: Enabled: true Minitest/AssertOutput: Enabled: true Minitest/AssertPathExists: Enabled: true Minitest/AssertSilent: Enabled: true Minitest/AssertWithExpectedArgument: Enabled: true Minitest/LiteralAsActualArgument: Enabled: true Minitest/MultipleAssertions: Enabled: false Minitest/RefuteInDelta: Enabled: true Minitest/RefuteKindOf: Enabled: true Minitest/RefutePathExists: Enabled: true Minitest/TestMethodName: Enabled: true Minitest/UnreachableAssertion: Enabled: true Minitest/UnspecifiedException: Enabled: true ================================================ FILE: .ruby-version ================================================ ruby-2.5.3 ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct This Code of Conduct is based on the [Mission Protocol Code of Conduct v1.1](https://missionprotocol.org/codeofconduct/), Creative Commons 4.0 License (CC BY 4.0), 2020. ## Mission Our mission is to support the creators of the next generation of great products and services by facilitating high quality and rapid software delivery through lightweight, event-driven test environments. ## Objective Our objective is to focus on the mission we set out to accomplish, which we believe will produce an important social good in the world. Our standards are directed towards achieving this mission. We will conduct ourselves with professional integrity and set aside divisive discourse that doesn’t help us achieve our mission. ## Standards - Leadership principle: Project leaders are responsible and authorized to make judgments and take appropriate actions to define the mission and keep the project in line with this mission. - **Mission focus**: We will focus our efforts on accomplishing the project’s mission, so that it is fulfilled to its highest potential. - **Creative conflict**: We acknowledge that discussions and disagreements are a natural part of problem-solving and should happen in a constructive way. - **The whole is greater than the parts**: We are a project with diverse opinions and fields of engagement, but we are here to focus on what unites us, instead of what divides us. - **Principle of charity**: We assume positive intentions of members, contributors, and policies. ## Prohibited Behavior - Harassment, i.e., excessive, hostile, or bad-faith communication directed at another member or contributor - Attacks based on personal characteristics - Threats of violence - Threatening to publish or publishing people’s personal information without their consent - Repeatedly attempting to involve the project in issues outside of its mission - Posting illegal content - Abuse of the system for reporting code of conduct violations - Promoting any behavior listed above ## Responsibilities In line with the leadership principle, project leaders have the responsibility to clarify and interpret the mission, as well as the intentions and standards in this code of conduct, in order to maintain mission focus. When individuals engage in prohibited behavior, project leaders are expected to take expedient, fair, and appropriate action to address the violation(s). The standards and prohibitions in this document also apply to project leaders. ## Conflict Resolution ### Reporting We highly encourage contributors to resolve conflicts by directly reaching out to the other party or parties involved in the dispute. When this is insufficient, members can report the issue to project leaders via email at support@uffizzi.com within 31 days of the inciting event and request a formal resolution. Project leaders will make reasonable efforts to adjudicate incidents shortly after they are brought to their attention. ### Enforcement Project leaders shall take all reasonable actions to ensure the successful execution of the mission statement and the maximum effectiveness of the project. All material in official project spaces is subject to the code of conduct, and as such, can be deleted, modified, or rejected by project leaders if it is found to be in violation of the code of conduct. In repeated or severe cases, project leaders may exclude individuals from further contribution to the project on a temporary or permanent basis. ## Scope This code of conduct applies to official project spaces, which include but are not limited to: social media, conferences, code repositories, and discussion boards. This code of conduct also applies to members and contributors who represent the project in non-project spaces, such as when contributors give a talk on behalf of the project at a conference. In official spaces, members and contributors should operate with decorum that reflects positively on the project, its objectives, and its community. ## Licensing The Mission Protocol Code of Conduct was created under the CC BY 4.0 License in 2020. Frequently asked questions about Mission Protocol are available here. ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to `uffizzi_app` Thanks for your interest! We are actively working to release `uffizzi_app` and define how you can contribute - please follow for updates! Uffizzi welcomes contributions from everyone! ## Communication: If you need any help contributing, several maintainers are on the Uffizzi Users Slack group https://join.slack.com/t/uffizzi/shared_invite/zt-ffr4o3x0-J~0yVT6qgFV~wmGm19Ux9A. ## Releases We're using Semantic Versioning 2.0.0 to name our release tags: https://semver.org/ Be sure to update the `appVersion` within `charts/uffizzi-app/Chart.yaml` whenever you create a new release! And also update the tag for `image` within `charts/uffizzi-app/values.yaml`. ## Helm Chart Releases When you change the Helm chart, even if it's just bumping the `appVersion` and `image` tag, also increment the `version` within `charts/uffizzi-app/Chart.yaml`. This chart version does not need to match the app version, and it's probably better if it does not. When the new `Chart.yaml` makes it into the default branch, then the `chart-releaser` GitHub Action will create a new tag with a `uffizzi-app-` prefix. It will also update our Helm repo within the `gh-pages` branch. Let the automation handle this. # Procedures for outside collaborators: - Fork the project on Github. - Make any changes you want to `uffizzi_app`, commit them, and push them to your fork. - Create a Pull Request against `UffizziCloud/uffizzi_app:main`, and a maintainer will come by and review your inputs. They may ask for some changes or more information, and hopefully your contribution will be merged to the `main` branch! # Procedures for Uffizzi team members: 1. Clone the repository and checkout to `develop` branch. 2. Pull repository to ensure you have the latest changes. ```bash git pull --rebase develop ``` 3. Start new branch from `develop` ```bash git checkout -b feature/ISSUE_NUMBER_short_issue_description (e.g. feature/53_add_domain_settings) ``` 4. Make changes you need for the feature, commit them to the repo ```bash git add . git commit -m '[#ISSUE_NUMBER] short commit description' (e.g. git commit -m '[#53] added domain settings') git push origin BRANCH_NAME ``` 5. You already can create PR with develop branch as a target. Once the feature is ready let us know in the channel - we will review 6. Merge your feature to `qa` branch and push. Ensure your pipeline is successful. ```bash git checkout qa git pull --rebase qa git merge --no-ff BRANCH_NAME git push origin qa ``` # Running linter ```bash docker-compose run --rm web bundle exec rubocop -A ``` # Running test ```bash docker-compose run --rm core bash bin/rails test ``` # Migrations In order to add a new migration do the following steps: 1. Add a new migration inside the core 2. Run the command inside the `web` container ```bash rake uffizzi_core:install:migrations ``` This command copies the new migration to the `db/migrate` folder ================================================ FILE: Dockerfile ================================================ FROM ruby:2.7.5-slim ARG RAILS_ROOT=/app RUN mkdir -p $RAILS_ROOT WORKDIR $RAILS_ROOT RUN apt-get update \ && apt-get install -qq -y --no-install-recommends \ vim-tiny \ python2-dev \ libpq-dev \ build-essential \ curl \ less \ tzdata \ git \ postgresql-client \ bash \ screen \ shared-mime-info \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* RUN gem install bundler:2.3.9 COPY Gemfile Gemfile.lock $RAILS_ROOT/ COPY core/lib/uffizzi_core/version.rb $RAILS_ROOT/core/lib/uffizzi_core/version.rb COPY core/uffizzi_core.gemspec $RAILS_ROOT/core/uffizzi_core.gemspec COPY core/Gemfile* $RAILS_ROOT/core/ RUN bundle install --jobs 5 COPY . $RAILS_ROOT ENV PATH $RAILS_ROOT/bin:$PATH EXPOSE 7000 CMD /bin/bash -c "bundle exec rails db:create db:migrate && bundle exec puma -C config/puma.rb" ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.5' gem 'bcrypt', '~> 3.1.7' gem 'bootsnap', '>= 1.4.2', require: false gem 'config' gem 'health_check' gem 'pg', '>= 0.18', '< 2.0' gem 'puma', '~> 4.1' gem 'rack-cors' gem 'rails', '~> 6.1.0' gem 'sidekiq' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'uffizzi_core', path: './core' group :development, :test do gem 'awesome_print' gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem 'rubocop' gem 'rubocop-minitest' gem 'rubocop-rake' end ================================================ FILE: INSTALL.md ================================================ # Installation overview See the [Helm chart guide](https://github.com/UffizziCloud/uffizzi_app/blob/main/charts/uffizzi-app/README.md) for installing Uffizzi on your own Kubernetes cluster. Once Uffizzi is installed, you can use the [Uffizzi CLI](https://github.com/UffizziCloud/uffizzi_cli) to create and manage previews. Additionally, you can continuously deploy previews of your branches using [the GitHub Action](https://github.com/UffizziCloud/preview-action). ## Uffizzi Architecture Uffizzi consists of the following components: * Uffizzi App (this repository) - The primary REST API for creating and managing Previews * [Uffizzi Controller](https://github.com/UffizziCloud/uffizzi_controller) - A smart proxy service that handles requests from Uffizzi App to the Kubernetes API * [Uffizzi CLI](https://github.com/UffizziCloud/uffizzi_cli) - A command-line interface for Uffizzi App Uffizzi App requires the following external dependencies: * Kubernetes (k8s) cluster * PostgreSQL database * Redis cache ## Controller Design This `uffizzi_app` acts as a REST API for [`uffizzi_cli`](https://github.com/UffizziCloud/uffizzi_app) interface. It requires [`uffizzi_controller`](https://github.com/UffizziCloud/uffizzi_controller) as a supporting service. ## Uffizzi App Environment Variables - `RAILS_SECRET_KEY_BASE` - secret_key_base of Rails::Application - `DATABASE_HOST` - the database hostname (default: 127.0.0.1) - `DATABASE_USER` - the database username (default: postgres) - `DATABASE_PASSWORD` - the database password - `DATABASE_PORT` - the database port (default: 5432) - `BUNDLE_PATH` - the location of gems for `bundle install` command (not required) - `GEM_PATH` - the location where gems can be found (not required) - `GEM_HOME` - where gems will be installed (not required) - `RAILS_WORKERS_COUNT` - the number of `puma` workers (default: 18) - `RAILS_THREADS_COUNT` - the number of `puma` threads (default: 5) - `RAILS_PORT` - the `puma` port (default: 7000) - `RAILS_ENV` - the rails environment (default: development) - `SIDEKIQ_CONCURRENCY` - sidekiq concurrency (default: 5) - `ALLOWED_HOSTS` - allowed hosts for rails app used for Rack::Cors (default: []) - `APP_URL` - URL of the application - `REDIS_URL` - URL of a Redis server - `CONTROLLER_URL` - URL of the controller application (default: http://controller:8080) - `CONTROLLER_LOGIN` - the login of the controller application (default: '') - `CONTROLLER_PASSWORD` - the password of the controller application (default: '') # Test Uffizzi App Locally If you want to run Uffizzi on your workstation instead of using [the Helm chart](charts/uffizzi-app/README.md), then you can run it using Docker Compose. ## Prepare ```bash docker-compose run --rm web bash -c "bundle install && bundle exec rails db:setup" docker-compose up ``` ## Create a new user Run the following command and follow instructions: ```bash docker-compose run --rm web bash -c "rake uffizzi_core:create_user" ``` or run the command with environment variables: - `UFFIZZI_USER_EMAIL` - user's email - `UFFIZZI_USER_PASSWORD` - user's password - `UFFIZZI_PROJECT_NAME` - user's project name ```bash docker-compose run --rm -e UFFIZZI_USER_EMAIL=user@uffizzi.com -e UFFIZZI_USER_PASSWORD=password -e UFFIZZI_PROJECT_NAME=project web bash -c "rake uffizzi_core:create_user" ``` ## Connect from uffizzi-cli to the app ```bash docker-compose run --rm gem bash bundle exec uffizzi login --hostname http://web:7000 -u admin@uffizzi.com ``` ## API Documentation * [Development](http://lvh.me:7000/api-docs/index.html) Rebuild documentation locally: ```bash docker-compose run --rm core bash bundle exec rake core:generate_docs ``` # Health checks The default health check uri is `health_check`. To use a custom uri please add the `HEALTH_CHECK_URI` environment variable to the docker-compose.yml ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [2021] [Uffizzi] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ .PHONY: release release_patch release_minor release_major test NEXT_PATCH=$(shell docker compose run --rm core bash -c "bundle exec bump show-next patch") NEXT_MINOR=$(shell docker compose run --rm core bash -c "bundle exec bump show-next minor") NEXT_MAJOR=$(shell docker compose run --rm core bash -c "bundle exec bump show-next major") release_patch: export VERSION=${NEXT_PATCH} release_patch: make release release_minor: export VERSION=${NEXT_MINOR} release_minor: make release release_major: export VERSION=${NEXT_MAJOR} release_major: make release release: git checkout develop @echo 'Set a new version' docker compose run --rm core bash -c "bundle exec bump set ${VERSION}" docker compose run --rm core bash -c "bundle update uffizzi_core --conservative " docker compose run --rm web bash -c "bundle update uffizzi_core --conservative " git commit -am "Change version to ${VERSION}" @echo 'Update remote origin' git push origin develop git checkout main git pull origin --rebase main git merge --no-ff --no-edit develop git push origin main @echo 'Create a new tag' git tag core_v${VERSION} git push origin core_v${VERSION} test: docker compose run --rm core bash -c "bundle exec rails test" lint: docker compose run --rm web bash -c "bundle exec rubocop -A" ================================================ FILE: README.md ================================================


Uffizzi
Environments-as-a-Service

WebsiteSlackBlogTwitterDocumentation

Uffizzi helps teams build [internal developer platforms (IDPs)](/core-concepts/internal-developer-platform) in minutes, not months, by providing out-of-the-box [Kubernetes multi-tenancy](https://www.uffizzi.com/kubernetes-multi-tenancy), [virtual clusters](/core-concepts/ephemeral-environments/virtual-clusters), cloud-based [dev environments](/core-concepts/ephemeral-environments/dev-clusters), customizable templating, and more. Uffizzi provides a foundation for building IDPs, so platform teams can build end-to-end workflows, giving every developer access to self-service, [ephemeral environments](/core-concepts/ephemeral-environments) for development, testing, PRs, staging and more. Use Uffizzi environments to preview pull requests before merging or integrate with your CI pipeline for automated, end-to-end testing.    

Trusted by top teams

BackstageNocoDBForemLazygitD2IQParseDashboardFonoster

AnswerWindmillFlagsmithMemosCraterLivebookOnlineGoBoxyHQ

  Teams like [Backstage](https://github.com/backstage/backstage/tree/master/.github/uffizzi), [NocoDB](https://github.com/nocodb/nocodb/tree/develop/.github/uffizzi), and [Forem](https://github.com/forem/forem/blob/main/.github/workflows/uffizzi-preview.yml) have adopted Uffizzi because it's lightweight, fast, scalable, and more cost effective than competing solutions. Did you know that Spotify's Backstage team achieves rapid releases at scale using nearly 400 ephemeral environments per month? [Learn how →](https://www.uffizzi.com/ephemeral-environments)
![github-banner](https://user-images.githubusercontent.com/7218230/191119628-4d39c65d-465f-4011-9370-d53d7b54d8cc.png) ## Quickstart (~2 minute) Go to the [Quickstart Guide](https://docs.uffizzi.com/quickstart) to get started creating ephemeral environments. ## How it works Spin up ephemeral environments on demand from the CLI, web dashboard, or from a CI pipeline. Each ephemeral environment is continually refreshed when you push new commits. Uffizzi also handles clean up, so your environments last only as long as you need them. Uffizzi's modular design works with GitHub, GitLab, BitBucket, and any CI provider. preview-url ## Give us a star ⭐️ If you're interested in Uffizzi, give us a star. It helps others discover the project. ## Use cases Uffizzi is designed to integrate with any CI platform as a step in your pipeline. You can use Uffizzi to rapidly create: - Cloud dev environments with hot reloading of deployed services - On-demand test environments for Kubernetes applications - Pull request environments - Debugging environments - Hotfix environments - Demo environments - Release environments - Staging environments ## What types of apps are supported by Uffizzi? Uffizzi supports application configurations in Kubernetes manifests, Helm, kustomize, or Docker Compose. See [Using Uffizzi](https://docs.uffizzi.com/usage) to learn about the ways you can use Uffizzi. ## Why Uffizzi? Uffizzi provides a foundation for building IDPs, so platform teams can build end-to-end workflows, giving every developer access to self-service, ephemeral environments for development, testing, PRs, staging and more. Uffizzi is also useful for helping busy open source project leaders approve pull requests faster. Testing a live preview provides a more holistic way to assess a new feature or bug fix, rather than simply reviewing code changes. Uffizzi also removes the added step of pulling down the branch to test it locally: Uffizzi seamlessly integrates with CI providers like GitHub Actions and posts comments directly to pull request issues, so there is no additional step for the maintainer or the contributor. Learn how Uffizzi is helping [Backstage accelerate their development velocity by 20%](https://www.uffizzi.com/ephemeral-environments). ## Set up ephemeral environments for your application (If you haven't completed the [quickstart guide](https://docs.uffizzi.com/quickstart), we recommend starting there to understand how Uffizzi works and how it's configured.) There are three options to get Uffizzi: 1. **[Uffizzi Cloud](https://docs.uffizzi.com/cloud) (SaaS)** - This is fastest and easiest way to get started. Uffizzi Cloud is our fully managed option, so you don't have to worry about managing any infrastructure. You can get two concurrent environments for free, or unlock unlimited ephemeral environments with Uffizzi Pro. See our [Pricing page](https://www.uffizzi.com/pricing) for details. 2. **[Uffizzi Enterprise](https://docs.uffizzi.com/enterprise)** - Uffizzi Enterprise provides the option to run workloads on your own infrastucture, along with more flexibility in customizing your ephemeral environments experience. 3. **[Uffizzi Open Source](https://docs.uffizzi.com/open-source)** - Alternatively, you can install the open source version of Uffizzi on your own cluster by following the [self-hosted installation guide](INSTALL.md). ## Documentation - [Main documentation](https://docs.uffizzi.com) - [Docker Compose for Uffizzi ](https://docs.uffizzi.com/compose) - [Quickstart guide](https://docs.uffizzi.com/quickstart) ## Community - [Slack channel](https://join.slack.com/t/uffizzi/shared_invite/zt-ffr4o3x0-J~0yVT6qgFV~wmGm19Ux9A) - Get support or discuss the project - [Subscribe to our newsletter](https://www.linkedin.com/build-relation/newsletter-follow?entityUrn=7011448505391042560) - Receive monthly updates about new features and special events - [Contributing to Uffizzi](CONTRIBUTING.md) - Start here if you want to contribute - [Code of Conduct](CODE_OF_CONDUCT.md) - Let's keep it professional ## FAQs
My team tests locally. Why do I need Ephemeral Environments?
  1. Ephemeral Environments more closely resemble production. Uffizzi deploys images built from your CI pipeline—similar to the ones deployed to a production environment. Uffizzi Ephemeral Environments also include a full network stack, including a domain and TLS certificate.
  2. Ephemeral Environments provide many benefits including standardizing development configurations, avoiding the bottleneck of a single test/staging environment, acting as a quality gate to help keep dirty code out of your main branch. Teams can develop and test new features or bug fixes in clean, isolated environments.
  3. Public preview URLs allow every stakeholder on a team to review features and bug fixes. This helps shorten the feedback loop between developer and reviewer/tester, resulting in faster releases.
How is Uffizzi different from Codespaces, Gitpod, etc.?

Codespaces, Gitpod, and similar tools focus soley on providing development environments hosted in the cloud. They let you code locally (or in a browser-emulated editor) and see your changes in a live deployed environments. They can also provide developers access to more powerful machines than typically available on a laptop or desktop.

Uffizzi is a more full-featured platform designed for building self-serve developer platforms and for standardizing end-to-end developer workflows through on-demand dev, test, CI, and staging environments. Similar to Codespaces and Gitpod, Uffizzi offers cloud-based dev environments, but unlike these tools, Uffizzi users have access to the underlying Kubernetes clusters, enabling more complex configurations and customization via kubectl and similar tools. Uffizzi also supports creating virtual clusters for ephemeral test environments, as well as, CI integrations for pull request previews.

See our documentation for other common uses and guides.
How is Uffizzi different from GitHub Actions (or other CI providers)? Uffizzi does not replace GitHub Actions or any other CI provider. Uffizzi can be added as a step in your existing CI pipeline, after your container images are built and pushed to a container registry. For example, when you open a pull request, a GitHub Actions workflow can trigger the creation of new virtual cluster on Uffizzi and deploy that branch onto it. See our CI Recipes for configuration help.
What about my database?

All services deployed to Uffizzi ephemeral environments are deployed as containers—this includes databases, caches, and other stateful services This means that even if you use a managed database service like Amazon RDS for production, you should use a database image in your configuration (See our Ephemeral Environments Handbook for strategies on managing stateful services on Uffizzi.

What do you mean by "environments"? See our documentaion for what we mean.
Does Uffizzi support monorepos/polyrepos? Yes. Whether created via you're creating ephemeral environments from the CLI, dashboard, or CI pipeline, Uffizzi can deploy applications from one source or many. If you're using Uffizzi virtual clusters, you should define the sources in your Helm Charts, kustomizations, or manifests. For Docker Compose users, Uffizzi just needs to know the fully qualified container registry URL for where to find these built images. See the Uffizzi Compose reference for details.
Does Uffizzi support _____________? In general, if your application can be containerized, described with Kubernetes, Helm, kustomize, or Docker Compose, then it is likely compatible with Uffizzi. The one notable exception to this is that Uffizzi does not support Node-level access, such as Kubernetes DaemonSets.
How can my application services communicate? See Uffizzi Networking for details.
Is Uffizzi open source? Yes. If you have access to a Kubernetes cluster, you can install Uffizzi via Helm. Follow the self-hosted installation guide.
## License This library is licensed under the [Apache License, Version 2.0](LICENSE). ## Security If you discover a security related issues, please do **not** create a public github issue. Notify the Uffizzi team privately by sending an email to `security@uffizzi.com`. ================================================ FILE: Rakefile ================================================ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative 'config/application' Rails.application.load_tasks ================================================ FILE: app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../stylesheets .css ================================================ FILE: app/assets/fonts/mtiFontTrackingCode.js ================================================ eval(function (p, a, c, k, e, d) { e = function (c) { return c.toString(36) }; if (!''.replace(/^/, String)) { while (c--) { d[c.toString(a)] = k[c] || c.toString(a) } k = [function (e) { return d[e] }]; e = function () { return '\\w+' }; c = 1 }; while (c--) { if (k[c]) { p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]) } } return p }('4 8=9.f;4 6=9.k.m(",");4 2=3.j(\'l\');2.h=\'g/5\';2.d=\'b\';2.e=(\'7:\'==3.i.s?\'7:\':\'u:\')+\'//v.n.w/x/1.5?t=5&c=\'+8+\'&o=\'+6;(3.a(\'p\')[0]||3.a(\'q\')[0]).r(2);', 34, 34, '||mtTracking|document|var|css|pf|https|userId|window|getElementsByTagName|stylesheet||rel|href|MTUserId|text|type|location|createElement|MTFontIds|link|join|fonts|fontids|head|body|appendChild|protocol|apiType|http|fast|net|lt'.split('|'), 0, {})) ================================================ FILE: app/assets/images/.keep ================================================ ================================================ FILE: app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: app/channels/application_cable/channel.rb ================================================ # frozen_string_literal: true module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: app/channels/application_cable/connection.rb ================================================ # frozen_string_literal: true module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: app/controllers/application_controller.rb ================================================ # frozen_string_literal: true class ApplicationController < ActionController::Base end ================================================ FILE: app/controllers/concerns/.keep ================================================ ================================================ FILE: app/helpers/application_helper.rb ================================================ # frozen_string_literal: true module ApplicationHelper end ================================================ FILE: app/javascript/channels/consumer.js ================================================ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. import { createConsumer } from "@rails/actioncable" export default createConsumer() ================================================ FILE: app/javascript/channels/index.js ================================================ // Load all the channels within this directory and all subdirectories. // Channel files must be named *_channel.js. const channels = require.context('.', true, /_channel\.js$/) channels.keys().forEach(channels) ================================================ FILE: app/javascript/packs/application.js ================================================ // This file is automatically compiled by Webpack, along with any other files // present in this directory. You're encouraged to place your actual application logic in // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. require("@rails/ujs").start() require("turbolinks").start() require("@rails/activestorage").start() require("channels") // Uncomment to copy all static images under ../images to the output folder and reference // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>) // or the `imagePath` JavaScript helper below. // // const images = require.context('../images', true) // const imagePath = (name) => images(name, true) ================================================ FILE: app/jobs/application_job.rb ================================================ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: app/mailers/application_mailer.rb ================================================ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: app/models/application_record.rb ================================================ # frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: app/models/concerns/.keep ================================================ ================================================ FILE: app/views/layouts/application.html.erb ================================================ Uffizzi App <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> <%= yield %> ================================================ FILE: app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby # frozen_string_literal: true APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: bin/rake ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: bin/setup ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require 'fileutils' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir(APP_ROOT) do # This script is a way to setup or update your development environment automatically. # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # Install JavaScript dependencies # system('bin/yarn') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: bin/yarn ================================================ #!/usr/bin/env ruby # frozen_string_literal: true APP_ROOT = File.expand_path('..', __dir__) Dir.chdir(APP_ROOT) do exec('yarnpkg', *ARGV) rescue Errno::ENOENT warn('Yarn executable was not detected in the system.') warn('Download Yarn at https://yarnpkg.com/en/docs/install') exit(1) end ================================================ FILE: charts/uffizzi-app/Chart.yaml ================================================ apiVersion: v2 name: uffizzi-app version: 1.3.0 kubeVersion: ">= 1.21.0-0" # https://issuetracker.google.com/issues/77503699 description: "Uffizzi is an open-source engine for creating lightweight, ephemeral test environments for APIs and full-stack applications. Uffizzi enables teams to preview new features before merging." type: application keywords: - devops - uffizzi - continuous-previews - ephemeral - environments - pull-request - merge-request - on-demand - ci - cd - cp - idp home: https://uffizzi.com/ sources: - https://github.com/UffizziCloud/uffizzi dependencies: - name: uffizzi-controller version: "^2" repository: https://uffizzicloud.github.io/uffizzi_controller/ - name: postgresql version: "~13" repository: https://charts.bitnami.com/bitnami condition: postgresql.enabled - name: redis version: "~18" repository: https://charts.bitnami.com/bitnami condition: redis.enabled maintainers: - name: Uffizzi email: info@uffizzi.com url: https://uffizzi.com - name: Adam Vollrath email: adam.vollrath@uffizzi.com url: https://github.com/axisofentropy icon: https://app.uffizzi.com/favicon.png appVersion: "2.3.0" deprecated: false annotations: # Use this annotation to indicate that this chart version is a pre-release. # https://artifacthub.io/docs/topics/annotations/helm/ artifacthub.io/prerelease: "false" ================================================ FILE: charts/uffizzi-app/README.md ================================================ # Helm chart guide This chart installs Uffizzi. ## Requirements This chart requires a Kubernetes Cluster. While it will likely function on k8s >= 1.19, we have only tested upon k8s 1.21 - 1.23. The Cluster must be capable of provisioning `Ingress` resources that obtain public IP addresses and/or hostnames. We've tested Uffizzi on: - Google Kubernetes Engine (GKE) - Azure Kubernetes Service (AKS) - Amazon Elastic Kubernetes Service (EKS) ### Dependencies This chart depends upon three subcharts: - [`bitnami/postgresql`](https://artifacthub.io/packages/helm/bitnami/postgresql) - [`bitnami/redis`](https://artifacthub.io/packages/helm/bitnami/redis) - [`uffizzi-controller`](https://artifacthub.io/packages/helm/uffizzi-controller/uffizzi-controller) You can disable the `bitnami` subcharts if you want to manage your own datastores. ## Configuration This Helm chart requires integration with your DNS records and other services, so there are several required values. Create a YAML file with these values before installing this chart. There's an example below and you can read more about Helm Values Files here: https://helm.sh/docs/chart_template_guide/values_files/ ### Controller See the [Controller's Helm Chart](https://artifacthub.io/packages/helm/uffizzi-controller/uffizzi-controller) for its configuration, including its certificate authority. The controller itself depends upon two other popular Helm charts: - [`ingress-nginx`](https://kubernetes.github.io/ingress-nginx/) - [`cert-manager`](https://cert-manager.io/docs/) If you already have one or both of these applications installed, you may want to disable them for this Helm release. Specifically, your k8s Cluster may already have cert-manager's Custom Resource Definitions defined. ### Secrets When installing Uffizzi in a sensitive or production environment, it's important to generate strong passwords. Provide new values for the `ChangeMeNow` values in the example below. ### Example Helm Values File Example values file with required values: ```yaml global: postgresql: auth: postgresPassword: ChangeMeNow password: ChangeMeNow redis: password: ChangeMeNow uffizzi: firstUser: email: user@example.com password: ChangeMeNow controller: password: ChangeMeNow app_url: https://uffizzi.example.com webHostname: uffizzi.example.com allowed_hosts: uffizzi.example.com managed_dns_zone_dns_name: uffizzi.example.com uffizzi-controller: ingress: hostname: controller.uffizzi.example.com clusterIssuer: "letsencrypt" certEmail: admin@example.com ``` Edit these values and save them in a file named `myvals.yaml` or similar. ## Installation If this is your first time using Helm, consult their documentation: https://helm.sh/docs/intro/quickstart/ Begin by adding our Helm repository: ``` helm repo add uffizzi https://uffizzicloud.github.io/uffizzi/ ``` Then install the lastest version as a new release using the values you specified earlier. We recommend isolating Uffizzi in its own Namespace. ``` helm install my-uffizzi uffizzi/uffizzi-app --values myvals.yaml --namespace uffizzi --create-namespace ``` If you encounter any errors here, tell us about them in [our Slack](https://join.slack.com/t/uffizzi/shared_invite/zt-ffr4o3x0-J~0yVT6qgFV~wmGm19Ux9A). You should then see the release is installed: ``` helm list --namespace uffizzi ``` ### DNS After the Helm release is installed, add DNS records for the hostnames you specified in your values file. You can obtain the IP or hostname for Uffizzi's Ingress using `kubectl`: ``` kubectl get ingress --namespace uffizzi ``` Be sure to add a "wildcard" record for the domain specified in `managed_dns_zone_dns_name`. In the above example, that's `*.uffizzi.example.com`. ### Provisioning users You'll need to create at least one User Account to access your Uffizzi installation. The easiest way to do this is specify values for `global.uffizzi.firstUser` as shown in the example values file above. Uffizzi will attempt to provision this User each time it starts. If you did not specify a `firstUser`, or if you want to provision additional Users, you may execute an interactive `rake` task within the application server container: ``` kubectl exec -it deploy/my-uffizzi-web --namespace uffizzi -- rake uffizzi_core:create_user Enter User Email (default: user@example.com): user@example.com Enter Password: Enter Project Name (default: default): ``` ### Troubleshooting When installing this chart, you may see errors like this: ``` clusterroles.rbac.authorization.k8s.io "my-uffizzi-controller-flux-default-source-controller-helmchart" already exists ``` This happens when more than one resource within a dependency chart (in this case `flux`) has a very long name truncated into the same name as another resource. To avoid this, use shorter release names as in the example above. ## Usage If everything went well, you can now connect to the Uffizzi API service and begin Continously Deploying Previews! Use [the Uffizzi CLI](https://github.com/UffizziCloud/uffizzi_cli) or [the Uffizzi GitHub Action](https://github.com/UffizziCloud/preview-action) or your own API client. ## More Info See this project's main repository here: https://github.com/UffizziCloud/uffizzi And explore Uffizzi https://uffizzi.com ================================================ FILE: charts/uffizzi-app/templates/configmap-common.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: uffizzi-web-common-envs data: DATABASE_POOL: "16" DATABASE_PORT: {{ .Values.global.postgresql.service.ports.postgresql | quote }} DATABASE_HOST: {{ default (print .Release.Name "-postgresql") .Values.db_host | quote }} RAILS_ENV: {{ .Values.env | quote }} APP_URL: {{ .Values.app_url }} CONTROLLER_URL: {{ default (print "http://" .Release.Name "-controller:8080") .Values.controller_url }} EMAIL_DELIVERY_ENABLED: {{ .Values.feature_email_delivery | quote }} MANAGED_DNS_ZONE_DNS_NAME: {{ .Values.managed_dns_zone_dns_name | quote }} UFFIZZI_USER_EMAIL: {{ .Values.global.uffizzi.firstUser.email }} UFFIZZI_USER_PASSWORD: {{ .Values.global.uffizzi.firstUser.password | quote }} UFFIZZI_PROJECT_NAME: {{ .Values.global.uffizzi.firstUser.projectName | quote }} ================================================ FILE: charts/uffizzi-app/templates/configmap-sidekiq.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: uffizzi-web-sidekiq-envs data: SIDEKIQ_CONCURRENCY: "10" ================================================ FILE: charts/uffizzi-app/templates/configmap-web.yaml ================================================ apiVersion: v1 kind: ConfigMap metadata: name: uffizzi-web-service-envs data: ALLOWED_HOSTS: {{ .Values.allowed_hosts | quote }} RAILS_PORT: "7000" RAILS_SERVE_STATIC_FILES: "true" RAILS_THREADS_COUNT: "8" RAILS_WORKERS_COUNT: "2" ================================================ FILE: charts/uffizzi-app/templates/secret-web.yaml ================================================ apiVersion: v1 kind: Secret metadata: name: uffizzi-web-secret-envs type: Opaque data: DATABASE_USER: {{ .Values.global.postgresql.auth.username | b64enc }} DATABASE_PASSWORD: {{ .Values.global.postgresql.auth.password | b64enc }} DATABASE_NAME: {{ .Values.global.postgresql.auth.database | b64enc }} RAILS_SECRET_KEY_BASE: {{ .Values.rails_secret_key_base | default (printf "%x" (randAscii 64))| b64enc }} REDIS_URL: {{ default (print "redis://:" .Values.global.redis.password "@" .Release.Name "-redis-master") .Values.redis_url | b64enc }} APP_LOGIN: {{ .Values.basic_auth_login | b64enc }} APP_PASSWORD: {{ .Values.basic_auth_password | b64enc }} SIDEKIQ_LOGIN: {{ .Values.basic_auth_login | b64enc }} SIDEKIQ_PASSWORD: {{ .Values.basic_auth_password | b64enc }} CONTROLLER_LOGIN: {{ .Values.global.uffizzi.controller.username | b64enc }} CONTROLLER_PASSWORD: {{ .Values.global.uffizzi.controller.password | b64enc }} GITHUB_APP_ID: {{ .Values.github_app_id | b64enc }} GITHUB_APP_SLUG: {{ .Values.github_app_slug | b64enc }} GITHUB_CLIENT_ID: {{ .Values.github_client_id | b64enc }} GITHUB_CLIENT_SECRET: {{ .Values.github_client_secret | b64enc }} GITHUB_PRIVATE_KEY: {{ .Values.github_private_key | b64enc }} GITHUB_WEBHOOK_SECRET: {{ .Values.github_webhook_secret | b64enc }} ================================================ FILE: charts/uffizzi-app/templates/sidekiq-deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-sidekiq labels: app: uffizzi-sidekiq spec: replicas: {{ .Values.sidekiq_replicas }} selector: matchLabels: app: uffizzi-sidekiq template: metadata: labels: app: uffizzi-sidekiq spec: automountServiceAccountToken: false containers: - name: uffizzi-sidekiq image: {{ .Values.image }} command: - /bin/bash - -c args: - bundle exec sidekiq -C /app/config/sidekiq.yml envFrom: - secretRef: name: uffizzi-web-secret-envs optional: false - configMapRef: name: uffizzi-web-common-envs optional: false - configMapRef: name: uffizzi-web-sidekiq-envs optional: false imagePullPolicy: Always resources: requests: cpu: 400m memory: 800Mi ================================================ FILE: charts/uffizzi-app/templates/web-deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }}-web labels: app: uffizzi-web spec: replicas: {{ .Values.web_replicas }} selector: matchLabels: app: uffizzi-web template: metadata: labels: app: uffizzi-web spec: automountServiceAccountToken: false containers: - name: uffizzi-web image: {{ .Values.image }} command: - /bin/bash - -c args: - bundle exec rails db:create db:migrate && bundle exec rake uffizzi_core:create_user && bundle exec puma -C config/puma.rb envFrom: - secretRef: name: uffizzi-web-secret-envs optional: false - configMapRef: name: uffizzi-web-common-envs optional: false - configMapRef: name: uffizzi-web-service-envs optional: false imagePullPolicy: Always ports: - containerPort: 7000 protocol: TCP # readinessProbe: # failureThreshold: 3 # httpGet: # path: /health_check # port: 7000 # scheme: HTTP # periodSeconds: 10 # successThreshold: 1 # timeoutSeconds: 1 resources: requests: cpu: 150m memory: 800Mi ================================================ FILE: charts/uffizzi-app/templates/web-ingress.yaml ================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ .Release.Name }}-web-ingress annotations: cert-manager.io/cluster-issuer: {{ print .Release.Name "-" (index (.Values) "uffizzi-controller" "clusterIssuer") }} kubernetes.io/ingress.class: nginx spec: rules: - host: {{ .Values.webHostname }} http: paths: - path: / pathType: Prefix backend: service: name: {{ .Release.Name }}-web-service port: number: 80 tls: - secretName: {{ .Values.webHostname }} hosts: - {{ .Values.webHostname }} ================================================ FILE: charts/uffizzi-app/templates/web-service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }}-web-service spec: type: NodePort selector: app: uffizzi-web ports: - name: http port: 80 protocol: TCP targetPort: 7000 sessionAffinity: ClientIP ================================================ FILE: charts/uffizzi-app/values.yaml ================================================ global: postgresql: auth: postgresPassword: ChangeMeNow username: uffizzi-user password: ChangeMeNow database: uffizzi-app service: ports: postgresql: "5432" redis: password: ChangeMeNow uffizzi: firstUser: email: "" password: "" projectName: default controller: username: username password: ChangeMeNow env: production app_url: https://uffizzi.example.com webHostname: uffizzi.example.com image: uffizzi/app web-replicas: 3 sidekiq-replicas: 1 hostname: localhost controller_url: "" allowed_hosts: "" db_host: "" # default to dependent postgresql rails_secret_key_base: "" # default to half-random string redis_url: "" # default to dependent redis managed_dns_zone_dns_name: uffizzi.example.com basic_auth_login: username basic_auth_password: ChangeMeNow github_app_id: ChangeMeNow github_app_slug: ChangeMeNow github_client_id: ChangeMeNow github_client_secret: ChangeMeNow github_private_key: ChangeMeNow github_webhook_secret: ChangeMeNow sql_credentials_secret_name: example feature_mailchimp: false feature_email_delivery: false feature_google_maps: false feature_sentry: false feature_stripe: false uffizzi-controller: #dependency clusterIssuer: "letsencrypt" image: uffizzi/controller:latest ingress: hostname: uffizzi.example.com podCidr: 10.0.0.0/8 zerossl: eab: hmacKey: foo keyId: bar env: "production" sandbox: "false" certEmail: "user@example.com" cert-manager: # dependency of dependency installCRDs: true postgresql: #dependency enabled: true redis: # dependency enabled: true ================================================ FILE: ci/github-actions/README.md ================================================ # Use Uffizzi with GitHub Actions You can configure Uffizzi to create, update, and delete on-demand test environments with the GitHub Actions [reusable workflow](https://github.com/UffizziCloud/preview-action/blob/master/.github/workflows/reusable.yaml). This reusable workflow will execute the [Uffizzi CLI](https://github.com/UffizziCloud/uffizzi_cli) on a GitHub Actions runner, which then opens a connection to the Uffizzi API. ## Example usage The following example application demonstrates how to use Uffizzi with GitHub Actions: [Example voting app](https://github.com/UffizziCloud/example-voting-app/blob/main/.github/workflows/uffizzi-previews.yml) ================================================ FILE: ci/gitlab/README.md ================================================ # Use Uffizzi with GitLab CI You can configure Uffizzi to create, update, and delete on-demand Preview Environments with the GitLab CI [Environment Action](https://gitlab.com/uffizzi/environment-action). This action will execute the [Uffizzi CLI](https://github.com/UffizziCloud/uffizzi_cli) on a GitLab runner, which then opens a connection to the Uffizzi API. ================================================ FILE: config/application.rb ================================================ # frozen_string_literal: true require_relative 'boot' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) require 'uffizzi_core' module UffizziApp class Application < Rails::Application config.load_defaults(6.0) config.hosts = Settings.allowed_hosts config.middleware.insert_before(0, Rack::Cors) do allow do origins do |source| uri = URI.parse(source) Settings.allowed_hosts.any? { |host| uri.host == host || uri.host.ends_with?(host) } end resource '*', headers: :any, methods: [:get, :post, :options, :put, :patch, :delete], credentials: true end end end end ================================================ FILE: config/boot.rb ================================================ # frozen_string_literal: true ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' # Set up gems listed in the Gemfile. require 'bootsnap/setup' # Speed up boot time by caching expensive operations. ================================================ FILE: config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: uffizzi_app_production ================================================ FILE: config/credentials.yml.enc ================================================ /panGyOWYw7RDiAdnBU58UQcCf5LmUrtROWE1pQpIAlZ1BUUsCat6oE8MVO5TDTc4q0XOGjndQ28i+ffjpHCob1TPHlTwGmAWcaKawSOQeLPOgHHvtzumHfS5erwYLMn6DgKyUNqobtk0+0PFTiwY5Cpbukri+gwb7pc8rjweYSRilreS++X/pf7zm606NvRiIKgoVMciie25CVR9g1ll1jgjE/L750rrROrGsobLuMp6XEBdzjqI1O2P9fiyCFND3a2g4kq3ARnK7b/T2yk3NG31UABfSkNoGJk72vyxW+gottHKe1RhZ4kYWQvegycS1FrxSj2bEXcdjnWzOxs1lCzJgj2ImaYm/sN7pyDZ1zXiyYYqWpRWDbGtER0IOUVUg1bAxDK8BfFFj8kX3W4en8VkmmVcJWv7lDX--f9oik+xRebXs6ijD--rZd8unbXjXvCf6ar117b4Q== ================================================ FILE: config/database.yml ================================================ default: &default adapter: postgresql encoding: unicode host: <%= ENV.fetch("DATABASE_HOST") {"127.0.0.1"} %> port: <%= ENV.fetch("DATABASE_PORT") {5432} %> pool: <%= ENV.fetch("DATABASE_POOL") {5} %> username: <%= ENV.fetch("DATABASE_USER") {"postgres"} %> password: <%= ENV.fetch("DATABASE_PASSWORD") {""} %> development: <<: *default database: uffizzi_development test: <<: *default database: uffizzi_test production: <<: *default database: uffizzi_production ================================================ FILE: config/environment.rb ================================================ # frozen_string_literal: true # Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: config/environments/development.rb ================================================ # frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}", } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log config.logger = Logger.new($stdout) config.log_level = :debug # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. config.file_watcher = ActiveSupport::FileUpdateChecker end ================================================ FILE: config/environments/production.rb ================================================ # frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Use the lowest log level to ensure availability of diagnostic information # when problems arise. config.log_level = :debug # Prepend all log lines with the following tags. config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "uffizzi_app_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector # middleware. The `delay` is used to determine how long to wait after a write # to send a subsequent read to the primary. # # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # # The `database_resolver_context` class is used by the middleware to set # timestamps for the last write to the primary. The resolver uses the context # class timestamps to determine how long to wait before reading from the # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own # strategy for connection switching and pass that into the middleware through # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end ================================================ FILE: config/environments/test.rb ================================================ # frozen_string_literal: true # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. config.cache_classes = false config.action_view.cache_template_loading = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}", } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true end ================================================ FILE: config/initializers/application_controller_renderer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: config/initializers/assets.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Add Yarn node_modules folder to the asset load path. Rails.application.config.assets.paths << Rails.root.join('node_modules') # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! ================================================ FILE: config/initializers/content_security_policy.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # If you are using webpack-dev-server then specify webpack-dev-server host # policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Set the nonce only to specific directives # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # Rails.application.config.content_security_policy_report_only = true ================================================ FILE: config/initializers/cookies_serializer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: config/initializers/filter_parameter_logging.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [:password] ================================================ FILE: config/initializers/health_check.rb ================================================ # frozen_string_literal: true HealthCheck.setup do |config| config.uri = ENV['HEALTH_CHECK_URI'] || 'health_check' config.success = 'success' config.http_status_for_error_text = 500 config.http_status_for_error_object = 500 config.standard_checks = ['database', 'migrations'] config.full_checks = ['database', 'migrations'] end ================================================ FILE: config/initializers/inflections.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: config/initializers/mime_types.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: config/initializers/sidekiq.rb ================================================ # frozen_string_literal: true require 'sidekiq/web' require 'sidekiq-unique-jobs' Sidekiq.configure_client do |config| config.redis = { url: ENV['REDIS_URL'], size: 2 } config.client_middleware do |chain| chain.add(SidekiqUniqueJobs::Middleware::Client) end end Sidekiq.configure_server do |config| config.redis = { url: ENV['REDIS_URL'], size: 20 } config.client_middleware do |chain| chain.add(SidekiqUniqueJobs::Middleware::Client) end config.server_middleware do |chain| chain.add(SidekiqUniqueJobs::Middleware::Server) end SidekiqUniqueJobs::Server.configure(config) end if Settings.sidekiq.login.present? Sidekiq::Web.use(Rack::Auth::Basic) do |username, password| valid_login = ActiveSupport::SecurityUtils.secure_compare( ::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(Settings.sidekiq.login), ) valid_password = ActiveSupport::SecurityUtils.secure_compare( ::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(Settings.sidekiq.password), ) valid_login & valid_password end end SidekiqUniqueJobs.configure do |config| config.enabled = !Rails.env.test? end ================================================ FILE: config/initializers/wrap_parameters.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: ================================================ FILE: config/puma.rb ================================================ # frozen_string_literal: true threads_count = ENV.fetch('RAILS_THREADS_COUNT', 5) threads threads_count, threads_count port ENV.fetch('RAILS_PORT', 7000) environment ENV.fetch('RAILS_ENV', 'development') workers ENV.fetch('RAILS_WORKERS_COUNT', 18) preload_app! if ENV.fetch('RAILS_WORKERS_COUNT').to_i.positive? before_fork do ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) end on_worker_boot do ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end ================================================ FILE: config/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do mount UffizziCore::Engine => '/' health_check_routes end ================================================ FILE: config/secrets.yml ================================================ default: &default secret_key_base: <%=Settings.rails.secret_key_base%> development: <<: *default test: <<: *default production: <<: *default ================================================ FILE: config/settings.yml ================================================ rails: secret_key_base: <%= ENV['RAILS_SECRET_KEY_BASE'] %> app: ttl_reset_password_token: 900 host: <%= ENV['APP_URL'] %> login: <%= ENV['APP_LOGIN'] || '' %> password: <%= ENV['APP_PASSWORD'] || '' %> managed_dns_zone: <%= ENV['MANAGED_DNS_ZONE_DNS_NAME'] %> github: app_id: <%= ENV['GITHUB_APP_ID'] %> app_slug: <%= ENV['GITHUB_APP_SLUG'] %> client_id: <%= ENV['GITHUB_CLIENT_ID'] %> client_secret: <%= ENV['GITHUB_CLIENT_SECRET'] %> private_key: "<%= ENV['GITHUB_PRIVATE_KEY'] %>" webhook_secret: <%= ENV['GITHUB_WEBHOOK_SECRET'] %> docker_hub: registry_url: 'https://index.docker.io/v1/' public_namespace: 'library' sidekiq: login: <%= ENV['SIDEKIQ_LOGIN'] || '' %> password: <%= ENV['SIDEKIQ_PASSWORD'] || '' %> controller: url: <%= ENV['CONTROLLER_URL'] || 'http://controller:8080' %> login: <%= ENV['CONTROLLER_LOGIN'] || '' %> password: <%= ENV['CONTROLLER_PASSWORD'] || '' %> connection: retires_count: 1 next_retry_timeout_seconds: 1 timeout: 7 open_timeout: 5 limits: cpu: '200m' namespace_prefix: 'app-' resource_create_retry_time: <%= 15.seconds %> resource_update_retry_count: 60 vcluster_controller: url: <%= ENV['VCLUSTER_CONTROLLER_URL'] || 'http://controller:8080' %> login: <%= ENV['VCLUSTER_CONTROLLER_LOGIN'] || '' %> password: <%= ENV['VCLUSTER_CONTROLLER_PASSWORD'] || '' %> managed_dns_zone: <%= ENV['VCLUSTER_MANAGED_DNS_ZONE_DNS_NAME'] %> connection: retires_count: 1 next_retry_timeout_seconds: 1 timeout: 15 open_timeout: 10 billing_cycle: hours_count: 720 slack: invite_url: https://join.slack.com/t/uffizzi/shared_invite/zt-ffr4o3x0-J~0yVT6qgFV~wmGm19Ux9A allowed_hosts: <%= ENV['ALLOWED_HOSTS']&.split(',') || [] %> domain: <%= ENV['APP_URL'] %> sentry: dsn: <%= ENV['SENTRY_DSN'] %> release: <%= ENV['SENTRY_RELEASE'] %> env: <%= Rails.env %> features: email_delivery_enabled: <%= !!ActiveModel::Type::Boolean.new.cast(ENV['EMAIL_DELIVERY_ENABLED']) %> stripe_enabled: <%= !!ActiveModel::Type::Boolean.new.cast(ENV['STRIPE_ENABLED']) %> platform_cluster: project_id: <%= ENV['PLATFORM_PROJECT_ID'] %> google: registry_url: 'https://gcr.io/' github_container_registry: registry_url: 'https://ghcr.io/' compose: default_memory: 125 memory_postfixes: <%= ['b', 'k', 'm', 'g'] %> memory_values: <%= [125, 250, 500, 1000, 2000, 4000] %> port_min_value: 1 port_max_value: 65535 delete_after_postfixes: <%= ['h'] %> delete_after_min_value: 1 delete_after_max_value: 720 default_tag: latest dockerfile_default_path: Dockerfile default_branch: 'master' continuous_preview: default_delete_preview_after: 72 deployment: max_memory_limit: 8000 subdomain: length_limit: 63 default_job_retry_count: 5 vcluster: max_creation_retry_count: 5 ================================================ FILE: config/sidekiq.yml ================================================ --- :concurrency: <%= ENV["SIDEKIQ_CONCURRENCY"].nil? ? 5 : ENV["SIDEKIQ_CONCURRENCY"].to_i %> :verbose: true :queues: - [default, 5] - [active_storage_analysis, 5] - [active_storage_purge, 5] - [mailers, 5] - [billing, 6] - [deployments, 10] - [config_files, 10] - [compose_files, 10] - [resources, 10] - [databases, 10] - [projects, 10] - [accounts, 10] - [containers, 10] - [github, 10] - [disable_deployments, 5] - [clusters, 10] ================================================ FILE: config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> ================================================ FILE: config.ru ================================================ # frozen_string_literal: true # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application ================================================ FILE: core/.gitignore ================================================ /.bundle/ /doc/ /log/*.log /pkg/ /tmp/ /test/dummy/db/*.sqlite3 /test/dummy/db/*.sqlite3-* /test/dummy/log/*.log /test/dummy/storage/ /test/dummy/tmp/ .byebug_history ================================================ FILE: core/CHANGELOG.md ================================================ ## [0.1.0] - 2022-04-05 - Initial release ================================================ FILE: core/Dockerfile ================================================ FROM ruby:3.0.2-alpine3.14 RUN apk update && apk upgrade && apk add bash curl-dev ruby-dev build-base git gcompat \ curl ruby-json openssl postgresql-dev postgresql-client tzdata RUN mkdir -p /gem WORKDIR /gem ENV GEM_HOME="/usr/local/bundle" COPY lib/uffizzi_core/version.rb /gem/lib/uffizzi_core/ COPY uffizzi_core.gemspec /gem/ COPY Gemfile* /gem/ RUN bundle install --jobs 4 COPY . /gem ENV PATH /gem/bin:$GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH ================================================ FILE: core/Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'iri' gemspec ================================================ FILE: core/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: core/Makefile ================================================ release_gem: mkdir -p ${HOME}/.gem touch ${HOME}/.gem/credentials chmod 0600 ${HOME}/.gem/credentials printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > ${HOME}/.gem/credentials gem build *.gemspec gem push *.gem ================================================ FILE: core/README.md ================================================ # Uffizzi Core **Uffizzi CLI API, Models, Services and core libraries** ## Uffizzi Overview Uffizzi is the Full-stack Previews Engine that makes it easy for your team to preview code changes before merging—whether frontend, backend or microserivce. Define your full-stack apps with a familiar syntax based on Docker Compose, and Uffizzi will create on-demand test environments when you open pull requests or build new images. Preview URLs are updated when there’s a new commit, so your team can catch issues early, iterate quickly, and accelerate your release cycles. ## Getting started with Uffizzi The fastest and easiest way to get started with Uffizzi is via the fully hosted version available at https://uffizzi.com, which includes free plans for small teams and qualifying open-source projects. Alternatively, you can self-host Uffizzi via the open-source repositories available here on GitHub. The remainder of this README is intended for users interested in self-hosting Uffizzi or for those who are just curious about how Uffizzi works. ## Uffizzi Architecture Uffizzi consists of the following components: * [Uffizzi App](https://github.com/UffizziCloud/uffizzi_app) - The primary REST API for creating and managing Previews * [Uffizzi Controller](https://github.com/UffizziCloud/uffizzi_controller) - A smart proxy service that handles requests from Uffizzi App to the Kubernetes API * Uffizzi CLI (this repository) - A command-line interface for Uffizzi App * [Uffizzi Dashboard](https://app.uffizzi.com) - A graphical user interface for Uffizzi App, available as a paid service at https://uffizzi.com To host Uffizzi yourself, you will also need the following external dependencies: * Kubernetes (k8s) cluster * Postgres database * Redis cache ## Installation Add this line to your application's Gemfile: ```ruby gem 'uffizzi_core' ``` And then execute: ```bash $ bundle ``` Or install it yourself as: ```bash $ gem install uffizzi_core ``` ================================================ FILE: core/Rakefile ================================================ # frozen_string_literal: true require 'bundler/setup' APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) load 'rails/tasks/engine.rake' load 'rails/tasks/statistics.rake' require 'bundler/gem_tasks' require 'rake/testtask' Rake::TestTask.new(:test) do |t| t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = false end task default: :test namespace :core do desc 'Generate api docs' task generate_docs: :environment do SwaggerYard.register_custom_yard_tags! spec = SwaggerYard::Swagger.new File.open('swagger/v1/swagger.json', 'w') { |f| f << JSON.pretty_generate(spec.to_h) } end end ================================================ FILE: core/app/assets/config/uffizzi_core_manifest.js ================================================ //= link_directory ../stylesheets/uffizzi_core .css ================================================ FILE: core/app/assets/images/uffizzi_core/.keep ================================================ ================================================ FILE: core/app/assets/stylesheets/uffizzi_core/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: core/app/clients/uffizzi_core/amazon_registry_client.rb ================================================ # frozen_string_literal: true class UffizziCore::AmazonRegistryClient attr_accessor :client, :token, :registry_url def initialize(region:, access_key_id:, secret_access_key:) credentials = Aws::Credentials.new(access_key_id, secret_access_key) @client = Aws::ECR::Client.new(region: region, credentials: credentials) end def authorization_token client.get_authorization_token({}) end def batch_get_image(image:, tag:) client.batch_get_image({ image_ids: [{ image_tag: tag }], repository_name: image }) end end ================================================ FILE: core/app/clients/uffizzi_core/azure_registry_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::AzureRegistryClient::RequestResult < Hashie::Mash disable_warnings :key end ================================================ FILE: core/app/clients/uffizzi_core/azure_registry_client.rb ================================================ # frozen_string_literal: true class UffizziCore::AzureRegistryClient attr_accessor :connection, :token, :registry_url def initialize(registry_url:, username:, password:) @registry_url = registry_url @connection = build_connection(registry_url, username, password) @token = oauth2_token&.result&.access_token end def manifests(image:, tag:) url = "/v2/#{image}/manifests/#{tag}" response = connection.get(url) RequestResult.quiet.new(result: response.body, headers: response.headers) end def oauth2_token service = URI.parse(registry_url).hostname url = "/oauth2/token?service=#{service}" response = connection.get(url, {}) RequestResult.new(result: response.body) end def authenticated? token.present? end private def build_connection(registry_url, username, password) connection = Faraday.new(registry_url) do |faraday| faraday.request(:basic_auth, username, password) faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end end ================================================ FILE: core/app/clients/uffizzi_core/container_registry_request_decorator.rb ================================================ # frozen_string_literal: true module UffizziCore::ContainerRegistryRequestDecorator [:get, :post, :head].each do |method| define_method(method) do |url, params_or_body = nil, headers = nil, &block| super(url, params_or_body, headers, &block) rescue Faraday::ClientError => e raise UffizziCore::ContainerRegistryError.new(e.response) end end end ================================================ FILE: core/app/clients/uffizzi_core/controller_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::ControllerClient::RequestResult < Hashie::Mash disable_warnings :key end ================================================ FILE: core/app/clients/uffizzi_core/controller_client.rb ================================================ # frozen_string_literal: true class UffizziCore::ControllerClient class ConnectionError < StandardError; end attr_accessor :connection def initialize(connection_settings) @connection = build_connection(connection_settings) end def apply_config_file(deployment_id:, config_file_id:, body:) connection.post("/deployments/#{deployment_id}/config_files/#{config_file_id}", body) end def deployment_containers(deployment_id:) get("/deployments/#{deployment_id}/containers") end def deploy_containers(deployment_id:, body:) connection.post("/deployments/#{deployment_id}/containers", body) end def deployment_containers_metrics(deployment_id:) get("/deployments/#{deployment_id}/containers/metrics") end def deployment_container_logs(deployment_id:, container_name:, limit:, previous:) get("/deployments/#{deployment_id}/containers/#{container_name}/logs?limit=#{limit}&previous=#{previous}") end def deployment_containers_events(deployment_id:) events = get("/deployments/#{deployment_id}/containers/events").result.items pods_events = events.select { |event| event.involved_object.kind == 'Pod' } pods_events.map do |event| { first_timestamp: event.first_timestamp, last_timestamp: event.last_timestamp, reason: event.reason, message: event.message } end end def nodes get('/nodes') end def apply_credential(deployment_id:, body:) connection.post("/deployments/#{deployment_id}/credentials", body) end def delete_credential(deployment_id:, credential_id:) connection.delete("/deployments/#{deployment_id}/credentials/#{credential_id}") end def get_deployments_usage_metrics_containers(deployment_ids:, begin_at:, end_at:) query_params = { deployment_ids: deployment_ids, begin_at: begin_at, end_at: end_at, } get('/deployments/usage_metrics/containers', query_params) end def create_namespace(body:) post('/namespaces', body) end def namespace(namespace:) get("/namespaces/#{namespace}") end def delete_namespace(namespace:) connection.delete("/namespaces/#{namespace}") end def create_cluster(namespace:, body:) post("/namespaces/#{namespace}/cluster", body) end def show_cluster(namespace:, name:) get("/namespaces/#{namespace}/cluster/#{name}") end def patch_cluster(name:, namespace:, body:) patch("/namespaces/#{namespace}/cluster/#{name}", body) end def ingresses(namespace:) get("/namespaces/#{namespace}/ingresses") end private def get(url, params = {}) make_request(:get, url, params) end def post(url, params = {}) make_request(:post, url, params) end def patch(url, params = {}) make_request(:patch, url, params) end def make_request(method, url, params) response = connection.send(method, url, params) body = response.body underscored_body = UffizziCore::Converters.deep_underscore_keys(body) RequestResult.quiet.new(code: response.status, result: underscored_body) rescue Faraday::ServerError raise ConnectionError end def build_connection(settings) connection = settings.connection handled_exceptions = Faraday::Request::Retry::DEFAULT_EXCEPTIONS + [Faraday::ConnectionFailed] Faraday.new(settings.url) do |conn| conn.options.timeout = connection.timeout conn.options.open_timeout = connection.open_timeout conn.request(:basic_auth, settings.login, settings.password) conn.request(:json) conn.request(:retry, max: connection.retires_count, interval: connection.next_retry_timeout_seconds, exceptions: handled_exceptions) conn.response(:json) conn.response(:raise_error) conn.adapter(Faraday.default_adapter) end end end ================================================ FILE: core/app/clients/uffizzi_core/docker_hub_client/not_authorized_error.rb ================================================ # frozen_string_literal: true class UffizziCore::DockerHubClient class NotAuthorizedError < StandardError end end ================================================ FILE: core/app/clients/uffizzi_core/docker_hub_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::DockerHubClient class RequestResult < Hashie::Mash disable_warnings :key end end ================================================ FILE: core/app/clients/uffizzi_core/docker_hub_client.rb ================================================ # frozen_string_literal: true class UffizziCore::DockerHubClient attr_accessor :connection, :jwt, :credential BASE_URL = 'https://hub.docker.com' def initialize(credential = nil) @connection = build_connection @credential = credential return unless credential @jwt = authenticate end def authenticate params = { username: credential.username, password: credential.password } url = "#{BASE_URL}/v2/users/login/" response = connection.post(url, params) request_result = RequestResult.new(result: response.body) request_result.result.token rescue NoMethodError nil end def repository(namespace:, image:) url = "#{BASE_URL}/v2/repositories/#{namespace}/#{image}" response = connection.get(url) do |request| request.headers['Authorization'] = "JWT #{jwt}" end RequestResult.new(status: response.status, result: response.body) end def public_images(q:, page: 1, per_page: 25) url = "#{BASE_URL}/api/content/v1/products/search" params = { page_size: per_page, q: q, type: :image, page: page } response = connection.get(url, params) do |request| request.headers['Search-Version'] = 'v3' end RequestResult.new(result: response.body) end def private_images(account:, page: 1, per_page: 25) raise NotAuthorizedError if !authenticated? || account.empty? url = BASE_URL + "/v2/repositories/#{account}/" params = { page_size: per_page, page: page } response = connection.get(url, params) do |request| request.headers['Authorization'] = "JWT #{jwt}" end RequestResult.new(result: response.body) end def accounts raise NotAuthorizedError if !authenticated? url = "#{BASE_URL}/v2/repositories/namespaces/" response = connection.get(url) do |request| request.headers['Authorization'] = "JWT #{jwt}" end RequestResult.new(result: response.body) end def metadata(namespace:, image:) url = BASE_URL + "/v2/repositories/#{namespace}/#{image}/" response = connection.get(url) do |request| request.headers['Authorization'] = "JWT #{jwt}" end RequestResult.quiet.new(result: response.body) end def tags(namespace:, image:, q: '', page: 1, per_page: 10) url = BASE_URL + "/v2/repositories/#{namespace}/#{image}/tags" params = { page_size: per_page, page: page, name: q } response = connection.get(url, params) do |request| request.headers['Authorization'] = "JWT #{jwt}" end RequestResult.quiet.new(result: response.body) end def digest(image:, tag:, token:) url = "https://index.docker.io/v2/#{image}/manifests/#{tag}" response = connection.get(url) do |request| request.headers['Accept'] = 'application/vnd.docker.distribution.manifest.v2+json' request.headers['Authorization'] = "Bearer #{token}" end RequestResult.quiet.new(result: response.body, headers: response.headers) end def get_token(repository) params = { username: credential.username, password: credential.password } url = "https://auth.docker.io/token?service=registry.docker.io&scope=repository:#{repository}:pull" response = connection.get(url, params) RequestResult.new(result: response.body) end def authenticated? jwt.present? end private def build_connection connection = Faraday.new do |faraday| faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end end ================================================ FILE: core/app/clients/uffizzi_core/docker_registry_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::DockerRegistryClient::RequestResult < Hashie::Mash disable_warnings :key end ================================================ FILE: core/app/clients/uffizzi_core/docker_registry_client.rb ================================================ # frozen_string_literal: true class UffizziCore::DockerRegistryClient ACCEPTED_TYPES = [ 'application/vnd.oci.image.index.v1+json', 'application/vnd.oci.image.manifest.v1+json', 'application/vnd.docker.distribution.manifest.v1+json', 'application/vnd.docker.distribution.manifest.v2+json', 'application/vnd.docker.distribution.manifest.list.v2+json', '*/*', ].freeze def initialize(registry_url:, username: nil, password: nil) @registry_url = registry_url @connection = build_connection(username, password) end def authenticated? @connection.get("#{@registry_url}/v2/") true end def manifests(image:, tag:, namespace: nil) full_image = [namespace, image].compact.join('/') url = "#{@registry_url}/v2/#{full_image}/manifests/#{tag}" response = @connection.get(url) RequestResult.new(status: response.status, result: response.body) end private def build_connection(username, password) # initializing Faraday with the registry_url will trim the trailing slash required for the /v2/ request connection = Faraday.new do |faraday| faraday.headers['Accept'] = ACCEPTED_TYPES faraday.request(:basic_auth, username, password) if username.present? && password.present? faraday.request(:json) faraday.response(:json) faraday.response(:follow_redirects) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end end ================================================ FILE: core/app/clients/uffizzi_core/github_container_registry_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::GithubContainerRegistryClient class RequestResult < Hashie::Mash disable_warnings :key end end ================================================ FILE: core/app/clients/uffizzi_core/github_container_registry_client.rb ================================================ # frozen_string_literal: true class UffizziCore::GithubContainerRegistryClient attr_accessor :token, :registry_url def initialize(registry_url:, username:, password:) @registry_url = registry_url @username = username @password = password @token = access_token&.result&.token end def access_token service = URI.parse(registry_url).hostname url = "/token?service=#{service}" response = connection.get(url, {}) RequestResult.new(result: response.body) end def authenticated? token.present? end def manifests(image:, tag:) url = "/v2/#{@username}/#{image}/manifests/#{tag}" response = token_connection.get(url) RequestResult.quiet.new(result: response.body, headers: response.headers) end private def connection connection = Faraday.new(registry_url) do |faraday| faraday.request(:basic_auth, @username, @password) faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end def token_connection connection = Faraday.new(registry_url) do |faraday| faraday.request(:authorization, 'Bearer', token) faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end end ================================================ FILE: core/app/clients/uffizzi_core/google_registry_client/request_result.rb ================================================ # frozen_string_literal: true class UffizziCore::GoogleRegistryClient::RequestResult < Hashie::Mash disable_warnings :key end ================================================ FILE: core/app/clients/uffizzi_core/google_registry_client.rb ================================================ # frozen_string_literal: true class UffizziCore::GoogleRegistryClient attr_accessor :connection, :token, :registry_url def initialize(registry_url:, username:, password:) @registry_url = registry_url @connection = build_connection(registry_url, username, password) @token = access_token&.result&.token end def manifests(image:, tag:) url = "/v2/#{image}/manifests/#{tag}" response = connection.get(url) RequestResult.quiet.new(result: response.body, headers: response.headers) end def access_token service = URI.parse(registry_url).hostname url = "/v2/token?service=#{service}" response = connection.get(url, {}) RequestResult.new(result: response.body) end def authenticated? token.present? end private def build_connection(registry_url, username, password) connection = Faraday.new(registry_url) do |faraday| faraday.request(:basic_auth, username, password) faraday.request(:json) faraday.response(:json) faraday.response(:raise_error) faraday.adapter(Faraday.default_adapter) end connection.extend(UffizziCore::ContainerRegistryRequestDecorator) end end ================================================ FILE: core/app/contexts/uffizzi_core/account_context.rb ================================================ # frozen_string_literal: true class UffizziCore::AccountContext attr_reader :user, :user_access_module, :account, :params def initialize(user, user_access_module, account, params) @user = user @user_access_module = user_access_module @account = account @params = params end end ================================================ FILE: core/app/contexts/uffizzi_core/base_context.rb ================================================ # frozen_string_literal: true class UffizziCore::BaseContext attr_reader :user, :user_access_module, :params def initialize(user, user_access_module, params) @user = user @user_access_module = user_access_module @params = params end end ================================================ FILE: core/app/contexts/uffizzi_core/project/cluster_context.rb ================================================ # frozen_string_literal: true class UffizziCore::Project::ClusterContext attr_reader :user, :user_access_module, :project, :cluster, :params def initialize(user, project, user_access_module, cluster, params) @user = user @user_access_module = user_access_module @project = project @cluster = cluster @params = params end end ================================================ FILE: core/app/contexts/uffizzi_core/project_context.rb ================================================ # frozen_string_literal: true class UffizziCore::ProjectContext attr_reader :user, :user_access_module, :project, :account, :params def initialize(user, user_access_module, project, account, params) @user = user @user_access_module = user_access_module @account = account @project = project @params = params end end ================================================ FILE: core/app/contexts/uffizzi_core/webhooks_context.rb ================================================ # frozen_string_literal: true class UffizziCore::WebhooksContext attr_reader :request def initialize(request) @request = request end end ================================================ FILE: core/app/controller_modules/uffizzi_core/api/cli/v1/accounts_controller_module.rb ================================================ # frozen_string_literal: true module UffizziCore::Api::Cli::V1::AccountsControllerModule def update; end end ================================================ FILE: core/app/controller_modules/uffizzi_core/api/cli/v1/projects/clusters_controller_module.rb ================================================ # frozen_string_literal: true module UffizziCore::Api::Cli::V1::Projects::ClustersControllerModule private def update_show_trial_quota_exceeded_warning; end def check_account_quota; end def check_current_plan; end end ================================================ FILE: core/app/controller_modules/uffizzi_core/api/cli/v1/projects/deployments_controller_module.rb ================================================ # frozen_string_literal: true module UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerModule private def check_account_quota; end def update_show_trial_quota_exceeded_warning; end def check_current_plan; end end ================================================ FILE: core/app/controller_modules/uffizzi_core/api/cli/v1/projects_controller_module.rb ================================================ # frozen_string_literal: true module UffizziCore::Api::Cli::V1::ProjectsControllerModule private def update_show_trial_quota_exceeded_warning; end end ================================================ FILE: core/app/controllers/concerns/uffizzi_core/auth_management.rb ================================================ # frozen_string_literal: true module UffizziCore::AuthManagement def sign_in(user) session[:user_id] = user.id end def sign_out session[:user_id] = @current_user = nil end def signed_in? session[:user_id].present? && current_user.present? end def current_user @current_user ||= UffizziCore::User.find_by(id: current_user_id) end def auth_token header = request.headers['Authorization'] header&.split(' ')&.last end def current_user_id return session[:user_id] if session[:user_id].present? return unless auth_token.present? decoded_token = UffizziCore::TokenService.decode(auth_token) return unless decoded_token return if decoded_token.first['expires_at'] < DateTime.now decoded_token.first['user_id'] end def authenticate_request! current_user ? true : head(:unauthorized) end end ================================================ FILE: core/app/controllers/concerns/uffizzi_core/authorization_concern.rb ================================================ # frozen_string_literal: true module UffizziCore::AuthorizationConcern extend ActiveSupport::Concern included do before_action :init_authorize end def init_authorize return unless self.class.ancestors.include?(UffizziCore::ApplicationController) self.class.send(:define_method, policy_method_name) { send(:authorize, policy_method_params) } end def pundit_user policy_context end private def policy_method_name [:authorize, policy_name].join('_') end def policy_name controller_class = self.class.to_s controller_class.gsub(/::|Controller/, '').underscore end def policy_method_params controller_class = self.class.to_s params = controller_class.gsub(/Controller/, '').split('::') params.map(&:underscore).map(&:downcase).map(&:to_sym) end end ================================================ FILE: core/app/controllers/concerns/uffizzi_core/dependency_injection_concern.rb ================================================ # frozen_string_literal: true module UffizziCore::DependencyInjectionConcern extend ActiveSupport::Concern class_methods do def include_module_if_exists(module_name) include(Object.const_get(module_name)) if Object.const_defined?(module_name) end def prepend_module_if_exists(module_name) prepend(Object.const_get(module_name)) if Object.const_defined?(module_name) end end def user_access_module return unless module_exists?(:rbac) UffizziCore::UserAccessService.new(module_class(:rbac)) end def find_build_parser_module module_class(:build_parser) end def find_volume_parser_module module_class(:volume_parser) end def ci_module return unless module_exists?(:ci_module) module_class(:ci_module) end def ci_session return unless module_exists?(:ci_session) module_class(:ci_session) end def password_protection_module return unless module_exists?(:password_protection) module_class(:password_protection) end def find_ingress_parser_module module_class(:ingress_parser) end def notification_module return unless module_exists?(:notification_module) module_class(:notification_module) end def domain_module return unless module_exists?(:domain_module) module_class(:domain_module) end def deployment_memory_module return unless module_exists?(:deployment_memory_module) module_class(:deployment_memory_module) end def template_memory_module return unless module_exists?(:template_memory_module) module_class(:template_memory_module) end def controller_settings_service return unless module_exists?(:controller_settings) module_class(:controller_settings) end private def module_exists?(module_name) module_class(module_name).present? end def module_class(module_name) UffizziCore.dependencies[module_name]&.constantize end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/accounts/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ApplicationController < UffizziCore::Api::Cli::V1::ApplicationController def resource_account @resource_account ||= current_user.accounts.find(params[:account_id]) end def policy_context UffizziCore::AccountContext.new(current_user, user_access_module, resource_account, params) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/accounts/clusters_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ClustersController < UffizziCore::Api::Cli::V1::Accounts::ApplicationController include UffizziCore::DependencyInjectionConcern before_action :authorize_uffizzi_core_api_cli_v1_accounts_clusters def index return respond_with(clusters_by_account.includes(:project)) if valid_request_from_ci_workflow? clusters = clusters_by_user.or(clusters_by_admin_projects) respond_with(clusters.includes(:project)) end private def valid_request_from_ci_workflow? ci_module.valid_request_from_ci_workflow?(params) end def clusters_by_admin_projects projects = UffizziCore::Project .active .joins(:user_projects) .where(account: resource_account) .where(user_projects: { role: UffizziCore::UserProject.role.admin, user: current_user }) UffizziCore::Cluster.enabled.where(project_id: projects.select(:id)) end def clusters_by_user UffizziCore::Cluster.enabled.by_projects(account_projects).deployed_by_user(current_user) end def clusters_by_account UffizziCore::Cluster.enabled.by_projects(account_projects) end def account_projects UffizziCore::Project.active.where(account: resource_account) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/accounts/credentials_controller.rb ================================================ # frozen_string_literal: true # @resource Account/Credential class UffizziCore::Api::Cli::V1::Accounts::CredentialsController < UffizziCore::Api::Cli::V1::Accounts::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_accounts_credentials # Get a list of accounts credential # # @path [GET] /api/cli/v1/account/credentials # # @parameter credential(required,body) [object] def index credentials = resource_account.credentials.pluck(:type) render json: { credentials: credentials }, status: :ok end # Create account credential # # @path [POST] /api/cli/v1/account/credentials # # @parameter credential(required,body) [object] # @response [object] 201 Created successfully # @response [object] 422 Unprocessable entity # # @example # Possible types: # UffizziCore::Credential::Amazon, UffizziCore::Credential::Azure, UffizziCore::Credential::DockerHub, # UffizziCore::Credential::DockerRegistry, UffizziCore::Credential::Google, UffizziCore::Credential::GithubContainerRegistry def create credential_form = UffizziCore::Api::Cli::V1::Account::Credential::CreateForm.new credential_form.assign_attributes(credential_params) credential_form.account = resource_account credential_form.registry_url = registry_url(credential_form) credential_form.username = '_json_key' if credential_form.google? credential_form.activate UffizziCore::Account::CreateCredentialJob.perform_async(credential_form.id) if credential_form.save respond_with credential_form end # Update existing credential of the given type # # @path [PUT] /api/cli/v1/account/credentials/{type} # # @parameter type(required,path) [string] Credential type # @parameter credential(required,body) [object] # @response [object] 200 OK # @response [object] 422 Unprocessable entity def update credential = resource_account.credentials.find_by!(type: params[:type]) # Called every pipeline run from CLI with the --update-if-exists-option return respond_with credential unless credential_changed?(credential, credential_params) credential_form = credential.becomes(UffizziCore::Api::Cli::V1::Account::Credential::UpdateForm) credential_form.assign_attributes(credential_params) if credential_form.save UffizziCore::Account::UpdateCredentialJob.perform_async(credential_form.id) respond_with credential_form else respond_with credential_form, status: :unprocessable_entity end end # Check if credential of the type already exists in the account # # @path [GET] /api/cli/v1/account/credentials/{type}/check_credential # # @parameter credential(required,body) [object] # @response 422 Unprocessable entity # @response 200 OK def check_credential credential_form = UffizziCore::Api::Cli::V1::Account::Credential::CheckCredentialForm.new credential_form.type = params[:type] credential_form.account = resource_account if credential_form.valid? respond_with credential_form else respond_with credential_form.errors, status: :unprocessable_entity end end # Delete account credential # # @path [DELETE] /api/cli/v1/account/credentials/{type} # # @parameter type(required,path) [string] Type of the credential # @response 204 No Content # @response 401 Not authorized # @response [object>] 404 Not found def destroy credential = resource_account.credentials.find_by!(type: params[:type]) credential.destroy end private def credential_params params.require(:credential) end def registry_url(credential_form) if credential_form.docker_hub? Settings.docker_hub.registry_url elsif credential_form.google? Settings.google.registry_url elsif credential_form.github_container_registry? Settings.github_container_registry.registry_url else credential_form.registry_url end end def credential_changed?(credential, credential_params) credential.password != credential_params[:password] || credential.username != credential_params[:username] || credential.registry_url != credential_params[:registry_url] end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/accounts/projects_controller.rb ================================================ # frozen_string_literal: true # @resource Project class UffizziCore::Api::Cli::V1::Accounts::ProjectsController < UffizziCore::Api::Cli::V1::Accounts::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_accounts_projects def index projects = resource_account.projects.active respond_with projects, each_serializer: UffizziCore::Api::Cli::V1::ShortProjectSerializer end # Create a project # # @path [POST] /api/cli/v1/accounts/{account_id}/projects # @parameter params(required,body) [object] # # @response > 200 OK # @response 404 Not Found # @response 401 Not authorized # @response [object>] 422 Unprocessable entity def create project_form = UffizziCore::Api::Cli::V1::Project::CreateForm.new(project_params) project_form.account = resource_account UffizziCore::ProjectService.add_users_to_project!(project_form, project_form.account) if project_form.save respond_with project_form end private def project_params params.require(:project) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/accounts_controller.rb ================================================ # frozen_string_literal: true # @resource Project class UffizziCore::Api::Cli::V1::AccountsController < UffizziCore::Api::Cli::V1::ApplicationController include UffizziCore::Api::Cli::V1::AccountsControllerModule before_action :authorize_uffizzi_core_api_cli_v1_accounts # Get accounts of current user # # @path [GET] /api/cli/v1/accounts # # @response [object> >] 200 OK # @response 401 Not authorized def index accounts = current_user.accounts.order(name: :desc) respond_with accounts end # Get account by name # # @path [GET] /api/cli/v1/accounts/{name} # # @response [object>>> >] 200 OK # @response 401 Not authorized def show respond_with resource_account end private def policy_context account = resource_account || current_user.default_account UffizziCore::AccountContext.new(current_user, user_access_module, account, params) end def resource_account @resource_account ||= if params[:name] current_user.accounts.find_by!(name: params[:name]) else current_user.default_account end end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ApplicationController < UffizziCore::ApplicationController before_action :authenticate_request! def resource_project @resource_project ||= current_user.projects.find_by!(slug: params[:slug]) end def resource_account @resource_account ||= resource_project.account end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/ci/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Ci::ApplicationController < UffizziCore::Api::Cli::V1::ApplicationController before_action :authenticate_request! end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/ci/sessions_controller.rb ================================================ # frozen_string_literal: true # @resource Uffizzi class UffizziCore::Api::Cli::V1::Ci::SessionsController < UffizziCore::Api::Cli::V1::Ci::ApplicationController skip_before_action :authenticate_request!, only: [:create] # Create session # # @path [POST] /api/cli/v1/github/session # # @parameter user(required,body) [object] # @response [object] 201 Created successfully # @response [object>] 422 Unprocessable entity def create return render json: { errors: { title: [I18n.t('session.unsupported_login_type')] } }, status: :unprocessable_entity unless ci_session session_data, errors = ci_session.session_data_from_ci(user_params) return render json: { errors: errors }, status: :unprocessable_entity if errors.present? sign_in(session_data[:user]) data = { account_id: session_data[:account_id], project_slug: session_data[:project_slug], } render json: data, status: :created end private def user_params params.require(:user).permit(:token, :github_access_token) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ApplicationController < UffizziCore::Api::Cli::V1::ApplicationController def resource_project @resource_project ||= current_user.projects.find_by!(slug: params[:project_slug]) end def resource_account @resource_account ||= resource_project.account end def policy_context UffizziCore::ProjectContext.new(current_user, user_access_module, resource_project, resource_account, params) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/clusters/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Clusters::ApplicationController < UffizziCore::Api::Cli::V1::Projects::ApplicationController def resource_cluster @resource_cluster ||= if request_by_admin? || valid_request_from_ci_workflow? active_project_clusters.find_by!(name: params[:cluster_name]) else active_project_clusters.deployed_by_user(current_user).find_by!(name: params[:cluster_name]) end end private def active_project_clusters @active_project_clusters ||= resource_project.clusters.enabled end def request_by_admin? current_user.admin_access_to_project?(resource_project) end def valid_request_from_ci_workflow? ci_module.valid_request_from_ci_workflow?(params) end def policy_context UffizziCore::Project::ClusterContext.new(current_user, resource_project, user_access_module, resource_cluster, params) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/clusters/ingresses_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Clusters::IngressesController < UffizziCore::Api::Cli::V1::Projects::Clusters::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_clusters_ingresses def index hosts = UffizziCore::ControllerService.ingress_hosts(resource_cluster) user_hosts = UffizziCore::ClusterService.filter_user_ingress_host(resource_cluster, hosts) data = { ingresses: user_hosts, } respond_with data end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/clusters_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ClustersController < UffizziCore::Api::Cli::V1::Projects::ApplicationController include UffizziCore::Api::Cli::V1::Projects::ClustersControllerModule before_action :authorize_uffizzi_core_api_cli_v1_projects_clusters before_action :check_account_quota, only: [:create] before_action :check_current_plan after_action :update_show_trial_quota_exceeded_warning, only: [:create, :destroy] def index clusters = resource_project.clusters.enabled return respond_with clusters if request_by_admin? || valid_request_from_ci_workflow? respond_with clusters.deployed_by_user(current_user), each_serializer: UffizziCore::Api::Cli::V1::Projects::ShortClusterSerializer end def create version = cluster_params[:k8s_version] kubernetes_distribution = find_kubernetes_distribution(version) return render_distribution_version_error(version) if kubernetes_distribution.blank? cluster_form = UffizziCore::Api::Cli::V1::Cluster::CreateForm.new(cluster_params) cluster_form.project = resource_project cluster_form.deployed_by = current_user cluster_form.kubernetes_distribution = kubernetes_distribution return respond_with cluster_form unless cluster_form.save UffizziCore::ClusterService.start_deploy(cluster_form) respond_with cluster_form end def scale_down if resource_cluster.deployed? UffizziCore::ClusterService.scale_down!(resource_cluster) return respond_with resource_cluster end return render_scale_error(I18n.t('cluster.already_asleep', name: resource_cluster.name)) if resource_cluster.scaled_down? if resource_cluster.deploying_namespace? || resource_cluster.deploying? render_scale_error(I18n.t('cluster.deploy_in_process', name: resource_cluster.name)) end rescue AASM::InvalidTransition, UffizziCore::ClusterScaleError => e render_scale_error(e.message) end def scale_up if resource_cluster.scaled_down? UffizziCore::ClusterService.scale_up!(resource_cluster) return respond_with resource_cluster end return render_scale_error(I18n.t('cluster.already_awake', name: resource_cluster.name)) if resource_cluster.deployed? rescue AASM::InvalidTransition, UffizziCore::ClusterScaleError => e render_scale_error(e.message) end def show respond_with resource_cluster end def sync cluster_form = resource_cluster.becomes(UffizziCore::Api::Cli::V1::Cluster::SyncForm) cluster_form.sync_status cluster_form.save respond_with cluster_form end def destroy resource_cluster.disable! head(:no_content) end private def resource_cluster active_project_clusters = resource_project.clusters.enabled @resource_cluster ||= if request_by_admin? || valid_request_from_ci_workflow? active_project_clusters.find_by!(name: params[:name]) else active_project_clusters.deployed_by_user(current_user).find_by!(name: params[:name]) end end def request_by_admin? current_user.admin_access_to_project?(resource_project) end def valid_request_from_ci_workflow? ci_module.valid_request_from_ci_workflow?(params) end def cluster_params params.require(:cluster) end def render_scale_error(message) render json: { errors: { state: [message] } }, status: :unprocessable_entity end def find_kubernetes_distribution(version) return UffizziCore::KubernetesDistribution.default if version.blank? UffizziCore::KubernetesDistribution.find_by(version: version) end def render_distribution_version_error(version) available_versions = UffizziCore::KubernetesDistribution.pluck(:version).join(', ') message = I18n.t('kubernetes_distribution.not_available', version: version, available_versions: available_versions) render json: { errors: { kubernetes_distribution: [message] } }, status: :unprocessable_entity end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/compose_files_controller.rb ================================================ # frozen_string_literal: true # @resource ComposeFile class UffizziCore::Api::Cli::V1::Projects::ComposeFilesController < UffizziCore::Api::Cli::V1::Projects::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_compose_files # Get the compose file for the project # # @path [GET] /api/cli/v1/projects/{project_slug}/compose_file # # @parameter project_slug(required,path) [string] The project slug # # @response [ComposeFile] 200 OK # @response 401 Not authorized # @response [object>] 404 Not found def show respond_with compose_file end # Create a compose file for the project # # @path [POST] /api/cli/v1/projects/{project_slug}/compose_file # # @parameter project_slug(required,path) [string] The project slug # @parameter params(required,body) [object < # compose_file: object, # dependencies: Array>>] # # @response [ComposeFile] 201 OK # @response 422 A compose file already exists for this project # @response [ComposeFile] 422 Invalid compose file # @response 401 Not authorized def create params = { project: resource_project, user: current_user, compose_file_params: compose_file_params, dependencies: dependencies_params[:dependencies] || [], } compose_file_form, errors = create_or_update_compose_file(params) return render_errors(errors) if errors.present? respond_with compose_file_form end # Delete the compose file for the project # # @path [DELETE] /api/cli/v1/projects/{project_slug}/compose_file # # @parameter project_slug(required,path) [string] The project slug # # @response 204 No Content # @response 401 Not authorized def destroy compose_file.destroy head :no_content end private def compose_file compose_file = resource_project.compose_file raise ActiveRecord::RecordNotFound.new("Couldn't find UffizziCore::ComposeFile", UffizziCore::ComposeFile) if compose_file.blank? compose_file end def compose_file_params params.require(:compose_file) end def dependencies_params params.permit(dependencies: [:path, :source, :content, :use_kind, :is_file]) end def create_or_update_compose_file(params) existing_compose_file = resource_project.compose_file if existing_compose_file.present? UffizziCore::ComposeFileService.update(existing_compose_file, params) else kind = UffizziCore::ComposeFile.kind.main UffizziCore::ComposeFileService.create(params, kind) end end def render_errors(errors) json = { errors: errors } render json: json, status: :unprocessable_entity end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/activity_items_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ActivityItemsController < UffizziCore::Api::Cli::V1::Projects::Deployments::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_deployments_activity_items # Get activity items for a deployment # # @path [GET] /api/cli/v1/projects/{project_slug}/deployment/{deployment_id}/actiivity_items # # @parameter project_slug(required,path) [string] The project slug # @parameter deployment_d(required,path) [integer] The id of the deployment # # @response [ActivtyItem] 200 OK # @response 401 Not authorized # @response 404 Not found def index deployment = resource_project.deployments.enabled.find(params[:deployment_id]) unless deployment.active? return render json: { errors: { title: [I18n.t('deployment.invalid_state', id: deployment.id, state: deployment.state)] } }, status: :unprocessable_entity end activity_items = deployment .activity_items .page(page) .per(per_page) .order(updated_at: :desc) .ransack(q_param) .result meta = meta(activity_items) activity_items = activity_items.map do |activity_item| UffizziCore::Api::Cli::V1::Projects::Deployments::ActivityItemSerializer.new(activity_item).as_json end render json: { activity_items: activity_items, meta: meta, } end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ApplicationController < UffizziCore::Api::Cli::V1::Projects::ApplicationController def resource_deployment @resource_deployment ||= resource_project.deployments.active.find(params[:deployment_id]) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/containers/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::Containers::ApplicationController < UffizziCore::Api::Cli::V1::Projects::Deployments::ApplicationController def resource_container @resource_container ||= resource_deployment.active_containers.find_by!(service_name: params[:container_name]) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/containers/logs_controller.rb ================================================ # frozen_string_literal: true # @resource Project/Deployment/Container/Log class UffizziCore::Api::Cli::V1::Projects::Deployments::Containers::LogsController < UffizziCore::Api::Cli::V1::Projects::Deployments::Containers::ApplicationController # @path [GET] /api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/containers/{container_name}/logs # # @parameter project_slug(required,path) [string] The slug of the project # @parameter deployment_id(required,path) [integer] The id of the deployment # @parameter container_name(required,path) [integer] The name of the container # # @response [object >>] 200 OK # @response [object>] 404 Not found # @response 401 Not authorized def index response = UffizziCore::LogsService.fetch_container_logs(resource_container, logs_params) render json: response end private def logs_params params.permit(:limit, :previous) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/containers_controller.rb ================================================ # frozen_string_literal: true # @resource Container class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainersController < UffizziCore::Api::Cli::V1::Projects::Deployments::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_deployments_containers # Get a list of container services for a deployment # # @path [GET] /api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/containers # # @parameter project_slug(required,path) [string] The project slug # @parameter deployment_id(required,path) [integer] The id of the deployment # # @response [Container] 200 OK # @response 401 Not authorized # @response 404 Not found def index containers = resource_deployment.containers.active respond_with containers end def k8s_container_description deployment ||= resource_project.deployments.enabled.find(params[:deployment_id]) container = deployment.containers.active.find_by!(service_name: params[:container_name]) last_state = UffizziCore::ContainerService.last_state(container) render json: { last_state: last_state } end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments/events_controller.rb ================================================ # frozen_string_literal: true # @resource Event class UffizziCore::Api::Cli::V1::Projects::Deployments::EventsController < UffizziCore::Api::Cli::V1::Projects::Deployments::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_deployments_events # Get the events associated with deployment # # @path [GET] /api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/events # # @parameter project_slug(required,path) [string] The project_slug for the project # @parameter deployment_id(required,path) [integer] The id of the deployment # # @response [object< # events: Array # > >] 200 OK # # @response 401 Not authorized # @response 404 Not found def index response = UffizziCore::ControllerService.fetch_deployment_events(resource_deployment) events = { events: response } render json: events end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/deployments_controller.rb ================================================ # frozen_string_literal: true # @resource Deployment class UffizziCore::Api::Cli::V1::Projects::DeploymentsController < UffizziCore::Api::Cli::V1::Projects::ApplicationController include UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerModule before_action :authorize_uffizzi_core_api_cli_v1_projects_deployments before_action :check_account_quota, only: :create before_action :check_current_plan after_action :update_show_trial_quota_exceeded_warning, only: :destroy # Get a list of active deployements for a project # # @path [GET] /api/cli/v1/projects/{project_slug}/deployments # # @parameter project_slug(required,path) [string] The project slug # # @response [Array] 200 OK # @response 401 Not authorized def index search_labels = JSON.parse(q_param) filtered_deployments = deployments.with_labels(search_labels) respond_with filtered_deployments, each_serializer: UffizziCore::Api::Cli::V1::Projects::DeploymentsSerializer end # Get deployment information by id # # @path [GET] /api/cli/v1/projects/{project_slug}/deployments/{id} # # @parameter project_slug(required,path) [string] The project slug # # @response [Deployment] 200 OK # @response [object>] 404 Not found # @response 401 Not authorized def show respond_with deployment end # Create a deployment from a compose file # # @path [POST] /api/cli/v1/projects/{project_slug}/deployments # # @parameter project_slug(required,path) [string] The project slug # @parameter params(required,body) [object< # compose_file: object, # dependencies: Array>>] # # @response [Deployment] 201 OK # @response [object>] 422 Unprocessable Entity # @response [object>] 404 Not found # @response 401 Not authorized def create return render_deployment_exists_error if deployments.with_metadata.with_labels(metadata_params).exists? compose_file, errors = find_or_create_compose_file return render_invalid_file if compose_file.invalid_file? return render_errors(errors) if errors.present? errors = check_credentials(compose_file) return render_errors(errors) if errors.present? params = { metadata: metadata_params, creation_source: creation_source_params, } deployment = UffizziCore::DeploymentService.create_from_compose(compose_file, resource_project, current_user, params) respond_with deployment end # Update the deployment with new compose file # # @path [PUT] /api/cli/v1/projects/{project_slug}/deployments/{id}" # # @parameter project_slug(required,path) [string] The project slug # @parameter params(required,body) [object< # compose_file: object, # dependencies: Array>>] # # @response [Deployment] 201 OK # @response [object>] 422 Unprocessable Entity # @response [object>] 404 Not found # @response 401 Not authorized def update compose_file, errors = UffizziCore::ComposeFileService.create_temporary_compose( resource_project, current_user, compose_file_params, dependencies_params[:dependencies], ) return render_invalid_file if compose_file.invalid_file? return render_errors(errors) if errors.present? errors = check_credentials(compose_file) return render_errors(errors) if errors.present? deployment = deployments.find(params[:id]) updated_deployment = UffizziCore::DeploymentService.update_from_compose(compose_file, resource_project, current_user, deployment, metadata_params) respond_with updated_deployment end # @path [POST] /api/cli/v1/projects/{project_slug}/deployments/{id}/deploy_containers # # @parameter project_slug(required,path) [string] The project slug # @parameter id(required,path) [string] The id of the deployment # # @response 204 No Content # @response [object>] 404 Not found # @response 401 Not authorized def deploy_containers deployment = resource_project.deployments.active.find(params[:id]) deployment.update(deployed_by: current_user) resource_project.config_files.by_deployment(deployment).each do |config_file| UffizziCore::ConfigFile::ApplyJob.perform_async(deployment.id, config_file.id) end UffizziCore::Deployment::DeployContainersJob.perform_async(deployment.id) end # Disable deployment by id # # @path [DELETE] /api/cli/v1/projects/{project_slug}/deployments/{id} # # @parameter project_slug(required,path) [string] The project slug # # @response 204 No Content # @response 401 Not authorized def destroy UffizziCore::DeploymentService.disable!(deployment) deployment.deployment_events.create!(deployment_state: deployment.state, message: 'Destroyed by CLI') head :no_content end private def deployment @deployment ||= deployments.find(params[:id]) end def find_or_create_compose_file existing_compose_file = resource_project.compose_file if compose_file_params.present? UffizziCore::ComposeFileService.create_temporary_compose( resource_project, current_user, compose_file_params, dependencies_params[:dependencies], ) else raise ActiveRecord::RecordNotFound if existing_compose_file.blank? errors = [] [existing_compose_file, errors] end end def check_credentials(compose_file) credentials = resource_project.account.credentials check_credentials_form = UffizziCore::Api::Cli::V1::ComposeFile::CheckCredentialsForm.new check_credentials_form.compose_file = compose_file check_credentials_form.credentials = credentials return check_credentials_form.errors if check_credentials_form.invalid? nil end def deployments @deployments ||= resource_project.deployments.enabled end def deployment_params params.required(:deployment) end def compose_file_params params[:compose_file] end def dependencies_params params.permit(dependencies: [:path, :source, :content, :use_kind, :is_file]) end def metadata_params params[:metadata] end def creation_source_params params[:creation_source] end def render_invalid_file render json: { errors: { state: [I18n.t('compose.invalid_compose')] } }, status: :unprocessable_entity end def render_deployment_exists_error render json: { errors: { state: [I18n.t('deployment.already_exists')] } }, status: :unprocessable_entity end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects/secrets_controller.rb ================================================ # frozen_string_literal: true # @resource Project/Secrets class UffizziCore::Api::Cli::V1::Projects::SecretsController < UffizziCore::Api::Cli::V1::Projects::ApplicationController before_action :authorize_uffizzi_core_api_cli_v1_projects_secrets # Get secrets for the project # # @path [GET] /api/cli/v1/projects/{project_slug}/secrets # @parameter project_slug(required,path) [string] # @response [object>>] 200 OK # @response 401 Not authorized def index respond_with resource_project.secrets, root: :secrets end # Add secret to project # # @path [POST] /api/cli/v1/projects/{project_slug}/secrets/bulk_create # @parameter project_slug(required,path) [string] # @parameter secrets(required,body) [object>>] # @response [object>>] 201 Created # @response 422 A compose file already exists for this project # @response 401 Not authorized def bulk_create secrets_form = UffizziCore::Api::Cli::V1::Secret::BulkAssignForm.new secrets_form.secrets = resource_project.secrets secrets_form.assign_secrets(secrets_params) return respond_with secrets_form unless secrets_form.valid? resource_project.secrets.replace(secrets_form.secrets) UffizziCore::ProjectService.update_compose_secrets(resource_project) respond_with resource_project.secrets, root: :secrets end # Delete a secret from project by secret name # # @path [DELETE] /api/cli/v1/projects/{project_slug}/secrets/{secret_name} # @parameter project_slug(required,path) [string] # @response [Project] 200 OK # @response 404 # @response 401 Not authorized def destroy secret_name = CGI.unescape(params[:id]) secret = resource_project.secrets.find_by!(name: secret_name) UffizziCore::ProjectService.update_compose_secret_errors(resource_project, secret) secret.destroy head :no_content end private def secrets_params params.require(:secrets) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/projects_controller.rb ================================================ # frozen_string_literal: true # @resource Project class UffizziCore::Api::Cli::V1::ProjectsController < UffizziCore::Api::Cli::V1::ApplicationController include UffizziCore::Api::Cli::V1::ProjectsControllerModule before_action :authorize_uffizzi_core_api_cli_v1_projects after_action :update_show_trial_quota_exceeded_warning, only: :destroy # Get projects of current user # # @path [GET] /api/cli/v1/projects # # @response [object> >] 200 OK # @response 401 Not authorized def index projects = current_user.projects.active.order(updated_at: :desc) respond_with projects, each_serializer: UffizziCore::Api::Cli::V1::ShortProjectSerializer end # Get a project by slug # # @path [GET] /api/cli/v1/projects/{slug} # # @response > 200 OK # @response 404 Not Found # @response 401 Not authorized def show respond_with resource_project end # Delete a project # # @path [DELETE] /api/cli/v1/projects/{slug} # # @response 204 No content # @response 404 Not Found # @response 401 Not authorized def destroy resource_project.disable! head :no_content end private def project_params params.require(:project) end def policy_context current_project = current_user.projects.find_by(slug: params[:slug]) account = current_project&.account || current_user.default_account UffizziCore::AccountContext.new(current_user, user_access_module, account, params) end end ================================================ FILE: core/app/controllers/uffizzi_core/api/cli/v1/sessions_controller.rb ================================================ # frozen_string_literal: true # @resource Uffizzi class UffizziCore::Api::Cli::V1::SessionsController < UffizziCore::Api::Cli::V1::ApplicationController skip_before_action :authenticate_request!, only: [:create] # Create session # # @path [POST] /api/cli/v1/session # # @parameter user(required,body) [object] # @response [object> >>] 201 Created successfully # @response [object>] 422 Unprocessable entity def create session_form = UffizziCore::Api::Cli::V1::SessionCreateForm.new(session_params) if session_form.valid? sign_in(session_form.user) return respond_with session_form.user end respond_with session_form end # Destroy session # # @path [DELETE] /api/cli/v1/session # # @response 204 No Content def destroy sign_out head :no_content end private def session_params params.require(:user).permit(:email, :password) end end ================================================ FILE: core/app/controllers/uffizzi_core/application_controller.rb ================================================ # frozen_string_literal: true class UffizziCore::ApplicationController < ActionController::Base include Pundit::Authorization include UffizziCore::ResponseService include UffizziCore::AuthManagement include UffizziCore::AuthorizationConcern include UffizziCore::DependencyInjectionConcern DEFAULT_PAGE = 1 DEFAULT_PER_PAGE = 20 protect_from_forgery with: :exception RESCUABLE_EXCEPTIONS = [RuntimeError, TypeError, NameError, ArgumentError, SyntaxError].freeze unless Rails.env.test? rescue_from *RESCUABLE_EXCEPTIONS do |exception| render_server_error(exception) end end rescue_from ActiveRecord::RecordNotFound do |exception| render_not_found(exception) end rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized before_action :authenticate_request! skip_before_action :verify_authenticity_token respond_to :json def render_not_authorized render json: { errors: { title: [I18n.t('session.unauthorized')] } }, status: :forbidden end def policy_context UffizziCore::BaseContext.new(current_user, user_access_module, params) end def self.responder UffizziCore::JsonResponder end def render_not_found(exception) resource = exception.model || 'Resource' render json: { errors: { title: ["#{resource} Not Found"] } }, status: :not_found end def render_server_error(error) render json: { errors: { title: [error] } }, status: :internal_server_error end def render_errors(errors) json = { errors: errors } render json: json, status: :unprocessable_entity end def q_param params[:q] || ActionController::Parameters.new end def page params[:page] || DEFAULT_PAGE end def per_page params[:per_page] || DEFAULT_PER_PAGE end end ================================================ FILE: core/app/errors/uffizzi_core/cluster_scale_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ClusterScaleError < StandardError def initialize(action) message = I18n.t('cluster.scaling_failed', action: action) super(message) end end ================================================ FILE: core/app/errors/uffizzi_core/compose_file/build_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::BuildError < UffizziCore::ComposeFileError def initialize(message, extra_errors = {}) error_key = UffizziCore::ComposeFile::ErrorsService::TEMPLATE_BUILD_ERROR_KEY super(message, error_key, extra_errors) end end ================================================ FILE: core/app/errors/uffizzi_core/compose_file/credential_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::CredentialError < StandardError end ================================================ FILE: core/app/errors/uffizzi_core/compose_file/parse_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::ParseError < StandardError end ================================================ FILE: core/app/errors/uffizzi_core/compose_file/secrets_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::SecretsError < UffizziCore::ComposeFileError def initialize(message, extra_errors = {}) error_key = UffizziCore::ComposeFile::ErrorsService::SECRETS_ERROR_KEY super(message, error_key, extra_errors) end end ================================================ FILE: core/app/errors/uffizzi_core/compose_file_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFileError < StandardError attr_reader :errors def initialize(message, error_key = nil, extra_errors = {}) if [NilClass, String].exclude?(error_key.class) raise StandardError.new("#{self.class} arg 'error_key' should be a #{String} or #{NilClass}") end unless extra_errors.is_a?(Hash) raise StandardError.new("#{self.class} arg 'extra_errors' should be a #{Hash}") end @errors = error_key.nil? ? {} : { error_key => message.to_s }.merge(extra_errors) super(message) end end ================================================ FILE: core/app/errors/uffizzi_core/container_registry_error.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistryError < StandardError attr_reader :errors, :error_key def initialize(response) prepared_errors = prepare_errors(response[:body], response[:status]) @error_key = UffizziCore::ComposeFile::ErrorsService::DOCKER_REGISTRY_CONTAINER_ERROR_KEY @errors = { @error_key => prepared_errors } super(prepared_errors.to_json) end private def prepare_errors(body, status) parsed_body = JSON.parse!(body.to_s) parsed_body.fetch('errors', parsed_body) rescue JSON::ParserError, TypeError msg = body.empty? ? I18n.t('registry.error', code: status) : body { message: msg } end end ================================================ FILE: core/app/errors/uffizzi_core/deployment/image_pull_error.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::ImagePullError < StandardError attr_reader :deployment_id def initialize(deployment_id) super @deployment_id = deployment_id end end ================================================ FILE: core/app/errors/uffizzi_core/deployment_not_found_error.rb ================================================ # frozen_string_literal: true class UffizziCore::DeploymentNotFoundError < StandardError attr_reader :deployment_id def initialize(deployment_id) super @deployment_id = deployment_id end end ================================================ FILE: core/app/errors/uffizzi_core/registry_not_supported_error.rb ================================================ # frozen_string_literal: true class UffizziCore::RegistryNotSupportedError < StandardError end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/account/credential/check_credential_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Account::Credential::CheckCredentialForm include UffizziCore::ApplicationFormWithoutActiveRecord attribute :type attribute :account validate :credential_exists? private def credential_exists? errors.add(:type, :exist) if account.credentials.by_type(type).exists? end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/account/credential/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Account::Credential::CreateForm < UffizziCore::Credential include UffizziCore::ApplicationForm permit :type, :registry_url, :username, :password validates :password, presence: { message: :password_blank } validate :check_registry_url, if: -> { errors[:password].empty? } validate :check_credential_correctness, if: -> { errors[:password].empty? } validate :credential_exists?, if: -> { errors[:password].empty? } private def check_registry_url errors.add(:registry_url, :invalid_scheme) if URI.parse(registry_url).scheme.nil? end def check_credential_correctness errors.add(:username, :incorrect, type: type.text) unless correct? end def credential_exists? errors.add(:type, :exist) if account.credentials.by_type(type).exists? end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/account/credential/update_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Account::Credential::UpdateForm < UffizziCore::Credential include UffizziCore::ApplicationForm permit :registry_url, :username, :password validates :password, presence: { message: :password_blank } validate :check_registry_url, if: -> { errors[:password].empty? } validate :check_credential_correctness, if: -> { errors[:password].empty? } private def check_registry_url errors.add(:registry_url, :invalid_scheme) if URI.parse(registry_url).scheme.nil? end def check_credential_correctness errors.add(:username, :incorrect) unless correct? end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/cluster/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Cluster::CreateForm < UffizziCore::Cluster include UffizziCore::ApplicationForm permit :name, :manifest, :creation_source validate :check_manifest, if: -> { manifest.present? } private def check_manifest YAML.load_stream(manifest) rescue Psych::SyntaxError => e err = [e.problem, e.context].compact.join(' ') errors.add(:manifest, err) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/cluster/sync_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Cluster::SyncForm < UffizziCore::Cluster include UffizziCore::ApplicationForm permit :state def sync_status cluster_data = UffizziCore::ControllerService.show_cluster(self) asleep_in_cluster = cluster_data.status.sleep return unless deployed? || scaled_down? return if actual_status?(asleep_in_cluster) self.state = asleep_in_cluster ? UffizziCore::Cluster::STATE_SCALED_DOWN : UffizziCore::Cluster::STATE_DEPLOYED self end private def actual_status?(asleep_in_cluster) (asleep_in_cluster && scaled_down?) || (!asleep_in_cluster && deployed?) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/compose_file/check_credentials_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ComposeFile::CheckCredentialsForm include UffizziCore::ApplicationFormWithoutActiveRecord attr_reader :type attribute :compose_file attribute :credentials validate :check_containers_credentials private def check_containers_credentials compose_content = Base64.decode64(compose_file.content) compose_payload = { compose_file: compose_file } compose_data = UffizziCore::ComposeFileService.parse(compose_content, compose_payload) containers = compose_data[:containers] containers.each do |container| container_registry_service = UffizziCore::ContainerRegistryService.init_by_container(container, credentials) @type = container_registry_service.type next if container_registry_service.image_available?(credentials) raise UffizziCore::ComposeFile::CredentialError.new(I18n.t('compose.unprocessable_image', value: type)) end rescue UffizziCore::ContainerRegistryError => e errors.add(:credentials, I18n.t('compose.unprocessable_image', value: type)) errors.add(e.error_key, e.message) rescue UffizziCore::ComposeFile::CredentialError => e errors.add(:credentials, e.message) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/compose_file/cli_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ComposeFile::CliForm include UffizziCore::ApplicationFormWithoutActiveRecord attribute :compose_content_data, Hash attribute :compose_data, Hash attribute :compose_dependencies, Array attribute :compose_repositories, Array attribute :content, String attribute :source_kind, Symbol attribute :compose_file, UffizziCore::ComposeFile validates :content, presence: true validates :compose_file, presence: true validate :check_compose_parsed_data, if: -> { errors[:content].empty? } def check_compose_parsed_data compose_content = Base64.decode64(content) compose_payload = { compose_file: compose_file } self.compose_data = UffizziCore::ComposeFileService.parse(compose_content, compose_payload) rescue UffizziCore::ComposeFile::ParseError => e errors.add(:content, e.message) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/compose_file/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ComposeFile::CreateForm < UffizziCore::ComposeFile include UffizziCore::ApplicationForm permit :source, :path, :content validates :source, presence: true validates :path, presence: true validates :content, presence: true end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/compose_file/template_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ComposeFile::TemplateForm include UffizziCore::ApplicationFormWithoutActiveRecord attribute :credentials attribute :project, UffizziCore::Project attribute :user, UffizziCore::User attribute :compose_data, Hash attribute :source, String attribute :template_attributes, Hash attribute :template_build_error, String attribute :compose_dependencies, Array attribute :compose_repositories, Array validate :check_template_attributes def assign_template_attributes! self.template_attributes = UffizziCore::ComposeFileService.build_template_attributes( compose_data, source, credentials, project, compose_dependencies, compose_repositories, ) rescue StandardError => e self.template_build_error = e end private def check_template_attributes readable_errors = [ UffizziCore::ComposeFile::SecretsError, UffizziCore::ComposeFile::BuildError, UffizziCore::ContainerRegistryError, ] if readable_errors.include?(template_build_error.class) template_build_error.errors.each { |k, v| errors.add(k, v) } return end raise template_build_error if template_build_error.is_a?(StandardError) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/compose_file/update_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ComposeFile::UpdateForm < UffizziCore::ComposeFile include UffizziCore::ApplicationForm permit :content, :source, :path validates :content, presence: true end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/config_file/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ConfigFile::CreateForm < UffizziCore::ConfigFile include UffizziCore::ApplicationForm permit :filename, :kind, :payload validates :filename, presence: true validates :kind, presence: true validates :payload, presence: true end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/deployment/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Deployment::CreateForm < UffizziCore::Deployment include UffizziCore::ApplicationForm include UffizziCore::DependencyInjectionConcern permit :creation_source, :metadata, containers_attributes: [ :image, :service_name, :tag, :full_image_name, :port, :public, :memory_limit, :memory_request, :entrypoint, :command, :receive_incoming_requests, :continuously_deploy, { variables: [:name, :value], additional_subdomains: [], secret_variables: [:name, :value], volumes: [:source, :target, :type, :read_only], healthcheck: [:test, :interval, :timeout, :retries, :start_period, :disable, { test: [] }], repo_attributes: [ :namespace, :name, :slug, :type, :description, :is_private, :repository_id, :branch, :kind, :dockerfile_path, :dockerfile_context_path, :deploy_preview_when_pull_request_is_opened, :delete_preview_when_pull_request_is_closed, :deploy_preview_when_image_tag_is_created, :delete_preview_when_image_tag_is_updated, :share_to_github, :delete_preview_after, { args: [:name, :value] }, ], container_config_files_attributes: [ :config_file_id, :mount_path, ], container_host_volume_files_attributes: [ :host_volume_file_id, :source_path, ] }, ] validate :check_all_containers_have_unique_ports validate :check_exists_ingress_container validate :check_secrets_exist_in_database validate :check_max_memory_limit def assign_dependences!(project, user) self.project = project self.containers = containers.map do |container| container.repo.project = project if !container.repo.nil? container end self.deployed_by = user self end private def check_all_containers_have_unique_ports active_containers = containers.select(&:active?) errors.add(:containers, :duplicate_ports) unless UffizziCore::DeploymentService.all_containers_have_unique_ports?(active_containers) end def check_exists_ingress_container active_containers = containers.select(&:active?) errors.add(:containers, :incorrect_ingress_container) unless UffizziCore::DeploymentService.ingress_container?(active_containers) end def check_secrets_exist_in_database return if compose_file.nil? existing_secrets = project.secrets.pluck('name') compose_file.template.payload['containers_attributes'] .map { |container| container['secret_variables'].map { |secret| secret['name'] } }.flatten.uniq .select { |secret| existing_secrets.exclude?(secret) } .each do |secret| error_message = I18n.t('compose.project_secret_not_found', secret: secret) errors.add(:secret_variables, error_message) end end def check_max_memory_limit return if deployment_memory_module.valid_memory_limit?(self) deployment_memory_module.memory_limit_error_message(self) errors.add(:containers, message) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/deployment/update_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Deployment::UpdateForm < UffizziCore::Deployment include UffizziCore::ApplicationForm include UffizziCore::DependencyInjectionConcern permit :metadata, containers_attributes: [ :image, :service_name, :tag, :port, :full_image_name, :public, :memory_limit, :memory_request, :entrypoint, :command, :receive_incoming_requests, :continuously_deploy, { variables: [:name, :value], additional_subdomains: [], secret_variables: [:name, :value], volumes: [:source, :target, :type, :read_only], healthcheck: [:test, :interval, :timeout, :retries, :start_period, :disable, { test: [] }], repo_attributes: [ :namespace, :name, :slug, :type, :description, :is_private, :repository_id, :branch, :kind, :dockerfile_path, :dockerfile_context_path, :deploy_preview_when_pull_request_is_opened, :delete_preview_when_pull_request_is_closed, :deploy_preview_when_image_tag_is_created, :delete_preview_when_image_tag_is_updated, :share_to_github, :delete_preview_after, { args: [:name, :value] }, ], container_config_files_attributes: [ :config_file_id, :mount_path, ], container_host_volume_files_attributes: [ :host_volume_file_id, :source_path, ] }, ] validate :check_all_containers_have_unique_ports validate :check_exists_ingress_container validate :check_max_memory_limit def assign_dependences!(project, user) self.project = project self.containers = containers.map do |container| container.repo.project = project if !container.repo.nil? container end self.deployed_by = user self end private def check_all_containers_have_unique_ports active_containers = containers.select(&:active?) errors.add(:containers, :duplicate_ports) unless UffizziCore::DeploymentService.all_containers_have_unique_ports?(active_containers) end def check_exists_ingress_container active_containers = containers.select(&:active?) errors.add(:containers, :incorrect_ingress_container) unless UffizziCore::DeploymentService.ingress_container?(active_containers) end def check_max_memory_limit return if deployment_memory_module.valid_memory_limit?(self) deployment_memory_module.memory_limit_error_message(self) errors.add(:containers, message) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/project/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Project::CreateForm < UffizziCore::Project include UffizziCore::ApplicationForm permit :name, :slug, :description end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/project/update_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Project::UpdateForm < UffizziCore::Project include UffizziCore::ApplicationForm permit :name, :slug, :description validates :name, presence: true, uniqueness: { scope: :account } validates :slug, presence: true, uniqueness: true end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/secret/bulk_assign_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Secret::BulkAssignForm include UffizziCore::ApplicationFormWithoutActiveRecord MAX_SECRET_KEY_LENGTH = 256 attribute :secrets, Array validate :check_duplicates validate :check_length def assign_secrets(new_secrets) return if new_secrets.blank? new_secrets.each do |new_secret| secret = UffizziCore::Secret.new(name: new_secret['name'], value: new_secret['value']) secrets.append(secret) end end private def check_duplicates duplicates = [] groupped_secrets = secrets.group_by { |secret| secret['name'] } groupped_secrets.each_pair do |key, value| duplicates << key if value.size > 1 end error_message = I18n.t('secrets.duplicates_exists', secrets: duplicates.join(', ')) errors.add(:secrets, error_message) if duplicates.present? end def check_length secrets_with_invalid_key_length = secrets.select { |secret| secret['name'].length > MAX_SECRET_KEY_LENGTH } error_message = I18n.t('secrets.invalid_key_length') errors.add(:secrets, error_message) if secrets_with_invalid_key_length.present? end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/session_create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::SessionCreateForm include UffizziCore::ApplicationFormWithoutActiveRecord attribute :email, String attribute :password, String validates :email, :password, presence: true validate :check_authenticate, if: :email def user @user ||= UffizziCore::User.active.by_email(email).first end def check_authenticate return unless wrong_email_or_password? errors.add(:password, 'Email or password is incorrect.') end private def wrong_email_or_password? return true if user.nil? !user.authenticate(password) end end ================================================ FILE: core/app/forms/uffizzi_core/api/cli/v1/template/create_form.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Template::CreateForm < UffizziCore::Template include UffizziCore::DependencyInjectionConcern validate :check_max_memory_limit def check_max_memory_limit return if template_memory_module.valid_memory_limit?(self) message = template_memory_module.memory_limit_error_message(self) errors.add(:payload, message) end end ================================================ FILE: core/app/forms/uffizzi_core/application_form.rb ================================================ # frozen_string_literal: true module UffizziCore::ApplicationForm extend ActiveSupport::Concern include UffizziCore::MassAssignmentControlConcern class_methods do delegate :model_name, :name, to: :superclass end end ================================================ FILE: core/app/forms/uffizzi_core/application_form_without_active_record.rb ================================================ # frozen_string_literal: true module UffizziCore::ApplicationFormWithoutActiveRecord extend ActiveSupport::Concern included do include ActiveModel::Validations include ActiveModel::Conversion include ActiveModel::Serialization include ActiveModel::Validations::Callbacks include ::Virtus.model end def persisted? false end end ================================================ FILE: core/app/forms/uffizzi_core/mass_assignment_control_concern.rb ================================================ # frozen_string_literal: true module UffizziCore::MassAssignmentControlConcern extend ActiveSupport::Concern class_methods do def permit(*args) @_args = args end def _args @_args end end def assign_attributes(attrs = ActionController::Parameters.new) attrs = ActionController::Parameters.new if attrs.nil? new_attrs = attrs.permit(*self.class._args) super(new_attrs) end end ================================================ FILE: core/app/helpers/uffizzi_core/application_helper.rb ================================================ # frozen_string_literal: true module UffizziCore module ApplicationHelper end end ================================================ FILE: core/app/jobs/uffizzi_core/account/create_credential_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Account::CreateCredentialJob < UffizziCore::ApplicationJob sidekiq_options queue: :accounts, retry: 5 def perform(id) credential = UffizziCore::Credential.find(id) UffizziCore::AccountService.create_credential(credential) end end ================================================ FILE: core/app/jobs/uffizzi_core/account/update_credential_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Account::UpdateCredentialJob < UffizziCore::ApplicationJob sidekiq_options queue: :accounts, retry: 5 def perform(id) credential = UffizziCore::Credential.find(id) UffizziCore::AccountService.update_credential(credential) end end ================================================ FILE: core/app/jobs/uffizzi_core/activity_item/docker/update_digest_job.rb ================================================ # frozen_string_literal: true class UffizziCore::ActivityItem::Docker::UpdateDigestJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: 5 def perform(id) activity_item = UffizziCore::ActivityItem.find(id) UffizziCore::ActivityItemService.update_docker_digest!(activity_item) end end ================================================ FILE: core/app/jobs/uffizzi_core/application_job.rb ================================================ # frozen_string_literal: true module UffizziCore class ApplicationJob include Sidekiq::Worker end end ================================================ FILE: core/app/jobs/uffizzi_core/cluster/delete_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster::DeleteJob < UffizziCore::ApplicationJob sidekiq_options queue: :clusters, lock: :until_executed, retry: Settings.default_job_retry_count def perform(id) Rails.logger.info("DEPLOYMENT_PROCESS cluster_id=#{id} DeleteJob") cluster = UffizziCore::Cluster.find(id) UffizziCore::ControllerService.delete_namespace(cluster) end end ================================================ FILE: core/app/jobs/uffizzi_core/cluster/deploy_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster::DeployJob < UffizziCore::ApplicationJob sidekiq_options queue: :clusters, retry: Settings.default_job_retry_count def perform(id) cluster = UffizziCore::Cluster.find(id) UffizziCore::ClusterService.deploy_cluster(cluster) end end ================================================ FILE: core/app/jobs/uffizzi_core/cluster/manage_deploying_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster::ManageDeployingJob < UffizziCore::ApplicationJob sidekiq_options queue: :clusters, retry: Settings.default_job_retry_count def perform(id, try = 1) cluster = UffizziCore::Cluster.find(id) UffizziCore::ClusterService.manage_deploying(cluster, try) end end ================================================ FILE: core/app/jobs/uffizzi_core/cluster/manage_scaling_down_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster::ManageScalingDownJob < UffizziCore::ApplicationJob sidekiq_options queue: :clusters, retry: Settings.default_job_retry_count def perform(id) cluster = UffizziCore::Cluster.find(id) UffizziCore::ClusterService.manage_scale_down(cluster) end end ================================================ FILE: core/app/jobs/uffizzi_core/cluster/manage_scaling_up_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster::ManageScalingUpJob < UffizziCore::ApplicationJob sidekiq_options queue: :clusters, retry: Settings.default_job_retry_count def perform(id, try = 1) cluster = UffizziCore::Cluster.find(id) UffizziCore::ClusterService.manage_scale_up(cluster, try) end end ================================================ FILE: core/app/jobs/uffizzi_core/config_file/apply_job.rb ================================================ # frozen_string_literal: true class UffizziCore::ConfigFile::ApplyJob < UffizziCore::ApplicationJob sidekiq_options queue: :config_files, retry: Settings.controller.resource_create_retry_count sidekiq_retry_in do |count, exception| case exception when UffizziCore::DeploymentNotFoundError Rails.logger.info("DEPLOYMENT_PROCESS ApplyJob retry deployment_id=#{exception.deployment_id} count=#{count}") Settings.controller.resource_create_retry_time else if [Settings.default_job_retry_count, Settings.controller.resource_create_retry_count].include?(count) Sentry.capture_exception(exception) :kill else Settings.controller.resource_create_retry_time end end end def perform(deployment_id, config_file_id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment_id} Apply ConfigFile(config_file_id:#{config_file_id})") deployment = UffizziCore::Deployment.find(deployment_id) config_file = UffizziCore::ConfigFile.find(config_file_id) if deployment.disabled? Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} deployment was disabled stop config file applying") return end unless UffizziCore::ControllerService.namespace_exists?(deployment) raise UffizziCore::DeploymentNotFoundError, deployment_id end UffizziCore::ControllerService.apply_config_file(deployment, config_file) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/create_credential_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::CreateCredentialJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: Settings.controller.resource_create_retry_count sidekiq_retry_in do |count, exception| case exception when UffizziCore::DeploymentNotFoundError Rails.logger.info("DEPLOYMENT_PROCESS CreateCredentialJob retry deployment_id=#{exception.deployment_id} count=#{count}") Settings.controller.resource_create_retry_time else if [Settings.default_job_retry_count, Settings.controller.resource_create_retry_count].include?(count) Sentry.capture_exception(exception) :kill else Settings.controller.resource_create_retry_time end end end def perform(deployment_id, credential_id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment_id} CreateCredentialJob(cred_id:#{credential_id})") deployment = UffizziCore::Deployment.find(deployment_id) credential = UffizziCore::Credential.find(credential_id) if deployment.disabled? Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} deployment was disabled stop creating credential") return end unless UffizziCore::ControllerService.namespace_exists?(deployment) raise UffizziCore::DeploymentNotFoundError, deployment_id end UffizziCore::ControllerService.apply_credential(deployment, credential) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/create_credentials_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::CreateCredentialsJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: 5 def perform(deployment_id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment_id} CreateCredentialsJob") deployment = UffizziCore::Deployment.find(deployment_id) credentials = deployment.project.account.credentials.deployable credentials.each do |credential| UffizziCore::Deployment::CreateCredentialJob.perform_async(deployment.id, credential.id) end end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/create_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::CreateJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: 5 def perform(id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{id} CreateJob") deployment = UffizziCore::Deployment.find(id) UffizziCore::ControllerService.create_namespace(deployment) UffizziCore::Deployment::CreateCredentialsJob.perform_async(deployment.id) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/delete_credential_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::DeleteCredentialJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: 5 def perform(deployment_id, credential_id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment_id} DeleteCredentialJob(cred_id:#{credential_id})") deployment = UffizziCore::Deployment.find(deployment_id) credential = UffizziCore::Credential.find(credential_id) UffizziCore::ControllerService.delete_credential(deployment, credential) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/delete_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::DeleteJob < UffizziCore::ApplicationJob sidekiq_options queue: :disable_deployments, lock: :until_executed, retry: Settings.default_job_retry_count def perform(id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{id} DeleteJob") deployment = UffizziCore::Deployment.find(id) UffizziCore::ControllerService.delete_namespace(deployment) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/deploy_containers_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::DeployContainersJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: Settings.controller.resource_create_retry_count sidekiq_retry_in do |count, exception| case exception when UffizziCore::DeploymentNotFoundError Rails.logger.info("DEPLOYMENT_PROCESS DeployContainersJob retry deployment_id=#{exception.deployment_id} count=#{count}") Settings.controller.resource_create_retry_time else if [Settings.default_job_retry_count, Settings.controller.resource_create_retry_count].include?(count) Sentry.capture_exception(exception) :kill else Settings.controller.resource_create_retry_time end end end def perform(id, repeated = false) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{id} DeployContainersJob") deployment = UffizziCore::Deployment.find(id) if deployment.disabled? Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} deployment was disabled stop deploying") return end raise UffizziCore::DeploymentNotFoundError, id unless UffizziCore::ControllerService.namespace_exists?(deployment) UffizziCore::DeploymentService.deploy_containers(deployment, repeated) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/manage_deploy_activity_item_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::ManageDeployActivityItemJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: Settings.default_job_retry_count sidekiq_retry_in do |count, exception| case exception when UffizziCore::Deployment::ImagePullError Rails.logger.info("DEPLOYMENT_PROCESS ManageDeployActivityItemJob retry deployment_id=#{exception.deployment_id} count=#{count}") Settings.controller.resource_create_retry_time when ActiveRecord::RecordNotFound :kill if exception.model.constantize == UffizziCore::ActivityItem else if count == Settings.default_job_retry_count Sentry.capture_exception(exception) :kill else Settings.controller.resource_create_retry_time end end end sidekiq_retries_exhausted do |msg, exception| case exception when UffizziCore::Deployment::ImagePullError Rails.logger.info("DEPLOYMENT_PROCESS ManageDeployActivityItemJob exhausted #{msg.inspect} #{exception.inspect}") activity_item_id = msg['args'].first activity_item = UffizziCore::ActivityItem.find(activity_item_id) UffizziCore::ActivityItemService.fail_deployment!(activity_item) end end def perform(activity_item_id) activity_item = UffizziCore::ActivityItem.find(activity_item_id) container = activity_item.container if container.disabled? logger_message = "DEPLOYMENT_PROCESS deployment_id=#{container.deployment_id} activity_item_id=#{activity_item.id} deployment was disabled stop monitoring" Rails.logger.info(logger_message) return end UffizziCore::ActivityItemService.manage_deploy_activity_item(activity_item) end end ================================================ FILE: core/app/jobs/uffizzi_core/deployment/update_credential_job.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::UpdateCredentialJob < UffizziCore::ApplicationJob sidekiq_options queue: :deployments, retry: Settings.controller.resource_update_retry_count sidekiq_retry_in do |count, exception| case exception when UffizziCore::DeploymentNotFoundError Rails.logger.info("DEPLOYMENT_PROCESS UpdateCredentialJob retry deployment_id=#{exception.deployment_id} count=#{count}") Settings.controller.resource_update_retry_time else if [Settings.default_job_retry_count, Settings.controller.resource_update_retry_count].include?(count) Sentry.capture_exception(exception) :kill else Settings.controller.resource_create_retry_time end end end def perform(deployment_id, credential_id) Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment_id} UpdateCredentialJob(cred_id:#{credential_id})") deployment = UffizziCore::Deployment.find(deployment_id) credential = UffizziCore::Credential.find(credential_id) if deployment.disabled? Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} deployment was disabled. Stop updating credential") return end unless UffizziCore::ControllerService.namespace_exists?(deployment) raise UffizziCore::DeploymentNotFoundError, deployment_id end UffizziCore::ControllerService.apply_credential(deployment, credential) end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/account.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Account extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength included do include AASM include UffizziCore::AccountRepo include UffizziCore::StateMachineConcern extend Enumerize self.table_name = UffizziCore.table_names[:accounts] enumerize :kind, in: [:personal, :organizational], scope: true, predicates: true validates :kind, presence: true validates :domain, uniqueness: true, if: :domain has_many :memberships, dependent: :destroy has_many :users, through: :memberships has_many :credentials, dependent: :destroy has_many :projects, dependent: :destroy has_many :deployments, through: :projects has_many :payments, dependent: :destroy has_many :clusters, through: :projects aasm(:sso_state) do state :connection_not_configured, initial: true state :connection_disabled state :connection_active event :activate_connection do transitions from: [:connection_not_configured, :connection_disabled], to: :connection_active end event :deactivate_connection do transitions from: [:connection_active], to: :connection_disabled end event :reset_connection do transitions from: [:connection_active, :connection_disabled], to: :connection_not_configured end end def update_payment_issue_date update(payment_issue_at: DateTime.current) end def active_projects projects.active end def disable_projects active_projects.each(&:disable_deployments) end # This method is deprecated. Don't use it. def user users.find_by(memberships: { role: UffizziCore::Membership.role.admin }) end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/activity_item.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::ActivityItem extend ActiveSupport::Concern included do include UffizziCore::ActivityItemRepo self.table_name = UffizziCore.table_names[:activity_items] belongs_to :deployment belongs_to :container belongs_to :build, optional: true has_many :events, dependent: :destroy def docker? type == UffizziCore::ActivityItem::Docker.name end def image [namespace, name].compact.join('/') end def full_image return "#{image}:#{tag}" if docker? '' end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/cluster.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Cluster extend ActiveSupport::Concern include UffizziCore::ClusterRepo NAMESPACE_PREFIX = 'c' # rubocop:disable Metrics/BlockLength included do include AASM extend Enumerize self.table_name = UffizziCore.table_names[:clusters] belongs_to :project, class_name: UffizziCore::Project.name belongs_to :deployed_by, class_name: UffizziCore::User.name, foreign_key: :deployed_by_id, optional: true validates_uniqueness_of :name, conditions: -> { enabled }, scope: :project_id validates :name, presence: true, format: { with: /\A[a-zA-Z0-9-]*\z/ } enumerize :creation_source, in: UffizziCore.cluster_creation_sources, scope: true, predicates: true attribute :creation_source, :string, default: :manual validates :creation_source, presence: true belongs_to :kubernetes_distribution, optional: true aasm(:state) do state :deploying_namespace, initial: true state :failed_deploy_namespace state :deploying state :deployed state :scaling_down state :scaled_down state :scaling_up state :failed_scale_up state :failed state :disabled event :start_deploying do transitions from: [:deploying_namespace], to: :deploying end event :fail_deploy_namespace do transitions from: [:deploying_namespace], to: :failed_deploy_namespace end event :finish_deploy do transitions from: [:deploying], to: :deployed end event :start_scaling_down do transitions from: [:deployed], to: :scaling_down end event :scale_down do transitions from: [:scaling_down], to: :scaled_down end event :start_scaling_up do transitions from: [:scaled_down, :failed_scale_up], to: :scaling_up end event :scale_up do transitions from: [:scaling_up], to: :deployed end event :fail_scale_up do transitions from: [:scaling_up], to: :failed_scale_up end event :fail do transitions from: [:deploying], to: :failed end event :disable, after: :after_disable do transitions from: [ :deploying_namespace, :failed_deploy_namespace, :deploying, :deployed, :scaling_down, :scaling_up, :scaled_down, :failed, ], to: :disabled end end def after_disable UffizziCore::Cluster::DeleteJob.perform_async(id) end def namespace [NAMESPACE_PREFIX, id].join end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/comment.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Comment extend ActiveSupport::Concern included do include UffizziCore::CommentRepo self.table_name = UffizziCore.table_names[:comments] has_ancestry(cache_depth: true) const_set(:MAX_DEPTH_LEVEL, 1) belongs_to :user belongs_to :commentable, polymorphic: true validates :content, length: { maximum: 1500 }, presence: true validates :ancestry_depth, numericality: { less_than_or_equal_to: self::MAX_DEPTH_LEVEL, only_integer: true } end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/compose_file.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::ComposeFile extend ActiveSupport::Concern LOCAL_SOURCE = :local included do include UffizziCore::ComposeFileRepo include AASM extend Enumerize self.table_name = UffizziCore.table_names[:compose_files] belongs_to :project belongs_to :added_by, class_name: UffizziCore::User.name, foreign_key: :added_by_id, optional: true has_one :template, dependent: :destroy has_many :config_files, dependent: :destroy has_many :host_volume_files, dependent: :destroy has_many :deployments, dependent: :nullify enumerize :kind, in: UffizziCore.compose_file_kinds, predicates: true, scope: :shallow, default: :main validates :source, presence: true validate :main_compose_file_uniqueness, on: :create, if: -> { kind.main? } aasm(:auto_deploy) do state :disabled, initial: true state :enabled event :enable do transitions from: [:disabled], to: :enabled end event :disable do transitions from: [:enabled], to: :disabled end end aasm(:state) do state :valid_file, initial: true state :invalid_file event :set_valid do transitions from: [:invalid_file], to: :valid_file end event :set_invalid do transitions from: [:valid_file], to: :invalid_file end end def local_source? repository_id.nil? && branch.nil? end def source_kind return LOCAL_SOURCE if local_source? end private def main_compose_file_uniqueness return unless project.compose_files.main.exists? errors.add(:compose_file, 'A compose file already exist for this project') end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/config_file.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::ConfigFile extend ActiveSupport::Concern included do include UffizziCore::ConfigFileRepo extend Enumerize self.table_name = UffizziCore.table_names[:config_files] belongs_to :project belongs_to :added_by, class_name: UffizziCore::User.name, foreign_key: :added_by_id, optional: true belongs_to :compose_file, optional: true has_many :container_config_files, dependent: :destroy enumerize :kind, in: [:config_map, :secret], default: :config_map, predicates: true enumerize :creation_source, in: [:manual, :compose_file, :system], predicates: true, scope: true end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/container.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Container extend ActiveSupport::Concern REQUEST_MEMORY_RATIO = 4 # rubocop:disable Metrics/BlockLength included do include UffizziCore::ContainerRepo include AASM include UffizziCore::StateMachineConcern extend Enumerize self.table_name = UffizziCore.table_names[:containers] enumerize :kind, in: [:internal, :user], predicates: true belongs_to :deployment, touch: true belongs_to :repo, optional: true has_many :activity_items, dependent: :destroy has_many :container_config_files, dependent: :destroy has_many :config_files, through: :container_config_files has_many :container_host_volume_files, dependent: :destroy has_many :host_volume_files, through: :container_host_volume_files attribute :public, :boolean, default: false attribute :port, :integer, default: nil enumerize :source, in: [:github], skip_validations: true validates :port, presence: true, inclusion: { in: 0..65_535 }, uniqueness: { scope: :deployment_id }, if: :should_check_port after_commit :check_target_port, on: :create before_save :update_target_port_value, if: :will_save_change_to_port? before_create :set_defaults accepts_nested_attributes_for :repo accepts_nested_attributes_for :container_config_files, allow_destroy: true accepts_nested_attributes_for :container_host_volume_files, allow_destroy: true validates :variables, 'uffizzi_core/environment_variable_list': true, allow_nil: true validates :secret_variables, 'uffizzi_core/environment_variable_list': true, allow_nil: true validates :entrypoint, 'uffizzi_core/image_command_args': true, allow_nil: true validates :command, 'uffizzi_core/image_command_args': true, allow_nil: true validates :tag, presence: true aasm :continuously_deploy, column: :continuously_deploy, namespace: :cd do state :disabled, initial: true state :enabled end aasm :state, column: :state do state :active, initial: true state :disabled event :activate do transitions from: [:disabled], to: :active end event :disable do transitions from: [:active], to: :disabled end end def image_name "#{image}:#{tag}" end private def should_check_port public && active? end def set_defaults update_target_port_value end def update_target_port_value self.target_port = UffizziCore::ContainerService.target_port_value(self) end def check_target_port if target_port && deployment.containers.where(target_port: target_port).size > 1 update(target_port: UffizziCore::DeploymentService.find_unused_port(deployment)) end end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/container_config_file.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::ContainerConfigFile extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:container_config_files] belongs_to :container belongs_to :config_file end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/container_host_volume_file.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::ContainerHostVolumeFile extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:container_host_volume_files] belongs_to :container belongs_to :host_volume_file end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/coupon.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Coupon extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:coupons] end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/credential.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Credential extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength included do include AASM include UffizziCore::CredentialRepo extend Enumerize self.table_name = UffizziCore.table_names[:credentials] const_set(:CREDENTIAL_TYPES, [ 'UffizziCore::Credential::Amazon', 'UffizziCore::Credential::Azure', 'UffizziCore::Credential::DockerHub', 'UffizziCore::Credential::DockerRegistry', 'UffizziCore::Credential::GithubContainerRegistry', 'UffizziCore::Credential::Google', ]) belongs_to :account before_destroy :remove_token validates :registry_url, presence: true aasm :state, column: :state do state :not_connected, initial: true state :active state :unauthorized event :activate do transitions from: [:not_connected, :unauthorized], to: :active end event :unauthorize do transitions from: [:not_connected, :active], to: :unauthorized end event :disconnect do transitions from: [:active, :unauthorized], to: :not_connected end end UffizziCore::ContainerRegistryService.sources.each do |t| define_method :"#{t}?" do type == "UffizziCore::Credential::#{t.to_s.camelize}" end end def correct? credential = self return false unless credential container_registry_service = UffizziCore::ContainerRegistryService.init_by_subclass(credential.type) status = container_registry_service.credential_correct?(credential) if credential.persisted? && credential.active? && !status Rails.logger.warn("Wrong credential: credential_correct? credential_id=#{credential.id}") end status end private def remove_token account.projects.find_each do |project| project.deployments.find_each do |deployment| containers = deployment.containers attributes = { continuously_deploy: UffizziCore::Container::STATE_CD_DISABLED } containers.with_docker_hub_repo.update_all(attributes) if docker_hub? end end end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/deployment.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Deployment extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength included do include AASM include UffizziCore::StateMachineConcern include UffizziCore::DeploymentRepo extend Enumerize include UffizziCore::DependencyInjectionConcern self.table_name = UffizziCore.table_names[:deployments] enumerize :kind, in: [:standard], predicates: true, default: :standard belongs_to :project, touch: true belongs_to :deployed_by, class_name: UffizziCore::User.name, foreign_key: :deployed_by_id, optional: true belongs_to :template, optional: true belongs_to :compose_file, optional: true has_many :credentials, through: :project has_many :containers, dependent: :destroy, index_errors: true has_many :activity_items, dependent: :destroy has_many :deployment_events, dependent: :destroy has_one :ingress_container, -> { where(receive_incoming_requests: true) }, class_name: UffizziCore::Container.name validates :kind, presence: true enumerize :creation_source, in: [:manual, :demo, :continuous_preview, :compose_file_manual, :compose_file_continuous_preview, :github_actions, :gitlab_actions], predicates: true, scope: true, default: :manual accepts_nested_attributes_for :containers, allow_destroy: true after_destroy_commit :clean def active_containers containers.active end aasm(:state) do state :active, initial: true state :failed state :disabled event :activate, after: :after_activate do transitions from: [:disabled, :failed], to: :active end event :fail do transitions from: [:active], to: :failed end event :disable, after: :after_disable do transitions from: [:active, :failed], to: :disabled end end def after_activate update!(disabled_at: nil) end def after_disable clean update!(disabled_at: Time.now) end def clean active_containers.each(&:disable!) UffizziCore::Deployment::DeleteJob.perform_async(id) end def preview_url managed_dns_zone = controller_settings_service.deployment_settings_by_deployment(self).managed_dns_zone "#{subdomain}.#{managed_dns_zone}" end def namespace "deployment-#{id}" end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/deployment_event.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::DeploymentEvent extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:deployment_events] belongs_to :deployment end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/event.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Event extend ActiveSupport::Concern included do include UffizziCore::EventRepo extend Enumerize self.table_name = UffizziCore.table_names[:events] enumerize :state, in: UffizziCore.event_states, predicates: true, scope: true belongs_to :activity_item, touch: true end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/host_volume_file.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::HostVolumeFile extend ActiveSupport::Concern included do include UffizziCore::HostVolumeFileRepo self.table_name = UffizziCore.table_names[:host_volume_files] belongs_to :project belongs_to :added_by, class_name: UffizziCore::User.name, foreign_key: :added_by_id, optional: true belongs_to :compose_file, optional: true has_many :container_host_volume_files, dependent: :destroy validates :source, presence: true validates :path, presence: true validates :payload, presence: true validates :is_file, inclusion: [true, false] end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/kubernetes_distribution.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::KubernetesDistribution extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:kubernetes_distributions] has_many :clusters def self.default find_by(default: true) end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/membership.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Membership extend ActiveSupport::Concern included do include AASM include UffizziCore::StateMachineConcern include UffizziCore::MembershipRepo extend Enumerize self.table_name = UffizziCore.table_names[:memberships] enumerize :role, in: [:admin, :developer, :viewer], predicates: true validates :role, presence: true belongs_to :account belongs_to :user validates :role, presence: true aasm(:state) do state :active, initial: true state :blocked state :inactive event :activate do transitions from: [:blocked, :inactive], to: :active end event :deactivate do transitions from: [:active], to: :inactive end event :block do transitions from: [:active], to: :blocked end end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/payment.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Payment extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:payments] belongs_to :account scope :succeeded, -> { where(status: :succeeded) } scope :pending, -> { where(status: :pending) } scope :failed, -> { where(status: :failed) } end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/price.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Price extend ActiveSupport::Concern included do include UffizziCore::PriceRepo self.table_name = UffizziCore.table_names[:prices] belongs_to :product end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/product.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Product extend ActiveSupport::Concern included do include UffizziCore::ProductRepo self.table_name = UffizziCore.table_names[:products] has_one :price, dependent: :destroy UffizziCore::Product.inheritance_column = :sti end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/project.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Project extend ActiveSupport::Concern # rubocop:disable Metrics/BlockLength included do include AASM include UffizziCore::StateMachineConcern include UffizziCore::ProjectRepo self.table_name = UffizziCore.table_names[:projects] belongs_to :account has_many :repos, dependent: :destroy has_many :deployments, dependent: :destroy has_many :user_projects, dependent: :destroy has_many :users, through: :user_projects has_many :config_files, dependent: :destroy has_many :templates, dependent: :destroy has_many :credentials, through: :account has_many :compose_files, dependent: :destroy has_many :secrets, dependent: :destroy, as: :resource has_many :host_volume_files, dependent: :destroy has_many :clusters, dependent: :destroy validates :name, presence: true validates_uniqueness_of :name, conditions: -> { where(state: :active) }, scope: :account_id validates :slug, presence: true, uniqueness: true aasm(:state) do state :active, initial: true state :disabled event :activate do transitions from: [:disabled], to: :active end event :disable, after: :after_disable do transitions from: [:active], to: :disabled end end def after_disable disable_deployments disable_clusters end def active_deployments deployments.enabled end def active_clusters clusters.enabled end def disable_deployments active_deployments.each do |deployment| UffizziCore::DeploymentService.disable!(deployment) deployment.deployment_events.create!(deployment_state: deployment.state, message: 'Disabled after project was disabled') end end def disable_clusters active_clusters.each(&:disable!) end def compose_file compose_files.main.first end end # rubocop:enable Metrics/BlockLength end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/rating.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Rating extend ActiveSupport::Concern included do include AASM self.table_name = UffizziCore.table_names[:ratings] aasm(:state) do state :active, initial: true state :disabled event :activate do transitions from: [:disabled], to: :active end event :disable do transitions from: [:active], to: :disabled end end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/repo.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Repo extend ActiveSupport::Concern included do extend Enumerize include UffizziCore::RepoRepo self.table_name = UffizziCore.table_names[:repos] enumerize :kind, in: [:buildpacks18, :dockerfile, :dotnet, :gatsby, :barestatic], predicates: true belongs_to :project has_one :container, inverse_of: :repo, dependent: :destroy validates :dockerfile_path, presence: true, if: :dockerfile? validates :delete_preview_after, numericality: { greater_than: 0, only_integer: true }, allow_nil: true def docker_hub? type == UffizziCore::Repo::DockerHub.name end def azure? type == UffizziCore::Repo::Azure.name end def google? type == UffizziCore::Repo::Google.name end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/role.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Role extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:roles] has_and_belongs_to_many :users, join_table: UffizziCore.table_names[:users_roles] belongs_to :resource, polymorphic: true, optional: true validates :resource_type, inclusion: { in: Rolify.resource_types }, allow_nil: true scopify end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/secret.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Secret extend ActiveSupport::Concern included do self.table_name = UffizziCore.table_names[:secrets] belongs_to :resource, polymorphic: true validates :name, presence: true, uniqueness: { scope: :resource } end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/template.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::Template extend ActiveSupport::Concern included do include UffizziCore::TemplateRepo extend Enumerize self.table_name = UffizziCore.table_names[:templates] belongs_to :added_by, class_name: UffizziCore::User.name, foreign_key: :added_by_id belongs_to :project, touch: true belongs_to :compose_file, optional: true has_many :deployments, dependent: :nullify enumerize :creation_source, in: [:manual, :compose_file, :system], predicates: true, scope: true validates :name, presence: true end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/user.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::User extend ActiveSupport::Concern included do include AASM include ActiveModel::Validations include UffizziCore::StateMachineConcern include UffizziCore::HashidConcern include UffizziCore::UserRepo extend Enumerize self.table_name = UffizziCore.table_names[:users] rolify({ role_cname: UffizziCore::Role.name, role_join_table_name: UffizziCore.table_names[:users_roles] }) has_many :memberships, dependent: :destroy has_many :accounts, through: :memberships has_many :user_projects, dependent: :destroy has_many :projects, through: :user_projects has_many :deployments, class_name: UffizziCore::Deployment.name, foreign_key: :deployed_by_id, dependent: :nullify has_many :clusters, foreign_key: :deployed_by_id has_one_attached :avatar enumerize :creation_source, in: UffizziCore.user_creation_sources, predicates: true def default_account personal_account end def personal_account accounts.personal.find_by(owner_id: id) end def full_name "#{first_name} #{last_name}" end aasm(:state) do state :initial, initial: true state :active state :disabled event :activate do transitions from: [:initial, :disabled], to: :active end event :disable do transitions from: [:initial, :active], to: :disabled end end def admin_access_to_project?(project) project.user_projects.where(user_id: id, role: UffizziCore::UserProject.role.admin).exists? end end end ================================================ FILE: core/app/lib/uffizzi_core/concerns/models/user_project.rb ================================================ # frozen_string_literal: true module UffizziCore::Concerns::Models::UserProject extend ActiveSupport::Concern included do extend Enumerize self.table_name = UffizziCore.table_names[:user_projects] enumerize :role, in: UffizziCore.user_project_roles, predicates: true, scope: true validates :role, presence: true belongs_to :user belongs_to :project belongs_to :invited_by, class_name: UffizziCore::User.name, foreign_key: :invited_by_id, optional: true end end ================================================ FILE: core/app/lib/uffizzi_core/rbac/user_access_service.rb ================================================ # frozen_string_literal: true module UffizziCore::Rbac::UserAccessService class << self def admin_access_to_account?(_user, _account) true end def any_access_to_account?(_user, _account) true end def admin_or_developer_access_to_account?(_user, _account) true end def admin_or_developer_access_to_project?(_user, _project) true end def any_access_to_project?(_user, _project) true end def admin_access_to_project?(_user, _project) true end end end ================================================ FILE: core/app/mailers/uffizzi_core/application_mailer.rb ================================================ # frozen_string_literal: true module UffizziCore class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end end ================================================ FILE: core/app/models/concerns/uffizzi_core/hashid_concern.rb ================================================ # frozen_string_literal: true module UffizziCore::HashidConcern extend ActiveSupport::Concern class_methods do def hashid_service @hashid_service ||= Hashids.new("#{Rails.application.secrets.secret_key_base}-#{name}") end def find_by_hashid(hashid) id = hashid_service.decode(hashid.to_s).first find_by(id: id) end def find_by_hashid!(hashid) id = hashid_service.decode(hashid.to_s).first find(id) end end def hashid self.class.hashid_service.encode(id) end end ================================================ FILE: core/app/models/concerns/uffizzi_core/state_machine_concern.rb ================================================ # frozen_string_literal: true module UffizziCore::StateMachineConcern extend ActiveSupport::Concern class_methods do def aasm(attribute, *args) attr_accessor(:"#{attribute}_event") define_method("#{attribute}_event=") do |value| send(value) end super(attribute, *args) end end end ================================================ FILE: core/app/models/uffizzi_core/account.rb ================================================ # frozen_string_literal: true class UffizziCore::Account < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Account belongs_to :owner, class_name: UffizziCore::User.name, foreign_key: :owner_id aasm(:state) do state :active, initial: true state :payment_issue state :disabled state :draft event :activate do transitions from: [:payment_issue, :disabled, :draft], to: :active end event :raise_payment_issue, before_success: :update_payment_issue_date do transitions from: [:active, :disabled], to: :payment_issue end event :disable, after: :disable_projects do transitions from: [:active, :payment_issue], to: :disabled end end end ================================================ FILE: core/app/models/uffizzi_core/activity_item/docker.rb ================================================ # frozen_string_literal: true class UffizziCore::ActivityItem::Docker < UffizziCore::ActivityItem end ================================================ FILE: core/app/models/uffizzi_core/activity_item/github.rb ================================================ # frozen_string_literal: true class UffizziCore::ActivityItem::Github < UffizziCore::ActivityItem end ================================================ FILE: core/app/models/uffizzi_core/activity_item/memory_limit.rb ================================================ # frozen_string_literal: true class UffizziCore::ActivityItem::MemoryLimit < UffizziCore::ActivityItem end ================================================ FILE: core/app/models/uffizzi_core/activity_item.rb ================================================ # frozen_string_literal: true # @model # # @property id [integer] # @property namespace [string] # @property name(required) [string] # @property tag [string] # @property branch [string] # @property type [string] # @property container_id [string] # @property commit [string] # @property commit_message [string] # @property build_id [integer] # @property created_at [date] # @property updated_at [date] # @property data [object] # @property digest [string] # @property meta [object] class UffizziCore::ActivityItem < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::ActivityItem end ================================================ FILE: core/app/models/uffizzi_core/application_record.rb ================================================ # frozen_string_literal: true module UffizziCore class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end end ================================================ FILE: core/app/models/uffizzi_core/cluster.rb ================================================ # frozen_string_literal: true class UffizziCore::Cluster < ApplicationRecord include UffizziCore::Concerns::Models::Cluster end ================================================ FILE: core/app/models/uffizzi_core/comment.rb ================================================ # frozen_string_literal: true class UffizziCore::Comment < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Comment end ================================================ FILE: core/app/models/uffizzi_core/compose_file.rb ================================================ # frozen_string_literal: true # @model ComposeFile # @property id [integer] # @property source [string] # @property path [string] # @property content(required) [string] # @property auto_deploy [boolean] # @property state [string] # @property payload [string] class UffizziCore::ComposeFile < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::ComposeFile end ================================================ FILE: core/app/models/uffizzi_core/config_file.rb ================================================ # frozen_string_literal: true # @model # # @property filename [string] # @property kind [string] # @property payload [string] # @property source [string] class UffizziCore::ConfigFile < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::ConfigFile end ================================================ FILE: core/app/models/uffizzi_core/container.rb ================================================ # frozen_string_literal: true # @model Container # # @property id [integer] # @property name [string] # @property memory_limit [integer] # @property memory_request [integer] # @property continuously_deploy [string] # @property variables [object] # @property secret_variables [object] # @property container_config_files [ConfigFile] class UffizziCore::Container < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Container end ================================================ FILE: core/app/models/uffizzi_core/container_config_file.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerConfigFile < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::ContainerConfigFile end ================================================ FILE: core/app/models/uffizzi_core/container_host_volume_file.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerHostVolumeFile < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::ContainerHostVolumeFile end ================================================ FILE: core/app/models/uffizzi_core/continuous_preview.rb ================================================ # frozen_string_literal: true class UffizziCore::ContinuousPreview < UffizziCore::ApplicationRecord end ================================================ FILE: core/app/models/uffizzi_core/coupon.rb ================================================ # frozen_string_literal: true class UffizziCore::Coupon < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Coupon end ================================================ FILE: core/app/models/uffizzi_core/credential/amazon.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::Amazon < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential/azure.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::Azure < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential/docker_hub.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::DockerHub < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential/docker_registry.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::DockerRegistry < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential/github_container_registry.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::GithubContainerRegistry < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential/google.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential::Google < UffizziCore::Credential end ================================================ FILE: core/app/models/uffizzi_core/credential.rb ================================================ # frozen_string_literal: true class UffizziCore::Credential < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Credential enumerize :type, in: self::CREDENTIAL_TYPES, i18n_scope: ['enumerize.credential.type'] end ================================================ FILE: core/app/models/uffizzi_core/database.rb ================================================ # frozen_string_literal: true class UffizziCore::Database < UffizziCore::ApplicationRecord end ================================================ FILE: core/app/models/uffizzi_core/database_offering.rb ================================================ # frozen_string_literal: true class UffizziCore::DatabaseOffering < UffizziCore::ApplicationRecord end ================================================ FILE: core/app/models/uffizzi_core/deployment.rb ================================================ # frozen_string_literal: true # @model Deployment # # @property id [integer] # @property project_id [integer] # @property kind [string] # @property state [string] # @property preview_url [string] # @property tag [string] # @property branch [string] # @property commit [string] # @property image_id [string] # @property created_at [date-time] # @property updated_at [date-time] # @property ingress_container_ready [boolean] # @property ingress_container_state [string] # @property creation_source [string] # @property contaners [array] # @property deployed_by [object] class UffizziCore::Deployment < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Deployment end ================================================ FILE: core/app/models/uffizzi_core/deployment_event.rb ================================================ # frozen_string_literal: true class UffizziCore::DeploymentEvent < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::DeploymentEvent end ================================================ FILE: core/app/models/uffizzi_core/event.rb ================================================ # frozen_string_literal: true class UffizziCore::Event < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Event end ================================================ FILE: core/app/models/uffizzi_core/host_volume_file.rb ================================================ # frozen_string_literal: true class UffizziCore::HostVolumeFile < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::HostVolumeFile end ================================================ FILE: core/app/models/uffizzi_core/kubernetes_distribution.rb ================================================ # frozen_string_literal: true class UffizziCore::KubernetesDistribution < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::KubernetesDistribution end ================================================ FILE: core/app/models/uffizzi_core/membership.rb ================================================ # frozen_string_literal: true class UffizziCore::Membership < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Membership end ================================================ FILE: core/app/models/uffizzi_core/payment.rb ================================================ # frozen_string_literal: true class UffizziCore::Payment < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Payment end ================================================ FILE: core/app/models/uffizzi_core/price.rb ================================================ # frozen_string_literal: true class UffizziCore::Price < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Price end ================================================ FILE: core/app/models/uffizzi_core/product.rb ================================================ # frozen_string_literal: true class UffizziCore::Product < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Product end ================================================ FILE: core/app/models/uffizzi_core/project.rb ================================================ # frozen_string_literal: true # @model Project # @property slug [string] # @property name [string] # @property description [string] # @property created_at [date-time] # @property secrets [string] # @property default_compose [object] # @property deployments [object] # @property account [object] class UffizziCore::Project < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Project end ================================================ FILE: core/app/models/uffizzi_core/rating.rb ================================================ # frozen_string_literal: true class UffizziCore::Rating < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Rating end ================================================ FILE: core/app/models/uffizzi_core/repo/amazon.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::Amazon < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo/azure.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::Azure < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo/docker_hub.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::DockerHub < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo/docker_registry.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::DockerRegistry < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo/github_container_registry.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::GithubContainerRegistry < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo/google.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo::Google < UffizziCore::Repo end ================================================ FILE: core/app/models/uffizzi_core/repo.rb ================================================ # frozen_string_literal: true class UffizziCore::Repo < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Repo end ================================================ FILE: core/app/models/uffizzi_core/role.rb ================================================ # frozen_string_literal: true class UffizziCore::Role < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Role end ================================================ FILE: core/app/models/uffizzi_core/secret.rb ================================================ # frozen_string_literal: true class UffizziCore::Secret < ApplicationRecord include UffizziCore::Concerns::Models::Secret end ================================================ FILE: core/app/models/uffizzi_core/template.rb ================================================ # frozen_string_literal: true class UffizziCore::Template < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::Template end ================================================ FILE: core/app/models/uffizzi_core/user.rb ================================================ # frozen_string_literal: true class UffizziCore::User < ActiveRecord::Base include UffizziCore::Concerns::Models::User has_secure_password validates :email, presence: true, 'uffizzi_core/email': true, uniqueness: { case_sensitive: false } validates :password, allow_nil: true, length: { minimum: 8 }, on: :update end ================================================ FILE: core/app/models/uffizzi_core/user_project.rb ================================================ # frozen_string_literal: true class UffizziCore::UserProject < UffizziCore::ApplicationRecord include UffizziCore::Concerns::Models::UserProject end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/accounts/clusters_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ClustersPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_account?(context.user, context.account) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/accounts/credentials_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::CredentialsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.admin_access_to_account?(context.user, context.account) end def create? context.user_access_module.admin_access_to_account?(context.user, context.account) end def update? context.user_access_module.admin_access_to_account?(context.user, context.account) end def check_credential? context.user_access_module.admin_access_to_account?(context.user, context.account) end def destroy? context.user_access_module.admin_access_to_account?(context.user, context.account) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/accounts/projects_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ProjectsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_account?(context.user, context.account) end def create? context.user_access_module.admin_or_developer_access_to_account?(context.user, context.account) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/accounts_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::AccountsPolicy < UffizziCore::ApplicationPolicy def index? context.user.present? end def show? context.user_access_module.any_access_to_account?(context.user, context.account) end def update? false end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/clusters/ingresses_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Clusters::IngressesPolicy < UffizziCore::ApplicationPolicy def index? return true if context.user_access_module.admin_access_to_project?(context.user, context.project) context.cluster.deployed_by_id == context.user.id end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/clusters_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ClustersPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end def show? context.user_access_module.any_access_to_project?(context.user, context.project) end def create? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def scale_down? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def scale_up? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def sync? context.user_access_module.any_access_to_project?(context.user, context.project) end def destroy? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/compose_files_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ComposeFilesPolicy < UffizziCore::ApplicationPolicy def show? context.user_access_module.any_access_to_project?(context.user, context.project) end def create? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def destroy? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/deployments/activity_items_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ActivityItemsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/deployments/containers_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainersPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end def k8s_container_description? context.user_access_module.any_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/deployments/events_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::EventsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/deployments_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end def show? context.user_access_module.any_access_to_project?(context.user, context.project) end def create? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def update? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def destroy? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def deploy_containers? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects/secrets_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::SecretsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_project?(context.user, context.project) end def bulk_create? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end def destroy? context.user_access_module.admin_or_developer_access_to_project?(context.user, context.project) end end ================================================ FILE: core/app/policies/uffizzi_core/api/cli/v1/projects_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ProjectsPolicy < UffizziCore::ApplicationPolicy def index? context.user_access_module.any_access_to_account?(context.user, context.account) end def show? context.user_access_module.any_access_to_account?(context.user, context.account) end def destroy? context.user_access_module.admin_or_developer_access_to_account?(context.user, context.account) end end ================================================ FILE: core/app/policies/uffizzi_core/application_policy.rb ================================================ # frozen_string_literal: true class UffizziCore::ApplicationPolicy attr_reader :context, :record def initialize(context, record) raise Pundit::NotAuthorizedError, 'must be logged in' if !context.instance_of?(UffizziCore::WebhooksContext) && context.user.blank? @record = record @context = context end end ================================================ FILE: core/app/repositories/uffizzi_core/account_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::AccountRepo extend ActiveSupport::Concern included do scope :by_kind, ->(kind) { where(kind: kind) } scope :personal, -> { by_kind(UffizziCore::Account.kind.personal) } scope :organizational, -> { by_kind(UffizziCore::Account.kind.organizational) } end end ================================================ FILE: core/app/repositories/uffizzi_core/activity_item_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ActivityItemRepo extend ActiveSupport::Concern included do include UffizziCore::BasicOrderRepo scope :docker, -> { where(type: UffizziCore::ActivityItem::Docker.name) } end end ================================================ FILE: core/app/repositories/uffizzi_core/basic_order_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::BasicOrderRepo extend ActiveSupport::Concern included do scope :order_by_id, ->(order = :asc) { order(id: order) } end end ================================================ FILE: core/app/repositories/uffizzi_core/cluster_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ClusterRepo extend ActiveSupport::Concern included do scope :deployed, -> { where(state: UffizziCore::Cluster::STATE_DEPLOYED) } scope :enabled, -> { where.not(state: UffizziCore::Cluster::STATE_DISABLED) } scope :deployed_by_user, ->(user) { where(deployed_by: user) } scope :by_projects, ->(projects) { where(project_id: projects.select(:id)) } end end ================================================ FILE: core/app/repositories/uffizzi_core/comment_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::CommentRepo extend ActiveSupport::Concern included do scope :by_user, ->(user) { where(user: user) } end end ================================================ FILE: core/app/repositories/uffizzi_core/compose_file_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ComposeFileRepo extend ActiveSupport::Concern included do end end ================================================ FILE: core/app/repositories/uffizzi_core/config_file_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ConfigFileRepo extend ActiveSupport::Concern included do scope :by_deployment, ->(deployment) { select(:id) .joins(:container_config_files) .where(container_config_files: { container: deployment.containers }) .distinct } scope :by_source, ->(source) { where(source: source) } end end ================================================ FILE: core/app/repositories/uffizzi_core/container_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ContainerRepo extend ActiveSupport::Concern included do include UffizziCore::BasicOrderRepo scope :with_amazon_repo, -> { includes(:repo).where(repo: { type: UffizziCore::Repo::Amazon.name }) } scope :with_docker_hub_repo, -> { includes(:repo).where(repo: { type: UffizziCore::Repo::DockerHub.name }) } scope :with_public_access, -> { where(public: true) } scope :by_repo_type, ->(type) { includes(:repo).where(repo: { type: type }) } end end ================================================ FILE: core/app/repositories/uffizzi_core/credential_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::CredentialRepo extend ActiveSupport::Concern included do scope :by_type, ->(type) { where(type: type) } scope :docker_hub, -> { by_type(UffizziCore::Credential::DockerHub.name) } scope :docker_registry, -> { by_type(UffizziCore::Credential::DockerRegistry.name) } scope :azure, -> { by_type(UffizziCore::Credential::Azure.name) } scope :google, -> { by_type(UffizziCore::Credential::Google.name) } scope :amazon, -> { by_type(UffizziCore::Credential::Amazon.name) } scope :github_container_registry, -> { by_type(UffizziCore::Credential::GithubContainerRegistry.name) } scope :deployable, -> { by_type([ UffizziCore::Credential::DockerHub.name, UffizziCore::Credential::DockerRegistry.name, UffizziCore::Credential::Azure.name, UffizziCore::Credential::Google.name, UffizziCore::Credential::Amazon.name, UffizziCore::Credential::GithubContainerRegistry.name, ]) } end end ================================================ FILE: core/app/repositories/uffizzi_core/deployment_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::DeploymentRepo extend ActiveSupport::Concern included do scope :with_name, ->(name) { where(name: name) } scope :with_amazon_repos, -> { includes(containers: [:repo]).where(containers: { repos: { type: UffizziCore::Repo::Amazon.name } }) } scope :enabled, -> { where(state: [:active, :failed]) } scope :active_for_credential_id, ->(credential_id) { active.joins(project: :credentials).merge(UffizziCore::Project.active).where(credentials: { id: credential_id }) } scope :with_metadata, -> { where.not(metadata: {}) } scope :with_labels, ->(labels) { where("#{UffizziCore::Deployment.table_name}.metadata @> ?", labels.to_json) } end end ================================================ FILE: core/app/repositories/uffizzi_core/event_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::EventRepo extend ActiveSupport::Concern included do include UffizziCore::BasicOrderRepo end end ================================================ FILE: core/app/repositories/uffizzi_core/host_volume_file_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::HostVolumeFileRepo extend ActiveSupport::Concern included do scope :by_deployment, ->(deployment) { joins(:container_host_volume_files) .where(container_host_volume_files: { container: deployment.containers }) .distinct } scope :by_source, ->(source) { where(source: source) } end end ================================================ FILE: core/app/repositories/uffizzi_core/membership_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::MembershipRepo extend ActiveSupport::Concern included do scope :by_role_admin, -> { by_role(UffizziCore::Membership.role.admin) } scope :by_account, ->(account) { where(account_id: account.id) } end end ================================================ FILE: core/app/repositories/uffizzi_core/price_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::PriceRepo extend ActiveSupport::Concern class_methods do def container_memory_for(env) find_by(slug: env.kind) end end end ================================================ FILE: core/app/repositories/uffizzi_core/product_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ProductRepo extend ActiveSupport::Concern class_methods do def container_memory find_by(slug: :container_memory) end end end ================================================ FILE: core/app/repositories/uffizzi_core/project_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::ProjectRepo extend ActiveSupport::Concern included do scope :by_ids, ->(ids) { where(id: ids) } scope :by_accounts, ->(account_ids) { where(account_id: account_ids) } end end ================================================ FILE: core/app/repositories/uffizzi_core/repo_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::RepoRepo extend ActiveSupport::Concern included do scope :docker_hub, -> { where(type: UffizziCore::Repo::DockerHub.name) } end end ================================================ FILE: core/app/repositories/uffizzi_core/template_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::TemplateRepo extend ActiveSupport::Concern included do end end ================================================ FILE: core/app/repositories/uffizzi_core/usage_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::UsageRepo extend ActiveSupport::Concern included do scope :by_timestamp, ->(direction = :asc) { order("timestamp #{direction}") } end end ================================================ FILE: core/app/repositories/uffizzi_core/user_repo.rb ================================================ # frozen_string_literal: true module UffizziCore::UserRepo extend ActiveSupport::Concern included do scope :by_email, ->(email) { where('lower(email) = ?', email.downcase) } end end ================================================ FILE: core/app/responders/uffizzi_core/json_responder.rb ================================================ # frozen_string_literal: true class UffizziCore::JsonResponder < ActionController::Responder def api_behavior(*args, &block) if post? display(resource, status: :created) elsif put? || patch? display(resource, status: :ok) else super end end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/account_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::AccountSerializer < UffizziCore::BaseSerializer include UffizziCore::DependencyInjectionConcern include_module_if_exists('UffizziCore::Api::Cli::V1::AccountSerializerModule') type :account has_many :projects attributes :id, :name, :api_url, :vclusters_controller_url def api_url Settings.domain end def vclusters_controller_url controller_settings_service.vcluster_settings_by_account(object).url end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/accounts/cluster_serializer/project_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ClusterSerializer::ProjectSerializer < UffizziCore::BaseSerializer type :project attributes :name end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/accounts/cluster_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ClusterSerializer < UffizziCore::BaseSerializer type :cluster belongs_to :project attributes :name end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/accounts/credential_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::CredentialSerializer < UffizziCore::BaseSerializer attributes :id, :username, :password, :type, :state def password anonymize(object.password) end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/accounts/project_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Accounts::ProjectSerializer < UffizziCore::BaseSerializer type :project attributes :name, :slug, :account_id, :description, :created_at end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/project_serializer/account_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ProjectSerializer::AccountSerializer < UffizziCore::BaseSerializer attributes :id, :kind, :state, :name end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/project_serializer/compose_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ProjectSerializer::ComposeFileSerializer < UffizziCore::BaseSerializer type :compose_file attributes :source end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/project_serializer/deployment_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ProjectSerializer::DeploymentSerializer < UffizziCore::BaseSerializer type :deployment attributes :id, :preview_url, :state end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/project_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ProjectSerializer < UffizziCore::BaseSerializer type :project has_many :deployments has_many :secrets has_one :default_compose belongs_to :account attributes :name, :slug, :description, :created_at def default_compose object.compose_files.main.first end def deployments object.deployments.active.map do |deployment| deployment.state = if UffizziCore::DeploymentService.failed?(deployment) UffizziCore::Deployment::STATE_FAILED else UffizziCore::Deployment::STATE_ACTIVE end deployment end end def secrets return [] unless object.secrets object.secrets.map(&:name) end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/cluster_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ClusterSerializer < UffizziCore::BaseSerializer type :cluster attributes :id, :name, :state, :kubeconfig, :created_at, :host, :k8s_version def k8s_version object.kubernetes_distribution&.version || UffizziCore::KubernetesDistribution.default.version end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/compose_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ComposeFileSerializer < UffizziCore::BaseSerializer type :compose_file attributes :id, :source, :path, :auto_deploy, :state, :payload, :content end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployment_serializer/container_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentSerializer::ContainerSerializer < UffizziCore::BaseSerializer type :deployment attributes :id, :kind, :image, :tag, :variables, :secret_variables, :created_at, :updated_at, :memory_limit, :memory_request, :entrypoint, :command, :port, :public, :repo_id, :continuously_deploy, :receive_incoming_requests, :healthcheck, :volumes, :service_name def secret_variables return unless object.secret_variables.present? object.secret_variables.map do |var| { name: var['name'], value: anonymize(var['value']) } end end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployment_serializer/user_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentSerializer::UserSerializer < UffizziCore::BaseSerializer type :user attributes :id, :kind, :email def kind :internal end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployment_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentSerializer < UffizziCore::BaseSerializer include UffizziCore::DependencyInjectionConcern include_module_if_exists('UffizziCore::Api::Cli::V1::Projects::DeploymentSerializerModule') type :deployment attributes :id, :project_id, :state, :preview_url has_many :containers belongs_to :deployed_by def containers object.containers.active end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments/activity_item_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ActivityItemSerializer < UffizziCore::BaseSerializer type :activity_item attributes :id, :namespace, :name, :tag, :type, :branch, :commit, :commit_message, :state, :created_at, :updated_at, :build_id, :data, :container_id, :digest def type return :github if object.type == UffizziCore::ActivityItem::Github.name return :docker if object.type == UffizziCore::ActivityItem::Docker.name return :memory_limit if object.type == UffizziCore::ActivityItem::MemoryLimit.name nil end def commit object.commit.to_s.slice(0..6) end def state object.events.order_by_id.last&.state end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments/container_serializer/container_config_file_serializer/config_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainerSerializer::ContainerConfigFileSerializer::ConfigFileSerializer < UffizziCore::BaseSerializer attributes :id, :filename, :kind, :payload end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments/container_serializer/container_config_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainerSerializer::ContainerConfigFileSerializer < UffizziCore::BaseSerializer attributes :id, :mount_path belongs_to :config_file end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments/container_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainerSerializer < UffizziCore::BaseSerializer attributes :id, :name, :memory_limit, :memory_request, :continuously_deploy, :variables, :secret_variables, :healthcheck has_many :container_config_files type :container def name image_name = object.image.split('/').pop "#{image_name}:#{object.tag}" end def secret_variables return unless object.secret_variables.present? object.secret_variables.map do |var| { name: var['name'], value: anonymize(var['value']) } end end end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments_serializer/user_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentsSerializer::UserSerializer < UffizziCore::BaseSerializer type :user attributes :email end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/deployments_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::DeploymentsSerializer < UffizziCore::BaseSerializer type :deployment attributes :id, :created_at, :updated_at, :state, :preview_url, :metadata belongs_to :deployed_by end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/secret_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::SecretSerializer < UffizziCore::BaseSerializer attributes :name, :created_at, :updated_at end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/projects/short_cluster_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::Projects::ShortClusterSerializer < UffizziCore::BaseSerializer type :cluster attributes :id, :name end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/short_project_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::ShortProjectSerializer < UffizziCore::BaseSerializer type :project belongs_to :account, serializer: UffizziCore::Api::Cli::V1::ProjectSerializer::AccountSerializer # account_id supports CLI versions < 2.0.5 attributes :name, :slug, :account_id end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/user_serializer/account_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::UserSerializer::AccountSerializer < UffizziCore::BaseSerializer attributes :id, :kind, :state, :name end ================================================ FILE: core/app/serializers/uffizzi_core/api/cli/v1/user_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Api::Cli::V1::UserSerializer < UffizziCore::BaseSerializer type :user attributes :default_account def default_account UffizziCore::Api::Cli::V1::UserSerializer::AccountSerializer.new(object.default_account) end end ================================================ FILE: core/app/serializers/uffizzi_core/base_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::BaseSerializer < ActiveModel::Serializer def anonymize(field) '*' * field.length end end ================================================ FILE: core/app/serializers/uffizzi_core/controller/apply_config_file/config_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::ApplyConfigFile::ConfigFileSerializer < UffizziCore::BaseSerializer attributes :id, :filename, :kind, :payload end ================================================ FILE: core/app/serializers/uffizzi_core/controller/create_cluster/cluster_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::CreateCluster::ClusterSerializer < UffizziCore::BaseSerializer include UffizziCore::DependencyInjectionConcern include_module_if_exists('UffizziCore::Controller::CreateCluster::ClusterSerializerModule') attributes :name, :manifest, :base_ingress_host, :distro, :image, :account_id def base_ingress_host managed_dns_zone = controller_settings_service.vcluster_settings_by_vcluster(object).managed_dns_zone [object.namespace, managed_dns_zone].join('.') end def image kubernetes_distribution.image end def distro kubernetes_distribution.distro end def account_id object.project.account_id end private def kubernetes_distribution @kubernetes_distribution ||= object.kubernetes_distribution end end ================================================ FILE: core/app/serializers/uffizzi_core/controller/create_credential/credential_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::CreateCredential::CredentialSerializer < UffizziCore::BaseSerializer attributes :id, :registry_url, :username, :password def username return 'AWS' if object.amazon? object.username end def password if object.amazon? UffizziCore::ContainerRegistry::AmazonService.access_token(object) else object.password end end end ================================================ FILE: core/app/serializers/uffizzi_core/controller/create_deployment/deployment_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::CreateDeployment::DeploymentSerializer < UffizziCore::BaseSerializer attributes :kind end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/compose_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::ComposeFileSerializer < UffizziCore::BaseSerializer attributes :source_kind end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/container_serializer/container_config_file_serializer/config_file_serializer.rb ================================================ # frozen_string_literal: true # rubocop:disable Layout/LineLength class UffizziCore::Controller::DeployContainers::ContainerSerializer::ContainerConfigFileSerializer::ConfigFileSerializer < UffizziCore::BaseSerializer # rubocop:enable Layout/LineLength attributes :id, :filename, :kind, :payload end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/container_serializer/container_config_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::ContainerSerializer::ContainerConfigFileSerializer < UffizziCore::BaseSerializer attributes :mount_path belongs_to :config_file end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/container_serializer/container_host_volume_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::ContainerSerializer::ContainerHostVolumeFileSerializer < UffizziCore::BaseSerializer attributes :host_volume_file_id, :source_path end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/container_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::ContainerSerializer < UffizziCore::BaseSerializer attributes :id, :kind, :full_image_name, :variables, :secret_variables, :memory_limit, :memory_request, :entrypoint, :command, :port, :target_port, :public, :controller_name, :receive_incoming_requests, :healthcheck, :volumes, :service_name, :additional_subdomains has_many :container_config_files has_many :container_host_volume_files def entrypoint object.entrypoint.blank? ? nil : JSON.parse(object.entrypoint) end def command object.command.blank? ? nil : JSON.parse(object.command) end def healthcheck return {} if object.healthcheck.blank? command = object.healthcheck['test'] new_command = if command.is_a?(Array) items_to_remove = ['CMD', 'CMD-SHELL'] command.select { |item| items_to_remove.exclude?(item) } elsif object.healthcheck['test'].is_a?(String) command.split end object.healthcheck.merge('test' => new_command) end end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/credential_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::CredentialSerializer < UffizziCore::BaseSerializer attributes :id end ================================================ FILE: core/app/serializers/uffizzi_core/controller/deploy_containers/host_volume_file_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::DeployContainers::HostVolumeFileSerializer < UffizziCore::BaseSerializer attributes :id, :source, :path, :payload, :is_file def payload Base64.encode64(object.payload) end end ================================================ FILE: core/app/serializers/uffizzi_core/controller/update_cluster/cluster_serializer.rb ================================================ # frozen_string_literal: true class UffizziCore::Controller::UpdateCluster::ClusterSerializer < UffizziCore::BaseSerializer include UffizziCore::DependencyInjectionConcern include_module_if_exists('UffizziCore::Controller::UpdateCluster::ClusterSerializerModule') attributes :name, :manifest, :base_ingress_host def base_ingress_host managed_dns_zone = controller_settings_service.vcluster_settings_by_vcluster(object).managed_dns_zone [object.namespace, managed_dns_zone].join('.') end end ================================================ FILE: core/app/services/uffizzi_core/account_service.rb ================================================ # frozen_string_literal: true class UffizziCore::AccountService class << self def create_credential(credential) UffizziCore::Deployment.active_for_credential_id(credential.id).pluck(:id).each do |deployment_id| UffizziCore::Deployment::CreateCredentialJob.perform_async(deployment_id, credential.id) end end def update_credential(credential) UffizziCore::Deployment.active_for_credential_id(credential.id).pluck(:id).each do |deployment_id| UffizziCore::Deployment::UpdateCredentialJob.perform_async(deployment_id, credential.id) end end def delete_credential(credential) UffizziCore::Deployment.active_for_credential_id(credential.id).pluck(:id).each do |deployment_id| UffizziCore::Deployment::DeleteCredentialJob.perform_async(deployment_id, credential.id) end end end end ================================================ FILE: core/app/services/uffizzi_core/activity_item_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ActivityItemService COMPLETED_STATES = ['deployed', 'failed', 'cancelled'].freeze class << self include UffizziCore::DependencyInjectionConcern def create_docker_item!(repo, container) activity_item_attributes = { namespace: repo.namespace, name: repo.name, container: container, deployment_id: container.deployment_id, type: UffizziCore::ActivityItem::Docker.name, tag: container.tag, } create_item!(activity_item_attributes) end def fail_deployment!(activity_item) deployment = activity_item.container.deployment last_event = activity_item.events.order_by_id.last activity_item.events.create(state: UffizziCore::Event.state.failed) unless last_event&.failed? UffizziCore::DeploymentService.fail!(deployment) end def update_docker_digest!(activity_item) container = activity_item.container repo = container.repo credential = UffizziCore::RepoService.credential(repo) container_registry_service = UffizziCore::ContainerRegistryService.init_by_subclass(repo.type) digest = container_registry_service.digest(credential, activity_item.image, activity_item.tag) activity_item.update!(digest: digest) activity_item end def manage_deploy_activity_item(activity_item) container = activity_item.container deployment = container.deployment service = UffizziCore::ManageActivityItemsService.new(deployment) container_status_item = service.container_status_item(container) return if container_status_item.nil? status = container_status_item[:status] last_event = activity_item.events.order_by_id.last activity_item.events.create(state: status) if last_event&.state != status return handle_failed_status(activity_item, deployment) if failed?(status) if deployed?(status) && UffizziCore::ContainerService.ingress_container?(container) deployment.update!(last_deploy_at: last_event.created_at) deployment.deployment_events.create!(deployment_state: status) return end return unless [UffizziCore::Event.state.building, UffizziCore::Event.state.deploying].include?(status) UffizziCore::Deployment::ManageDeployActivityItemJob.perform_in(5.seconds, activity_item.id) end private def handle_failed_status(activity_item, deployment) UffizziCore::ActivityItemService.fail_deployment!(activity_item) notification_module.notify_about_failed_deployment(deployment) if notification_module.present? end def create_item!(activity_item_attributes) activity_item = UffizziCore::ActivityItem.find_by(activity_item_attributes) return activity_item unless completed?(activity_item) UffizziCore::ActivityItem.create!(activity_item_attributes) end def completed?(activity_item) return true if activity_item.nil? last_event = activity_item.events.order_by_id.last COMPLETED_STATES.include?(last_event.state) end def failed?(status) status == UffizziCore::Event.state.failed end def deployed?(status) status == UffizziCore::Event.state.deployed end end end ================================================ FILE: core/app/services/uffizzi_core/ci_service.rb ================================================ # frozen_string_literal: true class UffizziCore::CiService class << self def valid_request_from_ci_workflow?(_params) false end end end ================================================ FILE: core/app/services/uffizzi_core/cluster_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ClusterService class << self def start_deploy(cluster) UffizziCore::Cluster::DeployJob.perform_async(cluster.id) end def deploy_cluster(cluster) begin UffizziCore::ControllerService.create_namespace(cluster) rescue UffizziCore::ControllerClient::ConnectionError return cluster.fail_deploy_namespace! end cluster.start_deploying! begin UffizziCore::ControllerService.create_cluster(cluster) rescue UffizziCore::ControllerClient::ConnectionError return cluster.fail! end UffizziCore::Cluster::ManageDeployingJob.perform_in(5.seconds, cluster.id) end def scale_up!(cluster) cluster.start_scaling_up! UffizziCore::ControllerService.patch_cluster(cluster, sleep: false) UffizziCore::Cluster::ManageScalingUpJob.perform_in(5.seconds, cluster.id) end def manage_scale_up(cluster, try) return cluster.fail_scale_up! if try > Settings.vcluster.max_scale_up_retry_count return cluster.scale_up! if ready?(cluster) UffizziCore::Cluster::ManageScalingUpJob.perform_in(5.seconds, cluster.id, ++try) end def scale_down!(cluster) cluster.start_scaling_down! UffizziCore::ControllerService.patch_cluster(cluster, sleep: true) UffizziCore::Cluster::ManageScalingDownJob.perform_in(5.seconds, cluster.id) end def manage_scale_down(cluster) return cluster.scale_down! unless awake?(cluster) UffizziCore::Cluster::ManageScalingDownJob.perform_in(5.seconds, cluster.id) end def manage_deploying(cluster, try) return if cluster.disabled? return cluster.fail! if try > Settings.vcluster.max_creation_retry_count deployed_cluster = UffizziCore::ControllerService.show_cluster(cluster) if deployed_cluster.status.ready && deployed_cluster.status.kube_config.present? cluster.finish_deploy cluster.kubeconfig = deployed_cluster.status.kube_config cluster.host = deployed_cluster.status.host cluster.save! return end UffizziCore::Cluster::ManageDeployingJob.perform_in(5.seconds, cluster.id, ++try) end def filter_user_ingress_host(cluster, ingress_hosts) ingress_hosts.reject { |h| h == cluster.host } end private def awake?(cluster) data = UffizziCore::ControllerService.show_cluster(cluster) !data.status.sleep end def ready?(cluster) data = UffizziCore::ControllerService.show_cluster(cluster) data.status.ready end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/container_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::ContainerBuilderService attr_accessor :credentials, :project, :repositories def initialize(credentials, project, repositories = []) @credentials = credentials @project = project @repositories = repositories end # rubocop:disable Metrics/PerceivedComplexity def build_attributes(container_data, ingress_data, continuous_preview_global_data, compose_dependencies) image_data = container_data[:image] || {} build_data = container_data[:build] || {} environment = container_data[:environment] || [] deploy_data = container_data[:deploy] || {} configs_data = container_data[:configs] || [] secrets = container_data[:secrets] || [] container_name = container_data[:container_name] healthcheck_data = container_data[:healthcheck] || {} volumes_data = container_data[:volumes] || [] github_deps_service = UffizziCore::ComposeFile::GithubDependenciesService env_file_dependencies = github_deps_service.env_file_dependencies_for_container(compose_dependencies, container_name) configs_dependencies = github_deps_service.configs_dependencies_for_container(compose_dependencies, container_name) host_volumes_dependencies = github_deps_service.host_volumes_dependencies_for_container(compose_dependencies, container_name) is_ingress = ingress_container?(container_name, ingress_data) repo_attributes = repo_attributes(container_data, continuous_preview_global_data) additional_subdomains = is_ingress ? ingress_data.fetch(:additional_subdomains, []) : [] memory_limit = memory_limit(deploy_data) { tag: tag(image_data, repo_attributes), port: port(container_name, ingress_data), full_image_name: full_image_name(image_data, build_data, repo_attributes), image: image(container_data, image_data, build_data, credentials), public: is_ingress, entrypoint: entrypoint(container_data), command: command(container_data), variables: variables(environment, env_file_dependencies), secret_variables: secret_variables(secrets), memory_limit: memory_limit, memory_request: memory_request(memory_limit), repo_attributes: repo_attributes, continuously_deploy: continuously_deploy(deploy_data), receive_incoming_requests: is_ingress, container_config_files_attributes: container_config_files_attributes(configs_data, configs_dependencies), service_name: container_name, name: container_name, healthcheck: healthcheck_data, volumes: volumes_data, additional_subdomains: additional_subdomains, container_host_volume_files_attributes: container_host_volume_files_attributes(volumes_data, host_volumes_dependencies), } end # rubocop:enable Metrics/PerceivedComplexity private def container_registry(container_data) @container_registry ||= UffizziCore::ContainerRegistryService.init_by_container(container_data, @credentials) end def repo_attributes(container_data, continuous_preview_global_data) repo_attributes = build_repo_attributes(container_data) continuous_preview_container_data = container_data[:'x-uffizzi-continuous-preview'] || container_data[:'x-uffizzi-continuous-previews'] set_continuous_preview_attributes_to_repo(repo_attributes, continuous_preview_global_data.to_h, continuous_preview_container_data.to_h) end def build_repo_attributes(container_data) container_registry = container_registry(container_data) repo_type = container_registry.repo_type.name raise UffizziCore::ComposeFile::BuildError, I18n.t('compose.invalid_repo_type') if repo_type.blank? image_data = container_registry.image_data if container_registry.image_available?(credentials) docker_repo_builder = UffizziCore::ComposeFile::Builders::DockerRepoBuilderService.new(repo_type) return docker_repo_builder.build_attributes(image_data) end UffizziCore::ComposeFile::ErrorsService.raise_build_error!(container_registry.type) rescue UffizziCore::ContainerRegistryError => e UffizziCore::ComposeFile::ErrorsService.raise_build_error!(container_registry.type, e.errors) end def set_continuous_preview_attributes_to_repo(repo_attributes, global_data, container_data) condition_attributes = [ :deploy_preview_when_pull_request_is_opened, :delete_preview_when_pull_request_is_closed, :deploy_preview_when_image_tag_is_created, :delete_preview_when_image_tag_is_updated, :share_to_github, ] condition_attributes.each do |attribute| repo_attributes[attribute] = select_continuous_preview_attribute(global_data[attribute], container_data[attribute], false) end global = global_data.dig(:delete_preview_after, :value) local = container_data.dig(:delete_preview_after, :value) repo_attributes[:delete_preview_after] = select_continuous_preview_attribute(global, local, nil) repo_attributes end def select_continuous_preview_attribute(global_attribute, local_attribute, default_attribute) return local_attribute if local_attribute.present? return global_attribute if global_attribute.present? default_attribute end def tag(image_data, repo_attributes) image_data[:tag] || repo_attributes[:branch] end def port(container_name, ingress) return nil unless ingress_container?(container_name, ingress) ingress[:port] end def image(container_data, image_data, build_data, credentials) if image_data.present? container_registry(container_data).image_name(credentials) else "#{build_data[:account_name]}/#{build_data[:repository_name]}" end end def image_name(container_data) container_registry = container_registry(container_data) container_registry.image_name(credentials) end def full_image_name(image_data, build_data, repo_attributes) return image_data[:full_image_name] if image_data.present? "#{build_data[:account_name]}/#{build_data[:repository_name]}:#{repo_attributes[:branch]}" end def ingress_container?(container_name, ingress) ingress[:container_name] == container_name end def entrypoint(container_data) entrypoint = container_data[:entrypoint] entrypoint.present? ? entrypoint.to_s : nil end def command(container_data) command = container_data[:command] command.present? ? command.to_s : nil end def command_args(command_data) return nil if command_data[:command_args].blank? command_data[:command_args].to_s end def memory_limit(deploy_data) memory = deploy_data[:memory] return Settings.compose.default_memory if memory.nil? memory_value = case memory[:postfix] when 'b' memory[:value] / 1_000_000 when 'k' memory[:value] / 1000 when 'm' memory[:value] when 'g' memory[:value] * 1000 end unless Settings.compose.memory_values.include?(memory_value) raise UffizziCore::ComposeFile::BuildError, I18n.t('compose.invalid_memory') end memory_value end def memory_request(memory) memory / UffizziCore::Container::REQUEST_MEMORY_RATIO end def continuously_deploy(deploy_data) return :disabled if deploy_data[:auto] == false :enabled end def variables(variables_data, dependencies) variables_builder.build_attributes(variables_data, dependencies) end def secret_variables(secrets) variables_builder.build_secret_attributes(secrets) end def container_config_files_attributes(config_files_data, dependencies) UffizziCore::ComposeFile::Builders::ContainerConfigFilesBuilderService.build_attributes(config_files_data, dependencies, project) end def container_host_volume_files_attributes(volumes_data, host_volumes_dependencies) host_volumes_data = volumes_data.select do |v| v[:type] == UffizziCore::ComposeFile::Parsers::Services::VolumesParserService::HOST_VOLUME_TYPE end UffizziCore::ComposeFile::Builders::ContainerHostVolumeFilesBuilderService .build_attributes(host_volumes_data, host_volumes_dependencies, project) end def variables_builder @variables_builder ||= UffizziCore::ComposeFile::Builders::VariablesBuilderService.new(project) end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/container_config_files_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::ContainerConfigFilesBuilderService class << self def build_attributes(config_files_data, dependencies, project) return [] if config_files_data.empty? config_file_sources = dependencies.pluck(:source) config_files = project.config_files.with_creation_source(UffizziCore::ConfigFile.creation_source.compose_file) .by_source(config_file_sources) config_files_data.map do |config_file_data| detected_dependency = dependencies.detect { |dependency| dependency[:path] == config_file_data[:source] } detected_config_file = config_files.detect { |config_file| config_file.source == detected_dependency[:source] } if detected_config_file.nil? raise UffizziCore::ComposeFile::BuildError, I18n.t('compose.config_file_not_found', name: config_file_data[:source]) end { mount_path: config_file_data[:target], config_file_id: detected_config_file.id, } end end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/container_host_volume_files_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::ContainerHostVolumeFilesBuilderService class << self def build_attributes(container_host_volumes_data, host_volumes_dependencies, project) return [] if container_host_volumes_data.empty? host_volume_files = project .host_volume_files .by_source(host_volumes_dependencies.pluck(:source)) container_host_volumes_data.map do |container_host_volume_data| detected_dependency = host_volumes_dependencies.detect do |dependency| dependency[:raw_source] == container_host_volume_data[:source] end detected_host_volume_file = host_volume_files.detect { |host_volume_file| host_volume_file.source == detected_dependency[:source] } if detected_host_volume_file.nil? raise UffizziCore::ComposeFile::BuildError, I18n.t('compose.host_volume_file_not_found', name: container_host_volume_data[:source]) end { source_path: container_host_volume_data[:source], host_volume_file_id: detected_host_volume_file.id, } end end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/docker_repo_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::DockerRepoBuilderService attr_accessor :type def initialize(type) @type = type end def build_attributes(image_data) { kind: nil, name: image_data[:name], slug: image_data[:name], type: type, branch: nil, namespace: image_data[:namespace], is_private: nil, # TODO: detect description: '', repository_id: nil, dockerfile_path: '', dockerfile_context_path: nil, } end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/template_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::TemplateBuilderService attr_accessor :credentials, :project, :repositories def initialize(credentials, project, repositories = []) @credentials = credentials @project = project @repositories = repositories end def build_attributes(compose_data, compose_dependencies, source) containers_data = compose_data[:containers] ingress_data = compose_data[:ingress] continuous_preview_global_data = compose_data[:continuous_preview] containers_attributes = build_containers_attributes( containers_data, ingress_data, continuous_preview_global_data, compose_dependencies, ) { name: source, payload: { containers_attributes: containers_attributes, }, } end private def build_containers_attributes(containers_data, ingress_data, continuous_preview_global_data, compose_dependencies) containers_data.map do |container_data| builder = UffizziCore::ComposeFile::Builders::ContainerBuilderService.new(credentials, project, repositories) builder.build_attributes(container_data, ingress_data, continuous_preview_global_data, compose_dependencies) end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/builders/variables_builder_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Builders::VariablesBuilderService attr_accessor :project require 'dotenv' def initialize(project) @project = project end def build_attributes(variables_data, dependencies) variables = variables_data variables_from_dependencies = variables_from_dependencies(dependencies) variables + variables_from_dependencies end def build_secret_attributes(secrets) project_secrets = project.secrets || [] secrets.uniq.map do |secret| detected_secret = project_secrets.detect { |project_secret| project_secret['name'] == secret } error_message = I18n.t('compose.project_secret_not_found', secret: secret) raise UffizziCore::ComposeFile::SecretsError, error_message if detected_secret.nil? build_variable(detected_secret['name'], detected_secret['value']) end end private def variables_from_dependencies(dependencies) variables = dependencies.map do |dependency| variables_data = parse_variables_from_dependency(dependency) variables_data.map { |variable_data| build_variable(variable_data.first, variable_data.last) } end variables.flatten end def parse_variables_from_dependency(dependency) content = dependency[:content] return [] if content.blank? variables_content = UffizziCore::ComposeFile::GithubDependenciesService.content(dependency) parser = Dotenv::Parser.new(variables_content) parser.call.to_a end def build_variable(name, value) { name: name, value: value, } end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/config_files_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::ConfigFilesService def initialize(compose_file_form) @compose_file_form = compose_file_form @repository_id = compose_file_form.repository_id @branch = compose_file_form.branch @path = compose_file_form.path @user = compose_file_form.added_by @project = compose_file_form.project end def create_config_files(compose_dependencies) configs_dependencies = UffizziCore::ComposeFile::GithubDependenciesService.configs_dependencies(compose_dependencies) errors = [] configs_dependencies.each do |config_dependency| errors = create_config_file(config_dependency) errors << errors if errors end errors end private def create_config_file(config_dependency) source = UffizziCore::ComposeFile::GithubDependenciesService.build_source_path(@path, config_dependency[:path], @repository_id, @branch) config_file = @project.config_files.find_or_initialize_by(source: source) attributes = { filename: UffizziCore::ComposeFile::GithubDependenciesService.filename(config_dependency), payload: UffizziCore::ComposeFile::GithubDependenciesService.content(config_dependency), } config_file.assign_attributes(attributes) config_file_form = build_config_file_form(config_file) return config_file_form.errors if config_file_form.invalid? config_file_form.save nil end def build_config_file_form(config_file) config_file_form = config_file.becomes(UffizziCore::Api::Cli::V1::ConfigFile::CreateForm) config_file_form.project = @project config_file_form.added_by = @user config_file_form.compose_file = @compose_file_form config_file_form.creation_source = UffizziCore::ConfigFile.creation_source.compose_file config_file_form end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/config_option_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::ConfigOptionService class << self def valid_option_format?(option) if option.is_a?(TrueClass) || option.is_a?(FalseClass) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.boolean_option', value: option) end option.match(/^[\w.-]+$/).present? end def config_options(compose_data) compose_data.each_with_object([]) do |(key, value), keys| if compose_data.equal?(value) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.infinite_recursion', key: key) end keys << key keys.concat(config_options(value)) if value.is_a?(Hash) end end def prepare_file_path_value(file_path) pathname = Pathname.new(file_path) pathname.cleanpath.to_s.strip.delete_prefix('/') end def ingress_option(compose_data) compose_data.dig('x-uffizzi', 'ingress').presence || compose_data['x-uffizzi-ingress'].presence end def continuous_preview_option(compose_data) compose_data.dig('x-uffizzi', 'continuous_preview').presence || compose_data.dig('x-uffizzi', 'continuous_previews').presence || compose_data['x-uffizzi-continuous-preview'].presence || compose_data['x-uffizzi-continuous-previews'].presence end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/container_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::ContainerService class << self def has_secret?(container, secret) container['secret_variables'].any? { |container_secret| container_secret['name'] == secret['name'] } end def update_secret(container, secret) secret_index = container['secret_variables'].find_index { |container_secret| container_secret['name'] == secret['name'] } container['secret_variables'][secret_index] = secret container end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/dependencies_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::DependenciesService ENV_FILE_TYPE = 'env_file' CONFIG_TYPE = 'config' VOLUME_TYPE = 'volume' DEPENDENCY_CONFIG_USE_KIND = 'config_map' DEPENDENCY_VOLUME_USE_KIND = 'volume' class << self def build_dependencies(compose_data, compose_path, dependencies_params) config_dependencies_params = dependencies_params.select { |d| d[:use_kind] == DEPENDENCY_CONFIG_USE_KIND } volume_dependencies_params = dependencies_params.select { |d| d[:use_kind] == DEPENDENCY_VOLUME_USE_KIND } dependencies = compose_data[:containers].map do |container| env_file_dependencies = build_env_files_dependencies(container, compose_path, config_dependencies_params) configs_dependencies = build_configs_dependencies(container, compose_path, config_dependencies_params) volumes_dependencies = build_volumes_dependencies(container, compose_path, volume_dependencies_params) env_file_dependencies + configs_dependencies + volumes_dependencies end dependencies.compact.flatten end def build_env_files_dependencies(container, compose_path, dependencies_params) env_files = container[:env_file] return [] unless env_files.present? env_files.map do |path| dependency = dependencies_params.detect { |item| item[:path] == path } source = build_source_path(compose_path, path) base_file_params(dependency, container).merge(source: source, type: ENV_FILE_TYPE) end end def build_configs_dependencies(container, compose_path, dependencies_params) configs = container[:configs] return [] unless configs.present? configs.map do |config| dependency = dependencies_params.detect { |item| item[:path] == config[:source] } source = build_source_path(compose_path, dependency[:path]) base_file_params(dependency, container).merge(source: source, type: CONFIG_TYPE) end end def build_volumes_dependencies(container, compose_path, raw_dependencies) container_volumes = container[:volumes] return [] unless container_volumes.present? container_volumes .select { |c| c[:type] == UffizziCore::ComposeFile::Parsers::Services::VolumesParserService::HOST_VOLUME_TYPE } .map do |container_volume| detected_raw_dependency = raw_dependencies.detect { |raw_dependency| raw_dependency[:source] == container_volume[:source] } builded_source = build_source_path(compose_path, detected_raw_dependency[:source]) { content: detected_raw_dependency[:content], path: detected_raw_dependency[:path], container_name: container[:container_name], source: builded_source, raw_source: detected_raw_dependency[:source], type: VOLUME_TYPE, is_file: detected_raw_dependency[:is_file], } end end def base_file_params(dependency, container) { content: dependency[:content], path: dependency[:path], container_name: container[:container_name], } end def build_source_path(compose_path, dependency_path) prepared_compose_path = Pathname.new(compose_path).basename.to_s "#{prepared_compose_path}/#{dependency_path}" end def host_volume_binary_content(dependency) Base64.decode64(dependency[:content]) end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/errors_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::ErrorsService SECRETS_ERROR_KEY = 'secret_variables' TEMPLATE_BUILD_ERROR_KEY = 'template_build_error' DOCKER_REGISTRY_CONTAINER_ERROR_KEY = 'docker_registry_container_error' class << self def has_error?(compose_file, error_code) error = compose_file.payload.dig('errors', error_code) error.present? end def has_errors?(compose_file) compose_file.payload['errors'].present? end def update_compose_errors!(compose_file, errors, invalid_content) compose_file.payload['errors'] = errors compose_file.set_invalid if compose_file.valid_file? compose_file.content = invalid_content compose_file.save! compose_file end def reset_compose_errors!(compose_file) compose_file.payload['errors'] = nil compose_file.set_valid compose_file.save! compose_file end def reset_error!(compose_file, error_code) errors = compose_file.payload['errors'] return if errors.nil? new_errors = errors.except(error_code) compose_file.payload['errors'] = new_errors compose_file.save! compose_file end def raise_build_error!(type, extra_errors = {}) msg = I18n.t('compose.unprocessable_image', value: type) raise UffizziCore::ComposeFile::BuildError.new(msg, extra_errors) end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/github_dependencies_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::GithubDependenciesService ENV_FILE_TYPE = 'env_file' CONFIG_TYPE = 'config' VOLUME_TYPE = 'volume' class << self def filename(dependency) pathname = Pathname.new(dependency[:path]) pathname.basename.to_s end def content(dependency) Base64.decode64(dependency[:content]) end def env_file_dependencies_for_container(dependencies, container_name) dependencies.select { |dependency| dependency[:type] == ENV_FILE_TYPE && dependency[:container_name] == container_name } end def configs_dependencies_for_container(dependencies, container_name) configs_dependencies(dependencies).select { |dependency| dependency[:container_name] == container_name } end def host_volumes_dependencies_for_container(dependencies, container_name) dependencies.select { |dependency| dependency[:type] == VOLUME_TYPE && dependency[:container_name] == container_name } end def configs_dependencies(dependencies) dependencies.select { |dependency| dependency[:type] == CONFIG_TYPE } end def select_dependencies_by_type(dependencies, type) dependencies.select { |dependency| dependency[:type].to_s == type.to_s } end def build_source_path(compose_path, dependency_path, repository_id, branch) prepared_compose_path = Pathname.new(compose_path).basename.to_s base_source = "#{prepared_compose_path}/#{dependency_path}" return base_source if repository_id.blank? "#{repository_id}/#{branch}/#{base_source}" end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/host_volume_files_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::HostVolumeFilesService class << self def bulk_create(compose_file_form, compose_dependencies) volumes_dependencies = UffizziCore::ComposeFile::GithubDependenciesService .select_dependencies_by_type(compose_dependencies, UffizziCore::ComposeFile::DependenciesService::VOLUME_TYPE) errors = [] volumes_dependencies.each do |volume_dependency| new_errors = create(compose_file_form, volume_dependency) errors << new_errors if new_errors end errors end def create(compose_file_form, volume_dependency) source = volume_dependency[:source] host_volume_file = compose_file_form.project.host_volume_files.find_or_initialize_by(source: source) attributes = { payload: UffizziCore::ComposeFile::DependenciesService.host_volume_binary_content(volume_dependency), source: source, path: volume_dependency[:path], is_file: volume_dependency[:is_file], } host_volume_file.assign_attributes(attributes) host_volume_file.project = compose_file_form.project host_volume_file.added_by = compose_file_form.added_by host_volume_file.compose_file = compose_file_form return host_volume_file.errors if host_volume_file.invalid? host_volume_file.save nil end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/configs_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::ConfigsParserService class << self def parse(configs_data) return [] if configs_data.nil? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :configs) unless configs_data.is_a?(Hash) configs = [] configs_data.each_pair do |config_name, config_data| if config_data['file'].blank? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.config_file_option_empty', config_name: config_name) end configs << { config_name: config_name, config_file: config_data['file'], } end configs end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/continuous_preview_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::ContinuousPreviewParserService class << self def parse(continuous_preview_data) return {} if continuous_preview_data.nil? { deploy_preview_when_pull_request_is_opened: trigger_value(continuous_preview_data, 'deploy_preview_when_pull_request_is_opened'), delete_preview_when_pull_request_is_closed: trigger_value(continuous_preview_data, 'delete_preview_when_pull_request_is_closed'), deploy_preview_when_image_tag_is_created: trigger_value(continuous_preview_data, 'deploy_preview_when_image_tag_is_created'), delete_preview_when_image_tag_is_updated: trigger_value(continuous_preview_data, 'delete_preview_when_image_tag_is_updated'), delete_preview_after: delete_preview_after_value(continuous_preview_data['delete_preview_after']), share_to_github: trigger_value(continuous_preview_data, 'share_to_github'), } end private def trigger_value(continuous_preview_data, field) value = continuous_preview_data[field] return nil if value.nil? return value if value.in?([true, false]) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_bool_value', field: field, value: value) end def delete_preview_after_value(value) return {} if value.blank? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_string', option: :delete_preview_after) unless value.is_a?(String) hours, postfix = value.scan(/^([0-9]+)([a-zA-Z])$/).flatten raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_integer', option: :delete_preview_after) if hours.nil? formatted_hours = hours.to_i if formatted_hours < Settings.compose.delete_after_min_value raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_delete_after_min', value: Settings.compose.delete_after_min_value) end if formatted_hours > Settings.compose.delete_after_max_value raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_delete_after_max', value: Settings.compose.delete_after_max_value) end if postfix.nil? || !Settings.compose.delete_after_postfixes.include?(postfix.downcase) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_delete_after_postfix') end { value: formatted_hours, postfix: postfix.downcase, } end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/ingress_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::IngressParserService class << self def parse(ingress_data, services_data) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.no_ingress') if ingress_data.nil? container_name = container_name(ingress_data, services_data) port = port(ingress_data) additional_attributes = build_additional_attributes(ingress_data, services_data) { container_name: container_name, port: port, }.merge(additional_attributes) end private def container_name(ingress_data, services_data) container_name = ingress_data['service'] if container_name.nil? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.ingress_service_not_found') end unless services_data.keys.include?(container_name) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_ingress_service', value: container_name) end container_name end def port(ingress_data) port = ingress_data['port'] if port.nil? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.ingress_port_not_specified') end raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_integer', option: :port) unless port.is_a?(Integer) port_min = Settings.compose.port_min_value port_max = Settings.compose.port_max_value if port < port_min || port > port_max raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.port_out_of_range', port_min: port_min, port_max: port_max) end port end def build_additional_attributes(*) {} end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/named_volumes_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::NamedVolumesParserService VALID_VOLUME_NAME_REGEX = /^[a-zA-Z0-9._-]+$/.freeze class << self def parse(volumes_data) return [] if volumes_data.nil? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :volumes) unless volumes_data.is_a?(Hash) volume_names = volumes_data.keys volume_names.each do |volume_name| unless volume_name.match?(VALID_VOLUME_NAME_REGEX) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volume_invalid_name', name: volume_name) end end volume_names end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/secrets_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::SecretsParserService class << self def parse(secrets_data) return [] if secrets_data.nil? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :secrets) unless secrets_data.is_a?(Hash) secrets = [] secrets_data.each_pair do |secret_name, secret_data| raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.secret_name_blank', option: secret_name) if secret_data['name'].blank? if secret_data['external'] != true raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.secret_external', secret: secret_name) end secrets << { secret_name: secret_name, secret_variable: secret_data['name'], } end secrets end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/command_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::CommandParserService class << self def parse(command_data) return nil if command_data.blank? case command_data when String Shellwords.split(command_data) when Array command_data else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :command) end end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/configs_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::ConfigsParserService class << self def parse(configs, global_configs_data) return [] if configs.nil? configs.map do |config| config_data = case config when String process_short_syntax(config, global_configs_data) when Hash process_long_syntax(config, global_configs_data) else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :configs) end config_data end end private def process_short_syntax(config_name, global_configs_data) global_config = find_global_config!(config_name, global_configs_data) { source: UffizziCore::ComposeFile::ConfigOptionService.prepare_file_path_value(global_config[:config_file]), target: global_config[:config_file], } end def process_long_syntax(config_data, global_configs_data) global_config = find_global_config!(config_data['source'], global_configs_data) target = config_data['target'].blank? ? global_config[:config_file] : config_data['target'] { source: UffizziCore::ComposeFile::ConfigOptionService.prepare_file_path_value(global_config[:config_file]), target: target, } end def find_global_config!(config_name, global_configs_data) detected_config = global_configs_data.detect { |global_config_data| global_config_data[:config_name] == config_name } raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.global_config_not_found', config: config_name) if detected_config.nil? detected_config end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/deploy_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::DeployParserService class << self def parse(deploy_data) return {} if deploy_data.blank? auto = prepare_deploy_auto(deploy_data) memory = prepare_memory(deploy_data) { auto: auto, memory: memory, } end private def prepare_deploy_auto(deploy_data) auto = deploy_data['x-uffizzi-auto-deploy-updates'] return auto if auto.nil? || auto.in?([true, false]) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_bool_value', field: :auto, value: auto) end def prepare_memory(deploy_data) memory = deploy_data.dig('resources', 'limits', 'memory') return nil if memory.blank? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_memory_type', value: memory) unless memory.is_a?(String) value, postfix = memory.scan(/^([0-9]+)([a-zA-Z]+)$/).flatten if postfix.nil? || !Settings.compose.memory_postfixes.include?(postfix.downcase) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_memory_postfix', value: memory) end { value: value.to_i, postfix: postfix.downcase, } end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/entrypoint_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::EntrypointParserService class << self def parse(entrypoint_data) return nil if entrypoint_data.blank? case entrypoint_data when String [entrypoint_data] when Array entrypoint_data else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :entrypoint) end end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/env_file_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::EnvFileParserService class << self def parse(env_file) env_files = case env_file when String [prepare_file_path(env_file)] when Array env_file.map { |env_file_path| prepare_file_path(env_file_path) } else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :env_file) end check_duplicates(env_files) env_files end private def prepare_file_path(env_file_path) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.empty_env_file') if env_file_path.blank? UffizziCore::ComposeFile::ConfigOptionService.prepare_file_path_value(env_file_path) end def check_duplicates(env_files) return if env_files.uniq.length == env_files.length raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.env_file_duplicates', values: env_files) end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/environment_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::EnvironmentParserService extend UffizziCore::ComposeFile::Parsers::VariablesParserService class << self def parse(environment) return [] if environment.blank? case environment when Array environment.map { |variable| parse_variable_from_string(variable) } when Hash environment.to_a.map { |variable| parse_variable_from_array(variable) } else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :environment) end end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/healthcheck_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::HealthcheckParserService REQUIRED_START_COMMANDS = ['NONE', 'CMD', 'CMD-SHELL'].freeze REQUIRED_OPTIONS = ['test', 'disable'].freeze class << self def parse(healthcheck_data) return {} if healthcheck_data.blank? unless required_options_any?(healthcheck_data) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.healthcheck_missing_required_option', required_options: REQUIRED_OPTIONS.join(', ')) end command = parse_command(healthcheck_data) if healthcheck_data['test'].present? { test: command, interval: parse_time(:interval, healthcheck_data['interval']), timeout: parse_time(:timeout, healthcheck_data['timeout']), retries: parse_retries(healthcheck_data['retries']), start_period: parse_time(:start_period, healthcheck_data['start_period']), disable: parse_disable_option(healthcheck_data['disable'], command), } end private def parse_command(healthcheck_data) command = healthcheck_data['test'] raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.string_or_array_error', option: :test) if command.nil? case command when Array start_command = command.first unless REQUIRED_START_COMMANDS.include?(start_command) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.required_start_commands', available_commands: REQUIRED_START_COMMANDS.join(', ')) end command when String command else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :test) end end def parse_retries(value) return if value.nil? unless value.is_a?(Integer) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_retries', value: value) end value end def parse_time(key, value) return if value.nil? error_message = I18n.t('compose.invalid_time_interval', key: key, value: value) raise UffizziCore::ComposeFile::ParseError, error_message if value.is_a?(Integer) tokens = { 's' => 1, 'm' => 60, 'h' => (60 * 60), 'd' => (60 * 60 * 24), } time_parts = value.scan(/(\d+)(\w)/).compact time_parts.reduce(0) do |acc, part| amount, measure = part acc += amount.to_i * tokens[measure] acc rescue StandardError raise UffizziCore::ComposeFile::ParseError, error_message end end def parse_disable_option(value, command) return true if command.is_a?(Array) && command.first == 'NONE' return false if value.nil? return value if value.in?([true, false]) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_bool_value', field: 'disable', value: value) end def required_options_any?(healthcheck_data) (REQUIRED_OPTIONS & healthcheck_data.keys).present? end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/image_parser_service.rb ================================================ # frozen_string_literal: true require 'docker_distribution' class UffizziCore::ComposeFile::Parsers::Services::ImageParserService DEFAULT_TAG = Settings.compose.default_tag class << self def parse(value) return {} if value.blank? parsed_image = DockerDistribution::Normalize.parse_docker_ref(value) image_path = parsed_image.path namespace, name = get_namespace_and_name(image_path) full_image_name = "#{[parsed_image.domain, parsed_image.path].compact.join('/')}:#{parsed_image.tag}" { registry_url: parsed_image.domain, namespace: namespace, name: name, tag: parsed_image.tag, full_image_name: full_image_name, } rescue DockerDistribution::NameContainsUppercase raise_parse_error(I18n.t('compose.image_name_contains_uppercase_value', value: value)) rescue DockerDistribution::ReferenceInvalidFormat, DockerDistribution::ParseNormalizedNamedError raise_parse_error(I18n.t('compose.invalid_image_value', value: value)) end private def raise_parse_error(message) raise UffizziCore::ComposeFile::ParseError, message end def get_namespace_and_name(image_path) return [nil, image_path] unless image_path.index('/').present? image_path.split('/') end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/secrets_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::SecretsParserService class << self def parse(secrets, global_secrets_data) return [] if secrets.nil? secrets.map do |secret| variable_name = if secret.is_a?(String) process_short_syntax(secret, global_secrets_data) else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :secrets) end variable_name end end private def process_short_syntax(secret_name, global_secrets_data) global_secret = find_global_secret!(secret_name, global_secrets_data) global_secret[:secret_variable] end def find_global_secret!(secret_name, global_secrets_data) detected_secret = global_secrets_data.detect { |global_secret_data| global_secret_data[:secret_name] == secret_name } error_message = I18n.t('compose.global_secret_not_found', secret: secret_name) raise UffizziCore::ComposeFile::ParseError, error_message if detected_secret.nil? detected_secret end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services/volumes_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::Services::VolumesParserService HOST_VOLUME_TYPE = :host NAMED_VOLUME_TYPE = :named ANONYMOUS_VOLUME_TYPE = :anonymous READONLY_OPTION = 'ro' READ_WRITE_OPTION = 'rw' class << self def parse(volumes, additional_data) return [] if volumes.blank? if volumes.is_a?(String) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volumes_should_be_array', volumes: volumes) end volumes.map do |volume| volume_data = case volume when String process_short_syntax(volume, additional_data) when Hash process_long_syntax(volume, additional_data) else raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_type', option: :volumes) end volume_data end.uniq end private def process_short_syntax(volume_data, additional_data) volume_parts = volume_data.split(':').map(&:strip) read_only = volume_parts.last.to_s.downcase == READONLY_OPTION part1, part2 = volume_parts source_path = part1 target_path = [READONLY_OPTION, READ_WRITE_OPTION].include?(part2.to_s.downcase) ? nil : part2 raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volume_prop_is_required', prop_name: 'source') if source_path.blank? build_volume_attributes(source_path, target_path, read_only, additional_data) end def process_long_syntax(volume_data, additional_data) source_path = volume_data['source'].to_s.strip target_path = volume_data['target'].to_s.strip read_only = volume_data['read_only'].present? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volume_prop_is_required', prop_name: 'source') if source_path.blank? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volume_prop_is_required', prop_name: 'target') if target_path.blank? build_volume_attributes(source_path, target_path, read_only, additional_data) end def build_volume_attributes(source_path, target_path, read_only, params = {}) volume_type = build_volume_type(source_path, target_path) if volume_type == NAMED_VOLUME_TYPE validate_named_volume(source_path, target_path, params[:named_volumes_names], params[:service_name]) end if volume_type == ANONYMOUS_VOLUME_TYPE validate_anonymous_volume(source_path) end { source: source_path, target: target_path, type: volume_type, read_only: read_only, } end def build_volume_type(source_path, target_path) return HOST_VOLUME_TYPE if path?(source_path) && path?(target_path) return ANONYMOUS_VOLUME_TYPE if path?(source_path) && target_path.blank? return NAMED_VOLUME_TYPE if source_path.present? && !path?(source_path) && path?(target_path) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.volume_path_is_invalid', path: [source_path, target_path].join(':')) end def path?(path) path.to_s.start_with?('/', './', '../') end def validate_named_volume(source_path, target_path, named_volumes_names, service_name) if path_has_only_root?(target_path) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_volume_destination', spec: "#{source_path}:#{target_path}") end return if named_volumes_names.include?(source_path) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.named_volume_not_exists', source_path: source_path, target_path: target_path, service_name: service_name) end def validate_anonymous_volume(path) if path_has_only_root?(path) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_volume_destination', spec: path) end end def path_has_only_root?(path) path.size == 1 && path.include?('/') end end end ================================================ FILE: core/app/services/uffizzi_core/compose_file/parsers/services_parser_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ComposeFile::Parsers::ServicesParserService class << self include UffizziCore::DependencyInjectionConcern def parse(services, global_configs_data, global_secrets_data, compose_payload, global_named_volume_names) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.no_services') if services.nil? || services.keys.empty? services.keys.map do |service| unless valid_service_name?(service) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_service_name', value: service) end service_data = prepare_service_data(service, services.fetch(service), global_configs_data, global_secrets_data, compose_payload, global_named_volume_names) if service_data[:image].blank? && service_data[:build].blank? raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.image_build_no_specified', value: service) end service_data end end private def prepare_service_data(service_name, service_data, global_configs_data, global_secrets_data, compose_payload, global_named_volume_names) options_data = {} service_data.each_pair do |key, value| service_key = key.to_sym options_data[service_key] = case service_key when :image UffizziCore::ComposeFile::Parsers::Services::ImageParserService.parse(value) when :build check_and_parse_build_option(value, compose_payload) when :env_file UffizziCore::ComposeFile::Parsers::Services::EnvFileParserService.parse(value) when :environment UffizziCore::ComposeFile::Parsers::Services::EnvironmentParserService.parse(value) when :configs UffizziCore::ComposeFile::Parsers::Services::ConfigsParserService.parse(value, global_configs_data) when :secrets UffizziCore::ComposeFile::Parsers::Services::SecretsParserService.parse(value, global_secrets_data) when :deploy UffizziCore::ComposeFile::Parsers::Services::DeployParserService.parse(value) when :entrypoint UffizziCore::ComposeFile::Parsers::Services::EntrypointParserService.parse(value) when :command UffizziCore::ComposeFile::Parsers::Services::CommandParserService.parse(value) when :healthcheck UffizziCore::ComposeFile::Parsers::Services::HealthcheckParserService.parse(value) when :'x-uffizzi-continuous-preview', :'x-uffizzi-continuous-previews' UffizziCore::ComposeFile::Parsers::ContinuousPreviewParserService.parse(value) when :volumes parse_volumes(value, named_volumes_names: global_named_volume_names, service_name: service_name, compose_payload: compose_payload) end end options_data[:container_name] = service_name options_data end def check_and_parse_build_option(value, compose_payload) build_parser_module = find_build_parser_module raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.build_not_implemented') unless build_parser_module build_parser_module.parse(value, compose_payload) end def parse_volumes(volumes, additional_data) volume_parser_module = find_volume_parser_module return UffizziCore::ComposeFile::Parsers::Services::VolumesParserService.parse(volumes, additional_data) unless volume_parser_module volume_parser_module.parse(volumes, additional_data) end # All services are added as Additional Hosts in Kubernetes and must be RFC 123 compliant def valid_service_name?(name) rfc_123_pattern = /\A(?!-)[a-z0-9-]{1,63}(? new_secrets_errors } UffizziCore::ComposeFile::ErrorsService.update_compose_errors!(compose_file, compose_file_errors.merge(new_errors), compose_file.content) next end compose_file_errors.delete(['secret_variables']) next UffizziCore::ComposeFile::ErrorsService.reset_compose_errors!(compose_file) if compose_file_errors.empty? UffizziCore::ComposeFile::ErrorsService.update_compose_errors!(compose_file, compose_file_errors, compose_file.content) end compose_file.template.save! compose_file end def secrets_valid?(compose_file, secrets) secret_names = secrets.pluck('name') compose_file.template.payload['containers_attributes'].all? do |container| container['secret_variables'].all? { |secret| secret_names.include?(secret['name']) } end end def create_temporary_compose(resource_project, current_user, compose_file_params, dependencies) create_params = { project: resource_project, user: current_user, compose_file_params: compose_file_params, dependencies: dependencies || [] } kind = UffizziCore::ComposeFile.kind.temporary UffizziCore::ComposeFileService.create(create_params, kind) end private def process_compose_file(compose_file_form, params) cli_form = UffizziCore::Api::Cli::V1::ComposeFile::CliForm.new cli_form.content = compose_file_form.content cli_form.compose_file = compose_file_form.becomes(UffizziCore::ComposeFile) return [compose_file_form, cli_form.errors] if cli_form.invalid? dependencies = params[:dependencies].to_a compose_data = cli_form.compose_data compose_dependencies = build_compose_dependecies(compose_data, compose_file_form.path, dependencies) cli_form.compose_dependencies = compose_dependencies persist!(compose_file_form, cli_form) end def create_compose_form(params, kind) compose_file_params = params[:compose_file_params] compose_file_form = UffizziCore::Api::Cli::V1::ComposeFile::CreateForm.new(compose_file_params) compose_file_form.project = params[:project] compose_file_form.added_by = params[:user] compose_file_form.content = compose_file_params[:content] compose_file_form.kind = kind payload_dependencies = prepare_compose_file_dependencies(params[:dependencies]) compose_file_form.payload['dependencies'] = payload_dependencies compose_file_form end def create_update_compose_form(compose_file, params) compose_file_form = compose_file.becomes(UffizziCore::Api::Cli::V1::ComposeFile::UpdateForm) compose_file_params = params[:compose_file_params] compose_file_form.assign_attributes(compose_file_params) payload_dependencies = prepare_compose_file_dependencies(params[:dependencies]) compose_file_form.payload['dependencies'] = payload_dependencies compose_file_form end def build_compose_dependecies(compose_data, compose_path, dependencies) return [] if dependencies.empty? UffizziCore::ComposeFile::DependenciesService.build_dependencies(compose_data, compose_path, dependencies) end def prepare_compose_file_dependencies(compose_dependencies) compose_dependencies.map { |dependency| { path: dependency[:path] } } end def persist!(compose_file_form, cli_form) errors = [] ActiveRecord::Base.transaction do if !compose_file_form.save errors = compose_file_form.errors raise ActiveRecord::Rollback end config_files_service = UffizziCore::ComposeFile::ConfigFilesService.new(compose_file_form) config_file_errors = config_files_service.create_config_files(cli_form.compose_dependencies) host_volume_file_errors = UffizziCore::ComposeFile::HostVolumeFilesService .bulk_create(compose_file_form, cli_form.compose_dependencies) raise ActiveRecord::Rollback if config_file_errors.present? || host_volume_file_errors.present? project = compose_file_form.project user = compose_file_form.added_by template_service = UffizziCore::ComposeFile::TemplateService.new(cli_form, project, user) errors = template_service.create_template(compose_file_form) raise ActiveRecord::Rollback if errors.present? end [compose_file_form, errors] end def load_compose_data(compose_content) begin compose_data = YAML.safe_load(compose_content, aliases: true) rescue Psych::SyntaxError => e err = [e.problem, e.context].compact.join(' ') raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_file', err: err, line: e.line, column: e.column) end raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.unsupported_file') if compose_data.nil? || compose_data.is_a?(String) compose_data end def check_config_options_format(compose_data) options = UffizziCore::ComposeFile::ConfigOptionService.config_options(compose_data) options.each do |option| next if UffizziCore::ComposeFile::ConfigOptionService.valid_option_format?(option) raise UffizziCore::ComposeFile::ParseError, I18n.t('compose.invalid_config_option', value: option) end end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/amazon_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::AmazonService class << self def digest(credential, image, tag) response = client(credential).batch_get_image(image: image, tag: tag) response.images[0].image_id.image_digest rescue StandardError nil end def get_region_from_registry_url(url) parsed_url = URI.parse(url) host = parsed_url.host parsed_host = host.split('.') parsed_host[3] end def image_available?(credential, _image_data) credential.present? end def credential_correct?(credential) access_token(credential).present? end def access_token(credential) response = client(credential).authorization_token base64_data = response.authorization_data[0].authorization_token token_string = Base64.decode64(base64_data) token_items = token_string.split(':') token_items.pop rescue Aws::ECR::Errors::UnrecognizedClientException, Aws::ECR::Errors::InvalidSignatureException '' end private def client(credential) region = get_region_from_registry_url(credential.registry_url) UffizziCore::AmazonRegistryClient.new( region: region, access_key_id: credential.username, secret_access_key: credential.password, ) end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/azure_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::AzureService class << self def image_available?(credential, _image_data) credential.present? end def credential_correct?(credential) client(credential).authenticated? end def digest(*); end private def client(c) UffizziCore::AzureRegistryClient.new(registry_url: c.registry_url, username: c.username, password: c.password) end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/docker_hub_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::DockerHubService class << self def accounts(credential) client = user_client(credential) response = client.accounts Rails.logger.info("DockerHubService accounts response=#{response.inspect} credential_id=#{credential.id}") accounts_response = response.result accounts_response.nil? ? [] : accounts_response.namespaces end def image_available?(credential, image_data) namespace = image_data[:namespace] repo_name = image_data[:name] client(credential).repository(namespace: namespace, image: repo_name) true end def user_client(credential) return @client if @client&.credential&.username == credential.username @client = client(credential) unless @client.authenticated? Rails.logger.warn("broken credentials, DockerHubService credential_id=#{credential.id}") credential.unauthorize! unless credential.unauthorized? end @client end def digest(credential, image, tag) docker_hub_client = client(credential) token = docker_hub_client.get_token(image).result.token response = docker_hub_client.digest(image: image, tag: tag, token: token) response.headers['docker-content-digest'] # FIXME: can't get digest for private image: {"code":"UNAUTHORIZED","message":"authentication required"...} rescue UffizziCore::ContainerRegistryError 'unknown' end def credential_correct?(credential) client(credential).authenticated? end private def client(credential) UffizziCore::DockerHubClient.new(credential) end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/docker_registry_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::DockerRegistryService class << self def image_available?(credential, image_data) client_params = build_client_params(credential, image_data) client = UffizziCore::DockerRegistryClient.new(**client_params) client.manifests(namespace: image_data[:namespace], image: image_data[:name], tag: image_data[:tag]) true end def credential_correct?(credential) client(credential).authenticated? end def digest(*); end private def client(credential) params = { registry_url: credential.registry_url, username: credential.username, password: credential.password, } UffizziCore::DockerRegistryClient.new(params) end def build_client_params(credential, image_data) registry_url = credential&.registry_url || image_data[:registry_url] new_registry_url = registry_url.start_with?('https://', 'http://') ? registry_url : "https://#{registry_url}" { registry_url: new_registry_url, username: credential&.username, password: credential&.password, } end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/github_container_registry_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::GithubContainerRegistryService class << self def image_available?(credential, _image_data) credential.present? end def credential_correct?(credential) client(credential).authenticated? end def digest(*); end private def client(c) UffizziCore::GithubContainerRegistryClient.new(registry_url: c.registry_url, username: c.username, password: c.password) end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry/google_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistry::GoogleService class << self def digest(credential, image, tag) response = client(credential).manifests(image: image, tag: tag) response.headers['docker-content-digest'] end def image_available?(credential, _image_data) credential.present? end def credential_correct?(credential) client(credential).authenticated? end private def client(c) UffizziCore::GoogleRegistryClient.new(registry_url: c.registry_url, username: c.username, password: c.password) end end end ================================================ FILE: core/app/services/uffizzi_core/container_registry_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerRegistryService attr_accessor :type, :container_data class << self def init_by_subclass(credential_type) type = credential_type.demodulize.underscore new(type.to_sym) end def init_by_container(container, credentials) registry_url = container.dig(:image, :registry_url) return new(:docker_hub, container) if registry_url.include?('docker.io') return new(:azure, container) if registry_url.include?('azurecr.io') return new(:google, container) if registry_url.include?('gcr.io') return init_by_credentials(container, credentials) if registry_url.include?('amazonaws.com') return new(:github_container_registry, container) if registry_url.include?('ghcr.io') return new(:docker_registry, container) if docker_registry?(container) end def init_by_credentials(container, credentials) return new(:docker_registry, container) if credentials && credentials.docker_registry.where(username: 'AWS').exists? new(:amazon, container) end def docker_registry?(container) registry_url = container.dig(:image, :registry_url) return false if registry_url.nil? registry_domain_regexp = /(\w+\.\w{2,})(?::\d+)?\z/ registry_domain = registry_url.match(registry_domain_regexp)&.to_a&.last return false if registry_domain.nil? ['amazonaws.com', 'azurecr.io', 'gcr.io', 'ghcr.io'].exclude?(registry_domain) end def sources [:azure, :google, :amazon, :github_container_registry, :docker_registry, :docker_hub, *additional_sources] end def additional_sources [] end end def initialize(type, container_data = {}) @type = type @container_data = container_data raise ::UffizziCore::RegistryNotSupportedError unless self.class.sources.include?(type) end def digest(credential, image, tag) service.digest(credential, image, tag) end def service @service ||= "UffizziCore::ContainerRegistry::#{type.to_s.camelize}Service".safe_constantize end def repo_type @repo_type ||= "UffizziCore::Repo::#{type.to_s.camelize}".safe_constantize end def credential_correct?(credential) service.credential_correct?(credential) rescue URI::InvalidURIError, Faraday::ConnectionFailed, UffizziCore::ContainerRegistryError false end def image_data @image_data ||= container_data[:image] end def image_name(credentials) if image_data[:registry_url].present? && [:google, :github_container_registry, :docker_registry, :docker_hub].exclude?(type) return image_data[:name] end if type == :docker_registry && credential(credentials).nil? return [image_data[:registry_url], image_data[:namespace], image_data[:name]].compact.join('/') end "#{image_data[:namespace]}/#{image_data[:name]}" end def credential(credentials_scope) credentials_scope.send(type).first end def image_available?(credentials_scope) credential = credential(credentials_scope) service.image_available?(credential, image_data) end end ================================================ FILE: core/app/services/uffizzi_core/container_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ContainerService class << self def pod_name(container) return container.controller_name if container.controller_name.present? formatted_name = container.image_name formatted_name.parameterize.gsub('_', '-') end def target_port_value(container) container.port end def defines_env?(container, name) return false if container.variables.nil? container.variables.select { |v| v['name'].downcase.to_sym == name.to_sym }.any? end def continuously_deploy_enabled?(container) container.aasm(:continuously_deploy).current_state == UffizziCore::Container::STATE_CD_ENABLED end def last_state(container) pods = pods_by_container(container) container_status = container_status(container, pods) return {} if container_status.blank? || container_status&.dig('last_state')&.blank? container_status['last_state'].map do |code, state| { code: code, reason: state.reason, exit_code: state.exit_code, started_at: state.started_at, finished_at: state.finished_at, } end.first end def ingress_container?(container) container.receive_incoming_requests? end private def container_status(container, pods) pods .flat_map { |pod| pod&.status&.container_statuses } .compact .detect { |cs| cs.name.include?(container.controller_name) } end def pods_by_container(container) UffizziCore::ControllerService.fetch_pods(container.deployment) end end end ================================================ FILE: core/app/services/uffizzi_core/controller_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ControllerService class InvalidPublicPort < StandardError def initialize(containers) msg = "Deployment with ID #{containers.first.deployment.id} does not have any public port" super(msg) end end class << self include UffizziCore::DependencyInjectionConcern def apply_config_file(deployment, config_file) body = { config_file: UffizziCore::Controller::ApplyConfigFile::ConfigFileSerializer.new(config_file).as_json, } controller_client(deployment).apply_config_file(deployment_id: deployment.id, config_file_id: config_file.id, body: body) end def apply_credential(deployment, credential) image = if credential.github_container_registry? deployment.containers.by_repo_type(UffizziCore::Repo::GithubContainerRegistry.name).first&.image end options = { image: image } body = UffizziCore::Controller::CreateCredential::CredentialSerializer.new(credential, options).as_json controller_client(deployment).apply_credential(deployment_id: deployment.id, body: body) end def delete_credential(deployment, credential) controller_client(deployment).delete_credential(deployment_id: deployment.id, credential_id: credential.id) end def deploy_containers(deployment, containers) check_any_container_has_public_port(containers) do |exists| UffizziCore::DeploymentService.disable!(deployment) unless exists end containers = containers.map do |container| UffizziCore::Controller::DeployContainers::ContainerSerializer.new(container).as_json(include: '**') end credentials = deployment.credentials.deployable.map do |credential| UffizziCore::Controller::DeployContainers::CredentialSerializer.new(credential).as_json end host_volume_files = UffizziCore::HostVolumeFile.by_deployment(deployment).map do |host_volume_file| UffizziCore::Controller::DeployContainers::HostVolumeFileSerializer.new(host_volume_file).as_json end compose_file = if deployment.compose_file.present? UffizziCore::Controller::DeployContainers::ComposeFileSerializer.new(deployment.compose_file).as_json end body = { containers: containers, credentials: credentials, deployment_url: deployment.preview_url, compose_file: compose_file, host_volume_files: host_volume_files, } if password_protection_module.present? body = password_protection_module.add_password_configuration(body, deployment.project_id) end controller_client(deployment).deploy_containers(deployment_id: deployment.id, body: body) end def namespace_exists?(deployable) controller_client(deployable).namespace(namespace: deployable.namespace).code == 200 end def fetch_deployment_events(deployment) request_events(deployment) || [] end def fetch_pods(deployment) pods = controller_client(deployment).deployment_containers(deployment_id: deployment.id).result || [] pods.filter { |pod| pod.metadata.name.start_with?(Settings.controller.namespace_prefix) } end def fetch_namespace(deployable) controller_client(deployable).namespace(namespace: deployable.namespace).result || nil end def create_namespace(deployable) body = { namespace: deployable.namespace } controller_client(deployable).create_namespace(body: body).result || nil end def delete_namespace(deployable) controller_client(deployable).delete_namespace(namespace: deployable.namespace) end def create_cluster(cluster) body = UffizziCore::Controller::CreateCluster::ClusterSerializer.new(cluster).as_json controller_client(cluster).create_cluster(namespace: cluster.namespace, body: body).result end def show_cluster(cluster) controller_client(cluster).show_cluster(namespace: cluster.namespace, name: cluster.name).result end def delete_cluster(cluster) controller_client(cluster).delete_cluster(namespace: cluster.namespace) end def patch_cluster(cluster, sleep:) body = UffizziCore::Controller::UpdateCluster::ClusterSerializer.new(cluster).as_json body[:sleep] = sleep controller_client(cluster).patch_cluster(name: cluster.name, namespace: cluster.namespace, body: body) end def ingress_hosts(cluster) namespace = cluster.namespace ingresses = controller_client(cluster).ingresses(namespace: namespace).result.items ingresses.map do |ingress| ingress.spec.rules.map(&:host) end.flatten end private def check_any_container_has_public_port(containers) exists = containers.any? { |c| c.port.present? && c.public } yield(exists) if block_given? return if exists raise InvalidPublicPort.new(containers) end def request_events(deployment) controller_client(deployment).deployment_containers_events(deployment_id: deployment.id) end def controller_client(deployable) settings = case deployable when UffizziCore::Deployment controller_settings_service.deployment_settings_by_deployment(deployable) when UffizziCore::Cluster controller_settings_service.vcluster_settings_by_vcluster(deployable) else raise StandardError, "Deployable #{deployable.class.name} undefined" end UffizziCore::ControllerClient.new(settings) end end end ================================================ FILE: core/app/services/uffizzi_core/controller_settings_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ControllerSettingsService class << self def vcluster_settings_by_vcluster(_cluster) Settings.vcluster_controller end def vcluster_settings_by_account(_account) Settings.vcluster_controller end def deployment_settings_by_deployment(_deployment) Settings.controller.deep_dup.tap do |s| s.managed_dns_zone = Settings.app.managed_dns_zone end end end end ================================================ FILE: core/app/services/uffizzi_core/deployment/domain_service.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::DomainService class << self include UffizziCore::DependencyInjectionConcern def build_subdomain(deployment) return domain_module.build_subdomain(deployment) if domain_module.present? return build_docker_continuous_preview_subdomain(deployment) if deployment&.continuous_preview_payload&.fetch('docker', nil).present? build_default_subdomain(deployment) end def update_subdomain!(deployment) deployment.subdomain = build_subdomain(deployment) deployment.save! end private def build_docker_continuous_preview_subdomain(deployment) project = deployment.project continuous_preview_payload = deployment.continuous_preview_payload docker_payload = continuous_preview_payload['docker'] repo_name = docker_payload['image'].split('/').last image_tag = docker_payload['tag'] deployment_name = name(deployment) subdomain = "#{image_tag}-#{deployment_name}-#{repo_name}-#{project.slug}" format_subdomain(subdomain) end def build_default_subdomain(deployment) deployment_name = name(deployment) slug = deployment.project.slug.to_s subdomain = "#{deployment_name}-#{slug}" format_subdomain(subdomain) end def name(deployment) "deployment-#{deployment.id}" end def format_subdomain(full_subdomain_name) # Replace _ to - because RFC 1123 subdomain must consist of lower case alphanumeric characters, # '-' or '.', and must start and end with an alphanumeric character rfc_subdomain = full_subdomain_name.gsub('_', '-') subdomain_length_limit = Settings.deployment.subdomain.length_limit return rfc_subdomain if rfc_subdomain.length <= subdomain_length_limit sliced_subdomain = rfc_subdomain.slice(0, subdomain_length_limit) return sliced_subdomain.chop if sliced_subdomain.end_with?('-') sliced_subdomain end def format_url_safe(name) name.gsub(/ /, '-').gsub(/[^\w-]+/, '-') end end end ================================================ FILE: core/app/services/uffizzi_core/deployment/memory_service.rb ================================================ # frozen_string_literal: true class UffizziCore::Deployment::MemoryService class << self def valid_memory_limit?(_deployment) true end def memory_limit_error_message(_deployment); end end end ================================================ FILE: core/app/services/uffizzi_core/deployment_service.rb ================================================ # frozen_string_literal: true class UffizziCore::DeploymentService include UffizziCore::DependencyInjectionConcern prepend_module_if_exists('UffizziCore::DeploymentServiceModule') MIN_TARGET_PORT_RANGE = 37_000 MAX_TARGET_PORT_RANGE = 39_999 DEPLOYMENT_PROCESS_STATUSES = { building: :building, deploying: :deploying, failed: :failed, queued: :queued, }.freeze class << self include UffizziCore::DependencyInjectionConcern def create_from_compose(compose_file, project, user, params) deployment_attributes = ActionController::Parameters.new(compose_file.template.payload) deployment_form = UffizziCore::Api::Cli::V1::Deployment::CreateForm.new(deployment_attributes) deployment_form.assign_dependences!(project, user) deployment_form.compose_file = compose_file deployment_form.creation_source = params[:creation_source] || UffizziCore::Deployment.creation_source.compose_file_manual deployment_form.metadata = params[:metadata] || {} run_deployment_creation_tasks(deployment_form) if deployment_form.save deployment_form end def update_from_compose(compose_file, project, user, deployment, metadata) deployment_attributes = ActionController::Parameters.new(compose_file.template.payload) deployment_form = UffizziCore::Api::Cli::V1::Deployment::UpdateForm.new(deployment_attributes) deployment_form.assign_dependences!(project, user) deployment_form.compose_file = compose_file deployment_form.metadata = metadata || {} ActiveRecord::Base.transaction do deployment.containers.destroy_all deployment.compose_file.destroy! if deployment.compose_file&.kind&.temporary? deployment.activate unless deployment.active? new_params = params_for_update_deployment(deployment_form, compose_file) deployment.update!(new_params) end deployment end def deploy_containers(deployment, repeated = false) if !repeated create_activity_items(deployment) update_controller_container_names(deployment) end status = deployment_process_status(deployment) case status when DEPLOYMENT_PROCESS_STATUSES[:building] Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} repeat deploy_containers") UffizziCore::Deployment::DeployContainersJob.perform_in(1.minute, deployment.id, true) unless repeated deployment.deployment_events.create!(deployment_state: status) end when DEPLOYMENT_PROCESS_STATUSES[:deploying] Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} start deploying into controller") containers = deployment.active_containers containers_with_variables = add_default_deployment_variables!(containers, deployment) UffizziCore::ControllerService.deploy_containers(deployment, containers_with_variables) deployment.deployment_events.create!(deployment_state: status) else Rails.logger.info("DEPLOYMENT_PROCESS deployment_id=#{deployment.id} deployment has builds errors, stopping") end end def disable!(deployment) deployment.disable! compose_file = deployment.compose_file || deployment.template&.compose_file return unless compose_file&.kind&.temporary? compose_file.destroy! end def fail!(deployment) return if deployment.failed? deployment.fail! deployment.deployment_events.create!(deployment_state: deployment.state) compose_file = deployment.compose_file || deployment.template&.compose_file return unless compose_file&.kind&.temporary? compose_file.destroy! end def all_containers_have_unique_ports?(containers) ports = containers.map(&:port).compact containers.empty? || ports.size == ports.uniq.size end def ingress_container?(containers) containers.empty? || containers.map(&:receive_incoming_requests).count(true) == 1 end def find_unused_port(deployment) selected_port = nil Timeout.timeout(20) do loop do selected_port = rand(MIN_TARGET_PORT_RANGE..MAX_TARGET_PORT_RANGE) break if !deployment.containers.exists?(target_port: selected_port) end end selected_port end def setup_ingress_container(deployment, ingress_container, port) old_deployment_subdomain = deployment.subdomain containers = deployment.containers.active UffizziCore::Container.transaction do containers.update_all(receive_incoming_requests: false, port: nil, public: false) containers.find(ingress_container.id).update!(port: port, public: true, receive_incoming_requests: true) end deployment.reload new_deployment_subdomain = DomainService.build_subdomain(deployment) if new_deployment_subdomain != old_deployment_subdomain deployment.update(subdomain: new_deployment_subdomain) end UffizziCore::Deployment::DeployContainersJob.perform_async(deployment.id) end def pull_request_payload_present?(deployment) deployment.continuous_preview_payload.present? && deployment.continuous_preview_payload['pull_request'].present? end def failed?(deployment) deployment_status = deployment_process_status(deployment) deployment_status == DEPLOYMENT_PROCESS_STATUSES[:failed] end private def run_deployment_creation_tasks(deployment) UffizziCore::Deployment::DomainService.update_subdomain!(deployment) UffizziCore::Deployment::CreateJob.perform_async(deployment.id) end def deployment_process_status(deployment) containers = deployment.active_containers activity_items = containers.map { |container| container.activity_items.order_by_id.last }.compact events = activity_items.map { |activity_item| activity_item.events.order_by_id.last&.state } events = events.flatten.uniq return DEPLOYMENT_PROCESS_STATUSES[:queued] if events.empty? return DEPLOYMENT_PROCESS_STATUSES[:failed] if events.include?(UffizziCore::Event.state.failed) return DEPLOYMENT_PROCESS_STATUSES[:building] if events.include?(UffizziCore::Event.state.building) DEPLOYMENT_PROCESS_STATUSES[:deploying] end def create_activity_items(deployment) deployment.active_containers.each do |container| repo = container.repo activity_item = UffizziCore::ActivityItemService.create_docker_item!(repo, container) create_default_activity_item_event(activity_item) if UffizziCore::RepoService.credential(repo).present? && activity_item.docker? UffizziCore::ActivityItem::Docker::UpdateDigestJob.perform_async(activity_item.id) end UffizziCore::Deployment::ManageDeployActivityItemJob.perform_in(5.seconds, activity_item.id) end end def create_default_activity_item_event(activity_item) activity_item.events.create(state: UffizziCore::Event.state.deploying) if activity_item.docker? end def update_controller_container_names(deployment) deployment.active_containers.each do |container| next if container.controller_name.present? controller_name = generate_controller_container_name(container) container.update!(controller_name: controller_name) end end def generate_controller_container_name(container) Digest::SHA256.hexdigest("#{container.id}:#{container.image}")[0, 10] end def add_default_deployment_variables!(containers, deployment) containers.each do |container| envs = [] if container.port.present? && !UffizziCore::ContainerService.defines_env?(container, 'PORT') envs.push('name' => 'PORT', 'value' => container.target_port.to_s) end envs.push('name' => 'UFFIZZI_URL', 'value' => "https://#{deployment.preview_url}") envs.push('name' => 'UFFIZZI_DOMAIN', 'value' => deployment.preview_url) preview_url = "https://#{domain_module.build_preview_url(deployment)}" if domain_module.present? envs.push('name' => 'UFFIZZI_PREDICTABLE_URL', 'value' => preview_url || '') container.variables = [] if container.variables.nil? container.variables.push(*envs) end end def params_for_update_deployment(deployment_form, compose_file) { containers: deployment_form.containers, compose_file_id: compose_file.id, metadata: deployment_form.metadata, } end end end ================================================ FILE: core/app/services/uffizzi_core/logs_service.rb ================================================ # frozen_string_literal: true class UffizziCore::LogsService NOT_ALLOWED_SYMBOLS_IN_NAME_REGEX = /[^a-zA-Z0-9-]/.freeze DEFAULT_LOGS_LIMIT = 1000 class << self def fetch_container_logs(container, query = {}) response = request_logs(container, query).result || {} response = Hashie::Mash.new(response) logs = response.logs || [] { logs: format_logs(logs), } end private def request_logs(container, query) deployment = container.deployment controller_client.deployment_container_logs( deployment_id: deployment.id, container_name: UffizziCore::ContainerService.pod_name(container), limit: query[:limit] || DEFAULT_LOGS_LIMIT, previous: query[:previous] || false, ) end def format_logs(logs) logs.map do |item| timestamp, *payload = item.split formatted_timestamp = timestamp.present? ? timestamp.to_time(:utc).strftime('%Y-%m-%d %H:%M:%S.%L %Z') : nil { timestamp: formatted_timestamp, payload: payload.join(' ') } end end def controller_client UffizziCore::ControllerClient.new(Settings.controller) end end end ================================================ FILE: core/app/services/uffizzi_core/manage_activity_items_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ManageActivityItemsService attr_accessor :deployment, :containers, :pods, :namespace_data def initialize(deployment) @deployment = deployment @containers = deployment.active_containers @namespace_data = UffizziCore::ControllerService.fetch_namespace(deployment) @pods = UffizziCore::ControllerService.fetch_pods(deployment) end def container_status_item(container) container_status_items.detect { |container_statuses| container_statuses[:id] == container.id } end def container_status_items build_container_status_items(build_network_connectivities, build_containers_replicas) end private def build_network_connectivities containers.with_public_access.map do |container| { id: container.id, items: build_container_network_connectivity_items(container) } end end def build_container_network_connectivity_items(container) network_connectivities = container_network_connectivities(container) return [] if network_connectivities.nil? network_connectivities.map do |network_connectivity| type, value = network_connectivity { type: type, status: value.status } end end def build_containers_replicas containers.map do |container| items = pods.map do |pod| { name: item_name(pod, container), status: get_status(pod, container), } end { id: container.id, items: items } end end def build_container_status_items(network_connectivities, containers_replicas) containers.map do |container| network_connectivity = network_connectivities.detect { |item| item[:id] == container.id } container_replicas = containers_replicas.detect { |item| item[:id] == container.id } { id: container.id, status: build_container_status(container, network_connectivity, container_replicas), } end end def replicas_contains_status?(replicas, status) replicas.any? { |replica| replica[:status] == status } end def build_container_status(container, network_connectivity, container_replicas) error = replicas_contains_status?(container_replicas[:items], UffizziCore::Event.state.failed) container_is_running = replicas_contains_status?(container_replicas[:items], UffizziCore::Event.state.deployed) deployed = !error && container_is_running return container_status(error, deployed) unless container.public? network_connectivity[:items].each do |item| status = item[:status].to_sym error ||= status == :failed deployed &&= status == :success end container_status(error, deployed) end def container_status(error, deployed) return UffizziCore::Event.state.failed if error return UffizziCore::Event.state.deployed if deployed UffizziCore::Event.state.deploying end def item_name(pod, container) hash = pod.metadata.name.split('-').last "#{container.image_name}-#{hash}" end def get_status(pod, container) pod_container = pod_container(pod, container) return UffizziCore::Event.state.deploying if pod_container.nil? || pod_container[:state].to_h.empty? pod_container_status = pod_container[:state].keys.first state = pod_container[:state][pod_container_status] reason = state&.reason case pod_container_status.to_sym when :running UffizziCore::Event.state.deployed when :waiting raise UffizziCore::Deployment::ImagePullError, @deployment.id if ['ErrImagePull', 'ImagePullBackOff'].include?(reason) UffizziCore::Event.state.deploying else UffizziCore::Event.state.deploying end end def pod_container(pod, container) pod_name = UffizziCore::ContainerService.pod_name(container) pod&.status&.container_statuses&.detect { |cs| cs.name.include?(pod_name) } end def container_network_connectivities(container) network_connectivity = Hashie::Mash.new(JSON.parse(deployment_network_connectivity)) containers_network_connectivity = network_connectivity&.containers containers_network_connectivity[container.id.to_s] unless containers_network_connectivity.nil? end def deployment_network_connectivity namespace_data&.metadata&.annotations&.network_connectivity.presence || '{}' end end ================================================ FILE: core/app/services/uffizzi_core/project_service.rb ================================================ # frozen_string_literal: true class UffizziCore::ProjectService class << self def update_compose_secrets(project) compose_file = project.compose_file return if compose_file&.template.nil? project.secrets.each do |secret| if UffizziCore::ComposeFileService.has_secret?(compose_file, secret) UffizziCore::ComposeFileService.update_secret!(compose_file, secret) end end return unless UffizziCore::ComposeFileService.secrets_valid?(compose_file, project.secrets) secrets_error_key = UffizziCore::ComposeFile::ErrorsService::SECRETS_ERROR_KEY return unless UffizziCore::ComposeFile::ErrorsService.has_error?(compose_file, secrets_error_key) UffizziCore::ComposeFile::ErrorsService.reset_error!(compose_file, secrets_error_key) compose_file.set_valid! unless UffizziCore::ComposeFile::ErrorsService.has_errors?(compose_file) end def update_compose_secret_errors(project, secret) compose_file = project.compose_file return if compose_file.nil? return unless UffizziCore::ComposeFileService.has_secret?(compose_file, secret) error_message = I18n.t('compose.project_secret_not_found', secret: secret['name']) compose_file_errors = compose_file.payload['errors'] || {} secrets_errors = compose_file_errors[UffizziCore::ComposeFile::ErrorsService::SECRETS_ERROR_KEY].presence || [] new_secrets_errors = secrets_errors.append(error_message).uniq error = { UffizziCore::ComposeFile::ErrorsService::SECRETS_ERROR_KEY => new_secrets_errors } new_errors = compose_file_errors.merge(error) UffizziCore::ComposeFile::ErrorsService.update_compose_errors!(compose_file, new_errors, compose_file.content) end def add_users_to_project!(project, account) user_projects = account.memberships.map do |membership| { project: project, user: membership.user, role: membership.role } end UffizziCore::UserProject.create!(user_projects) end end end ================================================ FILE: core/app/services/uffizzi_core/repo_service.rb ================================================ # frozen_string_literal: true class UffizziCore::RepoService class << self def needs_target_port?(repo) return false if repo.nil? !repo.dockerfile? end def credential(repo) container_registry_service = UffizziCore::ContainerRegistryService.init_by_subclass(repo.type) credentials = repo.project.account.credentials container_registry_service.credential(credentials) end def image_name(repo) "e#{repo.container.deployment_id}r#{repo.id}-#{Digest::SHA256.hexdigest("#{self.class}:#{repo.branch}: #{repo.project_id}:#{repo.id}")[0, 10]}" end def image(repo) repo_credential = credential(repo) "#{repo_credential.registry_url}/#{image_name(repo)}" end end end ================================================ FILE: core/app/services/uffizzi_core/response_service.rb ================================================ # frozen_string_literal: true module UffizziCore::ResponseService def meta(collection) { total_count: collection.total_count, current_page: collection.current_page, per_page: collection.limit_value, count: collection.to_a.count, total_pages: collection.total_pages, } end end ================================================ FILE: core/app/services/uffizzi_core/template/memory_service.rb ================================================ # frozen_string_literal: true class UffizziCore::Template::MemoryService class << self def valid_memory_limit?(_template) true end def memory_limit_error_message(_template); end end end ================================================ FILE: core/app/services/uffizzi_core/token_service.rb ================================================ # frozen_string_literal: true module UffizziCore::TokenService class << self def encode(payload) JWT.encode(payload, Settings.rails.secret_key_base, 'HS256') end def decode(token) JWT.decode(token, Settings.rails.secret_key_base, true, algorithm: 'HS256') rescue JWT::DecodeError nil end def generate SecureRandom.hex end end end ================================================ FILE: core/app/services/uffizzi_core/user_access_service.rb ================================================ # frozen_string_literal: true class UffizziCore::UserAccessService attr_accessor :user_access_module delegate :admin_access_to_account?, :developer_access_to_account?, :viewer_access_to_account?, :admin_or_developer_access_to_account?, :any_access_to_account?, :admin_access_to_project?, :developer_access_to_project?, :viewer_access_to_project?, :admin_or_developer_access_to_project?, :any_access_to_project?, :global_admin?, :valid_token?, to: :@user_access_module def initialize(user_access_module) @user_access_module = user_access_module end end ================================================ FILE: core/app/services/uffizzi_core/user_generator_service.rb ================================================ # frozen_string_literal: true class UffizziCore::UserGeneratorService DEFAULT_USER_EMAIL = 'user@example.com' DEFAULT_PROJECT_NAME = 'default' DEFAULT_ACCOUNT_NAME = 'default' class << self def safe_generate(email, password, project_name) generate(email, password, project_name) rescue ActiveRecord::RecordInvalid => e puts e.message end def generate(email, password, project_name) user_attributes = build_user_attributes(email, password) project_attributes = build_project_attributes(project_name) ActiveRecord::Base.transaction do user = UffizziCore::User.create!(user_attributes) account_attributes = build_account_attributes(user) account = UffizziCore::Account.create!(account_attributes) user.memberships.create!(account: account, role: UffizziCore::Membership.role.admin) project = account.projects.create!(project_attributes) project.user_projects.create!(user: user, role: UffizziCore::UserProject.role.admin) end end private def build_user_attributes(email, password) user_attributes = { state: UffizziCore::User::STATE_ACTIVE, creation_source: UffizziCore::User.creation_source.system, } if email.present? user_attributes[:email] = email elsif IO::console.present? IO::console.write("Enter User Email (default: #{DEFAULT_USER_EMAIL}): ") user_attributes[:email] = IO::console.gets.strip.presence || DEFAULT_USER_EMAIL end user_attributes[:password] = if password.present? password elsif IO::console.present? IO::console.getpass('Enter Password: ') end user_attributes end def build_project_attributes(project_name) project_attributes = { state: UffizziCore::Project::STATE_ACTIVE, } if project_name.present? project_attributes[:name] = project_name elsif IO::console.present? IO::console.write("Enter Project Name (default: #{DEFAULT_PROJECT_NAME}): ") project_attributes[:name] = IO::console.gets.strip.presence || DEFAULT_PROJECT_NAME else project_attributes[:name] = DEFAULT_PROJECT_NAME end project_attributes[:slug] = prepare_project_slug(project_attributes[:name]) project_attributes end def build_account_attributes(user) { owner: user, name: DEFAULT_ACCOUNT_NAME, state: UffizziCore::Account::STATE_ACTIVE, kind: UffizziCore::Account.kind.personal, } end def prepare_project_slug(project_name) project_name.downcase.gsub(/[^A-Za-z0-9]/, '-') end end end ================================================ FILE: core/app/utils/uffizzi_core/converters.rb ================================================ # frozen_string_literal: true module UffizziCore::Converters class << self include ActionView::Helpers::NumberHelper def deep_lower_camelize_keys(object) case object when Array object.map do |element| element.deep_transform_keys { |key| key.to_s.camelize(:lower) } end when Hash object.deep_transform_keys { |key| key.to_s.camelize(:lower) } else object end end def deep_underscore_keys(object) case object when Array object.map do |element| element.deep_transform_keys { |key| key.to_s.underscore } end when Hash object.deep_transform_keys { |key| key.to_s.underscore } else object end end end end ================================================ FILE: core/app/validators/uffizzi_core/email_validator.rb ================================================ # frozen_string_literal: true class UffizziCore::EmailValidator < ActiveModel::EachValidator EMAIL_REGEXP = /\A([^@\s<>]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.freeze def validate_each(record, attribute, value) record.errors.add(attribute, (options[:message] || :invalid)) unless value&.match?(EMAIL_REGEXP) end end ================================================ FILE: core/app/validators/uffizzi_core/environment_variable_list_validator.rb ================================================ # frozen_string_literal: true class UffizziCore::EnvironmentVariableListValidator < ActiveModel::EachValidator def validate_each(record, attribute, variables) record.errors.add(attribute, :invalid) if variables.class != Array || !valid_variables?(variables) end def valid_variables?(variables) variables.each do |variable| return false if variable.class != Hash || !variable.key?('name') || variable['name'].nil? || !variable.key?('value') end true end end ================================================ FILE: core/app/validators/uffizzi_core/image_command_args_validator.rb ================================================ # frozen_string_literal: true class UffizziCore::ImageCommandArgsValidator < ActiveModel::EachValidator def validate_each(record, attribute, command_args) record.errors.add(attribute, :invalid) if !command_args.nil? && !valid_command_args?(command_args) end def valid_command_args?(raw_command_args) return true if raw_command_args.empty? begin command_args = JSON.parse(raw_command_args) rescue JSON::ParserError return false end return false if command_args.class != Array || command_args.empty? command_args.all? { |item| item.instance_of?(String) } end end ================================================ FILE: core/bin/rails ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. ENGINE_ROOT = File.expand_path('..', __dir__) ENGINE_PATH = File.expand_path('../lib/uffizzi_core/engine', __dir__) APP_PATH = File.expand_path('../test/dummy/config/application', __dir__) # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) require 'rails/all' require 'rails/engine/commands' ================================================ FILE: core/config/initializers/rswag_api.rb ================================================ # frozen_string_literal: true Rswag::Api.configure do |c| # Specify a root folder where Swagger JSON files are located # This is used by the Swagger middleware to serve requests for API descriptions # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure # that it's configured to generate files in the same folder c.swagger_root = "#{UffizziCore::Engine.root}/swagger" # Inject a lamda function to alter the returned Swagger prior to serialization # The function will have access to the rack env for the current request # For example, you could leverage this to dynamically assign the "host" property # # c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } end ================================================ FILE: core/config/initializers/rswag_ui.rb ================================================ # frozen_string_literal: true Rswag::Ui.configure do |c| # List the Swagger endpoints that you want to be documented through the swagger-ui # The first parameter is the path (absolute or relative to the UI host) to the corresponding # endpoint and the second is a title that will be displayed in the document selector # NOTE: If you're using rspec-api to expose Swagger files (under swagger_root) as JSON or YAML endpoints, # then the list below should correspond to the relative paths for those endpoints c.swagger_endpoint('/api-docs/v1/swagger.json', 'API V1 Docs') # Add Basic Auth in case your API is private # c.basic_auth_enabled = true # c.basic_auth_credentials 'username', 'password' end ================================================ FILE: core/config/initializers/swagger_yard.rb ================================================ # frozen_string_literal: true require 'swagger_yard' SwaggerYard.configure do |config| config.api_version = '1.0' config.title = 'Uffizzi docs' config.description = 'Your API does this' config.api_base_path = 'http://lvh.me:7000' config.controller_path = File.expand_path('app/controllers/uffizzi_core/api/**/*', UffizziCore::Engine.root) config.model_path = File.expand_path('app/models/uffizzi_core/**/*', UffizziCore::Engine.root) config.include_private = false end ================================================ FILE: core/config/locales/en.activerecord.yml ================================================ en: activerecord: errors: models: uffizzi_core/project: attributes: secret: not_found: There is no secret with name %{name} name: taken: "A project with the name '%{value}' already exists." slug: taken: "A project slug '%{value}' already taken." uffizzi_core/credential: attributes: registry_url: invalid_scheme: The protocol for the registry url is not specified password: password_blank: Password/Access Token can't be blank username: incorrect: "We were unable to log you into %{type}. Please verify that your username and password are correct." type: exist: Credential of that type already exist. uffizzi_core/deployment: attributes: containers: max_memory_limit_error: "Total memory limit of containers must be less than %{max}" uffizzi_core/user: attributes: email: invalid: Invalid email address ================================================ FILE: core/config/locales/en.yml ================================================ en: github: preview_url_message: | **This branch has been deployed using Uffizzi.** Preview URL: https://%{preview_url} View deployment details here: https://%{deployment_url} This is an automated comment. To turn off commenting, visit uffizzi.com. invalid_compose_message: | :exclamation: **Preview failed: invalid compose**        Uffizzi was unable to deploy a preview of this pull request because the compose file in this branch is invalid. compose: unsupported_file: Unsupported compose file invalid_file: 'Syntax error: %{err} at line %{line} column %{column}' invalid_compose: Invalid compose file invalid_config_option: Invalid config option '%{value}' - only a-zA-Z0-9._- characters are allowed no_services: Service services has neither an image nor a build context specified. At least one must be provided. invalid_service_name: "Invalid service name '%{value}': a service name must consist of lower case alphanumeric characters, ''-'', and must start and end with an alphanumeric character" no_ingress: Service ingress has not been defined. invalid_image_value: Invalid image value '%{value}' image_name_contains_uppercase_value: Image name '%{value}' contains uppercase characters unprocessable_image: Invalid credential '%{value} or image does not exist' invalid_repo_type: Unsupported repo type repo_not_found: The specified repository doesn't exist '%{name}' ingress_port_not_specified: Ingress port not specified ingress_service_not_found: Ingress service not found invalid_ingress_service: Invalid ingress service '%{value}' no_variable_name: Invalid environment variable 'name=%{name}' 'value=%{value}' invalid_config: Invalid config value 'source=%{source}' 'target=%{target}' port_out_of_range: Port should be specified between %{port_min} - %{port_max} invalid_memory_postfix: The specified value for memory '%{value}' should specify the units. The postfix should be one of the `b` `k` `m` `g` characters invalid_memory_type: Memory '%{value}' contains an invalid type, it should be a string invalid_memory_value: Invalid memory value '%{value}' invalid_memory: The memory should be one of the `125m` `250m` `500m` `1000m` `2000m` `4000m` values image_build_no_specified: Service '%{value}' has neither an image nor a build context specified. At least one must be provided. build_context_no_specified: The context option should be specified config_file_not_found: Config file not found '%{name}' host_volume_file_not_found: Host volume file not found '%{name}' invalid_context: Invalid context value '%{value}' invalid_bool_value: Invalid %{field} value '%{value}'. The value should be `true` or `false` invalid_delete_after_postfix: The postfix of the delete_preview_after value should be `h` invalid_integer: The specified value for %{option} should be an Integer type invalid_string: The specified value for %{option} should be a String type invalid_delete_after_min: Minimum delete_preview_after allowed is %{value}h invalid_delete_after_max: Maximum delete_preview_after allowed is %{value}h invalid_type: Unsupported type of '%{option}' option empty_env_file: env_file contains an empty value file_not_found: Couldn't find '%{path}' file env_file_duplicates: env_file contains non-unique items '%{values}' boolean_option: The service name %{value} must be a quoted string, i.e. '%{value}'. config_file_option_empty: "'%{config_name}' has an empty file" global_config_not_found: undefined config '%{config}' invalid_branch: Branch '%{branch}' doesn't exist for repository '%{repository_url}' repository_not_found: The specified repository doesn't exist '%{repository_url}' build_context_unknown_repository: Compose repository is undefined secret_name_blank: The specified name for '%{option}' secret can not be blank secret_external: The specified secret '%{secret}' should be external global_secret_not_found: undefined secret '%{secret}' project_secret_not_found: Project secret '%{secret}' not found continuous_preview_in_service_level: The option '%{option}' is not supported for service-level. Use 'x-uffizzi-continuous-preview' instead file_already_exists: A compose file already exists for this project. Run 'uffizzi compose update' to update this file or 'uffizzi compose rm' to remove it. For more options, see 'uffizzi compose --help' invalid_time_interval: "Invalid time interval: '%{key}:%{value}'. The time interval should be in the following format '{hours}h{minutes}m{seconds}s'. At least one value must be present." invalid_retries: "Invalid retries value: 'retries:%{value}'. The value should be an integer." string_or_array_error: "'%{option}' contains an invalid type, it should be a string, or an array" not_implemented: "'%{option}' option is not implemented" infinite_recursion: "Found infinite recursion for key '%{key}'" volume_path_is_invalid: The path '%{path}' is invalid volume_prop_is_required: The '%{prop_name}' is a required property volume_invalid_name: "Volumes value '%{name}' does not match any of the regexes: '^[a-zA-Z0-9._-]+$'" named_volume_not_exists: Named volume '%{source_path}:%{target_path}' is used in service '%{service_name}' but no declaration was found in the volumes section. invalid_volume_destination: Invalid volume specification '%{spec}' destination can't be '/' required_start_commands: "When 'test' is a list the first item must be one of: '%{available_commands}'" volumes_should_be_array: Volumes '%{volumes}' should be an arra healthcheck_missing_required_option: "One of these options is required: %{required_options}" build_not_implemented: "The 'build' directive is not supported by the uffizzi command-line tool. Use 'image' instead, or configure Uffizzi CI if you want Uffizzi Cloud to build your application from source. See https://docs.uffizzi.com/references/compose-spec/" secrets: duplicates_exists: Secret with key %{secrets} already exist. invalid_key_length: A secret key must be no longer than 256 characters. deployment: invalid_state: Preview with ID deployment-%{id} %{state} already_exists: An active deployment already exists cluster: already_asleep: The cluster %{name} is already asleep. already_awake: The cluster %{name} is already awake. scaling_failed: Failed to %{action} cluster. deploy_in_process: Please wait until the cluster %{name} is deployed. session: unsupported_login_type: This type of login is not supported unauthorized: Unauthorized kubernetes_distribution: not_available: "The k8s version %{version} is not supported. Available version are: %{available_versions}." enumerize: credential: type: "UffizziCore::Credential::GithubContainerRegistry": "Github Container Registry" "UffizziCore::Credential::DockerHub": "DockerHub" "UffizziCore::Credential::DockerRegistry": "Docker Registry" "UffizziCore::Credential::Azure": "Azure" "UffizziCore::Credential::Google": "Google" "UffizziCore::Credential::Amazon": "Amazon" registry: error: Container registry returned a %{code} error ================================================ FILE: core/config/routes.rb ================================================ # frozen_string_literal: true UffizziCore::Engine.routes.draw do mount Rswag::Api::Engine => '/api-docs' mount Rswag::Ui::Engine => '/api-docs' namespace :api, defaults: { format: :json } do namespace :cli do namespace :v1 do resources :projects, only: ['index', 'show', 'destroy'], param: :slug do scope module: :projects do resource :compose_file, only: ['show', 'create', 'destroy'] resources :clusters, only: [:index, :create, :show, :destroy], param: :name do member do put :scale_down put :scale_up put :sync end scope module: :clusters do resources :ingresses, only: ['index'] end end resources :deployments, only: ['index', 'show', 'create', 'destroy', 'update'] do post :deploy_containers, on: :member scope module: :deployments do resources :activity_items, only: ['index'] resources :events, only: ['index'] resources :containers, only: ['index'], param: :name do get :k8s_container_description scope module: :containers do resources :logs, only: ['index'] resources :builds, only: [] do collection do get :logs end end end end end end resources :secrets, only: ['index', 'destroy'] do collection do post :bulk_create end end end end resource :session, only: ['create', 'destroy'] namespace :ci do resource :session, only: ['create'] end resources :accounts, only: ['show', 'update'], param: :name resources :accounts, only: ['index'] do scope module: :accounts do resources :projects, only: ['index', 'create'] resources :clusters, only: ['index'] resources :credentials, only: ['index', 'create', 'update', 'destroy'], param: :type do member do get :check_credential end end end end end end end end ================================================ FILE: core/db/migrate/20220218121438_create_uffizzi_core_tables.rb ================================================ # frozen_string_literal: true class CreateUffizziCoreTables < ActiveRecord::Migration[6.1] def change create_table('uffizzi_core_accounts', force: :cascade) do |t| t.text('name') t.text('kind', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('customer_token') t.string('state') t.string('subscription_token') t.datetime('payment_issue_at') t.string('domain') t.boolean('sso_enabled', default: false) t.bigint('owner_id') t.integer('container_memory_limit') t.string('workos_organization_id') t.string('sso_state') t.index(['customer_token'], name: 'index_accounts_on_customer_token', unique: true) t.index(['domain'], name: 'index_accounts_on_domain', unique: true) t.index(['subscription_token'], name: 'index_accounts_on_subscription_token', unique: true) end create_table('uffizzi_core_activity_items', force: :cascade) do |t| t.bigint('deployment_id', null: false) t.string('namespace') t.string('name') t.string('tag') t.string('branch') t.string('type') t.bigint('container_id', null: false) t.string('commit') t.string('commit_message') t.bigint('build_id') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.jsonb('data', default: {}, null: false) t.string('digest') t.index(['container_id'], name: 'index_activity_items_on_container_id') t.index(['deployment_id'], name: 'index_activity_items_on_deployment_id') end create_table('uffizzi_core_builds', force: :cascade) do |t| t.bigint('repo_id', null: false) t.string('build_id') t.string('repository') t.string('branch') t.string('commit') t.string('committer') t.string('message') t.string('log_url') t.integer('status') t.datetime('started_at') t.datetime('ended_at') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.boolean('deployed') t.index(['build_id'], name: 'index_builds_on_build_id', unique: true) t.index(['repo_id'], name: 'index_builds_on_repo_id') end create_table('uffizzi_core_comments', force: :cascade) do |t| t.string('commentable_type') t.bigint('commentable_id') t.text('content') t.bigint('user_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('ancestry') t.integer('ancestry_depth', default: 0) t.index(['ancestry'], name: 'index_comments_on_ancestry') t.index(['commentable_type', 'commentable_id'], name: 'index_comments_on_commentable') t.index(['user_id'], name: 'index_comments_on_user_id') end create_table('uffizzi_core_compose_files', force: :cascade) do |t| t.string('source') t.bigint('repository_id') t.string('branch') t.string('path') t.string('auto_deploy') t.bigint('added_by_id') t.bigint('project_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('state') t.jsonb('payload', default: {}, null: false) t.text('content') t.string('kind', default: 'main') t.index(['project_id'], name: 'index_compose_files_on_project_id') end create_table('uffizzi_core_config_files', force: :cascade) do |t| t.string('filename') t.string('kind') t.bigint('added_by_id') t.text('payload') t.bigint('project_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('compose_file_id') t.string('creation_source') t.string('source') t.index(['compose_file_id'], name: 'index_config_files_on_compose_file_id') t.index(['project_id'], name: 'index_config_files_on_project_id') end create_table('uffizzi_core_container_config_files', force: :cascade) do |t| t.string('mount_path') t.bigint('container_id', null: false) t.bigint('config_file_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['config_file_id'], name: 'index_container_config_files_on_config_file_id') t.index(['container_id'], name: 'index_container_config_files_on_container_id') end create_table('uffizzi_core_containers', force: :cascade) do |t| t.string('image') t.string('tag') t.jsonb('variables') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('deployment_id') t.boolean('public', default: false, null: false) t.integer('port') t.bigint('repo_id') t.string('state') t.string('continuously_deploy', null: false) t.string('kind', default: 'user') t.integer('target_port') t.string('controller_name') t.boolean('receive_incoming_requests') t.integer('memory_request') t.integer('memory_limit') t.jsonb('secret_variables') t.string('entrypoint') t.string('command') t.index(['deployment_id'], name: 'index_containers_on_deployment_id') t.index(['repo_id'], name: 'index_containers_on_repo_id') end create_table('uffizzi_core_coupons', force: :cascade) do |t| t.string('token') t.string('name', null: false) t.string('currency', null: false) t.bigint('amount_off', null: false) t.string('duration', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) end create_table('uffizzi_core_credentials', force: :cascade) do |t| t.string('type') t.string('username') t.string('password') t.bigint('project_id') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('provider_ref') t.bigint('account_id') t.string('state') t.string('registry_url') t.index(['account_id'], name: 'index_credentials_on_account_id') t.index(['project_id'], name: 'index_credentials_on_project_id') t.index(['provider_ref'], name: 'index_credentials_on_provider_ref') end create_table('uffizzi_core_deployments', force: :cascade) do |t| t.bigint('project_id', null: false) t.text('kind', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('creator_name') t.string('subdomain') t.string('state') t.float('memory_limit') t.bigint('deployed_by_id') t.bigint('continuous_preview_id_deprecated') t.jsonb('continuous_preview_payload') t.string('creation_source') t.bigint('compose_file_id') t.bigint('template_id') t.index(['compose_file_id'], name: 'index_deployments_on_compose_file_id') t.index(['project_id'], name: 'index_deployments_on_project_id') t.index(['template_id'], name: 'index_deployments_on_template_id') end create_table('uffizzi_core_events', force: :cascade) do |t| t.bigint('activity_item_id', null: false) t.string('state') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['activity_item_id'], name: 'index_events_on_activity_item_id') end create_table('uffizzi_core_invitations', force: :cascade) do |t| t.text('email', null: false) t.text('token', null: false) t.string('status', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('invited_by_id', null: false) t.bigint('entityable_id', null: false) t.string('entityable_type', null: false) t.string('role', null: false) t.bigint('invitee_id') t.index(['token'], name: 'index_invitations_on_token', unique: true) end create_table('uffizzi_core_memberships', force: :cascade) do |t| t.bigint('user_id', null: false) t.bigint('account_id', null: false) t.text('role', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['account_id'], name: 'index_memberships_on_account_id') t.index(['user_id', 'account_id'], name: 'index_memberships_on_user_id_and_account_id') t.index(['user_id'], name: 'index_memberships_on_user_id') end create_table('uffizzi_core_payments', force: :cascade) do |t| t.bigint('account_id', null: false) t.string('charge_id') t.string('status') t.float('amount') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['account_id'], name: 'index_payments_on_account_id') end create_table('uffizzi_core_prices', force: :cascade) do |t| t.string('token') t.string('slug', null: false) t.string('name', null: false) t.float('units_price', null: false) t.bigint('units_amount', null: false) t.bigint('product_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['product_id'], name: 'index_prices_on_product_id') end create_table('uffizzi_core_products', force: :cascade) do |t| t.string('token') t.string('slug', null: false) t.string('name', null: false) t.string('type', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('kind') end create_table('uffizzi_core_projects', force: :cascade) do |t| t.text('name', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('account_id', null: false) t.string('state') t.string('slug') t.string('description') t.jsonb('secrets') t.index(['account_id', 'name'], name: 'index_projects_on_account_id_and_name', unique: true) t.index(['account_id'], name: 'index_projects_on_account_id') end create_table('uffizzi_core_ratings', force: :cascade) do |t| t.string('name') t.string('state') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) end create_table('uffizzi_core_repos', force: :cascade) do |t| t.string('namespace') t.string('name') t.string('tag') t.string('type') t.string('branch') t.bigint('project_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('description') t.boolean('is_private') t.string('slug') t.bigint('repository_id') t.string('kind') t.string('dockerfile_path') t.jsonb('args') t.string('dockerfile_context_path') t.boolean('deploy_preview_when_pull_request_is_opened') t.boolean('delete_preview_when_pull_request_is_closed') t.boolean('deploy_preview_when_image_tag_is_created') t.boolean('delete_preview_when_image_tag_is_updated') t.boolean('share_to_github') t.integer('delete_preview_after') t.string('tag_pattern_deprecated') t.index(['project_id'], name: 'index_repos_on_project_id') end create_table('uffizzi_core_roles', force: :cascade) do |t| t.string('name') t.string('resource_type') t.bigint('resource_id') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['name', 'resource_type', 'resource_id'], name: 'index_roles_on_name_and_resource_type_and_resource_id') t.index(['resource_type', 'resource_id'], name: 'index_roles_on_resource_type_and_resource_id') end create_table('uffizzi_core_templates', force: :cascade) do |t| t.string('name') t.bigint('added_by_id') t.jsonb('payload', null: false) t.bigint('project_id', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('compose_file_id') t.string('creation_source') t.index(['project_id'], name: 'index_templates_on_project_id') end create_table('uffizzi_core_user_projects', force: :cascade) do |t| t.bigint('user_id', null: false) t.bigint('project_id', null: false) t.text('role', null: false) t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.bigint('invited_by_id') t.index(['project_id'], name: 'index_user_projects_on_project_id') t.index(['user_id', 'project_id'], name: 'index_user_projects_on_user_id_and_project_id') t.index(['user_id'], name: 'index_user_projects_on_user_id') end create_table('uffizzi_core_users', force: :cascade) do |t| t.string('first_name') t.string('last_name') t.string('email', default: '', null: false) t.string('password_digest', default: '', null: false) t.string('confirmation_token') t.string('state') t.string('phone') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.string('github') t.string('website') t.string('twitter') t.string('linkedin') t.string('devto') t.string('facebook') t.string('blog') t.text('bio') t.string('status') t.string('availability') t.string('primary_skills') t.string('learning') t.string('coding_for') t.string('education') t.string('title') t.string('work') t.string('primary_location') t.string('creation_source') t.index('lower((email)::text)', name: 'index_email_on_lower_email', unique: true) end create_table('uffizzi_core_users_roles', id: false, force: :cascade) do |t| t.bigint('user_id') t.bigint('role_id') t.index(['role_id'], name: 'index_users_roles_on_role_id') t.index(['user_id', 'role_id'], name: 'index_users_roles_on_user_id_and_role_id') t.index(['user_id'], name: 'index_users_roles_on_user_id') end end end ================================================ FILE: core/db/migrate/20220309110201_remove_secrets_from_projects.rb ================================================ # frozen_string_literal: true class RemoveSecretsFromProjects < ActiveRecord::Migration[6.1] def change remove_column('uffizzi_core_projects', 'secrets') end end ================================================ FILE: core/db/migrate/20220310110150_create_project_secrets.rb ================================================ # frozen_string_literal: true class CreateProjectSecrets < ActiveRecord::Migration[6.1] def change create_table('uffizzi_core_project_secrets', force: :cascade) do |t| t.bigint('project_id', null: false) t.string('name') t.string('value') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['project_id'], name: 'index_project_secrets_on_project_id') end end end ================================================ FILE: core/db/migrate/20220325113342_add_name_to_uffizzi_containers.rb ================================================ # frozen_string_literal: true class AddNameToUffizziContainers < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :name, :string) end end ================================================ FILE: core/db/migrate/20220329123323_rename_project_secrets_to_secrets.rb ================================================ # frozen_string_literal: true class RenameProjectSecretsToSecrets < ActiveRecord::Migration[6.1] def change rename_table(:uffizzi_core_project_secrets, :uffizzi_core_secrets) end end ================================================ FILE: core/db/migrate/20220329124542_add_resource_to_secrets.rb ================================================ # frozen_string_literal: true class AddResourceToSecrets < ActiveRecord::Migration[6.1] def change add_belongs_to(:uffizzi_core_secrets, :resource, polymorphic: true) end end ================================================ FILE: core/db/migrate/20220329143241_remove_project_ref_from_secrets.rb ================================================ # frozen_string_literal: true class RemoveProjectRefFromSecrets < ActiveRecord::Migration[6.1] def change remove_reference(:uffizzi_core_secrets, :project) end end ================================================ FILE: core/db/migrate/20220419074956_add_health_check_to_containers.rb ================================================ # frozen_string_literal: true class AddHealthCheckToContainers < ActiveRecord::Migration[6.1] def change add_column :uffizzi_core_containers, :healthcheck, :jsonb end end ================================================ FILE: core/db/migrate/20220422151523_add_volumes_to_uffizzi_core_containers.rb ================================================ # frozen_string_literal: true class AddVolumesToUffizziCoreContainers < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :volumes, :jsonb) end end ================================================ FILE: core/db/migrate/20220525113412_rename_name_to_uffizzi_containers.rb ================================================ # frozen_string_literal: true class RenameNameToUffizziContainers < ActiveRecord::Migration[6.1] def change rename_column(:uffizzi_core_containers, :name, :service_name) end end ================================================ FILE: core/db/migrate/20220704135629_add_disabled_at_to_deployments.rb ================================================ # frozen_string_literal: true class AddDisabledAtToDeployments < ActiveRecord::Migration[6.1] def up change_table :uffizzi_core_deployments do |t| t.datetime :disabled_at end UffizziCore::Deployment.disabled.update_all('disabled_at = updated_at') end def down change_table :uffizzi_core_deployments do |t| t.remove :disabled_at end end end ================================================ FILE: core/db/migrate/20220805164628_add_metadata_to_deployment.rb ================================================ # frozen_string_literal: true class AddMetadataToDeployment < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_deployments, :metadata, :jsonb, default: {}) end end ================================================ FILE: core/db/migrate/20220901110752_create_host_volume_files.rb ================================================ # frozen_string_literal: true class CreateHostVolumeFiles < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_host_volume_files do |t| t.string :source t.string :path t.boolean :is_file t.binary :payload t.bigint :added_by_id t.timestamps t.references :project, null: false, foreign_key: true, index: { name: :index_host_volume_file_on_project_id }, foreign_key: { to_table: :uffizzi_core_projects } t.references :compose_file, null: false, foreign_key: true, index: { name: :index_host_volume_file_on_compose_file_id }, foreign_key: { to_table: :uffizzi_core_compose_files } end end end ================================================ FILE: core/db/migrate/20220901165313_create_container_host_volume_files.rb ================================================ # frozen_string_literal: true class CreateContainerHostVolumeFiles < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_container_host_volume_files do |t| t.string :source_path t.timestamps t.references :container, null: false, foreign_key: true, index: { name: :uf_core_cont_h_v_on_cont }, foreign_key: { to_table: :uffizzi_core_containers } t.references :host_volume_file, null: false, foreign_key: true, index: { name: :uf_core_cont_h_v_on_h_v_file }, foreign_key: { to_table: :uffizzi_core_host_volume_files } end end end ================================================ FILE: core/db/migrate/20220927113647_add_additional_subdomains_to_containers.rb ================================================ # frozen_string_literal: true class AddAdditionalSubdomainsToContainers < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :additional_subdomains, :string, array: true, default: []) end end ================================================ FILE: core/db/migrate/20230111000000_add_state_to_memberships.rb ================================================ # frozen_string_literal: true class AddStateToMemberships < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_memberships, :state, :string) end end ================================================ FILE: core/db/migrate/20230306142513_add_last_deploy_at_to_deployments.rb ================================================ # frozen_string_literal: true class AddLastDeployAtToDeployments < ActiveRecord::Migration[6.1] def up add_column :uffizzi_core_deployments, :last_deploy_at, :datetime UffizziCore::Deployment.where(last_deploy_at: nil).update_all('last_deploy_at = updated_at') end def down remove_column :uffizzi_core_deployments, :last_deploy_at end end ================================================ FILE: core/db/migrate/20230406154451_add_full_image_name_to_container.rb ================================================ # frozen_string_literal: true class AddFullImageNameToContainer < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :full_image_name, :string) end end ================================================ FILE: core/db/migrate/20230531135739_create_deployment_events.rb ================================================ # frozen_string_literal: true class CreateDeploymentEvents < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_deployment_events do |t| t.string :deployment_state t.string :message t.timestamps t.references :deployment, null: false, index: { name: :uf_core_dep_events_on_dep }, foreign_key: { to_table: :uffizzi_core_deployments } end end end ================================================ FILE: core/db/migrate/20230613101901_create_clusters.rb ================================================ # frozen_string_literal: true class CreateClusters < ActiveRecord::Migration[6.1] def change create_table('uffizzi_core_clusters', force: :cascade) do |t| t.references :project, null: false, foreign_key: true, index: { name: :index_cluster_on_project_id }, foreign_key: { to_table: :uffizzi_core_projects } t.bigint 'deployed_by_id', foreign_key: true t.string 'state' t.string 'name' t.text 'manifest' t.text 'kubeconfig' t.timestamps end end end ================================================ FILE: core/db/migrate/20230711101901_add_host_to_clusters.rb ================================================ # frozen_string_literal: true class AddHostToClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :host, :string) end end ================================================ FILE: core/db/migrate/20230810140316_add_source_to_uffizzi_core_clusters.rb ================================================ # frozen_string_literal: true class AddSourceToUffizziCoreClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :creation_source, :string) end end ================================================ FILE: core/db/migrate/20230824150022_update_name_constraint_to_projects.rb ================================================ # frozen_string_literal: true class UpdateNameConstraintToProjects < ActiveRecord::Migration[6.1] def change remove_index(:uffizzi_core_projects, [:account_id, :name]) add_index(:uffizzi_core_projects, [:account_id, :name], unique: true, where: "state = 'active'", name: 'proj_uniq_name') end end ================================================ FILE: core/db/migrate/20231009162719_create_uffizzi_core_kubernetes_distributions.rb ================================================ # frozen_string_literal: true class CreateUffizziCoreKubernetesDistributions < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_kubernetes_distributions do |t| t.string :version t.string :distro t.string :image t.boolean :default, default: false t.timestamps end end end ================================================ FILE: core/db/migrate/20231009182412_add_kubernetes_distribution_id_to_uffizzi_core_clusters.rb ================================================ # frozen_string_literal: true class AddKubernetesDistributionIdToUffizziCoreClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :kubernetes_distribution_id, :integer, foreign_key: true) end end ================================================ FILE: core/db/migrate/20240301200235_add_node_selector_to_cluster.rb ================================================ # frozen_string_literal: true class AddNodeSelectorToCluster < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :node_selector, :string) end end ================================================ FILE: core/db/migrate/20240314170113_delete_node_selector_from_cluster.rb ================================================ # frozen_string_literal: true class DeleteNodeSelectorFromCluster < ActiveRecord::Migration[6.1] def change remove_column(:uffizzi_core_clusters, :node_selector, :string) end end ================================================ FILE: core/db/seeds.rb ================================================ # frozen_string_literal: true user = UffizziCore::User.create!( email: 'admin@uffizzi.com', password: 'password', state: UffizziCore::User::STATE_ACTIVE, creation_source: UffizziCore::User.creation_source.system, ) account = UffizziCore::Account.create!( owner: user, name: 'default', state: UffizziCore::Account::STATE_ACTIVE, kind: UffizziCore::Account.kind.personal ) user.memberships.create!(account: account, role: UffizziCore::Membership.role.admin) project = account.projects.create!(name: 'default', slug: 'default', state: UffizziCore::Project::STATE_ACTIVE) project.user_projects.create!(user: user, role: UffizziCore::UserProject.role.admin) ================================================ FILE: core/lib/tasks/uffizzi_core_tasks.rake ================================================ # frozen_string_literal: true namespace :uffizzi_core do Rake::Task['install:migrations'].clear_comments desc 'Copy over the migration needed to the application' task install: :environment do if Rake::Task.task_defined?('uffizzi_core:install:migrations') Rake::Task['uffizzi_core:install:migrations'].invoke else Rake::Task['app:uffizzi_core:install:migrations'].invoke end end desc 'Create a new user' task create_user: :environment do UffizziCore::UserGeneratorService.safe_generate(ENV['UFFIZZI_USER_EMAIL'], ENV['UFFIZZI_USER_PASSWORD'], ENV['UFFIZZI_PROJECT_NAME']) end end ================================================ FILE: core/lib/uffizzi_core/engine.rb ================================================ # frozen_string_literal: true require 'active_model_serializers' module UffizziCore class Engine < ::Rails::Engine isolate_namespace UffizziCore config.before_initialize do config.i18n.load_path += Dir["#{config.root}/config/locales/**/*.yml"] end ActiveModelSerializers.config.adapter = :json end end ================================================ FILE: core/lib/uffizzi_core/version.rb ================================================ # frozen_string_literal: true module UffizziCore VERSION = '2.4.10' end ================================================ FILE: core/lib/uffizzi_core.rb ================================================ # frozen_string_literal: true require 'uffizzi_core/version' require 'uffizzi_core/engine' require 'aasm' require 'active_model_serializers' require 'ancestry' require 'aws-sdk-ecr' require 'aws-sdk-eventbridge' require 'aws-sdk-iam' require 'config' require 'hashie' require 'faraday' require 'enumerize' require 'jwt' require 'kaminari' require 'octokit' require 'pg' require 'pundit' require 'ransack' require 'responders' require 'rolify' require 'rswag/api' require 'rswag/ui' require 'sidekiq' require 'virtus' require 'faraday/follow_redirects' module UffizziCore mattr_accessor :dependencies, default: { rbac: 'UffizziCore::Rbac::UserAccessService', deployment_memory_module: 'UffizziCore::Deployment::MemoryService', template_memory_module: 'UffizziCore::Template::MemoryService', controller_settings: 'UffizziCore::ControllerSettingsService', ci_module: 'UffizziCore::CiService', } mattr_accessor :table_names, default: { accounts: :uffizzi_core_accounts, activity_items: :uffizzi_core_activity_items, builds: :uffizzi_core_builds, clusters: :uffizzi_core_clusters, comments: :uffizzi_core_comments, compose_files: :uffizzi_core_compose_files, config_files: :uffizzi_core_config_files, container_config_files: :uffizzi_core_container_config_files, containers: :uffizzi_core_containers, coupons: :uffizzi_core_coupons, credentials: :uffizzi_core_credentials, deployments: :uffizzi_core_deployments, events: :uffizzi_core_events, invitations: :uffizzi_core_invitations, memberships: :uffizzi_core_memberships, payments: :uffizzi_core_payments, prices: :uffizzi_core_prices, products: :uffizzi_core_products, projects: :uffizzi_core_projects, ratings: :uffizzi_core_ratings, repos: :uffizzi_core_repos, roles: :uffizzi_core_roles, secrets: :uffizzi_core_secrets, templates: :uffizzi_core_templates, user_projects: :uffizzi_core_user_projects, users: :uffizzi_core_users, users_roles: :uffizzi_core_users_roles, host_volume_files: :uffizzi_core_host_volume_files, container_host_volume_files: :uffizzi_core_container_host_volume_files, deployment_events: :uffizzi_core_deployment_events, kubernetes_distributions: :uffizzi_core_kubernetes_distributions, } mattr_accessor :user_creation_sources, default: [:system, :online_registration, :google, :sso] mattr_accessor :user_project_roles, default: [:admin, :developer, :viewer] mattr_accessor :account_sources, default: [:manual] mattr_accessor :compose_file_kinds, default: [:main, :temporary] mattr_accessor :event_states, default: [:waiting, :queued, :successful, :deployed, :failed, :building, :timeout, :cancelled, :deploying] mattr_accessor :cluster_creation_sources, default: [:manual] end ================================================ FILE: core/swagger/v1/swagger.json ================================================ { "swagger": "2.0", "info": { "title": "Uffizzi docs", "description": "Your API does this", "version": "1.0" }, "host": "lvh.me:7000", "basePath": "/", "schemes": [ "http" ], "paths": { "/api/cli/v1/account/credentials": { "get": { "tags": [ "Account/Credential" ], "operationId": "Account/Credential-index", "parameters": [ { "name": "credential", "description": "credential", "required": true, "in": "body", "schema": { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "type": { "type": "string" } } } } ], "responses": { "default": { "description": "Get a list of accounts credential" } }, "description": "Get a list of accounts credential", "summary": "Get a list of accounts credential", "x-controller": "uffizzi_core/api/cli/v1/account/credentials", "x-action": "index" }, "post": { "tags": [ "Account/Credential" ], "operationId": "Account/Credential-create", "parameters": [ { "name": "credential", "description": "credential", "required": true, "in": "body", "schema": { "type": "object", "properties": { "username": { "type": "string" }, "password": { "type": "string" }, "type": { "type": "string" } } } } ], "responses": { "default": { "description": "Create account credential", "examples": { "application/json": "Possible types:\nUffizziCore::Credential::Amazon, UffizziCore::Credential::Azure, UffizziCore::Credential::DockerHub,\nUffizziCore::Credential::DockerRegistry, UffizziCore::Credential::Google, UffizziCore::Credential::GithubContainerRegistry" } }, "201": { "description": "Created successfully", "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "username": { "type": "string" }, "password": { "type": "string" }, "type": { "type": "string" }, "state": { "type": "string" } } } }, "422": { "description": "Unprocessable entity", "schema": { "type": "object", "additionalProperties": { "type": "errors" } } } }, "description": "Create account credential", "summary": "Create account credential", "x-controller": "uffizzi_core/api/cli/v1/account/credentials", "x-action": "create" } }, "/api/cli/v1/account/credentials/{type}": { "put": { "tags": [ "Account/Credential" ], "operationId": "Account/Credential-update", "parameters": [ { "name": "credential", "description": "credential", "required": true, "in": "body", "schema": { "type": "object", "properties": { "type": { "type": "string" } } } }, { "name": "type", "description": "Credential type", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "id": { "type": "integer" }, "registry_url": { "type": "string" }, "username": { "type": "string" }, "password": { "type": "string" } } } }, "422": { "description": "Unprocessable entity", "schema": { "type": "object", "additionalProperties": { "type": "errors" } } } }, "description": "Update existing credential of the given type", "summary": "Update existing credential of the given type", "x-controller": "uffizzi_core/api/cli/v1/account/credentials", "x-action": "update" }, "delete": { "tags": [ "Account/Credential" ], "operationId": "Account/Credential-destroy", "parameters": [ { "name": "type", "description": "Type of the credential", "required": true, "in": "path", "type": "string" } ], "responses": { "204": { "description": "No Content" }, "401": { "description": "Not authorized" }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } } }, "description": "Delete account credential", "summary": "Delete account credential", "x-controller": "uffizzi_core/api/cli/v1/account/credentials", "x-action": "destroy" } }, "/api/cli/v1/account/credentials/{type}/check_credential": { "get": { "tags": [ "Account/Credential" ], "operationId": "Account/Credential-check_credential", "parameters": [ { "name": "credential", "description": "credential", "required": true, "in": "body", "schema": { "type": "object", "properties": { "type": { "type": "string" } } } }, { "name": "type", "description": "Scope response to type", "required": true, "in": "path", "type": "string" } ], "responses": { "422": { "description": "Unprocessable entity" }, "200": { "description": "OK" } }, "description": "Check if credential of the type already exists in the account", "summary": "Check if credential of the type already exists in the account", "x-controller": "uffizzi_core/api/cli/v1/account/credentials", "x-action": "check_credential" } }, "/api/cli/v1/projects/{project_slug}/compose_file": { "get": { "tags": [ "ComposeFile" ], "operationId": "ComposeFile-show", "parameters": [ { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/ComposeFile" } }, "401": { "description": "Not authorized" }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } } }, "description": "Get the compose file for the project", "summary": "Get the compose file for the project", "x-controller": "uffizzi_core/api/cli/v1/projects/compose_files", "x-action": "show" }, "post": { "tags": [ "ComposeFile" ], "operationId": "ComposeFile-create", "parameters": [ { "name": "params", "description": "params", "required": true, "in": "body", "schema": { "type": "object", "properties": { "compose_file": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } }, "dependencies": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } } } } } }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "201": { "description": "OK", "schema": { "$ref": "#/definitions/ComposeFile" } }, "422": { "description": "Invalid compose file", "schema": { "$ref": "#/definitions/ComposeFile" } }, "401": { "description": "Not authorized" } }, "description": "Create a compose file for the project", "summary": "Create a compose file for the project", "x-controller": "uffizzi_core/api/cli/v1/projects/compose_files", "x-action": "create" }, "delete": { "tags": [ "ComposeFile" ], "operationId": "ComposeFile-destroy", "parameters": [ { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "204": { "description": "No Content" }, "401": { "description": "Not authorized" } }, "description": "Delete the compose file for the project", "summary": "Delete the compose file for the project", "x-controller": "uffizzi_core/api/cli/v1/projects/compose_files", "x-action": "destroy" } }, "/api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/containers/{container_name}/logs": { "get": { "tags": [ "Project/Deployment/Container/Log" ], "operationId": "Project/Deployment/Container/Log-index", "parameters": [ { "name": "container_name", "description": "The name of the container", "required": true, "in": "path", "type": "integer" }, { "name": "deployment_id", "description": "The id of the deployment", "required": true, "in": "path", "type": "integer" }, { "name": "project_slug", "description": "The slug of the project", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "logs": { "type": "array", "items": { "type": "object", "properties": { "insert_id": { "type": "string" }, "payload": { "type": "string" } } } } } } }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, "401": { "description": "Not authorized" } }, "x-controller": "uffizzi_core/api/cli/v1/projects/deployments/containers/logs", "x-action": "index" } }, "/api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/containers": { "get": { "tags": [ "Container" ], "operationId": "Container-index", "parameters": [ { "name": "deployment_id", "description": "The id of the deployment", "required": true, "in": "path", "type": "integer" }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/Container" } }, "401": { "description": "Not authorized" }, "404": { "description": "Not found" } }, "description": "Get a list of container services for a deployment", "summary": "Get a list of container services for a deployment", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments/containers", "x-action": "index" } }, "/api/cli/v1/projects/{project_slug}/deployments/{deployment_id}/events": { "get": { "tags": [ "Event" ], "operationId": "Event-index", "parameters": [ { "name": "deployment_id", "description": "The id of the deployment", "required": true, "in": "path", "type": "integer" }, { "name": "project_slug", "description": "The project_slug for the project", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "events": { "type": "array", "items": { "type": "object", "properties": { "first_timestamp": { "type": "string" }, "last_timestamp": { "type": "string" }, "reason": { "type": "string" }, "message": { "type": "string" } } } } } } }, "401": { "description": "Not authorized" }, "404": { "description": "Not found" } }, "description": "Get the events associated with deployment", "summary": "Get the events associated with deployment", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments/events", "x-action": "index" } }, "/api/cli/v1/projects/{project_slug}/deployments": { "get": { "tags": [ "Deployment" ], "operationId": "Deployment-index", "parameters": [ { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Deployment" } } }, "401": { "description": "Not authorized" } }, "description": "Get a list of active deployements for a project", "summary": "Get a list of active deployements for a project", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "index" }, "post": { "tags": [ "Deployment" ], "operationId": "Deployment-create", "parameters": [ { "name": "params", "description": "params", "required": true, "in": "body", "schema": { "type": "object", "properties": { "compose_file": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } }, "dependencies": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } } } } } }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "201": { "description": "OK", "schema": { "$ref": "#/definitions/Deployment" } }, "422": { "description": "Unprocessable Entity", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "state": { "type": "string" } } } } } }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, "401": { "description": "Not authorized" } }, "description": "Create a deployment from a compose file", "summary": "Create a deployment from a compose file", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "create" } }, "/api/cli/v1/projects/{project_slug}/deployments/{id}": { "get": { "tags": [ "Deployment" ], "operationId": "Deployment-show", "parameters": [ { "name": "id", "description": "Scope response to id", "required": true, "in": "path", "type": "string" }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/Deployment" } }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, "401": { "description": "Not authorized" } }, "description": "Get deployment information by id", "summary": "Get deployment information by id", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "show" }, "delete": { "tags": [ "Deployment" ], "operationId": "Deployment-destroy", "parameters": [ { "name": "id", "description": "Scope response to id", "required": true, "in": "path", "type": "string" }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "204": { "description": "No Content" }, "401": { "description": "Not authorized" } }, "description": "Disable deployment by id", "summary": "Disable deployment by id", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "destroy" } }, "/api/cli/v1/projects/{project_slug}/deployments/{id}\"": { "put": { "tags": [ "Deployment" ], "operationId": "Deployment-update", "parameters": [ { "name": "id", "description": "Scope response to id", "required": true, "in": "path", "type": "string" }, { "name": "params", "description": "params", "required": true, "in": "body", "schema": { "type": "object", "properties": { "compose_file": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } }, "dependencies": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "source": { "type": "string" }, "content": { "type": "string" } } } } } } }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "201": { "description": "OK", "schema": { "$ref": "#/definitions/Deployment" } }, "422": { "description": "Unprocessable Entity", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "state": { "type": "string" } } } } } }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, "401": { "description": "Not authorized" } }, "description": "Update the deployment with new compose file", "summary": "Update the deployment with new compose file", "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "update" } }, "/api/cli/v1/projects/{project_slug}/deployments/{id}/deploy_containers": { "post": { "tags": [ "Deployment" ], "operationId": "Deployment-deploy_containers", "parameters": [ { "name": "id", "description": "The id of the deployment", "required": true, "in": "path", "type": "string" }, { "name": "project_slug", "description": "The project slug", "required": true, "in": "path", "type": "string" } ], "responses": { "204": { "description": "No Content" }, "404": { "description": "Not found", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "title": { "type": "string" } } } } } }, "401": { "description": "Not authorized" } }, "x-controller": "uffizzi_core/api/cli/v1/projects/deployments", "x-action": "deploy_containers" } }, "/api/cli/v1/projects/{project_slug}/secrets": { "get": { "tags": [ "Project/Secrets" ], "operationId": "Project/Secrets-index", "parameters": [ { "name": "project_slug", "description": "project_slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "secrets": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "created_at": { "type": "string", "format": "date" }, "updated_at": { "type": "string", "format": "date" } } } } } } }, "401": { "description": "Not authorized" } }, "description": "Get secrets for the project", "summary": "Get secrets for the project", "x-controller": "uffizzi_core/api/cli/v1/projects/secrets", "x-action": "index" } }, "/api/cli/v1/projects/{project_slug}/secrets/bulk_create": { "post": { "tags": [ "Project/Secrets" ], "operationId": "Project/Secrets-bulk_create", "parameters": [ { "name": "project_slug", "description": "project_slug", "required": true, "in": "path", "type": "string" }, { "name": "secrets", "description": "secrets", "required": true, "in": "body", "schema": { "type": "object", "properties": { "secrets": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "value": { "type": "string" } } } } } } } ], "responses": { "201": { "description": "Created", "schema": { "type": "object", "properties": { "secrets": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "created_at": { "type": "string", "format": "date" }, "updated_at": { "type": "string", "format": "date" } } } } } } }, "422": { "description": "A compose file already exists for this project" }, "401": { "description": "Not authorized" } }, "description": "Add secret to project", "summary": "Add secret to project", "x-controller": "uffizzi_core/api/cli/v1/projects/secrets", "x-action": "bulk_create" } }, "/api/cli/v1/projects/{project_slug}/secrets/{secret_name}": { "delete": { "tags": [ "Project/Secrets" ], "operationId": "Project/Secrets-destroy", "parameters": [ { "name": "project_slug", "description": "project_slug", "required": true, "in": "path", "type": "string" }, { "name": "secret_name", "description": "Scope response to secret_name", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/Project" } }, "404": { "description": "Delete a secret from project by secret name" }, "401": { "description": "Not authorized" } }, "description": "Delete a secret from project by secret name", "summary": "Delete a secret from project by secret name", "x-controller": "uffizzi_core/api/cli/v1/projects/secrets", "x-action": "destroy" } }, "/api/cli/v1/projects": { "get": { "tags": [ "Project" ], "operationId": "Project-index", "parameters": [ ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "projects": { "type": "array", "items": { "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" } } } } } } }, "401": { "description": "Not authorized" } }, "description": "Get projects of current user", "summary": "Get projects of current user", "x-controller": "uffizzi_core/api/cli/v1/projects", "x-action": "index" }, "post": { "tags": [ "Project" ], "operationId": "Project-create", "parameters": [ { "name": "params", "description": "params", "required": true, "in": "body", "schema": { "type": "object", "properties": { "name": { "type": "string" }, "slug": { "type": "string" }, "description": { "type": "string" } } } } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "project": { "$ref": "#/definitions/Project" } } } }, "404": { "description": "Not Found" }, "401": { "description": "Not authorized" }, "422": { "description": "Unprocessable entity", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "password": { "type": "string" } } } } } } }, "description": "Create a project", "summary": "Create a project", "x-controller": "uffizzi_core/api/cli/v1/projects", "x-action": "create" } }, "/api/cli/v1/projects/{slug}": { "get": { "tags": [ "Project" ], "operationId": "Project-show", "parameters": [ { "name": "slug", "description": "Scope response to slug", "required": true, "in": "path", "type": "string" } ], "responses": { "204": { "description": "No content" }, "404": { "description": "Not Found" }, "401": { "description": "Not authorized" } }, "description": "Get a project by slug", "summary": "Get a project by slug", "x-controller": "uffizzi_core/api/cli/v1/projects", "x-action": "show" }, "delete": { "tags": [ "Project" ], "operationId": "Project-destroy", "parameters": [ { "name": "slug", "description": "Scope response to slug", "required": true, "in": "path", "type": "string" } ], "responses": { "200": { "description": "OK", "schema": { "type": "object", "properties": { "project": { "$ref": "#/definitions/Project" } } } }, "404": { "description": "Not Found" }, "401": { "description": "Not authorized" } }, "description": "Delete a project", "summary": "Delete a project", "x-controller": "uffizzi_core/api/cli/v1/projects", "x-action": "destroy" } }, "/api/cli/v1/session": { "post": { "tags": [ "Uffizzi" ], "operationId": "Uffizzi-create", "parameters": [ { "name": "user", "description": "user", "required": true, "in": "body", "schema": { "type": "object", "properties": { "email": { "type": "string" }, "password": { "type": "string" } } } } ], "responses": { "201": { "description": "Created successfully", "schema": { "type": "object", "properties": { "user": { "type": "object", "properties": { "accounts": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "state": { "type": "string" } } } } } } } } }, "422": { "description": "Unprocessable entity", "schema": { "type": "object", "properties": { "errors": { "type": "object", "properties": { "password": { "type": "string" } } } } } } }, "description": "Create session", "summary": "Create session", "x-controller": "uffizzi_core/api/cli/v1/sessions", "x-action": "create" }, "delete": { "tags": [ "Uffizzi" ], "operationId": "Uffizzi-destroy", "parameters": [ ], "responses": { "204": { "description": "No Content" } }, "description": "Destroy session", "summary": "Destroy session", "x-controller": "uffizzi_core/api/cli/v1/sessions", "x-action": "destroy" } } }, "tags": [ { "name": "Account/Credential", "description": "" }, { "name": "ComposeFile", "description": "" }, { "name": "Container", "description": "" }, { "name": "Deployment", "description": "" }, { "name": "Event", "description": "" }, { "name": "Project", "description": "" }, { "name": "Project/Deployment/Container/Log", "description": "" }, { "name": "Project/Secrets", "description": "" }, { "name": "Uffizzi", "description": "" } ], "securityDefinitions": { }, "definitions": { "UffizziCore_ActivityItem": { "type": "object", "properties": { "id": { "type": "integer" }, "namespace": { "type": "string" }, "name": { "type": "string" }, "tag": { "type": "string" }, "branch": { "type": "string" }, "type": { "type": "string" }, "container_id": { "type": "string" }, "commit": { "type": "string" }, "commit_message": { "type": "string" }, "build_id": { "type": "integer" }, "created_at": { "type": "string", "format": "date" }, "updated_at": { "type": "string", "format": "date" }, "data": { "type": "object" }, "digest": { "type": "string" }, "meta": { "type": "object" } }, "required": [ "name" ] }, "ComposeFile": { "type": "object", "properties": { "id": { "type": "integer" }, "source": { "type": "string" }, "path": { "type": "string" }, "content": { "type": "string" }, "auto_deploy": { "type": "boolean" }, "state": { "type": "string" }, "payload": { "type": "string" } }, "required": [ "content" ] }, "UffizziCore_ConfigFile": { "type": "object", "properties": { "filename": { "type": "string" }, "kind": { "type": "string" }, "payload": { "type": "string" }, "source": { "type": "string" } } }, "Container": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "memory_limit": { "type": "integer" }, "memory_request": { "type": "integer" }, "continuously_deploy": { "type": "string" }, "variables": { "type": "object" }, "secret_variables": { "type": "object" }, "container_config_files": { "$ref": "#/definitions/ConfigFile" } } }, "Deployment": { "type": "object", "properties": { "id": { "type": "integer" }, "project_id": { "type": "integer" }, "kind": { "type": "string" }, "state": { "type": "string" }, "preview_url": { "type": "string" }, "tag": { "type": "string" }, "branch": { "type": "string" }, "commit": { "type": "string" }, "image_id": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }, "ingress_container_ready": { "type": "boolean" }, "ingress_container_state": { "type": "string" }, "creation_source": { "type": "string" }, "contaners": { "type": "array", "items": { "type": "string" } }, "deployed_by": { "type": "object" } } }, "Project": { "type": "object", "properties": { "slug": { "type": "string" }, "name": { "type": "string" }, "description": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }, "secrets": { "type": "string" }, "default_compose": { "type": "object", "properties": { "source": { "type": "string" } } }, "deployments": { "type": "object", "properties": { "id": { "type": "integer" }, "domain": { "type": "string" } } } } } } } ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/accounts/clusters_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Accounts::ClustersControllerTest < ActionController::TestCase setup do @developer = create(:user, :with_organizational_account) sign_in(@developer) end test '#index' do admin_user = create(:user, :with_organizational_account) admin_user_org_account = admin_user.accounts.organizational.first create(:membership, :developer, user: @developer, account: admin_user_org_account) admin_project = create(:project, :with_members, account: admin_user_org_account, members: [admin_user, @developer]) create(:cluster, :deployed, project: admin_project, deployed_by_id: admin_user.id) showable_cluster1 = create(:cluster, :deployed, project: admin_project, deployed_by_id: @developer.id) admin_project2 = create(:project, :with_members, account: admin_user_org_account, members: [admin_user]) create(:user_project, :admin, user: @developer, project: admin_project2) showable_cluster2 = create(:cluster, :deployed, project: admin_project2, deployed_by_id: admin_user.id) other_user = create(:user, :with_organizational_account) other_user_account = other_user.accounts.organizational.first create(:membership, :developer, user: @developer, account: other_user_account) other_project = create(:project, :with_members, account: other_user_account, members: [other_user, @developer]) create(:cluster, :deployed, project: other_project, deployed_by_id: other_user.id) create(:cluster, :deployed, project: other_project, deployed_by_id: @developer.id) get :index, params: { account_id: admin_user_org_account.id }, format: :json assert_response(:success) clusters_data = JSON.parse(response.body)['clusters'] assert_equal(2, clusters_data.count) assert_equal([showable_cluster1.name, showable_cluster2.name], clusters_data.pluck('name').sort) actual_project_name1 = clusters_data.min_by { |c| c['name'] }.dig('project', 'name') actual_project_name2 = clusters_data.max_by { |c| c['name'] }.dig('project', 'name') assert_equal(showable_cluster1.project.name, actual_project_name1) assert_equal(showable_cluster2.project.name, actual_project_name2) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/accounts/credentials_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Accounts::CredentialsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) @account = @user.personal_account @project = create(:project, :with_members, account: @account, members: [@user]) sign_in @user Sidekiq::Worker.clear_all Sidekiq::Testing.fake! end teardown do Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#index returns a list of credentials' do create(:credential, :docker_hub, account: @account) create(:credential, :amazon, account: @account) params = { account_id: @account.id } get :index, params: params, format: :json assert_response :success data = JSON.parse(response.body) assert_equal(['UffizziCore::Credential::DockerHub', 'UffizziCore::Credential::Amazon'], data['credentials']) end test '#create docker hub credential' do stubbed_dockerhub_login = stub_dockerhub_login attributes = attributes_for(:credential, :docker_hub) params = { account_id: @account.id, credential: attributes } differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success assert_requested(stubbed_dockerhub_login) end test '#create if docker hub auth is failed' do data = json_fixture('files/dockerhub/login_fail.json') stubbed_dockerhub_login = stub_dockerhub_login_fail(data) attributes = attributes_for(:credential, :docker_hub) params = { account_id: @account.id, credential: attributes } differences = { -> { UffizziCore::Credential.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :unprocessable_entity assert_requested(stubbed_dockerhub_login) end test '#create azure credential' do registry_url = generate(:url) oauth2_token_response = { access_token: generate(:string) } stubbed_azure_registry_oauth2_token = stub_azure_registry_oauth2_token(registry_url, oauth2_token_response) attributes = attributes_for(:credential, :azure, registry_url: registry_url) params = { account_id: @account.id, credential: attributes } differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_requested stubbed_azure_registry_oauth2_token assert_response :success end test '#create google credential' do attributes = attributes_for(:credential, :google) params = { account_id: @account.id, credential: attributes } token_response = { token: generate(:string) } stubbed_google_registry_token = stub_google_registry_token(token_response) differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_requested stubbed_google_registry_token assert_response :success end test '#create amazon credential' do attributes = attributes_for(:credential, :amazon) params = { account_id: @account.id, credential: attributes } UffizziCore::ContainerRegistry::AmazonService.expects(:credential_correct?).at_least(1).returns(true) differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create github_container_registry credential' do attributes = attributes_for(:credential, :github_container_registry) params = { account_id: @account.id, credential: attributes } registry_url = attributes[:registry_url] token_response = { token: generate(:string) } stub_github_container_registry_access_token(registry_url, token_response) differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create docker registry credential to ecr' do attributes = attributes_for(:credential, :docker_registry) attributes[:registry_url] = attributes_for(:credential, :amazon)[:registry_url] params = { account_id: @account.id, credential: attributes } UffizziCore::ContainerRegistry::DockerRegistryService.expects(:credential_correct?).at_least(1).returns(true) differences = { -> { UffizziCore::Credential.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#update when data changed' do stubbed_dockerhub_login = stub_dockerhub_login credential = create(:credential, :docker_hub, account: @account) new_username = 'new username' credential_attributes = attributes_for(:credential, :docker_hub, account: @account).merge(username: new_username) params = { account_id: @account.id, credential: credential_attributes, type: credential_attributes[:type] } differences = { -> { UffizziCore::Credential.count } => 0, } assert_difference(differences) do put :update, params: params, format: :json end assert_response(:success) assert_equal(credential.reload.username, new_username) assert_requested(stubbed_dockerhub_login) end test '#update when credential is the same' do stubbed_dockerhub_login = stub_dockerhub_login credential = create(:credential, :docker_hub, account: @account) params = { account_id: @account.id, credential: { password: credential.password, username: credential.username, registry_url: credential.registry_url, }, type: credential.type, } put :update, params: params, format: :json assert_response(:success) refute_requested(stubbed_dockerhub_login) end test '#create duplicate credential' do stub_dockerhub_login stub_controller credential = create(:credential, :docker_hub, account: @account) differences = { -> { UffizziCore::Credential.count } => 0 } attributes = attributes_for(:credential, :docker_hub, account: @account) params = { account_id: @account.id, credential: attributes } assert_difference differences do post :create, params: params, format: :json end assert_response :unprocessable_entity assert_equal UffizziCore::Credential.last.id, credential.id end test '#check_credential valid credential' do stub_dockerhub_login stub_controller attributes = attributes_for(:credential, :docker_hub, account: @account) params = { account_id: @account.id, type: attributes[:type] } post :check_credential, params: params, format: :json assert_response :success end test '#check_credential duplicate credential' do stub_dockerhub_login stub_controller credential = create(:credential, :docker_hub, account: @account) params = { account_id: @account.id, type: credential.type } post :check_credential, params: params, format: :json assert_response :unprocessable_entity end test '#destroy docker hub credential' do credential = create(:credential, :docker_hub, account: @account) params = { account_id: @account.id, type: credential.type } differences = { -> { UffizziCore::Credential.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy azure credential' do credential = create(:credential, :azure, account: @account) params = { account_id: @account.id, type: credential.type } differences = { -> { UffizziCore::Credential.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy google credential' do credential = create(:credential, :google, account: @account) params = { account_id: @account.id, type: credential.type } differences = { -> { UffizziCore::Credential.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy amazon credential' do credential = create(:credential, :amazon, account: @account) params = { account_id: @account.id, type: credential.type } differences = { -> { UffizziCore::Credential.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy github_container_registry credential' do credential = create(:credential, :github_container_registry, account: @account) params = { account_id: @account.id, type: credential.type } differences = { -> { UffizziCore::Credential.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy unexisted credential' do params = { account_id: @account.id, type: UffizziCore::Credential::DockerHub.name } differences = { -> { UffizziCore::Credential.count } => 0, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :not_found end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/accounts/projects_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Accounts::ProjectsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_organizational_account) @account = @admin.accounts.organizational.first sign_in(@admin) end test '#index' do create(:project, :with_members, account: @account, members: [@admin]) get :index, params: { account_id: @account.id }, format: :json data = JSON.parse(response.body) assert(data['projects'].first['account'].present?) assert_response(:success) end test '#create' do attributes = attributes_for(:project) create(:user, :developer_in_organization, organization: @account) differences = { -> { UffizziCore::Project.count } => 1, -> { UffizziCore::UserProject.count } => 2, } assert_difference differences do post :create, params: { account_id: @account.id, project: attributes }, format: :json end assert_response(:success) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/accounts_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::AccountsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) sign_in(@user) end test '#index' do get :index, format: :json assert_response(:success) end test '#show' do get :show, params: { name: 'wrong' }, format: :json assert_response(:not_found) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/clusters/ingresses_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::Clusters::IngressesControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) @cluster = create(:cluster, project: @project, deployed_by: @user) sign_in @user end test '#index' do data = json_fixture('files/controller/ingresses.json') stubbed_get_ingresses_request = stub_get_ingresses(data) params = { project_slug: @project.slug, cluster_name: @cluster.name } get :index, params: params, format: :json assert_response(:success) assert_requested(stubbed_get_ingresses_request) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/clusters_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::ClustersControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_organizational_account) @account = @admin.accounts.organizational.first @project = create(:project, :with_members, members: [@admin], account: @account) @developer = create(:user) create(:membership, :developer, account: @account, user: @developer) create(:user_project, :developer, project: @project, user: @developer) create(:kubernetes_distribution, :default) end teardown do Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#index lists all clusters to admins' do sign_in(@admin) create(:cluster, project: @project, deployed_by: @developer) params = { project_slug: @project.slug, } get :index, params: params, format: :json assert_response(:success) data = JSON.parse(response.body) assert_equal(1, data['clusters'].count) end test '#index only shows clusters deployed by the same user for non-adminss' do create(:cluster, project: @project, deployed_by: @admin) create(:cluster, project: @project, deployed_by: @developer) sign_in(@developer) params = { project_slug: @project.slug, } get :index, params: params, format: :json assert_response(:success) data = JSON.parse(response.body) assert_equal(1, data['clusters'].count) end test '#create' do sign_in(@admin) cluster_creation_data = json_fixture('files/controller/cluster_not_ready.json') params = { project_slug: @project.slug, cluster: { name: cluster_creation_data[:name], }, } expected_request = { name: cluster_creation_data[:name], manifest: nil, base_ingress_host: /#{UffizziCore::Cluster::NAMESPACE_PREFIX}\d/, } stubbed_create_cluster_request = stub_create_cluster_request_with_expected(cluster_creation_data, expected_request) stubbed_create_namespace_request = stub_create_namespace_request cluster_show_data = json_fixture('files/controller/cluster_ready.json') stubbed_cluster_request = stub_get_cluster_request(cluster_show_data) differences = { -> { UffizziCore::Cluster.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response(:success) assert(UffizziCore::Cluster.find_by(name: cluster_creation_data[:name]).creation_source.manual?) assert_requested(stubbed_create_cluster_request) assert_requested(stubbed_create_namespace_request) assert_requested(stubbed_cluster_request) end test '#create with wrong k8s version' do sign_in(@admin) params = { project_slug: @project.slug, cluster: { name: 'my cluster', k8s_version: 'wrong.version', }, } differences = { -> { UffizziCore::Cluster.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response(:unprocessable_entity) end test '#create when enabled cluster with the same name exists' do sign_in(@admin) name = 'test' create(:cluster, project: @project, deployed_by: @admin, name: name) params = { project_slug: @project.slug, cluster: { name: name, }, } differences = { -> { UffizziCore::Cluster.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response(:unprocessable_entity) end test '#create with manifest' do sign_in(@admin) manifest = File.read('test/fixtures/files/cluster/manifest.yml') cluster_creation_data = json_fixture('files/controller/cluster_not_ready.json') cluster_show_data = json_fixture('files/controller/cluster_ready.json') params = { project_slug: @project.slug, cluster: { name: cluster_creation_data[:name], manifest: manifest, }, } stubbed_create_namespace_request = stub_create_namespace_request expected_request = { name: cluster_creation_data[:name], manifest: manifest, base_ingress_host: /#{UffizziCore::Cluster::NAMESPACE_PREFIX}\d/, } stubbed_create_cluster_request = stub_create_cluster_request_with_expected(cluster_creation_data, expected_request) stubbed_get_cluster_request = stub_get_cluster_request(cluster_show_data) differences = { -> { UffizziCore::Cluster.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response(:success) assert_requested(stubbed_create_cluster_request) assert_requested(stubbed_create_namespace_request) assert_requested(stubbed_get_cluster_request) end test '#show shows cluster created by the same developer' do cluster = create(:cluster, project: @project, deployed_by: @developer, name: 'test') sign_in(@developer) params = { project_slug: @project.slug, name: cluster.name, } get :show, params: params, format: :json assert_response(:success) end test '#show does not show cluster created by a different user to developer' do sign_in(@developer) cluster = create(:cluster, project: @project, deployed_by: @admin, name: 'test') params = { project_slug: @project.slug, name: cluster.name, } get :show, params: params, format: :json assert_response(:not_found) end test '#show shows clusters created by a different user to admin' do sign_in(@admin) cluster = create(:cluster, project: @project, deployed_by: @developer, name: 'test') params = { project_slug: @project.slug, name: cluster.name, } get :show, params: params, format: :json assert_response(:success) end test '#scale_down' do sign_in(@admin) cluster = create(:cluster, project: @project, deployed_by: @admin, name: 'test', state: UffizziCore::Cluster::STATE_DEPLOYED) stubbed_scale_request = stub_scale_cluster_request cluster_show_data = json_fixture('files/controller/cluster_asleep.json') stubbed_cluster_request = stub_get_cluster_request(cluster_show_data) params = { project_slug: @project.slug, name: cluster.name, } put :scale_down, params: params, format: :json assert_response(:success) assert(cluster.reload.scaled_down?) assert_requested(stubbed_scale_request) assert_requested(stubbed_cluster_request) end test '#scale_up' do sign_in(@admin) cluster = create(:cluster, project: @project, deployed_by: @admin, name: 'test', state: UffizziCore::Cluster::STATE_SCALED_DOWN) stubbed_scale_request = stub_scale_cluster_request cluster_show_data = json_fixture('files/controller/cluster_awake.json') stubbed_cluster_request = stub_get_cluster_request(cluster_show_data) params = { project_slug: @project.slug, name: cluster.name, } put :scale_up, params: params, format: :json assert_response(:success) assert(cluster.reload.deployed?) assert_requested(stubbed_scale_request) assert_requested(stubbed_cluster_request) end test '#sync when the data is actual' do sign_in(@admin) cluster = create(:cluster, project: @project, deployed_by: @admin, name: 'test', state: UffizziCore::Cluster::STATE_DEPLOYED) cluster_show_data = json_fixture('files/controller/cluster_awake.json') stubbed_cluster_request = stub_get_cluster_request(cluster_show_data) params = { project_slug: @project.slug, name: cluster.name, } put :sync, params: params, format: :json assert_requested(stubbed_cluster_request) assert(cluster.reload.deployed?) end test '#sync when the data needs to be updated' do sign_in(@admin) cluster = create(:cluster, project: @project, deployed_by: @admin, name: 'test', state: UffizziCore::Cluster::STATE_DEPLOYED) cluster_show_data = json_fixture('files/controller/cluster_asleep.json') stubbed_cluster_request = stub_get_cluster_request(cluster_show_data) params = { project_slug: @project.slug, name: cluster.name, } put :sync, params: params, format: :json assert_requested(stubbed_cluster_request) assert(cluster.reload.scaled_down?) end test '#destroy developer can destroy a cluster created by him' do sign_in(@developer) cluster = create(:cluster, :deployed, project: @project, deployed_by: @developer, name: 'test') stubbed_delete_namespace_request = stub_delete_namespace_request(cluster) params = { project_slug: @project.slug, name: cluster.name, } delete :destroy, params: params, format: :json assert_response(:success) assert(cluster.reload.disabled?) assert_requested(stubbed_delete_namespace_request) end test '#destroy developer cannot destroy a cluster created by other user' do sign_in(@developer) cluster = create(:cluster, :deployed, project: @project, deployed_by: @admin, name: 'test') stubbed_delete_namespace_request = stub_delete_namespace_request(cluster) params = { project_slug: @project.slug, name: cluster.name, } delete :destroy, params: params, format: :json assert_response(:not_found) refute_requested(stubbed_delete_namespace_request) end test '#destroy admin can destroy a cluster created by other user' do sign_in(@admin) cluster = create(:cluster, :deployed, project: @project, deployed_by: @developer, name: 'test') stubbed_delete_namespace_request = stub_delete_namespace_request(cluster) params = { project_slug: @project.slug, name: cluster.name, } delete :destroy, params: params, format: :json assert_response(:success) assert(cluster.reload.disabled?) assert_requested(stubbed_delete_namespace_request) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/compose_files_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::ComposeFilesControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @project = create(:project, :with_members, account: @account, members: [@admin]) @compose_file = create(:compose_file, project: @project, added_by: @admin) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: @compose_file, project: @project, added_by: @admin, payload: template_payload) end test '#show - admin gets compose file' do sign_in @admin params = { project_slug: @project.slug } get :show, params: params, format: :json assert_response :success end test '#show - returns 404 if compose file does not exist' do sign_in @admin @compose_file.destroy! params = { project_slug: @project.slug } get :show, params: params, format: :json error_message = JSON.parse(response.body, symbolize_names: true)[:errors][:title].first assert_response :not_found assert_equal('UffizziCore::ComposeFile Not Found', error_message) end test '#create - docker compose file' do sign_in @admin @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-vote-app-docker.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) dependency = { path: 'configs/vote.conf', source: 'vote.conf', content: json_fixture('files/compose_dependencies/configs/vote_conf.json')[:content], use_kind: UffizziCore::ComposeFile::DependenciesService::DEPENDENCY_CONFIG_USE_KIND, } stub_dockerhub_repository_any params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [dependency], } differences = { -> { UffizziCore::ComposeFile.main.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create - main compose file when already exists' do sign_in @admin base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/test-compose-success-without-dependencies.yml') encoded_content = Base64.encode64(file_content) stub_dockerhub_repository_any compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], } differences = { -> { UffizziCore::ComposeFile.main.count } => 0, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create - check amazon ecr creation' do sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :amazon, account: @account) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-amazon.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::Template.count } => 1, -> { UffizziCore::ComposeFile.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create - check github registry creation' do sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :github_container_registry, account: @account) create(:credential, :docker_hub, account: @account) @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) content = json_fixture('files/github/compose_files/hello_world_compose_github_container_registry.json')[:content] compose_file_attributes = base_attributes.merge(content: content) differences = { -> { UffizziCore::Template.count } => 1, -> { UffizziCore::ComposeFile.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test 'create - ECR via Docker Registry' do sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) registry_url = 'https://323707565364.dkr.ecr.us-east-1.amazonaws.com' credential = create(:credential, :docker_registry, username: 'AWS', registry_url: registry_url, account: @account) @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-amazon.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content) stubbed_docker_registry_manifests = stub_docker_registry_manifests(credential.registry_url, 'test-compose', 'latest') differences = { -> { UffizziCore::Template.count } => 1, -> { UffizziCore::ComposeFile.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], } assert_difference differences do post :create, params: params, format: :json end assert_response :success assert_requested(stubbed_docker_registry_manifests) end test '#create - compose file with volumes' do sign_in @admin @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/compose_files/compose_with_volumes.yml') stub_dockerhub_repository_any encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], } differences = { -> { UffizziCore::ComposeFile.main.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create - with yaml aliases' do sign_in @admin @compose_file.destroy! stub_dockerhub_repository('library', 'nginx') base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-with_alias.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) dependency = { path: 'configs/vote.conf', source: 'vote.conf', content: json_fixture('files/compose_dependencies/configs/vote_conf.json')[:content], use_kind: UffizziCore::ComposeFile::DependenciesService::DEPENDENCY_CONFIG_USE_KIND, } params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [dependency], } differences = { -> { UffizziCore::ComposeFile.main.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success end test '#create - string command' do sign_in @admin @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-vote-app-with-command-as-string.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) stub_dockerhub_repository_any params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], } differences = { -> { UffizziCore::ComposeFile.main.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success template = UffizziCore::Template.last assert_equal('["postgres", "-c", "jit=off"]', template.payload['containers_attributes'].first['command']) end test '#create - service name does not match RFC 123' do sign_in @admin @compose_file.destroy! base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-invalid-service-name.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], } differences = { -> { UffizziCore::ComposeFile.main.count } => 0, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response(:unprocessable_entity) end test '#destroy - deletes compose file' do sign_in @admin params = { project_slug: @project.slug } differences = { -> { UffizziCore::ComposeFile.count } => -1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :no_content end test '#destroy - when compose_file not found' do sign_in @admin @compose_file.destroy! params = { project_slug: @project.slug } delete :destroy, params: params, format: :json error_message = JSON.parse(response.body, symbolize_names: true)[:errors][:title].first assert_response :not_found assert_equal('UffizziCore::ComposeFile Not Found', error_message) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments/activity_items_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::Deployments::ActivityItemsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) @deployment = create(:deployment, project: @project) sign_in @user end test '#index' do container = create(:container, :with_public_port, deployment: @deployment) create(:activity_item, :with_building_event, tag: container.tag, container: container, deployment: @deployment) params = { project_slug: @project.slug, deployment_id: @deployment.id, } get :index, params: params, format: :json assert_response :success end test '#index - with failed deployment' do @deployment.fail! container = create(:container, :with_public_port, deployment: @deployment) create(:activity_item, :with_building_event, tag: container.tag, container: container, deployment: @deployment) params = { project_slug: @project.slug, deployment_id: @deployment.id, } get :index, params: params, format: :json assert_response :unprocessable_entity response_body = JSON.parse(response.body) assert_equal("Preview with ID deployment-#{@deployment.id} failed", response_body['errors']['title'].first) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments/containers/logs_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::Deployments::Containers::LogsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) @deployment = create(:deployment, project: @project) @container = create(:container, deployment: @deployment) @pod_name = UffizziCore::ContainerService.pod_name(@container) @limit = 30 @previous = false sign_in @user end test '#index' do controller_response = json_fixture('files/controller/logs.json') pod_name = UffizziCore::ContainerService.pod_name(@container) stubbed_request = stub_container_log_request(@deployment.id, pod_name, @limit, @previous, controller_response) params = { project_slug: @project.slug, deployment_id: @deployment.id, container_name: @container.service_name, limit: @limit, previous: @previous, } get :index, params: params, format: :json assert_requested stubbed_request assert_response :success data = JSON.parse(response.body) expected_result = { 'logs' => [ { 'timestamp' => '2022-11-14 11:46:55.474 UTC', 'payload' => '/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration', }, ], } assert_equal(expected_result, data) end test '#index with empty logs info' do controller_response = UffizziCore::Converters.deep_lower_camelize_keys( { logs: [], }, ) stubbed_request = stub_container_log_request(@deployment.id, @pod_name, @limit, @previous, controller_response) params = { project_slug: @project.slug, deployment_id: @deployment.id, container_name: @container.service_name, limit: @limit, previous: @previous, } get :index, params: params, format: :json assert_requested stubbed_request assert_response :success end test '#index with controller error' do controller_response = UffizziCore::Converters.deep_lower_camelize_keys( { logs: [], }, ) stubbed_request = stub_container_log_request(@deployment.id, @pod_name, @limit, @previous, controller_response) params = { project_slug: @project.slug, deployment_id: @deployment.id, container_name: @container.service_name, limit: @limit, previous: @previous, } get :index, params: params, format: :json assert_requested stubbed_request assert_response :success end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments/containers_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::Deployments::ContainersControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) sign_in @user @account = @user.personal_account @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) @deployment = create(:deployment, project: @project) end test '#index' do repo = create(:repo, :docker_hub, project: @project) create( :container, deployment: @deployment, repo: repo, image: 'uffizzitest/webhooks-test-app', tag: 'latest', secret_variables: [{ 'name' => 'test', 'value' => 'test' }], ) params = { project_slug: @project.slug, deployment_id: @deployment.id } get :index, params: params, format: :json assert_response :success end test '#k8s_container_description - with last state' do container = create(:container, deployment: @deployment, controller_name: 'f03d008a48') controller_response = json_fixture('files/controller/deployment_containers_with_error.json') stubbed_request = stub_controller_containers_request(@deployment, controller_response) params = { project_slug: @project.slug, deployment_id: @deployment.id, container_name: container.service_name, } get :k8s_container_description, params: params, format: :json assert_requested stubbed_request assert_response :success data = JSON.parse(response.body) expected_result = { 'last_state' => { 'code' => 'terminated', 'reason' => 'Error', 'exit_code' => 127, 'started_at' => '2022-12-05T18:11:46Z', 'finished_at' => '2022-12-05T18:11:46Z', }, } assert_equal(expected_result, data) end test '#k8s_container_description - with empty last state' do container = create(:container, deployment: @deployment, controller_name: 'f03d008a48') controller_response = json_fixture('files/controller/deployment_containers.json') stubbed_request = stub_controller_containers_request(@deployment, controller_response) params = { project_slug: @project.slug, deployment_id: @deployment.id, container_name: container.service_name, } get :k8s_container_description, params: params, format: :json assert_requested stubbed_request assert_response :success data = JSON.parse(response.body) expected_result = { 'last_state' => {} } assert_equal(expected_result, data) end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments/events_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::Deployments::EventsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) @deployment = create(:deployment, project: @project) sign_in @user end test '#index' do first_timestamp = generate(:string) last_timestamp = generate(:string) reason = generate(:string) message = generate(:string) events = UffizziCore::Converters.deep_lower_camelize_keys( { items: [ { involved_object: { kind: 'Pod' }, first_timestamp: first_timestamp, last_timestamp: last_timestamp, reason: reason, message: message, }, ], }, ) stubbed_controller_get_deployment_events = stub_controller_get_deployment_events(@deployment, events) params = { project_slug: @project.slug, deployment_id: @deployment.id, } get :index, params: params, format: :json assert_requested(stubbed_controller_get_deployment_events) assert_response :success collected_result = { events: [ { first_timestamp: first_timestamp, last_timestamp: last_timestamp, reason: reason, message: message, }, ], }.to_json assert_equal(collected_result, response.body) end test '#index with empty logs info' do events = UffizziCore::Converters.deep_lower_camelize_keys( { items: [], }, ) stubbed_controller_get_deployment_events = stub_controller_get_deployment_events(@deployment, events) params = { project_slug: @project.slug, deployment_id: @deployment.id, } get :index, params: params, format: :json assert_requested(stubbed_controller_get_deployment_events) assert_response :success end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments_controller/create_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @project = create(:project, :with_members, account: @admin.personal_account, members: [@admin]) @deployment = create(:deployment, project: @project, state: UffizziCore::Deployment::STATE_ACTIVE) @metadata = { 'labels' => { 'github' => { 'repository' => 'feature/#24_My_awesome_feature', 'pull_request' => { 'number' => '24', }, }, }, } @deployment.update!(subdomain: UffizziCore::Deployment::DomainService.build_subdomain(@deployment)) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } @template = create(:template, :compose_file_source, compose_file: @compose_file, project: @project, added_by: @admin, payload: template_payload) sign_in @admin end test '#create - from the existing compose file' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! file_content = File.read('test/fixtures/files/test-compose-success.yml') encoded_content = Base64.encode64(file_content) compose_file = create(:compose_file, project: @project, added_by: @admin, content: encoded_content) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, :with_named_volume, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", healthcheck: { test: ['CMD', 'curl', '-f', 'https://localhost'] }, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) stub_dockerhub_repository('library', 'redis') params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: {} } differences = { -> { UffizziCore::Deployment.active.count } => 1, -> { UffizziCore::Repo::DockerHub.count } => 1, } assert_difference differences do post :create, params: params, format: :json end assert_response :success subdomains = UffizziCore::Deployment.active.map(&:subdomain) assert_nil(subdomains.detect { |s| s.include?('_') }) Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from the existing compose file with metadata' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! file_content = File.read('test/fixtures/files/test-compose-success.yml') encoded_content = Base64.encode64(file_content) compose_file = create(:compose_file, project: @project, added_by: @admin, content: encoded_content) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, :with_named_volume, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", healthcheck: { test: ['CMD', 'curl', '-f', 'https://localhost'] }, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) stub_dockerhub_repository('library', 'redis') params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: @metadata } post :create, params: params, format: :json assert_response :success deployment = compose_file.deployments.first assert_equal(@metadata, deployment.metadata) assert_equal(deployment.subdomain.downcase, deployment.subdomain) assert_equal(deployment.creation_source, UffizziCore::Deployment.creation_source.compose_file_manual) Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from the existing compose file with metadata when an active deployment exists - should return an eror' do create(:deployment, metadata: @metadata, project: @project) params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: @metadata } post :create, params: params, format: :json assert_response :unprocessable_entity end test '#create - from the existing compose file with github_actions creation source (for self-hosted version)' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! file_content = File.read('test/fixtures/files/test-compose-success.yml') encoded_content = Base64.encode64(file_content) compose_file = create(:compose_file, project: @project, added_by: @admin, content: encoded_content) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) stub_dockerhub_repository('library', 'redis') creation_source = UffizziCore::Deployment.creation_source.github_actions params = { project_slug: @project.slug, compose_file: {}, dependencies: [], creation_source: creation_source } post :create, params: params, format: :json assert_response :success deployment = compose_file.deployments.first assert_equal(creation_source, deployment.creation_source) Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from the existing compose file when credentials are removed' do create_namespace_request = stub_create_namespace_request file_content = File.read('test/fixtures/files/uffizzi-compose-vote-app-docker.yml') encoded_content = Base64.encode64(file_content) compose_file = create(:compose_file, project: @project, added_by: @admin, content: encoded_content) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) stub_dockerhub_private_repository('library', 'redis') params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: {} } differences = { -> { UffizziCore::Deployment.active.count } => 0, -> { UffizziCore::Repo::DockerHub.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :unprocessable_entity assert_not_requested(create_namespace_request) end test '#create - from the existing compose file - when the file is invalid' do create(:credential, :github_container_registry, account: @admin.personal_account) compose_file = create(:compose_file, :invalid_file, project: @project, added_by: @admin) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, full_image_name: "#{image}:#{target_branch}", receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: {} } differences = { -> { UffizziCore::Deployment.active.count } => 0, -> { UffizziCore::Repo::DockerHub.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :unprocessable_entity end test '#create - when compose file does not exist and no params given' do params = { project_slug: @project.slug, compose_file: {}, dependencies: [], metadata: {}, } differences = { -> { UffizziCore::ComposeFile.temporary.count } => 0, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :not_found end test '#create - when compose file does not exist and use docker registry without auth' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! stub_docker_registry_manifests('https://ttl.sh', 'abc', '1h') compose_file_content = File.read('test/fixtures/files/uffizzi-compose-docker-registry-anonymous.yml') encoded_compose_file_content = Base64.encode64(compose_file_content) compose_file = { source: '/gem/tmp/docker-compose.uffizzi.yaml', path: '/gem/tmp/docker-compose.uffizzi.yaml', content: encoded_compose_file_content, } params = { project_slug: @project.slug, compose_file: compose_file, dependencies: [], metadata: {}, } differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } assert_difference differences do post :create, params: params, format: :json end Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - with content when compose file does not exist' do deployment_data = json_fixture('files/controller/deployments.json') stubbed_namespace_request = stub_controller_get_namespace_request_any(deployment_data) stubbed_controller_create_name_request = stub_create_namespace_request stub_controller_apply_credential compose_file_name = 'test-compose-full.yml' file_content = File.read("test/fixtures/files/#{compose_file_name}") encoded_content = Base64.encode64(file_content) stub_dockerhub_repository_any # rubocop:disable Layout/LineLength params = { project_slug: @project.slug, compose_file: { source: "/gem/tmp/#{compose_file_name}", path: "/gem/tmp/#{compose_file_name}", content: encoded_content, }, dependencies: [ { content: "ZGF0YQ1==\n", is_file: false, path: '/gem/tmp/some_app_dir', source: './some_app_dir', use_kind: 'volume', }, { content: "ZGF0YQ2==\n", is_file: true, path: '/gem/tmp/files/some_app_file', source: './files/some_app_file', use_kind: 'volume', }, { content: "ZGF0YQ3==\n", is_file: false, path: '/gem/tmp/some_db_dir', source: './some_db_dir', use_kind: 'volume', }, { content: "ZGF0YQ4==\n", is_file: true, path: '/gem/tmp/some_db_file', source: './some_db_file', use_kind: 'volume', }, { content: "ZGF0YQ==\n", is_file: false, path: '/gem/tmp', source: './', use_kind: 'volume', }, { content: "S0VZPXZhbHVl\n", path: 'env_files/env_file.env', source: 'env_files/env_file.env', use_kind: 'config_map', }, { content: "UE9TVEdSRVNfVVNFUj1wb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRD1wb3N0\nZ3Jlcw==\n", path: 'local.env', source: 'local.env', use_kind: 'config_map', }, { content: "c2VydmVyIHsgbGlzdGVuICAgICAgIDg4ODg7IHNlcnZlcl9uYW1lICBsb2Nh\nbGhvc3Q7IGxvY2F0aW9uIC8geyBwcm94eV9wYXNzICAgICAgaHR0cDovLzEy\nNy4wLjAuMTo4MDg4LzsgfSBsb2NhdGlvbiAvdm90ZS8geyBwcm94eV9wYXNz\nICAgICAgaHR0cDovLzEyNy4wLjAuMTo4ODg4LzsgfSB9\n", path: 'config_files/config_file.conf', source: 'config_files/config_file.conf', use_kind: 'config_map', }, { content: "c2VydmVyIHsgbGlzdGVuICAgICAgIDgwODA7IHNlcnZlcl9uYW1lICBsb2Nh\nbGhvc3Q7IGxvY2F0aW9uIC8geyBwcm94eV9wYXNzICAgICAgaHR0cDovLzEy\nNy4wLjAuMTo4MDg4LzsgfSBsb2NhdGlvbiAvdm90ZS8geyBwcm94eV9wYXNz\nICAgICAgaHR0cDovLzEyNy4wLjAuMTo4ODg4LzsgfSB9\n", path: 'app.conf', source: 'app.conf', use_kind: 'config_map', }, ], metadata: {}, } # rubocop:enable Layout/LineLength differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Deployment.count } => 1, -> { UffizziCore::Container.count } => 3, -> { UffizziCore::HostVolumeFile.count } => 5, -> { UffizziCore::ConfigFile.count } => 1, -> { UffizziCore::Repo.count } => 3, } assert_difference differences do post :create, params: params, format: :json end assert_response :success assert_requested(stubbed_controller_create_name_request) assert_requested(stubbed_namespace_request) default_container_attributes = { image: nil, tag: nil, service_name: nil, variables: [], public: false, port: nil, state: 'active', continuously_deploy: 'enabled', kind: 'user', target_port: nil, controller_name: nil, receive_incoming_requests: false, memory_request: 125 / UffizziCore::Container::REQUEST_MEMORY_RATIO, memory_limit: 125, secret_variables: [], entrypoint: nil, command: nil, healthcheck: {}, volumes: [], additional_subdomains: [], source: nil, } app_container_attributes = { image: 'uffizzicloud/app', tag: 'latest', service_name: 'app', volumes: [ { type: 'host', source: './some_app_dir', target: '/var/app/some_dir', read_only: false, }, { type: 'host', source: './files/some_app_file', target: '/var/app/some_app_files', read_only: false, }, { type: 'host', source: './', target: '/var/entire_app', read_only: false, }, { type: 'named', source: 'app_share', target: '/some_app_share', read_only: true, }, { type: 'anonymous', source: '/some_anonymous_dir', target: nil, read_only: false, }, ], variables: [ { name: 'POSTGRES_USER', value: 'postgres POSTGRES_PASSWORD=postgres', }, { name: 'KEY', value: 'value', }, ], } db_container_attributes = { image: 'library/postgres', tag: 'latest', service_name: 'db', volumes: [ { type: 'host', source: './some_app_dir', target: '/var/db/some_dir_2', read_only: false, }, { type: 'host', source: './some_db_dir', target: '/var/db/some_dir_3', read_only: false, }, { type: 'host', source: './some_db_file', target: '/var/db/some_db_files', read_only: false, }, { type: 'named', source: 'db_share', target: '/some_db_share', read_only: true, }, ], } nginx_container_attributes = { image: 'library/nginx', tag: '1.32', service_name: 'nginx', port: 80, target_port: 80, public: true, receive_incoming_requests: true, } expected_app_container_attributes = default_container_attributes.merge(app_container_attributes) expected_db_container_attributes = default_container_attributes.merge(db_container_attributes) expected_nginx_container_attributes = default_container_attributes.merge(nginx_container_attributes) exclude_params = [:state, :source, :kind, :target_port, :controller_name] expected_template_app_container_attributes = default_container_attributes.merge(app_container_attributes).without(exclude_params) expected_template_db_container_attributes = default_container_attributes.merge(db_container_attributes).without(exclude_params) expected_template_nginx_container_attributes = default_container_attributes.merge(nginx_container_attributes).without(exclude_params) container_keys = default_container_attributes.keys deployment = UffizziCore::Deployment.last actual_containers_attributes = deployment.containers.map { |c| c.attributes.deep_symbolize_keys.slice(*container_keys) } actual_template_containers_attributes = deployment.compose_file.template.payload .deep_symbolize_keys[:containers_attributes] .map { |c| c.slice(*container_keys) } actual_app_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_db_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_nginx_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'nginx' } actual_template_app_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_template_db_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_template_nginx_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'nginx' } assert_equal expected_app_container_attributes, actual_app_container_attributes assert_equal expected_template_app_container_attributes, actual_template_app_container_attributes assert_equal expected_db_container_attributes, actual_db_container_attributes assert_equal expected_template_db_container_attributes, actual_template_db_container_attributes assert_equal expected_nginx_container_attributes, actual_nginx_container_attributes assert_equal expected_template_nginx_container_attributes, actual_template_nginx_container_attributes actual_host_volume_file_paths = UffizziCore::HostVolumeFile.pluck(:path) expected_host_volume_file_paths = params[:dependencies].select { |d| d[:use_kind] == 'volume' }.pluck(:path) assert_equal expected_host_volume_file_paths.sort, actual_host_volume_file_paths.sort actual_host_volume_file_sources = UffizziCore::HostVolumeFile.pluck(:source) expected_host_volume_file_sources = params[:dependencies] .select { |d| d[:use_kind] == 'volume' } .pluck(:source) .map { |s| "#{compose_file_name}/#{s}" } assert_equal expected_host_volume_file_sources.sort, actual_host_volume_file_sources.sort actual_host_volume_file_count_which_is_file = UffizziCore::HostVolumeFile.where(is_file: true).count assert_equal(2, actual_host_volume_file_count_which_is_file) end test '#create - file with local host volume when same host volume file exists' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! stub_dockerhub_login stub_dockerhub_repository('library', 'nginx') create(:credential, :docker_hub, account: @admin.personal_account) compose_file_content = File.read('test/fixtures/files/uffizzi-compose-with-host-volumes.yml') encoded_compose_file_content = Base64.encode64(compose_file_content) host_volume_content = Base64.encode64(File.binread('test/fixtures/files/file.tar.gz')) compose_file_params = { source: '/gem/tmp/dc.uffizzi-nginx.yaml', path: '/gem/tmp/dc.uffizzi-nginx.yaml', content: encoded_compose_file_content, } dependency = { path: '/gem/tmp/share_dir', source: './share_dir', content: host_volume_content, use_kind: UffizziCore::ComposeFile::DependenciesService::DEPENDENCY_VOLUME_USE_KIND, is_file: false, } compose_file = create(:compose_file, project: @project, added_by: @admin, content: encoded_compose_file_content) create(:host_volume_file, path: dependency[:path], source: 'dc.uffizzi-nginx.yaml/./share_dir', payload: Base64.decode64(dependency[:content]), is_file: false, project: @project, compose_file: compose_file) params = { project_slug: @project.slug, compose_file: compose_file_params, dependencies: [dependency] } differences = { -> { UffizziCore::Container.count } => 1, -> { UffizziCore::ContainerHostVolumeFile.count } => 1, -> { UffizziCore::HostVolumeFile.count } => 0, } assert_difference differences do post :create, params: params, format: :json end assert_response :success Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from amazon image with credentials' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! registry_url = 'https://323707565364.dkr.ecr.us-east-1.amazonaws.com' stub_docker_registry_manifests(registry_url, 'test-compose', 'latest') sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :amazon, :active, account: @account, registry_url: registry_url) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-amazon.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], metadata: {}, } assert_difference differences do post :create, params: params, format: :json end assert_response :success Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - compose file with jfrog docker registry with auth' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! stub_docker_registry_manifests('https://elnealo.jfrog.io', 'uffizzi-test-docker/webhook-test-app', 'latest') compose_file_content = File.read('test/fixtures/files/test-compose-success-jfrog.yml') encoded_compose_file_content = Base64.encode64(compose_file_content) compose_file = { source: '/gem/tmp/docker-compose.uffizzi.yaml', path: '/gem/tmp/docker-compose.uffizzi.yaml', content: encoded_compose_file_content, } params = { project_slug: @project.slug, compose_file: compose_file, dependencies: [], metadata: {}, } differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } assert_difference differences do post :create, params: params, format: :json end Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from azure image with credentials' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! registry_url = 'account.azurecr.io/nginx:latest' stub_docker_registry_manifests(registry_url, 'test-compose', 'latest') sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :azure, :active, account: @account, registry_url: registry_url) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-azure.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], metadata: {}, } assert_difference differences do post :create, params: params, format: :json end assert_response :success Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from google(gcr) image with credentials' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! registry_url = 'gcr.io/project1/test-compose:latest' stub_docker_registry_manifests(registry_url, 'test-compose', 'latest') sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :google, :active, account: @account, registry_url: registry_url) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-google.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], metadata: {}, } assert_difference differences do post :create, params: params, format: :json end assert_response :success Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from ghcr image with credentials' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! registry_url = 'ghcr.io/project1/test-compose:latest' stub_docker_registry_manifests(registry_url, 'test-compose', 'latest') sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :github_container_registry, :active, account: @account, registry_url: registry_url) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-ghcr.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], metadata: {}, } assert_difference differences do post :create, params: params, format: :json end assert_response :success Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#create - from dockerhub image with credentials' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! registry_url = 'project1/test-compose:latest' stub_docker_registry_manifests(registry_url, 'test-compose', 'latest') stubbed_dockerhub_login = stub_dockerhub_login stub_dockerhub_repository('project1', 'test-compose') sign_in @admin project = create(:project, :with_members, account: @account, members: [@admin]) create(:credential, :docker_hub, :active, account: @account, registry_url: registry_url) base_attributes = attributes_for(:compose_file).slice(:source, :path) file_content = File.read('test/fixtures/files/uffizzi-compose-dockerhub.yml') encoded_content = Base64.encode64(file_content) compose_file_attributes = base_attributes.merge(content: encoded_content, repository_id: nil) differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { UffizziCore::Container.count } => 1, } params = { project_slug: project.slug, compose_file: compose_file_attributes, dependencies: [], metadata: {}, } assert_difference differences do post :create, params: params, format: :json end assert_response :success assert_requested stubbed_dockerhub_login, times: 2 Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments_controller/deploy_containers_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @project = create(:project, :with_members, account: @admin.personal_account, members: [@admin]) @deployment = create(:deployment, project: @project, state: UffizziCore::Deployment::STATE_ACTIVE) @deployment.update!(subdomain: UffizziCore::Deployment::DomainService.build_subdomain(@deployment)) @credential = create(:credential, :github_container_registry, account: @account) sign_in @admin end test '#deploy_containers' do Sidekiq::Worker.clear_all Sidekiq::Testing.fake! stub_dockerhub_login repo1 = create(:repo, :docker_hub, project: @project) repo2 = create(:repo, :docker_hub, project: @project) create(:container, :with_public_port, deployment: @deployment, repo: repo1) create(:container, :with_public_port, deployment: @deployment, repo: repo2) params = { project_slug: @project.slug, id: @deployment.id } post :deploy_containers, params: params, format: :json assert_response :success assert { UffizziCore::Deployment::DeployContainersJob.jobs.size == 1 } Sidekiq::Worker.clear_all Sidekiq::Testing.inline! end test '#deploy_containers if deployment has not created yet' do UffizziCore::ControllerService.expects(:namespace_exists?).returns(false) params = { project_slug: @project.slug, id: @deployment.id } assert_raises UffizziCore::DeploymentNotFoundError do post :deploy_containers, params: params, format: :json end end test '#deploy_containers create a new docker hub activity item' do UffizziCore::ControllerService.expects(:namespace_exists?).returns(true) webhooks_data = json_fixture('files/dockerhub/webhooks/push/event_data.json') digest_data = json_fixture('files/dockerhub/digest.json') deployment_containers_data = json_fixture('files/controller/deployment_containers.json') deployment_data = json_fixture('files/controller/deployments.json') stubbed_namespace_request = stub_controller_get_namespace_request(@deployment, deployment_data) stubbed_containers_request = stub_controller_containers_request(@deployment, deployment_containers_data) stubbed_deploy_containers_request = stub_deploy_containers_request(@deployment) stubbed_dockerhub_login = stub_dockerhub_login params = { project_slug: @project.slug, id: @deployment.id } namespace, name = webhooks_data[:repository][:repo_name].split('/') repo = create(:repo, :docker_hub, project: @project, namespace: namespace, name: name) container = create( :container, :with_public_port, :continuously_deploy_enabled, deployment: @deployment, repo: repo, image: webhooks_data[:repository][:repo_name], tag: webhooks_data[:push_data][:tag], controller_name: deployment_containers_data.first[:spec][:containers].first[:controllerName], ) create(:credential, :docker_hub, account: @account) stubbed_digest_auth = stub_dockerhub_auth_for_digest(container.image) stubbed_digest = stub_dockerhub_get_digest(container.image, container.tag, digest_data) post :deploy_containers, params: params, format: :json assert_requested stubbed_digest assert_requested stubbed_digest_auth assert_requested stubbed_dockerhub_login assert_requested stubbed_deploy_containers_request assert_requested stubbed_containers_request assert_requested stubbed_namespace_request assert { UffizziCore::Deployment::DeployContainersJob.jobs.empty? } assert { UffizziCore::ActivityItem::Docker.count == 1 } end test '#deploy_containers skip activity item creation if existing has not finished yet' do UffizziCore::ControllerService.expects(:namespace_exists?).returns(true) webhooks_data = json_fixture('files/dockerhub/webhooks/push/event_data.json') digest_data = json_fixture('files/dockerhub/digest.json') deployment_containers_data = json_fixture('files/controller/deployment_containers.json') deployment_data = json_fixture('files/controller/deployments.json') namespace, name = webhooks_data[:repository][:repo_name].split('/') repo = create(:repo, :docker_hub, project: @project, namespace: namespace, name: name) container = create( :container, :with_public_port, :continuously_deploy_enabled, deployment: @deployment, repo: repo, image: webhooks_data[:repository][:repo_name], tag: webhooks_data[:push_data][:tag], controller_name: deployment_containers_data.first[:spec][:containers].first[:controllerName], ) create(:activity_item, :docker, :with_building_event, namespace: repo.namespace, name: repo.name, tag: container.tag, container: container, deployment: @deployment) stubbed_namespace_request = stub_controller_get_namespace_request(@deployment, deployment_data) stubbed_containers_request = stub_controller_containers_request(@deployment, deployment_containers_data) stubbed_deploy_containers_request = stub_deploy_containers_request(@deployment) stubbed_dockerhub_login = stub_dockerhub_login params = { project_slug: @project.slug, id: @deployment.id } create(:credential, :docker_hub, account: @account) stubbed_digest_auth = stub_dockerhub_auth_for_digest(container.image) stubbed_digest = stub_dockerhub_get_digest(container.image, container.tag, digest_data) post :deploy_containers, params: params, format: :json assert_requested stubbed_digest assert_requested stubbed_digest_auth assert_requested stubbed_dockerhub_login assert_requested stubbed_deploy_containers_request assert_requested stubbed_containers_request assert_requested stubbed_namespace_request assert { UffizziCore::Deployment::DeployContainersJob.jobs.empty? } assert { UffizziCore::ActivityItem::Docker.count == 1 } end test '#deploy_containers create a new activity item creation if existing has finished' do UffizziCore::ControllerService.expects(:namespace_exists?).returns(true) webhooks_data = json_fixture('files/dockerhub/webhooks/push/event_data.json') digest_data = json_fixture('files/dockerhub/digest.json') deployment_containers_data = json_fixture('files/controller/deployment_containers.json') deployment_data = json_fixture('files/controller/deployments.json') namespace, name = webhooks_data[:repository][:repo_name].split('/') repo = create(:repo, :docker_hub, project: @project, namespace: namespace, name: name) container = create( :container, :with_public_port, :continuously_deploy_enabled, deployment: @deployment, repo: repo, image: webhooks_data[:repository][:repo_name], tag: webhooks_data[:push_data][:tag], controller_name: deployment_containers_data.first[:spec][:containers].first[:controllerName], ) create(:activity_item, :docker, :with_deployed_event, namespace: repo.namespace, name: repo.name, tag: container.tag, container: container, deployment: @deployment) stubbed_namespace_request = stub_controller_get_namespace_request(@deployment, deployment_data) stubbed_containers_request = stub_controller_containers_request(@deployment, deployment_containers_data) stubbed_deploy_containers_request = stub_deploy_containers_request(@deployment) stubbed_dockerhub_login = stub_dockerhub_login params = { project_slug: @project.slug, id: @deployment.id } create(:credential, :docker_hub, account: @account) stubbed_digest_auth = stub_dockerhub_auth_for_digest(container.image) stubbed_digest = stub_dockerhub_get_digest(container.image, container.tag, digest_data) post :deploy_containers, params: params, format: :json assert_requested stubbed_digest assert_requested stubbed_digest_auth assert_requested stubbed_dockerhub_login assert_requested stubbed_deploy_containers_request assert_requested stubbed_containers_request assert_requested stubbed_namespace_request assert { UffizziCore::Deployment::DeployContainersJob.jobs.empty? } assert { UffizziCore::ActivityItem::Docker.count == 2 } end test '#deploy_containers create a new activity item creation without credential' do UffizziCore::ControllerService.expects(:namespace_exists?).returns(true) webhooks_data = json_fixture('files/dockerhub/webhooks/push/event_data.json') deployment_containers_data = json_fixture('files/controller/deployment_containers.json') deployment_data = json_fixture('files/controller/deployments.json') namespace, name = webhooks_data[:repository][:repo_name].split('/') repo = create(:repo, :docker_hub, project: @project, namespace: namespace, name: name) container = create( :container, :continuously_deploy_enabled, :with_public_port, :with_named_volume, deployment: @deployment, repo: repo, image: webhooks_data[:repository][:repo_name], tag: webhooks_data[:push_data][:tag], controller_name: deployment_containers_data.first[:spec][:containers].first[:controllerName], ) create(:activity_item, :docker, :with_deployed_event, namespace: repo.namespace, name: repo.name, tag: container.tag, container: container, deployment: @deployment) stubbed_namespace_request = stub_controller_get_namespace_request(@deployment, deployment_data) stubbed_containers_request = stub_controller_containers_request(@deployment, deployment_containers_data) stubbed_deploy_containers_request = stub_deploy_containers_request(@deployment) params = { project_slug: @project.slug, id: @deployment.id } post :deploy_containers, params: params, format: :json assert { UffizziCore::ActivityItem.last.digest.nil? } assert_requested stubbed_deploy_containers_request assert_requested stubbed_containers_request assert_requested stubbed_namespace_request assert { UffizziCore::Deployment::DeployContainersJob.jobs.empty? } assert { UffizziCore::ActivityItem::Docker.count == 2 } end test '#deploy_containers create a new activity items' do metadata = { 'labels' => { 'github' => { 'repository' => 'feature/#24_my_awesome_feature', 'event' => { 'number' => '24', }, }, }, } @deployment.update!(metadata: metadata) UffizziCore::ControllerService.expects(:namespace_exists?).at_least(1).returns(true) digest_data = json_fixture('files/dockerhub/digest.json') deployment_containers_data = json_fixture('files/controller/deployment_containers.json') deployment_data = json_fixture('files/controller/deployments.json') stubbed_namespace_request = stub_controller_get_namespace_request(@deployment, deployment_data) stubbed_containers_request = stub_controller_containers_request(@deployment, deployment_containers_data) stubbed_dockerhub_login = stub_dockerhub_login compose_file_name = 'test-compose-full.yml' file_content = File.read("test/fixtures/files/#{compose_file_name}") encoded_content = Base64.encode64(file_content) compose_file = create(:compose_file, content: encoded_content, repository_id: nil, branch: nil, project: @project) @deployment.update!(compose_file: compose_file) controller_name = deployment_containers_data.first[:spec][:containers].first[:controllerName] app_name = 'app' app_namespace = 'uffizzicloud' nginx_name = 'nginx' nginx_namespace = 'library' host_volume_file_attrs_app_dir = { type: 'host', source: './some_app_dir', target: '/var/app/some_dir', read_only: false, } host_volume_file_attrs_app_dir2 = { type: 'host', source: './', target: '/var/entire_app', read_only: false, } host_volume_file_attrs_app_file = { type: 'host', source: './files/some_app_file', target: '/var/app/some_app_files', read_only: false, } container_app_attrs = { controller_name: controller_name, service_name: app_name, image: "#{app_namespace}/#{app_name}", tag: 'latest', full_image_name: "#{app_namespace}/#{app_name}:latest", volumes: [ host_volume_file_attrs_app_dir, host_volume_file_attrs_app_file, host_volume_file_attrs_app_dir2, { type: 'named', source: 'app_share', target: '/some_app_share', read_only: true, }, { type: 'anonymous', source: '/some_anonymous_dir', target: nil, read_only: false, }, ], } container_nginx_attrs = { controller_name: controller_name, service_name: nginx_name, image: "#{nginx_namespace}/#{nginx_name}", tag: '1.32', full_image_name: "#{nginx_namespace}/#{nginx_name}:1.32", public: true, port: 80, target_port: 80, } host_volume_file_app_dir_params = { path: host_volume_file_attrs_app_dir[:source], source: "#{compose_file_name}/#{host_volume_file_attrs_app_dir[:source]}", payload: 'some_app_dir_data', is_file: false, project: @project, compose_file: compose_file, } host_volume_file_app_dir2_params = { path: host_volume_file_attrs_app_dir2[:source], source: "#{compose_file_name}/#{host_volume_file_attrs_app_dir2[:source]}", payload: 'some_app_dir_data', is_file: false, project: @project, compose_file: compose_file, } host_volume_file_app_file_params = { path: host_volume_file_attrs_app_file[:source], source: "#{compose_file_name}/#{host_volume_file_attrs_app_file[:source]}", payload: 'some_app_file_data', is_file: true, project: @project, compose_file: compose_file, } docker_hub_credential = create(:credential, :docker_hub, account: @account) app_repo = create(:repo, :docker_hub, project: @project, namespace: app_namespace, name: app_name) nginx_repo = create(:repo, :docker_hub, project: @project, namespace: nginx_namespace, name: nginx_name) host_volume_file_app_dir = create(:host_volume_file, **host_volume_file_app_dir_params) host_volume_file_app_dir2 = create(:host_volume_file, **host_volume_file_app_dir2_params) host_volume_file_app_file = create(:host_volume_file, **host_volume_file_app_file_params) app_container = create(:container, :continuously_deploy_enabled, **{ deployment: @deployment, repo: app_repo }.merge(container_app_attrs)) nginx_container = create(:container, :with_public_port, :continuously_deploy_enabled, **{ deployment: @deployment, repo: nginx_repo }.merge(container_nginx_attrs)) container_host_volume_files_app_dir = create(:container_host_volume_file, container: app_container, host_volume_file: host_volume_file_app_dir, source_path: host_volume_file_attrs_app_dir[:source]) container_host_volume_files_app_dir2 = create(:container_host_volume_file, container: app_container, host_volume_file: host_volume_file_app_dir2, source_path: host_volume_file_attrs_app_dir2[:source]) container_host_volume_files_app_file = create(:container_host_volume_file, container: app_container, host_volume_file: host_volume_file_app_file, source_path: host_volume_file_attrs_app_file[:source]) app_config_file = create(:config_file, :compose_file_source, project: @project, payload: 'data', filename: 'config_file.conf') app_container_config_file = create(:container_config_file, container: app_container, config_file: app_config_file) stubbed_digest_auth_app = stub_dockerhub_auth_for_digest(app_container.image) stubbed_digest_auth_nginx = stub_dockerhub_auth_for_digest(nginx_container.image) stubbed_digest_app = stub_dockerhub_get_digest(app_container.image, app_container.tag, digest_data) stubbed_digest_nginx = stub_dockerhub_get_digest(nginx_container.image, nginx_container.tag, digest_data) expected_default_container_params = { secret_variables: [], memory_limit: Settings.compose.default_memory, memory_request: Settings.compose.default_memory, entrypoint: nil, command: nil, port: nil, target_port: nil, public: false, receive_incoming_requests: false, healthcheck: {}, volumes: nil, container_config_files: [], additional_subdomains: [], container_host_volume_files: [], } expected_app_container_attrs = { id: app_container.id, kind: app_container.kind, variables: [ { name: 'UFFIZZI_URL', value: "https://#{@deployment.preview_url}", }, { name: 'UFFIZZI_DOMAIN', value: @deployment.preview_url, }, { name: 'UFFIZZI_PREDICTABLE_URL', value: '', }, ], container_host_volume_files: [ { host_volume_file_id: host_volume_file_app_dir.id, source_path: container_host_volume_files_app_dir.source_path, }, { host_volume_file_id: host_volume_file_app_dir2.id, source_path: container_host_volume_files_app_dir2.source_path, }, { host_volume_file_id: host_volume_file_app_file.id, source_path: container_host_volume_files_app_file.source_path, }, ], container_config_files: [ { mount_path: app_container_config_file.mount_path, config_file: { id: app_config_file.id, filename: app_config_file.filename, kind: app_config_file.kind, payload: app_config_file.payload, }, }, ], } expected_nginx_container_attrs = { id: nginx_container.id, kind: nginx_container.kind, variables: [ { name: 'UFFIZZI_URL', value: "https://#{@deployment.preview_url}", }, { name: 'UFFIZZI_DOMAIN', value: @deployment.preview_url, }, { name: 'PORT', value: '80', }, { name: 'UFFIZZI_PREDICTABLE_URL', value: '', }, ], } expected_app_container = expected_default_container_params .merge(expected_app_container_attrs) .merge(container_app_attrs.except(:image, :tag)) expected_nginx_container = expected_default_container_params .merge(expected_nginx_container_attrs) .merge(container_nginx_attrs.except(:image, :tag)) expected_request_container_to_controller = { containers: [ expected_app_container, expected_nginx_container, ], credentials: [{ id: docker_hub_credential.id }, { id: @credential.id }], deployment_url: @deployment.preview_url, compose_file: { source_kind: 'local' }, host_volume_files: [ { id: host_volume_file_app_dir.id, source: host_volume_file_app_dir_params[:source], path: host_volume_file_app_dir_params[:path], payload: Base64.encode64(host_volume_file_app_dir_params[:payload]), is_file: host_volume_file_app_dir_params[:is_file], }, { id: host_volume_file_app_dir2.id, source: host_volume_file_app_dir2_params[:source], path: host_volume_file_app_dir2_params[:path], payload: Base64.encode64(host_volume_file_app_dir2_params[:payload]), is_file: host_volume_file_app_dir2_params[:is_file], }, { id: host_volume_file_app_file.id, source: host_volume_file_app_file_params[:source], path: host_volume_file_app_file_params[:path], payload: Base64.encode64(host_volume_file_app_file_params[:payload]), is_file: host_volume_file_app_file_params[:is_file], }, ], } expected_request_config_file_to_controller = { config_file: { id: app_config_file.id, filename: app_config_file.filename, kind: app_config_file.kind, payload: app_config_file.payload, }, } stub_apply_config_file_request_with_expected(@deployment, app_config_file, expected_request_config_file_to_controller) stubbed_deploy_containers_request = stub_deploy_containers_request_with_expected(@deployment, expected_request_container_to_controller) params = { project_slug: @project.slug, id: @deployment.id } post :deploy_containers, params: params, format: :json assert_requested stubbed_deploy_containers_request assert_requested stubbed_digest_app assert_requested stubbed_digest_nginx assert_requested stubbed_digest_auth_app assert_requested stubbed_digest_auth_nginx assert_requested stubbed_dockerhub_login, times: 2 assert_requested stubbed_containers_request, times: 2 assert_requested stubbed_namespace_request, times: 2 assert { UffizziCore::Deployment::DeployContainersJob.jobs.empty? } assert { UffizziCore::ActivityItem::Docker.count == 2 } end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments_controller/destroy_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @project = create(:project, :with_members, account: @admin.personal_account, members: [@admin]) @deployment = create(:deployment, project: @project, state: UffizziCore::Deployment::STATE_ACTIVE) @metadata = { 'labels' => { 'github' => { 'repository' => 'feature/#24_my_awesome_feature', 'pull_request' => { 'number' => '24', }, }, }, } @deployment.update!(subdomain: UffizziCore::Deployment::DomainService.build_subdomain(@deployment)) @credential = create(:credential, :github_container_registry, account: @account) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } @template = create(:template, :compose_file_source, compose_file: @compose_file, project: @project, added_by: @admin, payload: template_payload) sign_in @admin end test '#destroy' do Sidekiq::Worker.clear_all Sidekiq::Testing.inline! stubbed_request = stub_delete_namespace_request(@deployment) container = create(:container, :with_public_port, deployment: @deployment) differences = { -> { UffizziCore::Deployment.active.count } => -1, } params = { project_slug: @project.slug, id: @deployment.id, } assert_difference differences do delete :destroy, params: params, format: :json end assert_requested(stubbed_request) assert { container.reload.disabled? } assert_response :success Sidekiq::Worker.clear_all end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/deployments_controller/update_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::DeploymentsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @project = create(:project, :with_members, account: @admin.personal_account, members: [@admin]) @deployment = create(:deployment, project: @project, state: UffizziCore::Deployment::STATE_ACTIVE) @metadata = { 'labels' => { 'github' => { 'repository' => 'feature/#24_my_awesome_feature', 'pull_request' => { 'number' => '24', }, }, }, } @deployment.update!(subdomain: UffizziCore::Deployment::DomainService.build_subdomain(@deployment)) @credential = create(:credential, :github_container_registry, account: @account) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } @template = create(:template, :compose_file_source, compose_file: @compose_file, project: @project, added_by: @admin, payload: template_payload) sign_in @admin end test '#update - update deployment created from main compose file' do file_content = File.read('test/fixtures/files/test-compose-success-without-dependencies.yml') compose_file = create(:compose_file, project: @project, added_by: @admin) create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: @template.payload) @deployment.update!(compose_file: compose_file) encoded_content = Base64.encode64(file_content) compose_file_attributes = attributes_for(:compose_file, :temporary, project: @project, added_by: @admin, content: encoded_content) stub_dockerhub_repository_any params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], id: @deployment[:id], metadata: {}, } differences = { -> { UffizziCore::ComposeFile.temporary.count } => 1, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 1, -> { @deployment.containers.count } => 3, } assert_difference differences do put :update, params: params, format: :json end assert_response :success container_keys = [:image, :tag, :service_name, :port, :public] actual_containers_attributes = UffizziCore::Container.all.map { |c| c.attributes.deep_symbolize_keys.slice(*container_keys) } actual_template_containers_attributes = UffizziCore::Template.last .payload .deep_symbolize_keys[:containers_attributes] .map { |c| c.slice(*container_keys) } actual_app_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_db_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_nginx_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'nginx' } actual_template_app_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_template_db_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_template_nginx_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'nginx' } expected_app_container_attributes = { image: 'uffizzicloud/app', tag: 'latest', service_name: 'app', port: nil, public: false, } expected_db_container_attributes = { image: 'library/postgres', tag: 'latest', service_name: 'db', port: nil, public: false, } expected_nginx_container_attributes = { image: 'library/nginx', tag: '1.32', service_name: 'nginx', port: 80, public: true, } assert_equal expected_app_container_attributes, actual_app_container_attributes assert_equal expected_app_container_attributes, actual_template_app_container_attributes assert_equal expected_db_container_attributes, actual_db_container_attributes assert_equal expected_db_container_attributes, actual_template_db_container_attributes assert_equal expected_nginx_container_attributes, actual_nginx_container_attributes assert_equal expected_nginx_container_attributes, actual_template_nginx_container_attributes end test '#update - update deployment with metadata' do file_content = File.read('test/fixtures/files/test-compose-success-without-dependencies.yml') compose_file = create(:compose_file, project: @project, added_by: @admin) create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: @template.payload) @deployment.update!(compose_file: compose_file) encoded_content = Base64.encode64(file_content) compose_file_attributes = attributes_for(:compose_file, :temporary, project: @project, added_by: @admin, content: encoded_content) 2.times { stub_dockerhub_repository_any } params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [], id: @deployment[:id], metadata: @metadata, } put :update, params: params, format: :json assert_response :success assert_equal(@metadata, @deployment.reload.metadata) end test '#update - update deployment created from temporary compose file' do file_content = File.read('test/fixtures/files/test-compose-full.yml') compose_file = create(:compose_file, :temporary, project: @project, added_by: @admin) create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: @template.payload) @deployment.update!(compose_file: compose_file) encoded_content = Base64.encode64(file_content) compose_file_attributes = attributes_for(:compose_file, :temporary, project: @project, added_by: @admin, content: encoded_content) stub_dockerhub_repository_any # rubocop:disable Layout/LineLength params = { project_slug: @project.slug, compose_file: compose_file_attributes, dependencies: [ { content: "ZGF0YQ1==\n", is_file: false, path: '/gem/tmp/some_app_dir', source: './some_app_dir', use_kind: 'volume', }, { content: "ZGF0YQ2==\n", is_file: true, path: '/gem/tmp/files/some_app_file', source: './files/some_app_file', use_kind: 'volume', }, { content: "ZGF0YQ3==\n", is_file: false, path: '/gem/tmp/some_db_dir', source: './some_db_dir', use_kind: 'volume', }, { content: "ZGF0YQ4==\n", is_file: true, path: '/gem/tmp/some_db_file', source: './some_db_file', use_kind: 'volume', }, { content: "ZGF0YQ==\n", is_file: false, path: '/gem/tmp', source: './', use_kind: 'volume', }, { content: "S0VZPXZhbHVl\n", path: 'env_files/env_file.env', source: 'env_files/env_file.env', use_kind: 'config_map', }, { content: "UE9TVEdSRVNfVVNFUj1wb3N0Z3JlcyBQT1NUR1JFU19QQVNTV09SRD1wb3N0\nZ3Jlcw==\n", path: 'local.env', source: 'local.env', use_kind: 'config_map', }, { content: "c2VydmVyIHsgbGlzdGVuICAgICAgIDg4ODg7IHNlcnZlcl9uYW1lICBsb2Nh\nbGhvc3Q7IGxvY2F0aW9uIC8geyBwcm94eV9wYXNzICAgICAgaHR0cDovLzEy\nNy4wLjAuMTo4MDg4LzsgfSBsb2NhdGlvbiAvdm90ZS8geyBwcm94eV9wYXNz\nICAgICAgaHR0cDovLzEyNy4wLjAuMTo4ODg4LzsgfSB9\n", path: 'config_files/config_file.conf', source: 'config_files/config_file.conf', use_kind: 'config_map', }, { content: "c2VydmVyIHsgbGlzdGVuICAgICAgIDgwODA7IHNlcnZlcl9uYW1lICBsb2Nh\nbGhvc3Q7IGxvY2F0aW9uIC8geyBwcm94eV9wYXNzICAgICAgaHR0cDovLzEy\nNy4wLjAuMTo4MDg4LzsgfSBsb2NhdGlvbiAvdm90ZS8geyBwcm94eV9wYXNz\nICAgICAgaHR0cDovLzEyNy4wLjAuMTo4ODg4LzsgfSB9\n", path: 'app.conf', source: 'app.conf', use_kind: 'config_map', }, ], id: @deployment[:id], metadata: {}, } # rubocop:enable Layout/LineLength differences = { -> { UffizziCore::ComposeFile.temporary.count } => 0, -> { UffizziCore::Template.with_creation_source(UffizziCore::Template.creation_source.compose_file).count } => 0, -> { @deployment.containers.count } => 3, -> { UffizziCore::HostVolumeFile.count } => 5, -> { UffizziCore::ConfigFile.count } => 1, } assert_difference differences do put :update, params: params, format: :json end assert_response :success default_container_attributes = { image: nil, tag: nil, service_name: nil, variables: [], public: false, port: nil, state: 'active', continuously_deploy: 'enabled', kind: 'user', target_port: nil, controller_name: nil, receive_incoming_requests: false, memory_request: 125 / UffizziCore::Container::REQUEST_MEMORY_RATIO, memory_limit: 125, secret_variables: [], entrypoint: nil, command: nil, healthcheck: {}, volumes: [], additional_subdomains: [], source: nil, } app_container_attributes = { image: 'uffizzicloud/app', tag: 'latest', service_name: 'app', volumes: [ { type: 'host', source: './some_app_dir', target: '/var/app/some_dir', read_only: false, }, { type: 'host', source: './files/some_app_file', target: '/var/app/some_app_files', read_only: false, }, { type: 'host', source: './', target: '/var/entire_app', read_only: false, }, { type: 'named', source: 'app_share', target: '/some_app_share', read_only: true, }, { type: 'anonymous', source: '/some_anonymous_dir', target: nil, read_only: false, }, ], variables: [ { name: 'POSTGRES_USER', value: 'postgres POSTGRES_PASSWORD=postgres', }, { name: 'KEY', value: 'value', }, ], } db_container_attributes = { image: 'library/postgres', tag: 'latest', service_name: 'db', volumes: [ { type: 'host', source: './some_app_dir', target: '/var/db/some_dir_2', read_only: false, }, { type: 'host', source: './some_db_dir', target: '/var/db/some_dir_3', read_only: false, }, { type: 'host', source: './some_db_file', target: '/var/db/some_db_files', read_only: false, }, { type: 'named', source: 'db_share', target: '/some_db_share', read_only: true, }, ], } nginx_container_attributes = { image: 'library/nginx', tag: '1.32', service_name: 'nginx', port: 80, target_port: 80, public: true, receive_incoming_requests: true, } expected_app_container_attributes = default_container_attributes.merge(app_container_attributes) expected_db_container_attributes = default_container_attributes.merge(db_container_attributes) expected_nginx_container_attributes = default_container_attributes.merge(nginx_container_attributes) exclude_params = [:state, :source, :kind, :target_port, :controller_name] expected_template_app_container_attributes = default_container_attributes.merge(app_container_attributes).without(exclude_params) expected_template_db_container_attributes = default_container_attributes.merge(db_container_attributes).without(exclude_params) expected_template_nginx_container_attributes = default_container_attributes.merge(nginx_container_attributes).without(exclude_params) container_keys = default_container_attributes.keys deployment = UffizziCore::Deployment.last actual_containers_attributes = deployment.containers.map { |c| c.attributes.deep_symbolize_keys.slice(*container_keys) } actual_template_containers_attributes = deployment.compose_file.template.payload .deep_symbolize_keys[:containers_attributes] .map { |c| c.slice(*container_keys) } actual_app_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_db_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_nginx_container_attributes = actual_containers_attributes.detect { |c| c[:service_name] == 'nginx' } actual_template_app_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'app' } actual_template_db_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'db' } actual_template_nginx_container_attributes = actual_template_containers_attributes.detect { |c| c[:service_name] == 'nginx' } assert_equal expected_app_container_attributes, actual_app_container_attributes assert_equal expected_template_app_container_attributes, actual_template_app_container_attributes assert_equal expected_db_container_attributes, actual_db_container_attributes assert_equal expected_template_db_container_attributes, actual_template_db_container_attributes assert_equal expected_nginx_container_attributes, actual_nginx_container_attributes assert_equal expected_template_nginx_container_attributes, actual_template_nginx_container_attributes end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects/secrets_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::Projects::SecretsControllerTest < ActionController::TestCase setup do @admin = create(:user, :with_personal_account) @account = @admin.personal_account @developer = create(:user, :developer_in_organization, organization: @account) @viewer = create(:user, :viewer_in_organization, organization: @account) secrets = [build(:secret, name: generate(:string), value: generate(:string))] @project = create(:project, :with_members, account: @account, members: [@admin, @developer, @viewer], secrets: secrets) sign_in @admin end test '#index admin gets list of secrets' do params = { project_slug: @project.slug, } get :index, params: params, format: :json assert_response :success end test '#bulk_create admin creates secrets' do new_secrets = [ { name: generate(:string), value: generate(:string) }, { name: generate(:string), value: generate(:string) }, ] params = { project_slug: @project.slug, secrets: new_secrets, } differences = { -> { UffizziCore::Project.find(@project.id).secrets.count } => new_secrets.count, } assert_difference differences do post :bulk_create, params: params, format: :json end assert_response :success end test '#bulk_create check if a secret has already been added' do new_secrets = [ { name: @project.secrets.first['name'], value: generate(:string) }, ] params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :unprocessable_entity response_body = JSON.parse(response.body) assert { response_body['errors']['secrets'].present? } end test '#bulk_create check if a secret name too long' do length = UffizziCore::Api::Cli::V1::Secret::BulkAssignForm::MAX_SECRET_KEY_LENGTH + 1 long_name = SecureRandom.alphanumeric(length) new_secrets = [ { name: long_name, value: generate(:string) }, ] params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :unprocessable_entity response_body = JSON.parse(response.body) assert { response_body['errors']['secrets'].present? } end test '#bulk_create check update secret in a compose' do new_secrets = [ { name: generate(:string), value: generate(:string) }, ] compose_file = create(:compose_file, project: @project, added_by: @admin) container_attributes = attributes_for( :container, :with_public_port, receive_incoming_requests: true, secret_variables: [{ name: new_secrets.first[:name], value: '' }], ) template_payload = { containers_attributes: [container_attributes] } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :success compose_file.reload @project.reload new_project_secret = @project.secrets.detect { |project_secret| project_secret['name'] == new_secrets.first[:name] } container = compose_file.template.payload['containers_attributes'].first compose_secret = container['secret_variables'].detect { |container_secret| container_secret['name'] == new_secrets.first[:name] } assert_equal(new_project_secret['value'], compose_secret['value']) end test '#bulk_create check update secret in a compose if it has invalid secrets' do new_secrets = [ { name: generate(:string), value: generate(:string) }, ] compose_additional_secrets = [ { name: generate(:string), value: generate(:string) }, ] compose_payload = { errors: { secret_variables: [generate(:string)] } } compose_file = create(:compose_file, :invalid_file, project: @project, added_by: @admin, payload: compose_payload) container_attributes = attributes_for( :container, :with_public_port, receive_incoming_requests: true, secret_variables: [{ name: new_secrets.first[:name], value: '' }] + compose_additional_secrets, ) template_payload = { containers_attributes: [container_attributes] } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :success compose_file.reload @project.reload new_project_secret = @project.secrets.detect { |project_secret| project_secret['name'] == new_secrets.first[:name] } container = compose_file.template.payload['containers_attributes'].first compose_secret = container['secret_variables'].detect { |container_secret| container_secret['name'] == new_secrets.first[:name] } assert_equal(new_project_secret['value'], compose_secret['value']) assert(compose_file.invalid_file?) refute_empty(compose_file.payload['errors']) end test '#bulk_create check update secret in a compose if it has errors with secrets' do @project.secrets.destroy_all new_secrets = [ { name: generate(:string), value: generate(:string) }, ] compose_payload = { errors: { secret_variables: [generate(:string)] } } compose_file = create(:compose_file, :invalid_file, project: @project, added_by: @admin, payload: compose_payload) container_attributes = attributes_for( :container, :with_public_port, receive_incoming_requests: true, secret_variables: [{ name: new_secrets.first[:name], value: '' }], ) template_payload = { containers_attributes: [container_attributes] } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :success compose_file.reload @project.reload new_project_secret = @project.secrets.detect { |project_secret| project_secret['name'] == new_secrets.first[:name] } container = compose_file.template.payload['containers_attributes'].first compose_secret = container['secret_variables'].detect { |container_secret| container_secret['name'] == new_secrets.first[:name] } assert { compose_file.valid_file? } assert_equal(new_project_secret['value'], compose_secret['value']) assert_empty(compose_file.payload['errors']) end test '#bulk_create check update secret in a compose if it has errors not related to secrets' do @project.secrets.destroy_all new_secrets = [ { name: generate(:string), value: generate(:string) }, ] compose_payload = { errors: { secret_variables: [generate(:string)], error_key: [generate(:string)] } } compose_file = create(:compose_file, :invalid_file, project: @project, added_by: @admin, payload: compose_payload) container_attributes = attributes_for( :container, :with_public_port, receive_incoming_requests: true, secret_variables: [{ name: new_secrets.first[:name], value: '' }], ) template_payload = { containers_attributes: [container_attributes] } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, secrets: new_secrets, } post :bulk_create, params: params, format: :json assert_response :success compose_file.reload @project.reload new_project_secret = @project.secrets.detect { |project_secret| project_secret['name'] == new_secrets.first[:name] } container = compose_file.template.payload['containers_attributes'].first compose_secret = container['secret_variables'].detect { |container_secret| container_secret['name'] == new_secrets.first[:name] } assert { compose_file.invalid_file? } assert_equal(new_project_secret['value'], compose_secret['value']) refute_empty(compose_file.payload['errors']) end test '#destroy admin deletes a secret' do deletable_secret = UffizziCore::Project.find(@project.id).secrets.last params = { project_slug: @project.slug, id: deletable_secret['name'], } differences = { -> { UffizziCore::Project.last.secrets.count } => -1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success end test '#destroy if a project has a compose with deleted secret' do deletable_secret = UffizziCore::Project.find(@project.id).secrets.last compose_file = create(:compose_file, project: @project, added_by: @admin) container_attributes = attributes_for( :container, :with_public_port, receive_incoming_requests: true, secret_variables: [deletable_secret], ) template_payload = { containers_attributes: [container_attributes] } create(:template, :compose_file_source, compose_file: compose_file, project: @project, added_by: @admin, payload: template_payload) params = { project_slug: @project.slug, id: deletable_secret['name'], } differences = { -> { UffizziCore::Project.last.secrets.count } => -1, -> { UffizziCore::ComposeFile.where(state: UffizziCore::ComposeFile::STATE_INVALID_FILE).count } => 1, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :success compose_file.reload assert { compose_file.invalid_file? } refute_empty(compose_file.payload['errors']) end test '#destroy if a secret doesn\'t exist' do params = { project_slug: @project.slug, id: generate(:string), } differences = { -> { UffizziCore::Project.last.secrets.count } => 0, } assert_difference differences do delete :destroy, params: params, format: :json end assert_response :not_found end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/projects_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::ProjectsControllerTest < ActionController::TestCase setup do @user = create(:user, :with_personal_account) sign_in @user @project = create(:project, :with_members, account: @user.personal_account, members: [@user]) end test '#index' do get :index, format: :json assert_response :success end test '#show' do create(:compose_file, project: @project, added_by: @user) create(:secret, resource: @project) create(:deployment, project: @project) get :show, params: { slug: @project.slug }, format: :json assert_response :success assert_equal(@project.name, JSON.parse(response.body)['project']['name']) end test '#destroy' do differences = { -> { UffizziCore::Project.active.count } => -1, } assert_difference differences do delete :destroy, params: { slug: @project.slug }, format: :json end assert_response :success end end ================================================ FILE: core/test/controllers/uffizzi_core/api/cli/v1/sessions_controller_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::Api::Cli::V1::SessionsControllerTest < ActionController::TestCase test '#create - successful' do password = generate(:password) user = create(:user, :with_personal_account, :active, password: password) params = { email: user.email, password: password } post :create, params: { user: params }, format: :json assert_response :success assert { signed_in? } end test '#create - failed with wrong password' do password = generate(:password) user = create(:user, :active, password: password) wrong_password = generate(:password) params = { email: user.email, password: wrong_password } post :create, params: { user: params }, format: :json assert_response :unprocessable_entity assert_not_empty JSON.parse(response.body)['errors'] assert { !signed_in? } end end ================================================ FILE: core/test/dummy/Rakefile ================================================ # frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative 'config/application' Rails.application.load_tasks ================================================ FILE: core/test/dummy/app/assets/config/manifest.js ================================================ //= link_tree ../images //= link_directory ../stylesheets .css //= link uffizzi_core_manifest.js ================================================ FILE: core/test/dummy/app/assets/images/.keep ================================================ ================================================ FILE: core/test/dummy/app/assets/stylesheets/application.css ================================================ /* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ ================================================ FILE: core/test/dummy/app/channels/application_cable/channel.rb ================================================ # frozen_string_literal: true module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: core/test/dummy/app/channels/application_cable/connection.rb ================================================ # frozen_string_literal: true module ApplicationCable class Connection < ActionCable::Connection::Base end end ================================================ FILE: core/test/dummy/app/controllers/application_controller.rb ================================================ # frozen_string_literal: true class ApplicationController < ActionController::Base end ================================================ FILE: core/test/dummy/app/controllers/concerns/.keep ================================================ ================================================ FILE: core/test/dummy/app/helpers/application_helper.rb ================================================ # frozen_string_literal: true module ApplicationHelper end ================================================ FILE: core/test/dummy/app/javascript/packs/application.js ================================================ // This is a manifest file that'll be compiled into application.js, which will include all the files // listed below. // // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. // // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the // compiled file. JavaScript code in this file should be added after the last require_* statement. // // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details // about supported directives. // //= require rails-ujs //= require activestorage //= require_tree . ================================================ FILE: core/test/dummy/app/jobs/application_job.rb ================================================ # frozen_string_literal: true class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: core/test/dummy/app/mailers/application_mailer.rb ================================================ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' end ================================================ FILE: core/test/dummy/app/models/application_record.rb ================================================ # frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end ================================================ FILE: core/test/dummy/app/models/concerns/.keep ================================================ ================================================ FILE: core/test/dummy/app/views/layouts/application.html.erb ================================================ Dummy <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all' %> <%= yield %> ================================================ FILE: core/test/dummy/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: core/test/dummy/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: core/test/dummy/bin/rails ================================================ #!/usr/bin/env ruby # frozen_string_literal: true load File.expand_path('spring', __dir__) APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' ================================================ FILE: core/test/dummy/bin/rake ================================================ #!/usr/bin/env ruby # frozen_string_literal: true load File.expand_path('spring', __dir__) require_relative '../config/boot' require 'rake' Rake.application.run ================================================ FILE: core/test/dummy/bin/setup ================================================ #!/usr/bin/env ruby # frozen_string_literal: true require 'fileutils' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end FileUtils.chdir(APP_ROOT) do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" system! 'bin/rails db:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" system! 'bin/rails restart' end ================================================ FILE: core/test/dummy/bin/spring ================================================ #!/usr/bin/env ruby # frozen_string_literal: true if !defined?(Spring) && [nil, 'development', 'test'].include?(ENV['RAILS_ENV']) # Load Spring without loading other gems in the Gemfile, for speed. require 'bundler' Bundler.locked_gems.specs.detect { |spec| spec.name == 'spring' }&.tap do |spring| Gem.use_paths(Gem.dir, Bundler.bundle_path.to_s, *Gem.path) gem 'spring', spring.version require 'spring/binstub' end end ================================================ FILE: core/test/dummy/config/application.rb ================================================ # frozen_string_literal: true require_relative 'boot' require 'byebug' require 'rack/cors' require 'rails/all' # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) require 'uffizzi_core' module Dummy class Application < Rails::Application config.load_defaults(Rails::VERSION::STRING.to_f) config.hosts = Settings.allowed_hosts config.middleware.insert_before(0, Rack::Cors) do allow do origins do |source| uri = URI.parse(source) Settings.allowed_hosts.any? { |host| uri.host == host || uri.host.ends_with?(host) } end resource '*', headers: :any, methods: [:get, :post, :options, :put, :patch, :delete], credentials: true end end end end ================================================ FILE: core/test/dummy/config/boot.rb ================================================ # frozen_string_literal: true # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) $LOAD_PATH.unshift(File.expand_path('../../../lib', __dir__)) ================================================ FILE: core/test/dummy/config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: dummy_production ================================================ FILE: core/test/dummy/config/database.yml ================================================ default: &default adapter: postgresql encoding: unicode host: <%= ENV.fetch("DATABASE_HOST") {"127.0.0.1"} %> port: <%= ENV.fetch("DATABASE_PORT") {5432} %> pool: <%= ENV.fetch("DATABASE_POOL") {5} %> username: <%= ENV.fetch("DATABASE_USER") {"postgres"} %> password: <%= ENV.fetch("DATABASE_PASSWORD") {""} %> development: <<: *default database: uffizzi_core_development test: <<: *default database: uffizzi_core_test ================================================ FILE: core/test/dummy/config/environment.rb ================================================ # frozen_string_literal: true # Load the Rails application. require_relative 'application' # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: core/test/dummy/config/environments/development.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{2.days.to_i}", } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true # Suppress logger output for asset requests. config.assets.quiet = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true end ================================================ FILE: core/test/dummy/config/environments/production.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.cache_classes = true # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true # Include generic and useful information about system operation, but avoid logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). config.log_level = :info # Prepend all log lines with the following tags. config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "dummy_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify # Log disallowed deprecations. config.active_support.disallowed_deprecation = :log # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new # Use a different logger for distributed setups. # require "syslog/logger" # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new($stdout) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Inserts middleware to perform automatic connection switching. # The `database_selector` hash is used to pass options to the DatabaseSelector # middleware. The `delay` is used to determine how long to wait after a write # to send a subsequent read to the primary. # # The `database_resolver` class is used by the middleware to determine which # database is appropriate to use based on the time delay. # # The `database_resolver_context` class is used by the middleware to set # timestamps for the last write to the primary. The resolver uses the context # class timestamps to determine how long to wait before reading from the # replica. # # By default Rails will store a last write timestamp in the session. The # DatabaseSelector middleware is designed as such you can define your own # strategy for connection switching and pass that into the middleware through # these configuration options. # config.active_record.database_selector = { delay: 2.seconds } # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end ================================================ FILE: core/test/dummy/config/environments/test.rb ================================================ # frozen_string_literal: true require 'active_support/core_ext/integer/time' # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. config.cache_classes = false config.action_view.cache_template_loading = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.hour.to_i}", } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raise exceptions for disallowed deprecations. config.active_support.disallowed_deprecation = :raise # Tell Active Support which deprecation messages to disallow. config.active_support.disallowed_deprecation_warnings = [] # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true end ================================================ FILE: core/test/dummy/config/initializers/application_controller_renderer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do # ApplicationController.renderer.defaults.merge!( # http_host: 'example.org', # https: false # ) # end ================================================ FILE: core/test/dummy/config/initializers/assets.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) ================================================ FILE: core/test/dummy/config/initializers/backtrace_silencers.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". Rails.backtrace_cleaner.remove_silencers! if ENV['BACKTRACE'] ================================================ FILE: core/test/dummy/config/initializers/config.rb ================================================ # frozen_string_literal: true Config.setup do |config| # Name of the constant exposing loaded settings config.const_name = 'Settings' # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'. # # config.knockout_prefix = nil # Overwrite an existing value when merging a `nil` value. # When set to `false`, the existing value is retained after merge. # # config.merge_nil_values = true # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged. # # config.overwrite_arrays = true # Load environment variables from the `ENV` object and override any settings defined in files. # # config.use_env = false # Define ENV variable prefix deciding which variables to load into config. # # Reading variables from ENV is case-sensitive. If you define lowercase value below, ensure your ENV variables are # prefixed in the same way. # # When not set it defaults to `config.const_name`. # config.env_prefix = 'SETTINGS' # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where # using dots in variable names might not be allowed (eg. Bash). # # config.env_separator = '.' # Ability to process variables names: # * nil - no change # * :downcase - convert to lower case # # config.env_converter = :downcase # Parse numeric values as integers instead of strings. # # config.env_parse_values = true # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details. # # config.schema do # required(:name).filled # required(:age).maybe(:int?) # required(:email).filled(format?: EMAIL_REGEX) # end # Evaluate ERB in YAML config files at load time. # # config.evaluate_erb_in_yaml = true end ================================================ FILE: core/test/dummy/config/initializers/content_security_policy.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Define an application-wide content security policy # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # Rails.application.config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # If you are using UJS then enable automatic nonce generation # Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Set the nonce only to specific directives # Rails.application.config.content_security_policy_nonce_directives = %w(script-src) # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only # Rails.application.config.content_security_policy_report_only = true ================================================ FILE: core/test/dummy/config/initializers/cookies_serializer.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. # Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json ================================================ FILE: core/test/dummy/config/initializers/filter_parameter_logging.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: core/test/dummy/config/initializers/inflections.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end ================================================ FILE: core/test/dummy/config/initializers/mime_types.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf ================================================ FILE: core/test/dummy/config/initializers/octokit.rb ================================================ # frozen_string_literal: true module Octokit class Client module Contents def contents?(repo, options = {}) options = options.dup repo_path = options.delete(:path) url = "#{Repository.path(repo)}/contents/#{repo_path}" head(url, options).nil? rescue Octokit::NotFound false end end end end ================================================ FILE: core/test/dummy/config/initializers/permissions_policy.rb ================================================ # frozen_string_literal: true # Define an application-wide HTTP permissions policy. For further # information see https://developers.google.com/web/updates/2018/06/feature-policy # # Rails.application.config.permissions_policy do |f| # f.camera :none # f.gyroscope :none # f.microphone :none # f.usb :none # f.fullscreen :self # f.payment :self, "https://secure.example.com" # end ================================================ FILE: core/test/dummy/config/initializers/wrap_parameters.rb ================================================ # frozen_string_literal: true # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end # To enable root element in JSON for ActiveRecord objects. # ActiveSupport.on_load(:active_record) do # self.include_root_in_json = true # end ================================================ FILE: core/test/dummy/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t 'hello' # # In views, this is aliased to just `t`: # # <%= t('hello') %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # The following keys must be escaped otherwise they will not be retrieved by # the default I18n backend: # # true, false, on, off, yes, no # # Instead, surround them with single quotes. # # en: # 'true': 'foo' # # To learn more, please read the Rails Internationalization guide # available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" ================================================ FILE: core/test/dummy/config/puma.rb ================================================ # frozen_string_literal: true # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum; this matches the default thread size of Active Record. # max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } threads min_threads_count, max_threads_count # Specifies the `worker_timeout` threshold that Puma will use to wait before # terminating a worker in development environments. # worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' # Specifies the `port` that Puma will listen on to receive requests; default is 3000. # port ENV.fetch('PORT', 3000) # Specifies the `environment` that Puma will run in. # environment ENV.fetch('RAILS_ENV', 'development') # Specifies the `pidfile` that Puma will use. pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') # Specifies the number of `workers` to boot in clustered mode. # Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). # # workers ENV.fetch("WEB_CONCURRENCY") { 2 } # Use the `preload_app!` method when specifying a `workers` number. # This directive tells Puma to first boot the application and load code # before forking the application. This takes advantage of Copy On Write # process behavior so workers use less memory. # # preload_app! # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart ================================================ FILE: core/test/dummy/config/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do mount UffizziCore::Engine => '/' end ================================================ FILE: core/test/dummy/config/settings.yml ================================================ app: ttl_reset_password_token: 900 host: http://lvh.me login: '' password: '' managed_dns_zone: example.com github: app_id: 123 app_slug: test client_id: test client_secret: test private_key: "-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKUZo1+jM7j760xsPlgt/5WzbRHL62kohW9hy8JpAauglOdgjRbY URXle+6+VNzGBU0kUXYjzUNJLBgC+JeubvUCAwEAAQJAEzQrO6mZD5BF60q/6bPY AcqwChzlEgNDmhQPBlr+db71EuLtmui7moCOTNLnZGIryd0uCGjEhXCwS653ivHW NQIhANcWtxH2xHviNafPAK+5mIABOCL+MXXCI5EnjUdARMc7AiEAxIDWD+bOzavo OGwiYrOQYcxxtZXGUUBKuQzpgvM5H48CIDcwbOj/GItxD7NvOg3c4XR226Ce+LHu jpHARE/z/bHhAiA+hxiWmsU3oPoV6iLO8YCB/kI4m94tQJ4GYFt1tdt5dQIhALQg cNfHZndOE7mwn6pm/JI8bXCseL7V2t+cXyCexlru -----END RSA PRIVATE KEY----- " webhook_secret: test registry_url: gcr.io/test docker_hub: registry_url: 'https://index.docker.io/v1/' public_namespace: 'library' controller: url: http://controller:8080 login: '' password: '' connection: retires_count: 1 next_retry_timeout_seconds: 1 timeout: 7 open_timeout: 5 limits: cpu: '200m' namespace_prefix: 'app-' resource_create_retry_time: 15.seconds resource_create_retry_count: 60 vcluster_controller: url: http://controller:8080 login: '' password: '' managed_dns_zone: '' connection: retires_count: 1 next_retry_timeout_seconds: 1 timeout: 7 open_timeout: 5 allowed_hosts: [] domain: http://lvh.me features: email_delivery_enabled: true stripe_enabled: false platform_cluster: project_id: test google: registry_url: 'https://gcr.io/' github_container_registry: registry_url: 'https://ghcr.io/' compose: default_memory: 125 memory_postfixes: <%= ['b', 'k', 'm', 'g'] %> memory_values: <%= [125, 250, 500, 1000, 2000, 4000] %> port_min_value: 1 port_max_value: 65535 delete_after_postfixes: <%= ['h'] %> delete_after_min_value: 1 delete_after_max_value: 720 default_tag: latest dockerfile_default_path: Dockerfile default_branch: 'master' continuous_preview: default_delete_preview_after: 72 deployment: max_memory_limit: 8000 subdomain: length_limit: 63 vcluster: max_creation_retry_count: 5 max_scale_up_retry_count: 5 ================================================ FILE: core/test/dummy/config/spring.rb ================================================ # frozen_string_literal: true Spring.watch( '.ruby-version', '.rbenv-vars', 'tmp/restart.txt', 'tmp/caching-dev.txt', ) ================================================ FILE: core/test/dummy/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> ================================================ FILE: core/test/dummy/config.ru ================================================ # frozen_string_literal: true # This file is used by Rack-based servers to start the application. require_relative 'config/environment' run Rails.application Rails.application.load_server ================================================ FILE: core/test/dummy/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2024_03_14_170113) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "uffizzi_core_accounts", force: :cascade do |t| t.text "name" t.text "kind", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "customer_token" t.string "state" t.string "subscription_token" t.datetime "payment_issue_at" t.string "domain" t.boolean "sso_enabled", default: false t.bigint "owner_id" t.integer "container_memory_limit" t.string "workos_organization_id" t.string "sso_state" t.index ["customer_token"], name: "index_accounts_on_customer_token", unique: true t.index ["domain"], name: "index_accounts_on_domain", unique: true t.index ["subscription_token"], name: "index_accounts_on_subscription_token", unique: true end create_table "uffizzi_core_activity_items", force: :cascade do |t| t.bigint "deployment_id", null: false t.string "namespace" t.string "name" t.string "tag" t.string "branch" t.string "type" t.bigint "container_id", null: false t.string "commit" t.string "commit_message" t.bigint "build_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.jsonb "data", default: {}, null: false t.string "digest" t.index ["container_id"], name: "index_activity_items_on_container_id" t.index ["deployment_id"], name: "index_activity_items_on_deployment_id" end create_table "uffizzi_core_builds", force: :cascade do |t| t.bigint "repo_id", null: false t.string "build_id" t.string "repository" t.string "branch" t.string "commit" t.string "committer" t.string "message" t.string "log_url" t.integer "status" t.datetime "started_at" t.datetime "ended_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.boolean "deployed" t.index ["build_id"], name: "index_builds_on_build_id", unique: true t.index ["repo_id"], name: "index_builds_on_repo_id" end create_table "uffizzi_core_clusters", force: :cascade do |t| t.bigint "project_id", null: false t.bigint "deployed_by_id" t.string "state" t.string "name" t.text "manifest" t.text "kubeconfig" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "host" t.string "creation_source" t.integer "kubernetes_distribution_id" t.index ["project_id"], name: "index_cluster_on_project_id" end create_table "uffizzi_core_comments", force: :cascade do |t| t.string "commentable_type" t.bigint "commentable_id" t.text "content" t.bigint "user_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "ancestry" t.integer "ancestry_depth", default: 0 t.index ["ancestry"], name: "index_comments_on_ancestry" t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable" t.index ["user_id"], name: "index_comments_on_user_id" end create_table "uffizzi_core_compose_files", force: :cascade do |t| t.string "source" t.bigint "repository_id" t.string "branch" t.string "path" t.string "auto_deploy" t.bigint "added_by_id" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "state" t.jsonb "payload", default: {}, null: false t.text "content" t.string "kind", default: "main" t.index ["project_id"], name: "index_compose_files_on_project_id" end create_table "uffizzi_core_config_files", force: :cascade do |t| t.string "filename" t.string "kind" t.bigint "added_by_id" t.text "payload" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "compose_file_id" t.string "creation_source" t.string "source" t.index ["compose_file_id"], name: "index_config_files_on_compose_file_id" t.index ["project_id"], name: "index_config_files_on_project_id" end create_table "uffizzi_core_container_config_files", force: :cascade do |t| t.string "mount_path" t.bigint "container_id", null: false t.bigint "config_file_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["config_file_id"], name: "index_container_config_files_on_config_file_id" t.index ["container_id"], name: "index_container_config_files_on_container_id" end create_table "uffizzi_core_container_host_volume_files", force: :cascade do |t| t.string "source_path" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "container_id", null: false t.bigint "host_volume_file_id", null: false t.index ["container_id"], name: "uf_core_cont_h_v_on_cont" t.index ["host_volume_file_id"], name: "uf_core_cont_h_v_on_h_v_file" end create_table "uffizzi_core_containers", force: :cascade do |t| t.string "image" t.string "tag" t.jsonb "variables" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "deployment_id" t.boolean "public", default: false, null: false t.integer "port" t.bigint "repo_id" t.string "state" t.string "continuously_deploy", null: false t.string "kind", default: "user" t.integer "target_port" t.string "controller_name" t.boolean "receive_incoming_requests" t.integer "memory_request" t.integer "memory_limit" t.jsonb "secret_variables" t.string "entrypoint" t.string "command" t.string "service_name" t.jsonb "healthcheck" t.jsonb "volumes" t.string "additional_subdomains", default: [], array: true t.string "full_image_name" t.index ["deployment_id"], name: "index_containers_on_deployment_id" t.index ["repo_id"], name: "index_containers_on_repo_id" end create_table "uffizzi_core_coupons", force: :cascade do |t| t.string "token" t.string "name", null: false t.string "currency", null: false t.bigint "amount_off", null: false t.string "duration", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_credentials", force: :cascade do |t| t.string "type" t.string "username" t.string "password" t.bigint "project_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "provider_ref" t.bigint "account_id" t.string "state" t.string "registry_url" t.index ["account_id"], name: "index_credentials_on_account_id" t.index ["project_id"], name: "index_credentials_on_project_id" t.index ["provider_ref"], name: "index_credentials_on_provider_ref" end create_table "uffizzi_core_deployment_events", force: :cascade do |t| t.string "deployment_state" t.string "message" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "deployment_id", null: false t.index ["deployment_id"], name: "uf_core_dep_events_on_dep" end create_table "uffizzi_core_deployments", force: :cascade do |t| t.bigint "project_id", null: false t.text "kind", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "creator_name" t.string "subdomain" t.string "state" t.float "memory_limit" t.bigint "deployed_by_id" t.bigint "continuous_preview_id_deprecated" t.jsonb "continuous_preview_payload" t.string "creation_source" t.bigint "compose_file_id" t.bigint "template_id" t.datetime "disabled_at" t.jsonb "metadata", default: {} t.datetime "last_deploy_at" t.index ["compose_file_id"], name: "index_deployments_on_compose_file_id" t.index ["project_id"], name: "index_deployments_on_project_id" t.index ["template_id"], name: "index_deployments_on_template_id" end create_table "uffizzi_core_events", force: :cascade do |t| t.bigint "activity_item_id", null: false t.string "state" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["activity_item_id"], name: "index_events_on_activity_item_id" end create_table "uffizzi_core_host_volume_files", force: :cascade do |t| t.string "source" t.string "path" t.boolean "is_file" t.binary "payload" t.bigint "added_by_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "project_id", null: false t.bigint "compose_file_id", null: false t.index ["compose_file_id"], name: "index_host_volume_file_on_compose_file_id" t.index ["project_id"], name: "index_host_volume_file_on_project_id" end create_table "uffizzi_core_invitations", force: :cascade do |t| t.text "email", null: false t.text "token", null: false t.string "status", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "invited_by_id", null: false t.bigint "entityable_id", null: false t.string "entityable_type", null: false t.string "role", null: false t.bigint "invitee_id" t.index ["token"], name: "index_invitations_on_token", unique: true end create_table "uffizzi_core_kubernetes_distributions", force: :cascade do |t| t.string "version" t.string "distro" t.string "image" t.boolean "default", default: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_memberships", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "account_id", null: false t.text "role", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "state" t.index ["account_id"], name: "index_memberships_on_account_id" t.index ["user_id", "account_id"], name: "index_memberships_on_user_id_and_account_id" t.index ["user_id"], name: "index_memberships_on_user_id" end create_table "uffizzi_core_payments", force: :cascade do |t| t.bigint "account_id", null: false t.string "charge_id" t.string "status" t.float "amount" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["account_id"], name: "index_payments_on_account_id" end create_table "uffizzi_core_prices", force: :cascade do |t| t.string "token" t.string "slug", null: false t.string "name", null: false t.float "units_price", null: false t.bigint "units_amount", null: false t.bigint "product_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["product_id"], name: "index_prices_on_product_id" end create_table "uffizzi_core_products", force: :cascade do |t| t.string "token" t.string "slug", null: false t.string "name", null: false t.string "type", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "kind" end create_table "uffizzi_core_projects", force: :cascade do |t| t.text "name", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "account_id", null: false t.string "state" t.string "slug" t.string "description" t.index ["account_id", "name"], name: "proj_uniq_name", unique: true, where: "((state)::text = 'active'::text)" t.index ["account_id"], name: "index_projects_on_account_id" end create_table "uffizzi_core_ratings", force: :cascade do |t| t.string "name" t.string "state" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_repos", force: :cascade do |t| t.string "namespace" t.string "name" t.string "tag" t.string "type" t.string "branch" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "description" t.boolean "is_private" t.string "slug" t.bigint "repository_id" t.string "kind" t.string "dockerfile_path" t.jsonb "args" t.string "dockerfile_context_path" t.boolean "deploy_preview_when_pull_request_is_opened" t.boolean "delete_preview_when_pull_request_is_closed" t.boolean "deploy_preview_when_image_tag_is_created" t.boolean "delete_preview_when_image_tag_is_updated" t.boolean "share_to_github" t.integer "delete_preview_after" t.string "tag_pattern_deprecated" t.index ["project_id"], name: "index_repos_on_project_id" end create_table "uffizzi_core_roles", force: :cascade do |t| t.string "name" t.string "resource_type" t.bigint "resource_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id" end create_table "uffizzi_core_secrets", force: :cascade do |t| t.string "name" t.string "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "resource_type" t.bigint "resource_id" t.index ["resource_type", "resource_id"], name: "index_uffizzi_core_secrets_on_resource" end create_table "uffizzi_core_templates", force: :cascade do |t| t.string "name" t.bigint "added_by_id" t.jsonb "payload", null: false t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "compose_file_id" t.string "creation_source" t.index ["project_id"], name: "index_templates_on_project_id" end create_table "uffizzi_core_user_projects", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "project_id", null: false t.text "role", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "invited_by_id" t.index ["project_id"], name: "index_user_projects_on_project_id" t.index ["user_id", "project_id"], name: "index_user_projects_on_user_id_and_project_id" t.index ["user_id"], name: "index_user_projects_on_user_id" end create_table "uffizzi_core_users", force: :cascade do |t| t.string "first_name" t.string "last_name" t.string "email", default: "", null: false t.string "password_digest", default: "", null: false t.string "confirmation_token" t.string "state" t.string "phone" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "github" t.string "website" t.string "twitter" t.string "linkedin" t.string "devto" t.string "facebook" t.string "blog" t.text "bio" t.string "status" t.string "availability" t.string "primary_skills" t.string "learning" t.string "coding_for" t.string "education" t.string "title" t.string "work" t.string "primary_location" t.string "creation_source" t.index "lower((email)::text)", name: "index_email_on_lower_email", unique: true end create_table "uffizzi_core_users_roles", id: false, force: :cascade do |t| t.bigint "user_id" t.bigint "role_id" t.index ["role_id"], name: "index_users_roles_on_role_id" t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" t.index ["user_id"], name: "index_users_roles_on_user_id" end add_foreign_key "uffizzi_core_clusters", "uffizzi_core_projects", column: "project_id" add_foreign_key "uffizzi_core_container_host_volume_files", "uffizzi_core_containers", column: "container_id" add_foreign_key "uffizzi_core_container_host_volume_files", "uffizzi_core_host_volume_files", column: "host_volume_file_id" add_foreign_key "uffizzi_core_deployment_events", "uffizzi_core_deployments", column: "deployment_id" add_foreign_key "uffizzi_core_host_volume_files", "uffizzi_core_compose_files", column: "compose_file_id" add_foreign_key "uffizzi_core_host_volume_files", "uffizzi_core_projects", column: "project_id" end ================================================ FILE: core/test/dummy/lib/assets/.keep ================================================ ================================================ FILE: core/test/dummy/log/.keep ================================================ ================================================ FILE: core/test/dummy/public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

================================================ FILE: core/test/dummy/public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

If you are the application owner check the logs for more information.

================================================ FILE: core/test/dummy/public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

================================================ FILE: core/test/factories/account.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :account, class: UffizziCore::Account do name kind { UffizziCore::Account.kind.personal } customer_token { nil } subscription_token { nil } state { nil } payment_issue_at { nil } owner { nil } created_at domain trait :with_admin do transient do admin { nil } end before(:create) do |account, evaluator| account.owner = evaluator.admin end after(:create) do |account, evaluator| if evaluator.admin user = evaluator.admin user.memberships.create(user: user, account: account, role: UffizziCore::Membership.role.admin) account.projects.each do |project| user.user_projects.create!(user: user, project: project, role: UffizziCore::UserProject.role.admin) end end end end trait :personal_account do kind { UffizziCore::Account.kind.personal } end trait :with_stripe_enitities do after(:create) do |account, _evaluator| if account UffizziCore::StripeService.create_customer(account) UffizziCore::StripeService.create_subscription(account) end end end trait :disabled do state { :disabled } end trait :payment_issue do state { :payment_issue } end trait :draft do state { UffizziCore::Account::STATE_DRAFT } end trait :sso_connection_active do sso_state { UffizziCore::Account::STATE_CONNECTION_ACTIVE } end trait :sso_connection_disabled do sso_state { UffizziCore::Account::STATE_CONNECTION_DISABLED } end end end ================================================ FILE: core/test/factories/activity_item.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :activity_item, class: UffizziCore::ActivityItem do deployment { nil } namespace name tag { '' } branch { '' } container { nil } commit commit_message build_id { nil } trait :with_building_event do after(:create) do |activity_item, _evaluator| activity_item.events.create(state: UffizziCore::Event.state.building) end end trait :with_deploying_event do after(:create) do |activity_item, _evaluator| activity_item.events.create(state: UffizziCore::Event.state.deploying) end end trait :with_deployed_event do after(:create) do |activity_item, _evaluator| activity_item.events.create(state: UffizziCore::Event.state.deployed) end end trait :with_failed_event do after(:create) do |activity_item, _evaluator| activity_item.events.create(state: UffizziCore::Event.state.failed) end end trait :docker do type { UffizziCore::ActivityItem::Docker.name } end trait :github do type { UffizziCore::ActivityItem::Github.name } end trait :memory_limit do type { UffizziCore::ActivityItem::MemoryLimit.name } end end end ================================================ FILE: core/test/factories/cluster.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :cluster, class: UffizziCore::Cluster do name { generate(:cluster_name) } project { nil } deployed_by { nil } trait :deployed do state { :deployed } end end end ================================================ FILE: core/test/factories/comments.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :comment, class: UffizziCore::Comment do commentable { nil } content parent { nil } user { nil } end end ================================================ FILE: core/test/factories/compose_files.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :compose_file, class: UffizziCore::ComposeFile do source { generate(:name) } repository_id { generate(:number) } branch path auto_deploy { UffizziCore::ComposeFile::STATE_DISABLED } kind { UffizziCore::ComposeFile.kind.main } trait :auto_deploy do auto_deploy { UffizziCore::ComposeFile::STATE_ENABLED } end trait :invalid_file do state { :invalid_file } end trait :temporary do kind { UffizziCore::ComposeFile.kind.temporary } end end end ================================================ FILE: core/test/factories/config_files.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :config_file, class: UffizziCore::ConfigFile do filename { generate(:name) } added_by { nil } kind { UffizziCore::ConfigFile.kind.config_map } payload { generate(:string) } project { nil } creation_source { UffizziCore::ConfigFile.creation_source.manual } end trait :compose_file_source do creation_source { UffizziCore::ConfigFile.creation_source.compose_file } end end ================================================ FILE: core/test/factories/container_config_files.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :container_config_file, class: UffizziCore::ContainerConfigFile do mount_path { generate(:path) } container { nil } config_file { nil } end end ================================================ FILE: core/test/factories/container_host_volume_files.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :container_host_volume_file, class: UffizziCore::ContainerHostVolumeFile do source_path { generate(:relative_path) } container { nil } host_volume_file { nil } end end ================================================ FILE: core/test/factories/containers.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :container, class: UffizziCore::Container do image full_image_name tag service_name variables { [] } secret_variables { [] } deployment repo { nil } receive_incoming_requests { false } entrypoint { nil } command { nil } continuously_deploy { UffizziCore::Container::STATE_CD_DISABLED } healthcheck { nil } volumes { nil } memory_limit { Settings.compose.default_memory } memory_request { Settings.compose.default_memory } trait :with_public_port do public { true } port end trait :continuously_deploy_enabled do continuously_deploy { UffizziCore::Container::STATE_CD_ENABLED } end trait :continuously_deploy_disabled do continuously_deploy { UffizziCore::Container::STATE_CD_DISABLED } end initialize_with { new } trait :active do state { UffizziCore::Container::STATE_ACTIVE } end trait :with_named_volume do volumes do [ { source: generate(:string), target: generate(:path), type: UffizziCore::ComposeFile::Parsers::Services::VolumesParserService::NAMED_VOLUME_TYPE, }, ] end end end end ================================================ FILE: core/test/factories/credentials.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :credential, class: UffizziCore::Credential do username { generate(:login) } password account { nil } registry_url { generate(:url) } trait :docker_hub do type { UffizziCore::Credential::DockerHub.name } end trait :azure do type { UffizziCore::Credential::Azure.name } end trait :google do type { UffizziCore::Credential::Google.name } registry_url { 'https://gcr.io/' } end trait :github_container_registry do type { UffizziCore::Credential::GithubContainerRegistry.name } registry_url { 'https://ghcr.io' } end trait :docker_registry do type { UffizziCore::Credential::DockerRegistry.name } registry_url { 'https://example.com/registry' } end trait :amazon do type { UffizziCore::Credential::Amazon.name } registry_url { 'https://123456789876.dkr.ecr.us-east-1.amazonaws.com' } end trait :unauthorized do state { :unauthorized } end trait :active do state { :active } end end end ================================================ FILE: core/test/factories/deployment.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :deployment, class: UffizziCore::Deployment do kind { nil } subdomain { generate :subdomain_name } created_at { DateTime.current } memory_limit { 1 } continuous_preview_payload { nil } creation_source { nil } metadata { {} } trait :active do state { :active } end trait :disabled do state { :disabled } end end end ================================================ FILE: core/test/factories/host_volume_files.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :host_volume_file, class: UffizziCore::HostVolumeFile do path { generate(:path) } source { generate(:relative_path) } payload { generate(:string) } added_by { nil } project { nil } compose_file { nil } end end ================================================ FILE: core/test/factories/kubernetes_distribution.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :kubernetes_distribution, class: UffizziCore::KubernetesDistribution do version { generate(:version) } image { generate(:string) } distro { generate(:string) } trait :default do default { true } end end end ================================================ FILE: core/test/factories/membership.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :membership, class: UffizziCore::Membership do user { nil } account { nil } role { UffizziCore::Membership.role.developer } trait :admin do role { UffizziCore::Membership.role.admin } end trait :developer do role { UffizziCore::Membership.role.developer } end trait :viewer do role { UffizziCore::Membership.role.viewer } end end end ================================================ FILE: core/test/factories/payments.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :payment, class: UffizziCore::Payment do account { nil } end end ================================================ FILE: core/test/factories/project.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :project, class: UffizziCore::Project do name { generate :name } slug { generate :slug } description { generate :description } account trait :with_members do transient do members { [] } end after(:create) do |project, evaluator| evaluator.members.each do |member| account = project.account role = account.memberships.find_by(user_id: member.id).role project.user_projects.create( user: member, invited_by: account.owner, role: role, ) end end end end end ================================================ FILE: core/test/factories/ratings.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :rating, class: UffizziCore::Rating do name end end ================================================ FILE: core/test/factories/repos.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :repo, class: UffizziCore::Repo do namespace name { generate(:string) } tag slug branch description repository_id { generate(:number) } deploy_preview_when_pull_request_is_opened { false } delete_preview_when_pull_request_is_closed { false } deploy_preview_when_image_tag_is_created { false } delete_preview_when_image_tag_is_updated { false } delete_preview_after { nil } share_to_github { false } trait :docker_hub do type { UffizziCore::Repo::DockerHub.name } end trait :azure do type { UffizziCore::Repo::Azure.name } end trait :google do type { UffizziCore::Repo::Google.name } end trait :amazon do type { UffizziCore::Repo::Amazon.name } end trait :kind_barestatic do kind { UffizziCore::Repo.kind.barestatic } end trait :kind_buildpacks18 do kind { UffizziCore::Repo.kind.buildpacks18 } end trait :kind_dockerfile do kind { UffizziCore::Repo.kind.dockerfile } end end end ================================================ FILE: core/test/factories/secrets.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :secret, class: UffizziCore::Secret do resource { nil } name { generate(:string) } value { generate(:string) } end end ================================================ FILE: core/test/factories/sequences.rb ================================================ # frozen_string_literal: true FactoryBot.define do sequence :time do Faker::Time.between(from: DateTime.now - 1, to: DateTime.now) end sequence :string, aliases: [ :tag, :namespace, :branch, :phone, :website, :twitter, :github, :linkedin, :devto, :facebook, :blog, :bio, :status, :availability, :primary_skills, :learning, :coding_for, :education, :title, :work, :primary_location, :content, :login ] do |n| "string_#{n}" end sequence :image do |n| "namespace/name-#{n}" end sequence :full_image_name do |n| "namespace/name-#{n}:latest" end sequence :kubernetes_name do |n| "kubernetes-name-#{n}" end sequence :commit do |n| "commit-hash-#{n}" end sequence :commit_message do |n| "commit-message-#{n}" end sequence :slug do |n| "slug_#{n}" end sequence :name, aliases: [:first_name, :last_name, :username, :description, :service_name] do |n| "Name #{n}" end sequence :token, aliases: [:confirmation_token, :customer_token, :subscription_token] do |_n| UffizziCore::TokenService.generate end sequence :url, aliases: [:image_url] do |n| "http://url#{n}.com" end sequence :password do |n| "Password1String-#{n}" end sequence :email do |n| "user#{n}@example.com" end sequence :instance_name do |n| "project-name-#{Time.now.to_i}-#{n}" end sequence :path do |n| "/volumes/directory-#{n}" end sequence :relative_path do |n| "./directory-#{n}" end sequence :number, aliases: [ :integer, :mileage, :ram, :port, :selected_storage_size, :storage_initial, :storage_capacity, :max_connections, :base_price_per_month, :extra_gigabyte_price_per_hour, :memory_limit, :claps, ] do |n| n end sequence :boolean, aliases: [ :has_public_ip, :has_storage_autoscaling, :has_high_availability, :backups_mandatory, :has_shared_memory, ] do Faker::Boolean.boolean end sequence :vcpu do |n| ['shared', n].sample end sequence :domain_name, aliases: [:domain] do Faker::Internet.domain_name(subdomain: true) end sequence :subdomain_name do |_n| Faker::Lorem .words(number: 2, supplemental: true) .join('-') .delete(',. ') .downcase end sequence :created_at, aliases: ['updated_at'] do DateTime.now end sequence :cluster_name do |n| "cluster-name-#{n}" end sequence :version do |n| "#{n}.#{n}#{n}" end end ================================================ FILE: core/test/factories/templates.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :template, class: UffizziCore::Template do name project { nil } added_by_id { nil } creation_source { UffizziCore::Template.creation_source.manual } trait :compose_file_source do creation_source { UffizziCore::Template.creation_source.compose_file } end end end ================================================ FILE: core/test/factories/user.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :user, class: UffizziCore::User do first_name last_name email password phone confirmation_token created_at github website twitter linkedin devto facebook blog bio status availability primary_skills learning coding_for education title work primary_location trait :with_personal_account do after(:create) do |user, _evaluator| create(:account, :with_admin, kind: UffizziCore::Account.kind.personal, admin: user, created_at: user.created_at) end end trait :with_organizational_account do after(:create) do |user, _evaluator| create(:account, :with_admin, kind: UffizziCore::Account.kind.organizational, admin: user, created_at: user.created_at) end end trait :active do state { :active } end trait :disabled do state { :disabled } end trait :global_admin do after(:create) do |user, _evaluator| user.add_role(:admin) end end trait :user_google do creation_source { :google } end trait :user_sso do creation_source { :sso } end trait :admin_in_organization do transient do organization { nil } end after(:create) do |user, evaluator| create(:membership, :admin, user: user, account: evaluator.organization) end end trait :developer_in_organization do transient do organization { nil } end after(:create) do |user, evaluator| create(:membership, :developer, user: user, account: evaluator.organization) end end trait :viewer_in_organization do transient do organization { nil } end after(:create) do |user, evaluator| create(:membership, :viewer, user: user, account: evaluator.organization) end end end end ================================================ FILE: core/test/factories/user_project.rb ================================================ # frozen_string_literal: true FactoryBot.define do factory :user_project, class: UffizziCore::UserProject do user { nil } project { nil } role { nil } trait :admin do role { UffizziCore::UserProject.role.admin } end trait :developer do role { UffizziCore::UserProject.role.developer } end trait :viewer do role { UffizziCore::UserProject.role.viewer } end end end ================================================ FILE: core/test/fixtures/files/cluster/manifest.yml ================================================ apiVersion: v1 kind: Pod metadata: name: web-app spec: containers: - name: nginx image: nginx ports: - containerPort: 80 --- apiVersion: v1 kind: Service metadata: name: web-service spec: selector: app.kubernetes.io/name: web-app ports: - protocol: TCP port: 8080 targetPort: 80 ================================================ FILE: core/test/fixtures/files/compose_dependencies/configs/vote_conf.json ================================================ { "content": "c2VydmVyIHsKICBsaXN0ZW4gICAgICAgODA4MDsKICBzZXJ2ZXJfbmFtZSAg\nZXhhbXBsZS5jb207CiAgbG9jYXRpb24gLyB7CiAgICBwcm94eV9wYXNzICAg\nICAgaHR0cDovLzEyNy4wLjAuMTo4MC87CiAgfQogIGxvY2F0aW9uIC92b3Rl\nLyB7CiAgICBwcm94eV9wYXNzICAgICAgaHR0cDovLzEyNy4wLjAuMTo4ODg4\nLzsKICB9Cn0K\n" } ================================================ FILE: core/test/fixtures/files/compose_files/azure_services/nginx.yml ================================================ services: nginx: image: account.azurecr.io/nginx:latest x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/boolean_option.yml ================================================ services: on: image: nginx:latest ================================================ FILE: core/test/fixtures/files/compose_files/compose_empty.yml ================================================ ================================================ FILE: core/test/fixtures/files/compose_files/compose_memory.yml ================================================ services: nginx: image: nginx:latest deploy: resources: limits: memory: 1000m redis: image: redis:latest deploy: resources: limits: memory: 250000000b ubuntu: image: ubuntu:latest deploy: resources: limits: memory: 4g postgres: image: postgres:latest deploy: resources: limits: memory: 125000k x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/compose_vote_app_github.yml ================================================ services: redis: image: redis:latest postgres: image: postgres:9.6 environment: POSTGRES_USER: USER POSTGRES_PASSWORD: PASSWORD nginx: image: nginx:latest configs: - source: vote_conf target: /etc/nginx/conf.d worker: build: context: https://github.com/ACCOUNT/example-voting-worker dockerfile: Dockerfile deploy: resources: limits: memory: 250M vote: build: context: https://github.com/ACCOUNT/example-voting-vote dockerfile: Dockerfile result: build: context: https://github.com/ACCOUNT/example-voting-result dockerfile: Dockerfile configs: vote_conf: file: ./vote.conf continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true x-uffizzi-ingress: service: nginx port: 8080 ================================================ FILE: core/test/fixtures/files/compose_files/compose_with_continuous_preview.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: 12h x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/compose_with_only_line.yml ================================================ services ================================================ FILE: core/test/fixtures/files/compose_files/compose_with_syntax_error.yml ================================================ services: redis: image: redis:latest hello-world image: nginx x-uffizzi-continuous-preview: delete_preview_after: 12h x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/compose_with_volumes.yml ================================================ services: web: image: web_service:latest volumes: - share_data:/var/web/logs - share_data_2:/var/web/logs:ro nginx: image: nginx:latest volumes: - source: share_data target: /some_share_data read_only: true - source: share_data_2 target: /some_share_data_2 - source: share_data_2 target: /some_share_data_22 volumes: share_data: share_data_2: x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/compose_without_image.yml ================================================ services: nginx: environment: TEST: 123 x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/compose_without_services.yml ================================================ x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/config_file_dependencies.yml ================================================ services: nginx: image: nginx:latest configs: - source: vote.conf target: /etc/nginx/conf.d x-uffizzi-ingress: service: nginx port: 8080 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/account_custom_image.yml ================================================ services: nginx: image: account/custom_image:v1.0 x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_auto_deploy_off.yml ================================================ services: hello-world: image: nginx deploy: x-uffizzi-auto-deploy-updates: false x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_config_files.yml ================================================ services: nginx: image: nginx:latest configs: - source: vote.conf target: /etc/nginx x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_config_invalid_ingress_service.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: service: test port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_configs_long_syntax.yml ================================================ services: nginx: image: nginx:latest configs: - source: vote_conf target: /etc/nginx configs: vote_conf: file: ./configs/vote.conf x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_configs_long_syntax_without_target.yml ================================================ services: nginx: image: nginx:latest configs: - source: vote_conf configs: vote_conf: file: ./etc/nginx/vote.conf x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_configs_short_syntax.yml ================================================ services: nginx: image: nginx:latest configs: - vote_conf configs: vote_conf: file: ./vote.conf x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_configs_short_syntax_invalid_path.yml ================================================ services: nginx: image: nginx:latest configs: - vote_conf configs: vote_conf: file: '' x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_configs_short_syntax_unknown_config.yml ================================================ services: nginx: image: nginx:latest configs: - unknown_config configs: vote_conf: file: ./etc/nginx/vote.conf defaulf_conf: file: ./etc/nginx/default.conf x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_cp_delete_after_integer.yml ================================================ services: hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: 99 x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_hours.yml ================================================ services: hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_max.yml ================================================ services: hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: 99999999h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_min.yml ================================================ services: hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: 0h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_postfix.yml ================================================ services: hello-world: image: nginx x-uffizzi-continuous-preview: delete_preview_after: 24m x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_env_file.yml ================================================ services: hello-world: image: nginx env_file: .env x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_env_file_duplicates.yml ================================================ services: hello-world: image: nginx env_file: - ./.env - .env - .env/ - .env - infra/secrets.env x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_env_file_empty.yml ================================================ services: hello-world: image: nginx env_file: x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_env_file_empty_in_array.yml ================================================ services: hello-world: image: nginx env_file: - .env - - .local.env x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_envs.yml ================================================ services: hello-world: image: nginx environment: MULTILINE_KEY: | -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAvt9uoL0Ke7He/EmdxhfBn3hwg0WLASk6zoxLlTcvNpCMUEED QkUUQX9S1PCUDuW8Urr87ZRCnzurnj7EGjFSeKS7qt4kesIfaAsyBC8Sf8D7IZ7d QuOS/f1R5Qt1ofSSmgmUvg6rROJs7CqtI0YLYysr2Dn2fuTZf4Jc -----END RSA PRIVATE KEY----- SECRET: 'secret' RAILS_WORKERS_COUNT: 0 APP_URL: 'http://lvh.me:7000' APP_PASSWORD: '' RUBYOPT: -W:no-deprecated -W:no-experimental CDN_HOST: https://cdn.cloud?k=1 x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_ingress_port_non_integer.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: service: nginx port: '2345' ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_deploy_auto.yml ================================================ services: hello-world: image: nginx deploy: x-uffizzi-auto-deploy-updates: TEST x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_memory.yml ================================================ services: nginx: image: nginx:latest deploy: resources: limits: memory: 3g x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_memory_min_value.yml ================================================ services: nginx: image: nginx:latest deploy: resources: limits: memory: 5M x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_memory_postfix.yml ================================================ services: nginx: image: nginx:latest deploy: resources: limits: memory: 5TEST x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_memory_type.yml ================================================ services: nginx: image: nginx:latest deploy: resources: limits: memory: 100 x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_invalid_port.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: service: nginx port: 99999 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_uffizzi_ingress.yml ================================================ services: nginx: image: nginx:latest x-uffizzi: ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_with_continuous_preview_without_x-uffizzi.yml ================================================ services: nginx: image: nginx:latest continuous_preview: delete_preview_after: 10h share_to_github: false x-uffizzi-ingress: service: nginx port: 80 continuous_preview: delete_preview_after: 10h share_to_github: false ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_with_ingress_without_x-uffizzi.yml ================================================ services: nginx: image: nginx:latest ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_without_ingress.yml ================================================ services: nginx: image: nginx:latest ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_without_ingress_port.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: service: nginx ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_without_ingress_service.yml ================================================ services: nginx: image: nginx:latest x-uffizzi-ingress: port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/nginx_without_tag.yml ================================================ services: nginx: image: nginx x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/postgres_secrets.yml ================================================ services: postgres: image: postgres secrets: - postgres_user - postgres_password secrets: postgres_user: external: true name: POSTGRES_USER postgres_password: external: true name: POSTGRES_PASSWORD x-uffizzi-ingress: service: postgres port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/postgres_secrets_duplicates.yml ================================================ services: postgres: image: postgres secrets: - postgres_user - postgres_password secrets: postgres_user: external: true name: POSTGRES_USER postgres_password: external: true name: POSTGRES_USER x-uffizzi-ingress: service: postgres port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/postgres_secrets_unknown.yml ================================================ services: postgres: image: postgres:latest secrets: - postgres secrets: postgres_user: external: true name: POSTGRES_USER postgres_password: external: true name: POSTGRES_PASSWORD x-uffizzi-ingress: service: postgres port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/postgres_secrets_without_external.yml ================================================ services: postgres: image: postgres secrets: - postgres_user - postgres_password secrets: postgres_user: external: true name: POSTGRES_USER postgres_password: external: false name: POSTGRES_PASSWORD x-uffizzi-ingress: service: postgres port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/postgres_secrets_without_name.yml ================================================ services: postgres: image: postgres secrets: - postgres_user - postgres_password secrets: postgres_user: external: true name: POSTGRES_USER postgres_password: external: true x-uffizzi-ingress: service: postgres port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/volumes_anonymous.yml ================================================ services: nginx: image: nginx:latest volumes: - /var/web/logs_1 - /var/web/logs_2 x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/dockerhub_services/volumes_named.yml ================================================ services: nginx: image: nginx:latest volumes: - share_data:/var/web/logs_1 volumes: share_data: x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/github_services/hello_world.yml ================================================ services: hello-world: build: context: https://github.com/ACCOUNT/hello-world x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/google_services/cloudsql.yml ================================================ services: nginx: image: gcr.io/project-name/nginx:latest sql-proxy: image: gcr.io/cloudsql-docker/gce-proxy:1.19.1 command: - '/cloud_sql_proxy' - '-dir=/cloudsql' - '-instances=instance=tcp:5432' - '-credential_file=/sql-proxy.json' x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/google_services/cloudsql_entrypoint.yml ================================================ services: nginx: image: gcr.io/project-name/nginx:latest sql-proxy: image: gcr.io/cloudsql-docker/gce-proxy:1.19.1 entrypoint: /cloud_sql_proxy -dir=/cloudsql -instances=instance=tcp:5432 -credential_file=/sql-proxy.json x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/google_services/nginx.yml ================================================ services: nginx: image: gcr.io/project-name/nginx:latest x-uffizzi-ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/array_command_success.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 1m30s timeout: 10s retries: 3 start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/disabled_healthcheck.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: ["NONE"] interval: 1m30s timeout: 10s retries: 3 start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/healthcheck_with_disable.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: disable: true x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/invalid_command.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: 1 interval: 1m30s timeout: 10s retries: 3 start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/invalid_interval.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 1p30w timeout: 10s retries: 3 start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/invalid_options.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: interval: 1m30s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/invalid_retries.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: ["CMD", "curl", "-f", "http://localhost"] interval: 1m30s timeout: 10s retries: '3' start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/healthcheck/string_command_success.yml ================================================ services: redis: image: redis:latest hello-world: image: nginx healthcheck: test: curl -f http://localhost interval: 1m30s timeout: 10s retries: 3 start_period: 40s x-uffizzi: continuous_preview: delete_preview_after: 10h x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/compose_files/invalid_service.yml ================================================ services: hello-^&*world: image: nginx x-uffizzi-ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/controller/cluster_asleep.json ================================================ { "name": "uffizzi-test-cluster-vcluster", "namespace": "uffizzi-test-cluster", "uid": "90cffab2-812b-418f-a84b-5dcdded15bfd", "status": { "ready": true, "sleep": true, "kubeConfig": "test-kubeconfig" } } ================================================ FILE: core/test/fixtures/files/controller/cluster_awake.json ================================================ { "name": "uffizzi-test-cluster-vcluster", "namespace": "uffizzi-test-cluster", "uid": "90cffab2-812b-418f-a84b-5dcdded15bfd", "status": { "ready": true, "sleep": false, "kubeConfig": "test-kubeconfig" } } ================================================ FILE: core/test/fixtures/files/controller/cluster_not_ready.json ================================================ { "name": "uffizzi-test-cluster-vcluster", "namespace": "uffizzi-test-cluster", "uid": "90cffab2-812b-418f-a84b-5dcdded15bfd", "status": { "ready": false, "kubeConfig": "" } } ================================================ FILE: core/test/fixtures/files/controller/cluster_ready.json ================================================ { "name": "uffizzi-test-cluster-vcluster", "namespace": "uffizzi-test-cluster", "uid": "90cffab2-812b-418f-a84b-5dcdded15bfd", "status": { "ready": true, "kubeConfig": "test-kubeconfig" } } ================================================ FILE: core/test/fixtures/files/controller/deployment_containers.json ================================================ [ { "metadata": { "name": "app-test-env-1-6f4c8896d-xrcfq", "generateName": "app-test-env-1-6f4c8896d-", "namespace": "test-env-1", "uid": "7ed22d85-495e-457f-9916-880b54f8d1bc", "resourceVersion": "72170704", "creationTimestamp": "2021-08-20T13:05:33Z", "labels": { "app": "app-test-env-1", "app.kubernetes.io/managed-by": "uffizzi", "pod-template-hash": "6f4c8896d" }, "annotations": { "kubectl.kubernetes.io/restartedAt": "2021-08-20T13:05:33Z" }, "ownerReferences": [ { "apiVersion": "apps/v1", "kind": "ReplicaSet", "name": "app-test-env-1-6f4c8896d", "uid": "81df5b4b-acbd-4384-9e97-9dc663275728", "controller": true, "blockOwnerDeletion": true } ] }, "spec": { "containers": [ { "name": "f03d008a48", "image": "uffizzitest/webhooks-test-app:latest", "ports": [ { "name": "default-port", "containerPort": 80, "protocol": "TCP" } ], "env": [ { "name": "PORT", "value": "80" } ], "resources": { "limits": { "cpu": "25m", "memory": "250Mi" }, "requests": { "cpu": "25m", "memory": "250Mi" } }, "startupProbe": { "tcpSocket": { "port": 80 }, "initialDelaySeconds": 10, "timeoutSeconds": 1, "periodSeconds": 15, "successThreshold": 1, "failureThreshold": 80 }, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "imagePullPolicy": "Always", "controllerName" : "f03d008a48" } ], "restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "dnsPolicy": "ClusterFirst", "serviceAccountName": "default", "serviceAccount": "default", "automountServiceAccountToken": false, "nodeName": "gke-example-default-pool-8139be1d-wmaj", "securityContext": {}, "imagePullSecrets": [ { "name": "credential-dockerhub-3" } ], "schedulerName": "default-scheduler", "tolerations": [ { "key": "sandbox.gke.io/runtime", "operator": "Exists" }, { "key": "node.kubernetes.io/not-ready", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 }, { "key": "node.kubernetes.io/unreachable", "operator": "Exists", "effect": "NoExecute", "tolerationSeconds": 300 } ], "priority": 0, "enableServiceLinks": true, "preemptionPolicy": "PreemptLowerPriority" }, "status": { "phase": "Running", "conditions": [ { "type": "Initialized", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2021-08-20T13:05:33Z" }, { "type": "Ready", "status": "False", "lastProbeTime": null, "lastTransitionTime": "2021-08-20T13:05:33Z", "reason": "ContainersNotReady", "message": "containers with unready status: [f03d008a48]" }, { "type": "ContainersReady", "status": "False", "lastProbeTime": null, "lastTransitionTime": "2021-08-20T13:05:33Z", "reason": "ContainersNotReady", "message": "containers with unready status: [f03d008a48]" }, { "type": "PodScheduled", "status": "True", "lastProbeTime": null, "lastTransitionTime": "2021-08-20T13:05:33Z" } ], "hostIP": "10.128.0.18", "podIP": "10.108.4.232", "podIPs": [ { "ip": "10.108.4.232" } ], "startTime": "2021-08-20T13:05:33Z", "containerStatuses": [ { "name": "f03d008a48", "state": { "running": { "startedAt": "2021-08-20T13:05:57Z" } }, "lastState": {}, "ready": false, "restartCount": 0, "image": "uffizzitest/webhooks-test-app:latest", "imageID": "docker-pullable://uffizzitest/webhooks-test-app@sha256:a1efdad4c909517ae9eecd1a362d6f30046822f8e47793d0211aa0d5a3cc2b16", "containerID": "docker://4b04f42cb9c9c24837b9ae4e458c57a31abebc2f03529447aae5350834185c80", "started": true } ], "qosClass": "Guaranteed" } } ] ================================================ FILE: core/test/fixtures/files/controller/deployment_containers_with_error.json ================================================ [ { "metadata": { "name": "app-deployment-1-54c7f5f884-xcn5h", "generate_name": "app-deployment-1-54c7f5f884-", "namespace": "deployment-1", "uid": "4992a195-4156-4aa0-a76a-43345621e7ff", "resource_version": "249176761", "creation_timestamp": "2022-12-05T18:11:04Z", "labels": { "app": "app-deployment-1", "app.kubernetes.io/managed_by": "uffizzi", "pod_template_hash": "54c7f5f884" }, "annotations": { "kubectl.kubernetes.io/restarted_at": "2022-12-05T18:11:03Z" }, "owner_references": [ { "api_version": "apps/v1", "kind": "ReplicaSet", "name": "app-deployment-1-54c7f5f884", "uid": "e7615d70-e805-423c-91f4-a6679b194365", "controller": true, "block_owner_deletion": true } ] }, "spec": { "containers": [ { "name": "f03d008a48", "image": "library/nginx:latest", "args": [ "exit" ], "ports": [ { "name": "default-port", "container_port": 80, "protocol": "TCP" } ], "env": [ { "name": "PORT", "value": "80" }, { "name": "UFFIZZI_URL", "value": "https://deployment-1-test-preview-jymy.localhost:7000" }, { "name": "UFFIZZI_DOMAIN", "value": "deployment-1-test-preview-jymy.localhost:7000" } ], "resources": { "limits": { "cpu": "1", "memory": "125Mi" }, "requests": { "cpu": "0", "memory": "125Mi" } }, "startup_probe": { "tcp_socket": { "port": 80 }, "initial_delay_seconds": 10, "timeout_seconds": 1, "period_seconds": 15, "success_threshold": 1, "failure_threshold": 80 }, "termination_message_path": "/dev/termination-log", "termination_message_policy": "File", "image_pull_policy": "Always" } ], "restart_policy": "Always", "termination_grace_period_seconds": 30, "dns_policy": "ClusterFirst", "node_selector": { "sandbox.gke.io/runtime": "gvisor" }, "service_account_name": "default", "service_account": "default", "automount_service_account_token": false, "node_name": "gke-uffizzi-client-sandbox-588e8350-tk79", "security_context": {}, "scheduler_name": "default-scheduler", "tolerations": [ { "key": "sandbox.gke.io/runtime", "operator": "Exists" }, { "key": "node.kubernetes.io/not-ready", "operator": "Exists", "effect": "NoExecute", "toleration_seconds": 300 }, { "key": "node.kubernetes.io/unreachable", "operator": "Exists", "effect": "NoExecute", "toleration_seconds": 300 } ], "priority": 0, "enable_service_links": true, "preemption_policy": "PreemptLowerPriority" }, "status": { "phase": "Running", "conditions": [ { "type": "Initialized", "status": "True", "last_probe_time": null, "last_transition_time": "2022-12-05T18:11:04Z" }, { "type": "Ready", "status": "False", "last_probe_time": null, "last_transition_time": "2022-12-05T18:11:04Z", "reason": "ContainersNotReady", "message": "containers with unready status: [f03d008a48]" }, { "type": "ContainersReady", "status": "False", "last_probe_time": null, "last_transition_time": "2022-12-05T18:11:04Z", "reason": "ContainersNotReady", "message": "containers with unready status: [f03d008a48]" }, { "type": "PodScheduled", "status": "True", "last_probe_time": null, "last_transition_time": "2022-12-05T18:11:04Z" } ], "host_ip": "10.128.0.49", "pod_ip": "10.20.6.38", "pod_i_ps": [ { "ip": "10.20.6.38" } ], "start_time": "2022-12-05T18:11:04Z", "container_statuses": [ { "name": "f03d008a48", "state": { "waiting": { "reason": "CrashLoopBackOff", "message": "back-off 40s restarting failed container=f03d008a48 pod=app-deployment-1-54c7f5f884-xcn5h_deployment-1(4992a195-4156-4aa0-a76a-43345621e7ff)" } }, "last_state": { "terminated": { "exit_code": 127, "reason": "Error", "started_at": "2022-12-05T18:11:46Z", "finished_at": "2022-12-05T18:11:46Z", "container_id": "containerd://f467da4f32e086c93a92a54917c054ef15e8ab84413b843495c12a277e94b2dd" } }, "ready": false, "restart_count": 3, "image": "docker.io/library/nginx:latest", "image_id": "docker.io/library/nginx@sha256:e209ac2f37c70c1e0e9873a5f7231e91dcd83fdf1178d8ed36c2ec09974210ba", "container_id": "containerd://f467da4f32e086c93a92a54917c054ef15e8ab84413b843495c12a277e94b2dd", "started": false } ], "qos_class": "Burstable" } } ] ================================================ FILE: core/test/fixtures/files/controller/deployments.json ================================================ { "metadata": { "name": "test-env-1", "uid": "e7edcd16-0708-4d84-946b-c56db6127916", "resourceVersion": "72171313", "creationTimestamp": "2021-08-20T13:05:25Z", "labels": { "app.kubernetes.io/managed-by": "uffizzi", "kind": "standard", "name": "test-env-1" }, "annotations": { "errors": "", "ingressName": "ingress-1629464776", "ingress_ip": "104.154.78.201", "network_connectivity": "", "serviceName": "service-1629464734" }, "managedFields": [ { "manager": "controller", "operation": "Update", "apiVersion": "v1", "time": "2021-08-20T13:06:59Z", "fieldsType": "FieldsV1", "fieldsV1": { "f:metadata": { "f:annotations": { ".": {}, "f:errors": {}, "f:ingressName": {}, "f:ingress_ip": {}, "f:network_connectivity": {}, "f:serviceName": {} }, "f:labels": { ".": {}, "f:app.kubernetes.io/managed-by": {}, "f:kind": {}, "f:name": {} } }, "f:status": { "f:phase": {} } } } ] }, "spec": { "finalizers": [ "kubernetes" ] }, "status": { "phase": "Active" } } ================================================ FILE: core/test/fixtures/files/controller/ingresses.json ================================================ { "metadata": { "resource_version": "63142264" }, "items": [ { "metadata": { "name": "result-x-default-x-uc-pr-82", "namespace": "c169515535810170", "uid": "a46d2a3c-f111-4bc8-8301-b8d51c668059", "resource_version": "62737159", "generation": 1, "creation_timestamp": "2023-09-19T20:30:07Z", "labels": { "vcluster.loft.sh/managed_by": "uc-pr-82", "vcluster.loft.sh/namespace": "default" }, "annotations": { "app.uffizzi.com/ingress_sync": "true", "kubectl.kubernetes.io/last_applied_configuration": "{\"apiVersion\":\"networking.k8s.io/v1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{},\"name\":\"result\",\"namespace\":\"default\"},\"spec\":{\"ingressClassName\":\"uffizzi\",\"rules\":[{\"host\":\"result.example.com\",\"http\":{\"paths\":[{\"backend\":{\"service\":{\"name\":\"result\",\"port\":{\"number\":5001}}},\"path\":\"/\",\"pathType\":\"Prefix\"}]}}],\"tls\":[{\"hosts\":[\"result.example.com\"]}]}}\n", "vcluster.loft.sh/managed_annotations": "app.uffizzi.com/ingress-sync\nkubectl.kubernetes.io/last-applied-configuration", "vcluster.loft.sh/object_name": "result", "vcluster.loft.sh/object_namespace": "default" }, "owner_references": [ { "api_version": "v1", "kind": "Service", "name": "uc-pr-82", "uid": "8c502e06-1e48-40bd-a36c-96ecfd7ac414" } ], "managed_fields": [ { "manager": "plugin", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T20:30:07Z", "fields_type": "FieldsV1", "fields_v1": { "f:metadata": { "f:annotations": { ".": {}, "f:app.uffizzi.com/ingress_sync": {}, "f:kubectl.kubernetes.io/last_applied_configuration": {}, "f:vcluster.loft.sh/managed_annotations": {}, "f:vcluster.loft.sh/object_name": {}, "f:vcluster.loft.sh/object_namespace": {} }, "f:labels": { ".": {}, "f:vcluster.loft.sh/managed_by": {}, "f:vcluster.loft.sh/namespace": {} }, "f:owner_references": { ".": {}, "k:{\"uid\":\"8c502e06_1e48_40bd_a36c_96ecfd7ac414\"}": {} } }, "f:spec": { "f:ingress_class_name": {}, "f:rules": {}, "f:tls": {} } } }, { "manager": "nginx-ingress-controller", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T22:14:31Z", "fields_type": "FieldsV1", "fields_v1": { "f:status": { "f:load_balancer": { "f:ingress": {} } } }, "subresource": "status" } ] }, "spec": { "ingress_class_name": "uffizzi", "tls": [ { "hosts": [ "result-default-pr-82-c169515535810170.uclusters.app.qa-gke.uffizzi.com" ] } ], "rules": [ { "host": "result-default-pr-82-c169515535810170.uclusters.app.qa-gke.uffizzi.com", "http": { "paths": [ { "path": "/", "path_type": "Prefix", "backend": { "service": { "name": "result-x-default-x-uc-pr-82", "port": { "number": 5001 } } } } ] } } ] }, "status": { "load_balancer": { "ingress": [ { "ip": "34.134.99.100" } ] } } }, { "metadata": { "name": "uc-pr-82", "namespace": "c169515535810170", "uid": "ac012e17-b76c-4630-bc76-2039d1ff6270", "resource_version": "62737153", "generation": 1, "creation_timestamp": "2023-09-19T20:29:21Z", "labels": { "app.kubernetes.io/managed_by": "Helm", "helm.toolkit.fluxcd.io/name": "uc-pr-82", "helm.toolkit.fluxcd.io/namespace": "c169515535810170" }, "annotations": { "app.uffizzi.com/ingress_sync": "true", "meta.helm.sh/release_name": "uc-pr-82", "meta.helm.sh/release_namespace": "c169515535810170", "nginx.ingress.kubernetes.io/backend_protocol": "HTTPS", "nginx.ingress.kubernetes.io/ssl_passthrough": "true", "nginx.ingress.kubernetes.io/ssl_redirect": "true" }, "managed_fields": [ { "manager": "helm-controller", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T20:29:21Z", "fields_type": "FieldsV1", "fields_v1": { "f:metadata": { "f:annotations": { ".": {}, "f:app.uffizzi.com/ingress_sync": {}, "f:meta.helm.sh/release_name": {}, "f:meta.helm.sh/release_namespace": {}, "f:nginx.ingress.kubernetes.io/backend_protocol": {}, "f:nginx.ingress.kubernetes.io/ssl_passthrough": {}, "f:nginx.ingress.kubernetes.io/ssl_redirect": {} }, "f:labels": { ".": {}, "f:app.kubernetes.io/managed_by": {}, "f:helm.toolkit.fluxcd.io/name": {}, "f:helm.toolkit.fluxcd.io/namespace": {} } }, "f:spec": { "f:rules": {} } } }, { "manager": "nginx-ingress-controller", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T22:14:30Z", "fields_type": "FieldsV1", "fields_v1": { "f:status": { "f:load_balancer": { "f:ingress": {} } } }, "subresource": "status" } ] }, "spec": { "ingress_class_name": "uffizzi", "rules": [ { "host": "pr-82-c169515535810170.uclusters.app.qa-gke.uffizzi.com", "http": { "paths": [ { "path": "/", "path_type": "ImplementationSpecific", "backend": { "service": { "name": "uc-pr-82", "port": { "name": "https" } } } } ] } } ] }, "status": { "load_balancer": { "ingress": [ { "ip": "34.134.99.100" } ] } } }, { "metadata": { "name": "vote-x-default-x-uc-pr-82", "namespace": "c169515535810170", "uid": "114b3dc2-6644-4d88-89f2-bbaa0b141787", "resource_version": "62737156", "generation": 1, "creation_timestamp": "2023-09-19T20:30:07Z", "labels": { "vcluster.loft.sh/managed_by": "uc-pr-82", "vcluster.loft.sh/namespace": "default" }, "annotations": { "app.uffizzi.com/ingress_sync": "true", "kubectl.kubernetes.io/last_applied_configuration": "{\"apiVersion\":\"networking.k8s.io/v1\",\"kind\":\"Ingress\",\"metadata\":{\"annotations\":{},\"name\":\"vote\",\"namespace\":\"default\"},\"spec\":{\"ingressClassName\":\"uffizzi\",\"rules\":[{\"host\":\"vote.example.com\",\"http\":{\"paths\":[{\"backend\":{\"service\":{\"name\":\"vote\",\"port\":{\"number\":5000}}},\"path\":\"/\",\"pathType\":\"Prefix\"}]}}],\"tls\":[{\"hosts\":[\"vote.example.com\"]}]}}\n", "vcluster.loft.sh/managed_annotations": "app.uffizzi.com/ingress-sync\nkubectl.kubernetes.io/last-applied-configuration", "vcluster.loft.sh/object_name": "vote", "vcluster.loft.sh/object_namespace": "default" }, "owner_references": [ { "api_version": "v1", "kind": "Service", "name": "uc-pr-82", "uid": "8c502e06-1e48-40bd-a36c-96ecfd7ac414" } ], "managed_fields": [ { "manager": "plugin", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T20:30:07Z", "fields_type": "FieldsV1", "fields_v1": { "f:metadata": { "f:annotations": { ".": {}, "f:app.uffizzi.com/ingress_sync": {}, "f:kubectl.kubernetes.io/last_applied_configuration": {}, "f:vcluster.loft.sh/managed_annotations": {}, "f:vcluster.loft.sh/object_name": {}, "f:vcluster.loft.sh/object_namespace": {} }, "f:labels": { ".": {}, "f:vcluster.loft.sh/managed_by": {}, "f:vcluster.loft.sh/namespace": {} }, "f:owner_references": { ".": {}, "k:{\"uid\":\"8c502e06_1e48_40bd_a36c_96ecfd7ac414\"}": {} } }, "f:spec": { "f:ingress_class_name": {}, "f:rules": {}, "f:tls": {} } } }, { "manager": "nginx-ingress-controller", "operation": "Update", "api_version": "networking.k8s.io/v1", "time": "2023-09-19T22:14:30Z", "fields_type": "FieldsV1", "fields_v1": { "f:status": { "f:load_balancer": { "f:ingress": {} } } }, "subresource": "status" } ] }, "spec": { "ingress_class_name": "uffizzi", "tls": [ { "hosts": [ "vote-default-pr-82-c169515535810170.uclusters.app.qa-gke.uffizzi.com" ] } ], "rules": [ { "host": "vote-default-pr-82-c169515535810170.uclusters.app.qa-gke.uffizzi.com", "http": { "paths": [ { "path": "/", "path_type": "Prefix", "backend": { "service": { "name": "vote-x-default-x-uc-pr-82", "port": { "number": 5000 } } } } ] } } ] }, "status": { "load_balancer": { "ingress": [ { "ip": "34.134.99.100" } ] } } } ] } ================================================ FILE: core/test/fixtures/files/controller/logs.json ================================================ { "logs": [ "2022-11-14T11:46:55.474292870Z /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" ] } ================================================ FILE: core/test/fixtures/files/dockerhub/digest.json ================================================ { "status": 200, "headers": { "content-length": 1574, "content-type": "application/vnd.docker.distribution.manifest.v2+json", "docker-content-digest": "sha256:cd0c68c5479f2db4b9e2c5fbfdb7a8acb77625322dd5b474578515422d3ddb59", "docker-distribution-api-version": "registry/2.0", "etag": "sha256:cd0c68c5479f2db4b9e2c5fbfdb7a8acb77625322dd5b474578515422d3ddb59", "date": "Sun, 22 Aug 2021 10:11:10 GMT", "strict-transport-security": "max-age=31536000", "ratelimit-limit": "100;w=21600", "ratelimit-remaining": "100;w=21600" }, "body": { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 7699, "digest": "sha256:ddcca4b8a6f0367b5de2764dfe76b0a4bfa6d75237932185923705da47004347" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 27145985, "digest": "sha256:e1acddbe380c63f0de4b77d3f287b7c81cd9d89563a230692378126b46ea6546" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 1732, "digest": "sha256:a31098369fccd4adaac914a87e5def0f96dc1dbc385921d38db1f1f4cef8242a" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 1417929, "digest": "sha256:4a49b0eba86d37bb4fa4d1a9f8826a1222ea4eb7717192674d79341a998febf8" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 10116853, "digest": "sha256:fddf1399eface27528e89bb5296dc40fbabe7c6b2186c0fca6dce96ae9c5d9a1" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 134, "digest": "sha256:5c6658b59b7200ac5a9ec19f428dc5739a95ea2f6965715443692bff6da9a79d" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 408, "digest": "sha256:0b88638a5b778b573dd7c4e3fda17cab7d4b229defd7943a07ac982c65e98cbb" } ] } } ================================================ FILE: core/test/fixtures/files/dockerhub/login_fail.json ================================================ { "status": 401, "body": "{\"detail\":\"Incorrect authentication credentials\"}\n\n", "response_headers": { "date": "Thu, 17 Mar 2022 09:49:37 GMT", "transfer-encoding": "chunked", "server": "nginx", "x-frame-options": "deny", "x-content-type-options": "nosniff", "x-xss-protection": "1; mode=block", "strict-transport-security": "max-age=31536000" } } ================================================ FILE: core/test/fixtures/files/dockerhub/repository.json ================================================ { "user":"library", "name":"nginx", "namespace":"library", "repository_type":"image", "status":1, "description":"Official build of Nginx.", "is_private":false, "is_automated":false, "can_edit":false, "star_count":17333, "pull_count":6917945936, "last_updated":"2022-08-23T22:01:16.714365Z", "date_registered":"2014-06-05T19:14:45Z", "collaborator_count":0, "affiliation":null, "hub_user":"stackbrew", "has_starred":false, "full_description":"# Quick reference\n\n-\t**Maintained by**: \n\t[the NGINX Docker Maintainers](https://github.com/nginxinc/docker-nginx)\n\n-\t**Where to get help**: \n\t[the Docker Community Forums](https://forums.docker.com/), [the Docker Community Slack](https://dockr.ly/slack), or [Stack Overflow](https://stackoverflow.com/search?tab=newest\u0026q=docker)\n\n# Supported tags and respective `Dockerfile` links\n\n-\t[`1.23.1`, `mainline`, `1`, `1.23`, `latest`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/mainline/debian/Dockerfile)\n-\t[`1.23.1-perl`, `mainline-perl`, `1-perl`, `1.23-perl`, `perl`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/mainline/debian-perl/Dockerfile)\n-\t[`1.23.1-alpine`, `mainline-alpine`, `1-alpine`, `1.23-alpine`, `alpine`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/mainline/alpine/Dockerfile)\n-\t[`1.23.1-alpine-perl`, `mainline-alpine-perl`, `1-alpine-perl`, `1.23-alpine-perl`, `alpine-perl`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/mainline/alpine-perl/Dockerfile)\n-\t[`1.22.0`, `stable`, `1.22`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/stable/debian/Dockerfile)\n-\t[`1.22.0-perl`, `stable-perl`, `1.22-perl`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/stable/debian-perl/Dockerfile)\n-\t[`1.22.0-alpine`, `stable-alpine`, `1.22-alpine`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/stable/alpine/Dockerfile)\n-\t[`1.22.0-alpine-perl`, `stable-alpine-perl`, `1.22-alpine-perl`](https://github.com/nginxinc/docker-nginx/blob/f3d86e99ba2db5d9918ede7b094fcad7b9128cd8/stable/alpine-perl/Dockerfile)\n\n# Quick reference (cont.)\n\n-\t**Where to file issues**: \n\t[https://github.com/nginxinc/docker-nginx/issues](https://github.com/nginxinc/docker-nginx/issues)\n\n-\t**Supported architectures**: ([more info](https://github.com/docker-library/official-images#architectures-other-than-amd64)) \n\t[`amd64`](https://hub.docker.com/r/amd64/nginx/), [`arm32v5`](https://hub.docker.com/r/arm32v5/nginx/), [`arm32v6`](https://hub.docker.com/r/arm32v6/nginx/), [`arm32v7`](https://hub.docker.com/r/arm32v7/nginx/), [`arm64v8`](https://hub.docker.com/r/arm64v8/nginx/), [`i386`](https://hub.docker.com/r/i386/nginx/), [`mips64le`](https://hub.docker.com/r/mips64le/nginx/), [`ppc64le`](https://hub.docker.com/r/ppc64le/nginx/), [`s390x`](https://hub.docker.com/r/s390x/nginx/)\n\n-\t**Published image artifact details**: \n\t[repo-info repo's `repos/nginx/` directory](https://github.com/docker-library/repo-info/blob/master/repos/nginx) ([history](https://github.com/docker-library/repo-info/commits/master/repos/nginx)) \n\t(image metadata, transfer size, etc)\n\n-\t**Image updates**: \n\t[official-images repo's `library/nginx` label](https://github.com/docker-library/official-images/issues?q=label%3Alibrary%2Fnginx) \n\t[official-images repo's `library/nginx` file](https://github.com/docker-library/official-images/blob/master/library/nginx) ([history](https://github.com/docker-library/official-images/commits/master/library/nginx))\n\n-\t**Source of this description**: \n\t[docs repo's `nginx/` directory](https://github.com/docker-library/docs/tree/master/nginx) ([history](https://github.com/docker-library/docs/commits/master/nginx))\n\n# What is nginx?\n\nNginx (pronounced \"engine-x\") is an open source reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer, HTTP cache, and a web server (origin server). The nginx project started with a strong focus on high concurrency, high performance and low memory usage. It is licensed under the 2-clause BSD-like license and it runs on Linux, BSD variants, Mac OS X, Solaris, AIX, HP-UX, as well as on other *nix flavors. It also has a proof of concept port for Microsoft Windows.\n\n\u003e [wikipedia.org/wiki/Nginx](https://en.wikipedia.org/wiki/Nginx)\n\n![logo](https://raw.githubusercontent.com/docker-library/docs/01c12653951b2fe592c1f93a13b4e289ada0e3a1/nginx/logo.png)\n\n# How to use this image\n\n## Hosting some simple static content\n\n```console\n$ docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx\n```\n\nAlternatively, a simple `Dockerfile` can be used to generate a new image that includes the necessary content (which is a much cleaner solution than the bind mount above):\n\n```dockerfile\nFROM nginx\nCOPY static-html-directory /usr/share/nginx/html\n```\n\nPlace this file in the same directory as your directory of content (\"static-html-directory\"), run `docker build -t some-content-nginx .`, then start your container:\n\n```console\n$ docker run --name some-nginx -d some-content-nginx\n```\n\n## Exposing external port\n\n```console\n$ docker run --name some-nginx -d -p 8080:80 some-content-nginx\n```\n\nThen you can hit `http://localhost:8080` or `http://host-ip:8080` in your browser.\n\n## Complex configuration\n\n```console\n$ docker run --name my-custom-nginx-container -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx\n```\n\nFor information on the syntax of the nginx configuration files, see [the official documentation](http://nginx.org/en/docs/) (specifically the [Beginner's Guide](http://nginx.org/en/docs/beginners_guide.html#conf_structure)).\n\nIf you wish to adapt the default configuration, use something like the following to copy it from a running nginx container:\n\n```console\n$ docker run --name tmp-nginx-container -d nginx\n$ docker cp tmp-nginx-container:/etc/nginx/nginx.conf /host/path/nginx.conf\n$ docker rm -f tmp-nginx-container\n```\n\nThis can also be accomplished more cleanly using a simple `Dockerfile` (in `/host/path/`):\n\n```dockerfile\nFROM nginx\nCOPY nginx.conf /etc/nginx/nginx.conf\n```\n\nIf you add a custom `CMD` in the Dockerfile, be sure to include `-g daemon off;` in the `CMD` in order for nginx to stay in the foreground, so that Docker can track the process properly (otherwise your container will stop immediately after starting)!\n\nThen build the image with `docker build -t custom-nginx .` and run it as follows:\n\n```console\n$ docker run --name my-custom-nginx-container -d custom-nginx\n```\n\n### Using environment variables in nginx configuration (new in 1.19)\n\nOut-of-the-box, nginx doesn't support environment variables inside most configuration blocks. But this image has a function, which will extract environment variables before nginx starts.\n\nHere is an example using docker-compose.yml:\n\n```yaml\nweb:\n image: nginx\n volumes:\n - ./templates:/etc/nginx/templates\n ports:\n - \"8080:80\"\n environment:\n - NGINX_HOST=foobar.com\n - NGINX_PORT=80\n```\n\nBy default, this function reads template files in `/etc/nginx/templates/*.template` and outputs the result of executing `envsubst` to `/etc/nginx/conf.d`.\n\nSo if you place `templates/default.conf.template` file, which contains variable references like this:\n\n\tlisten ${NGINX_PORT};\n\noutputs to `/etc/nginx/conf.d/default.conf` like this:\n\n\tlisten 80;\n\nThis behavior can be changed via the following environment variables:\n\n-\t`NGINX_ENVSUBST_TEMPLATE_DIR`\n\t-\tA directory which contains template files (default: `/etc/nginx/templates`)\n\t-\tWhen this directory doesn't exist, this function will do nothing about template processing.\n-\t`NGINX_ENVSUBST_TEMPLATE_SUFFIX`\n\t-\tA suffix of template files (default: `.template`)\n\t-\tThis function only processes the files whose name ends with this suffix.\n-\t`NGINX_ENVSUBST_OUTPUT_DIR`\n\t-\tA directory where the result of executing envsubst is output (default: `/etc/nginx/conf.d`)\n\t-\tThe output filename is the template filename with the suffix removed.\n\t\t-\tex.) `/etc/nginx/templates/default.conf.template` will be output with the filename `/etc/nginx/conf.d/default.conf`.\n\t-\tThis directory must be writable by the user running a container.\n\n## Running nginx in read-only mode\n\nTo run nginx in read-only mode, you will need to mount a Docker volume to every location where nginx writes information. The default nginx configuration requires write access to `/var/cache` and `/var/run`. This can be easily accomplished by running nginx as follows:\n\n```console\n$ docker run -d -p 80:80 --read-only -v $(pwd)/nginx-cache:/var/cache/nginx -v $(pwd)/nginx-pid:/var/run nginx\n```\n\nIf you have a more advanced configuration that requires nginx to write to other locations, simply add more volume mounts to those locations.\n\n## Running nginx in debug mode\n\nImages since version 1.9.8 come with `nginx-debug` binary that produces verbose output when using higher log levels. It can be used with simple CMD substitution:\n\n```console\n$ docker run --name my-nginx -v /host/path/nginx.conf:/etc/nginx/nginx.conf:ro -d nginx nginx-debug -g 'daemon off;'\n```\n\nSimilar configuration in docker-compose.yml may look like this:\n\n```yaml\nweb:\n image: nginx\n volumes:\n - ./nginx.conf:/etc/nginx/nginx.conf:ro\n command: [nginx-debug, '-g', 'daemon off;']\n```\n\n## Entrypoint quiet logs\n\nSince version 1.19.0, a verbose entrypoint was added. It provides information on what's happening during container startup. You can silence this output by setting environment variable `NGINX_ENTRYPOINT_QUIET_LOGS`:\n\n```console\n$ docker run -d -e NGINX_ENTRYPOINT_QUIET_LOGS=1 nginx\n```\n\n## User and group id\n\nSince 1.17.0, both alpine- and debian-based images variants use the same user and group ids to drop the privileges for worker processes:\n\n```console\n$ id\nuid=101(nginx) gid=101(nginx) groups=101(nginx)\n```\n\n## Running nginx as a non-root user\n\nIt is possible to run the image as a less privileged arbitrary UID/GID. This, however, requires modification of nginx configuration to use directories writeable by that specific UID/GID pair:\n\n```console\n$ docker run -d -v $PWD/nginx.conf:/etc/nginx/nginx.conf nginx\n```\n\nwhere nginx.conf in the current directory should have the following directives re-defined:\n\n```nginx\npid /tmp/nginx.pid;\n```\n\nAnd in the http context:\n\n```nginx\nhttp {\n client_body_temp_path /tmp/client_temp;\n proxy_temp_path /tmp/proxy_temp_path;\n fastcgi_temp_path /tmp/fastcgi_temp;\n uwsgi_temp_path /tmp/uwsgi_temp;\n scgi_temp_path /tmp/scgi_temp;\n...\n}\n```\n\nAlternatively, check out the official [Docker NGINX unprivileged image](https://hub.docker.com/r/nginxinc/nginx-unprivileged).\n\n# Image Variants\n\nThe `nginx` images come in many flavors, each designed for a specific use case.\n\n## `nginx:\u003cversion\u003e`\n\nThis is the defacto image. If you are unsure about what your needs are, you probably want to use this one. It is designed to be used both as a throw away container (mount your source code and start the container to start your app), as well as the base to build other images off of.\n\n## `nginx:\u003cversion\u003e-perl` / `nginx:\u003cversion\u003e-alpine-perl`\n\nStarting with nginx:1.13.0 / mainline and nginx:1.12.0 / stable, the perl module has been removed from the default images. A separate `-perl` tag variant is available if you wish to use the perl module.\n\n## `nginx:\u003cversion\u003e-alpine`\n\nThis image is based on the popular [Alpine Linux project](https://alpinelinux.org), available in [the `alpine` official image](https://hub.docker.com/_/alpine). Alpine Linux is much smaller than most distribution base images (~5MB), and thus leads to much slimmer images in general.\n\nThis variant is useful when final image size being as small as possible is your primary concern. The main caveat to note is that it does use [musl libc](https://musl.libc.org) instead of [glibc and friends](https://www.etalabs.net/compare_libcs.html), so software will often run into issues depending on the depth of their libc requirements/assumptions. See [this Hacker News comment thread](https://news.ycombinator.com/item?id=10782897) for more discussion of the issues that might arise and some pro/con comparisons of using Alpine-based images.\n\nTo minimize image size, it's uncommon for additional related tools (such as `git` or `bash`) to be included in Alpine-based images. Using this image as a base, add the things you need in your own Dockerfile (see the [`alpine` image description](https://hub.docker.com/_/alpine/) for examples of how to install packages if you are unfamiliar).\n\n# License\n\nView [license information](http://nginx.org/LICENSE) for the software contained in this image.\n\nAs with all Docker images, these likely also contain other software which may be under other licenses (such as Bash, etc from the base distribution, along with any direct or indirect dependencies of the primary software being contained).\n\nSome additional license information which was able to be auto-detected might be found in [the `repo-info` repository's `nginx/` directory](https://github.com/docker-library/repo-info/tree/master/repos/nginx).\n\nAs for any pre-built image usage, it is the image user's responsibility to ensure that any use of this image complies with any relevant licenses for all software contained within.", "permissions":{ "read":true, "write":false, "admin":false }, "media_types":[ "application/vnd.docker.distribution.manifest.list.v2+json" ] } ================================================ FILE: core/test/fixtures/files/dockerhub/webhooks/push/event_data.json ================================================ { "push_data": { "pushed_at": 1613134095, "images": [], "tag": "latest", "pusher": "uffizzitest" }, "callback_url": "https://registry.hub.docker.com/u/uffizzitest/webhooks-test-app/hook/2i4i3icb013ec45fffigg200jaf2hjf50/", "repository": { "status": "Active", "description": "", "is_trusted": false, "full_description": "", "repo_url": "https://hub.docker.com/r/uffizzitest/webhooks-test-app", "owner": "uffizzitest", "is_official": false, "is_private": false, "name": "webhooks-test-app", "namespace": "uffizzitest", "star_count": 0, "comment_count": 0, "date_created": 1613133268, "repo_name": "uffizzitest/webhooks-test-app" }, "webhook": { "push_data": { "pushed_at": 1613134095, "images": [ ], "tag": "latest", "pusher": "uffizzitest" }, "callback_url": "https://registry.hub.docker.com/u/uffizzitest/webhooks-test-app/hook/2i4i3icb013ec45fffigg200jaf2hjf50/", "repository": { "status": "Active", "description": "", "is_trusted": false, "full_description": "", "repo_url": "https://hub.docker.com/r/uffizzitest/webhooks-test-app", "owner": "uffizzitest", "is_official": false, "is_private": false, "name": "webhooks-test-app", "namespace": "uffizzitest", "star_count": 0, "comment_count": 0, "date_created": 1613133268, "repo_name": "uffizzitest/webhooks-test-app" } } } ================================================ FILE: core/test/fixtures/files/github/compose_files/hello_world_compose_github_container_registry.json ================================================ { "name": "uffizzi-compose-github-container-registry.yml.yml", "path": "uffizzi-compose-github-container-registry.yml.yml", "sha": "401853dba1c7299da89c6593d9b8347cf6442b1a", "size": 141, "url": "https://api.github.com/repos/ACCOUNT/hello-world/contents/uffizzi-compose-github-container-registry.yml.yml?ref=compose", "html_url": "https://github.com/ACCOUNT/hello-world/blob/compose/uffizzi-compose-github-container-registry.yml.yml", "git_url": "https://api.github.com/repos/ACCOUNT/hello-world/git/blobs/401853dba1c7299da89c6593d9b8347cf6442b1a", "download_url": "https://raw.githubusercontent.com/ACCOUNT/hello-world/compose/uffizzi-compose-github-container-registry.yml.yml", "type": "file", "content": "c2VydmljZXM6CiAgaGVsbG8td29ybGQtYToKICAgIGltYWdlOiBnaGNyLmlv\nL3VmZml6emkvdGVzdC1jb21wb3NlOmxhdGVzdAoKeC11ZmZpenppLWluZ3Jl\nc3M6CiAgc2VydmljZTogaGVsbG8td29ybGQtYQogIHBvcnQ6IDgwCgp4LXVm\nZml6emktY29udGludW91c19wcmV2aWV3OgogIGRlcGxveV9wcmV2aWV3X3do\nZW5fcHVsbF9yZXF1ZXN0X2lzX29wZW5lZDogdHJ1ZQogIGRlbGV0ZV9wcmV2\naWV3X3doZW5fcHVsbF9yZXF1ZXN0X2lzX2Nsb3NlZDogdHJ1ZQogIGRlbGV0\nZV9wcmV2aWV3X2FmdGVyOiAxMGgKICBzaGFyZV90b19naXRodWI6IHRydWUK\nICBkZXBsb3lfcHJldmlld193aGVuX2ltYWdlX3RhZ19pc19jcmVhdGVkOiB0\ncnVlCiAgZGVsZXRlX3ByZXZpZXdfd2hlbl9pbWFnZV90YWdfaXNfdXBkYXRl\nZDogdHJ1ZQ==\n", "encoding": "base64", "_links": { "self": "https://api.github.com/repos/ACCOUNT/hello-world/contents/uffizzi-compose-github-container-registry.yml.yml?ref=compose", "git": "https://api.github.com/repos/ACCOUNT/hello-world/git/blobs/401853dba1c7299da89c6593d9b8347cf6442b1a", "html": "https://github.com/ACCOUNT/hello-world/blob/compose/uffizzi-compose-github-container-registry.yml.yml" } } ================================================ FILE: core/test/fixtures/files/test-compose-full.yml ================================================ services: app: image: uffizzicloud/app env_file: - local.env - ./env_files/env_file.env configs: - source: app_conf target: /etc/nginxz volumes: - ./some_app_dir:/var/app/some_dir - ./files/some_app_file:/var/app/some_app_files - ./:/var/entire_app - app_share:/some_app_share:ro - /some_anonymous_dir db: image: postgres:latest volumes: - source: ./some_app_dir target: /var/db/some_dir_2 - source: ./some_app_dir target: /var/db/some_dir_2 - source: ./some_db_dir target: /var/db/some_dir_3 - source: ./some_db_file target: /var/db/some_db_files - source: db_share target: /some_db_share read_only: true nginx: image: nginx:1.32 configs: app_conf: file: ./app.conf default_conf: file: config_files/config_file.conf volumes: app_share: db_share: x-uffizzi: ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/test-compose-success-jfrog.yml ================================================ services: webhooks-test-app: image: elnealo.jfrog.io/uffizzi-test-docker/webhook-test-app environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres x-uffizzi-ingress: service: webhooks-test-app port: 80 x-uffizzi-continuous-preview: delete_preview_after: 1h delete_preview_when_image_tag_is_updated: true deploy_preview_when_image_tag_is_created: true ================================================ FILE: core/test/fixtures/files/test-compose-success-without-dependencies.yml ================================================ services: app: image: uffizzicloud/app db: image: postgres:latest nginx: image: nginx:1.32 x-uffizzi: ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/test-compose-success.yml ================================================ services: hello-world: image: redis env_file: - local.env - ./env_files/env_file.env configs: - source: vote_conf target: /etc/nginxz volumes: - share_data:/var/cache - share_data_2:/var/cache_2:ro - /var/some_dir configs: vote_conf: file: ./vote.conf defaulf_conf: file: config_files/config_file.conf volumes: share_data: share_data_2: x-uffizzi: ingress: service: hello-world port: 80 additional_subdomains: - "example" - "example-hostname" ================================================ FILE: core/test/fixtures/files/test-compose-without-images.yml ================================================ services: hello-world: env_file: - local.env - ./env_files/env_file.env configs: - source: vote_conf target: /etc/nginxz configs: vote_conf: file: ./vote.conf defaulf_conf: file: config_files/config_file.conf ingress: service: hello-world port: 80 ================================================ FILE: core/test/fixtures/files/uffizzi-compose-amazon.yml ================================================ services: hello-world-a: image: 323707565364.dkr.ecr.us-east-1.amazonaws.com/test-compose:latest x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-azure.yml ================================================ services: hello-world-a: image: account.azurecr.io/test-compose:latest x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-docker-registry-anonymous.yml ================================================ services: hello-world-a: image: ttl.sh/abc:1h x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-dockerhub.yml ================================================ services: hello-world-a: image: project1/test-compose:latest x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-ghcr.yml ================================================ services: hello-world-a: image: ghcr.io/project1/test-compose:latest x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-google.yml ================================================ services: hello-world-a: image: gcr.io/project1/test-compose:latest x-uffizzi-ingress: service: hello-world-a port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-invalid-service-name.yml ================================================ services: hello_world: image: project1/test-compose:latest x-uffizzi-ingress: service: hello_world port: 80 x-uffizzi-continuous_preview: deploy_preview_when_pull_request_is_opened: true delete_preview_when_pull_request_is_closed: true delete_preview_after: 10h share_to_github: true deploy_preview_when_image_tag_is_created: true delete_preview_when_image_tag_is_updated: true ================================================ FILE: core/test/fixtures/files/uffizzi-compose-vote-app-docker-with-memory-request.yml ================================================ services: redis: image: redis:latest postgres: image: postgres:9.6 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres nginx: image: nginx:latest deploy: resources: limits: memory: 4000M configs: - source: vote_conf target: /etc/nginx/conf.d entrypoint: /usr/sbin/nginx-debug command: - "-g" - "daemon off;" worker: image: uffizzicloud/example-worker:latest deploy: resources: limits: memory: 4000M vote: image: uffizzicloud/example-vote:latest deploy: resources: limits: memory: 4000M result: image: uffizzicloud/example-result:latest x-uffizzi-ingress: service: nginx port: 8080 configs: vote_conf: file: configs/vote.conf defaulf_conf: file: config_files/config_file.conf ================================================ FILE: core/test/fixtures/files/uffizzi-compose-vote-app-docker.yml ================================================ services: redis: image: redis:latest postgres: image: postgres:9.6 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres nginx: image: nginx:latest configs: - source: vote_conf target: /etc/nginx/conf.d entrypoint: /usr/sbin/nginx-debug command: - "-g" - "daemon off;" worker: image: uffizzicloud/example-worker:latest deploy: resources: limits: memory: 250M vote: image: uffizzicloud/example-vote:latest result: image: uffizzicloud/example-result:latest x-uffizzi-ingress: service: nginx port: 8080 configs: vote_conf: file: configs/vote.conf defaulf_conf: file: config_files/config_file.conf ================================================ FILE: core/test/fixtures/files/uffizzi-compose-vote-app-with-command-as-string.yml ================================================ services: postgres: image: postgres:12 command: postgres -c jit=off restart: always ports: - "5432:5432" environment: POSTGRES_PASSWORD: postgrespassword nginx: image: nginx:1.32 command: - "-g" - "daemon off;" x-uffizzi: ingress: service: nginx port: 80 ================================================ FILE: core/test/fixtures/files/uffizzi-compose-with-host-volumes.yml ================================================ services: web: image: nginx volumes: - ./share_dir:/var/share_dir x-uffizzi-ingress: service: web port: 80 ================================================ FILE: core/test/fixtures/files/uffizzi-compose-with_alias.yml ================================================ x-nginx-configs: &nginx-configs - source: vote_conf target: /etc/nginx/conf.d x-srv-nginx: &srv-nginx nginx: image: nginx:latest configs: *nginx-configs entrypoint: /usr/sbin/nginx-debug command: - "-g" - "daemon off;" services: <<: *srv-nginx x-uffizzi-ingress: service: nginx port: 8080 configs: vote_conf: file: configs/vote.conf defaulf_conf: file: config_files/config_file.conf ================================================ FILE: core/test/services/activity_item_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::ActivityItemServiceTest < ActiveSupport::TestCase setup do Sidekiq::Testing.fake! Sidekiq::Worker.clear_all @user = create(:user, :with_personal_account) @project = create(:project, account: @user.personal_account) @deployment = create(:deployment, project: @project) end teardown do Sidekiq::Testing.inline! end test '#manage_deploy_activity_item - pending resource availability' do repo = create(:repo, :docker_hub, project: @project) container = create(:container, :with_public_port, deployment: @deployment, repo: repo, image: 'library/nginx') namespace, name = container.image.split('/') activity_item = create(:activity_item, :docker, :with_deploying_event, namespace: namespace, name: name, tag: container.tag, container: container, deployment: @deployment) namespace = UffizziCore::Converters.deep_lower_camelize_keys( { metadata: { annotations: { network_connectivity: { containers: { container.id => { service: { status: :pending, }, }, }, }.to_json, }, }, }, ) stubbed_controller_get_namespace_request = stub_controller_get_namespace_request(@deployment, namespace) pod_name = generate(:name) deployed_at = Time.current current_time = (deployed_at - 25.minute).iso8601 node_name = generate(:name) restart_count = 3 pods = UffizziCore::Converters.deep_lower_camelize_keys( [ { metadata: { name: pod_name, creation_timestamp: current_time, }, spec: { node_name: node_name, }, status: { container_statuses: [ { name: UffizziCore::ContainerService.pod_name(container), restart_count: restart_count, image: container.image_name, state: { running: { started_at: current_time, }, }, }, ], }, }, ], ) stubbed_controller_containers_request = stub_controller_containers_request(@deployment, pods) differences = { -> { UffizziCore::Event.count } => 0, -> { UffizziCore::Deployment::ManageDeployActivityItemJob.jobs.size } => 1, } assert_difference differences do UffizziCore::ActivityItemService.manage_deploy_activity_item(activity_item) assert_requested stubbed_controller_get_namespace_request assert_requested stubbed_controller_containers_request end end test '#manage_deploy_activity_item - resource is unavailable' do repo = create(:repo, :docker_hub, project: @project) container = create(:container, :with_public_port, deployment: @deployment, repo: repo, image: 'library/nginx') namespace, name = container.image.split('/') activity_item = create(:activity_item, :with_building_event, :docker, namespace: namespace, name: name, tag: container.tag, container: container, deployment: @deployment) domain_name = generate(:kubernetes_name) namespace = UffizziCore::Converters.deep_lower_camelize_keys( { metadata: { annotations: { network_connectivity: { containers: { container.id => { service: { ip: '127.0.0.1', status: :failed, port: container.port, domain_name: domain_name, }, }, }, }.to_json, }, }, }, ) stubbed_controller_get_namespace_request = stub_controller_get_namespace_request(@deployment, namespace) pod_name = generate(:name) deployed_at = Time.current current_time = (deployed_at - 25.minute).iso8601 node_name = generate(:name) restart_count = 3 pods = UffizziCore::Converters.deep_lower_camelize_keys( [ { metadata: { name: "#{Settings.controller.namespace_prefix}#{pod_name}", creation_timestamp: current_time, }, spec: { node_name: node_name, }, status: { container_statuses: [ { name: UffizziCore::ContainerService.pod_name(container), restart_count: restart_count, image: container.image_name, state: { terminated: {}, }, }, ], }, }, ], ) stubbed_controller_containers_request = stub_controller_containers_request(@deployment, pods) differences = { -> { UffizziCore::Event.count } => 1, -> { UffizziCore::Event.with_state(UffizziCore::Event.state.failed).count } => 1, -> { UffizziCore::Deployment::ManageDeployActivityItemJob.jobs.size } => 0, } assert_difference differences do UffizziCore::ActivityItemService.manage_deploy_activity_item(activity_item) assert_requested stubbed_controller_get_namespace_request assert_requested stubbed_controller_containers_request end end test '#manage_deploy_activity_item - successfully deployed resources' do repo = create(:repo, :docker_hub, project: @project) container = create(:container, :with_public_port, deployment: @deployment, repo: repo, image: 'library/nginx') namespace, name = container.image.split('/') activity_item = create(:activity_item, :with_deploying_event, :docker, namespace: namespace, name: name, tag: container.tag, container: container, deployment: @deployment) domain_name = generate(:kubernetes_name) namespace = UffizziCore::Converters.deep_lower_camelize_keys( { metadata: { annotations: { network_connectivity: { containers: { container.id => { service: { ip: '127.0.0.1', status: :success, port: container.port, domain_name: domain_name, }, }, }, }.to_json, }, }, }, ) stubbed_controller_get_namespace_request = stub_controller_get_namespace_request(@deployment, namespace) pod_name = generate(:name) deployed_at = Time.current current_time = (deployed_at - 25.minute).iso8601 node_name = generate(:name) restart_count = 0 pods = UffizziCore::Converters.deep_lower_camelize_keys( [ { metadata: { name: "#{Settings.controller.namespace_prefix}#{pod_name}", creation_timestamp: current_time, }, spec: { node_name: node_name, }, status: { container_statuses: [ { name: UffizziCore::ContainerService.pod_name(container), restart_count: restart_count, image: container.image_name, state: { running: { started_at: current_time, }, }, }, ], }, }, ], ) stubbed_controller_containers_request = stub_controller_containers_request(@deployment, pods) differences = { -> { UffizziCore::Event.count } => 1, -> { UffizziCore::Event.with_state(UffizziCore::Event.state.deployed).count } => 1, -> { UffizziCore::Deployment::ManageDeployActivityItemJob.jobs.size } => 0, } assert_difference differences do UffizziCore::ActivityItemService.manage_deploy_activity_item(activity_item) assert_requested stubbed_controller_get_namespace_request assert_requested stubbed_controller_containers_request end end end ================================================ FILE: core/test/services/compose_file_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::ComposeFileServiceTest < ActiveSupport::TestCase setup do @user = create(:user, :with_personal_account) @account = @user.personal_account @project = create(:project, account: @account) end test '#parse - check parse services with continuous preview' do content = file_fixture('files/compose_files/compose_with_continuous_preview.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) assert_equal(2, parsed_data[:containers].count) refute_empty(parsed_data[:continuous_preview]) refute_empty(parsed_data[:containers][1][:'x-uffizzi-continuous-preview']) end test '#parse - check invalid service name' do content = file_fixture('files/compose_files/invalid_service.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Invalid config option', e.message) end test '#parse - check parse docker image' do content = file_fixture('files/compose_files/dockerhub_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) assert_equal(1, parsed_data[:containers].count) refute_empty(parsed_data[:containers].first[:image]) end test '#parse - check parse docker image without tag' do content = file_fixture('files/compose_files/dockerhub_services/nginx_without_tag.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) container = parsed_data[:containers].first image = container[:image] tag = image[:tag] assert_equal(Settings.compose.default_tag, tag) end test '#parse - check parse env variables from an environment key' do content = file_fixture('files/compose_files/dockerhub_services/nginx_envs.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) content_data = YAML.safe_load(content) variables = parsed_data[:containers].first[:environment] assert_equal(variables.count, content_data['services']['hello-world']['environment'].keys.count) end test '#parse - check invalid ingress service' do content = file_fixture('files/compose_files/dockerhub_services/nginx_config_invalid_ingress_service.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Invalid ingress service', e.message) end test '#parse - check if an ingress service not found' do content = file_fixture('files/compose_files/dockerhub_services/nginx_without_ingress_service.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Ingress service not found', e.message) end test '#parse - check if an ingress port not specified' do content = file_fixture('files/compose_files/dockerhub_services/nginx_without_ingress_port.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Ingress port not specified', e.message) end test '#parse - check if an ingress port is not Integer' do content = file_fixture('files/compose_files/dockerhub_services/nginx_ingress_port_non_integer.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('should be an Integer type', e.message) end test '#parse - check compose without ingress' do content = file_fixture('files/compose_files/dockerhub_services/nginx_without_ingress.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Service ingress has not been defined', e.message) end test '#parse - check compose with ingress without x-uffizzi' do content = file_fixture('files/compose_files/dockerhub_services/nginx_with_ingress_without_x-uffizzi.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Service ingress has not been defined', e.message) end test '#parse - check compose with continuous_preview on top level without x-uffizzi' do content = file_fixture('files/compose_files/dockerhub_services/nginx_with_continuous_preview_without_x-uffizzi.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) assert_empty(parsed_data[:continuous_preview]) end test '#parse - check compose with continuous_preview on service level without x-uffizzi' do content = file_fixture('files/compose_files/dockerhub_services/nginx_with_continuous_preview_without_x-uffizzi.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) refute(parsed_data[:containers][0][:'x-uffizzi-continuous-preview']) end test '#parse - check compose without services' do content = file_fixture('files/compose_files/compose_without_services.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('At least one must be provided', e.message) end test '#parse - check if a port is out of acceptable range' do content = file_fixture('files/compose_files/dockerhub_services/nginx_invalid_port.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("Port should be specified between #{Settings.compose.port_min_value} - #{Settings.compose.port_max_value}", e.message) end test '#parse - check if a memory has invalid postfix' do content = file_fixture('files/compose_files/dockerhub_services/nginx_invalid_memory_postfix.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('The postfix should be one of', e.message) end test '#parse - check if a memory has invalid type' do content = file_fixture('files/compose_files/dockerhub_services/nginx_invalid_memory_type.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('it should be a string', e.message) end test '#parse - check if image and build data no specified' do content = file_fixture('files/compose_files/compose_without_image.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('At least one must be provided', e.message) end test '#parse - check if delete_preview_after has invalid postfix' do content = file_fixture('files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_postfix.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('value should be `h`', e.message) end test '#parse - check if delete_preview_after has invalid value' do content = file_fixture('files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_hours.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('should be an Integer type', e.message) end test '#parse - check if delete_preview_after less than min value' do content = file_fixture('files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_min.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Minimum delete_preview_after', e.message) end test '#parse - check if delete_preview_after more than max value' do content = file_fixture('files/compose_files/dockerhub_services/nginx_cp_invalid_delete_after_max.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Maximum delete_preview_after', e.message) end test '#parse - check if delete_preview_after has only integer value' do content = file_fixture('files/compose_files/dockerhub_services/nginx_cp_delete_after_integer.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('should be a String type', e.message) end test '#parse - check if deploy auto is invalid' do content = file_fixture('files/compose_files/dockerhub_services/nginx_invalid_deploy_auto.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Invalid auto value', e.message) end test '#parse - check if an env_file is empty' do content = file_fixture('files/compose_files/dockerhub_services/nginx_env_file_empty.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("Unsupported type of 'env_file'", e.message) end test '#parse - check if a list of env files has an empty value' do content = file_fixture('files/compose_files/dockerhub_services/nginx_env_file_empty_in_array.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('env_file contains an empty value', e.message) end test '#parse - check if a list of env files has duplicated values' do content = file_fixture('files/compose_files/dockerhub_services/nginx_env_file_duplicates.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('env_file contains non-unique items', e.message) end test '#parse - check a boolean option' do content = file_fixture('files/compose_files/boolean_option.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('The service name true must be a quoted string', e.message) end test '#parse - check if a config is unknown in short syntax' do content = file_fixture('files/compose_files/dockerhub_services/nginx_configs_short_syntax_unknown_config.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('undefined config', e.message) end test '#parse - check if a config path is not specified' do content = file_fixture('files/compose_files/dockerhub_services/nginx_configs_short_syntax_invalid_path.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('has an empty file', e.message) end test '#parse - check parse configs with a long syntax' do content = file_fixture('files/compose_files/dockerhub_services/nginx_configs_long_syntax.yml').read parsed_content = UffizziCore::ComposeFileService.parse(content) content_data = YAML.safe_load(content) config = parsed_content[:containers].first[:configs].first assert_equal(content_data['services']['nginx']['configs'].first['target'], config[:target]) end test '#parse - check parse configs with a long syntax if a target is not specified' do content = file_fixture('files/compose_files/dockerhub_services/nginx_configs_long_syntax_without_target.yml').read parsed_content = UffizziCore::ComposeFileService.parse(content) content_data = YAML.safe_load(content) config = parsed_content[:containers].first[:configs].first assert_equal(content_data['configs']['vote_conf']['file'], config[:target]) end test '#parse - check if a secret is unknown' do content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets_unknown.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('undefined secret', e.message) end test '#parse - check if a secret is not external' do content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets_without_external.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("'postgres_password' should be external", e.message) end test '#parse - check if a secret doesn\'t have a name' do content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets_without_name.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("'postgres_password' secret can not be blank", e.message) end test '#parse - raise an error if a build option is specified' do content = file_fixture('files/compose_files/github_services/hello_world.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("The 'build' directive is not supported", e.message) end test '#parse - parses compose file with healthcheck and converts time to seconds when the test command is array' do content = file_fixture('files/compose_files/healthcheck/array_command_success.yml').read result = UffizziCore::ComposeFileService.parse(content) container_with_healthcheck = result[:containers].select { |container| container[:container_name] == 'hello-world' }.first assert_equal(90, container_with_healthcheck[:healthcheck][:interval]) refute(container_with_healthcheck[:healthcheck][:disable]) end test '#parse - parses compose file with healthcheck and converts time to seconds when the test command is string' do content = file_fixture('files/compose_files/healthcheck/string_command_success.yml').read result = UffizziCore::ComposeFileService.parse(content) container_with_healthcheck = result[:containers].select { |container| container[:container_name] == 'hello-world' }.first assert_equal(90, container_with_healthcheck[:healthcheck][:interval]) refute(container_with_healthcheck[:healthcheck][:disable]) end test "#parse - parses compose file with healthcheck and sets disabled to false if the command is 'NONE'" do content = file_fixture('files/compose_files/healthcheck/disabled_healthcheck.yml').read result = UffizziCore::ComposeFileService.parse(content) container_with_healthcheck = result[:containers].select { |container| container[:container_name] == 'hello-world' }.first assert(container_with_healthcheck[:healthcheck][:disable]) end test '#parse - parses compose file with healthcheck disabled is true and test not set' do content = file_fixture('files/compose_files/healthcheck/healthcheck_with_disable.yml').read result = UffizziCore::ComposeFileService.parse(content) container_with_healthcheck = result[:containers].select { |container| container[:container_name] == 'hello-world' }.first assert(container_with_healthcheck[:healthcheck][:disable]) end test '#parse - raises error if the healthcheck command has invalid type' do content = file_fixture('files/compose_files/healthcheck/invalid_command.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match("Unsupported type of 'test' option", e.message) end test '#parse - raises error if the retries field has invalid type' do content = file_fixture('files/compose_files/healthcheck/invalid_retries.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_match('Invalid retries value', e.message) end test '#parse - raises error if healthcheck has invalid interval' do content = file_fixture('files/compose_files/healthcheck/invalid_interval.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end error_message = "The time interval should be in the following format '{hours}h{minutes}m{seconds}s'. " \ 'At least one value must be present.' assert_match(error_message, e.message) end test '#parse - raises error if test or disble option are not present' do content = file_fixture('files/compose_files/healthcheck/invalid_options.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end error_message = 'One of these options is required: test, disable' assert_match(error_message, e.message) end test '#build_template_attributes - check if x-uffizzi ingress is specified' do content = file_fixture('files/compose_files/dockerhub_services/nginx_uffizzi_ingress.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) content_data = YAML.safe_load(content) nginx_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/nginx/) } assert(nginx_container[:public]) assert_equal(nginx_container[:port], content_data['x-uffizzi']['ingress']['port']) end test '#build_template_attributes - check if deploy auto is disabled' do content = file_fixture('files/compose_files/dockerhub_services/nginx_auto_deploy_off.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) continuously_deploy = attributes[:payload][:containers_attributes].first[:continuously_deploy] assert_equal(:disabled, continuously_deploy) end test '#build_template_attributes - check container default memory' do content = file_fixture('files/compose_files/dockerhub_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) memory_limit = attributes[:payload][:containers_attributes].first[:memory_limit] assert_equal(Settings.compose.default_memory, memory_limit) end test '#build_template_attributes - public image and no credential exists' do content = file_fixture('files/compose_files/dockerhub_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stubbed_dockerhub_repository = stub_dockerhub_repository('library', 'nginx') UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) assert_requested(stubbed_dockerhub_repository) end test '#build_template_attributes - private image and no credential exists' do content = file_fixture('files/compose_files/dockerhub_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stubbed_dockerhub_repository = stub_dockerhub_private_repository('library', 'nginx') assert_raise(UffizziCore::ComposeFile::BuildError) do UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) end assert_requested(stubbed_dockerhub_repository) end test '#build_template_attributes - check if a memory is not in list of available values' do content = file_fixture('files/compose_files/dockerhub_services/nginx_invalid_memory.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') e = assert_raise(UffizziCore::ComposeFile::BuildError) do UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) end assert_match('The memory should be one of the', e.message) end test '#build_template_attributes - check container memory' do content = file_fixture('files/compose_files/compose_memory.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') stub_dockerhub_repository('library', 'redis') stub_dockerhub_repository('library', 'postgres') stub_dockerhub_repository('library', 'ubuntu') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) postgres_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/postgres/) } redis_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/redis/) } nginx_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/nginx/) } ubuntu_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/ubuntu/) } assert_equal(125, postgres_container[:memory_limit]) assert_equal(250, redis_container[:memory_limit]) assert_equal(1000, nginx_container[:memory_limit]) assert_equal(4000, ubuntu_container[:memory_limit]) end test '#build_template_attributes - check azure image build' do create(:credential, :azure, account: @account) content = file_fixture('files/compose_files/azure_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) container = attributes[:payload][:containers_attributes].first content_data = YAML.safe_load(content) image_url = content_data['services']['nginx']['image'].split(':').first tag = content_data['services']['nginx']['image'].split(':').last image_name = image_url.split('/').last assert_equal(image_name, container[:image]) assert_equal("#{image_url}:latest", container[:full_image_name]) assert_equal(tag, container[:tag]) assert_equal(image_name, container[:repo_attributes][:name]) refute(container[:repo_attributes][:namespace]) assert_equal(UffizziCore::Repo::Azure.name, container[:repo_attributes][:type]) end test '#build_template_attributes - check invalid azure credential' do content = file_fixture('files/compose_files/azure_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) e = assert_raise(UffizziCore::ComposeFile::BuildError) do UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) end assert_match(I18n.t('compose.unprocessable_image', value: 'azure'), e.message) end test '#build_template_attributes - check google image build' do create(:credential, :google, account: @account) content = file_fixture('files/compose_files/google_services/nginx.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) container = attributes[:payload][:containers_attributes].first content_data = YAML.safe_load(content) image_url = content_data['services']['nginx']['image'].split(':').first tag = content_data['services']['nginx']['image'].split(':').last project_name = image_url.split('/').second image_name = image_url.split('/').last assert_equal("#{project_name}/#{image_name}", container[:image]) assert_equal(tag, container[:tag]) assert_equal("#{image_url}:latest", container[:full_image_name]) assert_equal(image_name, container[:repo_attributes][:name]) assert_equal(project_name, container[:repo_attributes][:namespace]) assert_equal(UffizziCore::Repo::Google.name, container[:repo_attributes][:type]) end test '#build_template_attributes - check build of custom docker account' do content = file_fixture('files/compose_files/dockerhub_services/account_custom_image.yml').read stub_dockerhub_repository('account', 'custom_image') parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) container = attributes[:payload][:containers_attributes].first content_data = YAML.safe_load(content) image_url = content_data['services']['nginx']['image'].split(':').first tag = content_data['services']['nginx']['image'].split(':').last account_name = image_url.split('/').first image_name = image_url.split('/').last assert_equal("#{account_name}/#{image_name}", container[:image]) assert_equal(tag, container[:tag]) assert_equal(image_name, container[:repo_attributes][:name]) assert_equal(account_name, container[:repo_attributes][:namespace]) assert_equal(UffizziCore::Repo::DockerHub.name, container[:repo_attributes][:type]) end test '#build_template_attributes - check a command build' do create(:credential, :google, account: @account) content = file_fixture('files/compose_files/google_services/cloudsql.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) container = attributes[:payload][:containers_attributes].last refute_nil(container[:command]) assert_nil(container[:entrypoint]) end test '#build_template_attributes - check an entrypoint build' do create(:credential, :google, account: @account) content = file_fixture('files/compose_files/google_services/cloudsql_entrypoint.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) container = attributes[:payload][:containers_attributes].last refute_nil(container[:entrypoint]) assert_nil(container[:command]) end test '#build_template_attributes - check secrets variables build' do project_secrets = [build(:secret, name: 'POSTGRES_USER', value: generate(:string)), build(:secret, name: 'POSTGRES_PASSWORD', value: generate(:string))] @project.secrets.append(project_secrets) stub_dockerhub_repository('library', 'postgres') content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) content_data = YAML.safe_load(content) postgres_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/postgres/) } secret_variables = postgres_container[:secret_variables] assert_equal(2, secret_variables.size) refute_nil(secret_variables.detect { |secret| secret[:name] == content_data['secrets']['postgres_user']['name'] }) refute_nil(secret_variables.detect { |secret| secret[:name] == content_data['secrets']['postgres_password']['name'] }) end test '#build_template_attributes - check secrets variables build if a project secret doesn\'t exist' do content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'postgres') e = assert_raise(UffizziCore::ComposeFile::SecretsError) do UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) end assert_match("Project secret 'POSTGRES_USER' not found", e.message) end test '#build_template_attributes - check secrets variables build if a compose has duplicates' do project_secrets = [build(:secret, name: 'POSTGRES_USER', value: generate(:string)), build(:secret, name: 'POSTGRES_PASSWORD', value: generate(:string))] @project.secrets.append(project_secrets) stub_dockerhub_repository('library', 'postgres') content = file_fixture('files/compose_files/dockerhub_services/postgres_secrets_duplicates.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) content_data = YAML.safe_load(content) postgres_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/postgres/) } secret_variables = postgres_container[:secret_variables] assert_equal(1, secret_variables.size) refute_nil(secret_variables.detect { |secret| secret[:name] == content_data['secrets']['postgres_user']['name'] }) assert_nil(secret_variables.detect { |secret| secret[:name] == 'POSTGRES_PASSWORD' }) end test '#build_template_attributes - check global and container continuous preview attributes' do content = file_fixture('files/compose_files/compose_with_continuous_preview.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'redis') stub_dockerhub_repository('library', 'nginx') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) nginx_repo_attributes = attributes[:payload][:containers_attributes].detect { |item| item[:image] == 'library/nginx' }[:repo_attributes] redis_repo_attributes = attributes[:payload][:containers_attributes].detect { |item| item[:image] == 'library/redis' }[:repo_attributes] assert { nginx_repo_attributes[:delete_preview_after] == 12 } assert { redis_repo_attributes[:delete_preview_after] == 10 } end test '#build_template_attributes - check named volumes' do content = file_fixture('files/compose_files/dockerhub_services/volumes_named.yml').read parsed_data = UffizziCore::ComposeFileService.parse(content) stub_dockerhub_repository('library', 'nginx') attributes = UffizziCore::ComposeFileService.build_template_attributes(parsed_data, 'compose.yml', @account.credentials, @project) content_data = YAML.safe_load(content) nginx_container = attributes[:payload][:containers_attributes].detect { |container| container[:image].match(/nginx/) } first_volume = nginx_container[:volumes].first assert(nginx_container[:volumes]) assert_equal(UffizziCore::ComposeFile::Parsers::Services::VolumesParserService::NAMED_VOLUME_TYPE, first_volume[:type]) assert_equal(content_data.dig('services', 'nginx', 'volumes').first.split(':').first, first_volume[:source]) assert_equal(content_data.dig('services', 'nginx', 'volumes').first.split(':').second, first_volume[:target]) end test '#parse - handle Psych::SyntaxError' do content = file_fixture('files/compose_files/compose_with_syntax_error.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_equal("Syntax error: could not find expected ':' while scanning a simple key at line 5 column 3", e.message) end test '#parse - handle empty compose file' do content = file_fixture('files/compose_files/compose_empty.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_equal('Unsupported compose file', e.message) end test '#parse - handle when file has just a string' do content = file_fixture('files/compose_files/compose_with_only_line.yml').read e = assert_raise(UffizziCore::ComposeFile::ParseError) do UffizziCore::ComposeFileService.parse(content) end assert_equal('Unsupported compose file', e.message) end end ================================================ FILE: core/test/services/deployment_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::DeploymentServiceTest < ActiveSupport::TestCase setup do Sidekiq::Testing.fake! Sidekiq::Worker.clear_all @user = create(:user, :with_personal_account) @project = create(:project, account: @user.personal_account) @deployment = create(:deployment, project: @project) end teardown do Sidekiq::Testing.inline! end test '#deploy_containers - start to deploy dockerhub container' do repo = create(:repo, :docker_hub, project: @project) create(:container, :with_public_port, :active, deployment: @deployment, repo: repo) stubbed_deploy_containers_request = stub_request(:post, "#{Settings.controller.url}/deployments/#{@deployment.id}/containers") differences = { -> { UffizziCore::Event.count } => 1, -> { UffizziCore::Event.with_state(UffizziCore::Event.state.deploying).count } => 1, -> { UffizziCore::Deployment::DeployContainersJob.jobs.size } => 0, -> { UffizziCore::Deployment::ManageDeployActivityItemJob.jobs.size } => 1, } assert_difference differences do UffizziCore::DeploymentService.deploy_containers(@deployment) end assert_requested stubbed_deploy_containers_request end test '#disable when deployment has no compose file' do container = create(:container, :with_public_port, deployment: @deployment) differences = { -> { UffizziCore::Deployment.active.count } => -1, -> { UffizziCore::ComposeFile.count } => 0, -> { UffizziCore::Template.count } => 0, } assert_difference differences do UffizziCore::DeploymentService.disable!(@deployment) end container.reload assert { container.disabled? } end test '#disable when the main compose file exists' do user = create(:user, :with_personal_account) compose_file = create(:compose_file, project: @project, added_by: user) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } template = create(:template, :compose_file_source, compose_file: compose_file, project: @project, payload: template_payload, added_by: user) deployment = create(:deployment, project: @project, compose_file: compose_file, template: template) container = create(:container, :with_public_port, deployment: deployment) differences = { -> { UffizziCore::Deployment.active.count } => -1, -> { UffizziCore::ComposeFile.count } => 0, -> { UffizziCore::Template.count } => 0, } assert_difference differences do UffizziCore::DeploymentService.disable!(deployment) end container.reload assert { container.disabled? } end test '#disable when a temporary compose file exists' do user = create(:user, :with_personal_account) compose_file = create(:compose_file, :temporary, project: @project, added_by: user) image = generate(:image) image_namespace, image_name = image.split('/') target_branch = generate(:branch) repo_attributes = attributes_for( :repo, :docker_hub, namespace: image_namespace, name: image_name, branch: target_branch, ) container_attributes = attributes_for( :container, :with_public_port, image: image, tag: target_branch, receive_incoming_requests: true, repo_attributes: repo_attributes, ) template_payload = { containers_attributes: [container_attributes], } template = create(:template, :compose_file_source, compose_file: compose_file, project: @project, payload: template_payload, added_by: user) deployment = create(:deployment, project: @project, compose_file: compose_file, template: template) container = create(:container, :with_public_port, deployment: deployment) differences = { -> { UffizziCore::Deployment.active.count } => -1, -> { UffizziCore::ComposeFile.count } => -1, -> { UffizziCore::Template.count } => -1, } assert_difference differences do UffizziCore::DeploymentService.disable!(deployment) end container.reload assert { container.disabled? } end test '#failed - container failed' do container = create(:container, deployment: @deployment) create(:activity_item, :with_failed_event, container: container, deployment: @deployment) deployment_failed = UffizziCore::DeploymentService.failed?(@deployment) assert(deployment_failed) end test '#failed - container deployed' do container = create(:container, deployment: @deployment) create(:activity_item, :with_deployed_event, container: container, deployment: @deployment) deployment_failed = UffizziCore::DeploymentService.failed?(@deployment) refute(deployment_failed) end test '#failed - no containers' do deployment_failed = UffizziCore::DeploymentService.failed?(@deployment) refute(deployment_failed) end end ================================================ FILE: core/test/services/google_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::GoogleServiceTest < ActiveSupport::TestCase test '#digest' do image = generate(:image) tag = generate(:tag) user = create(:user, :with_personal_account) credential = create(:credential, :google, :active, account: user.personal_account) headers_response = { 'docker-content-digest': generate(:string) } token_response = { token: generate(:string) } stubbed_google_registry_token = stub_google_registry_token(token_response) stubbed_google_registry_manifests = stub_google_registry_manifests(image, tag, headers_response, {}) digest = UffizziCore::ContainerRegistry::GoogleService.digest(credential, image, tag) assert_requested stubbed_google_registry_token assert_requested stubbed_google_registry_manifests assert digest.present? end end ================================================ FILE: core/test/services/image_parser_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::ImageParserServiceTest < ActiveSupport::TestCase test '#parse' do image_parser_service = UffizziCore::ComposeFile::Parsers::Services::ImageParserService assert_equal( { registry_url: 'docker.io', namespace: 'library', name: 'redis', tag: 'latest', full_image_name: 'docker.io/library/redis:latest' }, image_parser_service.parse('redis'), ) assert_equal( { registry_url: 'docker.io', namespace: 'library', name: 'redis', tag: '5', full_image_name: 'docker.io/library/redis:5' }, image_parser_service.parse('redis:5'), ) assert_equal( { registry_url: 'docker.io', namespace: 'namespace', name: 'redis', tag: 'latest', full_image_name: 'docker.io/namespace/redis:latest' }, image_parser_service.parse('namespace/redis'), ) assert_equal( { registry_url: 'docker.io', namespace: 'namespace', name: 'redis', tag: 'latest', full_image_name: 'docker.io/namespace/redis:latest' }, image_parser_service.parse('docker.io/namespace/redis'), ) assert_equal( { registry_url: 'my-private.registry:5000', namespace: 'namespace', name: 'redis', tag: '5.3', full_image_name: 'my-private.registry:5000/namespace/redis:5.3' }, image_parser_service.parse('my-private.registry:5000/namespace/redis:5.3'), ) assert_equal( { registry_url: 'localhost:80', namespace: nil, name: 'redis', tag: '5.3', full_image_name: 'localhost:80/redis:5.3' }, image_parser_service.parse('localhost:80/redis:5.3'), ) assert_equal( { registry_url: 'docker.io', namespace: 'library', name: 'lower_case_name', tag: 'lower_case_tag', full_image_name: 'docker.io/library/lower_case_name:lower_case_tag' }, image_parser_service.parse('lower_case_name:lower_case_tag'), ) assert_equal( { registry_url: 'docker.io', namespace: 'namespace', name: 'lower_case_name', tag: 'UPPERCASE_TAG', full_image_name: 'docker.io/namespace/lower_case_name:UPPERCASE_TAG' }, image_parser_service.parse('namespace/lower_case_name:UPPERCASE_TAG'), ) assert_equal( { registry_url: 'asia.gcr.io', namespace: 'uffizzi', name: 'nginx', tag: 'latest', full_image_name: 'asia.gcr.io/uffizzi/nginx:latest' }, image_parser_service.parse('asia.gcr.io/uffizzi/nginx:latest'), ) end test '#parse with exception' do image_parser_service = UffizziCore::ComposeFile::Parsers::Services::ImageParserService assert_raises(UffizziCore::ComposeFile::ParseError) { image_parser_service.parse('very:wrong:image:path') } assert_raises(UffizziCore::ComposeFile::ParseError) { image_parser_service.parse('UPPERCASE_NAMESPACE/UPPERCASE_NAME:UPPERCASE_TAG') } assert_raises(UffizziCore::ComposeFile::ParseError) { image_parser_service.parse('UPPERCASE_NAME:UPPERCASE_TAG') } assert_raises(UffizziCore::ComposeFile::ParseError) { image_parser_service.parse('UPPERCASE_NAME') } end end ================================================ FILE: core/test/services/manage_activity_items_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::ManageActivityItemsServiceTest < ActiveSupport::TestCase setup do @user = create(:user, :with_personal_account) @project = create(:project, account: @user.personal_account) end test '#container_status_items - deployment has no containers' do deployment = create(:deployment, project: @project) stubbed_response_containers = [] namespace = UffizziCore::Converters.deep_lower_camelize_keys( { metadata: { annotations: { network_connectivity: {}.to_json, }, }, }, ) stub_controller_containers = stub_controller_containers_request(deployment, stubbed_response_containers) stub_get_controller_deployment = stub_controller_get_namespace_request(deployment, namespace) service = UffizziCore::ManageActivityItemsService.new(deployment) container_status_items = service.container_status_items assert { container_status_items.empty? } assert_requested(stub_get_controller_deployment) assert_requested(stub_controller_containers) end end ================================================ FILE: core/test/services/user_generator_service_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCore::UserGeneratorServiceTest < ActiveSupport::TestCase test '#generate' do email = generate(:email) password = generate(:password) project_name = generate(:string) differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if an email gets by a user' do email = nil password = generate(:password) project_name = generate(:string) new_email = generate(:email) differences = { -> { UffizziCore::User.where(email: new_email).count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:gets).returns(new_email) IO.stubs(:console).returns(console_mock) assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if an email is empty' do email = nil password = generate(:password) project_name = generate(:string) differences = { -> { UffizziCore::User.where(email: UffizziCore::UserGeneratorService::DEFAULT_USER_EMAIL).count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:gets).returns('') IO.stubs(:console).returns(console_mock) assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a password gets by a user' do email = generate(:email) password = nil project_name = generate(:string) new_password = generate(:password) differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:getpass).returns(new_password) IO.stubs(:console).returns(console_mock) assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a password is empty' do email = generate(:email) password = nil project_name = generate(:string) console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:getpass).returns('') IO.stubs(:console).returns(console_mock) assert_raises StandardError do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a project name gets by a user' do email = generate(:email) password = generate(:password) project_name = nil new_project_name = generate(:string) differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.where(name: new_project_name).count } => 1, } console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:gets).returns(new_project_name) IO.stubs(:console).returns(console_mock) assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a project name is empty' do email = generate(:email) password = generate(:password) project_name = nil differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.where(name: UffizziCore::UserGeneratorService::DEFAULT_PROJECT_NAME).count } => 1, } console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:gets).returns('') IO.stubs(:console).returns(console_mock) assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a user already exists' do email = generate(:email) password = generate(:password) project_name = generate(:string) create(:user, :with_personal_account, email: email) assert_raises ActiveRecord::RecordInvalid do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if a user already exists and email gets by user' do email = generate(:email) password = generate(:password) project_name = generate(:string) create(:user, :with_personal_account, email: email) console_mock = mock('console_mock') console_mock.stubs(:write) console_mock.stubs(:gets).returns(email) IO.stubs(:console).returns(console_mock) assert_raises ActiveRecord::RecordInvalid do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if have data but no console' do email = generate(:email) password = generate(:password) project_name = generate(:string) IO.stubs(:console).returns(nil) differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if no email and no console' do email = nil password = generate(:password) project_name = generate(:string) IO.stubs(:console).returns(nil) assert_raises ActiveRecord::RecordInvalid do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if no password and no console' do email = generate(:email) password = nil project_name = generate(:string) IO.stubs(:console).returns(nil) assert_raises ActiveRecord::RecordInvalid do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end test '#generate if no project_name and no console' do email = generate(:email) password = generate(:password) project_name = nil IO.stubs(:console).returns(nil) differences = { -> { UffizziCore::User.count } => 1, -> { UffizziCore::Account.count } => 1, -> { UffizziCore::Membership.count } => 1, -> { UffizziCore::Project.count } => 1, } assert_difference differences do UffizziCore::UserGeneratorService.generate(email, password, project_name) end end end ================================================ FILE: core/test/support/azure_registry_stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::AzureRegistryStubSupport def stub_azure_registry_oauth2_token(registry_url, response, code = 200) service = URI.parse(registry_url).hostname uri = "#{registry_url}/oauth2/token?service=#{service}" stub_request(:get, uri).to_return(status: code, body: response.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_azure_registry_manifests(registry_url, image, tag, headers, body) uri = "#{registry_url}/v2/#{image}/manifests/#{tag}" stub_request(:get, uri).to_return(status: 200, headers: headers, body: body.to_json) end end ================================================ FILE: core/test/support/controller_stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::ControllerStubSupport def stub_controller_apply_credential uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/deployments/[0-9]*/credentials} stub_request(:post, uri) end def stub_put_controller_deployment_request(deployment) uri = "#{Settings.controller.url}/deployments/#{deployment.id}" stub_request(:put, uri) end def stub_controller_get_namespace_request(deployable, data = nil) uri = "#{Settings.controller.url}/namespaces/#{deployable.namespace}" stub_request(:get, uri).to_return(status: 200, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_controller_get_namespace_request_any(data = nil) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/deployment-[0-9]*} stub_request(:get, uri).to_return(status: 200, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_delete_namespace_request(deployable) uri = "#{Settings.controller.url}/namespaces/#{deployable.namespace}" stub_request(:delete, uri) end def stub_controller_containers_request(deployment, data = nil) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/containers" stub_request(:get, uri).to_return(status: 200, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_controller_nodes_request uri = "#{Settings.controller.url}/nodes" stub_request(:get, uri) end def stub_deploy_containers_request(deployment) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/containers" stub_request(:post, uri) end def stub_apply_config_file_request(deployment, config_file) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/config_files/#{config_file.id}" stub_request(:post, uri) end def stub_apply_config_file_request_with_expected(deployment, config_file, expected_request) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/config_files/#{config_file.id}" stub_request(:post, uri).with do |req| actual_body = JSON.parse(req.body).deep_symbolize_keys.deep_sort expected_body = expected_request.deep_symbolize_keys.deep_sort is_equal = actual_body == expected_body ap(HashDiff.diff(actual_body, expected_body)) unless is_equal is_equal end end def stub_controller_get_deployment_events(deployment, body) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/containers/events" stub_request(:get, uri).to_return(status: 200, body: body.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_container_log_request(deployment_id, pod_name, limit, previous, data) uri = "#{Settings.controller.url}/deployments/#{deployment_id}/containers/#{pod_name}/logs?limit=#{limit}&previous=#{previous}" stub_request(:get, uri).to_return(status: 200, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_deploy_containers_request_with_expected(deployment, expected_request) uri = "#{Settings.controller.url}/deployments/#{deployment.id}/containers" stub_request(:post, uri).with do |req| actual_body = JSON.parse(req.body).deep_symbolize_keys.deep_sort expected_body = expected_request.deep_symbolize_keys.deep_sort is_equal = actual_body == expected_body ap(HashDiff.diff(actual_body, expected_body)) unless is_equal is_equal end end def stub_create_namespace_request uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces$} stub_request(:post, uri).to_return(status: 200, body: { namespace: 'namespace' }.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_get_cluster_request(data = {}, _status = 200) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/([A-Za-z0-9\-]+)/cluster/([A-Za-z0-9\-]+)} stub_request(:get, uri).to_return(status: 200, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_create_cluster_request(data = {}, status = 200) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/([A-Za-z0-9\-]+)/cluster} stub_request(:post, uri).to_return(status: status, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_scale_cluster_request(status = 200) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/([A-Za-z0-9\-]+)/cluster/([A-Za-z0-9\-]+)} stub_request(:patch, uri).to_return(status: status, headers: { 'Content-Type' => 'application/json' }) end def stub_create_cluster_request_with_expected(returned_data = {}, expected_request = {}, status = 200) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/([A-Za-z0-9\-]+)/cluster} stub_request(:post, uri).with do |req| actual_body = JSON.parse(req.body).deep_symbolize_keys.deep_sort expected_body = expected_request.deep_symbolize_keys.deep_sort is_equal = equal_hashes?(expected_body, actual_body) ap(HashDiff.diff(actual_body, expected_body)) unless is_equal is_equal end.to_return(status: status, body: returned_data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_get_ingresses(data = {}, status = 200) uri = %r{#{Regexp.quote(Settings.controller.url.to_s)}/namespaces/([A-Za-z0-9\-]+)/ingresses} stub_request(:get, uri).to_return(status: status, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end private def equal_hashes?(expected, actual) actual = actual.deep_symbolize_keys expected.deep_symbolize_keys.all? do |k, v| if v.is_a?(Regexp) v.match?(actual[k]) else v == actual[k] end end end end ================================================ FILE: core/test/support/docker_hub_stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::DockerHubStubSupport API_URL = 'https://hub.docker.com/v2' AUTH_URL = 'https://auth.docker.io' def stub_dockerhub_login response = { token: 'mytoken' } uri = %r{#{API_URL}/users/login/} stub_request(:post, uri).to_return(status: 200, body: response.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_dockerhub_login_fail(data) uri = %r{#{API_URL}/users/login/} stub_request(:post, uri).to_return(status: 401, body: data.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_dockerhub_auth_for_digest(repository) response = { token: 'mytoken' } url = %r{#{AUTH_URL}.+scope=repository:#{repository}:pull&service=registry[.]docker[.]io.+} stub_request(:get, url).to_return(status: 200, body: response.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_dockerhub_get_digest(image, tag, data) url = "https://index.docker.io/v2/#{image}/manifests/#{tag}" stub_request(:get, url).to_return(status: 200, body: data[:body].to_json, headers: data[:headers]) end def stub_dockerhub_api_tokens(data) uri = "#{API_URL}/api_tokens" stub_request(:post, uri).to_return(status: 201, body: data.to_json) end def stub_dockerhub_repository(namespace, repo_name) url = "#{API_URL}/repositories/#{namespace}/#{repo_name}" stub_request(:get, url).to_return(status: 200) end def stub_dockerhub_private_repository(namespace, repo_name) url = "#{API_URL}/repositories/#{namespace}/#{repo_name}" stub_request(:get, url).to_return(status: 404) end def stub_dockerhub_repository_any uri = %r{#{API_URL}/repositories/.*/.*} stub_request(:get, uri).to_return(status: 200) end end ================================================ FILE: core/test/support/docker_registry_stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::DockerRegistryStubSupport def stub_docker_registry_manifests(registry_url, image, tag, headers: {}, body: {}, status: 200) uri = "#{registry_url}/v2/#{image}/manifests/#{tag}" stub_request(:get, uri).to_return(status: status, headers: headers, body: body.to_json) end end ================================================ FILE: core/test/support/fixture_support.rb ================================================ # frozen_string_literal: true module UffizziCore::FixtureSupport def file_fixture(file_path) full_path = "#{ActiveSupport::TestCase.fixture_path}/#{file_path}" File.new(full_path) end def json_fixture(file_path, symbolize_names: true) data = file_fixture(file_path).read JSON.parse(data, symbolize_names: symbolize_names) end end ================================================ FILE: core/test/support/github_container_registry_support.rb ================================================ # frozen_string_literal: true module UffizziCore::GithubContainerRegistryStubSupport def stub_github_container_registry_access_token(registry_url, response) service = URI.parse(registry_url).hostname uri = "#{registry_url}/token?service=#{service}" stub_request(:get, uri).to_return(status: 200, body: response.to_json, headers: { 'Content-Type' => 'application/json' }) end end ================================================ FILE: core/test/support/google_registry_stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::GoogleRegistryStubSupport REGISTRY_URL = 'https://gcr.io' def stub_google_registry_token(response, code = 200) service = URI.parse(REGISTRY_URL).hostname uri = "#{REGISTRY_URL}/v2/token?service=#{service}" stub_request(:get, uri).to_return(status: code, body: response.to_json, headers: { 'Content-Type' => 'application/json' }) end def stub_google_registry_manifests(image, tag, headers, body) uri = "#{REGISTRY_URL}/v2/#{image}/manifests/#{tag}" stub_request(:get, uri).to_return(status: 200, headers: headers, body: body.to_json) end end ================================================ FILE: core/test/support/stub_support.rb ================================================ # frozen_string_literal: true module UffizziCore::StubSupport def stub_controller stub_request(:any, /#{Regexp.quote(Settings.controller.url.to_s)}\/*./) end end ================================================ FILE: core/test/test_helper.rb ================================================ # frozen_string_literal: true ENV['RAILS_ENV'] = 'test' require_relative '../test/dummy/config/environment' ActiveRecord::Migrator.migrations_paths = [File.expand_path('../test/dummy/db/migrate', __dir__)] ActiveRecord::Migrator.migrations_paths << File.expand_path('../db/migrate', __dir__) Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f } require 'awesome_print' require 'byebug' require 'factory_bot' require 'faker' require 'minitest/hooks/default' require 'minitest-power_assert' require 'minitest/mock' require 'mocha/minitest' require 'rails/test_help' require 'sidekiq/testing' require 'webmock/minitest' require 'octokit' require 'deepsort' require 'hash_diff' FactoryBot.reload WebMock.disable_net_connect! Sidekiq::Testing.inline! # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_path=) ActiveSupport::TestCase.fixture_path = File.expand_path('fixtures', __dir__) ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path ActiveSupport::TestCase.file_fixture_path = "#{ActiveSupport::TestCase.fixture_path}/files" ActiveSupport::TestCase.fixtures(:all) end FactoryBot.define do initialize_with { new(attributes) } end class ActiveSupport::TestCase include FactoryBot::Syntax::Methods include Minitest::Hooks include ActiveModel::Validations include UffizziCore::AuthManagement include UffizziCore::ControllerStubSupport include UffizziCore::DockerHubStubSupport include UffizziCore::FixtureSupport include UffizziCore::GoogleRegistryStubSupport include UffizziCore::StubSupport include UffizziCore::AzureRegistryStubSupport include UffizziCore::GithubContainerRegistryStubSupport include UffizziCore::DockerRegistryStubSupport setup do @routes = UffizziCore::Engine.routes end end ================================================ FILE: core/test/uffizzi_core_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class UffizziCoreTest < ActiveSupport::TestCase test 'it has a version number' do assert UffizziCore::VERSION end end ================================================ FILE: core/uffizzi_core.gemspec ================================================ # frozen_string_literal: true require_relative 'lib/uffizzi_core/version' # rubocop:disable Metrics/BlockLength Gem::Specification.new do |spec| spec.name = 'uffizzi_core' spec.version = UffizziCore::VERSION spec.authors = ['Josh Thurman', 'Grayson Adkins'] spec.email = ['info@uffizzi.com'] spec.summary = 'uffizzi_core' spec.description = 'uffizzi_core' spec.homepage = 'https://uffizzi.com' spec.license = 'Apache-2.0' spec.required_ruby_version = '>= 2.5.0' spec.metadata['homepage_uri'] = spec.homepage spec.metadata['source_code_uri'] = 'https://github.com/UffizziCloud/uffizzi_app/tree/main/core' spec.metadata['changelog_uri'] = 'https://github.com/UffizziCloud/uffizzi_app/blob/main/core/CHANGELOG.md' spec.files = Dir['{app,config,db,lib,swagger}/**/*', 'LICENSE', 'Rakefile', 'README.md'] spec.add_dependency 'aasm' spec.add_dependency 'actionpack', '~> 6.1.0' spec.add_dependency 'active_model_serializers' spec.add_dependency 'activerecord', '~> 6.1.0' spec.add_dependency 'activesupport', '~> 6.1.0' spec.add_dependency 'ancestry' spec.add_dependency 'aws-sdk-ecr', '~> 1.47' spec.add_dependency 'aws-sdk-eventbridge', '~> 1.30' spec.add_dependency 'aws-sdk-iam', '~> 1.61' spec.add_dependency 'docker_distribution' spec.add_dependency 'dotenv' spec.add_dependency 'enumerize' spec.add_dependency 'faraday' spec.add_dependency 'faraday_curl' spec.add_dependency 'faraday-follow_redirects' spec.add_dependency 'faraday_middleware' spec.add_dependency 'hashie' spec.add_dependency 'jwt' spec.add_dependency 'kaminari' spec.add_dependency 'octokit' spec.add_dependency 'pg', '>= 0.18', '< 2.0' spec.add_dependency 'pundit' spec.add_dependency 'rails', '~> 6.1.0' spec.add_dependency 'ransack' spec.add_dependency 'responders' spec.add_dependency 'rolify' spec.add_dependency 'rswag-api' spec.add_dependency 'rswag-ui' spec.add_dependency 'sidekiq' spec.add_dependency 'sidekiq-unique-jobs' spec.add_dependency 'swagger_yard' spec.add_dependency 'virtus' spec.add_development_dependency 'awesome_print' spec.add_development_dependency 'bcrypt', '~> 3.1.7' spec.add_development_dependency 'bump' spec.add_development_dependency 'bundler', '~> 2.2' spec.add_development_dependency 'byebug' spec.add_development_dependency 'config' spec.add_development_dependency 'deepsort', '~> 0.4.5' spec.add_development_dependency 'factory_bot' spec.add_development_dependency 'faker' spec.add_development_dependency 'hash_diff', '~> 1.1' spec.add_development_dependency 'minitest' spec.add_development_dependency 'minitest-hooks' spec.add_development_dependency 'minitest-power_assert' spec.add_development_dependency 'mocha' spec.add_development_dependency 'pry-byebug' spec.add_development_dependency 'pry-inline' spec.add_development_dependency 'puma' spec.add_development_dependency 'rack-cors' spec.add_development_dependency 'rake' spec.add_development_dependency 'webmock' end # rubocop:enable Metrics/BlockLength ================================================ FILE: db/migrate/20220219114713_create_uffizzi_core_tables.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220218121438) class CreateUffizziCoreTables < ActiveRecord::Migration[6.1] def change create_table 'uffizzi_core_accounts', force: :cascade do |t| t.text 'name' t.text 'kind', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'customer_token' t.string 'state' t.string 'subscription_token' t.datetime 'payment_issue_at' t.string 'domain' t.boolean 'sso_enabled', default: false t.bigint 'owner_id' t.integer 'container_memory_limit' t.string 'workos_organization_id' t.string 'sso_state' t.index ['customer_token'], name: 'index_accounts_on_customer_token', unique: true t.index ['domain'], name: 'index_accounts_on_domain', unique: true t.index ['subscription_token'], name: 'index_accounts_on_subscription_token', unique: true end create_table 'uffizzi_core_activity_items', force: :cascade do |t| t.bigint 'deployment_id', null: false t.string 'namespace' t.string 'name' t.string 'tag' t.string 'branch' t.string 'type' t.bigint 'container_id', null: false t.string 'commit' t.string 'commit_message' t.bigint 'build_id' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.jsonb 'data', default: {}, null: false t.string 'digest' t.index ['container_id'], name: 'index_activity_items_on_container_id' t.index ['deployment_id'], name: 'index_activity_items_on_deployment_id' end create_table 'uffizzi_core_builds', force: :cascade do |t| t.bigint 'repo_id', null: false t.string 'build_id' t.string 'repository' t.string 'branch' t.string 'commit' t.string 'committer' t.string 'message' t.string 'log_url' t.integer 'status' t.datetime 'started_at' t.datetime 'ended_at' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.boolean 'deployed' t.index ['build_id'], name: 'index_builds_on_build_id', unique: true t.index ['repo_id'], name: 'index_builds_on_repo_id' end create_table 'uffizzi_core_comments', force: :cascade do |t| t.string 'commentable_type' t.bigint 'commentable_id' t.text 'content' t.bigint 'user_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'ancestry' t.integer 'ancestry_depth', default: 0 t.index ['ancestry'], name: 'index_comments_on_ancestry' t.index ['commentable_type', 'commentable_id'], name: 'index_comments_on_commentable' t.index ['user_id'], name: 'index_comments_on_user_id' end create_table 'uffizzi_core_compose_files', force: :cascade do |t| t.string 'source' t.bigint 'repository_id' t.string 'branch' t.string 'path' t.string 'auto_deploy' t.bigint 'added_by_id' t.bigint 'project_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'state' t.jsonb 'payload', default: {}, null: false t.text 'content' t.string 'kind', default: 'main' t.index ['project_id'], name: 'index_compose_files_on_project_id' end create_table 'uffizzi_core_config_files', force: :cascade do |t| t.string 'filename' t.string 'kind' t.bigint 'added_by_id' t.text 'payload' t.bigint 'project_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'compose_file_id' t.string 'creation_source' t.string 'source' t.index ['compose_file_id'], name: 'index_config_files_on_compose_file_id' t.index ['project_id'], name: 'index_config_files_on_project_id' end create_table 'uffizzi_core_container_config_files', force: :cascade do |t| t.string 'mount_path' t.bigint 'container_id', null: false t.bigint 'config_file_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['config_file_id'], name: 'index_container_config_files_on_config_file_id' t.index ['container_id'], name: 'index_container_config_files_on_container_id' end create_table 'uffizzi_core_containers', force: :cascade do |t| t.string 'image' t.string 'tag' t.jsonb 'variables' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'deployment_id' t.boolean 'public', default: false, null: false t.integer 'port' t.bigint 'repo_id' t.string 'state' t.string 'continuously_deploy', null: false t.string 'kind', default: 'user' t.integer 'target_port' t.string 'controller_name' t.boolean 'receive_incoming_requests' t.integer 'memory_request' t.integer 'memory_limit' t.jsonb 'secret_variables' t.string 'entrypoint' t.string 'command' t.index ['deployment_id'], name: 'index_containers_on_deployment_id' t.index ['repo_id'], name: 'index_containers_on_repo_id' end create_table 'uffizzi_core_coupons', force: :cascade do |t| t.string 'token' t.string 'name', null: false t.string 'currency', null: false t.bigint 'amount_off', null: false t.string 'duration', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false end create_table 'uffizzi_core_credentials', force: :cascade do |t| t.string 'type' t.string 'username' t.string 'password' t.bigint 'project_id' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'provider_ref' t.bigint 'account_id' t.string 'state' t.string 'registry_url' t.index ['account_id'], name: 'index_credentials_on_account_id' t.index ['project_id'], name: 'index_credentials_on_project_id' t.index ['provider_ref'], name: 'index_credentials_on_provider_ref' end create_table 'uffizzi_core_deployments', force: :cascade do |t| t.bigint 'project_id', null: false t.text 'kind', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'creator_name' t.string 'subdomain' t.string 'state' t.float 'memory_limit' t.bigint 'deployed_by_id' t.bigint 'continuous_preview_id_deprecated' t.jsonb 'continuous_preview_payload' t.string 'creation_source' t.bigint 'compose_file_id' t.bigint 'template_id' t.index ['compose_file_id'], name: 'index_deployments_on_compose_file_id' t.index ['project_id'], name: 'index_deployments_on_project_id' t.index ['template_id'], name: 'index_deployments_on_template_id' end create_table 'uffizzi_core_events', force: :cascade do |t| t.bigint 'activity_item_id', null: false t.string 'state' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['activity_item_id'], name: 'index_events_on_activity_item_id' end create_table 'uffizzi_core_invitations', force: :cascade do |t| t.text 'email', null: false t.text 'token', null: false t.string 'status', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'invited_by_id', null: false t.bigint 'entityable_id', null: false t.string 'entityable_type', null: false t.string 'role', null: false t.bigint 'invitee_id' t.index ['token'], name: 'index_invitations_on_token', unique: true end create_table 'uffizzi_core_memberships', force: :cascade do |t| t.bigint 'user_id', null: false t.bigint 'account_id', null: false t.text 'role', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['account_id'], name: 'index_memberships_on_account_id' t.index ['user_id', 'account_id'], name: 'index_memberships_on_user_id_and_account_id' t.index ['user_id'], name: 'index_memberships_on_user_id' end create_table 'uffizzi_core_payments', force: :cascade do |t| t.bigint 'account_id', null: false t.string 'charge_id' t.string 'status' t.float 'amount' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['account_id'], name: 'index_payments_on_account_id' end create_table 'uffizzi_core_prices', force: :cascade do |t| t.string 'token' t.string 'slug', null: false t.string 'name', null: false t.float 'units_price', null: false t.bigint 'units_amount', null: false t.bigint 'product_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['product_id'], name: 'index_prices_on_product_id' end create_table 'uffizzi_core_products', force: :cascade do |t| t.string 'token' t.string 'slug', null: false t.string 'name', null: false t.string 'type', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'kind' end create_table 'uffizzi_core_projects', force: :cascade do |t| t.text 'name', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'account_id', null: false t.string 'state' t.string 'slug' t.string 'description' t.jsonb 'secrets' t.index ['account_id', 'name'], name: 'index_projects_on_account_id_and_name', unique: true t.index ['account_id'], name: 'index_projects_on_account_id' end create_table 'uffizzi_core_ratings', force: :cascade do |t| t.string 'name' t.string 'state' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false end create_table 'uffizzi_core_repos', force: :cascade do |t| t.string 'namespace' t.string 'name' t.string 'tag' t.string 'type' t.string 'branch' t.bigint 'project_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'description' t.boolean 'is_private' t.string 'slug' t.bigint 'repository_id' t.string 'kind' t.string 'dockerfile_path' t.jsonb 'args' t.string 'dockerfile_context_path' t.boolean 'deploy_preview_when_pull_request_is_opened' t.boolean 'delete_preview_when_pull_request_is_closed' t.boolean 'deploy_preview_when_image_tag_is_created' t.boolean 'delete_preview_when_image_tag_is_updated' t.boolean 'share_to_github' t.integer 'delete_preview_after' t.string 'tag_pattern_deprecated' t.index ['project_id'], name: 'index_repos_on_project_id' end create_table 'uffizzi_core_roles', force: :cascade do |t| t.string 'name' t.string 'resource_type' t.bigint 'resource_id' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.index ['name', 'resource_type', 'resource_id'], name: 'index_roles_on_name_and_resource_type_and_resource_id' t.index ['resource_type', 'resource_id'], name: 'index_roles_on_resource_type_and_resource_id' end create_table 'uffizzi_core_templates', force: :cascade do |t| t.string 'name' t.bigint 'added_by_id' t.jsonb 'payload', null: false t.bigint 'project_id', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'compose_file_id' t.string 'creation_source' t.index ['project_id'], name: 'index_templates_on_project_id' end create_table 'uffizzi_core_user_projects', force: :cascade do |t| t.bigint 'user_id', null: false t.bigint 'project_id', null: false t.text 'role', null: false t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.bigint 'invited_by_id' t.index ['project_id'], name: 'index_user_projects_on_project_id' t.index ['user_id', 'project_id'], name: 'index_user_projects_on_user_id_and_project_id' t.index ['user_id'], name: 'index_user_projects_on_user_id' end create_table 'uffizzi_core_users', force: :cascade do |t| t.string 'first_name' t.string 'last_name' t.string 'email', default: '', null: false t.string 'password_digest', default: '', null: false t.string 'confirmation_token' t.string 'state' t.string 'phone' t.datetime 'created_at', precision: 6, null: false t.datetime 'updated_at', precision: 6, null: false t.string 'github' t.string 'website' t.string 'twitter' t.string 'linkedin' t.string 'devto' t.string 'facebook' t.string 'blog' t.text 'bio' t.string 'status' t.string 'availability' t.string 'primary_skills' t.string 'learning' t.string 'coding_for' t.string 'education' t.string 'title' t.string 'work' t.string 'primary_location' t.string 'creation_source' t.index 'lower((email)::text)', name: 'index_email_on_lower_email', unique: true end create_table 'uffizzi_core_users_roles', id: false, force: :cascade do |t| t.bigint 'user_id' t.bigint 'role_id' t.index ['role_id'], name: 'index_users_roles_on_role_id' t.index ['user_id', 'role_id'], name: 'index_users_roles_on_user_id_and_role_id' t.index ['user_id'], name: 'index_users_roles_on_user_id' end end end ================================================ FILE: db/migrate/20220317112742_remove_secrets_from_projects.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220309110201) class RemoveSecretsFromProjects < ActiveRecord::Migration[6.1] def change remove_column('uffizzi_core_projects', 'secrets') end end ================================================ FILE: db/migrate/20220317112743_create_project_secrets.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220310110150) class CreateProjectSecrets < ActiveRecord::Migration[6.1] def change create_table('uffizzi_core_project_secrets', force: :cascade) do |t| t.bigint('project_id', null: false) t.string('name') t.string('value') t.datetime('created_at', precision: 6, null: false) t.datetime('updated_at', precision: 6, null: false) t.index(['project_id'], name: 'index_project_secrets_on_project_id') end end end ================================================ FILE: db/migrate/20220325113623_add_name_to_uffizzi_containers.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220325113342) class AddNameToUffizziContainers < ActiveRecord::Migration[6.1] def change add_column :uffizzi_core_containers, :name, :string end end ================================================ FILE: db/migrate/20220412133606_rename_project_secrets_to_secrets.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220329123323) class RenameProjectSecretsToSecrets < ActiveRecord::Migration[6.1] def change rename_table(:uffizzi_core_project_secrets, :uffizzi_core_secrets) end end ================================================ FILE: db/migrate/20220412133607_add_resource_to_secrets.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220329124542) class AddResourceToSecrets < ActiveRecord::Migration[6.1] def change add_belongs_to(:uffizzi_core_secrets, :resource, polymorphic: true) end end ================================================ FILE: db/migrate/20220412133608_remove_project_ref_from_secrets.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220329143241) class RemoveProjectRefFromSecrets < ActiveRecord::Migration[6.1] def change remove_reference(:uffizzi_core_secrets, :project) end end ================================================ FILE: db/migrate/20220420103952_add_health_check_to_containers.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220419074956) class AddHealthCheckToContainers < ActiveRecord::Migration[6.1] def change add_column :uffizzi_core_containers, :healthcheck, :jsonb end end ================================================ FILE: db/migrate/20220527135654_rename_name_to_uffizzi_containers.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220525113412) class RenameNameToUffizziContainers < ActiveRecord::Migration[6.1] def change rename_column(:uffizzi_core_containers, :name, :service_name) end end ================================================ FILE: db/migrate/20220617184754_add_volumes_to_uffizzi_core_containers.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220422151523) class AddVolumesToUffizziCoreContainers < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :volumes, :jsonb) end end ================================================ FILE: db/migrate/20220708093405_add_disabled_at_to_deployments.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220704135629) class AddDisabledAtToDeployments < ActiveRecord::Migration[6.1] def up change_table :uffizzi_core_deployments do |t| t.datetime :disabled_at end UffizziCore::Deployment.disabled.update_all('disabled_at = updated_at') end def down change_table :uffizzi_core_deployments do |t| t.remove :disabled_at end end end ================================================ FILE: db/migrate/20220817140346_add_metadata_to_deployment.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20220805164628) class AddMetadataToDeployment < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_deployments, :metadata, :jsonb, default: {}) end end ================================================ FILE: db/migrate/20220901110752_create_host_volume_files.rb ================================================ # frozen_string_literal: true class CreateHostVolumeFiles < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_host_volume_files do |t| t.string :source t.string :path t.boolean :is_file t.binary :payload t.bigint :added_by_id t.timestamps t.references :project, null: false, foreign_key: true, index: { name: :index_host_volume_file_on_project_id }, foreign_key: { to_table: :uffizzi_core_projects } t.references :compose_file, null: false, foreign_key: true, index: { name: :index_host_volume_file_on_compose_file_id }, foreign_key: { to_table: :uffizzi_core_compose_files } end end end ================================================ FILE: db/migrate/20220901165313_create_container_host_volume_files.rb ================================================ # frozen_string_literal: true class CreateContainerHostVolumeFiles < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_container_host_volume_files do |t| t.string :source_path t.timestamps t.references :container, null: false, foreign_key: true, index: { name: :uf_core_cont_h_v_on_cont }, foreign_key: { to_table: :uffizzi_core_containers } t.references :host_volume_file, null: false, foreign_key: true, index: { name: :uf_core_cont_h_v_on_h_v_file }, foreign_key: { to_table: :uffizzi_core_host_volume_files } end end end ================================================ FILE: db/migrate/20220927113647_add_additional_subdomains_to_containers.rb ================================================ # frozen_string_literal: true class AddAdditionalSubdomainsToContainers < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :additional_subdomains, :string, array: true, default: []) end end ================================================ FILE: db/migrate/20230111000000_add_state_to_memberships.rb ================================================ # frozen_string_literal: true class AddStateToMemberships < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_memberships, :state, :string) end end ================================================ FILE: db/migrate/20230306142805_add_last_deploy_at_to_deployments.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20230306142513) class AddLastDeployAtToDeployments < ActiveRecord::Migration[6.1] def up add_column :uffizzi_core_deployments, :last_deploy_at, :datetime UffizziCore::Deployment.update_all('last_deploy_at = updated_at') end def down remove_column :uffizzi_core_deployments, :last_deploy_at end end ================================================ FILE: db/migrate/20230406154547_add_full_image_name_to_container.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20230406154451) class AddFullImageNameToContainer < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_containers, :full_image_name, :string) end end ================================================ FILE: db/migrate/20230531135739_create_deployment_events.rb ================================================ # frozen_string_literal: true class CreateDeploymentEvents < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_deployment_events do |t| t.string :deployment_state t.string :message t.timestamps t.references :deployment, null: false, index: { name: :uf_core_dep_events_on_dep }, foreign_key: { to_table: :uffizzi_core_deployments } end end end ================================================ FILE: db/migrate/20230613110517_create_clusters.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20230613101901) class CreateClusters < ActiveRecord::Migration[6.1] def change create_table('uffizzi_core_clusters', force: :cascade) do |t| t.references :project, null: false, foreign_key: true, index: { name: :index_cluster_on_project_id }, foreign_key: { to_table: :uffizzi_core_projects } t.bigint 'deployed_by_id', foreign_key: true t.string 'state' t.string 'name' t.text 'manifest' t.text 'kubeconfig' t.timestamps end end end ================================================ FILE: db/migrate/20230711101901_add_host_to_clusters.rb ================================================ # frozen_string_literal: true class AddHostToClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :host, :string) end end ================================================ FILE: db/migrate/20230824150022_update_name_constraint_to_projects.rb ================================================ # frozen_string_literal: true class UpdateNameConstraintToProjects < ActiveRecord::Migration[6.1] def change remove_index(:uffizzi_core_projects, [:account_id, :name]) add_index(:uffizzi_core_projects, [:account_id, :name], unique: true, where: "state = 'active'", name: 'proj_uniq_name') end end ================================================ FILE: db/migrate/20231009103942_add_source_to_uffizzi_core_clusters.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20230810140316) class AddSourceToUffizziCoreClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :creation_source, :string) end end ================================================ FILE: db/migrate/20231009163516_create_uffizzi_core_kubernetes_distributions.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20231009162719) class CreateUffizziCoreKubernetesDistributions < ActiveRecord::Migration[6.1] def change create_table :uffizzi_core_kubernetes_distributions do |t| t.string :version t.string :distro t.string :image t.boolean :default t.timestamps end end end ================================================ FILE: db/migrate/20231009201239_add_kubernetes_distribution_id_to_uffizzi_core_clusters.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20231009182412) class AddKubernetesDistributionIdToUffizziCoreClusters < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :kubernetes_distribution_id, :integer, foreign_key: true) end end ================================================ FILE: db/migrate/20240301200916_add_node_selector_to_cluster.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20240301200235) class AddNodeSelectorToCluster < ActiveRecord::Migration[6.1] def change add_column(:uffizzi_core_clusters, :node_selector, :string) end end ================================================ FILE: db/migrate/20240314170425_delete_node_selector_from_cluster.uffizzi_core.rb ================================================ # frozen_string_literal: true # This migration comes from uffizzi_core (originally 20240314170113) class DeleteNodeSelectorFromCluster < ActiveRecord::Migration[6.1] def change remove_column(:uffizzi_core_clusters, :node_selector, :string) end end ================================================ FILE: db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema.define(version: 2024_03_14_170425) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "uffizzi_core_accounts", force: :cascade do |t| t.text "name" t.text "kind", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "customer_token" t.string "state" t.string "subscription_token" t.datetime "payment_issue_at" t.string "domain" t.boolean "sso_enabled", default: false t.bigint "owner_id" t.integer "container_memory_limit" t.string "workos_organization_id" t.string "sso_state" t.index ["customer_token"], name: "index_accounts_on_customer_token", unique: true t.index ["domain"], name: "index_accounts_on_domain", unique: true t.index ["subscription_token"], name: "index_accounts_on_subscription_token", unique: true end create_table "uffizzi_core_activity_items", force: :cascade do |t| t.bigint "deployment_id", null: false t.string "namespace" t.string "name" t.string "tag" t.string "branch" t.string "type" t.bigint "container_id", null: false t.string "commit" t.string "commit_message" t.bigint "build_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.jsonb "data", default: {}, null: false t.string "digest" t.index ["container_id"], name: "index_activity_items_on_container_id" t.index ["deployment_id"], name: "index_activity_items_on_deployment_id" end create_table "uffizzi_core_builds", force: :cascade do |t| t.bigint "repo_id", null: false t.string "build_id" t.string "repository" t.string "branch" t.string "commit" t.string "committer" t.string "message" t.string "log_url" t.integer "status" t.datetime "started_at" t.datetime "ended_at" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.boolean "deployed" t.index ["build_id"], name: "index_builds_on_build_id", unique: true t.index ["repo_id"], name: "index_builds_on_repo_id" end create_table "uffizzi_core_clusters", force: :cascade do |t| t.bigint "project_id", null: false t.bigint "deployed_by_id" t.string "state" t.string "name" t.text "manifest" t.text "kubeconfig" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "host" t.string "creation_source" t.integer "kubernetes_distribution_id" t.index ["project_id"], name: "index_cluster_on_project_id" end create_table "uffizzi_core_comments", force: :cascade do |t| t.string "commentable_type" t.bigint "commentable_id" t.text "content" t.bigint "user_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "ancestry" t.integer "ancestry_depth", default: 0 t.index ["ancestry"], name: "index_comments_on_ancestry" t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable" t.index ["user_id"], name: "index_comments_on_user_id" end create_table "uffizzi_core_compose_files", force: :cascade do |t| t.string "source" t.bigint "repository_id" t.string "branch" t.string "path" t.string "auto_deploy" t.bigint "added_by_id" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "state" t.jsonb "payload", default: {}, null: false t.text "content" t.string "kind", default: "main" t.index ["project_id"], name: "index_compose_files_on_project_id" end create_table "uffizzi_core_config_files", force: :cascade do |t| t.string "filename" t.string "kind" t.bigint "added_by_id" t.text "payload" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "compose_file_id" t.string "creation_source" t.string "source" t.index ["compose_file_id"], name: "index_config_files_on_compose_file_id" t.index ["project_id"], name: "index_config_files_on_project_id" end create_table "uffizzi_core_container_config_files", force: :cascade do |t| t.string "mount_path" t.bigint "container_id", null: false t.bigint "config_file_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["config_file_id"], name: "index_container_config_files_on_config_file_id" t.index ["container_id"], name: "index_container_config_files_on_container_id" end create_table "uffizzi_core_container_host_volume_files", force: :cascade do |t| t.string "source_path" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "container_id", null: false t.bigint "host_volume_file_id", null: false t.index ["container_id"], name: "uf_core_cont_h_v_on_cont" t.index ["host_volume_file_id"], name: "uf_core_cont_h_v_on_h_v_file" end create_table "uffizzi_core_containers", force: :cascade do |t| t.string "image" t.string "tag" t.jsonb "variables" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "deployment_id" t.boolean "public", default: false, null: false t.integer "port" t.bigint "repo_id" t.string "state" t.string "continuously_deploy", null: false t.string "kind", default: "user" t.integer "target_port" t.string "controller_name" t.boolean "receive_incoming_requests" t.integer "memory_request" t.integer "memory_limit" t.jsonb "secret_variables" t.string "entrypoint" t.string "command" t.string "service_name" t.jsonb "healthcheck" t.jsonb "volumes" t.string "additional_subdomains", default: [], array: true t.string "full_image_name" t.index ["deployment_id"], name: "index_containers_on_deployment_id" t.index ["repo_id"], name: "index_containers_on_repo_id" end create_table "uffizzi_core_coupons", force: :cascade do |t| t.string "token" t.string "name", null: false t.string "currency", null: false t.bigint "amount_off", null: false t.string "duration", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_credentials", force: :cascade do |t| t.string "type" t.string "username" t.string "password" t.bigint "project_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "provider_ref" t.bigint "account_id" t.string "state" t.string "registry_url" t.index ["account_id"], name: "index_credentials_on_account_id" t.index ["project_id"], name: "index_credentials_on_project_id" t.index ["provider_ref"], name: "index_credentials_on_provider_ref" end create_table "uffizzi_core_deployment_events", force: :cascade do |t| t.string "deployment_state" t.string "message" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "deployment_id", null: false t.index ["deployment_id"], name: "uf_core_dep_events_on_dep" end create_table "uffizzi_core_deployments", force: :cascade do |t| t.bigint "project_id", null: false t.text "kind", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "creator_name" t.string "subdomain" t.string "state" t.float "memory_limit" t.bigint "deployed_by_id" t.bigint "continuous_preview_id_deprecated" t.jsonb "continuous_preview_payload" t.string "creation_source" t.bigint "compose_file_id" t.bigint "template_id" t.datetime "disabled_at" t.jsonb "metadata", default: {} t.datetime "last_deploy_at" t.index ["compose_file_id"], name: "index_deployments_on_compose_file_id" t.index ["project_id"], name: "index_deployments_on_project_id" t.index ["template_id"], name: "index_deployments_on_template_id" end create_table "uffizzi_core_events", force: :cascade do |t| t.bigint "activity_item_id", null: false t.string "state" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["activity_item_id"], name: "index_events_on_activity_item_id" end create_table "uffizzi_core_host_volume_files", force: :cascade do |t| t.string "source" t.string "path" t.boolean "is_file" t.binary "payload" t.bigint "added_by_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "project_id", null: false t.bigint "compose_file_id", null: false t.index ["compose_file_id"], name: "index_host_volume_file_on_compose_file_id" t.index ["project_id"], name: "index_host_volume_file_on_project_id" end create_table "uffizzi_core_invitations", force: :cascade do |t| t.text "email", null: false t.text "token", null: false t.string "status", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "invited_by_id", null: false t.bigint "entityable_id", null: false t.string "entityable_type", null: false t.string "role", null: false t.bigint "invitee_id" t.index ["token"], name: "index_invitations_on_token", unique: true end create_table "uffizzi_core_kubernetes_distributions", force: :cascade do |t| t.string "version" t.string "distro" t.string "image" t.boolean "default" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_memberships", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "account_id", null: false t.text "role", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "state" t.index ["account_id"], name: "index_memberships_on_account_id" t.index ["user_id", "account_id"], name: "index_memberships_on_user_id_and_account_id" t.index ["user_id"], name: "index_memberships_on_user_id" end create_table "uffizzi_core_payments", force: :cascade do |t| t.bigint "account_id", null: false t.string "charge_id" t.string "status" t.float "amount" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["account_id"], name: "index_payments_on_account_id" end create_table "uffizzi_core_prices", force: :cascade do |t| t.string "token" t.string "slug", null: false t.string "name", null: false t.float "units_price", null: false t.bigint "units_amount", null: false t.bigint "product_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["product_id"], name: "index_prices_on_product_id" end create_table "uffizzi_core_products", force: :cascade do |t| t.string "token" t.string "slug", null: false t.string "name", null: false t.string "type", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "kind" end create_table "uffizzi_core_projects", force: :cascade do |t| t.text "name", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "account_id", null: false t.string "state" t.string "slug" t.string "description" t.index ["account_id", "name"], name: "proj_uniq_name", unique: true, where: "((state)::text = 'active'::text)" t.index ["account_id"], name: "index_projects_on_account_id" end create_table "uffizzi_core_ratings", force: :cascade do |t| t.string "name" t.string "state" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end create_table "uffizzi_core_repos", force: :cascade do |t| t.string "namespace" t.string "name" t.string "tag" t.string "type" t.string "branch" t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "description" t.boolean "is_private" t.string "slug" t.bigint "repository_id" t.string "kind" t.string "dockerfile_path" t.jsonb "args" t.string "dockerfile_context_path" t.boolean "deploy_preview_when_pull_request_is_opened" t.boolean "delete_preview_when_pull_request_is_closed" t.boolean "deploy_preview_when_image_tag_is_created" t.boolean "delete_preview_when_image_tag_is_updated" t.boolean "share_to_github" t.integer "delete_preview_after" t.string "tag_pattern_deprecated" t.index ["project_id"], name: "index_repos_on_project_id" end create_table "uffizzi_core_roles", force: :cascade do |t| t.string "name" t.string "resource_type" t.bigint "resource_id" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" t.index ["resource_type", "resource_id"], name: "index_roles_on_resource_type_and_resource_id" end create_table "uffizzi_core_secrets", force: :cascade do |t| t.string "name" t.string "value" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "resource_type" t.bigint "resource_id" t.index ["resource_type", "resource_id"], name: "index_uffizzi_core_secrets_on_resource" end create_table "uffizzi_core_templates", force: :cascade do |t| t.string "name" t.bigint "added_by_id" t.jsonb "payload", null: false t.bigint "project_id", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "compose_file_id" t.string "creation_source" t.index ["project_id"], name: "index_templates_on_project_id" end create_table "uffizzi_core_user_projects", force: :cascade do |t| t.bigint "user_id", null: false t.bigint "project_id", null: false t.text "role", null: false t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.bigint "invited_by_id" t.index ["project_id"], name: "index_user_projects_on_project_id" t.index ["user_id", "project_id"], name: "index_user_projects_on_user_id_and_project_id" t.index ["user_id"], name: "index_user_projects_on_user_id" end create_table "uffizzi_core_users", force: :cascade do |t| t.string "first_name" t.string "last_name" t.string "email", default: "", null: false t.string "password_digest", default: "", null: false t.string "confirmation_token" t.string "state" t.string "phone" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.string "github" t.string "website" t.string "twitter" t.string "linkedin" t.string "devto" t.string "facebook" t.string "blog" t.text "bio" t.string "status" t.string "availability" t.string "primary_skills" t.string "learning" t.string "coding_for" t.string "education" t.string "title" t.string "work" t.string "primary_location" t.string "creation_source" t.index "lower((email)::text)", name: "index_email_on_lower_email", unique: true end create_table "uffizzi_core_users_roles", id: false, force: :cascade do |t| t.bigint "user_id" t.bigint "role_id" t.index ["role_id"], name: "index_users_roles_on_role_id" t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" t.index ["user_id"], name: "index_users_roles_on_user_id" end add_foreign_key "uffizzi_core_clusters", "uffizzi_core_projects", column: "project_id" add_foreign_key "uffizzi_core_container_host_volume_files", "uffizzi_core_containers", column: "container_id" add_foreign_key "uffizzi_core_container_host_volume_files", "uffizzi_core_host_volume_files", column: "host_volume_file_id" add_foreign_key "uffizzi_core_deployment_events", "uffizzi_core_deployments", column: "deployment_id" add_foreign_key "uffizzi_core_host_volume_files", "uffizzi_core_compose_files", column: "compose_file_id" add_foreign_key "uffizzi_core_host_volume_files", "uffizzi_core_projects", column: "project_id" end ================================================ FILE: db/seeds.rb ================================================ # frozen_string_literal: true puts 'Creating User' user = UffizziCore::User.create!( email: 'admin@uffizzi.com', password: 'password', state: UffizziCore::User::STATE_ACTIVE, creation_source: UffizziCore::User.creation_source.system, ) puts 'Creating Accounts' personal_account = UffizziCore::Account.create!( owner: user, name: 'personal', state: UffizziCore::Account::STATE_ACTIVE, kind: UffizziCore::Account.kind.personal, ) organizational_account = UffizziCore::Account.create!( owner: user, name: 'organizational', state: UffizziCore::Account::STATE_ACTIVE, kind: UffizziCore::Account.kind.organizational, ) user.memberships.create!(account: personal_account, role: UffizziCore::Membership.role.admin) user.memberships.create!(account: organizational_account, role: UffizziCore::Membership.role.admin) personal_project = personal_account.projects.create!(name: 'default', slug: 'default', state: UffizziCore::Project::STATE_ACTIVE) personal_project.user_projects.create!(user: user, role: UffizziCore::UserProject.role.admin) organizational_project = organizational_account.projects.create!(name: 'uffizzi', slug: 'uffizzi', state: UffizziCore::Project::STATE_ACTIVE) organizational_project.user_projects.create!(user: user, role: UffizziCore::UserProject.role.admin) puts 'Creating Kubernetes Distributions' UffizziCore::KubernetesDistribution.create(distro: 'k3s', version: '1.25', image: 'rancher/k3s:v1.25.14-k3s1') UffizziCore::KubernetesDistribution.create(distro: 'k3s', version: '1.26', image: 'rancher/k3s:v1.26.9-k3s1') UffizziCore::KubernetesDistribution.create(distro: 'k3s', version: '1.27', default: true, image: 'rancher/k3s:v1.27.6-k3s1') UffizziCore::KubernetesDistribution.create(distro: 'k3s', version: '1.28', image: 'rancher/k3s:v1.28.2-k3s1') ================================================ FILE: docker-compose.yml ================================================ version: '3.9' x-web-environment: &web-environment RAILS_SECRET_KEY_BASE: 44a599292ee918ca52c5060bb73b9a5b754628d6d67c64d0066c2ecf25381ef67b2b7a9981332316cc09e0a4bdbd08f07b7a9277d77fd4b4f2a39a488860c18c DATABASE_HOST: db DATABASE_USERNAME: postgres DATABASE_PASSWORD: postgres BUNDLE_PATH: /bundle_cache GEM_HOME: /bundle_cache GEM_PATH: /bundle_cache RAILS_WORKERS_COUNT: 0 RAILS_THREADS_COUNT: 10 SIDEKIQ_CONCURRENCY: 1 RAILS_PORT: 7001 RAILS_ENV: development ALLOWED_HOSTS: lvh.me,.lvh.me,lvh.me:1313,.lvh.me:1313,lvh.me:7001,.lvh.me:7001,localhost,.ngrok.io,localhost:3000,web REDIS_URL: redis://redis APP_URL: http://web:7001 RUBYGEMS_API_KEY: ${RUBYGEMS_API_KEY} services: web: build: . environment: *web-environment env_file: - .env volumes: &web-volumes - &app-volume .:/app:cached - ~/.ssh:/root/.ssh - &bash-history ~/.bash_history:/root/.bash_history - &bundle-cache-volume bundle_cache:/bundle_cache ports: - 7001:7001 depends_on: - db command: bash -c "bundle install && bundle exec rails db:create db:migrate && bundle exec puma -C config/puma.rb" core: build: core environment: *web-environment env_file: - .env volumes: - ./core:/gem:cached - *bash-history - *bundle-cache-volume depends_on: - db command: bash -c "bundle install && bundle exec rails db:create db:migrate" db: image: postgres:11.4 ports: - 5432:5432 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres sidekiq: build: . environment: *web-environment env_file: - .env volumes: *web-volumes depends_on: - redis - web command: bash -c "bundle exec sidekiq -C /app/config/sidekiq.yml" redis: image: redis bundle_cache: image: busybox volumes: - *bundle-cache-volume volumes: bundle_cache: core: networks: default: name: 'uffizzi_default_network' ================================================ FILE: docs/continuous-previews.md ================================================ ## INTRODUCTION- # Why this Open Source Project is Needed: These are our observations after 2+ years interviewing over 150+ developers, DevOps, and cross-functional team members across a variety of industries and at various levels of business maturity - from nascent start-ups to long-running enterprises: -It's universally beneficial to bring QA into the Development process, to catch issues early, iterate quickly, and to merge clean code into Develop or Main. -Virtually all teams from can benefit from an on-demand preview environment capability and industry defined best practices. -Many teams - typically more advanced teams with time, resources, and expertise - build and maintain an Internal Development Platform (IDP) to address a Preview requirement. -Most teams lack the time, resources, and expertise to build and maintain an internal capability - for most a Preview tool is on a wish list. -As a community we are not actively collaborating or innovating towards a well-defined Preview Process and technical capability: we should be. -There's no official Open Source Preview specific tool within the CNCF. There are several CI, CD, and deployment tools within the CNCF and more broadly across the industry. These tools, while useful, are not purpose built for the task of Previewing. They fit in the Ops and DevOps lane and do not provide a Developer friendly interface for configuration. -Docker compose is a configuration-as-code format that best balances DevOps requirements (GitOps, Infrastructure-as-code, Functionality) with Developer accessibility. -Previewing should be more about collaboration between the teammate(s) writing the code and the teammate(s) previewing what they've done than it is about deploying - "Individuals and Interactions over processes and tools." https://agilemanifesto.org/ -The maturity of cloud native development, containerization, and container orchestration have laid the foundation for a Preview capability and best practices that nearly all teams can benefit from. # What this Project/Team aims to accomplish: -Lead a community effort to define best practices for Previewing and to provide a modular, purpose-built, open source Preview engine that will more broadly enable teams to benefit from a Preview capability. -Provide well-defined guidelines for Previewing - see https://continuous-previews.org ================================================ FILE: docs/quickstart-guide.md ================================================ # Quickstart guide This guide describes configuring a [GitHub Action](https://github.com/UffizziCloud/preview-action) to create event-driven test environments for every pull requests. ## Reusable Workflow We've published a [Reusable Workflow](https://docs.github.com/en/actions/using-workflows/reusing-workflows#calling-a-reusable-workflow) for your GitHub Actions. This can handle creating, updating, and deleting test lightweight test environments with Uffizzi. It will also publish environment URLs (also known as "preview URLs") within comments on your pull requests. We recommend using this workflow instead of the individual actions. ### Workflow Calling Example This example builds and publishes an image to Docker Hub. It then renders a Uffizzi Docker Compose file from a template and caches it. Finally, calls the reusable workflow to create, update, or delete the preview associated with this pull request. ``` name: Build Images and Handle Uffizzi Previews. on: pull_request: types: [opened,reopened,synchronize,closed] jobs: build-image: name: Build and Push image runs-on: ubuntu-latest outputs: # You'll need this output to later render the Compose file. tags: ${{ steps.meta.outputs.tags }} steps: - name: Login to DockerHub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Checkout git repo uses: actions/checkout@v3 - name: Docker metadata id: meta uses: docker/metadata-action@v3 with: images: example/image - name: Build and Push Image to Docker Hub uses: docker/build-push-action@v2 with: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} render-compose-file: name: Render Docker Compose File runs-on: ubuntu-latest needs: - build-image outputs: compose-file-cache-key: ${{ steps.hash.outputs.hash }} compose-file-cache-path: docker-compose.rendered.yml steps: - name: Checkout git repo uses: actions/checkout@v3 - name: Render Compose File run: | IMAGE=$(echo ${{ needs.build-image.outputs.tags }}) export IMAGE # Render simple template from environment variables. envsubst < docker-compose.template.yml > docker-compose.rendered.yml cat docker-compose.rendered.yml - name: Hash Rendered Compose File id: hash run: echo "::set-output name=hash::$(md5sum docker-compose.rendered.yml | awk '{ print $1 }')" - name: Cache Rendered Compose File uses: actions/cache@v3 with: path: docker-compose.rendered.yml key: ${{ steps.hash.outputs.hash }} deploy-uffizzi-preview: name: Use Remote Workflow to Preview on Uffizzi needs: render-compose-file uses: UffizziCloud/preview-action/.github/workflows/reusable.yaml with: compose-file-cache-key: ${{ needs.render-compose-file.outputs.compose-file-cache-key }} compose-file-cache-path: ${{ needs.render-compose-file.outputs.compose-file-cache-path }} username: user@example.com server: https://uffizzi.example.com/ project: default secrets: password: ${{ secrets.UFFIZZI_PASSWORD }} permissions: contents: read pull-requests: write ``` ### Workflow Inputs #### `compose-file-cache-key` **Required** Key of hashed compose file, using [GitHub's `cache` action](https://github.com/marketplace/actions/cache) #### `compose-file-cache-path` **Required** Path of hashed compose file, using [GitHub's `cache` action](https://github.com/marketplace/actions/cache) #### `username` **Required** Uffizzi username. To use the Uffizzi managed API service, you must first [create an account here](https://app.uffizzi.com/sign_up). #### `project` **Required** Uffizzi project name #### `server` URL of your Uffizzi installation. To use the Uffizzi managed API service, set this value to `https://app.uffizzi.com`. ### Workflow Secrets #### `password` Your Uffizzi password. Specify a GitHub Encrypted Secret and use it! See example above. # The Action Itself If you wish to use this action by itself outside of the reusable workflow, you can. It will only create new previews, not update nor delete. ## Inputs ### `compose-file` **Required** Path to a compose file within your repository ### `username` **Required** Uffizzi username ### `project` **Required** Uffizzi project name ### `server` URL of your Uffizzi installation ### `password` Your Uffizzi password. Specify a GitHub Encrypted Secret and use it! See example below. ## Example usage ```yaml uses: UffizziCloud/preview-action@v2 with: compose-file: 'docker-compose.uffizzi.yaml' username: 'admin@uffizzi.com' server: 'https://app.uffizzi.com' project: 'default' password: ${{ secrets.UFFIZZI_PASSWORD }} ``` ================================================ FILE: lib/assets/.keep ================================================ ================================================ FILE: lib/tasks/.keep ================================================ ================================================ FILE: log/.keep ================================================ ================================================ FILE: public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

================================================ FILE: public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

If you are the application owner check the logs for more information.

================================================ FILE: public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

================================================ FILE: public/robots.txt ================================================ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file ================================================ FILE: storage/.keep ================================================ ================================================ FILE: test/application_system_test_case.rb ================================================ # frozen_string_literal: true require 'test_helper' class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] end ================================================ FILE: test/channels/application_cable/connection_test.rb ================================================ # frozen_string_literal: true require 'test_helper' class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase # test "connects with cookies" do # cookies.signed[:user_id] = 42 # # connect # # assert_equal connection.user_id, "42" # end end ================================================ FILE: test/controllers/.keep ================================================ ================================================ FILE: test/fixtures/.keep ================================================ ================================================ FILE: test/fixtures/files/.keep ================================================ ================================================ FILE: test/helpers/.keep ================================================ ================================================ FILE: test/integration/.keep ================================================ ================================================ FILE: test/mailers/.keep ================================================ ================================================ FILE: test/models/.keep ================================================ ================================================ FILE: test/system/.keep ================================================ ================================================ FILE: test/test_helper.rb ================================================ # frozen_string_literal: true ENV['RAILS_ENV'] ||= 'test' require_relative '../config/environment' require 'rails/test_help' class ActiveSupport::TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end ================================================ FILE: tmp/.keep ================================================ ================================================ FILE: uffizzi-compose-example.yml ================================================ services: redis: image: redis:latest postgres: image: postgres:9.6 environment: POSTGRES_USER: ${VOTE_APP_POSTGRES.USER} POSTGRES_PASSWORD: ${VOTE_APP_POSTGRES.PASSWORD} nginx: image: nginx:latest configs: - source: vote.conf target: /etc/nginx/conf.d worker: build: context: https://github.com/UffizziCloud/example-voting-worker:main dockerfile: Dockerfile deploy: resources: limits: memory: 250M vote: build: context: https://github.com/UffizziCloud/example-voting-vote:main dockerfile: Dockerfile deploy: resources: limits: memory: 250M result: build: context: https://github.com/UffizziCloud/example-voting-result:main dockerfile: Dockerfile continuous_preview: deploy_preview_when_pull_request_is_opened: false delete_preview_when_pull_request_is_closed: false deploy_preview_for_image_tag_PR_#-branchname: true delete_preview_when_pull_request_is_closed: false delete_preview_in_x_hours: 24 share_to_github: true ingress: service: nginx port: 8080 ================================================ FILE: vendor/.keep ================================================