katello 4.13.0.rc1 → 4.13.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +334 -23
  3. data/app/controllers/katello/api/v2/repositories_controller.rb +1 -1
  4. data/app/lib/actions/katello/capsule_content/sync_capsule.rb +7 -2
  5. data/app/lib/actions/katello/organization/manifest_delete.rb +6 -1
  6. data/app/lib/actions/katello/repository/create.rb +17 -11
  7. data/app/lib/actions/katello/repository/create_root.rb +4 -2
  8. data/app/lib/actions/katello/upstream_subscriptions/bind_entitlement.rb +1 -1
  9. data/app/lib/actions/pulp3/orchestration/orphan_cleanup/remove_orphans.rb +1 -0
  10. data/app/lib/actions/pulp3/orphan_cleanup/purge_completed_tasks.rb +15 -0
  11. data/app/models/katello/content_view.rb +2 -0
  12. data/app/models/katello/glue/pulp/repos.rb +8 -1
  13. data/app/models/katello/repository.rb +5 -1
  14. data/app/models/katello/root_repository.rb +4 -2
  15. data/app/models/katello/trace_status.rb +1 -1
  16. data/app/services/katello/pulp3/api/core.rb +8 -0
  17. data/app/services/katello/pulp3/api/docker.rb +4 -0
  18. data/app/services/katello/pulp3/repository/yum.rb +1 -6
  19. data/app/views/foreman/smart_proxies/_content_tab.html.erb +3 -1
  20. data/db/migrate/20240520142245_add_container_push_props_to_repo.rb +7 -0
  21. data/db/migrate/20240531193030_remove_sha1_repository_checksum_type.rb +10 -0
  22. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/checksum.service.js +6 -1
  23. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/views/new-repository.html +0 -3
  24. data/lib/katello/plugin.rb +12 -0
  25. data/lib/katello/version.rb +1 -1
  26. data/package.json +0 -1
  27. data/webpack/components/Content/ContentTable.js +0 -1
  28. data/webpack/components/Content/__tests__/__snapshots__/ContentTable.test.js.snap +0 -1
  29. data/webpack/scenes/ModuleStreams/ModuleStreamsPage.js +33 -39
  30. data/webpack/scenes/ModuleStreams/__tests__/ModuleStreamPage.test.js +4 -2
  31. data/webpack/scenes/ModuleStreams/__tests__/__snapshots__/ModuleStreamsTable.test.js.snap +0 -1
  32. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +4 -2
  33. metadata +7 -7
  34. data/webpack/utils/__tests__/useParamsWithHash.test.js +0 -22
  35. data/webpack/utils/paramsFromHash.js +0 -16
  36. data/webpack/utils/useUrlParams.js +0 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd34bfe58b23b1d93a3997cebe69df8ee33721ed70f580c6e9702f7fc6ba6bd2
4
- data.tar.gz: 0c11d4179198241b8086a718fd240ae3c91af22a9912637aa88b9e2534fb11ff
3
+ metadata.gz: 24a3d52ddfd156ad5a11c37f63def11242d86b6c67f7e7b5e97cbad375ec0ffa
4
+ data.tar.gz: f307cdc997b68aac82c1f7a9b5d9e380eb9c8a93dcacd7e87a3a05dd6c5589c5
5
5
  SHA512:
6
- metadata.gz: 8007ac10ec5431af852d6ed99534aaa40b986c2f007769a591f32d59a47afdc9f7535e850bfb6c263d9301476776a31f4b6ecc5223e76083a3923e92db66ca6e
7
- data.tar.gz: b2d7cfadb5189daa8be9bba7ddd8b4bd4b17993ed64ff2db28a87738f9bab6a7f0ef8242c2e599293307530625cb56aebc33b62f876386b58e38cbc73c50020a
6
+ metadata.gz: c188709e872b9e152963f860462b267a480533e7634d5804708838df3701cade9ec0dc8d9c0bc72423bccd93383947a7b1d5a4a624a03245a4b8784c9c73a36e
7
+ data.tar.gz: 3caf9ffe2eda0fb1f1f3593f1e2badcc92946a483c16f448421193faf067960b73380c81bc2349a6c9bef4b64550ee6410afbf926157b0d132533897614dfc6d
@@ -3,15 +3,14 @@ module Katello
3
3
  class Api::Registry::RegistryProxiesController < Api::V2::ApiController
4
4
  before_action :disable_strong_params
5
5
  before_action :confirm_settings
6
- before_action :confirm_push_settings, only: [:start_upload_blob, :upload_blob, :finish_upload_blob,
7
- :push_manifest]
8
6
  skip_before_action :authorize
9
7
  before_action :optional_authorize, only: [:token, :catalog]
10
8
  before_action :registry_authorize, except: [:token, :v1_search, :catalog]
11
9
  before_action :authorize_repository_read, only: [:pull_manifest, :tags_list]
12
- # TODO: authorize_repository_write commented out until Katello indexes Pulp container push repositories.
13
- # before_action :authorize_repository_write, only: [:start_upload_blob, :upload_blob, :finish_upload_blob,
14
- # :push_manifest]
10
+ # TODO: authorize_repository_write commented out due to container push changes. Additional task needed to fix.
11
+ # before_action :authorize_repository_write, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
12
+ before_action :container_push_prop_validation, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
13
+ before_action :create_container_repo_if_needed, only: [:start_upload_blob, :upload_blob, :finish_upload_blob, :push_manifest]
15
14
  skip_before_action :check_media_type, only: [:start_upload_blob, :upload_blob, :finish_upload_blob,
16
15
  :push_manifest]
17
16
 
@@ -85,6 +84,301 @@ module Katello
85
84
  return false
86
85
  end
87
86
 
87
+ def container_push_prop_validation(props = nil)
88
+ # Handle validation and repo creation for container pushes before talking to pulp
89
+ return false unless confirm_push_settings
90
+ props = parse_blob_push_props if props.nil?
91
+ return false unless check_blob_push_field_syntax(props)
92
+
93
+ # validate input and find the org and product either using downcase label or id
94
+ if props[:schema] == "label"
95
+ return false unless check_blob_push_org_label(props)
96
+ return false unless check_blob_push_product_label(props)
97
+ else
98
+ return false unless check_blob_push_org_id(props)
99
+ return false unless check_blob_push_product_id(props)
100
+ end
101
+
102
+ return false unless check_blob_push_container(props)
103
+ true
104
+ end
105
+
106
+ def parse_blob_push_props(path_string = nil)
107
+ # path string should follow one of these formats:
108
+ # - /v2/{org_label}/{product_label}/{name}/blobs/uploads...
109
+ # - /v2/id/{org_id}/{product_id}/{name}/blobs/uploads...
110
+ # - /v2/{org_label}/{product_label}/{name}/manifests/...
111
+ # - /v2/id/{org_id}/{product_id}/{name}/manifests/...
112
+ # inputs not matching format will return {valid_format: false}
113
+ path_string = @_request.fullpath if path_string.nil?
114
+ segments = path_string.split('/')
115
+
116
+ if segments.length >= 7 && segments[0] == "" && segments[1] == "v2" &&
117
+ segments[2] != "id" && (segments[5] == "blobs" || segments[5] == "manifests")
118
+
119
+ return {
120
+ valid_format: true,
121
+ schema: "label",
122
+ organization: segments[2],
123
+ product: segments[3],
124
+ name: segments[4]
125
+ }
126
+ elsif segments.length >= 8 && segments[0] == "" && segments[1] == "v2" &&
127
+ segments[2] == "id" && (segments[6] == "blobs" || segments[6] == "manifests")
128
+
129
+ return {
130
+ valid_format: true,
131
+ schema: "id",
132
+ organization: segments[3],
133
+ product: segments[4],
134
+ name: segments[5]
135
+ }
136
+ else
137
+ return {valid_format: false}
138
+ end
139
+ end
140
+
141
+ def check_blob_push_field_syntax(props)
142
+ # check basic url field syntax
143
+ unless props[:valid_format]
144
+ return render_podman_error(
145
+ "NAME_INVALID",
146
+ "Invalid format. Container pushes should follow 'organization_label/product_label/name' OR 'id/organization_id/product_id/name' schema.",
147
+ :bad_request
148
+ )
149
+ end
150
+ return true
151
+ end
152
+
153
+ # rubocop:disable Metrics/MethodLength
154
+ def check_blob_push_org_label(props)
155
+ org_label = props[:organization]
156
+ unless org_label.present? && org_label.length > 0
157
+ return render_podman_error(
158
+ "NAME_INVALID",
159
+ "Invalid format. Organization label cannot be blank.",
160
+ :bad_request
161
+ )
162
+ end
163
+ org = Organization.where("LOWER(label) = '#{org_label}'") # convert to lowercase
164
+ # reject ambiguous orgs (possible due to lowercase conversion)
165
+ if org.length > 1
166
+ # Determine if the repo already exists in one of the possible products. If yes,
167
+ # inform the user they need to destroy the existing repo and use the ID format
168
+ unless props[:product].blank? || props[:name].blank?
169
+ org.each do |o|
170
+ products = get_matching_products_from_org(o, props[:product])
171
+ products.each do |prod|
172
+ root_repos = get_root_repo_from_product(prod, props[:name])
173
+ unless root_repos.empty?
174
+ return render_podman_error(
175
+ "NAME_INVALID",
176
+ "Due to a change in your organizations, this container name has become "\
177
+ "ambiguous (org name '#{org_label}'). If you wish to continue using this "\
178
+ "container name, destroy the organization in conflict with '#{o.name} (id "\
179
+ "#{o.id}). If you wish to keep both orgs, destroy '#{o.label}/#{prod.label}/"\
180
+ "#{root_repos.first.label}' and retry your push using the id format.",
181
+ :conflict
182
+ )
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ # Otherwise tell them to try pushing with ID format
189
+ return render_podman_error(
190
+ "NAME_INVALID",
191
+ "Organization label '#{org_label}' is ambiguous. Try using an id-based container name.",
192
+ :conflict
193
+ )
194
+ end
195
+ if org.length == 0
196
+ return render_podman_error(
197
+ "NAME_UNKNOWN",
198
+ "Organization not found: '#{org_label}'",
199
+ :not_found
200
+ )
201
+ end
202
+ @organization = org.first
203
+ true
204
+ end
205
+
206
+ def check_blob_push_org_id(props)
207
+ org_id = props[:organization]
208
+ unless org_id.present? && org_id == org_id.to_i.to_s
209
+ return render_podman_error(
210
+ "NAME_INVALID",
211
+ "Invalid format. Organization id must be an integer without leading zeros.",
212
+ :bad_request
213
+ )
214
+ end
215
+ @organization = Organization.find_by_id(org_id.to_i)
216
+ if @organization.nil?
217
+ return render_podman_error(
218
+ "NAME_UNKNOWN",
219
+ "Organization id not found: '#{org_id}'",
220
+ :not_found
221
+ )
222
+ end
223
+ true
224
+ end
225
+
226
+ def check_blob_push_product_label(props)
227
+ prod_label = props[:product]
228
+ unless prod_label.present? && prod_label.length > 0
229
+ return render_podman_error(
230
+ "NAME_INVALID",
231
+ "Invalid format. Product label cannot be blank.",
232
+ :bad_request
233
+ )
234
+ end
235
+ product = get_matching_products_from_org(@organization, prod_label)
236
+ # reject ambiguous products (possible due to lowercase conversion)
237
+ if product.length > 1
238
+ # Determine if the repo already exists in one of the possible products. If yes,
239
+ # inform the user they need to destroy the existing repo and use the ID format
240
+ unless props[:name].blank?
241
+ product.each do |prod|
242
+ root_repos = get_root_repo_from_product(prod, props[:name])
243
+ unless root_repos.empty?
244
+ return render_podman_error(
245
+ "NAME_INVALID",
246
+ "Due to a change in your products, this container name has become ambiguous "\
247
+ "(product name '#{prod_label}'). If you wish to continue using this container "\
248
+ "name, destroy the product in conflict with '#{prod.name}' (id #{prod.id}). If "\
249
+ "you wish to keep both products, destroy '#{@organization.label}/#{prod.label}/"\
250
+ "#{root_repos.first.label}' and retry your push using the id format.",
251
+ :conflict
252
+ )
253
+ end
254
+ end
255
+ end
256
+
257
+ return render_podman_error(
258
+ "NAME_INVALID",
259
+ "Product label '#{prod_label}' is ambiguous. Try using an id-based container name.",
260
+ :conflict
261
+ )
262
+ end
263
+ if product.length == 0
264
+ return render_podman_error(
265
+ "NAME_UNKNOWN",
266
+ "Product not found: '#{prod_label}'",
267
+ :not_found
268
+ )
269
+ end
270
+ @product = product.first
271
+ true
272
+ end
273
+
274
+ def check_blob_push_product_id(props)
275
+ prod_id = props[:product]
276
+ unless prod_id.present? && prod_id == prod_id.to_i.to_s
277
+ return render_podman_error(
278
+ "NAME_INVALID",
279
+ "Invalid format. Product id must be an integer without leading zeros.",
280
+ :bad_request
281
+ )
282
+ end
283
+ @product = @organization.products.find_by_id(prod_id.to_i)
284
+ if @product.nil?
285
+ return render_podman_error(
286
+ "NAME_UNKNOWN",
287
+ "Product id not found: '#{prod_id}'",
288
+ :not_found
289
+ )
290
+ end
291
+ true
292
+ end
293
+
294
+ def get_matching_products_from_org(organization, product_label)
295
+ return organization.products.where("LOWER(label) = '#{product_label}'") # convert to lowercase
296
+ end
297
+
298
+ def get_root_repo_from_product(product, root_repo_name)
299
+ return product.root_repositories.where(label: root_repo_name)
300
+ end
301
+
302
+ def check_blob_push_container(props)
303
+ unless props[:name].present? && props[:name].length > 0
304
+ return render_podman_error(
305
+ "NAME_INVALID",
306
+ "Invalid format. Container name cannot be blank.",
307
+ :bad_request
308
+ )
309
+ end
310
+
311
+ @container_name = props[:name]
312
+ @container_push_name_format = props[:schema]
313
+ if @container_push_name_format == "label"
314
+ @container_path_input = "#{props[:organization]}/#{props[:product]}/#{props[:name]}"
315
+ else
316
+ @container_path_input = "id/#{props[:organization]}/#{props[:product]}/#{props[:name]}"
317
+ end
318
+
319
+ # If the repo already exists, check if the existing push format matches
320
+ root_repo = get_root_repo_from_product(@product, @container_name).first
321
+ if !root_repo.nil? && @container_push_name_format != root_repo.container_push_name_format
322
+ return render_podman_error(
323
+ "NAME_INVALID",
324
+ "Repository name '#{@container_name}' already exists in this product using a different naming scheme. Please retry your request with the #{root_repo.container_push_name_format} format or destroy and recreate the repository using your preferred schema.",
325
+ :conflict
326
+ )
327
+ end
328
+
329
+ true
330
+ end
331
+
332
+ def create_container_repo_if_needed
333
+ if get_root_repo_from_product(@product, @container_name).empty?
334
+ root = @product.add_repo(
335
+ name: @container_name,
336
+ label: @container_name,
337
+ download_policy: 'immediate',
338
+ content_type: Repository::DOCKER_TYPE,
339
+ unprotected: true,
340
+ is_container_push: true,
341
+ container_push_name: @container_path_input,
342
+ container_push_name_format: @container_push_name_format
343
+ )
344
+ sync_task(::Actions::Katello::Repository::CreateRoot, root, @container_path_input)
345
+ end
346
+ end
347
+
348
+ def blob_push_cleanup
349
+ # after manifest upload, index content and set version href using pulp api
350
+ root_repo = get_root_repo_from_product(@product, @container_name)&.first
351
+ instance_repo = root_repo&.library_instance
352
+
353
+ unless root_repo.present? && instance_repo.present?
354
+ return render_podman_error(
355
+ "BLOB_UPLOAD_UNKNOWN",
356
+ "Could not locate local uploaded repository for content indexing.",
357
+ :not_found
358
+ )
359
+ end
360
+
361
+ api = ::Katello::Pulp3::Repository.api(SmartProxy.pulp_primary, ::Katello::Repository::DOCKER_TYPE).container_push_api
362
+ api_response = api.list(name: @container_path_input)&.results&.first
363
+ latest_version_href = api_response&.latest_version_href
364
+ pulp_href = api_response&.pulp_href
365
+
366
+ if latest_version_href.empty? || pulp_href.empty?
367
+ return render_podman_error(
368
+ "BLOB_UPLOAD_UNKNOWN",
369
+ "Could not locate repository properties for content indexing.",
370
+ :not_found
371
+ )
372
+ end
373
+
374
+ instance_repo.update!(version_href: latest_version_href)
375
+ ::Katello::Pulp3::RepositoryReference.where(root_repository_id: instance_repo.root_id,
376
+ content_view_id: instance_repo.content_view.id, repository_href: pulp_href).create!
377
+ instance_repo.index_content
378
+
379
+ true
380
+ end
381
+
88
382
  def find_writable_repository
89
383
  Repository.docker_type.syncable.find_by_container_repository_name(params[:repository])
90
384
  end
@@ -205,17 +499,6 @@ module Katello
205
499
  redirect_client { Resources::Registry::Proxy.get(@_request.fullpath, headers, max_redirects: 0) }
206
500
  end
207
501
 
208
- def push_manifest
209
- headers = translated_headers_for_proxy
210
- headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
211
- body = @_request.body.read
212
- pulp_response = Resources::Registry::Proxy.put(@_request.fullpath, body, headers)
213
- pulp_response.headers.each do |key, value|
214
- response.header[key.to_s] = value
215
- end
216
- head pulp_response.code
217
- end
218
-
219
502
  def start_upload_blob
220
503
  headers = translated_headers_for_proxy
221
504
  headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
@@ -225,6 +508,7 @@ module Katello
225
508
  pulp_response.headers.each do |key, value|
226
509
  response.header[key.to_s] = value
227
510
  end
511
+
228
512
  head pulp_response.code
229
513
  end
230
514
 
@@ -268,6 +552,21 @@ module Katello
268
552
  head pulp_response.code
269
553
  end
270
554
 
555
+ def push_manifest
556
+ headers = translated_headers_for_proxy
557
+ headers['Content-Type'] = request.headers['Content-Type'] if request.headers['Content-Type']
558
+ body = @_request.body.read
559
+ pulp_response = Resources::Registry::Proxy.put(@_request.fullpath, body, headers)
560
+ pulp_response.headers.each do |key, value|
561
+ response.header[key.to_s] = value
562
+ end
563
+
564
+ cleanup_result = blob_push_cleanup if pulp_response.code.between?(200, 299)
565
+ return false unless cleanup_result
566
+
567
+ head pulp_response.code
568
+ end
569
+
271
570
  def ping
272
571
  response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
273
572
  render json: {}, status: :ok
@@ -278,10 +577,10 @@ module Katello
278
577
  end
279
578
 
280
579
  def v1_search
281
- # Checks for podman client and issues a 404 in that case. Podman
580
+ # Checks for v2 client and issues a 404 in that case. Podman
282
581
  # examines the response from a /v1_search request. If the result
283
582
  # is a 4XX, it will then proceed with a request to /_catalog
284
- if request.headers['HTTP_USER_AGENT'].downcase.include?('libpod')
583
+ if request.headers['HTTP_DOCKER_DISTRIBUTION_API_VERSION'] == 'registry/2.0'
285
584
  render json: {}, status: :not_found
286
585
  return
287
586
  end
@@ -423,8 +722,11 @@ module Katello
423
722
 
424
723
  def confirm_push_settings
425
724
  return true if SETTINGS.dig(:katello, :container_image_registry, :allow_push)
426
- render_error('custom_error', :status => :not_found,
427
- :locals => { :message => "Registry push not supported" })
725
+ render_podman_error(
726
+ "UNSUPPORTED",
727
+ "Registry push is not enabled. To enable, add ':katello:'->':container_image_registry:'->':allow_push: true' in the katello settings file.",
728
+ :unprocessable_entity
729
+ )
428
730
  end
429
731
 
430
732
  def request_url
@@ -446,10 +748,19 @@ module Katello
446
748
  Rails.logger.debug "With body: #{filter_sensitive_data(response.body)}\n" unless route_name == 'pull_blob'
447
749
  end
448
750
 
751
+ def render_podman_error(code, message, status = :bad_request)
752
+ # Renders a podman-compatible error and returns false.
753
+ # code: uppercase string code from opencontainer error code spec:
754
+ # https://specs.opencontainers.org/distribution-spec/?v=v1.0.0#DISTRIBUTION-SPEC-140
755
+ # message: a custom error string
756
+ # status: a symbol in the 400 block of the rails response code table:
757
+ # https://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option
758
+ render json: {errors: [{code: code, message: message}]}, status: status
759
+ false
760
+ end
761
+
449
762
  def item_not_found(item)
450
- msg = "#{item} was not found!"
451
- # returning errors based on registry specifications in https://docs.docker.com/registry/spec/api/#errors
452
- render json: {errors: [code: :invalid_request, message: msg, details: msg]}, status: :not_found
763
+ render_podman_error("NAME_UNKNOWN", "#{item} was not found!", :not_found)
453
764
  end
454
765
  end
455
766
  end
@@ -48,7 +48,7 @@ Pass [] to make repo available for clients regardless of OS version. Maximum len
48
48
  param :ssl_client_cert_id, :number, :desc => N_("Identifier of the content credential containing the SSL Client Cert"), :allow_nil => true
49
49
  param :ssl_client_key_id, :number, :desc => N_("Identifier of the content credential containing the SSL Client Key"), :allow_nil => true
50
50
  param :unprotected, :bool, :desc => N_("true if this repository can be published via HTTP")
51
- param :checksum_type, String, :desc => N_("Checksum of the repository, currently 'sha1' & 'sha256' are supported")
51
+ param :checksum_type, String, :desc => N_("Checksum used for published repository contents. Supported types: %s") % Katello::RootRepository::CHECKSUM_TYPES.join(', ')
52
52
  param :docker_upstream_name, String, :desc => N_("Name of the upstream docker repository")
53
53
  param :include_tags, Array, :desc => N_("Comma-separated list of tags to sync for a container image repository")
54
54
  param :exclude_tags, Array, :desc => N_("Comma-separated list of tags to exclude when syncing a container image repository. Default: any tag ending in \"-source\"")
@@ -69,8 +69,13 @@ module Actions
69
69
  end
70
70
 
71
71
  def update_content_counts(_execution_plan)
72
- smart_proxy = ::SmartProxy.unscoped.find(input[:smart_proxy_id])
73
- ::ForemanTasks.async_task(::Actions::Katello::CapsuleContent::UpdateContentCounts, smart_proxy)
72
+ if Setting[:automatic_content_count_updates]
73
+ smart_proxy = ::SmartProxy.unscoped.find(input[:smart_proxy_id])
74
+ ::ForemanTasks.async_task(::Actions::Katello::CapsuleContent::UpdateContentCounts, smart_proxy)
75
+ else
76
+ Rails.logger.info "Skipping content counts update as automatic content count updates are disabled. To enable automatic content count updates, set the 'automatic_content_count_updates' setting to true.
77
+ To update content counts manually, run the 'Update Content Counts' action."
78
+ end
74
79
  end
75
80
 
76
81
  def resource_locks
@@ -16,10 +16,15 @@ module Actions
16
16
  repositories.each do |repo|
17
17
  plan_action(Katello::Repository::RefreshRepository, repo)
18
18
  end
19
- plan_self
19
+ plan_self(:organization_name => organization.name)
20
20
  end
21
21
  end
22
22
 
23
+ def run
24
+ organization = ::Organization.find_by(name: input[:organization_name])
25
+ organization&.manifest_expiration_date(cached: false) # update organization.manifest_imported? value
26
+ end
27
+
23
28
  def failure_notification(plan)
24
29
  ::Katello::UINotifications::Subscriptions::ManifestDeleteError.deliver!(
25
30
  :subject => subject_organization,
@@ -13,10 +13,13 @@ module Actions
13
13
 
14
14
  org = repository.organization
15
15
  sequence do
16
- create_action = plan_action(Pulp3::Orchestration::Repository::Create,
17
- repository, SmartProxy.pulp_primary, force_repo_create)
18
-
19
- return if create_action.error
16
+ # Container push repositories will already be in pulp. The version_href is
17
+ # directly updated after a push.
18
+ unless root.is_container_push
19
+ create_action = plan_action(Pulp3::Orchestration::Repository::Create,
20
+ repository, SmartProxy.pulp_primary, force_repo_create)
21
+ return if create_action.error
22
+ end
20
23
 
21
24
  # when creating a clone, the following actions are handled by the
22
25
  # publish/promote process
@@ -32,13 +35,16 @@ module Actions
32
35
  end
33
36
  end
34
37
 
35
- concurrence do
36
- plan_self(:repository_id => repository.id, :clone => clone)
37
- if !clone && repository.url.present?
38
- repository.product.alternate_content_sources.with_type(repository.content_type).each do |acs|
39
- acs.smart_proxies.each do |smart_proxy|
40
- smart_proxy_acs = ::Katello::SmartProxyAlternateContentSource.create(alternate_content_source_id: acs.id, smart_proxy_id: smart_proxy.id, repository_id: repository.id)
41
- plan_action(Pulp3::Orchestration::AlternateContentSource::Create, smart_proxy_acs)
38
+ # Container push repos do not need metadata generation or ACS (they do not sync)
39
+ unless root.is_container_push
40
+ concurrence do
41
+ plan_self(:repository_id => repository.id, :clone => clone)
42
+ if !clone && repository.url.present?
43
+ repository.product.alternate_content_sources.with_type(repository.content_type).each do |acs|
44
+ acs.smart_proxies.each do |smart_proxy|
45
+ smart_proxy_acs = ::Katello::SmartProxyAlternateContentSource.create(alternate_content_source_id: acs.id, smart_proxy_id: smart_proxy.id, repository_id: repository.id)
46
+ plan_action(Pulp3::Orchestration::AlternateContentSource::Create, smart_proxy_acs)
47
+ end
42
48
  end
43
49
  end
44
50
  end
@@ -2,13 +2,15 @@ module Actions
2
2
  module Katello
3
3
  module Repository
4
4
  class CreateRoot < Actions::EntryAction
5
- def plan(root)
5
+ def plan(root, relative_path = nil)
6
6
  root.save!
7
7
  repository = ::Katello::Repository.new(:environment => root.organization.library,
8
8
  :content_view_version => root.organization.library.default_content_view_version,
9
9
  :root => root)
10
- repository.relative_path = repository.custom_repo_path
10
+ repository.container_repository_name = relative_path
11
+ repository.relative_path = relative_path || repository.custom_repo_path
11
12
  repository.save!
13
+
12
14
  action_subject(repository)
13
15
  plan_action(::Actions::Katello::Repository::Create, repository)
14
16
  end
@@ -4,7 +4,7 @@ module Actions
4
4
  class BindEntitlement < Actions::Base
5
5
  def run
6
6
  output[:response] = ::Katello::Resources::Candlepin::UpstreamConsumer
7
- .bind_entitlement(pool)
7
+ .bind_entitlement(**pool)
8
8
  end
9
9
 
10
10
  def humanized_name
@@ -14,6 +14,7 @@ module Actions
14
14
  plan_action(Actions::Pulp3::OrphanCleanup::DeleteOrphanRemotes, proxy)
15
15
  end
16
16
  plan_action(Actions::Pulp3::OrphanCleanup::RemoveOrphans, proxy)
17
+ plan_action(Actions::Pulp3::OrphanCleanup::PurgeCompletedTasks, proxy)
17
18
  end
18
19
  end
19
20
  end
@@ -0,0 +1,15 @@
1
+ module Actions
2
+ module Pulp3
3
+ module OrphanCleanup
4
+ class PurgeCompletedTasks < Pulp3::AbstractAsyncTask
5
+ def plan(smart_proxy)
6
+ plan_self(:smart_proxy_id => smart_proxy.id)
7
+ end
8
+
9
+ def run
10
+ output[:pulp_tasks] = ::Katello::Pulp3::Api::Core.new(smart_proxy).purge_completed_tasks
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -454,6 +454,8 @@ module Katello
454
454
  ).each do |facet|
455
455
  facet.update_applicability_counts
456
456
  facet.update_errata_status
457
+ rescue NoMethodError
458
+ Rails.logger.warn _('Errata statuses not updated for deleted content facet with UUID %s') % facet.uuid
457
459
  end
458
460
  end
459
461
  end
@@ -133,7 +133,14 @@ module Katello
133
133
 
134
134
  repo_param[:mirroring_policy] = Katello::RootRepository::MIRRORING_POLICY_ADDITIVE if repo_param[:mirroring_policy].blank?
135
135
 
136
- RootRepository.new(repo_param.merge(:product_id => self.id))
136
+ repo_param = repo_param.merge(:product_id => self.id)
137
+
138
+ # Container push may concurrently call root add several times before the db can update.
139
+ if repo_param[:is_container_push]
140
+ RootRepository.create_or_find_by!(repo_param)
141
+ else
142
+ RootRepository.new(repo_param)
143
+ end
137
144
  end
138
145
  end
139
146
  end
@@ -118,7 +118,7 @@ module Katello
118
118
  end}
119
119
 
120
120
  before_validation :set_pulp_id
121
- before_validation :set_container_repository_name, :if => :docker?
121
+ before_validation :set_container_repository_name, :unless => :skip_container_name?
122
122
 
123
123
  scope :has_url, -> { joins(:root).where.not("#{RootRepository.table_name}.url" => nil) }
124
124
  scope :on_demand, -> { joins(:root).where("#{RootRepository.table_name}.download_policy" => ::Katello::RootRepository::DOWNLOAD_ON_DEMAND) }
@@ -268,6 +268,10 @@ module Katello
268
268
  self.content_view_version.content_view
269
269
  end
270
270
 
271
+ def skip_container_name?
272
+ self.library_instance? && self.root.docker? && self.root.is_container_push
273
+ end
274
+
271
275
  def library_instance?
272
276
  self.content_view.default?
273
277
  end
@@ -17,7 +17,7 @@ module Katello
17
17
  DOWNLOAD_POLICIES = [DOWNLOAD_IMMEDIATE, DOWNLOAD_ON_DEMAND].freeze
18
18
 
19
19
  IGNORABLE_CONTENT_UNIT_TYPES = %w(srpm treeinfo).freeze
20
- CHECKSUM_TYPES = %w(sha1 sha256).freeze
20
+ CHECKSUM_TYPES = %w(sha256 sha384 sha512).freeze
21
21
 
22
22
  SUBSCRIBABLE_TYPES = [Repository::YUM_TYPE, Repository::OSTREE_TYPE, Repository::DEB_TYPE].freeze
23
23
  SKIPABLE_METADATA_TYPES = [Repository::YUM_TYPE, Repository::DEB_TYPE].freeze
@@ -112,6 +112,8 @@ module Katello
112
112
  only_integer: true
113
113
  }
114
114
 
115
+ validates :container_push_name_format, inclusion: { in: ['label', 'id'].freeze, allow_nil: true}
116
+
115
117
  scope :subscribable, -> { where(content_type: RootRepository::SUBSCRIBABLE_TYPES) }
116
118
  scope :skipable_metadata_check, -> { where(content_type: RootRepository::SKIPABLE_METADATA_TYPES) }
117
119
  scope :has_url, -> { where.not(:url => nil) }
@@ -195,7 +197,7 @@ module Katello
195
197
  def ensure_docker_repo_unprotected
196
198
  unless unprotected
197
199
  errors.add(:base, _("Container Image Repositories are not protected at this time. " \
198
- "They need to be published via http to be available to containers."))
200
+ "They need to be published via http to be available to containers."))
199
201
  end
200
202
  end
201
203
 
@@ -36,7 +36,7 @@ module Katello
36
36
  end
37
37
 
38
38
  def to_status(_options = {})
39
- traces = host.host_traces.pluck(:app_type)
39
+ traces = host.host_traces.reload.pluck(:app_type)
40
40
  traces.delete(Katello::HostTracer::TRACE_APP_TYPE_SESSION)
41
41
 
42
42
  if traces.include?(Katello::HostTracer::TRACE_APP_TYPE_STATIC)
@@ -176,6 +176,14 @@ module Katello
176
176
  nil
177
177
  end
178
178
 
179
+ def purge_class
180
+ PulpcoreClient::Purge
181
+ end
182
+
183
+ def purge_completed_tasks
184
+ tasks_api.purge(purge_class.new(finished_before: DateTime.now - Setting[:completed_pulp_task_protection_days]))
185
+ end
186
+
179
187
  def delete_orphans
180
188
  [orphans_api.cleanup(PulpcoreClient::OrphansCleanup.new(orphan_protection_time: (smart_proxy.pulp_mirror? ? 0 : Setting[:orphan_protection_time])))]
181
189
  end
@@ -15,6 +15,10 @@ module Katello
15
15
  def recursive_add_api
16
16
  PulpContainerClient::ContainerRecursiveAddApi.new(api_client)
17
17
  end
18
+
19
+ def container_push_api
20
+ PulpContainerClient::RepositoriesContainerPushApi.new(api_client)
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -22,12 +22,7 @@ module Katello
22
22
 
23
23
  def publication_options(repository_version)
24
24
  options = super(repository_version)
25
- options.merge(
26
- {
27
- metadata_checksum_type: root.checksum_type,
28
- package_checksum_type: root.checksum_type
29
- }
30
- )
25
+ options.merge(checksum_type: root.checksum_type)
31
26
  end
32
27
 
33
28
  def specific_create_options
@@ -1,3 +1,5 @@
1
- <%= javascript_include_tag *webpack_asset_paths('katello', extension: 'js') %>
1
+ <% content_for(:javascripts) do %>
2
+ <%= webpacked_plugins_js_for :katello %>
3
+ <% end %>
2
4
  <% @smartProxyId= @smart_proxy.id %>
3
5
  <%= react_component('Content', smartProxyId: @smartProxyId, organizationId: Organization.current&.id,) %>
@@ -0,0 +1,7 @@
1
+ class AddContainerPushPropsToRepo < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :katello_root_repositories, :is_container_push, :boolean, default: false
4
+ add_column :katello_root_repositories, :container_push_name, :string, default: nil
5
+ add_column :katello_root_repositories, :container_push_name_format, :string, default: nil
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ class RemoveSha1RepositoryChecksumType < ActiveRecord::Migration[6.1]
2
+ def up
3
+ ::Katello::Repository.where(saved_checksum_type: 'sha1').update(saved_checksum_type: nil)
4
+ ::Katello::RootRepository.where(checksum_type: 'sha1').update(checksum_type: nil)
5
+ end
6
+
7
+ def down
8
+ fail ActiveRecord::IrreversibleMigration
9
+ end
10
+ end
@@ -10,7 +10,12 @@
10
10
  angular.module('Bastion.repositories').service('Checksum',
11
11
  ['translate', function (translate) {
12
12
 
13
- this.checksums = [{name: translate('Default'), id: null}, {id: 'sha256', name: 'sha256'}, {id: 'sha1', name: 'sha1'}];
13
+ this.checksums = [
14
+ {name: translate('Default'), id: null},
15
+ {id: 'sha256', name: 'sha256'},
16
+ {id: 'sha384', name: 'sha384'},
17
+ {id: 'sha512', name: 'sha512'}
18
+ ];
14
19
 
15
20
  this.checksumType = function (checksum) {
16
21
  if (checksum === null) {
@@ -371,9 +371,6 @@
371
371
  ng-model="repository.checksum_type"
372
372
  ng-options="type.id as type.name for type in checksums">
373
373
  </select>
374
- <p class="help-block" translate>
375
- For older operating systems such as Red Hat Enterprise Linux 5 or CentOS 5 it is recommended to use sha1.
376
- </p>
377
374
  </div>
378
375
 
379
376
  <div class="checkbox" ng-hide="repository.content_type === 'docker' || repository.content_type === 'ansible_collection'">
@@ -634,6 +634,12 @@ Foreman::Plugin.register :katello do
634
634
  full_name: N_('Orphaned Content Protection Time'),
635
635
  description: N_('Time in minutes before content that is not contained within a repository and has not been accessed is considered orphaned.')
636
636
 
637
+ setting 'completed_pulp_task_protection_days',
638
+ type: :integer,
639
+ default: 30,
640
+ full_name: N_('Completed pulp task protection days'),
641
+ description: N_('How many days before a completed Pulp task is purged by Orphan Cleanup.')
642
+
637
643
  setting 'remote_execution_prefer_registered_through_proxy',
638
644
  type: :boolean,
639
645
  default: false,
@@ -651,6 +657,12 @@ Foreman::Plugin.register :katello do
651
657
  default: true,
652
658
  full_name: N_('Distribute archived content view versions'),
653
659
  description: N_("If this is enabled, repositories of content view versions without environments (\"archived\") will be distributed at '/pulp/content/<organization>/content_views/<content view>/X.Y/...'.")
660
+
661
+ setting 'automatic_content_count_updates',
662
+ type: :boolean,
663
+ default: true,
664
+ full_name: N_('Calculate content counts on smart proxies automatically'),
665
+ description: N_("If this is enabled, content counts on smart proxies will be updated automatically after content sync.")
654
666
  end
655
667
  end
656
668
 
@@ -1,3 +1,3 @@
1
1
  module Katello
2
- VERSION = "4.13.0.rc1".freeze
2
+ VERSION = "4.13.0".freeze
3
3
  end
data/package.json CHANGED
@@ -51,7 +51,6 @@
51
51
  "angular": "1.8.2",
52
52
  "bootstrap-select": "1.13.18",
53
53
  "ngreact": "^0.5.0",
54
- "query-string": "^6.1.0",
55
54
  "react-bootstrap": "^0.32.1",
56
55
  "use-deep-compare-effect": "^1.6.1"
57
56
  }
@@ -20,7 +20,6 @@ const ContentTable = ({ content, tableSchema, onPaginationChange }) => {
20
20
  loadingText={__('Loading')}
21
21
  >
22
22
  <Table
23
- ouiaId="content-table-table"
24
23
  columns={tableSchema}
25
24
  rows={results}
26
25
  pagination={pagination}
@@ -15,7 +15,6 @@ exports[`Content Table should render and contain appropriate components 1`] = `
15
15
  }
16
16
  itemCount={2}
17
17
  onPaginationChange={[Function]}
18
- ouiaId="content-table-table"
19
18
  pagination={Object {}}
20
19
  rows={
21
20
  Array [
@@ -1,59 +1,53 @@
1
- import React, { Component } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import PropTypes from 'prop-types';
3
- import qs from 'query-string';
3
+ import { useUrlParams } from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
4
4
  import { translate as __ } from 'foremanReact/common/I18n';
5
5
  import { orgId } from '../../services/api';
6
6
  import TableSchema from '../ModuleStreams/ModuleStreamsTableSchema';
7
7
  import GenericContentPage from '../../components/Content/GenericContentPage';
8
8
 
9
- class ModuleStreamsPage extends Component {
10
- constructor(props) {
11
- super(props);
9
+ const ModuleStreamsPage = (props) => {
10
+ const { searchParam } = useUrlParams();
11
+ const [searchQuery, setSearchQuery] = useState(searchParam || '');
12
+ const { getModuleStreams } = props;
12
13
 
13
- const queryParams = qs.parse(this.props.location.search);
14
- this.state = {
15
- searchQuery: queryParams.search || '',
16
- };
17
- }
18
-
19
- componentDidMount() {
20
- this.props.getModuleStreams({
21
- search: this.state.searchQuery,
14
+ useEffect(() => {
15
+ getModuleStreams({
16
+ search: searchQuery,
22
17
  });
23
- }
18
+ }, [getModuleStreams, searchQuery]);
24
19
 
25
- onPaginationChange = (pagination) => {
26
- this.props.getModuleStreams({
20
+ const onPaginationChange = (pagination) => {
21
+ props.getModuleStreams({
27
22
  ...pagination,
28
23
  });
29
24
  };
30
25
 
31
- onSearch = (search) => {
32
- this.props.getModuleStreams({ search });
26
+ const onSearch = (search) => {
27
+ props.getModuleStreams({ search });
33
28
  };
34
29
 
35
- updateSearchQuery = (searchQuery) => {
36
- this.setState({ searchQuery });
30
+ const updateSearchQuery = (newSearchQuery) => {
31
+ setSearchQuery(newSearchQuery);
37
32
  };
38
33
 
39
- render() {
40
- const { moduleStreams } = this.props;
41
- return (
42
- <GenericContentPage
43
- header={__('Module Streams')}
44
- content={moduleStreams}
45
- tableSchema={TableSchema}
46
- onSearch={this.onSearch}
47
- autocompleteEndpoint="/katello/api/v2/module_streams"
48
- autocompleteQueryParams={{ organization_id: orgId() }}
49
- bookmarkController="katello_module_streams"
50
- updateSearchQuery={this.updateSearchQuery}
51
- initialInputValue={this.state.searchQuery}
52
- onPaginationChange={this.onPaginationChange}
53
- />
54
- );
55
- }
56
- }
34
+ const { moduleStreams } = props;
35
+ return (
36
+ <GenericContentPage
37
+ header={__('Module Streams')}
38
+ content={moduleStreams}
39
+ tableSchema={TableSchema}
40
+ onSearch={onSearch}
41
+ autocompleteEndpoint="/katello/api/v2/module_streams"
42
+ autocompleteQueryParams={{ organization_id: orgId() }}
43
+ bookmarkController="katello_module_streams"
44
+ updateSearchQuery={updateSearchQuery}
45
+ initialInputValue={searchQuery}
46
+ onPaginationChange={onPaginationChange}
47
+ />
48
+ );
49
+ };
50
+
57
51
 
58
52
  ModuleStreamsPage.propTypes = {
59
53
  location: PropTypes.shape({
@@ -1,19 +1,21 @@
1
1
  import React from 'react';
2
2
  import { shallow } from 'enzyme';
3
3
  import toJson from 'enzyme-to-json';
4
+ import * as hooks from 'foremanReact/components/PF4/TableIndexPage/Table/TableHooks';
4
5
  import ModuleStreamsPage from '../ModuleStreamsPage';
5
6
  import GenericContentPage from '../../../components/Content/GenericContentPage';
6
7
 
7
8
  describe('Module streams page', () => {
8
9
  it('should render and contain appropiate components', async () => {
9
10
  const moduleStreams = {};
10
- const mockLocation = { search: '' };
11
+ jest.spyOn(hooks, 'useUrlParams').mockImplementation(() => ({
12
+ searchParam: '',
13
+ }));
11
14
  const getModuleStreams = () => {};
12
15
 
13
16
  const wrapper = shallow(<ModuleStreamsPage
14
17
  moduleStreams={moduleStreams}
15
18
  getModuleStreams={getModuleStreams}
16
- location={mockLocation}
17
19
  />);
18
20
 
19
21
  expect(toJson(wrapper)).toMatchSnapshot();
@@ -88,7 +88,6 @@ exports[`Module streams table should render and contain appropiate components 1`
88
88
  }
89
89
  itemCount={0}
90
90
  onPaginationChange={[Function]}
91
- ouiaId="content-table-table"
92
91
  pagination={Object {}}
93
92
  rows={Array []}
94
93
  />
@@ -137,6 +137,8 @@ class ManageManifestModal extends Component {
137
137
  return name;
138
138
  };
139
139
 
140
+ const manifestExpiredMessage = manifestExpirationDate ? __('Your manifest expired on {expirationDate}. To continue using Red Hat content, import a new manifest.') : __('Your manifest has expired. To continue using Red Hat content, import a new manifest.');
141
+
140
142
  return (
141
143
  <ForemanModal id={MANAGE_MANIFEST_MODAL_ID} title={__('Manage Manifest')}>
142
144
  <Tabs id="manifest-history-tabs">
@@ -181,7 +183,7 @@ class ManageManifestModal extends Component {
181
183
  title={__('Manifest expired')}
182
184
  >
183
185
  <FormattedMessage
184
- defaultMessage={__('Your manifest expired on {expirationDate}. To continue using Red Hat content, import a new manifest.')}
186
+ defaultMessage={manifestExpiredMessage}
185
187
  values={{
186
188
  expirationDate: new Date(manifestExpirationDate).toDateString(),
187
189
  }}
@@ -198,7 +200,7 @@ class ManageManifestModal extends Component {
198
200
  {getManifestName()}
199
201
  </Col>
200
202
  </Row>
201
- {isManifestImported && manifestExpirationDate &&
203
+ {isManifestImported && Boolean(manifestExpirationDate) &&
202
204
  <Row>
203
205
  <Col sm={5} />
204
206
  <Col sm={7}>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katello
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.13.0.rc1
4
+ version: 4.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - N/A
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-30 00:00:00.000000000 Z
11
+ date: 2024-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -965,6 +965,7 @@ files:
965
965
  - app/lib/actions/pulp3/orphan_cleanup/delete_orphan_remotes.rb
966
966
  - app/lib/actions/pulp3/orphan_cleanup/delete_orphan_repository_versions.rb
967
967
  - app/lib/actions/pulp3/orphan_cleanup/delete_orphaned_migrated_repositories.rb
968
+ - app/lib/actions/pulp3/orphan_cleanup/purge_completed_tasks.rb
968
969
  - app/lib/actions/pulp3/orphan_cleanup/remove_orphans.rb
969
970
  - app/lib/actions/pulp3/orphan_cleanup/remove_unneeded_repos.rb
970
971
  - app/lib/actions/pulp3/repository/commit_upload.rb
@@ -2058,6 +2059,8 @@ files:
2058
2059
  - db/migrate/20240207191223_remove_entitlement_mode_host_statuses.rb
2059
2060
  - db/migrate/20240423112842_add_fields_to_katello_docker_manifest.rb
2060
2061
  - db/migrate/20240502192021_change_katello_repository_rpms_id_seq_to_big_int.rb
2062
+ - db/migrate/20240520142245_add_container_push_props_to_repo.rb
2063
+ - db/migrate/20240531193030_remove_sha1_repository_checksum_type.rb
2061
2064
  - db/seeds.d/101-locations.rb
2062
2065
  - db/seeds.d/102-organizations.rb
2063
2066
  - db/seeds.d/104-proxy.rb
@@ -5392,13 +5395,10 @@ files:
5392
5395
  - webpack/test-utils/nockWrapper.js
5393
5396
  - webpack/test-utils/react-testing-lib-wrapper.js
5394
5397
  - webpack/test_setup.js
5395
- - webpack/utils/__tests__/useParamsWithHash.test.js
5396
5398
  - webpack/utils/helpers.js
5397
- - webpack/utils/paramsFromHash.js
5398
5399
  - webpack/utils/useDebounce.js
5399
5400
  - webpack/utils/useEventListener.js
5400
5401
  - webpack/utils/useKatelloDocUrl.js
5401
- - webpack/utils/useUrlParams.js
5402
5402
  homepage: http://www.katello.org
5403
5403
  licenses:
5404
5404
  - GPL-2.0
@@ -5417,9 +5417,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
5417
5417
  version: '4'
5418
5418
  required_rubygems_version: !ruby/object:Gem::Requirement
5419
5419
  requirements:
5420
- - - ">"
5420
+ - - ">="
5421
5421
  - !ruby/object:Gem::Version
5422
- version: 1.3.1
5422
+ version: '0'
5423
5423
  requirements: []
5424
5424
  rubygems_version: 3.4.19
5425
5425
  signing_key:
@@ -1,22 +0,0 @@
1
- import paramsFromHash from '../paramsFromHash';
2
-
3
- test('can parse both hash and query params', () => {
4
- const { hash, params: { foo } } = paramsFromHash('#filters?foo=bar');
5
- expect(hash).toBe('filters');
6
- expect(foo).toBe('bar');
7
- });
8
-
9
- test('can parse just hash', () => {
10
- const { hash } = paramsFromHash('#filters');
11
- expect(hash).toBe('filters');
12
- });
13
-
14
- test('can parse just query params', () => {
15
- const { params: { foo } } = paramsFromHash('?foo=bar');
16
- expect(foo).toBe('bar');
17
- });
18
-
19
- test("won't error with blank string", () => {
20
- const { hash } = paramsFromHash('');
21
- expect(hash).toBe('');
22
- });
@@ -1,16 +0,0 @@
1
- import qs from 'query-string';
2
-
3
- /*
4
- For when you have a hash with your params in the URL
5
- pass in the the 'hash' object from react-router's useLocation();
6
- e.g. "mysite.com/foo#bar?baz=bop"
7
- will return { hash: 'bar', params: { baz: "bop" }}
8
- */
9
- const paramsFromHash = (hash) => {
10
- const [baseHash, queryParams = {}] = hash.split('?');
11
- const params = qs.parse(queryParams);
12
- const trimmedHash = baseHash.replace('#', '').replace('/', '');
13
- return { hash: trimmedHash, params };
14
- };
15
-
16
- export default paramsFromHash;
@@ -1,14 +0,0 @@
1
- import qs from 'query-string';
2
- import { useLocation } from 'react-router-dom';
3
-
4
- // Allows hash and params e.g. "/foo#bar?query=baz"
5
- // returns { hash: "bar", params: { query: "baz" } }
6
- const useUrlParamsWithHash = () => {
7
- const { hash: fullParams } = useLocation();
8
- const [hash, queryParams = {}] = fullParams.split('?');
9
- const params = qs.parse(queryParams);
10
- const trimmedhash = hash.replace('#', '');
11
- return { hash: trimmedhash, params };
12
- };
13
-
14
- export default useUrlParamsWithHash;