Skip to content

Upgrading

The release notes list the breaking changes of each release, e.g., of 0.4.0, and link the upgrade steps on this page. Upgrades of Helm releases are described in Deployment. This page describes how to upgrade, and how existing setups and data are affected.

The steps add up: when skipping versions, apply the sections of all versions in between, starting with the oldest, which is the lowest on this page. Where sections advise different rollouts, follow the strictest (e.g., upgrade with the app scaled to 0 replicas). 1.0.0rc2 starts with a fresh database (see From 1.0.0rc1): when upgrading to it or later, skip what the sections below say about stored data (e.g., fixes with mongosh or migrations of stored subjects). Their steps for the config, the deployment and clients still apply.

From 1.0.0rc1

This version needs a fresh database: data is not kept, and there is no migration. The fields that the registry adds to the API and stores moved to camelCase (see API style), so this version can't read what earlier versions stored. It refuses to start if it finds such data or indexes (also after only emptying the collections), naming the collections. This also holds for upgrades from any earlier version:

  1. Scale the app to 0 replicas.
  2. Point it to an empty database (e.g., a new volume), or delete the database serviceStore. Back up what you want to keep first, and register services, service types and roles again through the API.
  3. Upgrade the app and the clients together, e.g., cloud-registry-ui to its 1.0.0-rc2.

Going back to 1.0.0rc1 needs its database, as it was.

Outgoing requests: the registry now fetches service info from public addresses when users import it and for syncs, so it needs outgoing connections, on top of those to the token issuers; nothing changes if they are blocked, except that imports fail and synced services become out-of-sync. It adds the collections service_info_fetches and notification_dismissals, and the policy rules import-service-info, owners-sync-services, maintainers-sync-services, get-service-sync, list-syncs, list-notifications and dismiss-notifications, which overlays that give policies as a list must include. Sync is available by default, so owners can turn it on for their services: each worker then runs a background thread, which queries the database once a minute and fetches service info, by default every 24 hours per service (the index it uses is created on startup); set custom.endpoints.services.service_info.sync: false to prevent it. The Helm chart has an optional policy for the app's connections.

FOCA 0.21: the app now builds on FOCA 0.21 (see FOCA's migration guide), whose stricter request checks change some responses, so check clients that rely on them:

  • Query parameters that an operation doesn't declare now return 400 Bad Request on all operations, including GET /services, GET /service-info and POST /services/types, which ignored them.
  • createdAt and updatedAt that aren't date-times now return 400 Bad Request for all users, instead of being ignored for users other than admins.
  • Some 400 Bad Request responses have another detail: URLs that aren't valid URIs (without scheme, or with whitespace) get FOCA's message, which quotes the value (and is logged at DEBUG since FOCA 0.22, see below), so don't send secrets in those fields, and unpaired surrogates in JSON bodies get one that doesn't name the field.
  • FOCA checks the formats of responses too, so stored date-time and uri values must be valid, or reads of the service and GET /services return 500. Requests can't store such values, and this version needs a fresh database, but if data got in around the API, the app logs an error on startup naming the services: correct or delete them in the database.

FOCA 0.22: the app now builds on FOCA 0.22 (see FOCA's migration guide). Requests that fail the validation of the API specification keep the validator's reason as detail (exceptions.validation_reasons). Where the invalid fields are known, their responses gain the members type (FOCA's problem type, a URL ending in /problems/invalid-request/), code (invalid-request) and errors, which lists them; bodies that aren't JSON or have unpaired surrogates get none of these. The logs change, so check log checks and alerts that rely on them:

  • Requests (Incoming request) and denials of access control (Access denied) name the route of the request (e.g., /ga4gh/registry/v1/services/{serviceId}) instead of its path; the path is logged at DEBUG.
  • Rejected requests are logged by the locations and codes of their invalid fields (e.g., Request rejected: #/url (invalid-format)) instead of the validator's reason (otherwise by the mapped detail, e.g., The request is malformed.); the reason is logged at DEBUG only, so rejected values are no longer logged at INFO.
  • FOCA warns once per worker of what it deprecates ahead of its release on ASGI, e.g., of the werkzeug classes in the app's exception map.

Invalid fields: requests that the registry's own checks reject (e.g., URLs with user info or schemes their field doesn't allow, environments, unregistered types, identifiers that can't be stored in roles) now get the same 400 Bad Request as those that fail the schema validation: all invalid fields in errors, each with a JSON Pointer (pointer) or parameter name (parameter), a stable code and a message, with the problem type and code invalid-request (see Errors). Their detail lists the locations and messages of all invalid fields (e.g., '#/url': Must use one of these schemes: 'https'.) instead of describing the first one. Clients that show or parse detail should show the messages of errors at their fields instead.

Service URLs: a url with leading or trailing spaces now returns 400 Bad Request instead of being stored, so remove the spaces from what you register again.

API: fields and query parameters change their names, without aliases. Keys of the config stay snake_case.

Where Before Now
GET /service-info registration_rules registrationRules
its rules url_schemes urlSchemes
its rules service_types serviceTypes
its rules max_unused_service_types maxUnusedServiceTypes
its rules max_services_per_user maxServicesPerUser
/service-info/listing service_id serviceId
permissions user_id userId
GET /me user_id userId
GET /me admin_available adminAvailable
GET /me may_add_service_types mayAddServiceTypes
GET /me service_types_pending serviceTypesPending
GET /me, roles in services service_id serviceId
GET /approvals service_types serviceTypes
invitations service_id serviceId
invitations created_by createdBy
invitations created_at createdAt
invitations expires_at expiresAt
invitations, accepted user_id userId
invitations, accepted accepted_at acceptedAt
POST /invitations/accept service_id serviceId
GET /services/<id>/invitations, query include_accepted includeAccepted
verifications verified_at verifiedAt
GET /verifications service_id serviceId

The paths of users' roles change too, as they collided with those of groups, which API clients and overlays with their own policy rules must follow:

  • API: PUT and DELETE /services/<id>/permissions/<user-id> move to /services/<id>/permissions/users/<user-id>. There is no alias: the former paths are not found (404 Not Found). /services/<id>/permissions/groups/<group> and GET /services/<id>/permissions stay. Users called groups, or whose identifiers start with groups/, were refused and can now be granted roles and accept invitations; the namespaces of issuers (security.auth.issuers) may now start with groups/.
  • Policies: the rule owners-change-permissions moves from /ga4gh/registry/v1/services/:id/permissions/* to .../permissions/users/*, and no longer covers the roles of groups; owners-change-group-permissions (.../permissions/groups/*) keeps its path and is now the only rule for them. Overlays that only set rules to null need no change, but note that owners-change-group-permissions: null now stops owners from changing roles of groups, which needed a change of both rules before. Overlays that redefine owners-change-permissions, or give policies as a list, must move their rules:
    • .../permissions/* still lets owners change roles of users, but also of groups: change it to .../permissions/users/*. The app warns on startup if the rule of groups is changed or removed, but such a rule is still configured.
    • .../permissions/:user and rules per namespace (e.g., .../permissions/https:/*) don't match the new paths, so that owners get 403 Forbidden: change them to .../permissions/users/* (or .../permissions/users/https:/*). The app warns on startup if owners can create invitations, but no rule lets them grant roles to users.

FOCA 0.20: the app now builds on FOCA 0.20 (see FOCA's migration guide), whose image has no pip. Images derived from the app's image that run pip install must use uv instead, mounted from a pinned stage, as the image has none either (see the guide). The app has no gateway plugins, so the plugin store and its secret don't apply. The Helm chart is now on foca-common 0.4.0, which checks audiences: the chart no longer accepts an invalid global config.security.auth.audience if each issuer has its own, which it used to ignore. New values are optional: cloudRegistry.startupProbe, cloudRegistry.readinessProbe and cloudRegistry.livenessProbe replace the default probes of the app's container.

Environment of the service info: the app now refuses to start if service_info.service.environment isn't in custom.endpoints.services.environments (see Service environments). The defaults pass (dev, which the default list has), as does an empty list or a service info without environment, so check this only if you changed one of the two: in that case, add the value to the list, or set the environment to a listed value, before upgrading. Otherwise, the pods of this version don't start, and the error names both settings.

Listing the registry itself: admins can now add the registry to its own services (see Listing the registry itself). Nothing changes until they do.

Invitation labels: invitations can have a label (see Invitations). Only invitations with a label have the field in responses, so clients that reject unknown fields of invitations must accept label.

Expired invitations: those that weren't accepted are now kept (see Invitations):

  • GET /services/<id>/invitations lists them, and no longer lists accepted invitations unless asked (?includeAccepted=true). All invitations have a new status (open, expired or accepted). Clients that look for acceptances in the list must ask for them, and clients that show all listed invitations as open must check status.
  • DELETE /services/<id>/invitations/<invitation-id> removes expired ones, which used to be 404 Not Found. Their links still can't be accepted (404).
  • The new option custom.access_control.invitations.keep_expired_days (default: 30, at most 365) sets when they are removed automatically. 0 removes them when they expire, as before. Check the default against your privacy statement: until then, invitations that weren't accepted keep their labels, which may name people, and who created them and when (createdBy, createdAs, createdAt). With the defaults, that is up to 37 days after they were created (expiry_days + keep_expired_days; before, 7), and current owners see the creators.
  • Revoking a user's roles on a service now also deletes the invitations to it that the user created and nobody accepted, also those that stayed valid because they were created as an admin or through a group, and with them createdBy, createdAs and createdAt. Accepted ones keep these fields until they expire.
  • max_per_service still counts the same: invitations that haven't expired, open or accepted.
  • The app sets the TTL index of the collection invitations when it starts and the value differs, so that its database user needs to be able to drop and create indexes (as readWrite can).

From 0.6.0

Data is kept. Owners can invite users to their services by link (see Invitations):

  • The app stores invitations in the new collection invitations, whose indexes it creates on startup (among them TTL indexes, with which MongoDB deletes expired invitations).
  • The base config has three new rules (owners-manage-invitations, owners-revoke-invitations, accept-invitations in security.access_control.policies). Overlays that give the policies as a list, rather than changing single rules by name, must add them, or the new endpoints return 403 Forbidden.
  • Deployments that stopped owners from granting roles, by removing or restricting owners-change-permissions by name (see Requiring login for reads), inherit the rules of invitations, with which owners can grant owner and maintainer again: set owners-manage-invitations and owners-revoke-invitations to null in the same overlay. The app warns on startup if owners can create invitations, but no rule lets them grant roles to users.
  • The new options custom.access_control.invitations (expiry_days, at most 3650, and max_per_service) have defaults (see Invitations).

Users other than admins may now register at most 50 services each (custom.access_control.max_services_per_user; see Services per user): check how many services your users registered, and raise the limit, or set null to turn it off, in the same rollout if needed. Users who registered more keep their services, but can't register others until they are below the limit. After the upgrade, list them, e.g., with mongosh (see MongoDB; with your limit instead of 50):

db.getSiblingDB("serviceStore").service_counts.find({count: {$gte: 50}})

The app counts each service for the user who registered it, stored with the service from now on. On startup, it adds that user to the services stored before: the only user who is owner of the service (services with no or several such owners, e.g., owned by a group, count for nobody), and counts the services per user (new collection service_counts, and a new index of services). Restart the app once after the rollout (or upgrade with the app scaled to 0 replicas), so that services registered, updated or deleted by pods of 0.6.0 during the rollout are counted correctly.

Services that admins registered before the upgrade have the admin as their only owner, so they count for the admin, unlike those that admins register in admin mode from now on. The app can't tell admins apart on startup (they are admins through the groups in their tokens), so if an admin registered many services (e.g., imported them), make these count for nobody after the upgrade (with the admin's user identifier), and restart the app, so that it corrects the counters:

db.getSiblingDB("serviceStore").services.updateMany(
  {_createdBy: "<admin's user identifier>"},
  {$set: {_createdBy: null}},
)

Registrations and updates (POST /services, PUT /services/{serviceId}) with organization fields other than name and url are now rejected (400 Bad Request; see Registrations). Clients that send further fields in organization must drop them. Services stored with such fields are still served; the app warns of them on startup (see Stored services).

With the Helm chart, the default cloudRegistry.image is now fully qualified, docker.io/elixircloud/cloud-registry (see Image): adapt image allow-lists, admission policies (e.g., of Kyverno or Gatekeeper) and registry mirror rules that match the short name elixircloud/cloud-registry, or pods may be rejected, or pull past the mirror, after helm upgrade.

From 0.5.0

Data is kept. The app now builds on FOCA 0.18, which provides several of its settings and checks; change the config before upgrading:

  1. Public URL: custom.endpoints.service is removed: set FOCA's server.public_url to the absolute URL of the app as clients see it, including any path prefix of a proxy (formerly api_path), e.g., https://registry.example.org/ instead of url_prefix: https, external_host: registry.example.org and external_port: 443 (see Public URLs). The app refuses to start without it, and FOCA refuses URLs with a query or fragment. With the Helm chart, hostName keeps working (https://<hostName>/); set publicUrl for anything else (see Deployment).
  2. Admins required: custom.access_control.require_admin moves to FOCA's security.access_control.require_admin (true in the base config, as before); the old key is rejected. The other settings of custom.access_control (service_types, max_unused_service_types) stay.
  3. Gunicorn: the image loads FOCA's settings (foca.gunicorn_config); cloud_registry.gunicorn_config no longer exists. The settings are the same; custom commands that load the old module must load FOCA's.
  4. Admin mode: admins act as admins only in requests that ask for it (header X-Request-Roles: admin; see Admin mode). Tell the maintainers of clients and scripts that need admin rights to send the header, or remove the opt-in (security.access_control.opt_in_roles: []); for web UIs, see Admin mode. In GET /me, admin is now true only in admin mode; clients decide whether to offer the switch by the new field admin_available. FOCA refuses configured rules that grant the role admin (security.access_control.policies) on startup. Stored ones (e.g., granted via the permissions API) still apply, without admin mode, and FOCA warns of them when policies are first loaded. Revoke them (DELETE /admin/access-control/permissions/<rule-id>, in admin mode), and add the users to an admin group instead. During a rolling update, pods of the previous version still grant admin rights without the header, and may store new admin grants via the permissions API: if admin mode must hold from the start, stop them before the new pods serve requests (e.g., scale the app to 0 replicas, then upgrade), or revoke stored admin grants once the rollout has finished.
  5. Owners' roles of groups: the policy rule owners-change-permissions now covers /ga4gh/registry/v1/services/:id/permissions/* (before: .../permissions/:user), as user identifiers may contain slashes (with several token issuers), so it also covers the roles of groups (.../permissions/groups/<group>). Overlays that restrict owners' roles of groups (e.g., owners-change-group-permissions: null) must now also change owners-change-permissions, or owners can grant and revoke roles of groups again: remove it, or redefine it for users only (see Requiring login for reads, also for the startup warning). Later versions move the roles of users (see From 1.0.0rc1), so that the rules no longer overlap.

Noticeable, but no action needed:

  • Startup checks of the group settings, of the admins and of the service info are FOCA's, with FOCA's log messages (other wording and loggers):
    • FOCA refuses group mappings without a group claim (security.access_control.claim_groups) and group claims with surrounding whitespace with a ValueError (before: a RuntimeError of the app), and doesn't warn if no group claim is set at all.
    • FOCA's admin check also counts configured rules (security.access_control.policies) that grant the role admin (only without admin mode, which refuses such rules), and its warning without admins no longer names what nobody can do in the mode of service types (see Service types).
    • FOCA warns of values of the service info in reserved example domains (e.g., id, organization.url, contactUrl and documentationUrl), and doesn't check organization.name.
  • Requests rejected by the validation of the API specification still get the reason as detail, now from FOCA (exceptions.validation_details); the reasons are logged by FOCA. Responses that fail the validation of the API specification (e.g., of stored services that are invalid) are now logged by Connexion (ERROR, with the response), as FOCA only filters the log records of rejected requests.

From 0.4.0

Data is kept. Before upgrading:

  1. Check the environments of the stored services, e.g., with mongosh (see MongoDB for access to the database of a Helm deployment):

    db.getSiblingDB("serviceStore").services.distinct("environment")
    

    Registrations may only use the allowed environments now, by default prod, test, dev and staging. To keep other values, allow them in custom.endpoints.services.environments in the same rollout; otherwise, services with other values are served, but can only be updated with an allowed value (see Stored services). 2. Add the rules add-service-types and list-approvals to overlays that give the policies as a list (see Service types). 3. Tell clients that the registry sets the timestamps now: those that users other than admins send are ignored. 4. Users other than admins may add at most 10 types that no service has (custom.access_control.max_unused_service_types; see Service types); change the limit in the same rollout if needed, and rate-limit POST /services/types and POST /services at the ingress (see Rate limits). 5. Overlays that set their own x-bearerInfoFunc (in api.specs.registry.add_security_fields) keep working, but tokens sent with a public read that the overlay's function rejects are then validated twice. The base config now uses cloud_registry.ga4gh.registry.permissions.validate_bearer_token, a thin wrapper around FOCA's validate_token(); drop the setting from the overlay, or point it to that function (see Authentication).

Then upgrade, and restart the app once after the rollout (or upgrade with the app scaled to 0 replicas), so that it stores the types of the services that pods of 0.4.0 registered during the rollout (see Service types). On startup, the app also creates its new indexes and reconciles which services are pending; nothing needs to be done for them. Switch to the mode moderated (see Service types) only after that: while pods of 0.4.0 still register services, a user's pending type can hide another user's service that a pod of 0.4.0 registered with the same type, until an admin approves the type.

From versions before 0.4.0

Redeploy with a fresh install and an empty database; don't migrate the data. The previously deployed version (FOCA 0.12) accepted tokens of any issuer and had no ownership checks, so anyone with any OpenID Connect token could create, change or delete entries: its data can't be trusted. In the new version, services stored before have no owner, so only admins could manage them.

  1. Optionally, export the registered services as a reference for their operators (GET /ga4gh/registry/v1/services).
  2. Uninstall the release (helm uninstall <release> -n <namespace>; this deletes the MongoDB volume and data). Releases of the legacy charts can't be upgraded anyway: the chart renames its resources and changes the pod selectors.
  3. Create a Secret with the MongoDB credentials (MongoDB requires authentication now; see Authentication).
  4. Write the values file of the deployment (see Configuration of the Helm chart). The identity provider is deployment configuration now, without defaults (see Deployment configuration):
    • hostName: the public host name (required);
    • config.security.auth.issuers: the trusted token issuers, i.e., your identity provider (required; the base config trusts none);
    • config.security.auth.audience: the client IDs of the apps that call the registry, e.g., its front end (required);
    • config.security.access_control.claim_groups: the claim with the groups of users, also in config.security.auth.userinfo_claims if the identity provider releases it via user info only (required with group mappings);
    • config.security.access_control.group_mapping: the admin group mapped to the role admin (required, unless config.security.access_control.require_admin is false; before 0.6.0, config.custom.access_control.require_admin);
    • mongodb.auth.existingSecret: the name of the Secret;
    • config.service_info.service: the service info of the deployment (e.g., id, name, organization, contactUrl and environment: prod; the defaults changed, see the configuration changes in the release notes, so set the previous id, ELIXIR_CLOUD_SERVICE_REGISTRY_1, to keep it for clients), which is no longer set via the API;
    • remove cloud_registry.appName (use fullnameOverride to choose the resource names) and apiServer, and move any changes made to the app-config ConfigMap to config;
    • rename host_name to hostName and cloud_registry to cloudRegistry (rendering fails while the old keys are set), and mongodb.volumeSize to mongodb.persistence.size (rendering fails with volumeSize; see Persistence);
    • optionally, enable the app's NetworkPolicy (cloudRegistry.networkPolicy.enabled: true), with cloudRegistry.networkPolicy.ingressFrom set to the pods of the cluster's ingress controller or Gateway (there are no default peers; see Network policies). MongoDB's NetworkPolicy is enabled by default and only lets the app's pods connect (see Network policies).
  5. Install the chart attached to the release (cloud-registry-1.0.0-rc2.tgz): helm install <release> cloud-registry-1.0.0-rc2.tgz -n <namespace> -f values-prod.yaml. The chart version goes from 4.0.0 (the last chart version in the repository) back to the app version, which it now follows; that's no problem for a fresh install.
  6. Ask the operators to register their services again (POST /services); they become their owners. Admins verify them (see Verified services).

To keep the data anyway, dump and restore it as described in From the legacy charts: the app then adds keys to the stored services on startup and refuses to start while some of them have invalid URLs or are duplicates (see Stored services), and admins have to assign owners (PUT /services/{serviceId}/permissions/users/{userId}).

Docker Compose setups keep the data in a named volume now; see Docker Compose data to keep using the former bind mount.

Docker Compose data

Earlier versions kept the data in ../data/cloud-registry/db. To keep using it, bind-mount it in a docker-compose.override.yaml:

services:
    mongodb:
        volumes:
            - ../data/cloud-registry/db:/data/db

Public URLs

Previously, custom.endpoints.service.api_path held the API base path itself (e.g., ga4gh/registry/v1); remove that value when upgrading (see Public URLs).

Service info

Previously, service info could be set via POST /service-info and was stored in the database. That endpoint is gone, and stored service info is no longer served (the service_info collection can be dropped): put it in the config when upgrading (see Service info).

Service types

Previously, service types were not stored: GET /services/types listed the types of the registered services. Now they are stored (see Service types). On startup, the app stores the types of the registered services, so GET /services/types lists the same types as before; afterwards, it also lists types that no service has (yet), and types stay registered when the services that had them change or are deleted (admins remove them). Types of stored services that aren't objects with the strings group, artifact and version are skipped. Services registered by pods of the previous version during a rolling update don't have their types stored until the app restarts: restart it once after the rollout (e.g., kubectl rollout restart deployment/<name> -n <namespace>).

Overlays that give the policies (security.access_control.policies) as a list must add the new rules add-service-types and list-approvals (see config.yaml), or users can't add types with POST /services/types or list pending approvals with GET /approvals.

Several token issuers

Trusting a second token issuer (see Several token issuers) namespaces the identifiers of users by issuer, also those of the issuer trusted so far, unless its subjects are kept bare. Stored identifiers of its users (their roles on services, roles granted via the permissions API, the service types they added, the services they registered (for the limit of services per user), their display names, and the invitations they created or accepted) then no longer match them (e.g., open invitations of owners are deleted when they are accepted, as their creators seem to be no longer owners):

  • Recommended: keep the subjects of the issuer trusted so far bare (namespace: ""), so that nothing needs to be migrated. Its users keep their identifiers, and those of the new issuer are namespaced.
  • Otherwise: after the config change, namespace the stored identifiers of the issuer trusted so far with the migration of the image, with the app's new config (e.g., in a pod of the app, whose deployment name depends on the release; --dry-run only counts them, and changes nothing in the database, whereas the migration itself sets up the app as on startup, so run it with the image of the deployed version). A dry run counts what is stored, so its numbers can differ: configured rules are those that the running app stored. It doesn't check the config as startup does, either. The migration, also with --dry-run, refuses to run on a database of a version before 1.0.0rc2, while the config trusts a single issuer, or keeps the subjects of an issuer bare:
kubectl exec deploy/cloud-registry -- \
  python -m cloud_registry.namespace_subjects https://login.example.org/oidc/

The argument is the namespace of the issuer trusted so far (by default, the issuer). Its users lack their roles from the rollout until the migration, so run it right after the rollout, while users don't change roles or accept invitations (e.g., announce a short maintenance window): roles revoked during the migration may be restored by it, invitations accepted during it may be deleted or stay claimed although their role wasn't granted (create new ones then), and requests that store display names may fail (clients can retry them). Running app instances load the changed rules on their next request. Configured rules that name bare users (security.access_control.policies) are counted, but not changed: namespace them in the config. The migration can be run again (namespaced identifiers are kept).

Stored services

This section describes services stored by versions up to 1.0.0rc1. From 1.0.0rc2, which starts with a fresh database, the app no longer adds keys or creators to stored services on startup, and no longer warns of invalid URLs or further fields of organizations. It still warns of URL schemes and environments that are not allowed, e.g., after the config changed.

Services stored before the registry set the timestamps (createdAt, updatedAt) keep those they were registered with, which their owners could set to any value (before timestamps were checked, also to values that aren't RFC 3339 date-times), until they are updated: then updatedAt is set, and createdAt is kept if it is an RFC 3339 date-time (services without one don't get one). Admins can correct them via the API (PUT /services/{serviceId}); as their timestamps are checked, they send a valid createdAt, or none, when they update services with invalid ones.

Services stored before the environments were checked may have other values (e.g., production); they are served as they are, but can only be updated with an allowed value (or without environment). Allow their values in custom.endpoints.services.environments to keep them.

Services stored before the length limits were introduced may have longer values; they can't be updated unchanged, only with the values shortened.

Services stored before organization fields other than name and url were rejected may have further fields in organization, of any size; they are still served, but can only be updated without them. The app warns of them on startup, naming them. Have their owners (or admins) update them via the API (PUT /services/{serviceId}), or remove the fields in the database, e.g., with mongosh (see MongoDB), which keeps only name and url, and only changes the service if its organization is an object with both:

const services = db.getSiblingDB("serviceStore").services
const organizationValid = {
  $expr: {$eq: [{$type: "$organization"}, "object"]},
  "organization.name": {$type: "string"},
  "organization.url": {$type: "string"},
}
services.updateOne({id: "<id>", ...organizationValid}, [
  {$set: {organization: {$arrayToObject: {$filter: {
    input: {$objectToArray: "$organization"},
    cond: {$in: ["$$this.k", ["name", "url"]]},
  }}}}},
])

Services whose organization lacks name or url (or isn't an object, or has values that aren't strings; only data written outside the API, which has always required both) aren't served: with response validation on (validate_responses in the base config), GET /services/{serviceId} returns 500 Internal Server Error for them, and so does the whole list, GET /services, until both are set. The app doesn't warn of them on startup, and the example above doesn't match them (matchedCount: 0), so that it doesn't leave an organization of {}. List them, and set both fields (or delete the services); the update only changes services that are still broken (e.g., not fixed via the API meanwhile), and keeps the other fields of organizations that are objects:

const organizationBroken = {$nor: [organizationValid]}
services.find(organizationBroken, {_id: 0, id: 1, organization: 1})
services.updateOne({id: "<id>", ...organizationBroken}, [
  {$set: {organization: {$mergeObjects: [
    {$cond: [{$eq: [{$type: "$organization"}, "object"]}, "$organization", {}]},
    {name: "<name>", url: "<URL>"},
  ]}}},
])

Then clean up their further fields as above.

Services stored before the check of duplicate services get their key on startup; only services without a key are checked again. If some of them have invalid URLs, or the same URL and API type as another one, the app refuses to start and names all of them. Invalid URLs include some that were accepted before: URLs with schemes other than http and https, whitespace or control characters, malformed percent-encodings (also in queries and fragments), characters that URLs only allow percent-encoded (e.g., <, |, \), hosts with percent-encodings or empty labels (e.g., a..org), IPv4 addresses not in dotted-decimal form (e.g., 127.1), or port 0. Fix or delete them in the database, e.g., with mongosh (see MongoDB for access to the database of a Helm deployment):

db.getSiblingDB("serviceStore").services.deleteOne({id: "<id>"})
db.getSiblingDB("serviceStore").services.updateOne({id: "<id>"}, {$set: {url: "<URL>"}})

Then start the app again; versions up to 1.0.0rc1 add the missing keys.

Services that already have a key (stored by dev images built before URLs were checked, or before the stricter checks above) are not checked again: if their URLs are now invalid, they are still served, but can only be updated with a valid URL, and registrations of the same service with a validly spelled URL may not be detected as duplicates. Versions up to 1.0.0rc1 warn of them on startup, naming them; update or delete them as above.

Services stored with http URLs (accepted before https became the only allowed scheme by default) are still served, but can only be updated with an allowed scheme; the app warns of them on startup, naming them. Have their owners (or admins) update them with an https URL via the API (PUT /services/{serviceId}), which also updates their keys, or delete them; or allow http (not recommended, as clients would send access tokens unencrypted).