    def service_from_spec(
        self,
        spec: Dict,
        from_config=True,
    ) -> Optional[Service]:
        """Create the service instance and related objects from
        config data.

        Args:
            spec (Dict): The spec of service, defined in the config file.
            from_config (bool, optional): `True` if the service will be created
            from the config file, `False` if it is created from REST API.
            Defaults to `True`.

        Returns:
            Optional[Service]: The created service
        """

        if 'name' not in spec:
            raise ValueError(f'service spec must have a name: {spec}')

        name = spec['name']

        if self.subdomain_host:
            parsed_host = urlparse(self.subdomain_host)
            domain = self.subdomain_hook(name, parsed_host.hostname, kind="service")
            host = f"{parsed_host.scheme}://{domain}"
            if parsed_host.port:
                host = f"{host}:{parsed_host.port}"
        else:
            domain = host = ''

        # get/create orm
        orm_service = orm.Service.find(self.db, name=name)
        if orm_service is None:
            # not found, create a new one
            orm_service = orm.Service(name=name, from_config=from_config)
            self.db.add(orm_service)
            if spec.get('admin', False):
                self.log.warning(
                    f"Service {name} sets `admin: True`, which is deprecated in JupyterHub 2.0."
                    " You can assign now assign roles via `JupyterHub.load_roles` configuration."
                    " If you specify services in the admin role configuration, "
                    "the Service admin flag will be ignored."
                )
                roles.update_roles(self.db, entity=orm_service, roles=['admin'])
        else:
            # Do nothing if the config file tries to modify a API-base service
            # or vice versa.
            if orm_service.from_config != from_config:
                if from_config:
                    self.log.error(
                        f"The service {name} from the config file is trying to modify a runtime-created service with the same name"
                    )
                else:
                    self.log.error(
                        f"The runtime-created service {name} is trying to modify a config-based service with the same name"
                    )
                return
        orm_service.admin = spec.get('admin', False)

        self.db.commit()
        service = Service(
            parent=self,
            app=self,
            base_url=self.base_url,
            db=self.db,
            orm=orm_service,
            roles=orm_service.roles,
            domain=domain,
            host=host,
            hub=self.hub,
        )

        traits = service.traits(input=True)
        for key, value in spec.items():
            trait = traits.get(key)
            if trait is None:
                raise AttributeError(f"No such service field: {key}")
            setattr(service, key, value)
            # also set the value on the orm object
            # unless it's marked as not in the db
            # (e.g. on the oauth object)
            if trait.metadata.get("in_db", True):
                setattr(orm_service, key, value)

        if service.api_token:
            self.service_tokens[service.api_token] = service.name
        elif service.managed:
            # generate new token
            # TODO: revoke old tokens?
            service.api_token = service.orm.new_api_token(note="generated at startup")

        if service.url:
            parsed = urlparse(service.url)
            if parsed.scheme not in {"http", "https"}:
                raise ValueError(
                    f"Unsupported scheme in URL for service {name}: {service.url}. Must be http[s]"
                )

            port = None
            if parsed.port is not None:
                port = parsed.port
            elif parsed.scheme == 'http':
                port = 80
            elif parsed.scheme == 'https':
                port = 443

            server = service.orm.server = orm.Server(
                proto=parsed.scheme,
                ip=parsed.hostname,
                port=port,
                cookie_name=service.oauth_client_id,
                base_url=service.prefix,
            )
            self.db.add(server)
        else:
            service.orm.server = None

        if service.oauth_available:
            self.log.info(
                f"Creating service {service.name} with oauth_client_id={service.oauth_client_id}"
            )
            if not service.oauth_redirect_uri:
                # redirect uri has a default value if a URL is configured,
                # but must be specified explicitly for external services
                raise ValueError(
                    f"Service {service.name} has oauth configured, but is missing required oauth_redirect_uri."
                )
            allowed_scopes = set()
            if service.oauth_client_allowed_scopes:
                allowed_scopes.update(service.oauth_client_allowed_scopes)
            if service.oauth_roles:
                if not allowed_scopes:
                    # DEPRECATED? It's still convenient and valid,
                    # e.g. 'admin'
                    allowed_roles = list(
                        self.db.query(orm.Role).filter(
                            orm.Role.name.in_(service.oauth_roles)
                        )
                    )
                    allowed_scopes.update(roles.roles_to_scopes(allowed_roles))
                else:
                    self.log.warning(
                        f"Ignoring oauth_roles for {service.name}: {service.oauth_roles},"
                        f" using oauth_client_allowed_scopes={allowed_scopes}."
                    )
            oauth_client = self.oauth_provider.add_client(
                client_id=service.oauth_client_id,
                client_secret=service.api_token,
                redirect_uri=service.oauth_redirect_uri,
                description=f"JupyterHub service {service.name}",
            )
            service.orm.oauth_client = oauth_client
            # add access-scopes, derived from OAuthClient itself
            allowed_scopes.update(scopes.access_scopes(oauth_client))
            oauth_client.allowed_scopes = sorted(allowed_scopes)
        else:
            self.log.info(f"Creating service {service.name} without oauth.")
            if service.oauth_client:
                self.log.warning(
                    f"Deleting unused oauth client for service {service.name} with client_id={service.oauth_client.identifier}"
                )
                self.db.delete(service.oauth_client)

        self._service_map[name] = service

        return service
Create the service instance and related objects from
config data.

Args:
spec (Dict): The spec of service, defined in the config file.
from_config (bool, optional): `True` if the service will be created
from the config file, `False` if it is created from REST API.
Defaults to `True`.

Returns:
Optional[Service]: The created service
jupyterhub/app.py