From 9498dc957345829f29fe0bc4e55c969783b457be Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 7 Jun 2023 15:09:14 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/rules.gitlab-ci.yml | 42 ++--- .rubocop.yml | 2 + .../rspec/factory_bot/avoid_create.yml | 47 ++++- app/controllers/clusters/base_controller.rb | 2 +- .../clusters/clusters_controller.rb | 1 - .../metrics/dashboard/prometheus_api_proxy.rb | 53 ------ .../environments/prometheus_api_controller.rb | 20 --- .../projects/prometheus/alerts_controller.rb | 21 --- app/models/organizations/organization.rb | 1 + app/models/user.rb | 21 ++- app/validators/abstract_path_validator.rb | 17 +- .../organizations/path_validator.rb | 15 ++ db/docs/subscription_add_on_purchases.yml | 10 ++ db/docs/subscription_add_ons.yml | 10 ++ ...30531134916_create_subscription_add_ons.rb | 12 ++ ...01_create_subscription_add_on_purchases.rb | 18 ++ ..._on_id_on_subscription_add_on_purchases.rb | 18 ++ ...ace_id_on_subscription_add_on_purchases.rb | 15 ++ db/schema_migrations/20230531134916 | 1 + db/schema_migrations/20230531135001 | 1 + db/schema_migrations/20230531142032 | 1 + db/schema_migrations/20230531142053 | 1 + db/structure.sql | 61 +++++++ .../geo/replication/container_registry.md | 21 +-- .../geo/replication/troubleshooting.md | 4 +- doc/administration/geo/setup/index.md | 8 +- doc/development/gitlab_shell/index.md | 2 +- doc/raketasks/restore_gitlab.md | 2 +- .../left_sidebar/img/sidebar_bottom_v16_1.png | Bin 22244 -> 7229 bytes .../left_sidebar/img/sidebar_middle_v16_1.png | Bin 21452 -> 7789 bytes doc/user/workspace/index.md | 2 +- .../event_definition.yml | 16 ++ .../metric_definition.yml | 5 +- lib/api/ml/mlflow/entrypoint.rb | 2 +- .../analytics/internal_events_generator.rb | 114 +++++++++--- lib/gitlab/path_regex.rb | 16 ++ lib/google_cloud/authentication.rb | 20 +++ lib/google_cloud/logging_service/logger.rb | 41 +++++ locale/gitlab.pot | 12 ++ .../admin/clusters_controller_spec.rb | 68 -------- .../groups/clusters_controller_spec.rb | 75 -------- .../projects/clusters_controller_spec.rb | 86 --------- .../prometheus_api_controller_spec.rb | 96 ----------- .../prometheus/alerts_controller_spec.rb | 71 -------- spec/factories/merge_request_diffs.rb | 2 +- spec/factories/merge_requests.rb | 8 +- .../clusters/cluster_health_dashboard_spec.rb | 126 -------------- .../internal_events_generator_spec.rb | 102 +++++++++-- spec/lib/gitlab/path_regex_spec.rb | 17 ++ spec/lib/google_cloud/authentication_spec.rb | 53 ++++++ .../logging_service/logger_spec.rb | 61 +++++++ spec/models/merge_request_diff_spec.rb | 6 +- .../models/organizations/organization_spec.rb | 31 ++++ .../merge_request_diff_preloader_spec.rb | 2 +- spec/models/user_spec.rb | 99 +++++++++-- .../api/ml/mlflow/experiments_spec.rb | 5 - spec/requests/api/ml/mlflow/runs_spec.rb | 5 - ...erge_request_without_merge_request_diff.rb | 7 - .../prometheus_api_proxy_shared_examples.rb | 163 ------------------ .../api/ml/mlflow/mlflow_shared_examples.rb | 9 +- .../organizations/path_validator_spec.rb | 40 +++++ 61 files changed, 857 insertions(+), 930 deletions(-) delete mode 100644 app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb delete mode 100644 app/controllers/projects/environments/prometheus_api_controller.rb create mode 100644 app/validators/organizations/path_validator.rb create mode 100644 db/docs/subscription_add_on_purchases.yml create mode 100644 db/docs/subscription_add_ons.yml create mode 100644 db/migrate/20230531134916_create_subscription_add_ons.rb create mode 100644 db/migrate/20230531135001_create_subscription_add_on_purchases.rb create mode 100644 db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb create mode 100644 db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb create mode 100644 db/schema_migrations/20230531134916 create mode 100644 db/schema_migrations/20230531135001 create mode 100644 db/schema_migrations/20230531142032 create mode 100644 db/schema_migrations/20230531142053 create mode 100644 generator_templates/gitlab_internal_events/event_definition.yml create mode 100644 lib/google_cloud/authentication.rb create mode 100644 lib/google_cloud/logging_service/logger.rb delete mode 100644 spec/controllers/projects/environments/prometheus_api_controller_spec.rb delete mode 100644 spec/features/clusters/cluster_health_dashboard_spec.rb create mode 100644 spec/lib/google_cloud/authentication_spec.rb create mode 100644 spec/lib/google_cloud/logging_service/logger_spec.rb delete mode 100644 spec/support/helpers/models/merge_request_without_merge_request_diff.rb delete mode 100644 spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb create mode 100644 spec/validators/organizations/path_validator_spec.rb diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index f57662acac9..b4f268a6563 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -161,16 +161,14 @@ - "scripts/rspec_helpers.sh" .ci-build-images-patterns: &ci-build-images-patterns - - ".gitlab-ci.yml" - ".gitlab/ci/build-images.gitlab-ci.yml" .ci-review-patterns: &ci-review-patterns - - ".gitlab-ci.yml" - ".gitlab/ci/frontend.gitlab-ci.yml" - ".gitlab/ci/build-images.gitlab-ci.yml" - ".gitlab/ci/review.gitlab-ci.yml" - ".gitlab/ci/cng/**/*" - - ".gitlab/ci/review-apps/**/*" + - ".gitlab/ci/review-apps/*.yml" - "scripts/review_apps/**/*" - "scripts/trigger-build.rb" - "{,ee/,jh/}{bin,config}/**/*.rb" @@ -179,13 +177,12 @@ - "lib/gitlab/ci/templates/**/*.gitlab-ci.yml" .ci-qa-patterns: &ci-qa-patterns - - ".gitlab-ci.yml" - ".gitlab/ci/frontend.gitlab-ci.yml" - ".gitlab/ci/build-images.gitlab-ci.yml" - ".gitlab/ci/qa.gitlab-ci.yml" - ".gitlab/ci/package-and-test/*.yml" - - ".gitlab/ci/review-apps/qa.gitlab-ci.yml" - - ".gitlab/ci/review-apps/rules.gitlab-ci.yml" + - ".gitlab/ci/package-and-test-nightly/*.yml" + - ".gitlab/ci/qa-common/*.yml" .gitaly-patterns: &gitaly-patterns - "GITALY_SERVER_VERSION" @@ -323,8 +320,9 @@ - "{,ee/,jh/}{bin,config,db,generator_templates,lib}/**/*" - "{,ee/,jh/}spec/**/*" # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" + - ".gitlab/ci/database.gitlab-ci.yml" + - ".gitlab/ci/rails.gitlab-ci.yml" + - ".gitlab/ci/rails/*.yml" - "*_VERSION" - "scripts/rspec_helpers.sh" # Mapped patterns (see tests.yml) @@ -377,8 +375,7 @@ - "GITALY_SERVER_VERSION" - "lib/gitlab/setup_helper.rb" # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" + - ".gitlab/ci/database.gitlab-ci.yml" # DB backup patterns .db-backup-patterns: &db-backup-patterns @@ -402,7 +399,7 @@ - ".dockerignore" - "{,jh/}qa/**/*" -# Code patterns + .ci-patterns +# Code patterns .code-patterns: &code-patterns - "{package.json,yarn.lock}" - ".browserslistrc" @@ -412,7 +409,6 @@ - "Dockerfile.assets" - "vendor/assets/**/*" - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - - ".gitlab-ci.yml" - "*_VERSION" - "{,jh/}Gemfile{,.lock}" - "Rakefile" @@ -420,9 +416,6 @@ - "config.ru" - "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "doc/api/graphql/reference/*" # Files in this folder are auto-generated - # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" # Mapped patterns (see tests.yml) - "data/whats_new/*.yml" - "doc/index.md" @@ -444,9 +437,6 @@ - "config.ru" - "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*" - "doc/api/graphql/reference/*" # Files in this folder are auto-generated - # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" # Backstage changes - "Dangerfile" - "danger/**/*" @@ -475,9 +465,6 @@ - "config.ru" - "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*" - "doc/api/graphql/reference/*" # Files in this folder are auto-generated - # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" # QA changes - ".dockerignore" - "{,jh/}qa/**/*" @@ -495,7 +482,6 @@ - "Dockerfile.assets" - "vendor/assets/**/*" - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - - ".gitlab-ci.yml" - "*_VERSION" - "{,jh/}Gemfile{,.lock}" - "Rakefile" @@ -503,9 +489,6 @@ - "config.ru" - "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*" - "doc/api/graphql/reference/*" # Files in this folder are auto-generated - # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" # Backstage changes - "Dangerfile" - "danger/**/*" @@ -533,7 +516,6 @@ - "Dockerfile.assets" - "vendor/assets/**/*" - ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}" - - ".gitlab-ci.yml" - "*_VERSION" - "{,jh/}Gemfile{,.lock}" - "Rakefile" @@ -541,9 +523,6 @@ - "config.ru" - "{,ee/,jh/}{app,bin,config,db,generator_templates,haml_lint,lib,locale,public,scripts,storybook,symbol,vendor}/**/*" - "doc/api/graphql/reference/*" # Files in this folder are auto-generated - # CI changes - - ".gitlab-ci.yml" - - ".gitlab/ci/**/*" # Backstage changes - "Dangerfile" - "danger/**/*" @@ -599,7 +578,6 @@ - "{,ee/,jh/}config/feature_flags/{development,ops}/*.yml" .glfm-patterns: &glfm-patterns - - ".gitlab/ci/rules.gitlab-ci.yml" - "glfm_specification/**/*" - "scripts/glfm/**/*" - "scripts/lib/glfm/**/*" @@ -1247,7 +1225,7 @@ - <<: *if-merge-request changes: *frontend-dependency-patterns - <<: *if-merge-request - changes: [".gitlab/ci/rules.gitlab-ci.yml", ".gitlab/ci/frontend.gitlab-ci.yml"] + changes: [".gitlab/ci/frontend.gitlab-ci.yml"] - <<: *if-automated-merge-request changes: *code-backstage-patterns - <<: *if-security-merge-request @@ -1270,7 +1248,7 @@ changes: *frontend-dependency-patterns when: never - <<: *if-merge-request - changes: [".gitlab/ci/rules.gitlab-ci.yml", ".gitlab/ci/frontend.gitlab-ci.yml"] + changes: [".gitlab/ci/frontend.gitlab-ci.yml"] when: never - <<: *if-merge-request changes: *code-backstage-patterns diff --git a/.rubocop.yml b/.rubocop.yml index 1630d59c17a..95240342be3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -557,6 +557,7 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/components/**/*.rb' - 'spec/mailers/**/*.rb' - 'spec/routes/directs/*.rb' + - 'spec/lib/sidebars/**/*.rb' - 'ee/spec/presenters/**/*.rb' - 'ee/spec/serializers/**/*.rb' - 'ee/spec/helpers/**/*.rb' @@ -564,6 +565,7 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/components/**/*.rb' - 'ee/spec/mailers/**/*.rb' - 'ee/spec/routes/directs/*.rb' + - 'ee/spec/lib/sidebars/**/*.rb' RSpec/FactoryBot/StrategyInCallback: Enabled: true diff --git a/.rubocop_todo/rspec/factory_bot/avoid_create.yml b/.rubocop_todo/rspec/factory_bot/avoid_create.yml index d98ad31f754..060b17970ac 100644 --- a/.rubocop_todo/rspec/factory_bot/avoid_create.yml +++ b/.rubocop_todo/rspec/factory_bot/avoid_create.yml @@ -6,7 +6,6 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/components/namespaces/free_user_cap/notification_alert_component_spec.rb' - 'ee/spec/components/namespaces/free_user_cap/usage_quota_alert_component_spec.rb' - 'ee/spec/components/namespaces/free_user_cap/usage_quota_trial_alert_component_spec.rb' - - 'ee/spec/components/namespaces/storage/limit_alert_component_spec.rb' - 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb' - 'ee/spec/components/namespaces/storage/project_pre_enforcement_alert_component_spec.rb' - 'ee/spec/components/namespaces/storage/subgroup_pre_enforcement_alert_component_spec.rb' @@ -49,7 +48,6 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/helpers/ee/security_orchestration_helper_spec.rb' - 'ee/spec/helpers/ee/subscribable_banner_helper_spec.rb' - 'ee/spec/helpers/ee/todos_helper_spec.rb' - - 'ee/spec/helpers/trials_helper_spec.rb' - 'ee/spec/helpers/ee/users/callouts_helper_spec.rb' - 'ee/spec/helpers/ee/welcome_helper_spec.rb' - 'ee/spec/helpers/ee/wiki_helper_spec.rb' @@ -79,9 +77,19 @@ RSpec/FactoryBot/AvoidCreate: - 'ee/spec/helpers/subscriptions_helper_spec.rb' - 'ee/spec/helpers/timeboxes_helper_spec.rb' - 'ee/spec/helpers/trial_status_widget_helper_spec.rb' + - 'ee/spec/helpers/trials_helper_spec.rb' - 'ee/spec/helpers/users/identity_verification_helper_spec.rb' - 'ee/spec/helpers/users_helper_spec.rb' - 'ee/spec/helpers/vulnerabilities_helper_spec.rb' + - 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb' + - 'ee/spec/lib/sidebars/groups/menus/epics_menu_spec.rb' + - 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb' + - 'ee/spec/lib/sidebars/groups/menus/wiki_menu_spec.rb' + - 'ee/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb' + - 'ee/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb' + - 'ee/spec/lib/sidebars/search/panel_spec.rb' + - 'ee/spec/lib/sidebars/user_profile/panel_spec.rb' + - 'ee/spec/lib/sidebars/user_settings/panel_spec.rb' - 'ee/spec/mailers/ci_minutes_usage_mailer_spec.rb' - 'ee/spec/mailers/credentials_inventory_mailer_spec.rb' - 'ee/spec/mailers/devise_mailer_spec.rb' @@ -330,6 +338,40 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/helpers/whats_new_helper_spec.rb' - 'spec/helpers/wiki_helper_spec.rb' - 'spec/helpers/wiki_page_version_helper_spec.rb' + - 'spec/lib/sidebars/admin/menus/abuse_reports_menu_spec.rb' + - 'spec/lib/sidebars/admin/menus/monitoring_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/ci_cd_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/group_information_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/issues_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/kubernetes_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/merge_requests_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/observability_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/packages_registries_menu_spec.rb' + - 'spec/lib/sidebars/groups/menus/settings_menu_spec.rb' + - 'spec/lib/sidebars/groups/super_sidebar_panel_spec.rb' + - 'spec/lib/sidebars/projects/menus/analytics_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/confluence_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/deployments_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/hidden_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/issues_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/monitor_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/project_information_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/repository_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/security_compliance_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/settings_menu_spec.rb' + - 'spec/lib/sidebars/projects/menus/shimo_menu_spec.rb' + - 'spec/lib/sidebars/projects/panel_spec.rb' + - 'spec/lib/sidebars/projects/super_sidebar_panel_spec.rb' + - 'spec/lib/sidebars/search/panel_spec.rb' + - 'spec/lib/sidebars/user_profile/panel_spec.rb' + - 'spec/lib/sidebars/user_settings/panel_spec.rb' + - 'spec/lib/sidebars/your_work/menus/issues_menu_spec.rb' + - 'spec/lib/sidebars/your_work/menus/merge_requests_menu_spec.rb' + - 'spec/lib/sidebars/your_work/menus/todos_menu_spec.rb' + - 'spec/lib/sidebars/your_work/panel_spec.rb' - 'spec/mailers/abuse_report_mailer_spec.rb' - 'spec/mailers/devise_mailer_spec.rb' - 'spec/mailers/emails/auto_devops_spec.rb' @@ -376,7 +418,6 @@ RSpec/FactoryBot/AvoidCreate: - 'spec/presenters/packages/conan/package_presenter_spec.rb' - 'spec/presenters/packages/detail/package_presenter_spec.rb' - 'spec/presenters/packages/helm/index_presenter_spec.rb' - - 'spec/presenters/packages/npm/package_presenter_spec.rb' - 'spec/presenters/packages/nuget/package_metadata_presenter_spec.rb' - 'spec/presenters/packages/nuget/packages_metadata_presenter_spec.rb' - 'spec/presenters/packages/nuget/packages_versions_presenter_spec.rb' diff --git a/app/controllers/clusters/base_controller.rb b/app/controllers/clusters/base_controller.rb index dd5be596ad1..e7b76b87ad9 100644 --- a/app/controllers/clusters/base_controller.rb +++ b/app/controllers/clusters/base_controller.rb @@ -10,7 +10,7 @@ class Clusters::BaseController < ApplicationController feature_category :deployment_management urgency :low, [ - :index, :show, :environments, :cluster_status, :prometheus_proxy, + :index, :show, :environments, :cluster_status, :destroy, :new_cluster_docs, :connect, :new, :create_user ] diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb index 873aa5e18dc..2f6331a6822 100644 --- a/app/controllers/clusters/clusters_controller.rb +++ b/app/controllers/clusters/clusters_controller.rb @@ -2,7 +2,6 @@ class Clusters::ClustersController < Clusters::BaseController include RoutableActions - include Metrics::Dashboard::PrometheusApiProxy include MetricsDashboard before_action :cluster, only: [:cluster_status, :show, :update, :destroy, :clear_cache] diff --git a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb deleted file mode 100644 index 6a24a7308b7..00000000000 --- a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Metrics::Dashboard::PrometheusApiProxy - extend ActiveSupport::Concern - include RenderServiceResults - - included do - before_action :authorize_read_prometheus!, only: [:prometheus_proxy] - end - - def prometheus_proxy - return not_found if Feature.enabled?(:remove_monitor_metrics) - - variable_substitution_result = - proxy_variable_substitution_service.new(proxyable, permit_params).execute - - return error_response(variable_substitution_result) if variable_substitution_result[:status] == :error - - prometheus_result = ::Prometheus::ProxyService.new( - proxyable, - proxy_method, - proxy_path, - variable_substitution_result[:params] - ).execute - - return continue_polling_response if prometheus_result.nil? - return error_response(prometheus_result) if prometheus_result[:status] == :error - - success_response(prometheus_result) - end - - private - - def proxyable - raise NotImplementedError, "#{self.class} must implement method: #{__callee__}" - end - - def proxy_variable_substitution_service - raise NotImplementedError, "#{self.class} must implement method: #{__callee__}" - end - - def permit_params - params.permit! - end - - def proxy_method - request.method - end - - def proxy_path - params[:proxy_path] - end -end diff --git a/app/controllers/projects/environments/prometheus_api_controller.rb b/app/controllers/projects/environments/prometheus_api_controller.rb deleted file mode 100644 index cbb16d596a0..00000000000 --- a/app/controllers/projects/environments/prometheus_api_controller.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -class Projects::Environments::PrometheusApiController < Projects::ApplicationController - include Metrics::Dashboard::PrometheusApiProxy - - before_action :proxyable - - feature_category :metrics - urgency :low - - private - - def proxyable - @proxyable ||= project.environments.find(params[:id]) - end - - def proxy_variable_substitution_service - ::Prometheus::ProxyVariableSubstitutionService - end -end diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb index 27ac64e5758..80a8dbf4729 100644 --- a/app/controllers/projects/prometheus/alerts_controller.rb +++ b/app/controllers/projects/prometheus/alerts_controller.rb @@ -3,8 +3,6 @@ module Projects module Prometheus class AlertsController < Projects::ApplicationController - include MetricsDashboard - respond_to :json protect_from_forgery except: [:notify] @@ -14,7 +12,6 @@ module Projects prepend_before_action :repository, :project_without_auth, only: [:notify] before_action :authorize_read_prometheus_alerts!, except: [:notify] - before_action :alert, only: [:metrics_dashboard] feature_category :incident_management urgency :low @@ -33,17 +30,6 @@ module Projects .new(project, params.permit!) end - def alert - @alert ||= alerts_finder(metric: params[:id]).execute.first || render_404 - end - - def alerts_finder(opts = {}) - Projects::Prometheus::AlertsFinder.new({ - project: project, - environment: params[:environment_id] - }.reverse_merge(opts)) - end - def extract_alert_manager_token(request) Doorkeeper::OAuth::Token.from_bearer_authorization(request) end @@ -52,13 +38,6 @@ module Projects @project ||= Project .find_by_full_path("#{params[:namespace_id]}/#{params[:project_id]}") end - - def metrics_dashboard_params - { - embedded: true, - prometheus_alert_id: alert.id - } - end end end end diff --git a/app/models/organizations/organization.rb b/app/models/organizations/organization.rb index 5eaef1419c1..764f752b557 100644 --- a/app/models/organizations/organization.rb +++ b/app/models/organizations/organization.rb @@ -17,6 +17,7 @@ module Organizations validates :path, presence: true, + 'organizations/path': true, length: { minimum: 2, maximum: 255 } def default? diff --git a/app/models/user.rb b/app/models/user.rb index 03d2b5d19ba..3016532fc1c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1673,13 +1673,14 @@ class User < ApplicationRecord self.note = "#{new_note}\n#{note}".strip block_or_ban - DeleteUserWorker.perform_in(DELETION_DELAY_IN_DAYS, deleted_by.id, id, params.to_h) - else - block if params[:hard_delete] - DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h) + return end + + block if params[:hard_delete] + + DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h) end # rubocop: disable CodeReuse/ServiceClass @@ -2351,9 +2352,19 @@ class User < ApplicationRecord ban end + def has_possible_spam_contributions? + events + .for_action('commented') + .or(events.for_action('created').where(target_type: %w[Issue MergeRequest])) + .any? + end + def should_delay_delete?(deleted_by) is_deleting_own_record = deleted_by.id == id - is_deleting_own_record && ::Feature.enabled?(:delay_delete_own_user) + + is_deleting_own_record && + ::Feature.enabled?(:delay_delete_own_user) && + has_possible_spam_contributions? end def pbkdf2? diff --git a/app/validators/abstract_path_validator.rb b/app/validators/abstract_path_validator.rb index 45ac695c5ec..ff390a624c5 100644 --- a/app/validators/abstract_path_validator.rb +++ b/app/validators/abstract_path_validator.rb @@ -26,11 +26,22 @@ class AbstractPathValidator < ActiveModel::EachValidator return end - full_path = record.build_full_path - return unless full_path + if build_full_path_to_validate_against_reserved_names? + path_to_validate_against_reserved_names = record.build_full_path + return unless path_to_validate_against_reserved_names + else + path_to_validate_against_reserved_names = value + end - unless self.class.valid_path?(full_path) + unless self.class.valid_path?(path_to_validate_against_reserved_names) record.errors.add(attribute, "#{value} is a reserved name") end end + + def build_full_path_to_validate_against_reserved_names? + # By default, entities using the `Routable` concern can build full paths. + # But entities like `Organization` do not have a parent, and hence cannot build full paths, + # and this method can be overridden to return `false` in such cases. + true + end end diff --git a/app/validators/organizations/path_validator.rb b/app/validators/organizations/path_validator.rb new file mode 100644 index 00000000000..a1c22654a32 --- /dev/null +++ b/app/validators/organizations/path_validator.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Organizations + class PathValidator < ::NamespacePathValidator + def self.path_regex + Gitlab::PathRegex.organization_path_regex + end + + def build_full_path_to_validate_against_reserved_names? + # full paths cannot be built for organizations because organizations do not have a parent + # and it does not include the `Routable` concern. + false + end + end +end diff --git a/db/docs/subscription_add_on_purchases.yml b/db/docs/subscription_add_on_purchases.yml new file mode 100644 index 00000000000..21915cff545 --- /dev/null +++ b/db/docs/subscription_add_on_purchases.yml @@ -0,0 +1,10 @@ +--- +table_name: subscription_add_on_purchases +description: Stores add-on purchase information +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122662 +milestone: '16.1' +feature_categories: +- subscription_management +classes: +- GitlabSubscriptions::AddOnPurchase +gitlab_schema: gitlab_main diff --git a/db/docs/subscription_add_ons.yml b/db/docs/subscription_add_ons.yml new file mode 100644 index 00000000000..93730f80a99 --- /dev/null +++ b/db/docs/subscription_add_ons.yml @@ -0,0 +1,10 @@ +--- +table_name: subscription_add_ons +description: Stores available add-ons for which purchases are stored in `subscription_add_on_purchases`. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122662 +milestone: '16.1' +feature_categories: +- subscription_management +classes: +- GitlabSubscriptions::AddOn +gitlab_schema: gitlab_main diff --git a/db/migrate/20230531134916_create_subscription_add_ons.rb b/db/migrate/20230531134916_create_subscription_add_ons.rb new file mode 100644 index 00000000000..5faee049534 --- /dev/null +++ b/db/migrate/20230531134916_create_subscription_add_ons.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateSubscriptionAddOns < Gitlab::Database::Migration[2.1] + def change + create_table :subscription_add_ons, if_not_exists: true do |t| + t.timestamps_with_timezone null: false + + t.integer :name, limit: 2, null: false, index: { unique: true } + t.text :description, null: false, limit: 512 + end + end +end diff --git a/db/migrate/20230531135001_create_subscription_add_on_purchases.rb b/db/migrate/20230531135001_create_subscription_add_on_purchases.rb new file mode 100644 index 00000000000..6fdf1fdd495 --- /dev/null +++ b/db/migrate/20230531135001_create_subscription_add_on_purchases.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1] + def change + create_table :subscription_add_on_purchases, if_not_exists: true do |t| + t.timestamps_with_timezone null: false + + t.bigint :subscription_add_on_id, null: false + t.bigint :namespace_id, null: false + t.integer :quantity, null: false + t.date :expires_on, null: false + t.text :purchase_xid, null: false, limit: 255 + + t.index :subscription_add_on_id + t.index :namespace_id + end + end +end diff --git a/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb b/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb new file mode 100644 index 00000000000..234cd2fa3af --- /dev/null +++ b/db/migrate/20230531142032_add_foreign_key_subscription_add_on_id_on_subscription_add_on_purchases.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddForeignKeySubscriptionAddOnIdOnSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :subscription_add_on_purchases, + :subscription_add_ons, + column: :subscription_add_on_id, + on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :subscription_add_on_purchases, column: :subscription_add_on_id + end + end +end diff --git a/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb b/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb new file mode 100644 index 00000000000..7f7083a3a9c --- /dev/null +++ b/db/migrate/20230531142053_add_foreign_key_namespace_id_on_subscription_add_on_purchases.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddForeignKeyNamespaceIdOnSubscriptionAddOnPurchases < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :subscription_add_on_purchases, :namespaces, column: :namespace_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :subscription_add_on_purchases, column: :namespace_id + end + end +end diff --git a/db/schema_migrations/20230531134916 b/db/schema_migrations/20230531134916 new file mode 100644 index 00000000000..5cf00727101 --- /dev/null +++ b/db/schema_migrations/20230531134916 @@ -0,0 +1 @@ +fc2e3d8e6aca7b00569340b0468488a4b0545b39e67857a5b40824f6d0a62a97 \ No newline at end of file diff --git a/db/schema_migrations/20230531135001 b/db/schema_migrations/20230531135001 new file mode 100644 index 00000000000..32850b297da --- /dev/null +++ b/db/schema_migrations/20230531135001 @@ -0,0 +1 @@ +1a672c9412b8ceeec35fd375bf86dde325781c9cb94340995d2cab4bb804e4bf \ No newline at end of file diff --git a/db/schema_migrations/20230531142032 b/db/schema_migrations/20230531142032 new file mode 100644 index 00000000000..bae2817773a --- /dev/null +++ b/db/schema_migrations/20230531142032 @@ -0,0 +1 @@ +3e77f991a4daa9756b541255e3b8da9d8accb52a5a4b625613771982e3dff3b5 \ No newline at end of file diff --git a/db/schema_migrations/20230531142053 b/db/schema_migrations/20230531142053 new file mode 100644 index 00000000000..55da4601012 --- /dev/null +++ b/db/schema_migrations/20230531142053 @@ -0,0 +1 @@ +0a4b3b8848f486e34e1f0426bae4e15f67e851447fc3fe397cf2039e03b185b5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index cc22a017044..23e2df53c91 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23031,6 +23031,45 @@ CREATE SEQUENCE status_page_settings_project_id_seq ALTER SEQUENCE status_page_settings_project_id_seq OWNED BY status_page_settings.project_id; +CREATE TABLE subscription_add_on_purchases ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + subscription_add_on_id bigint NOT NULL, + namespace_id bigint NOT NULL, + quantity integer NOT NULL, + expires_on date NOT NULL, + purchase_xid text NOT NULL, + CONSTRAINT check_3313c4d200 CHECK ((char_length(purchase_xid) <= 255)) +); + +CREATE SEQUENCE subscription_add_on_purchases_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE subscription_add_on_purchases_id_seq OWNED BY subscription_add_on_purchases.id; + +CREATE TABLE subscription_add_ons ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + name smallint NOT NULL, + description text NOT NULL, + CONSTRAINT check_4c39d15ada CHECK ((char_length(description) <= 512)) +); + +CREATE SEQUENCE subscription_add_ons_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE subscription_add_ons_id_seq OWNED BY subscription_add_ons.id; + CREATE TABLE subscriptions ( id integer NOT NULL, user_id integer, @@ -25847,6 +25886,10 @@ ALTER TABLE ONLY status_page_published_incidents ALTER COLUMN id SET DEFAULT nex ALTER TABLE ONLY status_page_settings ALTER COLUMN project_id SET DEFAULT nextval('status_page_settings_project_id_seq'::regclass); +ALTER TABLE ONLY subscription_add_on_purchases ALTER COLUMN id SET DEFAULT nextval('subscription_add_on_purchases_id_seq'::regclass); + +ALTER TABLE ONLY subscription_add_ons ALTER COLUMN id SET DEFAULT nextval('subscription_add_ons_id_seq'::regclass); + ALTER TABLE ONLY subscriptions ALTER COLUMN id SET DEFAULT nextval('subscriptions_id_seq'::regclass); ALTER TABLE ONLY suggestions ALTER COLUMN id SET DEFAULT nextval('suggestions_id_seq'::regclass); @@ -28317,6 +28360,12 @@ ALTER TABLE ONLY status_page_published_incidents ALTER TABLE ONLY status_page_settings ADD CONSTRAINT status_page_settings_pkey PRIMARY KEY (project_id); +ALTER TABLE ONLY subscription_add_on_purchases + ADD CONSTRAINT subscription_add_on_purchases_pkey PRIMARY KEY (id); + +ALTER TABLE ONLY subscription_add_ons + ADD CONSTRAINT subscription_add_ons_pkey PRIMARY KEY (id); + ALTER TABLE ONLY subscriptions ADD CONSTRAINT subscriptions_pkey PRIMARY KEY (id); @@ -32871,6 +32920,12 @@ CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON status_ CREATE INDEX index_status_page_settings_on_project_id ON status_page_settings USING btree (project_id); +CREATE INDEX index_subscription_add_on_purchases_on_namespace_id ON subscription_add_on_purchases USING btree (namespace_id); + +CREATE INDEX index_subscription_add_on_purchases_on_subscription_add_on_id ON subscription_add_on_purchases USING btree (subscription_add_on_id); + +CREATE UNIQUE INDEX index_subscription_add_ons_on_name ON subscription_add_ons USING btree (name); + CREATE INDEX index_subscriptions_on_project_id ON subscriptions USING btree (project_id); CREATE UNIQUE INDEX index_subscriptions_on_subscribable_and_user_id_and_project_id ON subscriptions USING btree (subscribable_id, subscribable_type, user_id, project_id); @@ -35420,6 +35475,9 @@ ALTER TABLE ONLY abuse_reports ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_405568b491 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY subscription_add_on_purchases + ADD CONSTRAINT fk_410004d68b FOREIGN KEY (subscription_add_on_id) REFERENCES subscription_add_ons(id) ON DELETE CASCADE; + ALTER TABLE ONLY ci_pipeline_schedule_variables ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE; @@ -35768,6 +35826,9 @@ ALTER TABLE ONLY issues ALTER TABLE ONLY ml_candidates ADD CONSTRAINT fk_a1d5f1bc45 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE SET NULL; +ALTER TABLE ONLY subscription_add_on_purchases + ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE p_ci_builds ADD CONSTRAINT fk_a2141b1522 FOREIGN KEY (auto_canceled_by_id) REFERENCES ci_pipelines(id) ON DELETE SET NULL; diff --git a/doc/administration/geo/replication/container_registry.md b/doc/administration/geo/replication/container_registry.md index 66c67f29c1c..1c1d9074a04 100644 --- a/doc/administration/geo/replication/container_registry.md +++ b/doc/administration/geo/replication/container_registry.md @@ -73,12 +73,11 @@ To configure Container Registry replication: Make sure that you have Container Registry set up and working on the **primary** site before following the next steps. -We need to make Container Registry send notification events to the -**primary** site. +To be able to replicate new container images, the Container Registry must send notification events to the +**primary** site for every push. The token shared between the Container Registry and the web nodes on the +**primary** is used to make communication more secure. -For each application and Sidekiq node on the **primary** site: - -1. SSH into the node and login as the `root` user: +1. SSH into your GitLab **primary** server and login as root (for GitLab HA, you only need a Registry node): ```shell sudo -i @@ -107,15 +106,17 @@ For each application and Sidekiq node on the **primary** site: that starts with a letter. You can generate one with `< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c 32 | sed "s/^[0-9]*//"; echo` NOTE: - If you use an external Registry (not the one integrated with GitLab), you also have to specify + If you use an external Registry (not the one integrated with GitLab), you only need to specify the notification secret (`registry['notification_secret']`) in the `/etc/gitlab/gitlab.rb` file. - NOTE: - If you use GitLab HA, you also have to specify the notification secret (`registry['notification_secret']`) in - `/etc/gitlab/gitlab.rb` file for every web node. +1. For GitLab HA only. Edit `/etc/gitlab/gitlab.rb` on every web node: -1. Reconfigure each node: + ```ruby + registry['notification_secret'] = '' + ``` + +1. Reconfigure each node you just updated: ```shell gitlab-ctl reconfigure diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 9dcca8d0ac5..758d75ee294 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -120,7 +120,9 @@ The following environment variables are supported. #### Sync status Rake task Current sync information can be found manually by running this Rake task on any -node running Rails (Puma, Sidekiq, or Geo Log Cursor) on the Geo **secondary** site: +node running Rails (Puma, Sidekiq, or Geo Log Cursor) on the Geo **secondary** site. + +GitLab does **not** verify objects that are stored in Object Storage. If you are using Object Storage, you will see all of the "verified" checks showing 0 successes. This is expected and not a cause for concern. ```shell sudo gitlab-rake geo:status diff --git a/doc/administration/geo/setup/index.md b/doc/administration/geo/setup/index.md index 4dbd2ef6167..94df58cc9eb 100644 --- a/doc/administration/geo/setup/index.md +++ b/doc/administration/geo/setup/index.md @@ -30,11 +30,13 @@ a single-node Geo site or a multi-node Geo site. If both Geo sites are based on the [1K reference architecture](../../reference_architectures/1k_users.md): -1. [Set up the database replication](database.md) (`primary (read-write) <-> secondary (read-only)` topology). +1. Set up the database replication based on your choice of PostgreSQL instances (`primary (read-write) <-> secondary (read-only)` topology): + - [Using Omnibus PostgreSQL instances](database.md) . + - [Using external PostgreSQL instances](external_database.md) 1. [Configure GitLab](../replication/configuration.md) to set the **primary** and **secondary** sites. -1. Optional: [Configure Object storage](../../object_storage.md) +1. Recommended: [Configure unified URLs](../secondary_proxy/index.md) to use a single, unified URL for all Geo sites. +1. Optional: [Configure Object storage replication](../../object_storage.md) 1. Optional: [Configure a secondary LDAP server](../../auth/ldap/index.md) for the **secondary** sites. See [notes on LDAP](../index.md#ldap). -1. Optional: [Configure Geo secondary proxying](../secondary_proxy/index.md) to use a single, unified URL for all Geo sites. This step is recommended to accelerate most read requests while transparently proxying writes to the primary Geo site. 1. Optional: [Configure Container Registry for the secondary site](../replication/container_registry.md). 1. Follow the [Using a Geo Site](../replication/usage.md) guide. diff --git a/doc/development/gitlab_shell/index.md b/doc/development/gitlab_shell/index.md index 0663341f806..ef034761a1f 100644 --- a/doc/development/gitlab_shell/index.md +++ b/doc/development/gitlab_shell/index.md @@ -218,5 +218,5 @@ sequenceDiagram ## Related topics - [LICENSE](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/main/LICENSE) -- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/main/PROCESS.md) +- [Processes](process.md) - [Using the GitLab Shell chart](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/) diff --git a/doc/raketasks/restore_gitlab.md b/doc/raketasks/restore_gitlab.md index c7abc7ac562..b6e9824f9b4 100644 --- a/doc/raketasks/restore_gitlab.md +++ b/doc/raketasks/restore_gitlab.md @@ -14,7 +14,7 @@ information. Be sure to read and test the complete restore process at least once before attempting to perform it in a production environment. You can restore a backup only to _the exact same version and type (CE/EE)_ of -GitLab that you created it on (for example CE 9.1.0). +GitLab that you created it on (for example CE 15.1.4). If your backup is a different version than the current installation, you must [downgrade](../update/package/downgrade.md) or [upgrade](../update/package/index.md#upgrade-to-a-specific-version-using-the-official-repositories) your GitLab installation diff --git a/doc/tutorials/left_sidebar/img/sidebar_bottom_v16_1.png b/doc/tutorials/left_sidebar/img/sidebar_bottom_v16_1.png index 75e6f496c0cb83b472856c1e381c61c000a4c611..dd2c7b82d40beebc6cef6460a709b22e2c026eab 100644 GIT binary patch delta 6620 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 aOyob&Q}~CtIU5=P0000a zz4q#^?p|6|bIu|{UKaET4jT>(4D6GHxQHU~c?AXrZVdYY*uo|>l?6Va%mifw!NBU` z;9m@&f$xMy;)*h0V4mb)VE(~iU{Am<|06Ik7e+9!6MZl+u5>UkO#7@h1s}=1)z~JWQM(_5O-px@~=VuYa9_1CnHA-duIzfTf+Bo4GisEoOy|f-zWO-pMR~>#NFb5 zX0mnq_hA7C$nf66z(mi;@c%M%wlMwwn7z0B%j}=y`qy+k?}Kp(+gaN=D%l$tneZ|3 z{4@T4?)sm_{j0aUg}aHhhKPj?a1DS1;rq(U`ER5D-SWRJRhh3#wrLubDKxrqNZ z{?Eq$ZTQc?>i;t(GYiXqPWc}#|6%!lz+7^U7Qk!-tNgy59sk7Keo2B^AG)*nCQrOG~JxY zWOsUAZrSL(AD-MKK!pMq?0G>J&*C6bERp?$qet*2$HsQB2v$}{NC*pN%aB7O8cQNc z8y7^V{|ejB^hPcY519C)Yn)noAV7gTfd~<;`7*q4zP|?g&yx@qH_r1zK=_jpBEv8f zY6_gV-~D{K<_F6eI6=Zx zB7LiqCwF)Et)n_L4%s(eUVcJ}L{jlIK03rCp(x@^Y?}lMSSUh$VUj}%rAjh>j~8?p zB>Vz}2UNa9$nj|=8p$-O5O0rb{mBiVr5|3o)JzsL-%C}i;37ZMT>bK~zuKenlyEP9 zVPkWMr&1C#9{={asmY;Ro!m^j-k5X7Y{*~Im{;A3!}ErtTBkEqwMt8!2Fi5a_H0qE zsaW37_VLEg)PM1N6edF?^Yy=#U(}^clXH&R>K>oR9o_O<_=fp>+vk-gi=xE# z*3Nzczi6TRKfHAovrplXu&K8DBm0c|ZBHw#R`PAi5SgU}4?t#X}Cd+4`;zCodl^jZz9dZ#C6BE$P0o+r^-gNmjI|F|0Fr z-nh?I>mYr?VJSxO=IZG5xj_{}AP5kR!Z==3qu2ZC{~BZ-TwO3@r`|2mJxKBAPYsjl z7!+BYiR?EPjbX2+Q_dD|#7@uK0VMvH(h1=wr<5`Es%W+`JJt}kTK9^L~ERH_&8(P zGU!r|weY_9jm&kYoCl%RzI;R@Ke4z!=P#YnZn0O0+pWhk`#2p1_VO5ucWa!^r%MXMYYh%) zRM+U=Ynum$_{B<{-=6h^5wKp`xsaX?xBJ45q$Q7j|Bm`8{2`(Kb0KD*dZkvgAQ_$5 z;%bxKU?#iWZKYO|wdm{1Cb2>91Bd$&EY++&I;}dg(O^tSkg#Qg-6jf>I6aK*dcjjfm$CH%ykx7PD@V~x;x4##s3(edbY*w*GPUqNW*c;i8PA*eC!s2o{4L0p&Q&>FhZ*@P0P_5C37|UQTYZ=SQ7P3+{mB=OS`2ngze{IelYl;{T~EV(9@!<)5dUg6QLNBGKjNdNTEy?Y z2Wz?TfNyKdV1ICf6-~e|`)e|jJ9lMKI@d6nS_6XJ?zJSj!soe(S|%d#*K~~@L+9Fs z@yW;Iy|IiRFpYX(5Tttp%$Rx^2wkEQtx4YsF3djgBqU*VsD}loxUW4hJ+bB*j~;y2+wAawTz1-H zR9xx}fyA%PT(rafhzF~wg%KT_ZmAYNtUVU69l+v>}ou(*rQ7GiH zLOl4pu9(MEW9SYb| z5-nWZa+L}(*$#Y5zAm3<*||y;NLSa(iwP3_M>>=8WMf5~MJAu88)}BPj~4K@=FGR! zxpF-GP=o%X)Itkl#Jw@XA(HzJdrK%w13yH!i9;0fdYT&y6GFvyuN!bZz{bC^{-u>c zoR07xW)BuIGx=n4IGUDw5c!Bu*vsSQIuY`Cjam9wTzu1PyRIswHqq(`J*t%_z~{+H zD%kF7bK^Cc(@vz&3l)yTj@U&6Pgu&sc4-%Hp|$&L>51>>$N!h-NTTb-V@R2=D`JR4}H=Vj3>C%VUpMRm@XiWgF4ioYVMtF>FN)SHYm z9QjhIRfK*G=N^e}UGJiGhV*|p4uSiuNb>8#fW+bN%BL)fS&xSc{<$oAE7d`L6=w&B zpTkKd^WG_VdZ$1C%;m#}#yuz%(J8j%C9)e2F<-3!bQn(tdEMRoS!sp&g#8kpTD3wb zTI!JDXXA7(7v_zwTX`F60fvZnW0zmQvCOuh>tfV3HqH`=o!O42+9)J9#k!vASBiJ&T7%lNP$>}8F)fc9Nk$8o zDLeB}trVUT(wa89@1Zh#4WM;v8s%&WcBIHEy}jc`?Y)l0Ms=wu{cgrm-DUs$J`{$t zBG7f&^F<)evzaC>{>X3m4b~eT)QI>Ze;r32JMB0LLdi5>YK^ky^qFXjt<|YpcH8r-O0@tPYhAM{Mxh7VJ4eL7rVXApi&O zK}AOPHI_OOwFm;j;88i%H&|G(RDXXuzrjZ(^rZ>CZV#)E9O5`o0X2dCJ{-BFk>*9# z83dfvQNzXM;vzJ($3R$3$N|wChl`6AfdVy#2%bTbsgouV0v)ggEAp!xkzU3S=(1Ho zNcek5589PV8JUp|g8VtM*rI}L=G~YXb|^G{D38nnIzTu<9xAzX7Q$k?8+uAfTyq*Rja&jT zS+PuJG%ja--}+y&R63p5F|4nCtfF_9mtvb;e;JJioXZavjdEU4y~*MpiS#SFIx%I? zOxTJ%(CPK+aJ9piVuEtxkbdEje4GAWs#J1-NwQGxLepR~E4lCHw?3 zR}SpYuku(hg)xRVQ8aW|&;jN`Js7#}sn*yse6BB~^tvtL1k_+179xfi1Lq?tf-xub zKg-{?e)j1*=wWYf#lwyJ*@U{Dac^Dj4oW6iEmnsCVO*a3rQsJIbIPo&tc#0jlGD|8 zOhk*>e3Q|GNY=zrZ6J`A&E*T~c)T(M!d4NOXYj_~a}a1LN&D5kDWht&#>0-d{Jzn! zGfIUPv*e{(HLg7}Pow)2-#B4_>_)_Khw9H-S&*-sEEKqye@QUzLk5dW?O&0sg5oWZ zspZV4-|S^ioLr{!zaJw!=J>;^R$WV{Tck4*S+Eyp3S=B_NcVT+34MiJoZbAO3GDP- zUZ2ja#?m>JGOmNd5C#TfaKu|-NbGXfEPc<8AHkpR&Vx!8y_|n-&>+q$UTO>1IFv4xei=4cKz~ zM-o_M2pB}%f}cALy@4KmqiJsw-&@k72{J&{1)}h~b7fzL#EWgtxLAkA)(fPq+8#Regu;R-kJWOO2)clVNU4v|B}LF1TxPvw$ATt zCx`5IYsw5LG7*nr`NTV0GvM_*NhF}yvV<0qVN~Pk6lHNnhn-A={k zvgCBDzQTH``SLB8TpWEXOgXAQXRB^1CNf3+w3Q;mJLa=(tdA$w@tjhoq)3M(rk6Xv zlw4rXkWBn3rcezhfBZCAz#H z6Zrl3lp^4jWPu>yNHhWJY^8B}me=b~sK`e&|FkK`gP{N*I@|i>qoC}SuifdXBS%BA z5G9bwMkj3j;HAth{ewwCk^BdKW9rO6989!^JgB7%wlfshZg~m7Uy~S$*5eW zC-3g;2;mRTrgJ(qIeOQ@v@3cms5Qlc3Xr${NCZ@GT#cE4ksQOHNLvYy~08; z1^CD0*7$8bO<|_C#7Q3k7pI`7`LUqE=r?>++_W{2iyDBgkNYFgLe%NKek74g3C(`o z<#Y>=AX7vRCx!qp91@q4ImQT(>10l4!)2c{{xM|}o#v0QW;-sIljLd5E?7(`At8S; zP%xen%Eax*h0%bb+mF*`0Js$q{5IsDL9O0lr%_ouNo= zLBbCxjLTO_2n!mKs6Y!L?3AL#mTEuY{hs2xYlRBnc&Q-$80b%^zX1!&U;lr3S-@+; zABk85zPq(61Xu_Re}8*^4FL*CG$`<8Wa3G{5F9q%g6{Va##G!46b%T#o_rsT_b2^X zHUS^a`EMT$Z16w^`a=^Ez5pE~W}pNGe3^;6uY5%n+d~pSWOW_+KZ2Y1o zTd>G2ll#>JmAB8|0&4P+$BeVx>B2#UR1%0WB=9G!LJuEx{k~9{FM@NlW4ig!b_Hs9 z%*HJR4)Z&PCr4+yLEYqXkV+K;%KAe7SYHpIX#O0GYo#-ra4B?k$=}OnbA39nO-1H{ z05Z&}=JWG>3dSUE2Luh6@%)MT3N_Lsq8jor|AO2)1he8PER)y+aqEo zRa0|l&cmk?piOG_&QWX#vfOAQY2 zk;rvV&-drG*2~l{H?wc?L_S=^30|Sl@E8W`Z89zHm&A%ivO&tZ!;^0hMzqXk6C%M@ zcje!JheHUrfPdfH`y5zSe``mw7?a!%_p6v4T%5hpKy(;pOc(qCL;rN42o}yFhDP{A zN}pa*F_F~*tt9=Z$JwM z=iO1_OpDu4D7CAt0xi*z|K8!L=Q1s3#x(l-B-7&imkSP^GEaPCv*WU-!{ZI` zn4dVDkC5x+R+6&79NqfbvdFFU@3J13nrtW%Rt46Rdq0+^)#*3xhO|rr;B-%y>Tnp!^c?p`ioP)M z^6*ejqJ~W8@IKEu{j^yy4qa$oTLi`7t95gvbK8L<;`0QHM4%EW6-QIoY+s|Jno1Kj z*l#O(R@civCkC&CNvgBFY0EWvs8#2SM(HVZd2`Z5aQPOT9qH=w zwMf!N+pIPTHCn3FIm}|LwK!-^Rt%eP9*Uy?5MAgu)^0VP>3f~?-^W7j&CbJ$E!_^F z=Q|tuG}s+>yXDZ#w{?Zb2XG${L*?C>R-t8zBnRkamI%7Kgl+$5rSXU4OoIiQa+Rj& zAUPhDP8$7L0(;F@g)RCX?zsD@mHXFqFXz*_RCLI6HkS`@zRz?k4Hf}Fgg67g_dT)M z<~N)BvwOZ)^F*m`6?P}%BFr^X2RV)Y_)1KvVk;`jx{I)?=XO;=8Zr#Q71>&w3rxiE z_V2Y;-g=WsF@{iRebK$12RsgYSbkR9XuZt0NSZVTqtPatlZ|a*(C3_(W8DhXafjO# z*3+d{sr6L}C@;H>4nfSs;rmzu{t#}rKjmHqw^fn?e)v4T!LoR(`SuCDCuF}S6YqFJ zLY8sjnva(1=x~pJVX6{X?u#rYbQM_S(8$MNv&tj&ueVn}Tn>i9<$B!{Brcx?iN*LT z+s$Fm>-6mHsfAl}5$CI(VGkvc;3SesVH;iz#JrV=4e$JlqUvpf$7BdjuhzURx7;%e zecBsN+TGq2fKe8|SAVSDYzYy2vkGUim?7k+LSEb@9yX)+R`HwnoSdlmtOH-@Hq2-^(as=YHNG)$(aBDaa@T*Rub@;}%x$@U46eX`L{0QD3bGY3C4!Y9jlnLR- z@$6pRG7l2qZ;5&MXo@i|BUhAyr8VgoCp5P>OYexcsFYOFMkoQ0$W@DCNQJ0%)=C0f#?WD;j<-`d?SsxSx z$(e|__2|TYfr>ckMS|93y-{!=10)Z<$FpXaF;02Hw9Jly#WLwrvAW1*bq0N=WVzW| zFN8Tf4%pstdh$3!$Nf%CFe1hHE4ZAcYfv9g?SJ3dnBfI<1~A z~eLpiejp z?1|fZN8$XS7!?g>(-D7ylSQRNR!dcwxT(USAL9XZ(W!Kq71x?`%`|G20~0#!^Wj;v z8qA*wQutqZMBmTB#ZjHn&6U&#*Eumzt-7!AUEq}?NIZZ&zQ4pnUIdc=Xm{{UR7##? zUNJs9J3B#wK!FoQq9kMHB@_vpXQ*$?)M%}Rr7l{3OW_=CJQtyaFj}TJI3n8nBMN#+ z;w3&Y+bz~+?Md%`?GGB?m!75z#YqXxz>|f>uQ4riAc@3sk9yVDJi)%HmqpVd5e8=0 z2{PF%@%AEuR+)|+V_$xaz8;Jy|2DLrdvcIjmCHP%|1BF%W!(1cwX z4n2#CM#VYUy^hDP%(EzP#7l&*x)Z*4BRf#`fLH_eDb)*q#f{!;B1Ha;63^*ac~1P^zz0ybi2r3_y<8hRLYu)qO#BYBbs>Oz zJ#iFL1==Vlr_<<{N2V7r5ep;NszMnHhd17JG^`dNBa1eb{z)ECr-9gMY+A@kbJaoz zATeu&b*h*;Rj9F8f@Mu^$rU=-;1K`iOOmkpFRc`?Kxc;rBr`1#$} z0GMZd@TbV)N&$_(N`zqPybuluoW|o_tTSMOJ(4JlkAwGXx*cF|X@o$o!xZLURn`?c zD*M9)hi$9>QmOooOcJ9H$>sOAtj`Pwc?pS7;MamZ#-z5~u78vzU&h&&Reo#aqoHZ2 z0nwFA?0^LE-~a^xUa7nApps-d$n&xw4TZ#*t3J2*SSFcFozUT zON6sN;m?yb$r;;CdIRujhOK(B`NAN><(bd=Gv-RcGMUXE&~^{*P_6OAk)bL-f+tn- zG7-5vUYR)TpRiWy{y?Nrt5P}qOp~H!JnwWk-UoOmg}klihw}xxF}+&KkuN3U38*xw zbYXFEH3^kATFpV{(4TwL4`W`KHwIhF6jHfTqvI;6W~ z(tq1DqxR|t5U5TRXf@|Z`erWR)!MAel7T96Yijrp#&W`PGg*X-B2^gk-ty$>;5rQ* ziD5;M!IOMW4%gf7FK%!9%96|g4o<`6_2t+JJBIh-X4q-(jg8;8R1Jhj!0U}*cV3#Y z=5n$y;3X%A%jGU^M@=(YK3{RM8~5p!gKxRRfo32sH*aXl46FQJo z4-tN>buXTLfRRe0FG>^lxxtwtsJ=hX_lo12X1id`oVIi0LVkL|01~l+Eh*{YJvgWhx3#m-fdyWEX7p zQJC~)R`|aPQ5Ba~3stKC3f+~CTCME6NFCbgbywjE{+8aEr3f;cKS=?UuWKtjWi`7JA zX+H#|?6LZ~vLEanKTCz``y$MDY@bj#sMB+lEq;{9f)UrovXhQGG=yFK^0B$Y~PQLXaT zFALg;H;a#Hkk|6E7xfWk}+%E*sH6Z&S;s9{4_&66ycBTg-Ek}^wuSrub-+a1& zQmHL@SkoHScMrf!iFF2|e^UQ)kQ?ssy`O;Oe_MkM@;$GK&B-~h^BHZ@KPJI>$D>$L zZI^0M;_!I~wn@SiN_5xt4OOePW#BeHVAAJ+QfW1k)p*Sd9$W`70%O3zz1<)AeGp!W zz0J0!uCMM?e-(+Rpduoc-Mj&Wr+-qR(Ny_RV+fIanIbegwX%53*f-Q4^QI&ER*`x# z7i^O)M5oIOJ6YM;! zO3G{$Xk0AD55a*#gS;MrUb`h+tJ$`1{uW>CFDIAcesEeh;`sb}4zFlUPG`^U(Nx`! zdc+1HT*VThWoDCMa5f0D$(%kXcJ{GMc7hn3H-PQ3GaueF--b=5Qj)w+NN+1E{t_>v zQvy5u7%S`TJ2)_Hh+-UGV|)B&quZ9lzkSEc6bf^Z`31cjMT!<$sY+ZC zG;8lhYcH<2IXPz>=UKkXWP<-59+S&u!~1}Q1-!)jhYW_Yx!Hj`Ie4Y6cqZh@_z$fkMLo%-W#XloNW?yRd zlL`ruv7pv;c|a#~Wd;==nkdU1=gU=U0YdC_xpT4v z@wi`I*rF6){tgG(=}Tsc78NC$M)?;Hv3Q`7v6(PLqGqDXBz#tY3br6F5SQQ*B*Y-m zuP2b{-(i!ShXRiR5ypO5z}Os|5ug$1(FfIiZczP339C*C5y1VKwjp4KRmJ= zn+Py!Lnjj0;2=;$1s)PeB9p(PK^Zc=GiOoUpmK6p5%hpBj}N*cG$ikw+SVul5?-Zk zleYq~O^82mJIj)e%>FoJ_+Ujkc**+@GU*8wew0-zC-aaJZ#KDh5!khg%VLka`BhXu zmO_r$eplZJ4JL!xgp{wWvbOQlckAWuH^M8_X%BOPv9F@MBB6xHTFAAR*M*bYVwa#{ zKwd)hO13E8NTtOFYOq@5QuJu(2N+)nfV`S(F7xT=oP#;ZkaSsT7#_OJm=8EsqKGDl ztKUpns2^59uH}GguydUb$Gz>5Jl)$1w_eUBCOw@vYfn&p$qUqKv<+#p(JV73XMfv` zQ#jh_q(CXDLi(m$4Go<89=7trx=>`AI{4{k5z^Cs{I$;C*a_cJB>Vc!|4l(rUR@wy zF++_GitMi(T4s=x`;^?3DwYLG4vHL>$mI%dcze{jo^ciYc=TU!vCKDmz9Ukcfmhv^TKn+I;WLFaH)y=dBk{;V8lKCjUWcvVk-1Q%5Z{u3gvVDIK5<>CSMiX* zTHqvCb02y1LaXqTdAcuJ&Y249T72Mr`sKYLFX5f_q{eL zBbXo~%HAiPc%L|qC*i! z2}Gl0KckVAQ(+|aF<0rec8gsZ58og)XZAMeHkdO#bYrMT48{-?>R{vbkx(D@8=z$4 zd`yBDBt(VnN1tw$ct{9pisIRJmlD~eQd-(wY0Ok_v1dA2s0v9Wi-M7P`n5rYZ`8kA6hJLDs{+Lo#}89 zXk-9vi5X8JXZr;;Wp!o6aG|o5N#zq22zd%czu@`^j$8wTsL9FQ?rwb7)L%z3DF2Om zQn`Q$Z>7mb3XU88e8VF!V#MqAv?6?>6`$2|AsT(!LwZqX1PwUF{t~XPw`b4Km>yMw z(O6i^Ph!-aKy0@&Me)FD*jO7rLPDmF3J*~S70{Edqp+4Ue71d-+R;wKEG5b5XdLBA z34mZ$%Au$2Y=Oe`w6O2#N!~VHAvhh>`xFxe`=8^N*i1hFid@WKRkY9|^%`H>mjprl{N>78IiP8eX*NgxV_4wkGG z%>)Tyfwv1HM7TB_#6dVPMy64XL^d=)Pyk~f3jpi{Bk28AgX<-8LJJEF1l#~%#A$*4 zoW%N(lP(!X?L`xT{-Q+he8PP8%onTomrYgUVbMB92r!zKvYVt)Z7`mO7)Y zEbc{2NcZOjo13#$BtTgx)WI>JxBX(6pQwNwZVDy+RF(?~gIG4bxXG{2v8ymXf16Vf zz1_lOM7|C1(h9aS2_%ClsO(k?=-h6$YI_HnY<2<@Sut9HG+q>eR|pAsReg7y zmxO1|ABWqAzygve3Q)GtbbG%cO&SFt8 z!I7ILzJ0Q27w%*>nVC$^R_Qv#doAYw`5r(a0T>wFNAwjfmH_@szcfG>nfUI>jg3kQ za$^hm9{$lONE$Z%V&eRI>7-jYWKW;yb0FyZd|;DK<VHCeUp`hHZh zdXiGThy%b}=N|O#OBg_oxpui-^;;l(dEh=6%YX)X25@khki_mqiIE&GI~^*t$PI)K#aEsbg9hJ;gZfn|_V1Dzl6sJfcD{VuCJaDSZczO38>kvO;7F`T zAi=`1VoiBk=lc*Q6co4;NgfsRctW9x0Go@CXbw;k5ghO?2aj%dlzL~kA;6Pf&3kDz zX;aXlXp!F?T90fBxdxzsei#nE4Irn(fHK5^AYVmBUBxE9MR^Bh6pXD-Y%D5%5fkcK zMV)C{q_e)en1#938P%wF7em)%Rb;Rb{O)3u?4-2lFu=85e+o=(l3p*ecJR6X%=Ptr zNI(Mso_8e~SWH8>6%OV{RB|vsbdm;roJ3Twj$2r|&VG1^Kmi2e9>`=|G(+{ECcR3&Nn zo*ecTNpZ*#v?I|m9zWI@4bB}}2}_Y&27=Mddp+5DD5s>m2|YbN`k>@#H>=`U z!*t_=24F&$_j6cBt5$?UD`yV?Yv$s2;cxI|$qqJ^L948Z)u|72OfT4`KMW(`VtP|9 zW&%Y5M-w~gf+UQbr)}5mbmenp_+pKo(ObaZpF3nbqbN&~%3Of+_ipu9Pfx-^nQt1D zKe^jGR*J^^3NB%xWo&M+ocQ@f^(~-ts>lkaK&mae5X67&3v!GdcZnG3;2C)t%4cP6 z+dVr|(%)_S^iZZiLL&BS-Rmy!ypvoNw(nxIYE@mh<6W;5z+1=px>O6UU9O@c(PX@8H^`$@V1ASx_rWjhn~;7Ges!X zK5EpP4pgnWO?T_rt&wX0q+4HB>(>NEK`@nx*S|2e7PF?(>5Q@|sMI_3iTZlb5VD4h8*+n|W2bXtj15b|ou5K7NbC zU;Y=q?*yo5NEy6L+0MT&wZcb+Qg`|NCWUU#b;5NU-5E7Bp)!{*cPXamNnA)vGCb({ zTa_yyH9=kJt%kJKn47|k!vhcfxu2V!wbbdjZ1$G7*&X1{zjrClEl z0xCR+0N;HyTT-KlkU}_=+kC!ZxSVJrVwVKh{l*s$`)4MUCMd=6WX^(8u_3-Z>@dtt zy>?5l!w0QaaBy&teXHnKFi|cf&59Ip)@*N}AEx2iHq`W2xBV{4qIknicFVp;-$!=4GLgcS^0sx==+c&5x70Lqz zAyGG$fan4(Hg_)zbeHk~nh>GqyX@PS?QHLhCjrTh4}Bm6$i=umcAyL*d`+xQ z{a|7@+U%ylYV8R0*O#xJa>hiwrlSndZm4*`VbX{5csNx!Ewl4-MKyBot+o!Be2yFQdDm52odf}?lf+ie=GElI#t zgeSa0cSSck5_=-teH{oljsA38?x%;x>v=;uG6yr1#rd_#b|V^YpN4XFrPf!iMtgAH zO6_R;p&8$%G#)pD1SG2y@%VHw{uUlwU5iqtCFtqi=*{!3uI#rM2?0$D2L7zM+u;P> z8S~O~gf68ftXwEkhF|rKq4`E-Mye>4+E2Ud-Qig?Pi=JyH@o)oRWG;mz64=VbRwU} ziwLi=O1bi{yBdf`MRYEgqw$b9cr=iDX~@Y&aBoT#@M{OlI*_^mCXvA;@VbpJ(23 zIaX_LmMS={-szNTwSNF0Xwl+7NBVj0US1U~=*CZ$`ImArbH^oeMBUK*Sn1`OIrry~ zM_P}X&t#E4l3q|Lne;YXbW*j2F*5od+rtkJkAgoMS^pGsIP88%vO1!C6jwr^bAR@> z^%_YI&n3*k_+;yMK&vt5()>q9zSx@TokVsz98-A%#o@)AE<@;Xd5UqBX&0f^o4EB> zuY1+mJjL*64;w zXM82LS!po9=y!RV5#Vm8wA%V{TjbcK$(M6~*>|6iiozWKgW9f(=i!TSeFC?f?YDEX zg>j|oO73x;hYEPM{0G>q;XNLQ7j(|9Z-hBE0%qAfSR4e0?96Fub>1E z#psTQuBwMTgMK1~76Z~)KYc?0Raw3|0GjJoe&KSp44cSqGhD3Z9onFmPx$+3K_ZvO zC%S^GbtlM43{t!DD2KY0*$edn9(vMfeec@3GxV1GH$=XTeZF(ZwYr>6lk7 z8Wehaak}h;Ek3aoMsRYp1Bg2F%|qMc9}Jt!78M^bCbQl4~yCUnbvP5n%McYcY@{9H&c@$;rdH}U)=8M&k zGT?u9sHjX1C%O%wsdnHs4GraZ<&b7LzAr?|t4{vu^hya^%f)}y<$X5j)k{iIiYq~3 zxBUxZ$hz_LbmFM{0go#|qN&HgV28`?3DNx4URclae2dBBdx6{+7TtTQaV=l3?10Xo zK%3r@>-Q+}i%zS9c8Qm+H?+xi1uOcGrng9(i1P%S@mBoH&M&FL4u08=61|#}1Qq_b zZxm;%EsdHAT_ZBWH02~?~g z0ELLNY3%?R5td5lJD*mfyq)`ofXxH7oRu|IAnMd-9!wSl0@uY}k-qjr?rH_ zetDQnqOvxuRKoaJQ&x_+>8k|>4YxOVSG8KJzYXEC8I}4jkB#gLLoEL-vX^eTT1{83 zT2)ZpY`YxVT_UG;_!5@Z!sTKc2bG)lLpH(fm7pAw``1FHPKegqdU5^Wk&UaXD>=x} z?W)&o$|Sw-<1=N0g__9kQ3mFwWCX}_L@99$P31coig1G5oQw-Sdf-LOO#5x&K9>dt zhpN5~&k`}xdM;617r;nFc|5NzkSx@fJ)p+YnF@1)$3ePLx~-NW2@(^<)OI2gsVo|` zvI}3soKSNRHXt7UtaCf=E$?oIzysx1GCBzwEQe~p<8wRz5Pcp*+San<_j(DEr2;(E zpI6CYAPisJ^O_Csp9QD8IGR{od1}P4lLg6Lomh2+*QnTOc`}LYdF(xf6c3s5gJNkP zBqWxJQQgBP2>f0GFzB=h0kPN!$*`n$n{$S~WX}Ol+^NFH?P2|WLo!4<``22Y@vJyd z;?xNiyDjdmDUwuIKB`mOlyL|-3ERLAmtF>e*EiO{r)elZysuM~uLYJ%Vk$kDDV85H zBva@EWbwES*PlM6^V{*^Di#r&$*L%xn(B4l@f3*7<=@TI;H=UJ!1Z_Y)yLqR|Na}z z6e&4|yjmZ<^(TKR*7s&H4%C7TXb;~CyBmg5dR9SX=@{TSn=8`|q$m88EH<6Q)H448 z>o=s6|5Kxb7hwOn^`coqU-<6BKbeWZsYuVjWL#=40{Lo94`yhB7!@`Dw3Unj0Gl}APJd_QPQ_F#DPa2qjlo{ORH^MbV;|(w}yZ%ch%`2=VH$0oivDpA(LkZ{YCv z#j@FA!4%2QhA57TpQS{5Xde?O{#8~Jp2JeKbLb-{na!a<{515$l8v!%<+gM(Z*@7& z&gmkGFT6G+t??}=9Ue#vi^X~twB3RmEY~F+keCR8%!uh^vJPM)Fq>@FrE$56L#eK= zQt9*z`+0iiii@OD&_!c;k`qZFVAcC^IYr^TUS_`68A`g}Jmgz8F*qzvWbh*Kvs;Lv zQ@+as2n(V-Duoq0J?X61n}Wng+e)9iUCuC~SC4_!P!k;5&9*@zFc7l^t`?&)e{gz0wvpDxZj8qTh3<06DaFG2L03Bf4y5OtIZ z(SrmDhKMA(7||n`=tS?mOVkh|7%jT!Er>39iCzcandkZ5vfh91HEYdTGv_}0?0x;N z%blk4XSjRP=ier^&c=(s)G@j#`LDud0W<6$ypT<|qN`D`k*gMOt&OTsAn10IBOiSX zMOGKy`=DZW8tV1=&8iN}<{{hpwf%s*!sO177uy%zcN+KaH2ae)3N9Nlh}hiOh#t=d zy6iWn^{-KT+jEL$JRA5XCMKk;B7rfL4BB7AT7flj*i)~cRmMGnH=POSZnsan6;{Rz z<9iLUNr9hd>O8b;NDmoK+KFSAy%52&=G%X$qWZdtjM@`hs)sRdqM+#tbxlV_u{F|p z9B%1~9QT1GzfK;z2&SjdWX*#_(E#x$!S$zl(wV<&+~LM}fc@SwWYiTtS?j_E=mzba zU+R*Zb33eR^iZDp@*u>Hj!q>XRJ(Arbmn212oSA}S|9T)1(p~~7VZrq4wqC>Vzv|N zx83z^#=hO+)y-?`&~6@4rsJc`UQj-b=>pbgOW&dpqN6{BZvhnRoBdpmmRETd~v2%i`bN`EVEp={l;bnslfr( z8)8(Q(rW&bs=Sxx%kOvdk-?WPJdOq=iJ~t!O7gVVqLlBD|EjWWpRPL_sXkn8dQa3( zTkRTz=eOjsu_O4TfS@;3qBKf`&~hA=`=LHY$A9TtcPn-hFoV%IE7T=X=yz zxxqKze2Hn$SaR>X8TN$bprroIs)*HF6O9I0k1hUP%0142wFE9>MA;8=mfC?Hk9T`lTSy+k2u~C zfF{j?>rxkOnsR9HKLY<%nR*O&g~I}!H2bLA;p=o6$2g_a8>&SlJ?T(njrXeP%SONt zkW*5f&J0~Bd7{#Gc1MAeerHHaV`b5SAv|50Cu_f%FI{mh=4oYU7%<2;#uZC7PFs-A zb&7W}|IPwl9V@FchvsX;McSer#?=){1l_1GW*uc> zflNh6Y>Ig3-|GMG+);%Px*lL9hpY0sI;u3h^VBXf&~}pge~+(vs>n@}OXH79^M6z< zC#vl8w*Zj)LAp&Jw3}=i%r$Se&>%!)^`}2%BPB`5f!Zy@&}^n&1~wlckS?3|GGS^Z zMcI@!C_F;Nq(=Q#wg>82DvmRtei2GY#F{x}Qp*MC7sNixM`p~S)HAH>;w;Kz&i{?& zpkGa7%Vw*Z5X9kup^=XTc=izPEa?*a&TlUM)DcyOu=B9y9{qwHh0_XvMj{`z_~ zrCe%2(q%S;JEwyB)i|+5j4)kac=viqk$EOz+Vgy^c)^Nad}$g7{rFI5ZhqY~)peUF zqQ_>v^l{h>lpE4(wo(4Y;nC_Z(G}b3uQ~)aA3U>0-{V8|c^=TN42Z>paaWCkt=%oy zw#x*2RI^cmz(MFs0{-cWu$Nc_hJE`nRloXrqsoGQaTu*2sAbyn-u2@*V>hDoaf8FF ze-}>0+#GUTt&|`@?{Fy#*5=TGiRPF$-au)vEen&R!Wug5V)?7eFV6(!4z~WW>y;}a7fLsSXA35gB@ZG}eg)5epIrq6t;()k9zkW?D;l#(p+>Lpd9T6Y(QqqSNoZwi&EoSMu`VLZDEO~ zPwKdesR%d&y@gqx5s0>aHOImFfXWem_cntQ>`Rhj!^)PISuAxxz|l;(r<(IBEkWj$ znDfS1u9Cx2$l6eTzTFK9Rv4|Orl#xh3UH85kpxt%_LskKeB}5kOFXeUnNHwo@IreS zr?Ee2@PoQ{=kBMTY$j}2VJlK{7HMDZpy-TFD4aJ-T%hScc2xBpsJQI}fz$fPs2vSi zw?lWDjPgk0NmhbY-iAx=!5KjQe0{y|zrDXV*Q{(%_mYKAzo4ipvS-r)ab34ef$%Jko+DW>sM~MLsD3c#P=|Zv}HsMM$eVVjq-j8@1d3J7s?kKiIrtiKBKjH;- zZOy`F0YH&ueKZp~G4|;>WM0!QtGyCrq%^=zu8)6trR%V>VG|Tn%!S0b`VFT)p>>Pd z8j$gpld-P{Q`IkGc18_8>WLy|&21W1v@CzQ=WxnFkBo1P{elFP#)l)XE&rLSt*sFR zJ+|}av>#BVI9=>s#lk%fQy3&|K64_VCM89D$+79m=l@fEeIE>4vpW?G*SwXW!rA%M zt&z%N4d=9TK$4#66?>x3^w&bpN#<#+1~W(k29_N%Kl>z>Dmt zTQdA6hqTSJ0^_KZnk9I$nC);M2{2@JS^szC&5l88aYN8@#6TrTPN#O1VooimuC!38 zQvs{`-Uq)#ufqZa9?lxdOmCYvHp;g=$FR-lmulXKqqrYJ+Xf7dWYLrhX42W#%_LAx z@hXx9*?cllc;atn(x6Tdd6PMh1?JOULDo2^3A)&@YVMA99!N$ApGYJlS?VKfpyejU z&*U%_#&!fzL-cI-UC#ttzPh`*-r-rQ-UNA41u&3mLN07;5$U!kxqz(>nT3XneBmTQ z4lO;zxC%Rwu4@4sYJ{{KHGq1Qv<^3_nW%ZxGwwWr_lL{_S-eZ)5D>#VvvZ25ymN~7 zuyncH;KhAuu;&TxH`;i?cJ_B@K&}uK=K6CoiJEKV3^@~RdlL}~S%{#Kr+Y!!?J+}Z zB9SrSEK;&LIhk$M-yZ4a?aU12-{tuQxLXvmx@MZ>ukblCdU|>t>_0};jQS3rgl)kKj}`y`PJNrfLYB%2uYItDzU*J^s$1am`(oksk&zdd;Hu2 z9UCm+|0ABFSJT2iJ@i|+c(#I{w+Bce`2Ih*2#o`;af7qs+`>PR2hC(j&4=qhi7c6Q zvaEmFCBSx|FdG9lK=$fjTfr%)x9Vv@zO{96!*LlZ*WSybovX^Do+=jGF(}2Wvy1d} zUjSU+vGw317B9^|3!5ZMAFO6dQiCr+q72Lqn=N_iaSBI_I#bqp95goiTz#;rW!4rd z;rVV?-< z_rv?hG!R|~7$Vw+s7_?QrhwBW9`AVN`jz0)7b{w-eZDp`k(gq`uhe}gDwzF6Va@r< z9GA@`!vkc=;=uOIz0V2axst+|E0LH z=HYnE-}nfbImKb6wZ3)<*2P`&Hew+XJLsq8Zd78Fa}SZ%)-)~aC;8H;J1Yze(T!8z zzSx#;Sc>LhGW-2BB@2MSk$MD(BmLd%csTo%Ch7H~V#_f|7%ij9Rop85}4ACfdnt zn{92h2~)lI+36QKVAmfe2l+Z=LAFhY&KhF5MvAx!x zDML3H=rP~ITW|@_Y2n&FYm~1@Ie^DX+fTn;87cJWF9K(qZ;9N;&0)GPTP)t-s|NsAV zb9vg?-RS4-z`(@g?eFL2?ECut<>cz(;pfW9&E(_g@$mEQ?C*++ zjsGD4_4M~HEj9FxfbQ<^jEj$>qNe};{`K|t{{8(zK}Wp2zxMX_IypbzK_iZhlF`xA z@bB>R^Ypp7yxG~>c7JtylaiK;ijMvK{Pgqo`S|&MeS?{poOpM9^78Yyw!5^nxA^z? zk&uI3?`t9O z@bUEY^;=qBgMx;TkCXZN`cO|)udcGTwz=!->*nR=;^O1Q#ec`Zzrkf>X}Y?-oSUEQ z?CtF9@5{=}o}QxX>+GwluCubXzP`ZS-QdW_$+5At&CSo4mz$}mtnu;kFV9#pr51K+T5zCt+}|oh=z@nlb3*hgu}wc zgoB9hytwA&>wneN*X`}@va+@3=IGbf**G^o`uh9ihIy;=Ke7Q-`?Prl$eHviqOx|<9|RS)6&&zYjN_SB7J><`uY6( z^Ye3Zc5!cYx=g6SIn+IFsgfF(oVd2B zTVhUoj_GomCO!QtT~7b&eaj4wfkr~=f#JR1IlMRb&hP$aKKJ{(%X_?moF56;;DkiL z27em_O)qLM(u?#Wy?|#utHCzYC9!SLY8KNk9sbo-Yadh7avA=g~0{Z6WD@5 zVPt+AqkM-^|K5%>^s!AC^vfCMf9^EU-};8>h~>hV^I7_?Kb_&|ySna}9sRa#KZySK z3dMaE{((Yq%k1d?x_IZdsneh7QYgM;>3_dbD7wy=(0}WEBzuUQ^P~INig^hS#{{%V zD4kaN#Xoy@^8DlI?<*9F7k@uX?AdP=Y#Q+BKctAYgi0jms9cGqZ>~g=L$!JO((o8e`c>aeqH4k4+9~*m>lmEii z*x2WP(Gp1=#<-iNADj8ZF@NREYeL^Hu{0SD|1aCQ_?7nVqD2$(Kdh;)9*1w5 z`DZ_hCHNoH@E;TS*pKe;=r>al63G@K?!EE!63d!_#}q~A?~%e|s`<4FJ4yT3;`?07 zfSLgA@SS_T2OkjT*nix-`I0_=@CW{!KaZk@B)f6~sDbhHJ1A;zzmfhPHGe$)^nC4W z8SVhjSFQICX!T>i_~KXO_`3&V*ZWjzV3%aq0OiNg*N@Wwf~uCZG4xcQL{GKMneNP= z%)CFrPM1#p&rhBDoTU9(>#=*iW|G^{E!3gq4E<)R#dl5)MGX+EuBQgJ{D-A~?E9GV zoq2Qq{eM1hx6|i;m%4kSjh^$XC6f9UDkz6yEGFm)MUgiN{c(S? z7JO?S>-_}u)~!!JJ9X-_r-qB2vHN`f_3p02{5OUSuMXRo%U*9}0(xufr(b{l)L1a9 ztDLFt^V>%6YpK-!UezN>kOUlQCMZGZ8<8N#I>)qakE zY^?T7m6c%3&;yM2Ecx5C&71YcV6r_x(=+T0_b%Gz&uU{3nLeQTVFSZX$mh?uu^Ak+ zne|39l3pMkU}Xh4f6?ihn;RNj`te85w$gpG!kSbF&V5d(|DCwzn^>y_nH% zDG+Pdx-?z7ZQI!a)_*kof{}*0FuOG%5L?r@*0Oozift#T)kOJ`h9D4_4L2OSZ42rS z+eaIO2T$W~cmy4t-dE<Tj?Fg(d$+r6kWlqs72wBoNO$n#(#HEoqMZEO{II%45i;^ zO0P!6p~g6d`{y|*d74ru_i@4Ll+0)?_DXqJV>$c`;V zNVXBGBIHp8gnvFw7NKQ)kEY5~!wFrg{JaiaWGkZOKR-}}5IRb#(B2%_E!*gn8lfdM zo@O%cy^2wNuo@r=sk0FxFwJQw%o(8~#~5_p6D@^Q6#XWW)2=}%B26ZbbU_h$GFiGF zAyqs|3__@+!UgTMmldmX5OzlU53WMT%gjW3sMv~-+}!ceUq1%eyG41GMJEs#{B#F;$qJ51?QQiA|8(R&bo z2nG2Uqkn7zJrt9_&AB}Kdcql=VE0RM(xQY>dRKT8r57}&w?a2rJ{$;rxw&XRgo@v{ zm!l=HnvD7cgen=--=h6qN~HNIO6AcXMW3vXM@x>P^X_}lIs?5u>fX2sy`}X>6@@4& zfruZ3mg>x9+@};H6yXApjBY?7if2(X4xx(H-G2zJD&)~aj10+=nbVA@%EWYk+L8icvmTdA_h z%gxQR@QgEQ;1l^;zK2uTOxovr`g+hOkJEW$+zT}r`>p)n;p_vZ_&pl1$?xIZw9`kQ zY{5NVHEf~hyGs*&lJe4C1HHpRlZpyFeShO#X?&tq$$z#s4p)-HWq^yGIK&Q?Wn!EQ zuw%#S5^$--A+BJbe-ei{(yo4+0CfFMJ~1jyIdO3Ys19X4iD}&XRap*f@zYwI=?dv8 zLr^x-T8vf0URf$tF8j&vD5Y6W#`cgPRju!bj3Xh~sWJqYkUZ540i@{-kqgprzkhzp zE1pGy!67S(@1Ch_rytA;!^wpq-LU`U^>Ca6_P8fK3|~8z=|^4;Z&xy3xRmr-Q4Fj& zIi$xInViTjVmz4?Qt3SU1DRqWzP7GPpse&!T{U|(r|#1GJiwl{O-zRY2;+-J5A~L8yrIY`R zG+{MFwjNWqgMBDKU=@I)2hkoNAcXdG#f?r_7m1q)c9oGnlp8=KF|-9spmXQ4MP#N2wYzHVK^cuR>i@aTDq-P`NcQ<~I%y%=PuhmSL`G3sBmsbl_ zn6|asJ+Ty*GiaHK#&Ms_PL$ow%_^rRdi3g78|k@eK}uHALd+Da=!UC%2t5~+i$v46 zW?@w+^NSD_VOxgX4DB%~hGXWF%IQzrk!7T3Bd7aejmwnBeY*wsTYMLb6rT4%`Z?nv z?39qt_Dtv!S{3>`B@~fW%YThe-(H))pQ!3e@-z)%h-e@3=!tg2N@GI@e3{U5LAglm ztmo5%hnXcP17ICCwy!1aOIi9LB`K#lL;t4${bh#800r*YZOS`cm0`v6<$E{cV6SDF zwV=a8P^R%xI)ewNy$***W_j}I9Wh=<1Mh?_G;I-kW6EHXH`$Il%;pWo4o$Z-kCKuv3vpi^hg}vlRqVEFy#g62hX06stkmHz^QAsuV4%1nvWPc|b&k2Z6ejRa#m;-1~p<(KqY* zDgK}1cK2i^t6l}P5`WVDkV#JUlHZ&@-96K%yUT8>F3k^g8De1l4lQUaA;EBTr55waby(thY;RK5lM6~6P}cS&EN|CWJ58~Kw1$2Vv`p2U znu*1GwPellE5yA211QheenG4REoY1Q^rM})u!0s&Gmv=x;v05Ho2Z1)8}k21<^#g! zo%Fg*$@#3$cz-3!r=rjM#`?3zmL$RObmljC-ulU_@|ai1;7$&haSeNNHmwwE$rmBi_Dlb)}?=k(m}bB~^{zvt`k zx!rTS=eo~v`p@}@g#6FBIohdTDk5R2K3QH?&MuE${(pE;4ISIrqWdRLVxRFLg7e5{ zmwXCu$^z`o3f&j%Aua4BIe*oOCV(~3dr|09bua7{X<<*Fn4n9(3-9q1K+%T$wgRB< z3lZ+wphhg&eamhSNzfsZd-lc#4*$Bi)=p&ZH@Jq<_cjGHo*s3^zIURe6n%Jp{R`bw z()aQ;u782cO+@DTi)eOYkU5mp%Vr2-Hxv>Z3CZJ~f$%V#;G>RVLaz&Nlx9ACLP$#C zIFRY4k_SYI=4)$l^idOej^~YH-~eWDVIkd4)gt%9$qB3ZBkR|sOsn~+>+jJdt@8-c z&&gzZc!aM5G_x&Y>%(#M>fVyOhH6}Z8-!I|5{RWmvY@+{YL)IhOB$SEm(~8cZ!cZf5PQ&H@NOE7CoQ8=XTF^ z&*^iEp3mQNdT#f;$IC5xK7Y^cp4&awJ%6Xq@zKuhzC)Mmu<~b@1BP-SI4LajNd zdmp;aHc#;SkuNDhcLxn4DH+|axG1{cicM!MprXVCm#|1e>Vy=V&$|fpRVca<#_f_h zxP=}j(!0}VbYbX+>ga6K&ZY~YDQKvxOh@kx8vr(UyFhH+kWac5dev2<@1so~hkyGX zIoy)*^eOZAwsG>&$Yyf701T69b^i{xS>!nYjD0h8xSG+ct{Ru|M(0SCJT~(L7iFF9 zU(@|u^3barh;D1MvXsW|+hDtCNw~C1=*NJ3&!_a7tHxzq3UC;300aYBN54kv`cw33 z9u1H$m1~LgAM;2krKe?M>ZgNO$bZLVfYNKO8mtYthJ(Q%##u*y-3HJ`>-{p_>Kpt`lvs1sVDwG+77gYMr%&ncTS+tDiIA&BVjaL%T6*KNz(#ky_+$1x;SR(`J6q`QT%t3-E(?Q zf1&9cx$ZeVr|12BPSO9)pK!U|bKP^@=M??-%-K8omv7azixAuk$($G@08|xB&S-;UW;YgsvL((N}NVF)Pmd4(1QWQ_AMI_)YwF5 ziJ%%ak9%X5wy~*m)qSy5`7eYs_>x^nwYUC3oi`uE=<}2tMRdGCkN>Qoho>s^&=Qoa z5thfG0AL?va{CIYRSinIdP8C{)1Fv~f2#%O1W$F;@O|4#%s0{R>{{9N+LbQI3y^ql|ak2Fn=~Cz=>tax`@pk0(qIM>qMw5|dh^I&W z{rz4h7)F*T{jjw_Ex3nPh^s+O_bhhLkoqrK9y`{fsfIJ#{qKqNM@L7sFh(rPZwCG1 z=;#cwVCt{y>VJ;WGcnPK+qRWV4^!%4!r*#*)fJWRk9%C+1p3j@Q4158ka9dW3L?E$ zu$!Jn-R;uoS#0-a0zDM=uc7L*(woxMe4$S_wh~nqm{GB1s29zGtp#8fdDWV4CE8j} zp>IcOOFU@1JILh5=T30CX42REGApYpdi_7`My=p2x__VLkRL3CwAG8oX2QpRsCn!U z16=q240`gnBGy9CmzYcvY4u#sm(ijT7Cf&k{w0WtY19acqc1j@YUAmv5UNt{imT62 zWqqHZdlQXJTD@b>rgkG@1BTMUmQINCGqs&QtQP3Nrg5z66g_K_CkBSL1PjFM>xprif_RD7HDa&d6VLyFm!bKG(44RzH zXCLkG`FsA9!=G@u?m0bwr^|KEbNXQYOb7l1{^aNB(!O zSPx(btnL;8*d|Irns^z1L{KzTe8GWMaTUbw9TDSQh5lU_hw~`CVPYab_R^}xBd^n! zn}5+0h-$>CuxPmbOy21IwRSJ1JjQQy_OyzgU#t6uw9}#v zZO+6jzJu2R?6u$sw8-06@WCPEAH*LK1b+{9c6KZfTHgk8LzZAoQi4lpeL0d5<&5zf_n1k%=8DPEutg>t8?gPCzDM+J8x#*dpn)4R8xyp(16t$*9$r! zzcExIh@DMg0Sg@hJHqHeZPn|r60&2-y%F1F^b`v#bj7nr2Nj5z@{q0%Kq>v}^yi|{ zbIUFv^cwsCt4#4Gn#}L)yKBwFYJc1mDiH*QX0y2j43s`2jNXB7rXQsAv`q);e3jOr>b9$K>AC4Fh_kZwxAQnqq zm$O)U334(0ycQx6;6^y;~zA5lFKpnH!5Q(6`DrV|*D*g1D9xQ=l(%$-fj5~4Y9AFekuznTnxRmeL6tJLo(aC8U2 zC2~^|P*mEt{1m1(pX3lmtX;aW0qXhx_qJLhbuO#%{3a3AbvAtJFBK}PGuA@~v*iugj=~|PJA%=0ka*P+; zlSH~%hSU;2J$K_#Nh{(@QSv|?O7t}xYDi6kR)tz?azHWCXa&U?8@eWvzK>4-LQjG? zyWL($jXcEic}}H8q>{{q+t@BLWjYDk5+UX!K%xSQPXpMu0e|f3-_-Q0Wj8LPJdkxO zQBfFus~$N~Ln$K8dO16IJXOi+67El!}x=n9mxoPHnL zBbQTpG8dMyZKN6GmrsCZ8$h+dZC?7Vf0EUNA!Iw!q_?x_UEi7m?JY*5Q6)nI{3%Lr zP01wh=gCi3L@$I)ic@$ok+1)&geBlBec_fl=0v|D2dY7AVeVJ z$)JxDkiH0bJ3_e<5J}(vPFWfp@}<9zr1&FN39aI&lf?Og*}N3kwB>*Qj`tHki@=ty SX%Mmi0000iZFhDK1jySuwv0YSPEknZm04F5Ub_n+nB z+F^QUXXbt0`+0^#!9RttQN$1=7#Nrp6n401DqMilR7On^2F8O92F5oK2IdZ&Euz*cx|i}lTH5Q@$uQ<$;WN~Y zC5UIU1SPB;RdiE*gkW~3BYgMlNakO*cWrK6++^kEJRjO@Z{+-X{k(iIGII2{x|JA8 zPsPhi5QqRPMrlf!<={93hk}AaoZFx#FD8l&<0;@N0QiYvVK>*xlV!1joOEyMp1&7?r`&-(!MgjiI5y`HM=4iv|kQ2-y~wQu;!QVz>+lDJZa1 z$5pXOap6x-?X_SnzaGOdNlQar9FROcEd$En)H%`MzEEaSi+!&Zh03n<_J~tySENNT z#(tj${N)~~si~>h*&&?fF10C{#LfwC{6&MMx`c&CYBEwhZWfyzvwd3@Bt?tIsSJO{ zy8X{S3{o3o5h%?^%N?(+r4?zQRC7f6w!5fFTJn`I!@;h}wgtU>W;2{BH|*CTDSp}2 zt~(VN*gh7nG5O(qJ}}}9WeD6zJ9(QXKyZ+52qt_pHUa`{Ew;+|S{efBpXm@gLrj#S zj8M zH$F$aPsfO&i?R3E-jDv-Z^#I|&ryF0x1d#H9gA+vn@UZ}22w_8VMAMX(30Oh0`N{6 zB)(IIE~P;k1rLTSUCZ|vAJPQN(&*kxWe8-Wc(%JmOdDNCuO6@c`d($b&wC=Yi)ON# zP4eXAu;PoQaJMG!_Ha|J)O3aDt>txaqsr&JSLaeSza6a_o%ZkFKQ~izUM|_BEo^9; zJBq&cpTIxQPf1?_jm%gE$KSDv9RTUpB)sE^qvDH0lp0H1);&9c8qY=Y<~%0V*LLC; zn~p2`6=8ILVq)9xuewJ?+HGwZ*c7QEo-Wjjj4H`%&n|^fwjV~i(_3Asj5@ramwWhg zjbBAHigod=W=noqQq#~F4{jXuAGbK~^Zkhd*o@j%9`fZ90(Ck^YTlIQDFXD6Im8gZ z*kf2|+a6lPww0*DCK9$mHo;G~1iz`GD?Qqasi9aCrPLhO`|etM3OGJK3*_2WR@ReH zrH?n+1}ho`ujX``7`9JVdrAC`XX@X^G3dbAAt7|_@mQNITu)NYj&gMmm7@P35kmF2 zI)DvABV4@~cDsQz*#5<|vjdWO#4FV)!SBCJl(HNmQZZ1QlYo=G0=-7yGYbnzk~tU- z)kd$F>a}lWtf~EuENw0Tb@M`cC2r(zkU&-tLm#I1aQq1~mG9KC$f#2|b8=khy8yHP z4c5UzeQJ=;?pl7;bC*vNpFOpp+X?p8pX6Rk?Q-2-O9E#7sBewgzQAOma&Jj|>B_|B zP#F&Qg&pMa{u=q6zCxbjTaMwa;Z#1w^*+VsZVv9r>~!goNeVbdss}>W8~Eh@p-$~? ztFAGRufJSf7TMS zZgz0c5~xp5Q4Lz05P>^Jys)>(M(rLL9OE2OG@dTBNs;kPnmxtR$TC*;6XB$@tNWFm ztAZ@L+#!D(?RrOEw^C9I!qF)tWv#kD_F-fh15_;=&v?%MSNY7(6%e^Ryb zc-`6^LgDlCG@E1Gi~EYrS_`WV_eU^w5UQ9Z8(WrCq31RE%Km;T!%!t(0b8i4XU*#$ z#9Wv&6$ZWS-Z?(SS8F#$Y1l@PD|!T3{Qloi*J$Yb;H z75e=EQbiU6&H!&ePKzc_)7R1_F2k8HBb5h~Zy?&sv_*&um}}iE^gc`Le*E};{pzi4 z88FGUmUGFDYexkcSl*X+L3Fp}e_j1FT!PLvhhk-R;`spi$Y#gw;GQ5E%+KVceIrt807Ex8DsW4gQMG4k84kC;! zQ)JHz?&|?a3{#l=uex%L!7FPGOTaPGpW%eRIz)R_nTpUAYR3sFe@I}lV$djJcWesx zuTU80v0Q|NyOoZG2`8_I;W3sh{@TkO?G9M9j*Vs)7KTlE_thk-usvPNI+&5{bVw{G ziD;<9c17Wg&44;8ik3@q)&e2g_AcAGllQ0j$S0sCdrXT>6^!TgBo* zsL*vO-`{No)a9g;H0G_JrYXMGT=u_2*-l$&`uY5(-S4|_(>8R_Yl8bs!j45v7#Ywcf8WgP;(n-dDJ?54Z}LE+ zzjH5RAbCIGOr_pAZ!8V}@OffqoW|s0XB@f){FL1p%cdb6wbO4M$=o66t8N5eVk5u4 zc9<3HZjBYIX0b7tZM(tww|Q z_6m#uWUOFj9j9qJv)8_bh4fB)b2u(X3!$Yq4HDOO2G#F;{@$q2Stu&gaO+4zl{fv-}ii4{4R$OfRfpKqCV;2niedITV0PJE%?CPwg^Vmd&5a2d^L zmy@wtkdsyD*RC`aKV7r%JgSTJi>B4`HD{d0@_Q>?vH@iD$c4P z{?6J#83pa^E;elAW-X7@uPIh=2bkjAHW%rn39K3)Ru(O&e(EQEe|!3=>$DxmIK+hU z!<~?)+BFra?lW$>0A6#hc;@J+N;w4(P?u|wKxlQ(>atSuKPi{rnz zRJeClFTB~Z+KzInlokwajG_dh60pB98e;Z%M#xWFA6}l;dEQ+hT5aeSI6vZf%;Mbm z$z9oyuk5ABvh}qLLG@R!);8Y`Y<$w|n2DEjK!Yp5Q4uaw45yeOddrVl#gbZBXkeWw zg!Xfoqqn*LUEkaybZ<43@bt~HxWA~bxNo2k3yPqLH6h*wNn;rou$%&LMrREY%moHU zQAS;1t3<=-$#G|x^XO~X%V)=4_;&#jzU{Q@x6f^LYA~Q?tRjJ}TVC%;Zpy=O|9u*0 z>bx81M@b2Jg9fXfI!Ur5*&7u_iYl)a`X&|}n@0rK)?2@P#J3RMmC91}Z`nw%Dm?vd zas7HcJ3lI%e^=`VQ9v(icjQgtg2_TSO#1qQXT4P=OV?oYld4HllbkFT)~oGk?3e9g z!d^PHQWyS|$8kYbABp#5=ozJ0WJs=yT-cWD%_6#s=UcFnmjR{h(_4f%Og(R)K8oIg zH&;%k%Cwu_ecBF2qr1`w_VBR+QganOVmdyJcbiG$s`UnVsEbC3j)`E~$OQNJp%B6# zZW|fwt_zixU$6{q4~v)+czfzaj!)dHrXXJS_`Ub?hNB74m&{uTcj7+MqX;KeP=lv> z1A)L5BkUreQ1qj=lckVWBs>+0G* zA-i^as|A!XRMfj}KCY#wrKy?Q=4<%-Qu@Aj9p_?r2uWaQdiGb>*g3Z?GecWCHm;Z9eRS_N@*-Y3fdstR##Xz~CbGZGQ zCC)ITZ(mhab*}e&SK+uh^DhDfC+s4XY%-S(p!($D7A?Ux#@_S8N_FhEj2c=-F2K9H z>Rc6zCPq0-E$UBk@xcFgy9luse{~` zP!a=^lcj|#7J?Mvtv_3ulb4ZMIpk4r5Wp+?hv=7g+FHl5eL6eEiG^r23zHhJ-JS7) zSK!2p0N>VD(m_}Bp~eU*k-n*;J01x=z=Rj8x*;@!|OaZK>tTAL)|Rabab}cq{io zuWE%tN!d~<^L;MDKAE={CafjW&wvOgtROE;vOquh5QoLW+I* z71FPK|Aef+K!gPr=M9Qei-{&gY#;|7*(*uAzNFq{XKWREzQuz1eD#+x<^6b#3=@3f z^#w7}yCX*>uC2*ac5{~ni<73VGqzz$-^>}S;LG2vPQTS?DP52Bt3ID{CUKgPL=Ful zvY~-RbCd0A5J+HwmyRIYX7PF$Ne@Y6GwK;hZ`Cn=c$k`vqobydS>IXD++sq{(c2SyZsTf^cq?Jm=11OrZ`<6ErrlXM~Vj^IX8C`aeZl{Q{r!iu2TOX z6@aaeNgQkZN-~xd3$->IMJ7;Oo=luxX)Yv+EGY@1 zhx6;sDxec9jo*>L<7y8x2o)cWt#abe_md*k+Th^IGzeUSIE>I@TY8}~tNMbwtYk8`RLuL^X^3c%3L*W z3N^Rw0+FDQP`8-bD4YyVDzBZNKjK;Mk7SW7_IZg_>Y&x-CC?N=C(f@&Hp`=INJ6J} z|2kp~jMW43#xj=fZ=7Z-xw2u-O4RB(Z^-z50^wa9?Z{4Ra-Q*@x-Tz{|-(_j;6X;9ro!(#UBI?wB@go*_`nlU5{)nf~)hdqeb8vFMD6%?{ z|5gS6c)s0}@MF8j>x22)%>DTeZ_kR!F2Bd7Rbsv(k*PABboT)fv91J2RS2mbJ98Mt zE&zzwa(eNaFryu_iHvSlTW$`?CcGf1c!^;c=dj zWhV3&MWysLEG#$5$o&z(X4r&4PEJ17;w-;2uV*U@+?-Ht45uarK_~l$%VuW!GCGyg z7er?~_D-!+vio9E6ojt8#+-*ejn4;;*LESNnRh!dF#hQFu6}_)bo~}%|D7YCY*eHo zmfl1knaMr`hd2dihY5UeEYWYKrvKM${q{hl3eDwk8Ocd01#3KESnpQmd8T|m%3@Gs zHCOfxcqrpom~);k`*a4M$`KkO2ARNP6abNu21i4s{moTeA&`x$RX ziW*q`PM3h`#{`xII3EA&V-a7l@fVZl_EJjW#u>T4?~Qof$|o|93Vds@U8>-qcJO_` zMX(9%N=}|a9JCbhjk8z&%4}i?(G&wTj6N?4rS3LgU$z79>83o1V6X0 znyDyKhuKhq^|XW%BJ5vEV?qLwXwh{uPOl*8z}315+mow`M8M`KJKF^80aw=6VrzYDgCvRh&LnVc*ltXP*E0<@KR{J8w5N(~yEB`R#H!hC))iM$D-A9{qZqSl5P z6g~gWUo=I$OLzn+bEe=w(_mQJ6FIz*0024fju;)5Y|^}nMBe#jDVY1h(G?<@OK>>I z_s^txQ9nyOKEhaHdP4{)tmKQ(5P_@Vqkj}JEzYLQ3h=$Scn=*2qKtaNiOLZE@7V6h ziA)h7KURkI1w=kl8msI$Zk))7@WiD+;QW0t;jU25gxWDF0S78bo-2Yz0l{iR(&fyO zx(BlO9z7&a8^I-_d24^Ia=%Rbt4d+ONf?`_68#2(BRD7H) z6N&)%;e8P09hdpfLQzXl^?zK^uTbC^!+aGVCJQZ4z_n3A=I~&NC;#Fpnb1M zWZq>)$EjdfF~WUe>E+pu0M#Y*VNSNmX%|({~e+a`1eu5Vc{F7`DYd~wI zK}JRT>)2SgrXrc&D#tFlfPZ+FvGygR|>N=Gtbim@A35&ied&jos=3x-(nJ_U6sc-jwNm zmMt3MA2ccrWrSWg0a61^i#%(R0UL3z|6JFwAmDSEP;QJ0h1wA`1df za0Jh-czpnGFh6N;95u$OWKy|deaiXB&8fM?cn$wwd)~5t=#5|hZJns)%ZZ0X9Ny?Z z{{wO5>e*(gehBr4Fz9P2Y-soUo$aM6AGF0bV&8NzI6$LJ1Bdm-D6o26%CGX+vI$ge zKY#W0mTI$^EZ!)u&ew|0wONuHwKg|P#1@2ezs5J2&-T_kVb+TKTD?*UWI4pkJ#?h> z@bmEz4K-sLwz>KBD&xC(zVj`a{sZ)1?w_+(lxNg@a7D>Iki&cirt z4I&OD0_Fa>(h(9+DC95VfRyOPO1H#ZyET@-K8nYcM`o{9`5Ql^xmr((&c$}m>p-P+ zfu8!(*^|wtU-qi1<1al1UfbQ+<734i^VyN5lV#7%W>r=Gk{dCHPYetq}s0Sh$Gtoi7zSVkM*?rXS=|5mGaU%N8PdUiym)5n0%e(g<^ zF^;NEPVOh}+nn57DyH{xxP=BviaGD-t2sDUfMp8ZMoW}Kfx3WJ*W>Q1!&daIe3J>* zpJ97*@b#R?0-iUnXEwHevP4fEuWuEyh&DJNuPoInA4!=-mJB)#f z;-#<8-2aP`cQhbFLY13tQa}`gtzG&^XYt11^nw``6s228k(}5G_V)HpgOa0EyjsIU zcq0N0$H7D!OV_=^Dy1$Me9$iGSY==}ej$R%N$-1neZ|K?G}9O~yD?ls!mpg6*#n?@ zv{&{y+hF7a?`T(R^Ud^GhUp#^g6?O=*7`xU4VHgV50QtcI5}Uvcirt;nQT=^@srzx zM2=T|Y;)sK(ra_GFY;&M$GIGMLA=4sm$xm_krkriIoOxNdF9ygjE5@;$ zQ@UDEsL;=yCc0#oWm`ShovCW`%A=FFS9Ty4gh#eX!s85LygUnrQo~y}9PlRvbtw7< zo6PZ(VosO4-FhFLF|5IugbA;C*7-=qx{zCz@pp9Ekv(e7>iT2li zXN0!SerAWoIQ5Och(Znd6k2rHSH|f242>^$(UcQItv0)IM8d$Dsm4-(ikW90TP3fN znzrobVzVh$>EqPj33e*O((!Sv(ahEFne@tb7-(TMdV%6PHJ??LsN}U=zqO_|#lI1A zbshB=N~4(J5-FYgJmm#`TieqW0_p!GNkjPa43s2x>xUGD?6^l981m8+s|L$8rAb)F z$AZhQf>1l^7gB*9ZpO5@`-$pLzfhSqthFl5n3>k23hZ&e&Un>L1_cFi8p;uwNT)-H zkwWYCv+`6UxECiX)G-gR>weGA%R0*H7PPhsptZe9!VS?>&JzM4*(Pf3ii zXR%!UOu)VrpREq6_?N|Mo;%a*{=Gm@i=K)O&8I*idByu6`EqwV^UJvgRdGi$XLDCu z^Z8}0MTMpQJ!=0UD$eoA2?XjmRECi3{KnQdP#EO-R(QW|5l5pX9;rLTFEfrLB)9GC z?7ZwkFkS&XDyY#cJu#M3+H6LQI;*r9u~MerAa83F86^EbFqNqZ(=Or_B&=+FlgP7M zdY>Cc{>%E^)zN%ET#1Fk;G@1ir%AIBQ(}svV^txN9|<+*#jB-eN9|te=Rj^rkAels zb$hkv&;w|bMEEyx9Gty*>dghBktrxT@q3t%jBZijiiPAUiq}%<7JvEp&Y~W;^SHD! zWlKiu@7mW1ny7>+*0i3rS*hl$_XiKkS$|}vFHK0~soQsip8jq2#-}OI*wd4vrB>*E z!oo+Vv!9Fym)j1yEy`Mv%*-d;wnQW(pDt$d$YbA=gRG)-eahjv{lqADH~fP}g<3RX zk_`YeT~ZoCHkBLW`*pr89nqIN*3ta&Cd->dRrc*-P! z)l{Wk2pti1<6Tl(RZCCz$?{# zrC;UTD zdOdp6-{HaP^8>Fa{E?(N^_iwxm9Y#9MNKWJmA&}!?#0sI*N+Xjy)LAhbs#?9thIqi zhWBLx%*Gvc!P&SZq!>6RmymrK;3E$I`}f(wQ!L}3=+%=_dO|(g#6NDE4QW+X$Jmf* zCmIGA`8>rROC!xQX)>KRUy0Q8jzJIEynt^tSyDNIMu$$pBdNT$Km5s%Loi=&N(BXD zn~$XN#Krv+OA(rDF+)y!@0=1smM0c-B{S9bfy)cY+IU<W#{fGQq&fH}(S*yMV^Vg{tg>0?QG*rq3)w&=u@7 zQTz~%*E#-tT4ljlndr=)qiJc>)yd%NB~8QgB?S$je>U=XlqR>tP8UqShS69v>pa94 zzdO7L9+@`%tzuV?@hcTqsu%e7$>R6d`5K*^jXc8%zF2glJINB>@M@OqWG)}S2#Zrz zjV8+3>8Ts-Re9$;a!tL*H=bzaAvZQXA8CmxFihld3ZOY2QNdEW2soCMVUND>KOVkE zM2K~qrp?I2NOtGlB6ea1!y!&DU|e2-o~+5Ax#-XdD`XX6SJ%rEIRPMTq?k-esYT+A zBeZ~uv>lLZzr1}SoV`2SUw1?ZA-ck?SaNYNP4HHCvInJjP=wlvh9-A|v##^BD$>rY%_is|b%!$^WclHC+)}Qshy>+J?;%~lI3iaaM-yLQl(>YH*=ffxqER*Ilw&WZeJNq3Ly(}#xj}+mj zhL$K+oN2BUq5{!4Fsg;V8W`lk-dZwj3ABP3~2vN!E` zhl@a8O;3#SkjD4l!;ql&0`Nl(UaZRtLF4o^6nHo8_VjT0<@;7`Ox(*V2*41`jPzX` zO^C;=)+4l;;01<9`rZ^O-;b(TQ9?cj`ZD*{Ls(^*A$i^iP-44<%0O^y*a&~5VLb1C zntGhq%FDepnbweR9+@*4NpafPRj**@i6-S>Xj>_xX0W5onk)}PW!({B-{M38`0iS8 z4D*P7b-}*v^-sH#x<78f;Bwc^74*Xl_zQ)Yk_Ng*`;JDLqJ+?lE80{bT!di|I7du53#irUZ`i$WNc zsyllO{c+lQ^3;^xZ_R^n7s!5Xb9xI3;?>O|LQwp+OS-ceb-;~gAo@4D21wdla;#kL z8}oTyG0)V)7HQNZFtG^tPHAo7>7?P3k->qqGMIoRW$%IE$68tbSd{UN(}mkyu4w;$2^a+Mj`2v?|knRtUORMe%Y| zH7?Kg0zF>FTWw5>=@LYZqMHntIGyc!t6Bd9mek%0ORg-J!<3c1y}?_b`$Jc#i;G{R z2=ONZ3NVoLSrS1b$~cv=HC@1!%jb@%okQhu^B{3qkWm;GYZVDp#5G(lvSQa%z<8;(J^l!A_TGZ#MCi&aSGgL;hr( zqZv(h?oOJEez6gi`ay=IU7;ToP1Dy^G~R2{{SdUGQ%3Z_X&p?*lJ3R7OL}rLfV$on zxeCpMM>g4;qA=c4p3DYC78$S0A2-lEVmSZt{rmbEaP${|$v|%RZMod*$`X6b7WI2b zOSr*j(uY(SwIU&7k^9lgKd(X$0Y5*#vQ4nNf7Nm45#th<21d%LjJYJ1Yt6;R$fj?% zJl5Sz^Q(Fmu;|JUU;L($flKeDvm+rPq2s34XgTb@{U-x$fA(u&HU$+F^f z=B7NW4%Cp3{=UP38Fq5wHsO?dYEZeq#g52xZSHT;cSMMKdTBnY;F0ISI_-=RNQIM( zh+&XS1xQCy^bTrTul)VmsS!i#o)(-EFk!t;=lujS!`Jl@n1`)hVwt^T72c$nx!FTM z+30rup%#geh&(wCWY7paoDHEv?d@gbq~H)>9kEqt=T1EDKK&BRc0*Yrw;w(7*2na& zfBw?N(&W6~z;DK#(%0P$E1bzke&+*>QFxw;sZ<$@Dap5U>zpFz?3H0({sd8}$zuzK zT$GM9D2JpbRrSR*hP{?|FDT$XJBzr3eQFojnfv`~KL1k)%2N`b`5LnT zP$;abtc6MK$eXO2e0%SF(AV91Z4;u9%tvG_nMV6-@^;I9{PJ&B+=yXzzrpotz5!= zHm79LY~iMOd}KR|e)=FKb&8#(KhK%MZc3lpSY}SM+2TxUGFbqy^u@?FQAWLiW4PFj zeZTGobQ=M^SBC;GY^%!Yx5HekIYpgqDQ`)<4^7}?$1{!+nFy>Aa4EvOs1qjjAu&xAWTQ`o3)mfS$jFnbn2XsR9FP+b~Y&DeklLtQWS@lJVocAP680MqXkD;}@T)w|{SQk+r zwcr2n_aJ7V?qOtPWKQ(` z+e8OEgk=!B-WI+ql&nkR<(Ib_US+Wa>s*)13)gMvRpUc}vxI}4m!we?q+`sZG!{UB z4y4wcUo^Zefr5TJtRAKD*8!1s79un(;H6ZjLk5zBorMV*<0+#)y$t`#!te7J-IH8d zQc@{YLeJ9x6S?9qQsn=_aG0t>!sm-KY6u)L3asie*TiH(3o6_<(J&i7mH{x>A-eZL zwBSJPxHdWl_)?gZF_|)o8j?3)*W`M<*lU|H+64;38);hlchl}? zTfrpLrDCAlkX4H977_QCvvdRj11y9tk$t1pu8hefYw&6AM`Zgz+Mx{0j^lFSK7-4 zH=AC>X#>Z6x8}pb!KOa8%sFJo!%g9fGr6D}1dKHLPUOpz;JcA*$C@W}nTvtw2MhJU zV43E*{d!Ogq{T7L+n|eDCBO68EI|QOk{`P0X6`}MSlfAV?OC+M`1WHARU{cJ;B$WY zIf-lAmCx7o5f1U((P`9hX?!&RmrTG#T-L7ER7M8kbTsvvfX%REwHebu!&TDgL`-ay zzf5Ox=3vAMG~~y!d`yXiAJns`0yx4XA{&5VBZ!d41?=GcVWaxWYOi$42FO(c?AL`u z_-nU0JHar4!y>KCj&LS>XfIHtKsxhvW+NI`Y^77QYnk9_7mP0jI&2PR8S%I(zP4yn zWCFcWdi#Ud6)2UUEtwfBU&v|JwlY5{s+d-;w79f1U%-Ap+kC)(HC?KGvR}qOR;}(C z-bi{ztE}|47=6QZ1b*oJcR$U1bzZhZ6g!(v4f;akSG0|={imgScHq;E1w;yPmo!;j zU2$BY)Nk=Z?0DEi1H-)JQAigYyi6NrAxB~zhXqRM!M!O$!FE(8{#^oMFLsLTj*!sz zJ-j65OUX=h(jOgbFg}^@GN;{a3DtE}#y}%rccXU0-($JndS?H#PQ!zdi`IOzqdCZv z-~LIxNA`>#j-?0KE{-PU>21%q6iudzR>`y)?F=Z_m$ruWz)nr|{jRt~uklOf;D4&I z^<|mF<(_6XycH(0H>O1El`Z$K`S2Y|J$e05$v>a_*K!FAp>WSEQM2>G82h;eI0$dG zD#Z1qxD?63VzW#O$5+tk^UpJ0E@*@y*J{S!gv|Yn(#{eEBiK8K zHwl2p6UcO+?OVg?qtaNBWh#Ysw&^V~cY0M(9zQB^O}0LO)_Im}tdx|xxyw*lsH3Cf zWZGN$sgIVp?iJx-Gq!CE!knD`vHbkI9u3a+zXLOTXzF!#(xf{=!EP~;&TFO1$4hD1 zN%oNN6~ZzP9B*>rXykY3)$zHXbR>B#Dp0vJ1V(I8!N_bEc{4xhQ5yyU=y6~U_repiS33AnFA!-D>X}z z&>lf+$h~_Q41Z_iRJ6P`>R`rWRGTyA(W>|uZkMB}f_qMvNN!5w`1J7C1mQG$vjdDx zBI>?_g1qx%$0v?a=BQ1t^}gY;W=HAa!vS)Y1{>lcjbiGG*K^_GUIa*(YS%w?YCi{g zULR%O`NW5NW01}SSQZs119w)`pU($^O&>P-@*Z^v4TQPSePc#L4w;!Bd((X0k4VnOy}{upVwpZw-u+)1IsnjCw+N@`KIQhw6!^-! zF_f5Ci7`*N>{%&@c5PGl!1nP3tFTnFl|UhhJz;b+lk&vq|61SJq@-vZ9Mycn7y+s8 zz6Lb9wttQ{1FkC1t6bYgzdM0`%fA86MNR0V2aGigjg(JX+5H@~`b+9)rYzZ}4GkS# z5`!vIEccUw!jA~pDD;H9h1dltBQorv2~zW$38jbhXY*}|FFJ}hMeeJ zVc5-p)UTb%a2MSe-m22qSy*=DojQMFQiPkbi@5<3Od=Z2Bg@#@TCN4UDRqa1RoQe; zw>L()vSE!s(cls&vpDk-bbCZaSiFV6D@$j%uzvfI75a+AdbY%XX4*?vuL(*u@TR@u zX+3ry-Pf0i_Fw*L2$1+^i8&JBCV}q`Z zkl=^n07_$6(7iuBC3usV_d-l&a73}*ah56XW2p|Zz4U7~KP*vnW3;ekkRjjLTAfa? z^HKeMLGaVE1jT*zijtLeB<6iyYh879wJ0Bu=bYm)my(lf8uN>j+*X_Tv0cqq{a3%< zlY>DgIn4R9e$mN_Y2#6P_Lt$klS$>yo^@$uzCXWy5rYp^V1f?Zs`Viss!Fj2`(wy& zGC~|2*dgt*U~BCPc%dvXM=uUmb1E?VmeLl_q~%MeNP6lFI=Nowmv~7xzxW2CX@HOs zZ$`f!J;r^ojZdQgbqGomFwKaG*YNq&fe$oY5%Mo?um7?j7NlzA8wY6KAgfc6BpTof?RTLj@xIkheJe!AfoV$(icIB2;Qv5SD!C- zxjr@-7H2Kzt;T0`2H&W zWN+F!%VD})4^6kurl4wu4C_DyQmCBicbXZ_p<_4ra&Mli_9i?B-q=9)zYM3cv#GmD zN=bEheoyELCxuOG0DY@5F!DE$HXmQfwI-tJ;DJXwy1{5<&F<*nu;!S8VKMQO)TrhT zih*c-d#0(fP>S~gCm1R?`gp9Sd?&3)alp_z*1HPCt}d}hN_9HIk1d~R-!U8cTs8++ zx}4|z!y#EOj|1WZ@6SWmKPG*PeD&lLX*ULfbw1CTNTU*2Bc+x5=BB2T@v%-`TfOeM z-;>SNMITRvPWYiwx4ah>m9c_GPi-q(3B)7!x7LO#?+dHEZqKX+vpz4>xRW-uwmJqt z!Doj)UW~pfGR$Rq$EP8Z{}kKt@Po8Z^X;sdm$=}?!8{Ottn3x%LF~cH@3;wSN;$FI zK8)3c`u!@qL#c1y!vn58oixL;_^m|x!L(vGiI;G`Tzuf#FQHq063;#mlbvyan1qSj zb_Vh=Y7}Vu*Tup6Ev42%-Xobpe5zZ;#}1Mb&C;H5Xx;M{X_schh>% z$H9WTchbn9$5 z+2c+)kNGcmXS)P8?*=ghwGCt1DsaxF+68;87mb_!Q*{}Bv!A2rL6s(d5IMmc@F z$GY8A?NNM{i$8GHivUh zJ(<(6iq1u-O^QeGEiXdfH|WUnNYLgTUfWCb9!Y27AG<=#8}f1zg;( z>RixIj20ClQB5d2^7`R!nx(M%M`CG@SWom^l!82yV9O1==N|b>0tXVm7-b7pRC zE{zMq7V#x|{9kP4!9_0u%bqh5MFo^PGGE=Drfn8B;7Mg1Q6ymb~&2BDNwrgJCDX0d6i>6dEL7#`=!}= zpI0Q6jK79dLEeA4Ob2%$`7M2-Jn>nQM;{p1f~$eGm^{>T<(AS=Yb?g0Y(<*ZS(V)l z2S5BMjdX-5d`f6N)uHEZrijl&U`Prm43X|wZuTkWB756_AIukDo!PzXV%!z6Si#5= zU&LSpWurf6@`_nlBn9hfXyBo7p>T(Xgfb`xp>8%4(tZCb+NIb4olJ_-waSU%n<>n= zBj^x~`*L1j`>Q~iStarlcs_tZ_Nh9<)3F~v^0|d)2m}vn zG)weRn6yf|QbJuw)JL0MS!sZ`z|#}|FH}ltCqe3kBUu>PB(=*8&d7R`DAF}W+k^Q& z9e9>yi$BkQ$C_7}4Y`nqMR7QRnQNQTG1kiLbD-Vxsu#3HeXFXV>^_;D33A4oG+i}>rrM}#t&7n>-f1nC}P?c@Ae+-+yY3k_RRj|cWynG%gU{g))C1z=McFzFU>t9`H-ehcYO;a$Pye>GdWj_HslW}UM*n8zek15d zz%rKfR*)LqZn+I(c{!mtwgDkZ*Yhq~SIUY=QQDHUmLUQ?=ySDVrDXdnMLo6Lq=e8J zHa>yULWj$JFYM?YVqTfZxi$nIG(UkK;P(_D$nHRQJ56QDVhn56sqJqgu5NEwx{9nvvXsr(X zhjZl>Dz}eH$$>LL;3g&u|BJ<_*#3*ZzK#9w=>MOVJ?;LNmZ`MAyDmMM(nA4(86_ks z()Y*iaQ|QdqosD5PUoM#$K|g{8}fgxkr5G-6>=pfMg)OE`$Q5dv933mRje>D*t#$O zi~tXgQ~4`uI^K`m@;uNK@w@Nobgtv@zC#aJN7|fO@AJmguGD`Cx_}RLbeHKkDilP^ zh{3$;Q^@d87ML4jr5kV3E3ZHDw;1(W7!Tx?k=bwkm2@vRj+8{NME*rtP%Z-`BIiDL zwDJimfd2*qWRA@5^KBu$9{@f+P8jSGpRdSXe#bGz?88osSob172fKvKEI;Ca|i!dxWv=seUJGs#zimxaRTe$d>qdNtHTR4r%qjlJ_vp9GI8R| z9s?8X(Eb_GlWXnLneE4sbLZQ$P4-B_W0_KJ;SekeljXY7w>Y-#-X|+K7cpvDCgvw@ z;G;7{?>`eYeHlUL0`6d$a?gRV%8t7Pcg9o)wd7p;{}Yi8Zt?Fn+T?Tk59$pc<~`LZ5Oi;Ez9kgL?Jq;s%qGct1fU1L3k^!zLIpVi@!v@DXg>xCy3B zo1sJ-JwAN+2!;&)1ftOQ+mxx_LCY4+&_TqHn#PhE#7SR#joW>Dq0hW;c`tqve`00m zK^h;`XTidMVEBkn*eAfRzy1b;27U}xt5-pnVxPgyfLrX7&uAee;+-InEMIo?fR*0a zmZc;WpM;7U08>OS9&NLI>eOlQ^ehS|(9=x$3gzIuzdw7Dq2b2m%U9r&PX;rah7B9S zFTebX+gHbe>>Pbe%u!wa+E<&JULjsCIcI7!%b@Tp+iTR_fhnc zGGxd=cBDv5$uR{HdFRpI7Mt8;QQ5ZOR@St)`jjc(p$cyg-rf!1<%<`v30-076Af#W zt+8o$S=#d15H)J-4TW)E`HJYslZetSU%nC>z^m*ljsx)@H8RD}_W)5&f80i#>;j`k z4PzHsfq}Y4!OYkwxw&P=#$hviBKqOSsXWwb(4Zcs&&G`s@%ekTUKv_>eZ`4|9&5F{DTJ%aqH?$d@ld~I|P!o zi3SOkEqZwUY7;p9Je{YOF(vBTH#=`+0pM%(=3}EL6e%Na(YGOJOb<5th% z=t_%(gVbpf@J^jNpu3}^TmYSvIrlXWEFe{^tA!GZ<<@Gzuj zkG{-@cF$Fa8XQI7DXcnr4JM*J=E=#2c3J6s<;v9@f8oMKs9(RH&Tg@^ZF#%)ZP;!@ zjc7w}HAa+yo`E-B`)dt*D7;9K!gSzz=gE^7ifeh2Av=cRq=Xa6zyAaQ-VN90ccwk% zX}@7ZWGc>3dLv==f9lue@vlEROeBG(p0a7vCfs|6qZ{kSjlFqab;?frrczz4smdE`c^-~KQ zrJp6F7_-)QS2*XBT)M&LDs-$nyxDk)}Y0TRX)y;wh3$Wj~SwGKVb-YB0 z;!09f@$0Y=iUgraqedJ~>iM!ID|l!?_fd!%14C2+9d*#qV93xxn%?s$x?)O-6ah1o zj=aNk>fC`-$EDfM>KdGaB=xUjIeWGgBtHA>_NRdijt5!`NmUZjZVdEe9Kl3W1c2!J0 zKG^}tf8&XE4@I73DBVT>OX!VOt&-ErPHD-(WWj<3b$A=PXy3jqTIHXhHZl&VhaNI` z7|<}n6p=PUzTC+Yt`L=jhAdPkYN-6q`9lEut|zPbtSAXlQ4rzh3lZmiA@1Gr ze~ZGO^XKu|+SHegi(}(@*2)@8ge$08P`8KeoYWNQ^@q zX*#WImmWBLap5v54Hg|MRzTl#ICk#S`y;lT=SE+EGiFRvZejFTzI<8u;m7aUyKAS8 z-FQ}@d-pETrAsHy)3ew6FmO|;<&7TPo$9*7Ey(Zu>Xe>BnSF4$Ep|9cfUq3 zrWC=VWw~VWcXwBh$2B+5Iw7MY8AjdQGHZpJcS&#Swefp-JIPNjAyln@_>D*t1Si@A4$)2a#iWHhv<{Sy#rXwd>bzf3UzeM%#iSyoRbty|uQ$QyQA`)25s`b8252JSPX0 z$t7d1tBD_#T?9V4$fk~bXzI1im_8vo0usUlz%~0bh>1mO@ar&$y?KX6O{$NbnkIVC{Yb8J%HILEZZ&MGsG2<&oGx_EV3YwnlI^HPu9y?a;wkxFc; zcTeS2k>aH@1Z=i3rkztJP?f9!_65;0u5tjoTz24A7#H!l#DTq29Jm#U13NMxQ(WE> zoHo7o^7`+H2)sTba{XT-e~_$YxW)?XT(%)VYz#ylS_1KRab3H6G^+cUnlykI8b&yR zeSxyzRKFK+T4RhX45#^On%YvV*p!`mSMqUhY+l;JTS#(2Al)EfTmSU9MAW%b3vj@m zJksZHh=05f>~g$jp-*`32KG4`flJdtpvqSY!>A>&j&|2 zmV-Wrp=Yn|P^wf3e|Ylb87##Wb4!-sC$$4U0xz#pXh<8w0#CPFy=om4!bLUOAM+&G z$cRb&g#>}Lf`AHF=YWmHp6gD+?eg9bi1sLrx^@gW6={a^J?iN@Yr!Ga{lNZ%DBK@H znX+EYu6S`z7%=cdTt2&k7q*0lhvT>8v+#fB0>A-MY1LDQkO^MWD6# z%a^Z$pup$6exH`UDqq1-#vQ0(P=n**;*9J48{+~M-Nii$bJ`VWJATId;<EJh$@fBn;=kUJG=4o;qJY$otj!zVaaCn{CCBp;Q3v`m%MdFsvM;}bNWypA0^ zl9PPY?*nMr3L762M2llcV<=px5Pkzb4wvXQgB8nH>um0E5kJ!T_>)1^Ly`p9I< z*6o^Nw8Qn_+V#qc%X5ix;3`6G^xA-=u+uRLzf7a$vqV8b&ow-I_3pt2Li&vK z#`OSPe;e-)rS%#h`go;Osgn3Ps(Pt#-@d(YP5VRUb@HSyHWzI z1n8QU6D!(7Y1|0JN#%%}cBsD>*s^5@4rQKd%0=mg1OZDBNY*^P^4+^4be7!ER9bVt z)6a{77>PT)L`UPW0gZO_iD!X)Xg|R3{Cy4_f5L5>8Yd~-rpj^T$T8TxYcJnJS;k63 z3Z=eza6P+1^o{t13+h#fN*Eofr!PMX2e!cI~*A zFJEze^q1c%_mHC?U`qs&wK#`9_Mpuq>1WPPH%S$kcqsgccTz;EcbhgXao3kMJQWul z9E|(N2cU}lFRQ^SRbmB(h66VO0yN`%f4WC(Oe}tr9;W$`_JG|e-#%CFoUG38-LpSY zaW-$-%BnwY|3p)PUS-f&iF5+D-6B<=s9bs6z6%X`v@aa>;xv0O``5YZI8@cQZ{LlZ zi@xS>;y0|{48@BV(+nRtk^B<`QV0PR*jc{zo_bji4-ejDm9{^!R`5w}rXHSnf10~` zjmFBP8EnU?I}#`obef0n(X*RDs9LscjvHQXgIhQ6z(@W1a2omw?whZtXwv7yZL-FW z8G)NdRKwkFf9G)eeqNubtdqX@hJ$O>t^u?@favJ43OsrByeiTRLCA|U}kLNY_U z(Z9c33j&rR@b=SMOLL~e^yUaW6c)B{E=C@f;-;;0000 +category: GitlabInternalEvents +action: <%= event %> +label_description: +property_description: +value_description: +extra_properties: +identifiers:<%= event_identifiers %> +product_section: <%= options.fetch(:section) %> +product_stage: <%= options.fetch(:stage) %> +product_group: <%= options.fetch(:group) %> +milestone: "<%= milestone %>" +introduced_by_url: <%= options.fetch(:mr) %> +distributions:<%= distributions %> +tiers:<%= tiers %> diff --git a/generator_templates/gitlab_internal_events/metric_definition.yml b/generator_templates/gitlab_internal_events/metric_definition.yml index 88823355acc..c0ff71a09ce 100644 --- a/generator_templates/gitlab_internal_events/metric_definition.yml +++ b/generator_templates/gitlab_internal_events/metric_definition.yml @@ -1,6 +1,5 @@ --- key_path: <%= args.second %> -name: <%= args.second %> description: <%= args.last %> product_section: <%= options.fetch(:section) %> product_stage: <%= options.fetch(:stage) %> @@ -14,5 +13,5 @@ time_frame: <%= args.third %> data_source: redis_hll data_category: optional instrumentation_class: <%= class_name %> -<%= distribution %> -<%= tier %> +distribution: <%= distributions %> +tier: <%= tiers %> diff --git a/lib/api/ml/mlflow/entrypoint.rb b/lib/api/ml/mlflow/entrypoint.rb index 880b1efeb5a..048234eccd1 100644 --- a/lib/api/ml/mlflow/entrypoint.rb +++ b/lib/api/ml/mlflow/entrypoint.rb @@ -26,7 +26,7 @@ module API authenticate! - not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project) + not_found! unless can?(current_user, :read_model_experiments, user_project) end rescue_from ActiveRecord::ActiveRecordError do |e| diff --git a/lib/generators/gitlab/analytics/internal_events_generator.rb b/lib/generators/gitlab/analytics/internal_events_generator.rb index 775dac969f2..e945b4de3db 100644 --- a/lib/generators/gitlab/analytics/internal_events_generator.rb +++ b/lib/generators/gitlab/analytics/internal_events_generator.rb @@ -26,15 +26,24 @@ module Gitlab end end.freeze + NEGATIVE_ANSWERS = %w[no n].freeze + POSITIVE_ANSWERS = %w[yes y].freeze TOP_LEVEL_DIR = 'config' TOP_LEVEL_DIR_EE = 'ee' DESCRIPTION_MIN_LENGTH = 50 KNOWN_EVENTS_PATH = 'lib/gitlab/usage_data_counters/known_events/common.yml' KNOWN_EVENTS_PATH_EE = 'ee/lib/ee/gitlab/usage_data_counters/known_events/common.yml' + DESCRIPTION_INQUIRY = %( + Please describe in at least #{DESCRIPTION_MIN_LENGTH} characters + what %{entity} %{entity_type} represents, + consider mentioning: %{considerations}. + Your answer will be processed by a full-text search tool and help others find and reuse this %{entity_type}. + ).freeze + source_root File.expand_path('../../../../generator_templates/gitlab_internal_events', __dir__) - desc 'Generates metric definitions yml files and known events entries' + desc 'Generates metric definitions, event definition yml files and known events entries' class_option :skip_namespace, hide: true @@ -80,12 +89,20 @@ module Gitlab def create_metric_file validate! + template "event_definition.yml", + event_file_path(event), + ask_description(event, "event", "what the event is supposed to track, where, and when") + time_frames.each do |time_frame| template "metric_definition.yml", - file_path(time_frame), + metric_file_path(time_frame), key_path(time_frame), time_frame, - ask_description(time_frame) + ask_description( + key_path(time_frame), + "metric", + "events, and event attributes in the description" + ) end # ToDo: Delete during https://gitlab.com/groups/gitlab-org/-/epics/9542 cleanup @@ -96,45 +113,68 @@ module Gitlab def known_event_entry <<~YML - - name: #{options[:event]} - aggregation: weekly + - name: #{event} + aggregation: weekly YML end - def ask_description(time_frame) + def event_identifiers + return unless include_default_event_properties? + + "\n- project\n- user\n- namespace" + end + + def include_default_event_properties? question = <<~DESC - Please describe in at least #{DESCRIPTION_MIN_LENGTH} characters - what #{key_path(time_frame)} metric represents, - consider mentioning: events, and event attributes in the description. - your answer will be processed to power a full-text search tool and help others find and reuse this metric. + By convention all events automatically include the following properties: + * environment: string, + * source: string (eg: ruby, javascript) + * user_id: number + * project_id: number + * namespace_id: number + * plan: string (eg: free, premium, ultimate) + Would you like to add default properties to the event? Y(es)/N(o) DESC - say("") - description = ask(question) + answer = Gitlab::TaskHelpers.prompt(question, POSITIVE_ANSWERS + NEGATIVE_ANSWERS) + POSITIVE_ANSWERS.include?(answer) + end - while description.length < DESCRIPTION_MIN_LENGTH - error_mgs = <<~ERROR - Provided description is to short: #{description.length} of required #{DESCRIPTION_MIN_LENGTH} characters + def event_file_path(event) + path = File.join(TOP_LEVEL_DIR, 'events', "#{event}.yml") + path = File.join(TOP_LEVEL_DIR_EE, path) unless free? + path + end + + def event + options[:event] + end + + def ask_description(entity, type, considerations) + say("") + desc = ask(format(DESCRIPTION_INQUIRY, entity: entity, entity_type: type, considerations: considerations)) + + while desc.length < DESCRIPTION_MIN_LENGTH + error_msg = <<~ERROR + Provided description is too short: #{desc.length} of required #{DESCRIPTION_MIN_LENGTH} characters ERROR - say(set_color(error_mgs), :red) + say(set_color(error_msg, :red)) - description = ask("Please provide description that is #{DESCRIPTION_MIN_LENGTH} characters long.\n") + desc = ask("Please provide description that is #{DESCRIPTION_MIN_LENGTH} characters long.\n") end - description + desc end - def distribution - content = [ - free? ? "- ce" : nil, - "- ee" - ].compact.join("\n") + def distributions + dist = "\n" + dist += "- ce\n" if free? - "distribution:\n#{content}" + "#{dist}- ee" end - def tier - "tier:\n- #{options[:tiers].join("\n- ")}" + def tiers + "\n- #{options[:tiers].join("\n- ")}" end def milestone @@ -146,10 +186,10 @@ module Gitlab end def key_path(time_frame) - "count_distinct_#{options[:unique_on]}_from_#{options[:event]}_#{time_frame}" + "count_distinct_#{options[:unique_on]}_from_#{event}_#{time_frame}" end - def file_path(time_frame) + def metric_file_path(time_frame) path = File.join(TOP_LEVEL_DIR, 'metrics', TIME_FRAME_DIRS[time_frame], "#{key_path(time_frame)}.yml") path = File.join(TOP_LEVEL_DIR_EE, path) unless free? path @@ -161,6 +201,7 @@ module Gitlab def validate! raise "Required file: #{known_events_file_name} does not exists." unless File.exist?(known_events_file_name) + raise "An event '#{event}' already exists" if event_exists? validate_tiers! @@ -197,6 +238,23 @@ module Gitlab raise "Metric definition with key path '#{key_path(time_frame)}' already exists" end + def event_exists? + return true if ::Gitlab::UsageDataCounters::HLLRedisCounter.known_event?(event) + + existing_events_from_definitions.include?(event) + end + + def existing_events_from_definitions + events_glob_path = File.join(TOP_LEVEL_DIR, 'events', "*.yml") + ee_events_glob_path = File.join(TOP_LEVEL_DIR_EE, events_glob_path) + + [ee_events_glob_path, events_glob_path].flat_map do |glob_path| + Dir.glob(glob_path).map do |path| + YAML.safe_load(File.read(path))["action"] + end + end + end + def free? options[:tiers].include? "free" end diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index b0804c2ff66..e112423f167 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -122,6 +122,7 @@ module Gitlab ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze + ILLEGAL_ORGANIZATION_PATH_WORDS = (TOP_LEVEL_ROUTES | PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze # The namespace regex is used in JavaScript to validate usernames in the "Register" form. However, Javascript # does not support the negative lookbehind assertion (? e + ::Gitlab::ErrorTracking.track_exception(e, client_email: client_email) + nil + end + end +end diff --git a/lib/google_cloud/logging_service/logger.rb b/lib/google_cloud/logging_service/logger.rb new file mode 100644 index 00000000000..2c6dd6ea732 --- /dev/null +++ b/lib/google_cloud/logging_service/logger.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module GoogleCloud + module LoggingService + class Logger + WRITE_URL = "https://logging.googleapis.com/v2/entries:write" + SCOPE = "https://www.googleapis.com/auth/logging.write" + + def initialize + @auth = GoogleCloud::Authentication.new(scope: SCOPE) + end + + def log(client_email, private_key, payload) + access_token = @auth.generate_access_token(client_email, private_key) + + return unless access_token + + headers = build_headers(access_token) + + post(WRITE_URL, body: payload, headers: headers) + end + + private + + def build_headers(access_token) + { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' } + end + + def post(url, body:, headers:) + Gitlab::HTTP.post( + url, + body: body, + headers: headers + ) + rescue URI::InvalidURIError => e + Gitlab::ErrorTracking.log_exception(e) + rescue *Gitlab::HTTP::HTTP_ERRORS + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8a59f043171..a118014a0d4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -29623,9 +29623,18 @@ msgstr "" msgid "NamespaceLimits|%{linkStart}%{username}%{linkEnd} changed the limit to %{limit} at %{date}" msgstr "" +msgid "NamespaceLimits|Confirm deletion" +msgstr "" + msgid "NamespaceLimits|Confirm limits change" msgstr "" +msgid "NamespaceLimits|Deletion confirmation" +msgstr "" + +msgid "NamespaceLimits|Do you confirm the deletion of the selected namespace from the exclusion list?" +msgstr "" + msgid "NamespaceLimits|Enter a valid number greater or equal to zero." msgstr "" @@ -29650,6 +29659,9 @@ msgstr "" msgid "NamespaceLimits|Storage Phased Notification" msgstr "" +msgid "NamespaceLimits|There was an error deleting the namespace: \"%{errorMessage}\"." +msgstr "" + msgid "NamespaceLimits|There was an error fetching the exclusion list, try refreshing the page." msgstr "" diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb index d04cd20f4e6..35bfb829bf7 100644 --- a/spec/controllers/admin/clusters_controller_spec.rb +++ b/spec/controllers/admin/clusters_controller_spec.rb @@ -102,39 +102,6 @@ RSpec.describe Admin::ClustersController, feature_category: :deployment_manageme end end - it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do - let(:cluster) { create(:cluster, :instance, :provided_by_gcp) } - - let(:metrics_dashboard_req_params) do - { - id: cluster.id - } - end - end - - describe 'GET #prometheus_proxy' do - let(:user) { admin } - let(:proxyable) do - create(:cluster, :instance, :provided_by_gcp) - end - - it_behaves_like 'metrics dashboard prometheus api proxy' do - context 'with anonymous user' do - let(:prometheus_body) { nil } - - before do - sign_out(admin) - end - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - describe 'POST #create_user' do let(:params) do { @@ -283,41 +250,6 @@ RSpec.describe Admin::ClustersController, feature_category: :deployment_manageme let(:subject) { get_show } end - describe 'functionality' do - context 'when remove_monitor_metrics FF is disabled' do - before do - stub_feature_flags(remove_monitor_metrics: false) - end - - render_views - - it 'responds successfully' do - get_show - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:cluster)).to eq(cluster) - end - - it 'renders integration tab view' do - get_show(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_integrations') - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when remove_monitor_metrics FF is enabled' do - render_views - - it 'renders details tab view' do - get_show(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_details') - expect(response).to have_gitlab_http_status(:ok) - end - end - end - describe 'security' do it { expect { get_show }.to be_allowed_for(:admin) } it { expect { get_show }.to be_denied_for(:user) } diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index f36494c3d78..6c747d4f00f 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -115,46 +115,6 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem end end - it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do - let(:cluster) { create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) } - - let(:metrics_dashboard_req_params) do - { - id: cluster.id, - group_id: group.name - } - end - end - - describe 'GET #prometheus_proxy' do - let(:proxyable) do - create(:cluster, :provided_by_gcp, cluster_type: :group_type, groups: [group]) - end - - it_behaves_like 'metrics dashboard prometheus api proxy' do - let(:proxyable_params) do - { - id: proxyable.id.to_s, - group_id: group.name - } - end - - context 'with anonymous user' do - let(:prometheus_body) { nil } - - before do - sign_out(user) - end - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - describe 'POST create for existing cluster' do let(:params) do { @@ -353,41 +313,6 @@ RSpec.describe Groups::ClustersController, feature_category: :deployment_managem let(:subject) { go } end - describe 'functionality' do - context 'when remove_monitor_metrics FF is disabled' do - before do - stub_feature_flags(remove_monitor_metrics: false) - end - - render_views - - it 'renders view' do - go - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:cluster)).to eq(cluster) - end - - it 'renders integration tab view', :aggregate_failures do - go(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_integrations') - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when remove_monitor_metrics FF is enabled' do - render_views - - it 'renders details tab view', :aggregate_failures do - go(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_details') - expect(response).to have_gitlab_http_status(:ok) - end - end - end - describe 'security' do it('is allowed for admin when admin mode is enabled', :enable_admin_mode) { expect { go }.to be_allowed_for(:admin) } it('is denied for admin when admin mode is disabled') { expect { go }.to be_denied_for(:admin) } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index f976b5bfe67..bface886674 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -113,57 +113,6 @@ RSpec.describe Projects::ClustersController, feature_category: :deployment_manag end end - describe 'GET #prometheus_proxy' do - let(:proxyable) do - create(:cluster, :provided_by_gcp, projects: [project]) - end - - it_behaves_like 'metrics dashboard prometheus api proxy' do - let(:proxyable_params) do - { - id: proxyable.id.to_s, - namespace_id: project.namespace.full_path, - project_id: project.path - } - end - - context 'with anonymous user' do - let(:prometheus_body) { nil } - - before do - sign_out(user) - end - - it 'redirects to signin page' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to redirect_to(new_user_session_path) - end - end - - context 'with a public project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED) - end - - context 'with guest user' do - let(:prometheus_body) { nil } - - before do - project.add_guest(user) - end - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - end - it_behaves_like 'GET #metrics_dashboard for dashboard', 'Cluster health' do let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } @@ -396,41 +345,6 @@ RSpec.describe Projects::ClustersController, feature_category: :deployment_manag let(:subject) { go } end - describe 'functionality' do - context 'when remove_monitor_metrics FF is disabled' do - before do - stub_feature_flags(remove_monitor_metrics: false) - end - - render_views - - it "renders view" do - go - - expect(response).to have_gitlab_http_status(:ok) - expect(assigns(:cluster)).to eq(cluster) - end - - it 'renders integration tab view' do - go(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_integrations') - expect(response).to have_gitlab_http_status(:ok) - end - end - - context 'when remove_monitor_metrics FF is enabled' do - render_views - - it 'renders details tab view', :aggregate_failures do - go(tab: 'integrations') - - expect(response).to render_template('clusters/clusters/_details') - expect(response).to have_gitlab_http_status(:ok) - end - end - end - describe 'security' do it 'is allowed for admin when admin mode enabled', :enable_admin_mode do expect { go }.to be_allowed_for(:admin) diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb deleted file mode 100644 index ef2d743c82f..00000000000 --- a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Projects::Environments::PrometheusApiController do - let_it_be(:user) { create(:user) } - let_it_be_with_reload(:project) { create(:project) } - let_it_be(:proxyable) { create(:environment, project: project) } - - before do - project.add_reporter(user) - sign_in(user) - end - - describe 'GET #prometheus_proxy' do - it_behaves_like 'metrics dashboard prometheus api proxy' do - let(:proxyable_params) do - { - id: proxyable.id.to_s, - namespace_id: project.namespace.full_path, - project_id: project.path - } - end - - context 'with variables' do - let(:prometheus_body) { '{"status":"success"}' } - let(:pod_name) { "pod1" } - - before do - expected_params[:query] = %{up{pod_name="#{pod_name}"}} - expected_params[:variables] = { 'pod_name' => pod_name } - end - - it 'replaces variables with values' do - get :prometheus_proxy, params: prometheus_proxy_params.merge( - query: 'up{pod_name="{{pod_name}}"}', variables: { 'pod_name' => pod_name } - ) - - expect(response).to have_gitlab_http_status(:success) - expect(Prometheus::ProxyService).to have_received(:new) - .with(proxyable, 'GET', 'query', expected_params) - end - - context 'with invalid variables' do - let(:params_with_invalid_variables) do - prometheus_proxy_params.merge( - query: 'up{pod_name="{{pod_name}}"}', variables: ['a'] - ) - end - - it 'returns 400' do - get :prometheus_proxy, params: params_with_invalid_variables - - expect(response).to have_gitlab_http_status(:bad_request) - expect(Prometheus::ProxyService).not_to receive(:new) - end - end - end - - context 'with anonymous user' do - let(:prometheus_body) { nil } - - before do - sign_out(user) - end - - it 'redirects to signin page' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to redirect_to(new_user_session_path) - end - end - - context 'with a public project' do - before do - project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - project.project_feature.update!(metrics_dashboard_access_level: ProjectFeature::ENABLED) - end - - context 'with guest user' do - let(:prometheus_body) { nil } - - before do - project.add_guest(user) - end - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - end - end -end diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb index 44292b9ce19..3e64631fbf1 100644 --- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb +++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb @@ -6,7 +6,6 @@ RSpec.describe Projects::Prometheus::AlertsController, feature_category: :incide let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:environment) { create(:environment, project: project) } - let_it_be(:metric) { create(:prometheus_metric, project: project) } before do project.add_maintainer(user) @@ -43,16 +42,6 @@ RSpec.describe Projects::Prometheus::AlertsController, feature_category: :incide end end - shared_examples 'project non-specific metric' do |status| - let(:other) { create(:prometheus_alert) } - - it "returns #{status}" do - make_request(id: other.prometheus_metric_id) - - expect(response).to have_gitlab_http_status(status) - end - end - describe 'POST #notify' do let(:alert_1) { build(:alert_management_alert, :prometheus, project: project) } let(:alert_2) { build(:alert_management_alert, :prometheus, project: project) } @@ -115,67 +104,7 @@ RSpec.describe Projects::Prometheus::AlertsController, feature_category: :incide end end - describe 'GET #metrics_dashboard', feature_category: :metrics do - let!(:alert) do - create(:prometheus_alert, project: project, environment: environment, prometheus_metric: metric) - end - - before do - stub_feature_flags(remove_monitor_metrics: false) - end - - it 'returns a json object with the correct keys' do - get :metrics_dashboard, params: request_params(id: metric.id, environment_id: alert.environment.id), format: :json - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.keys).to contain_exactly('dashboard', 'status', 'metrics_data') - end - - it 'is the correct embed' do - get :metrics_dashboard, params: request_params(id: metric.id, environment_id: alert.environment.id), format: :json - - title = json_response['dashboard']['panel_groups'][0]['panels'][0]['title'] - - expect(title).to eq(metric.title) - end - - it 'finds the first alert embed without environment_id' do - get :metrics_dashboard, params: request_params(id: metric.id), format: :json - - title = json_response['dashboard']['panel_groups'][0]['panels'][0]['title'] - - expect(title).to eq(metric.title) - end - - it 'returns 404 for non-existant alerts' do - get :metrics_dashboard, params: request_params(id: 0), format: :json - - expect(response).to have_gitlab_http_status(:not_found) - end - - it 'returns 404 when metrics dashboard feature is unavailable' do - stub_feature_flags(remove_monitor_metrics: true) - - get :metrics_dashboard, params: request_params(id: 0), format: :json - - expect(response).to have_gitlab_http_status(:not_found) - end - end - def project_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project) end - - def request_params(opts = {}, defaults = {}) - project_params(opts.reverse_merge(defaults)) - end - - def alert_path(alert) - project_prometheus_alert_path( - project, - alert.prometheus_metric_id, - environment_id: alert.environment, - format: :json - ) - end end diff --git a/spec/factories/merge_request_diffs.rb b/spec/factories/merge_request_diffs.rb index f93f3f22109..d81f355737e 100644 --- a/spec/factories/merge_request_diffs.rb +++ b/spec/factories/merge_request_diffs.rb @@ -2,7 +2,7 @@ FactoryBot.define do factory :merge_request_diff do - association :merge_request, factory: :merge_request_without_merge_request_diff + association :merge_request, :skip_diff_creation state { :collected } commits_count { 1 } diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 4941a31982f..390db24dde8 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -313,6 +313,12 @@ FactoryBot.define do sequence(:source_branch) { |n| "feature#{n}" } end + trait :skip_diff_creation do + before(:create) do |merge_request, _| + merge_request.skip_ensure_merge_request_diff = true + end + end + after(:build) do |merge_request| target_project = merge_request.target_project source_project = merge_request.source_project @@ -357,7 +363,5 @@ FactoryBot.define do merge_request.update!(labels: evaluator.labels) end end - - factory :merge_request_without_merge_request_diff, class: 'MergeRequestWithoutMergeRequestDiff' end end diff --git a/spec/features/clusters/cluster_health_dashboard_spec.rb b/spec/features/clusters/cluster_health_dashboard_spec.rb deleted file mode 100644 index e932f8c6b98..00000000000 --- a/spec/features/clusters/cluster_health_dashboard_spec.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Cluster Health board', :js, :kubeclient, :use_clean_rails_memory_store_caching, :sidekiq_inline, -feature_category: :deployment_management do - include KubernetesHelpers - include PrometheusHelpers - - let_it_be(:current_user) { create(:user) } - let_it_be(:clusterable) { create(:project) } - let_it_be(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } - let_it_be(:cluster_path) { project_cluster_path(clusterable, cluster) } - - before do - stub_feature_flags(remove_monitor_metrics: false) - - clusterable.add_maintainer(current_user) - - sign_in(current_user) - end - - it 'shows cluster board section within the page' do - visit cluster_path - - expect(page).to have_text('Health') - - click_link 'Health' - - expect(page).to have_css('.cluster-health-graphs') - end - - context 'feature remove_monitor_metrics enabled' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - - it 'does not show the cluster health tab' do - visit cluster_path - - expect(page).not_to have_text('Health') - end - - it 'does not show the cluster health section' do - visit project_cluster_path(clusterable, cluster, { tab: 'health' }) - - expect(page).not_to have_text('you must first enable Prometheus in the Integrations tab') - end - end - - context 'no prometheus available' do - it 'shows enable Prometheus message' do - visit cluster_path - - click_link 'Health' - - expect(page).to have_text('you must first enable Prometheus in the Integrations tab') - end - end - - context 'when there is cluster with enabled prometheus' do - before do - create(:clusters_integrations_prometheus, enabled: true, cluster: cluster) - stub_kubeclient_discover(cluster.platform.api_url) - end - - context 'waiting for data' do - before do - stub_empty_response - end - - it 'shows container and waiting for data message' do - visit cluster_path - - click_link 'Health' - - wait_for_requests - - expect(page).to have_css('.prometheus-graphs') - expect(page).to have_text('Waiting for performance data') - end - end - - context 'connected, prometheus returns data' do - before do - stub_connected - - visit cluster_path - - click_link 'Health' - - wait_for_requests - end - - it 'renders charts' do - expect(page).to have_css('.prometheus-graphs') - expect(page).to have_css('.prometheus-graph') - expect(page).to have_css('.prometheus-graph-title') - expect(page).to have_css('[_echarts_instance_]') - expect(page).to have_css('.prometheus-graph', count: 2) - expect(page).to have_content('Avg') - end - - it 'focuses the single panel on toggle' do - click_button('More actions', match: :first) - click_button('Expand panel') - - expect(page).to have_css('.prometheus-graph', count: 1) - - click_button('Collapse panel') - - expect(page).to have_css('.prometheus-graph', count: 2) - end - end - - def stub_empty_response - stub_prometheus_request(/prometheus-prometheus-server/, status: 204, body: {}) - stub_prometheus_request(%r{prometheus/api/v1}, status: 204, body: {}) - end - - def stub_connected - stub_prometheus_request(/prometheus-prometheus-server/, body: prometheus_values_body) - stub_prometheus_request(%r{prometheus/api/v1}, body: prometheus_values_body) - end - end -end diff --git a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb index 38992a29dcb..0afd3201853 100644 --- a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb +++ b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb @@ -6,8 +6,8 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat include UsageDataHelpers let(:temp_dir) { Dir.mktmpdir } - let(:tmpfile) { Tempfile.new('test-metadata') } let(:ee_temp_dir) { Dir.mktmpdir } + let(:tmpfile) { Tempfile.new('test-metadata') } let(:existing_key_paths) { {} } let(:description) { "This metric counts unique users viewing analytics metrics dashboard section" } let(:group) { "group::analytics instrumentation" } @@ -16,6 +16,8 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat let(:mr) { "https://gitlab.com/some-group/some-project/-/merge_requests/123" } let(:event) { "view_analytics_dashboard" } let(:unique_on) { "user_id" } + let(:time_frames) { %w[7d] } + let(:include_default_identifiers) { 'yes' } let(:options) do { time_frames: time_frames, @@ -34,7 +36,6 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat let(:metric_definition_7d) do { "key_path" => key_path_7d, - "name" => key_path_7d, "description" => description, "product_section" => section, "product_stage" => stage, @@ -54,10 +55,12 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat end before do - stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir) stub_const("#{described_class}::TOP_LEVEL_DIR_EE", ee_temp_dir) + stub_const("#{described_class}::TOP_LEVEL_DIR", temp_dir) stub_const("#{described_class}::KNOWN_EVENTS_PATH", tmpfile.path) stub_const("#{described_class}::KNOWN_EVENTS_PATH_EE", tmpfile.path) + # Stub version so that `milestone` key remains constant between releases to prevent flakiness. + stub_const('Gitlab::VERSION', '13.9.0') allow_next_instance_of(described_class) do |instance| allow(instance).to receive(:ask) @@ -65,8 +68,8 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat .and_return(description) end - allow(Gitlab::Usage::MetricDefinition) - .to receive(:definitions).and_return(existing_key_paths) + allow(Gitlab::TaskHelpers).to receive(:prompt).and_return(include_default_identifiers) + allow(Gitlab::Usage::MetricDefinition).to receive(:definitions).and_return(existing_key_paths) end after do @@ -75,12 +78,85 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat FileUtils.rm_rf(tmpfile.path) end - describe 'Creating metric definition file' do - before do - # Stub version so that `milestone` key remains constant between releases to prevent flakiness. - stub_const('Gitlab::VERSION', '13.9.0') + describe 'Creating event definition file' do + let(:event_definition_path) { Dir.glob(File.join(temp_dir, "events/#{event}.yml")).first } + let(:identifiers) { %w[project user namespace] } + let(:event_definition) do + { + "category" => "GitlabInternalEvents", + "action" => event, + "description" => description, + "product_section" => section, + "product_stage" => stage, + "product_group" => group, + "label_description" => nil, + "property_description" => nil, + "value_description" => nil, + "extra_properties" => nil, + "identifiers" => identifiers, + "milestone" => "13.9", + "introduced_by_url" => mr, + "distributions" => %w[ce ee], + "tiers" => %w[free premium ultimate] + } end + it 'creates an event definition file using the template' do + described_class.new([], options).invoke_all + + expect(YAML.safe_load(File.read(event_definition_path))).to eq(event_definition) + end + + context 'for ultimate only feature' do + let(:event_definition_path) do + Dir.glob(File.join(ee_temp_dir, temp_dir, "events/#{event}.yml")).first + end + + it 'creates an event definition file using the template' do + described_class.new([], options.merge(tiers: %w[ultimate])).invoke_all + + expect(YAML.safe_load(File.read(event_definition_path))) + .to eq(event_definition.merge("tiers" => ["ultimate"], "distributions" => ["ee"])) + end + end + + context 'without default identifiers' do + let(:include_default_identifiers) { 'no' } + + it 'creates an event definition file using the template' do + described_class.new([], options).invoke_all + + expect(YAML.safe_load(File.read(event_definition_path))) + .to eq(event_definition.merge("identifiers" => nil)) + end + end + + context 'with duplicated event' do + context 'in known_events files' do + before do + allow(::Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:known_event?).with(event).and_return(true) + end + + it 'raises error' do + expect { described_class.new([], options).invoke_all }.to raise_error(RuntimeError) + end + end + + context 'in event definition files' do + before do + Dir.mkdir(File.join(temp_dir, "events")) + File.write(File.join(temp_dir, "events", "#{event}.yml"), { action: event }.to_yaml) + end + + it 'raises error' do + expect { described_class.new([], options).invoke_all }.to raise_error(RuntimeError) + end + end + end + end + + describe 'Creating metric definition file' do context 'for single time frame' do let(:time_frames) { %w[7d] } @@ -146,10 +222,14 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat it 'asks again for description' do allow_next_instance_of(described_class) do |instance| allow(instance).to receive(:ask) + .with(/By convention all events automatically include the following properties/) + .and_return(include_default_identifiers) + + allow(instance).to receive(:ask).twice .with(/Please describe in at least 50 characters/) .and_return("I am to short") - expect(instance).to receive(:ask) + expect(instance).to receive(:ask).twice .with(/Please provide description that is 50 characters long/) .and_return(description) end @@ -166,7 +246,6 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat let(:metric_definition_28d) do metric_definition_7d.merge( "key_path" => key_path_28d, - "name" => key_path_28d, "time_frame" => "28d" ) end @@ -186,7 +265,6 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat let(:metric_definition_28d) do metric_definition_7d.merge( "key_path" => key_path_28d, - "name" => key_path_28d, "time_frame" => "28d" ) end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 718b20c59ed..53dc145dcc4 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -258,6 +258,23 @@ RSpec.describe Gitlab::PathRegex do end end + describe '.organization_path_regex' do + subject { described_class.organization_path_regex } + + it 'rejects reserved words' do + expect(subject).not_to match('admin/') + expect(subject).not_to match('api/') + expect(subject).not_to match('create/') + expect(subject).not_to match('new/') + end + + it 'accepts other words' do + expect(subject).to match('simple/') + expect(subject).to match('org/') + expect(subject).to match('my_org/') + end + end + describe '.full_namespace_path_regex' do subject { described_class.full_namespace_path_regex } diff --git a/spec/lib/google_cloud/authentication_spec.rb b/spec/lib/google_cloud/authentication_spec.rb new file mode 100644 index 00000000000..5c7f3e51152 --- /dev/null +++ b/spec/lib/google_cloud/authentication_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GoogleCloud::Authentication, feature_category: :audit_events do + describe '#generate_access_token' do + let_it_be(:client_email) { 'test@example.com' } + let_it_be(:private_key) { 'private_key' } + let_it_be(:scope) { 'https://www.googleapis.com/auth/logging.write' } + let_it_be(:json_key_io) { StringIO.new({ client_email: client_email, private_key: private_key }.to_json) } + + let(:service_account_credentials) { instance_double('Google::Auth::ServiceAccountCredentials') } + + subject(:generate_access_token) do + described_class.new(scope: scope).generate_access_token(client_email, private_key) + end + + before do + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).with(json_key_io: json_key_io, + scope: scope).and_return(service_account_credentials) + allow(StringIO).to receive(:new).with({ client_email: client_email, + private_key: private_key }.to_json).and_return(json_key_io) + end + + context 'when credentials are valid' do + before do + allow(service_account_credentials).to receive(:fetch_access_token!).and_return({ 'access_token' => 'token' }) + end + + it 'calls make_creds with correct parameters' do + expect(Google::Auth::ServiceAccountCredentials).to receive(:make_creds).with(json_key_io: json_key_io, + scope: scope) + + generate_access_token + end + + it 'fetches access token' do + expect(generate_access_token).to eq('token') + end + end + + context 'when an error occurs' do + before do + allow(service_account_credentials).to receive(:fetch_access_token!).and_raise(StandardError) + end + + it 'handles the exception and returns nil' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + expect(generate_access_token).to be_nil + end + end + end +end diff --git a/spec/lib/google_cloud/logging_service/logger_spec.rb b/spec/lib/google_cloud/logging_service/logger_spec.rb new file mode 100644 index 00000000000..31f8bb27ec5 --- /dev/null +++ b/spec/lib/google_cloud/logging_service/logger_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GoogleCloud::LoggingService::Logger, feature_category: :audit_events do + let_it_be(:client_email) { 'test@example.com' } + let_it_be(:private_key) { 'private_key' } + let_it_be(:payload) { [{ logName: 'test-log' }.to_json] } + let_it_be(:access_token) { 'access_token' } + let_it_be(:expected_headers) do + { 'Authorization' => "Bearer #{access_token}", 'Content-Type' => 'application/json' } + end + + subject(:log) { described_class.new.log(client_email, private_key, payload) } + + describe '#log' do + context 'when access token is available' do + before do + allow_next_instance_of(GoogleCloud::Authentication) do |instance| + allow(instance).to receive(:generate_access_token).with(client_email, private_key).and_return(access_token) + end + end + + it 'generates access token and calls Gitlab::HTTP.post with correct parameters' do + expect(Gitlab::HTTP).to receive(:post).with( + described_class::WRITE_URL, + body: payload, + headers: expected_headers + ) + + log + end + + context 'when URI::InvalidURIError is raised' do + before do + allow(Gitlab::HTTP).to receive(:post).and_raise(URI::InvalidURIError) + end + + it 'logs the exception' do + expect(Gitlab::ErrorTracking).to receive(:log_exception) + + log + end + end + end + + context 'when access token is not available' do + let(:access_token) { nil } + + it 'does not call Gitlab::HTTP.post' do + allow_next_instance_of(GoogleCloud::Authentication) do |instance| + allow(instance).to receive(:generate_access_token).with(client_email, private_key).and_return(access_token) + end + + expect(Gitlab::HTTP).not_to receive(:post) + + log + end + end + end +end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index e2c87b0d85c..57a9963c0f1 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -1178,14 +1178,14 @@ RSpec.describe MergeRequestDiff, feature_category: :code_review_workflow do end describe '.latest_diff_for_merge_requests' do - let_it_be(:merge_request_1) { create(:merge_request_without_merge_request_diff) } + let_it_be(:merge_request_1) { create(:merge_request, :skip_diff_creation) } let_it_be(:merge_request_1_diff_1) { create(:merge_request_diff, merge_request: merge_request_1, created_at: 3.days.ago) } let_it_be(:merge_request_1_diff_2) { create(:merge_request_diff, merge_request: merge_request_1, created_at: 1.day.ago) } - let_it_be(:merge_request_2) { create(:merge_request_without_merge_request_diff) } + let_it_be(:merge_request_2) { create(:merge_request, :skip_diff_creation) } let_it_be(:merge_request_2_diff_1) { create(:merge_request_diff, merge_request: merge_request_2, created_at: 3.days.ago) } - let_it_be(:merge_request_3) { create(:merge_request_without_merge_request_diff) } + let_it_be(:merge_request_3) { create(:merge_request, :skip_diff_creation) } subject { described_class.latest_diff_for_merge_requests([merge_request_1, merge_request_2]) } diff --git a/spec/models/organizations/organization_spec.rb b/spec/models/organizations/organization_spec.rb index bb3d0c2307d..3b202b85b48 100644 --- a/spec/models/organizations/organization_spec.rb +++ b/spec/models/organizations/organization_spec.rb @@ -18,6 +18,37 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } it { is_expected.to validate_length_of(:path).is_at_least(2).is_at_most(255) } + + describe 'path validator' do + using RSpec::Parameterized::TableSyntax + + let(:default_path_error) do + "can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'." + end + + let(:reserved_path_error) do + "is a reserved name" + end + + where(:path, :valid, :error_message) do + 'path.' | false | ref(:default_path_error) + 'path.git' | false | ref(:default_path_error) + 'new' | false | ref(:reserved_path_error) + '.path' | true | nil + 'org__path' | true | nil + 'some-name' | true | nil + 'simple' | true | nil + end + + with_them do + it 'validates organization path' do + organization = build(:organization, name: 'Default', path: path) + + expect(organization.valid?).to be(valid) + expect(organization.errors.full_messages.to_sentence).to include(error_message) if error_message.present? + end + end + end end context 'when using scopes' do diff --git a/spec/models/preloaders/merge_request_diff_preloader_spec.rb b/spec/models/preloaders/merge_request_diff_preloader_spec.rb index 9a76d42e73f..9ca5039c4e6 100644 --- a/spec/models/preloaders/merge_request_diff_preloader_spec.rb +++ b/spec/models/preloaders/merge_request_diff_preloader_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Preloaders::MergeRequestDiffPreloader do let_it_be(:merge_request_1) { create(:merge_request) } let_it_be(:merge_request_2) { create(:merge_request) } - let_it_be(:merge_request_3) { create(:merge_request_without_merge_request_diff) } + let_it_be(:merge_request_3) { create(:merge_request, :skip_diff_creation) } let(:merge_requests) { [merge_request_1, merge_request_2, merge_request_3] } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d10a8afc51e..0e3382b4c6f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe User, feature_category: :user_profile do + using RSpec::Parameterized::TableSyntax + include ProjectForksHelper include TermsHelper include ExclusiveLeaseHelpers @@ -4168,8 +4170,6 @@ RSpec.describe User, feature_category: :user_profile do end describe '#following_users_allowed?' do - using RSpec::Parameterized::TableSyntax - let_it_be(:user) { create(:user) } let_it_be(:followee) { create(:user) } @@ -6021,27 +6021,42 @@ RSpec.describe User, feature_category: :user_profile do let(:user) { create(:user, note: "existing note") } let(:deleted_by) { create(:user) } - it 'blocks the user then schedules them for deletion if a hard delete is specified' do - expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, { hard_delete: true }) + shared_examples 'schedules user for deletion without delay' do + it 'schedules user for deletion without delay' do + expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, {}) + expect(DeleteUserWorker).not_to receive(:perform_in) + user.delete_async(deleted_by: deleted_by) + end + end + + shared_examples 'it does not block the user' do + it 'does not block the user' do + user.delete_async(deleted_by: deleted_by) + + expect(user).not_to be_blocked + end + end + + it 'blocks the user if hard delete is specified' do user.delete_async(deleted_by: deleted_by, params: { hard_delete: true }) expect(user).to be_blocked end - it 'schedules user for deletion without blocking them' do - expect(DeleteUserWorker).to receive(:perform_async).with(deleted_by.id, user.id, {}) + it_behaves_like 'schedules user for deletion without delay' - user.delete_async(deleted_by: deleted_by) - - expect(user).not_to be_blocked - end + it_behaves_like 'it does not block the user' context 'when target user is the same as deleted_by' do let(:deleted_by) { user } subject { user.delete_async(deleted_by: deleted_by) } + before do + allow(user).to receive(:has_possible_spam_contributions?).and_return(true) + end + shared_examples 'schedules the record for deletion with the correct delay' do it 'schedules the record for deletion with the correct delay' do freeze_time do @@ -6061,12 +6076,64 @@ RSpec.describe User, feature_category: :user_profile do expect(user).not_to be_banned end + context 'with possible spam contribution' do + context 'with comments' do + it_behaves_like 'schedules the record for deletion with the correct delay' do + before do + allow(user).to receive(:has_possible_spam_contributions?).and_call_original + + note = create(:note_on_issue, author: user) + create(:event, :commented, target: note, author: user) + end + end + end + + context 'with other types' do + where(:resource, :action, :delayed) do + 'Issue' | :created | true + 'MergeRequest' | :created | true + 'Issue' | :closed | false + 'MergeRequest' | :closed | false + 'WorkItem' | :created | false + end + + with_them do + before do + allow(user).to receive(:has_possible_spam_contributions?).and_call_original + + case resource + when 'Issue' + create(:event, action, :for_issue, author: user) + when 'MergeRequest' + create(:event, action, :for_merge_request, author: user) + when 'WorkItem' + create(:event, action, :for_work_item, author: user) + end + end + + if params[:delayed] + it_behaves_like 'schedules the record for deletion with the correct delay' + else + it_behaves_like 'schedules user for deletion without delay' + end + end + end + end + + context 'when user has no possible spam contributions' do + before do + allow(user).to receive(:has_possible_spam_contributions?).and_return(false) + end + + it_behaves_like 'schedules user for deletion without delay' + end + context 'when the user is a spammer' do before do allow(user).to receive(:spammer?).and_return(true) end - context 'when the user acount is less than 7 days old' do + context 'when the user account is less than 7 days old' do it_behaves_like 'schedules the record for deletion with the correct delay' it 'creates an abuse report with the correct data' do @@ -6140,13 +6207,9 @@ RSpec.describe User, feature_category: :user_profile do stub_feature_flags(delay_delete_own_user: false) end - it 'schedules user for deletion without blocking them' do - expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id, {}) + it_behaves_like 'schedules user for deletion without delay' - subject - - expect(user).not_to be_blocked - end + it_behaves_like 'it does not block the user' it 'does not update the note' do expect { user.delete_async(deleted_by: deleted_by) }.not_to change { user.note } @@ -7303,8 +7366,6 @@ RSpec.describe User, feature_category: :user_profile do let(:user_id) { user.id } describe 'update user' do - using RSpec::Parameterized::TableSyntax - where(:attributes) do [ { state: 'blocked' }, diff --git a/spec/requests/api/ml/mlflow/experiments_spec.rb b/spec/requests/api/ml/mlflow/experiments_spec.rb index 1a2577e69e7..fc2e814752c 100644 --- a/spec/requests/api/ml/mlflow/experiments_spec.rb +++ b/spec/requests/api/ml/mlflow/experiments_spec.rb @@ -20,7 +20,6 @@ RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do end let(:current_user) { developer } - let(:ff_value) { true } let(:access_token) { tokens[:write] } let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } let(:project_id) { project.id } @@ -52,10 +51,6 @@ RSpec.describe API::Ml::Mlflow::Experiments, feature_category: :mlops do response end - before do - stub_feature_flags(ml_experiment_tracking: ff_value) - end - describe 'GET /projects/:id/ml/mlflow/api/2.0/mlflow/experiments/get' do let(:experiment_iid) { experiment.iid.to_s } let(:route) { "/projects/#{project_id}/ml/mlflow/api/2.0/mlflow/experiments/get?experiment_id=#{experiment_iid}" } diff --git a/spec/requests/api/ml/mlflow/runs_spec.rb b/spec/requests/api/ml/mlflow/runs_spec.rb index 746372b7978..a85fe4d867a 100644 --- a/spec/requests/api/ml/mlflow/runs_spec.rb +++ b/spec/requests/api/ml/mlflow/runs_spec.rb @@ -26,7 +26,6 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do end let(:current_user) { developer } - let(:ff_value) { true } let(:access_token) { tokens[:write] } let(:headers) { { 'Authorization' => "Bearer #{access_token.token}" } } let(:project_id) { project.id } @@ -40,10 +39,6 @@ RSpec.describe API::Ml::Mlflow::Runs, feature_category: :mlops do response end - before do - stub_feature_flags(ml_experiment_tracking: ff_value) - end - RSpec.shared_examples 'MLflow|run_id param error cases' do context 'when run id is not passed' do let(:params) { {} } diff --git a/spec/support/helpers/models/merge_request_without_merge_request_diff.rb b/spec/support/helpers/models/merge_request_without_merge_request_diff.rb deleted file mode 100644 index e9f97a2c95a..00000000000 --- a/spec/support/helpers/models/merge_request_without_merge_request_diff.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class MergeRequestWithoutMergeRequestDiff < ::MergeRequest # rubocop:disable Gitlab/NamespacedClass - self.inheritance_column = :_type_disabled - - def ensure_merge_request_diff; end -end diff --git a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb b/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb deleted file mode 100644 index 9cdde13b36b..00000000000 --- a/spec/support/shared_examples/controllers/metrics/dashboard/prometheus_api_proxy_shared_examples.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples_for 'metrics dashboard prometheus api proxy' do - let(:service_params) { [proxyable, 'GET', 'query', expected_params] } - let(:service_result) { { status: :success, body: prometheus_body } } - let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) } - let(:proxyable_params) do - { - id: proxyable.id.to_s - } - end - - let(:expected_params) do - ActionController::Parameters.new( - prometheus_proxy_params( - proxy_path: 'query', - controller: described_class.controller_path, - action: 'prometheus_proxy' - ) - ).permit! - end - - before do - stub_feature_flags(remove_monitor_metrics: false) - - allow_next_instance_of(Prometheus::ProxyService, *service_params) do |proxy_service| - allow(proxy_service).to receive(:execute).and_return(service_result) - end - end - - context 'with valid requests' do - context 'with success result' do - let(:prometheus_body) { '{"status":"success"}' } - let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) } - - it 'returns prometheus response' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) - expect(response).to have_gitlab_http_status(:ok) - expect(json_response).to eq(prometheus_json_body) - end - - context 'with nil query' do - let(:params_without_query) do - prometheus_proxy_params.except(:query) - end - - before do - expected_params.delete(:query) - end - - it 'does not raise error' do - get :prometheus_proxy, params: params_without_query - - expect(Prometheus::ProxyService).to have_received(:new).with(*service_params) - end - end - end - - context 'with nil result' do - let(:service_result) { nil } - - it 'returns 204 no_content' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(json_response['status']).to eq(_('processing')) - expect(json_response['message']).to eq(_('Not ready yet. Try again later.')) - expect(response).to have_gitlab_http_status(:no_content) - end - end - - context 'with 404 result' do - let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } } - - it 'returns body' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - expect(json_response['body']).to eq('value') - end - end - - context 'with error result' do - context 'with http_status' do - let(:service_result) do - { http_status: :service_unavailable, status: :error, message: 'error message' } - end - - it 'sets the http response status code' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:service_unavailable) - expect(json_response['status']).to eq('error') - expect(json_response['message']).to eq('error message') - end - end - - context 'without http_status' do - let(:service_result) { { status: :error, message: 'error message' } } - - it 'returns bad_request' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['status']).to eq('error') - expect(json_response['message']).to eq('error message') - end - end - end - - context 'when metrics dashboard feature is unavailable' do - before do - stub_feature_flags(remove_monitor_metrics: true) - end - - it 'returns 404 not found' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - expect(response.body).to be_empty - end - end - end - - context 'with inappropriate requests' do - let(:prometheus_body) { nil } - - context 'without correct permissions' do - let(:user2) { create(:user) } - - before do - sign_out(user) - sign_in(user2) - end - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'with invalid proxyable id' do - let(:prometheus_body) { nil } - - it 'returns 404' do - get :prometheus_proxy, params: prometheus_proxy_params(id: proxyable.id + 1) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - private - - def prometheus_proxy_params(params = {}) - { - proxy_path: 'query', - query: '1' - }.merge(proxyable_params).merge(params) - end -end diff --git a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb index 2ca62698daf..f2c38d70508 100644 --- a/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/ml/mlflow/mlflow_shared_examples.rb @@ -47,8 +47,13 @@ RSpec.shared_examples 'MLflow|shared error cases' do end end - context 'when ff is disabled' do - let(:ff_value) { false } + context 'when model experiments is unavailable' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(current_user, :read_model_experiments, project) + .and_return(false) + end it "is Not Found" do is_expected.to have_gitlab_http_status(:not_found) diff --git a/spec/validators/organizations/path_validator_spec.rb b/spec/validators/organizations/path_validator_spec.rb new file mode 100644 index 00000000000..415c10d98df --- /dev/null +++ b/spec/validators/organizations/path_validator_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::PathValidator, feature_category: :cell do + let(:validator) { described_class.new(attributes: [:path]) } + + describe '.valid_path?' do + it 'handles invalid utf8' do + expect(described_class.valid_path?(+"a\0weird\255path")).to be_falsey + end + end + + describe '#validates_each' do + it 'adds a message when the path is not in the correct format' do + organization = build(:organization) + + validator.validate_each(organization, :path, "Path with spaces, and comma's!") + + expect(organization.errors[:path]).to include(Gitlab::PathRegex.namespace_format_message) + end + + it 'adds a message when the path is reserved when creating' do + organization = build(:organization, path: 'help') + + validator.validate_each(organization, :path, 'help') + + expect(organization.errors[:path]).to include('help is a reserved name') + end + + it 'adds a message when the path is reserved when updating' do + organization = create(:organization) + organization.path = 'help' + + validator.validate_each(organization, :path, 'help') + + expect(organization.errors[:path]).to include('help is a reserved name') + end + end +end