diff --git a/.github/ISSUE_TEMPLATE/new-release.md b/.github/ISSUE_TEMPLATE/new-release.md index a06fa78c9..0d45f5ad1 100644 --- a/.github/ISSUE_TEMPLATE/new-release.md +++ b/.github/ISSUE_TEMPLATE/new-release.md @@ -22,10 +22,10 @@ Release captain responsible - <@gh_username> ### 2. Prepare the codebase for a new release -- [ ] Prepare the release by running the `cut-release-pr.sh` script +- [ ] Prepare the release by running the `cut-release-pr.sh` script (ensure you have the conda-store-dev env activated and a valid GITHUB_ACCESS_TOKEN) ```bash - ./cut-release-pr.sh -r -c + ./tools/cut-release-pr.sh -r -c ``` - [ ] Ensure that the conda-store, conda-store-server, conda-store-ui versions have been updated @@ -58,5 +58,4 @@ Release captain responsible - <@gh_username> - [ ] Update the [conda-forge feedstock version](https://github.com/conda-forge/conda-store-feedstock) through a PR or review and merge the regro-bot PR. - [ ] If needed - update `meta.yaml` or `recipe.yaml` and re-render the feedstock. - [ ] Open a follow-up PR to bump `conda-store` and `conda-store-server` versions to the next dev-release number (for example `2024.10.1`). -- [ ] Open a follow-up PR to bump the `conda-store-server` version in the [`conda-store-ui` compose file](https://github.com/conda-incubator/conda-store-ui/blob/main/docker-compose.yml). - [ ] Celebrate, you're done! ๐ŸŽ‰ diff --git a/.github/workflows/build_docker_image.yaml b/.github/workflows/build_docker_image.yaml index 36776a0ec..91c929fe3 100644 --- a/.github/workflows/build_docker_image.yaml +++ b/.github/workflows/build_docker_image.yaml @@ -21,9 +21,6 @@ jobs: docker-image: - conda-store - conda-store-server - platform: - - linux/amd64 - - linux/arm64 steps: - name: "Checkout Repository ๐Ÿ›Ž" uses: actions/checkout@v4 @@ -47,6 +44,16 @@ jobs: - name: "Set up Docker Buildx ๐Ÿ—" uses: docker/setup-buildx-action@v3 + # Required for building multi-platform images. See + # https://docs.docker.com/build/ci/github-actions/multi-platform/#build-and-load-multi-platform-images + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } - name: "Login to Docker Hub ๐Ÿณ" uses: docker/login-action@v3 @@ -92,4 +99,4 @@ jobs: cache-to: type=gha,mode=max build-args: | python_version=${{ env.PYTHON_VERSION_DEFAULT }} - platforms: ${{ matrix.platform }} + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4fcc18ec8..8fc97b872 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -147,6 +147,16 @@ jobs: - name: "Set up Docker Buildx ๐Ÿ—" uses: docker/setup-buildx-action@v3 + # Required for building multi-platform images. See + # https://docs.docker.com/build/ci/github-actions/multi-platform/#build-and-load-multi-platform-images + with: + daemon-config: | + { + "debug": true, + "features": { + "containerd-snapshotter": true + } + } - name: "Login to Docker Hub ๐Ÿณ" uses: docker/login-action@v3 @@ -188,4 +198,5 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bcd76ba95..f13f83436 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,10 +12,14 @@ ci: autofix_commit_msg: | [pre-commit.ci] Apply automatic pre-commit fixes +exclude: | + (?x)^( + docusaurus-docs/static/openapi.json + ) repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: "v0.8.6" + rev: "v0.9.4" hooks: - id: ruff exclude: "examples|tests/assets" @@ -32,8 +36,9 @@ repos: - id: end-of-file-fixer exclude: ".python-version-default" - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.8 + - repo: https://github.com/biomejs/pre-commit + rev: "v0.6.1" hooks: - - id: prettier - exclude: ^(examples/|templates/|) + - id: biome-check + additional_dependencies: ["@biomejs/biome@1.9.4"] + exclude: ^(examples/|templates/) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2390f6424..74b054291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,3 @@ - # Changelog All notable changes to this project will be documented in this file. @@ -7,6 +6,96 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The project changed to `CalVer` in September 2023. --- +## [2025.2.2] - 2025-02-11 + +([full changelog](https://github.com/conda-incubator/conda-store/compare/2025.2.1...58b51292123ddb124135a848b01a98a8b72d5b0c)) + +### Merged PRs + +IMPROVEMENTS: +- Mount code into docker container [#1052](https://github.com/conda-incubator/conda-store/pull/1052) ([@soapy1](https://github.com/soapy1)) + +BUG FIXES: +- Re-add docker blob build artifact type [#1086](https://github.com/conda-incubator/conda-store/pull/1086) ([@soapy1](https://github.com/soapy1)) + +DEPRECATIONS: +- [MAINT] Deprecate `/api/v1/environments/` [#1061](https://github.com/conda-incubator/conda-store/pull/1061) ([@peytondmurray](https://github.com/peytondmurray)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/conda-incubator/conda-store/graphs/contributors?from=2025-02-04&to=2025-02-11&type=c)) + +[@github-actions](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Agithub-actions+updated%3A2025-02-04..2025-02-11&type=Issues) | [@netlify](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Anetlify+updated%3A2025-02-04..2025-02-11&type=Issues) | [@peytondmurray](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Apeytondmurray+updated%3A2025-02-04..2025-02-11&type=Issues) | [@soapy1](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Asoapy1+updated%3A2025-02-04..2025-02-11&type=Issues) + +## [2025.2.1] - 2025-02-04 + +([full changelog](https://github.com/conda-incubator/conda-store/compare/2025.1.2-rc1...a6ce878082c7247989b0100990a47457bb807758)) + +### Merged PRs + +IMPROVEMENTS: +- [MAINT] Replace prettier with biome; fix bad pre-commit exclusion rule [#1075](https://github.com/conda-incubator/conda-store/pull/1075) ([@peytondmurray](https://github.com/peytondmurray)) +- Build and push docker images for multiple platforms in one step [#1067](https://github.com/conda-incubator/conda-store/pull/1067) ([@soapy1](https://github.com/soapy1)) + + +BUG FIXES: +- Undo migration to remove redundant channel column [#1071](https://github.com/conda-incubator/conda-store/pull/1071) ([@soapy1](https://github.com/soapy1)) +- Fix conda store server deps [#1069](https://github.com/conda-incubator/conda-store/pull/1069) ([@soapy1](https://github.com/soapy1)) +- [BUG] Fix bad invocation of CondaStoreError [#1080](https://github.com/conda-incubator/conda-store/pull/1080) ([@peytondmurray](https://github.com/peytondmurray)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/conda-incubator/conda-store/graphs/contributors?from=2025-01-31&to=2025-02-04&type=c)) + +[@peytondmurray](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Apeytondmurray+updated%3A2025-01-31..2025-02-04&type=Issues) | [@soapy1](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Asoapy1+updated%3A2025-01-31..2025-02-04&type=Issues) + +## [2025.1.1] - 2025-01-29 + +([full changelog](https://github.com/conda-incubator/conda-store/compare/2024.11.2...a965967d8678dd3a944cd359b96ff84402ea96a6)) + +### Merged PRs + +FEATURES: +- Sort envs returned by REST API by current build's scheduled_on time [#881](https://github.com/conda-incubator/conda-store/pull/881) ([@peytondmurray](https://github.com/peytondmurray)) + + +IMPROVEMENTS: +- Build and release a arm64 docker image as part of the release process [#1060](https://github.com/conda-incubator/conda-store/pull/1060) ([@soapy1](https://github.com/soapy1)) +- Ensure settings are retrieved for the right level [#1054](https://github.com/conda-incubator/conda-store/pull/1054) ([@soapy1](https://github.com/soapy1)) +- Fix user-journey tests for running against non-default targets [#1050](https://github.com/conda-incubator/conda-store/pull/1050) ([@soapy1](https://github.com/soapy1)) +- Get database url from conda-store config in tests [#1045](https://github.com/conda-incubator/conda-store/pull/1045) ([@soapy1](https://github.com/soapy1)) +- Add settings module [#1044](https://github.com/conda-incubator/conda-store/pull/1044) ([@soapy1](https://github.com/soapy1)) +- Add env var setting to disable ssl verification for user journey tests [#1040](https://github.com/conda-incubator/conda-store/pull/1040) ([@soapy1](https://github.com/soapy1)) +- Remove exception handler that prints to the console [#1037](https://github.com/conda-incubator/conda-store/pull/1037) ([@soapy1](https://github.com/soapy1)) +- Don't output db secret to logs [#1033](https://github.com/conda-incubator/conda-store/pull/1033) ([@soapy1](https://github.com/soapy1)) +- Bump nanoid from 3.3.7 to 3.3.8 in /docusaurus-docs [#1024](https://github.com/conda-incubator/conda-store/pull/1024) ([@dependabot](https://github.com/dependabot)) +- Separate conda-store app instance from config [#1023](https://github.com/conda-incubator/conda-store/pull/1023) ([@soapy1](https://github.com/soapy1)) +- fix below typo (bellow -> below) [#1020](https://github.com/conda-incubator/conda-store/pull/1020) ([@Adam-D-Lewis](https://github.com/Adam-D-Lewis)) +- Run unit tests on all supported python versions [#1015](https://github.com/conda-incubator/conda-store/pull/1015) ([@soapy1](https://github.com/soapy1)) +- Setup pluggy with locking plugin [#965](https://github.com/conda-incubator/conda-store/pull/965) ([@soapy1](https://github.com/soapy1)) + +BUG FIXES: +- Allow Namespace pydantic model to have a null metadata_ entry [#1063](https://github.com/conda-incubator/conda-store/pull/1063) ([@soapy1](https://github.com/soapy1)) +- Trim conda-store package dependencies [#1056](https://github.com/conda-incubator/conda-store/pull/1056) ([@soapy1](https://github.com/soapy1)) +- Update test_disk_usage for change in default `du` behaviour in ubuntu 24.04 [#1041](https://github.com/conda-incubator/conda-store/pull/1041) ([@soapy1](https://github.com/soapy1)) +- Login page without UI enabled [#1039](https://github.com/conda-incubator/conda-store/pull/1039) ([@soapy1](https://github.com/soapy1)) +- fix: handle None system metrics in user admin panel [#1029](https://github.com/conda-incubator/conda-store/pull/1029) ([@imprvhub](https://github.com/imprvhub)) +- Fix conda_package_build "build" column type [#1011](https://github.com/conda-incubator/conda-store/pull/1011) ([@soapy1](https://github.com/soapy1)) +- Remove redundant channel column [#1010](https://github.com/conda-incubator/conda-store/pull/1010) ([@soapy1](https://github.com/soapy1)) + +DEPRECATIONS: +- api: /registry, /build//docker +- config: CondaStoreServer.enable_registry, CondaStoreServer.registry_external_url, CondaStoreServer.container_registry_class, ContainerRegistry. +- Remove upload to docker registry code snippets [#1017](https://github.com/conda-incubator/conda-store/pull/1017) ([@soapy1](https://github.com/soapy1)) +- Deprecate docker registry [#1016](https://github.com/conda-incubator/conda-store/pull/1016) ([@soapy1](https://github.com/soapy1)) +- Remove docker build tasks [#1001](https://github.com/conda-incubator/conda-store/pull/1001) ([@soapy1](https://github.com/soapy1)) + +### Contributors to this release + +([GitHub contributors page for this release](https://github.com/conda-incubator/conda-store/graphs/contributors?from=2024-11-26&to=2025-01-29&type=c)) + +[@Adam-D-Lewis](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3AAdam-D-Lewis+updated%3A2024-11-26..2025-01-29&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Adependabot+updated%3A2024-11-26..2025-01-29&type=Issues) | [@github-actions](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Agithub-actions+updated%3A2024-11-26..2025-01-29&type=Issues) | [@imprvhub](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Aimprvhub+updated%3A2024-11-26..2025-01-29&type=Issues) | [@jaimergp](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Ajaimergp+updated%3A2024-11-26..2025-01-29&type=Issues) | [@netlify](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Anetlify+updated%3A2024-11-26..2025-01-29&type=Issues) | [@peytondmurray](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Apeytondmurray+updated%3A2024-11-26..2025-01-29&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Apre-commit-ci+updated%3A2024-11-26..2025-01-29&type=Issues) | [@rigzba21](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Arigzba21+updated%3A2024-11-26..2025-01-29&type=Issues) | [@soapy1](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Asoapy1+updated%3A2024-11-26..2025-01-29&type=Issues) | [@trallard](https://github.com/search?q=repo%3Aconda-incubator%2Fconda-store+involves%3Atrallard+updated%3A2024-11-26..2025-01-29&type=Issues) + ## [2024.11.2] - 2024-11-26 ([full changelog](https://github.com/conda-incubator/conda-store/compare/2024.11.1...710d61c86b6b8591672c4819328ebd11f9c3a917)) diff --git a/conda-store-server/conda_store_server/__init__.py b/conda-store-server/conda_store_server/__init__.py index b4f44d4e1..c8a5d338f 100644 --- a/conda-store-server/conda_store_server/__init__.py +++ b/conda-store-server/conda_store_server/__init__.py @@ -14,7 +14,7 @@ from .app import CondaStore -__version__ = "2024.11.3-dev" +__version__ = "2025.2.3-dev" CONDA_STORE_DIR = platformdirs.user_data_path(appname="conda-store") diff --git a/conda-store-server/conda_store_server/_internal/action/generate_constructor_installer.py b/conda-store-server/conda_store_server/_internal/action/generate_constructor_installer.py index 237eda3ff..e076e046b 100644 --- a/conda-store-server/conda_store_server/_internal/action/generate_constructor_installer.py +++ b/conda-store-server/conda_store_server/_internal/action/generate_constructor_installer.py @@ -129,7 +129,7 @@ def write_file(filename, s): """ if pip_dependencies: post_install += f""" -python -m pip install {' '.join(pip_dependencies)} +python -m pip install {" ".join(pip_dependencies)} """ # Writes files to disk diff --git a/conda-store-server/conda_store_server/_internal/alembic/versions/89637f546129_remove_conda_package_build_channel.py b/conda-store-server/conda_store_server/_internal/alembic/versions/89637f546129_remove_conda_package_build_channel.py deleted file mode 100644 index cfecde382..000000000 --- a/conda-store-server/conda_store_server/_internal/alembic/versions/89637f546129_remove_conda_package_build_channel.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright (c) conda-store development team. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -"""remove conda package build channel - -Revision ID: 89637f546129 -Revises: bf065abf375b -Create Date: 2024-12-04 13:09:25.562450 - -""" -from alembic import op -from sqlalchemy import Column, INTEGER, String, ForeignKey, table, select - - -# revision identifiers, used by Alembic. -revision = '89637f546129' -down_revision = 'bf065abf375b' -branch_labels = None -depends_on = None - -# Due to the issue fixed in https://github.com/conda-incubator/conda-store/pull/961 -# many conda_package_build entries have the wrong package entry (but the right channel). -# Because the packages are duplicated, we can not recreate the _conda_package_build_uc -# constraint without the channel_id. -# So, this function will go thru each conda_package_build and re-associate it with the -# correct conda_package based on the channel id. -def fix_misrepresented_packages(conn): - # conda_packages is a hash of channel-id_name_version to conda_package id - conda_packages = {} - - # dummy tables to run queries against - conda_package_build_table = table( - "conda_package_build", - Column("id", INTEGER), - Column("channel_id", INTEGER), - Column("package_id", INTEGER, ForeignKey("conda_package.id")), - ) - conda_package_table = table( - "conda_package", - Column("id", INTEGER), - Column("channel_id", INTEGER), - Column("name", String), - Column("version", String), - ) - - def get_conda_package_id(conn, channel_id, name, version): - hashed_name = f"{channel_id}-{name}-{version}" - - # if package exists in the conda_packages dict, return it - if hashed_name in conda_packages: - return conda_packages[hashed_name] - - # if not, then query the db for the package - package = conn.execute( - select(conda_package_table).where( - conda_package_table.c.channel_id == channel_id, - conda_package_table.c.name == name, - conda_package_table.c.version == version, - ) - ).first() - - # save the package into the conda_packages dict - conda_packages[hashed_name] = package.id - return package.id - - for row in conn.execute( - select( - conda_package_build_table.c.id, - conda_package_build_table.c.package_id, - conda_package_build_table.c.channel_id, - conda_package_table.c.name, - conda_package_table.c.version - ).join( - conda_package_build_table, - conda_package_build_table.c.package_id == conda_package_table.c.id - ) - ): - # the channel_id might already be empty - if row[2] is None: - continue - - package_id = get_conda_package_id(conn, row[2], row[3], row[4]) - # if found package id does not match the found package id, we'll need to updated it - if package_id != row[1]: - update_package_query = conda_package_build_table.update().where( - conda_package_build_table.c.id == op.inline_literal(row[0]) - ).values( - {"package_id": op.inline_literal(package_id)} - ) - conn.execute(update_package_query) - conn.commit() - -def upgrade(): - bind = op.get_bind() - - # So, go thru each conda_package_build and re-associate it with the correct conda_package - # based on the channel id. - fix_misrepresented_packages(bind) - - with op.batch_alter_table("conda_package_build") as batch_op: - # remove channel column from constraints - batch_op.drop_constraint( - "_conda_package_build_uc", - ) - - # re-add the constraint without the channel column - batch_op.create_unique_constraint( - "_conda_package_build_uc", - [ - "package_id", - "subdir", - "build", - "build_number", - "sha256", - ], - ) - - # remove channel column - batch_op.drop_column( - "channel_id", - ) - - -def downgrade(): - with op.batch_alter_table("conda_package_build") as batch_op: - # remove channel column from constraints - batch_op.drop_constraint( - constraint_name="_conda_package_build_uc", - ) - - # add channel column - batch_op.add_column( - Column("channel_id", INTEGER) - ) - - batch_op.create_foreign_key("fk_channel_id", "conda_channel", ["channel_id"], ["id"]) - - # re-add the constraint with the channel column - batch_op.create_unique_constraint( - constraint_name="_conda_package_build_uc", - columns=[ - "channel_id", - "package_id", - "subdir", - "build", - "build_number", - "sha256", - ], - ) diff --git a/conda-store-server/conda_store_server/_internal/orm.py b/conda-store-server/conda_store_server/_internal/orm.py index 32829fe0a..65cc7b841 100644 --- a/conda-store-server/conda_store_server/_internal/orm.py +++ b/conda-store-server/conda_store_server/_internal/orm.py @@ -562,7 +562,7 @@ def update_packages(self, db, subdirs=None): packages = {} for p_build in packages_data: - package_key = f'{p_build["name"]}-{p_build["version"]}-{self.id}' + package_key = f"{p_build['name']}-{p_build['version']}-{self.id}" # Filtering out : if the key already exists in existing_packages_keys, # then the package is already if DB, we don't add it. @@ -758,6 +758,7 @@ class CondaPackageBuild(Base): __table_args__ = ( UniqueConstraint( + "channel_id", "package_id", "subdir", "build", @@ -772,6 +773,14 @@ class CondaPackageBuild(Base): package_id: Mapped[int] = mapped_column(ForeignKey("conda_package.id")) package: Mapped["CondaPackage"] = relationship(back_populates="builds") + """ + Some package builds have the exact same data from different channels. + Thus, when adding a channel, populating CondaPackageBuild can encounter + duplicate keys errors. That's why we need to distinguish them by channel_id. + """ + channel_id: Mapped[int] = mapped_column(ForeignKey("conda_channel.id")) + channel: Mapped["CondaChannel"] = relationship(CondaChannel) + build: Mapped[str] = mapped_column(Unicode(64), index=True) build_number: Mapped[int] constrains: Mapped[dict] = mapped_column(JSON) diff --git a/conda-store-server/conda_store_server/_internal/schema.py b/conda-store-server/conda_store_server/_internal/schema.py index 471943c99..026905884 100644 --- a/conda-store-server/conda_store_server/_internal/schema.py +++ b/conda-store-server/conda_store_server/_internal/schema.py @@ -81,7 +81,7 @@ def from_list(cls, lst): class Namespace(BaseModel): id: int name: Annotated[str, StringConstraints(pattern=f"^[{ALLOWED_CHARACTERS}]+$")] # noqa: F722 - metadata_: Dict[str, Any] = None + metadata_: Optional[Dict[str, Any]] = None role_mappings: List[NamespaceRoleMapping] = [] model_config = ConfigDict(from_attributes=True) @@ -104,7 +104,13 @@ class BuildArtifactType(str, enum.Enum): CONDA_PACK = "CONDA_PACK" DOCKER_MANIFEST = "DOCKER_MANIFEST" CONSTRUCTOR_INSTALLER = "CONSTRUCTOR_INSTALLER" - _ = "CONTAINER_REGISTRY" + + # Deprecated + # Old database may still have docker or container build artifacts. + # So, these enum values must stay in order to remain backwards compatible + # however, no new artifacts of these types should be created. + CONTAINER_REGISTRY = "CONTAINER_REGISTRY" + DOCKER_BLOB = "DOCKER_BLOB" class BuildStatus(enum.Enum): diff --git a/conda-store-server/conda_store_server/_internal/server/app.py b/conda-store-server/conda_store_server/_internal/server/app.py index c6246d703..dfed8b89a 100644 --- a/conda-store-server/conda_store_server/_internal/server/app.py +++ b/conda-store-server/conda_store_server/_internal/server/app.py @@ -84,7 +84,9 @@ class CondaStoreServer(Application): ) enable_registry = Bool( - True, help="enable the docker registry for conda-store", config=True + False, + help="(deprecated) enable the docker registry for conda-store", + config=True, ) enable_metrics = Bool( @@ -101,7 +103,7 @@ class CondaStoreServer(Application): registry_external_url = Unicode( "localhost:8080", - help='external hostname and port to access docker registry cannot contain "http://" or "https://"', + help='(deprecated) external hostname and port to access docker registry cannot contain "http://" or "https://"', config=True, ) @@ -253,6 +255,13 @@ async def conda_store_middleware(request: Request, call_next): request.state.authentication = self.authentication request.state.templates = self.templates response = await call_next(request) + + # Handle requests that are sent to deprecated endpoints; + # see conda_store_server._internal.server.views.api.deprecated + # for additional information + if hasattr(request.state, "deprecation_date"): + response.headers["Deprecation"] = "True" + response.headers["Sunset"] = request.state.deprecation_date return response @app.exception_handler(HTTPException) @@ -370,9 +379,7 @@ def _check_worker(self, delay=5): with session_factory() as db: q = db.query(orm.Worker).first() if q is not None and q.initialized: - self.log.info( - f"{_Color.GREEN}" "Worker initialized" f"{_Color.RESET}" - ) + self.log.info(f"{_Color.GREEN}Worker initialized{_Color.RESET}") break time.sleep(delay) diff --git a/conda-store-server/conda_store_server/_internal/server/views/api.py b/conda-store-server/conda_store_server/_internal/server/views/api.py index 81569081b..ac51a78ab 100644 --- a/conda-store-server/conda_store_server/_internal/server/views/api.py +++ b/conda-store-server/conda_store_server/_internal/server/views/api.py @@ -3,7 +3,8 @@ # license that can be found in the LICENSE file. import datetime -from typing import Any, Dict, List, Optional +from functools import wraps +from typing import Any, Callable, Dict, List, Optional import pydantic import yaml @@ -105,6 +106,50 @@ def paginated_api_response( } +def deprecated(sunset_date: datetime.date) -> Callable: + """Add deprecation headers to a HTTP request and response. + + This will include the deprecation date on the request object, + which will then be picked up by the conda_store_middleware + to add the deprecation date to the response object. + + See the conda-store backwards compatibility policy for appropriate use of + deprecations https://conda.store/community/policies/backwards-compatibility. + + Note that decorated functions _must_ include the request parameter. + This is not an elegant way of doing this, but FastAPI has no other + way of achieving the same effect without confusing its request/response + inference machinery, which relies on the type annotations of the + routes. + + Parameters + ---------- + sunset_date : datetime.date + the date that the endpoint will have it's functionality removed + + Returns + ------- + Callable + Decorator which wraps an endpoint + """ + + def decorator(func): + @wraps(func) + async def add_deprecated_headers(request: Request, *args, **kwargs): + # It's not possible to add the deprecation headers to the + # output of `func(*args, **kwargs)`, since that may be a + # simple dict object, not a Response + request.state.deprecation_date = sunset_date.strftime( + "%a, %d %b %Y 00:00:00 UTC" + ) + result = await func(*args, request=request, **kwargs) + return result + + return add_deprecated_headers + + return decorator + + @router_api.get( "/", response_model=schema.APIGetStatus, @@ -603,10 +648,11 @@ async def api_delete_namespace( @router_api.get( - "/environment/", - response_model=schema.APIListEnvironment, + "/environment/", response_model=schema.APIListEnvironment, deprecated=True ) +@deprecated(sunset_date=datetime.date(2025, 3, 17)) async def api_list_environments_v1( + request: Request, auth: Authentication = Depends(dependencies.get_auth), conda_store: CondaStore = Depends(dependencies.get_conda_store), entity: AuthenticationToken = Depends(dependencies.get_entity), @@ -1327,14 +1373,14 @@ async def api_get_build_archive( @router_api.get("/build/{build_id}/docker/", deprecated=True) +@deprecated(sunset_date=datetime.date(2025, 3, 17)) async def api_get_build_docker_image_url( - build_id: int, request: Request, + build_id: int, conda_store=Depends(dependencies.get_conda_store), server=Depends(dependencies.get_server), auth=Depends(dependencies.get_auth), ): - response_headers = {"Deprecation": "True"} with conda_store.get_db() as db: build = api.get_build(db, build_id) auth.authorize_request( @@ -1346,7 +1392,7 @@ async def api_get_build_docker_image_url( if build.has_docker_manifest: url = f"{server.registry_external_url}/{build.environment.namespace.name}/{build.environment.name}:{build.build_key}" - return PlainTextResponse(url, headers=response_headers) + return PlainTextResponse(url) else: content = { @@ -1354,7 +1400,8 @@ async def api_get_build_docker_image_url( "message": f"Build {build_id} doesn't have a docker manifest", } return JSONResponse( - status_code=400, content=content, headers=response_headers + status_code=400, + content=content, ) diff --git a/conda-store-server/conda_store_server/_internal/server/views/registry.py b/conda-store-server/conda_store_server/_internal/server/views/registry.py index 92e01da91..2a8f079e2 100644 --- a/conda-store-server/conda_store_server/_internal/server/views/registry.py +++ b/conda-store-server/conda_store_server/_internal/server/views/registry.py @@ -2,6 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +import datetime import json import time @@ -11,6 +12,7 @@ from conda_store_server import api from conda_store_server._internal import orm, schema from conda_store_server._internal.server import dependencies +from conda_store_server._internal.server.views.api import deprecated from conda_store_server.server.schema import Permissions router_registry = APIRouter(tags=["registry"]) @@ -77,7 +79,10 @@ def replace_words(s, words): return environment_name -def get_docker_image_manifest(conda_store, image, tag, timeout=10 * 60): +@deprecated(sunset_date=datetime.date(2025, 3, 17)) +async def get_docker_image_manifest( + conda_store, image, tag, request: Request, timeout=10 * 60 +): namespace, *image_name = image.split("/") # /v2//manifest/ @@ -127,13 +132,15 @@ def get_docker_image_manifest(conda_store, image, tag, timeout=10 * 60): return RedirectResponse(conda_store.storage.get_url(manifests_key)) -def get_docker_image_blob(conda_store, image, blobsum): +@deprecated(sunset_date=datetime.date(2025, 3, 17)) +async def get_docker_image_blob(conda_store, image, blobsum, request: Request): blob_key = f"docker/blobs/{blobsum}" return RedirectResponse(conda_store.storage.get_url(blob_key)) -@router_registry.get("/v2/") -def v2( +@router_registry.get("/v2/", deprecated=True) +@deprecated(sunset_date=datetime.date(2025, 3, 17)) +async def v2( request: Request, entity=Depends(dependencies.get_entity), ): @@ -143,10 +150,9 @@ def v2( return _json_response({}) -@router_registry.get( - "/v2/{rest:path}", -) -def list_tags( +@router_registry.get("/v2/{rest:path}", deprecated=True) +@deprecated(sunset_date=datetime.date(2025, 3, 17)) +async def list_tags( rest: str, request: Request, conda_store=Depends(dependencies.get_conda_store), @@ -183,8 +189,8 @@ def list_tags( # /v2//manifests/ elif parts[-2] == "manifests": tag = parts[-1] - return get_docker_image_manifest(conda_store, image, tag) + return get_docker_image_manifest(conda_store, image, tag, request) # /v2//blobs/ elif parts[-2] == "blobs": blobsum = parts[-1].split(":")[1] - return get_docker_image_blob(conda_store, image, blobsum) + return get_docker_image_blob(conda_store, image, blobsum, request) diff --git a/conda-store-server/conda_store_server/_internal/utils.py b/conda-store-server/conda_store_server/_internal/utils.py index 43c665042..b572c2890 100644 --- a/conda-store-server/conda_store_server/_internal/utils.py +++ b/conda-store-server/conda_store_server/_internal/utils.py @@ -172,7 +172,7 @@ def compile_arn_sql_like( match = allowed_regex.match(arn) if match is None: raise ValueError( - "Could not find a match for the requested " f"namespace/environment={arn}" + f"Could not find a match for the requested namespace/environment={arn}" ) return ( diff --git a/conda-store-server/conda_store_server/conda_store_config.py b/conda-store-server/conda_store_server/conda_store_config.py index 21e4d61b8..2c8f82c31 100644 --- a/conda-store-server/conda_store_server/conda_store_config.py +++ b/conda-store-server/conda_store_server/conda_store_config.py @@ -19,8 +19,8 @@ ) from traitlets.config import LoggingConfigurable -from conda_store_server import CONDA_STORE_DIR, BuildKey, api, storage -from conda_store_server._internal import conda_utils, environment, schema, utils +from conda_store_server import CONDA_STORE_DIR, BuildKey, api, exception, storage +from conda_store_server._internal import conda_utils, environment, schema from conda_store_server.server import schema as auth_schema @@ -59,7 +59,7 @@ def conda_store_validate_action( auth_schema.Permissions.ENVIRONMENT_CREATE, auth_schema.Permissions.ENVIRONMENT_UPDATE, ) and (storage_threshold > system_metrics.disk_free): - raise utils.CondaStoreError( + raise exception.CondaStoreError( f"`CondaStore.storage_threshold` reached. Action {action.value} prevented due to insufficient storage space" ) diff --git a/conda-store-server/pyproject.toml b/conda-store-server/pyproject.toml index b3812f117..fc3269a8f 100644 --- a/conda-store-server/pyproject.toml +++ b/conda-store-server/pyproject.toml @@ -52,7 +52,6 @@ dependencies = [ "celery", "fastapi", "filelock", - "flower", "itsdangerous", "jinja2", "pluggy", @@ -94,6 +93,7 @@ dependencies = [ "build", "docker-compose", "docker-py<7", + "flower", "httpx", "pre-commit", "pytest", diff --git a/conda-store-server/tests/_internal/alembic/version/test_89637f546129_remove_conda_package_build_channel.py b/conda-store-server/tests/_internal/alembic/version/test_89637f546129_remove_conda_package_build_channel.py deleted file mode 100644 index 35c8d7b8e..000000000 --- a/conda-store-server/tests/_internal/alembic/version/test_89637f546129_remove_conda_package_build_channel.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) conda-store development team. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -import pytest -from sqlalchemy import text -from sqlalchemy.exc import OperationalError - -from conda_store_server import api -from conda_store_server._internal import orm - - -def setup_bad_data_db(conda_store): - """A database fixture populated with - * 2 channels - * 2 conda packages - * 5 conda package builds - """ - with conda_store.session_factory() as db: - # create test channels - api.create_conda_channel(db, "test-channel-1") - api.create_conda_channel(db, "test-channel-2") - db.commit() - - # create some sample conda_package's - # For simplicity, the channel_id's match the id of the conda_package. - # So, when checking that the package build entries have been reassembled - # the right way, check that the package_id in the conda_package_build is - # equal to what would have been the channel_id (before the migration is run) - conda_package_records = [ - { - "id": 1, - "channel_id": 1, - "name": "test-package-1", - "version": "1.0.0", - }, - { - "id": 2, - "channel_id": 2, - "name": "test-package-1", - "version": "1.0.0", - }, - ] - for cpb in conda_package_records: - conda_package = orm.CondaPackage(**cpb) - db.add(conda_package) - db.commit() - - # create some conda_package_build's - conda_package_builds = [ - { - "id": 1, - "build": "py310h06a4308_0", - "package_id": 1, - "build_number": 0, - "sha256": "one", - "subdir": "linux-64", - }, - { - "id": 2, - "build": "py311h06a4308_0", - "package_id": 1, - "build_number": 0, - "sha256": "two", - "subdir": "linux-64", - }, - { - "id": 3, - "build": "py38h06a4308_0", - "package_id": 1, - "build_number": 0, - "sha256": "three", - "subdir": "linux-64", - }, - { - "id": 4, - "build": "py39h06a4308_0", - "package_id": 2, - "build_number": 0, - "sha256": "four", - "subdir": "linux-64", - }, - { - "id": 5, - "build": "py310h06a4308_0", - "package_id": 2, - "build_number": 0, - "sha256": "five", - "subdir": "linux-64", - }, - ] - default_values = { - "depends": "", - "md5": "", - "timestamp": 0, - "constrains": "", - "size": 0, - } - for cpb in conda_package_builds: - conda_package = orm.CondaPackageBuild(**cpb, **default_values) - db.add(conda_package) - db.commit() - - # force in some channel data - # conda_package_build 1 should have package_id 2 after migration - db.execute(text("UPDATE conda_package_build SET channel_id=2 WHERE id=1")) - # conda_package_build 2 should have package_id 1 after migration - db.execute(text("UPDATE conda_package_build SET channel_id=1 WHERE id=2")) - # conda_package_build 3 should have package_id 1 after migration - db.execute(text("UPDATE conda_package_build SET channel_id=1 WHERE id=3")) - # conda_package_build 4 should have package_id 2 after migration - db.execute(text("UPDATE conda_package_build SET channel_id=2 WHERE id=4")) - - # don't set conda_package_build 5 channel_id as a test case - # conda_package_build 5 package_id should be unchanged (2) after migration - - db.commit() - - -def test_remove_conda_package_build_channel_basic( - conda_store, alembic_config, alembic_runner -): - """Simply run the upgrade and downgrade for this migration""" - # migrate all the way to the target revision - alembic_runner.migrate_up_to("89637f546129") - - # try downgrading - alembic_runner.migrate_down_one() - - # ensure the channel_id column exists, will error if channel_id column does not exist - with conda_store.session_factory() as db: - db.execute(text("SELECT channel_id from conda_package_build")) - - # try upgrading once more - alembic_runner.migrate_up_one() - - # ensure the channel_id column exists, will error if channel_id column does not exist - with conda_store.session_factory() as db: - with pytest.raises(OperationalError): - db.execute(text("SELECT channel_id from conda_package_build")) - - -def test_remove_conda_package_build_bad_data( - conda_store, alembic_config, alembic_runner -): - """Simply run the upgrade and downgrade for this migration""" - # migrate all the way to the target revision - alembic_runner.migrate_up_to("89637f546129") - - # try downgrading - alembic_runner.migrate_down_one() - - # ensure the channel_id column exists, will error if channel_id column does not exist - with conda_store.session_factory() as db: - db.execute(text("SELECT channel_id from conda_package_build")) - - # seed db with data that has broken data - setup_bad_data_db(conda_store) - - # try upgrading once more - alembic_runner.migrate_up_one() - - # ensure the channel_id column exists, will error if channel_id column does not exist - with conda_store.session_factory() as db: - with pytest.raises(OperationalError): - db.execute(text("SELECT channel_id from conda_package_build")) - - # ensure all packages builds have the right package associated - with conda_store.session_factory() as db: - build = ( - db.query(orm.CondaPackageBuild) - .filter(orm.CondaPackageBuild.id == 1) - .first() - ) - assert build.package_id == 2 - - build = ( - db.query(orm.CondaPackageBuild) - .filter(orm.CondaPackageBuild.id == 2) - .first() - ) - assert build.package_id == 1 - - build = ( - db.query(orm.CondaPackageBuild) - .filter(orm.CondaPackageBuild.id == 3) - .first() - ) - assert build.package_id == 1 - - build = ( - db.query(orm.CondaPackageBuild) - .filter(orm.CondaPackageBuild.id == 4) - .first() - ) - assert build.package_id == 2 - - build = ( - db.query(orm.CondaPackageBuild) - .filter(orm.CondaPackageBuild.id == 5) - .first() - ) - assert build.package_id == 2 diff --git a/conda-store-server/tests/_internal/server/views/test_api.py b/conda-store-server/tests/_internal/server/views/test_api.py index c9af85e6d..dc968c537 100644 --- a/conda-store-server/tests/_internal/server/views/test_api.py +++ b/conda-store-server/tests/_internal/server/views/test_api.py @@ -3,6 +3,7 @@ # license that can be found in the LICENSE file. import contextlib +import datetime import json import os import sys @@ -12,6 +13,7 @@ import pytest import traitlets import yaml +from fastapi import Request from fastapi.testclient import TestClient from conda_store_server import CONDA_STORE_DIR, __version__ @@ -46,6 +48,26 @@ def mock_get_entity(): testclient.app.dependency_overrides = {} +def test_deprecation_warning(testclient): + from fastapi.responses import JSONResponse + + from conda_store_server._internal.server.views.api import deprecated + + router = testclient.app.router + + @router.get("/foo") + @deprecated(datetime.date(2024, 12, 17)) + async def api_status(request: Request): + return JSONResponse( + status_code=400, + content={"ok": "ok"}, + ) + + result = testclient.get("/foo") + assert result.headers.get("Deprecation") == "True" + assert result.headers.get("Sunset") == "Tue, 17 Dec 2024 00:00:00 UTC" + + def test_api_version_unauth(testclient): response = testclient.get("/api/v1/") response.raise_for_status() @@ -157,6 +179,23 @@ def test_api_list_namespace_auth(testclient, seed_conda_store, authenticate): assert sorted([_.name for _ in r.data]) == ["default", "namespace1", "namespace2"] +def test_api_list_namespace_including_missing_metadata_( + testclient, seed_namespace_with_edge_cases, authenticate +): + """Test that a namespace with metadata_ = None can be retrieved. + + See https://github.com/conda-incubator/conda-store/issues/1062 + for additional context. + """ + response = testclient.get("api/v1/namespace") + response.raise_for_status() + + r = schema.APIListNamespace.model_validate(response.json()) + assert r.status == schema.APIStatus.OK + namespaces = [_.name for _ in r.data] + assert "namespace_missing_meta" in namespaces + + def test_api_get_namespace_unauth(testclient, seed_conda_store): response = testclient.get("api/v1/namespace/default") response.raise_for_status() @@ -222,6 +261,8 @@ def test_api_list_environments_auth( r = model.model_validate(response.json()) assert r.status == schema.APIStatus.OK + if version == "v1": + assert response.headers.get("Deprecation") assert sorted([_.name for _ in r.data]) == ["name1", "name2", "name3", "name4"] @@ -1171,7 +1212,6 @@ def test_api_list_environments_paginate( cursor = None cursor_param = "" while cursor is None or cursor != Cursor.end(): - # breakpoint() response = testclient.get(f"api/v2/environment/?limit={limit}{cursor_param}") response.raise_for_status() diff --git a/conda-store-server/tests/_internal/test_orm.py b/conda-store-server/tests/_internal/test_orm.py index ac2e45a73..d35601be2 100644 --- a/conda-store-server/tests/_internal/test_orm.py +++ b/conda-store-server/tests/_internal/test_orm.py @@ -53,6 +53,7 @@ def populated_db(db): 1, { "build": "py310h06a4308_0", + "channel_id": 1, "build_number": 0, "sha256": "11f080b53b36c056dbd86ccd6dc56c40e3e70359f64279b1658bb69f91ae726f", "subdir": "linux-64", @@ -67,6 +68,7 @@ def populated_db(db): 1, { "build": "py311h06a4308_0", + "channel_id": 1, "build_number": 0, "sha256": "f0719ee6940402a1ea586921acfaf752fda977dbbba74407856a71ba7f6c4e4a", "subdir": "linux-64", @@ -81,6 +83,7 @@ def populated_db(db): 1, { "build": "py38h06a4308_0", + "channel_id": 1, "build_number": 0, "sha256": "39e39a23baebd0598c1b59ae0e82b5ffd6a3230325da4c331231d55cbcf13b3e", "subdir": "linux-64", @@ -217,13 +220,14 @@ def test_update_packages_add_existing_pkg_new_version( assert count == 2 count = populated_db.query(orm.CondaPackage).count() assert count == 3 - num_builds = ( + builds = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 1) - .count() + .filter(orm.CondaPackageBuild.channel_id == 1) + .all() ) - assert num_builds == 4 + assert len(builds) == 4 + for b in builds: + assert b.package.channel_id == 1 count = populated_db.query(orm.CondaPackageBuild).count() assert count == 4 @@ -291,13 +295,14 @@ def test_update_packages_multiple_subdirs(mock_repdata, populated_db): .count() ) assert count == 2 - num_builds = ( + builds = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 1) - .count() + .filter(orm.CondaPackageBuild.channel_id == 1) + .all() ) - assert num_builds == 5 + assert len(builds) == 5 + for b in builds: + assert b.package.channel_id == 1 @mock.patch("conda_store_server._internal.conda_utils.download_repodata") @@ -321,15 +326,13 @@ def check_packages(): assert len(conda_packages) == 1 conda_packages = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 1) + .filter(orm.CondaPackageBuild.channel_id == 1) .all() ) assert len(conda_packages) == 4 conda_packages = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 2) + .filter(orm.CondaPackageBuild.channel_id == 2) .all() ) assert len(conda_packages) == 0 @@ -368,13 +371,14 @@ def test_update_packages_new_package_channel(mock_repdata, populated_db, test_re .count() ) assert count == 2 - num_builds = ( + builds = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 2) - .count() + .filter(orm.CondaPackageBuild.channel_id == 2) + .all() ) - assert num_builds == 1 + assert len(builds) == 1 + for b in builds: + assert b.package.channel_id == 2 count = populated_db.query(orm.CondaPackageBuild).count() assert count == 4 @@ -401,10 +405,49 @@ def test_update_packages_multiple_builds( # ensure it is added to conda package builds count = populated_db.query(orm.CondaPackageBuild).count() assert count == 5 - num_builds = ( + builds = ( populated_db.query(orm.CondaPackageBuild) - .join(orm.CondaPackage) - .filter(orm.CondaPackage.channel_id == 2) - .count() + .filter(orm.CondaPackageBuild.channel_id == 2) + .all() + ) + assert len(builds) == 2 + for b in builds: + assert b.package.channel_id == 2 + + +@mock.patch("conda_store_server._internal.conda_utils.download_repodata") +def test_update_packages_channel_consistency( + mock_repdata, populated_db, test_repodata_multiple_packages +): + mock_repdata.return_value = test_repodata_multiple_packages + + channel = ( + populated_db.query(orm.CondaChannel).filter(orm.CondaChannel.id == 2).first() + ) + channel.update_packages(populated_db, "linux-64") + + # ensure the package builds end up with the correct channel + builds = ( + populated_db.query(orm.CondaPackageBuild) + .filter(orm.CondaPackageBuild.channel_id == 2) + .all() + ) + for b in builds: + assert b.channel_id == 2 + assert b.package.channel_id == 2 + + # again with another channel + channel = ( + populated_db.query(orm.CondaChannel).filter(orm.CondaChannel.id == 1).first() + ) + channel.update_packages(populated_db, "linux-64") + + # ensure the package builds end up with the correct channel + builds = ( + populated_db.query(orm.CondaPackageBuild) + .filter(orm.CondaPackageBuild.channel_id == 1) + .all() ) - assert num_builds == 2 + for b in builds: + assert b.channel_id == 1 + assert b.package.channel_id == 1 diff --git a/conda-store-server/tests/_internal/test_schema.py b/conda-store-server/tests/_internal/test_schema.py index 92067d3aa..487febf47 100644 --- a/conda-store-server/tests/_internal/test_schema.py +++ b/conda-store-server/tests/_internal/test_schema.py @@ -2,6 +2,8 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +import datetime + import pytest from conda_store_server._internal import schema @@ -62,3 +64,36 @@ def test_parse_lockfile_obj(test_lockfile): } specification = schema.LockfileSpecification.model_validate(lockfile_spec) assert specification.model_dump()["lockfile"] == test_lockfile + + +@pytest.mark.parametrize( + ("build"), + [ + { + "id": 1, + "environment_id": 1, + "status": "BUILDING", + "scheduled_on": datetime.datetime.now(), + "size": 123, + }, + { + "id": 2, + "environment_id": 1, + "status": "BUILDING", + "scheduled_on": datetime.datetime.now(), + "size": 123, + "build_artifacts": [ + {"id": 1, "artifact_type": "YAML", "key": "not_a_real_key"}, + {"id": 2, "artifact_type": "DOCKER_BLOB", "key": "not_a_real_key"}, + { + "id": 3, + "artifact_type": "CONTAINER_REGISTRY", + "key": "not_a_real_key", + }, + ], + }, + ], +) +def test_parse_build(build): + output = schema.Build.model_validate(build).model_dump() + assert output is not None diff --git a/conda-store-server/tests/conftest.py b/conda-store-server/tests/conftest.py index edb7e738f..a24459611 100644 --- a/conda-store-server/tests/conftest.py +++ b/conda-store-server/tests/conftest.py @@ -417,6 +417,18 @@ def alembic_config(conda_store): return {"file": ini_file} +@pytest.fixture +def seed_namespace_with_edge_cases(db: Session, conda_store): + namespaces = [ + orm.Namespace(name="normal_namespace"), + orm.Namespace(name="namespace_missing_meta", metadata_=None), + ] + + for namespace in namespaces: + db.add(namespace) + db.commit() + + def _seed_conda_store( db: Session, conda_store, diff --git a/conda-store-server/tests/user_journeys/utils/api_utils.py b/conda-store-server/tests/user_journeys/utils/api_utils.py index fb83a5a8e..c00a8d9c8 100644 --- a/conda-store-server/tests/user_journeys/utils/api_utils.py +++ b/conda-store-server/tests/user_journeys/utils/api_utils.py @@ -284,7 +284,7 @@ def get_builds( if namespace: query_params.append(f"namespace={namespace}") - return self._make_request(f'api/v1/build/?{"&".join(query_params)}').json()[ + return self._make_request(f"api/v1/build/?{'&'.join(query_params)}").json()[ "data" ] diff --git a/conda-store/conda_store/__init__.py b/conda-store/conda_store/__init__.py index facc3fc55..579c752a3 100644 --- a/conda-store/conda_store/__init__.py +++ b/conda-store/conda_store/__init__.py @@ -2,4 +2,4 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -__version__ = "2024.11.3-dev" +__version__ = "2025.2.3-dev" diff --git a/docker-compose.yaml b/docker-compose.yaml index 760ef93ed..c3340792c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,6 +9,7 @@ services: volumes: - ./tests/assets/environments:/opt/environments:ro - ./tests/assets/conda_store_config.py:/opt/conda_store/conda_store_config.py:ro + - ./conda-store-server:/opt/conda-store-server:ro depends_on: conda-store-server: condition: service_healthy @@ -36,6 +37,7 @@ services: condition: service_healthy volumes: - ./tests/assets/conda_store_config.py:/opt/conda_store/conda_store_config.py:ro + - ./conda-store-server:/opt/conda-store-server:ro healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8080/conda-store/api/v1/"] diff --git a/docusaurus-docs/babel.config.js b/docusaurus-docs/babel.config.js index bfd75dbdf..f8c7bb99e 100644 --- a/docusaurus-docs/babel.config.js +++ b/docusaurus-docs/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: [require.resolve("@docusaurus/core/lib/babel/preset")], + presets: [require.resolve("@docusaurus/core/lib/babel/preset")], }; diff --git a/docusaurus-docs/community/policies/backwards-compatibility.md b/docusaurus-docs/community/policies/backwards-compatibility.md index f243f31b7..a91194150 100644 --- a/docusaurus-docs/community/policies/backwards-compatibility.md +++ b/docusaurus-docs/community/policies/backwards-compatibility.md @@ -139,42 +139,57 @@ get community feedback. #### Removing versions of API endpoints -It is not recommended to remove versions of API endpoints. Removing API -endpoints, or versions of endpoints, breaks backwards compatibility and should -only be done under exceptional circumstances such as a security vulnerability. +Removal of an API endpoint is sometimes necessary. Deprecation notices and removals will +always be documented in the [conda-store release notes](https://github.com/conda-incubator/conda-store/blob/main/CHANGELOG.md). Further, documentation should be updated to reflect these changes. This should include: +- version number of the release where the deprecation was introduced +- provide suggestions for alternatives (if possible) +- provide justification for the removal (such as a link to the issue + or CVE that necessitated the removal). -If the desire is to prevent a developer from relying on an API endpoint, adding -a warning to the API documentation along with a recommended alternative should -be used rather than a deprecation or removal. +In order to make these kind of breaking changes responsibly, follow the steps +outlined below. -In the case of a removed endpoint, or endpoint version, conda-store should -return a status code of `410 Gone` to indicate the endpoint has been removed -along with a json object stating when and why the endpoint was removed and what -version of the endpoint is available currently (if any). +##### 1. Deprecate the endpoint -```python +In this stage, conda-store should not be introducing a breaking change. The +deprecation step communicates to users that the endpoint is marked for removal +and will be removed in a future release. + +:::info[For vulnerabilities] + +In the case of a serious security vulnerability, conda-store may skip the deprecation +step and remove the endpoint immediately. + +::: + +Deprecations in the REST API follow the +form outlined by the [deprecation header RFC](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-deprecation-header-02). To deprecate an endpoint add the following +response headers to the endpoint + +```json { - # the pull request that removed the endpoint - "reference_pull_request": "https://github.com/conda-incubator/conda-store/pull/0000", - # the date the endpoint was removed - "removal_date": "2021-06-24", - # the reason for the removal, ideally with a link to a CVE if one is available - "removal_reason": "Removed to address CVE-2021-32677 (https://nvd.nist.gov/vuln/detail/CVE-2021-32677)", - # the endpoint that developers should use as a replacement - "new_endpoint": "api/v3/this/should/be/used/instead", + "Deprecation": "True", + "Sunset": } ``` -If an API endpoint must be deprecated, a deprecation warning should be added -for at least one release before the endpoint is removed. This requirement may -be waived in the case of a serious security vulnerability. +The "removal date" should be specified as a [HTTP-Date](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date). -It should always be clearly communicated in release notes and documentation -when an API endpoint is deprecated or removed. This should include: -- version number of the release where this was deprecated -- provide suggestions for alternatives (if possible) -- provide justification for the removal (such as a link to the issue - or CVE that necessitated the removal). +###### Choosing a deprecation date + +The `Sunset` date indicates the date after which conda-store will remove the functionality +of this endpoint. +* Any time before this date, users should expect this endpoint to work +* Any time after this date, the endpoint may still be available (due to no release having gone out), but users should expect that this endpoint will be removed + +Since the conda-store project users CalVer, the `Sunset` date should be set to at least 2 months +from the release that the deprecation notice first appears in. + +##### 2. Remove the endpoint + +Once we have reached the `Sunset` date from step (1), the API endpoint may be fully +removed. At this stage, users should expect to recieve a `404 Not Found` error for the +endpoint. ### Python API @@ -301,3 +316,14 @@ object like a dict. These are considered non-breaking. Public variables should not have their type changed. Public constants should not have their type or their value changed. + +##### Configuration elements + +Deprecated configuration elements will be marked with a `deprecation` note +in the docs and the CLI `--help-all` output. For example: +... +--CondaStoreServer.enable_registry + (deprecated) enable the docker registry for conda-store + Default: False +... +``` diff --git a/docusaurus-docs/conda-store/explanations/artifacts.md b/docusaurus-docs/conda-store/explanations/artifacts.md index 39a52901a..19564713a 100644 --- a/docusaurus-docs/conda-store/explanations/artifacts.md +++ b/docusaurus-docs/conda-store/explanations/artifacts.md @@ -63,10 +63,11 @@ Click **"Download archive"** button to download the archive of your conda enviro To install the tarball, follow the [instructions for the target machine in the conda-pack documentation][conda-pack-usage]. -## Docker images +## Docker images (deprecated) -:::warning -Docker image creation is currently not supported. +:::warning[Deprecation notice] +Docker image creation is currently not supported. This feature is scheduled to be +removed March 17, 2025. ::: ### Authentication diff --git a/docusaurus-docs/conda-store/references/configuration-options.md b/docusaurus-docs/conda-store/references/configuration-options.md index fb2af9c68..5b81c9d25 100644 --- a/docusaurus-docs/conda-store/references/configuration-options.md +++ b/docusaurus-docs/conda-store/references/configuration-options.md @@ -4,7 +4,7 @@ description: conda-store configuration options # Configuration options -### Traitlets +## Traitlets :::warning This page is in active development, content may be inaccurate and incomplete. @@ -24,7 +24,7 @@ conda-store-server --config conda-store-worker --config ``` -### Data directory +## Data directory The `CONDA_STORE_DIR` Python variable specifies the conda-store data directory, which is used by some of the configuration options mentioned below, like @@ -49,7 +49,7 @@ Please use the conda-store configuration options mentioned below instead. [platformdirs]: https://github.com/platformdirs/platformdirs -### `conda_store_server._internal.app.CondaStore` +## `conda_store_server._internal.app.CondaStore` `CondaStore.storage_class` configures the storage backend to use for storing build artifacts from @@ -210,15 +210,17 @@ setting is useful if you want to protect environments from modification from certain users and groups. Note: this configuration option is not supported on Windows. -`CondaStore.serialize_builds` DEPRECATED no longer has any effect - `CondaStore.post_update_environment_build_hook` is an optional configurable to allow for custom behavior that will run after an environment's current build changes. `CondaStore.lock_backend` is the name of the default lock plugin to use when locking a conda environment. By default, conda-store uses [conda-lock](https://github.com/conda/conda-lock). -### `conda_store_server.storage.S3Storage` +### Deprecated configuration options for `conda_store_server._internal.app.CondaStore` + +`CondaStore.serialize_builds` no longer has any effect + +## `conda_store_server.storage.S3Storage` conda-store uses [minio-py](https://github.com/minio/minio-py) as a client to connect to S3 "like" object stores. @@ -264,7 +266,7 @@ credentials class. `S3Storage.credentials_kwargs` keyword arguments to pass for creation of credentials class. -### `conda_store_server.storage.LocalStorage` +## `conda_store_server.storage.LocalStorage` `LocalStorage.storage_path` is the base directory to use for storing build artifacts. @@ -273,7 +275,7 @@ build artifacts. artifacts. This url assumes that the base will be a static server serving `LocalStorage.storage_path`. -### `conda_store_server.server.auth.AuthenticationBackend` +## `conda_store_server.server.auth.AuthenticationBackend` `AuthenticationBackend.secret` is the symmetric secret to use for encrypting tokens. @@ -287,7 +289,7 @@ in a similar manner to how things are done with jupyterhub. Format for the values is a dictionary with keys being the tokens and values being the `schema.AuthenticaitonToken` all fields are optional. -### `conda_store_server.server.auth.AuthorizationBackend` +## `conda_store_server.server.auth.AuthorizationBackend` `AuthorizationBackend.role_mappings` is a dictionary that maps `roles` to application `permissions`. There are three default roles at the @@ -300,12 +302,14 @@ bindings that an unauthenticated user assumes. `AuthorizationBackend.authenticated_role_bindings` are the base role bindings that an authenticated user assumes. -### `conda_store_server.server.auth.Authentication` +## `conda_store_server.server.auth.Authentication` `Authentication.cookie_name` is the name for the browser cookie used to authenticate users. -`Authentication.cookie_domain` use when wanting to set a subdomain wide cookie. For example setting this to `example.com` would allow the cookie to be valid for `example.com` along with `*.example.com`. +`Authentication.cookie_domain` use when wanting to set a subdomain wide +cookie. For example setting this to `example.com` would allow the cookie +to be valid for `example.com` along with `*.example.com`. `Authentication.authentication_backend` is the class to use for authentication logic. The default is `AuthenticationBackend` and will @@ -318,7 +322,7 @@ likely not need to change. `Authentication.login_html` is the HTML to display for a given user as the login form. -### `conda_store_server.server.auth.DummyAuthentication` +## `conda_store_server.server.auth.DummyAuthentication` Has all the configuration settings of `Authetication`. This class is modeled after the [JupyterHub DummyAuthentication @@ -328,7 +332,7 @@ class](https://github.com/jupyterhub/jupyterhub/blob/9f3663769e96d2e4f665fd6ef48 login with. Effectively a static password. This rarely if ever should be used outside of testing. -### `conda_store_server.server.auth.GenericOAuthAuthentication` +## `conda_store_server.server.auth.GenericOAuthAuthentication` A provider-agnostic OAuth authentication provider. Configure endpoints, secrets and other parameters to enable any OAuth-compatible @@ -365,7 +369,7 @@ especially useful when web service is behind a proxy. `GenericOAuthAuthentication.tls_verify` to optionally turn of TLS verification useful for custom signed certificates. -### `conda_store_server.server.auth.GithubOAuthAuthentication` +## `conda_store_server.server.auth.GithubOAuthAuthentication` Inherits from `Authentication` and `GenericOAuthAuthentication` so should be fully configurable from those options. @@ -376,7 +380,7 @@ is `https://github.com`. `GithubOAuthAuthentication.github_api` is the REST API url for GitHub. Default is `https://api.github.com`. -### `conda_store_server.server.auth.JupyterHubOAuthAuthentication` +## `conda_store_server.server.auth.JupyterHubOAuthAuthentication` Inherits from `Authentication` and `GenericOAuthAuthentication` so should be fully configurable from those options. @@ -384,7 +388,7 @@ should be fully configurable from those options. `GithubOAuthAuthentication.jupyterhub_url` is the url for connecting to JupyterHub. The URL should not include the `/hub/`. -### `conda_store_server.server.auth.RBACAuthorizationBackend` +## `conda_store_server.server.auth.RBACAuthorizationBackend` `RBACAuthorizationBackend.role_mappings_version` specifies the role mappings version to use: 1 (default, legacy), 2 (new, recommended). @@ -429,7 +433,7 @@ metadata and set the roles: PUT /api/v1/namespace/{namespace}/ ``` -### `conda_store_server._internal.server.app.CondaStoreServer` +## `conda_store_server._internal.server.app.CondaStoreServer` `CondaStoreServer.log_level` is the level for all server logging. Default is `INFO`. Common options are `DEBUG`, `INFO`, @@ -441,9 +445,6 @@ endpoints. Default True. `CondaStoreServer.enable_api` a Boolean on whether to expose the API endpoints. Default True. -`CondaStoreServer.enable_registry` a Boolean on whether to expose the -registry endpoints. Default True. - `CondaStoreServer.enable_metrics` a Boolean on whether to expose the metrics endpoints. Default True. @@ -453,9 +454,6 @@ to. The default is all IP addresses `0.0.0.0`. `CondaStoreServer.port` is the port for conda-store server to use. Default is `8080`. -`CondaStoreServer.registry_external_url` is the external hostname and -port to access docker registry cannot contain `http://` or `https://`. - `CondaStoreServer.url_prefix` is the prefix URL (subdirectory) for the entire application. All but the registry routes obey this. This is due to the docker registry API specification not supporting url prefixes. @@ -486,7 +484,15 @@ to serve in form `[(path, method, function), ...]`. `path` is a string, `method` is `get`, `post`, `put`, `delete` etc. and function is a regular python fastapi function. -### `conda_store_server.._internal.worker.app.CondaStoreWorker` +### Deprecated configuration options for `conda_store_server._internal.server.app.CondaStoreServer` + +`CondaStoreServer.enable_registry` (deprecated) a Boolean on whether to +expose the registry endpoints. Default False. + +`CondaStoreServer.registry_external_url` (deprecated) is the external hostname +and port to access docker registry cannot contain `http://` or `https://`. + +## `conda_store_server._internal.worker.app.CondaStoreWorker` `CondaStoreWorker.log_level` is the level for all server logging. Default is `INFO`. Common options are `DEBUG`, `INFO`, @@ -500,6 +506,6 @@ single filename to watch. the number of threads on your given machine. If set will limit the number of concurrent celery tasks to the integer. -### (deprecated) `conda_store_server.registry.ContainerRegistry` +## (deprecated) `conda_store_server.registry.ContainerRegistry` `ContainerRegistry.container_registries` (deprecated) dictionary of registries_url to upload built container images with callable function to configure registry instance with credentials. diff --git a/docusaurus-docs/docusaurus.config.js b/docusaurus-docs/docusaurus.config.js index 4b662141a..6726730e7 100644 --- a/docusaurus-docs/docusaurus.config.js +++ b/docusaurus-docs/docusaurus.config.js @@ -10,197 +10,201 @@ const darkCodeTheme = require("prism-react-renderer/themes/dracula"); /** @type {import('@docusaurus/types').Config} */ const config = { - title: "conda-store", - tagline: "Data science environments for collaboration", - favicon: "img/favicon.ico", + title: "conda-store", + tagline: "Data science environments for collaboration", + favicon: "img/favicon.ico", - // Set production url - url: "https://conda.store", - // Set // pathname under which your site is served - baseUrl: "/", + // Set production url + url: "https://conda.store", + // Set // pathname under which your site is served + baseUrl: "/", - organizationName: "conda-incubator", - projectName: "conda-store", + organizationName: "conda-incubator", + projectName: "conda-store", - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "warn", - // Even if you don't use internalization, you can use this field to set useful - // metadata like html lang. For example, if your site is Chinese, you may want - // to replace "en" with "zh-Hans". - i18n: { - defaultLocale: "en", - locales: ["en"], - }, + // Even if you don't use internalization, you can use this field to set useful + // metadata like html lang. For example, if your site is Chinese, you may want + // to replace "en" with "zh-Hans". + i18n: { + defaultLocale: "en", + locales: ["en"], + }, - // Add Plausible snippet as script - scripts: [ - { - src: "https://plausible.io/js/script.js", - defer: true, - "data-domain": "conda.store", - } - ], + // Add Plausible snippet as script + scripts: [ + { + src: "https://plausible.io/js/script.js", + defer: true, + "data-domain": "conda.store", + }, + ], - // Install plugins, then add here - plugins: [ - require.resolve("docusaurus-lunr-search"), - [ - "content-docs", - /** @type {import('@docusaurus/plugin-content-docs').Options} */ - ({ - id: "community", - path: "community", - routeBasePath: "/community", - breadcrumbs: true, - }), - ], - [ - "content-docs", - /** @type {import('@docusaurus/plugin-content-docs').Options} */ - ({ - id: "conda-store-ui", - path: "conda-store-ui", - routeBasePath: "/conda-store-ui", - breadcrumbs: true, - }), - ], - [ - "content-docs", - /** @type {import('@docusaurus/plugin-content-docs').Options} */ - ({ - id: "jupyterlab-conda-store", - path: "jupyterlab-conda-store", - routeBasePath: "/jupyterlab-conda-store", - breadcrumbs: true, - }), - ], - ], + // Install plugins, then add here + plugins: [ + require.resolve("docusaurus-lunr-search"), + [ + "content-docs", + /** @type {import('@docusaurus/plugin-content-docs').Options} */ + ({ + id: "community", + path: "community", + routeBasePath: "/community", + breadcrumbs: true, + }), + ], + [ + "content-docs", + /** @type {import('@docusaurus/plugin-content-docs').Options} */ + ({ + id: "conda-store-ui", + path: "conda-store-ui", + routeBasePath: "/conda-store-ui", + breadcrumbs: true, + }), + ], + [ + "content-docs", + /** @type {import('@docusaurus/plugin-content-docs').Options} */ + ({ + id: "jupyterlab-conda-store", + path: "jupyterlab-conda-store", + routeBasePath: "/jupyterlab-conda-store", + breadcrumbs: true, + }), + ], + ], - presets: [ - [ - "classic", - /** @type {import('@docusaurus/preset-classic').Options} */ - ({ - docs: { - id: "conda-store", - path: "conda-store", - routeBasePath: "conda-store", - editUrl: - "https://github.com/conda-incubator/conda-store/tree/main/docusaurus-docs", - }, - theme: { - customCss: require.resolve("./src/css/custom.css"), - }, - }), - ], - ], + presets: [ + [ + "classic", + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + id: "conda-store", + path: "conda-store", + routeBasePath: "conda-store", + editUrl: + "https://github.com/conda-incubator/conda-store/tree/main/docusaurus-docs", + }, + theme: { + customCss: require.resolve("./src/css/custom.css"), + }, + }), + ], + ], - themeConfig: - /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ - ({ - // og:image - image: "img/horizontal-logo.png", - metadata: [ - { name: 'keywords', content: 'conda, python, conda environments, dependencies, reproducibility' }, - ], - navbar: { - title: "Home", - logo: { - alt: "conda-store logo - conda-store Homepage", - src: "img/logo.svg", - }, - items: [ - { - label: "conda-store", - to: "conda-store/introduction", - position: "left", - }, - { - label: "conda-store UI", - to: "conda-store-ui/introduction", - position: "left", - }, - { - label: "JupyterLab extension", - to: "jupyterlab-conda-store/introduction", - position: "left", - }, - { - label: "Community", - to: "community/introduction", - position: "left", - }, - { - label: "GitHub", - position: "right", - items: [ - { - label: "conda-store", - href: "https://github.com/conda-incubator/conda-store", - }, - { - label: "conda-store-ui", - href: "https://github.com/conda-incubator/conda-store-ui", - }, - { - label: "jupyterlab-conda-store", - href: "https://github.com/conda-incubator/jupyterlab-conda-store", - }, - ], - }, - ], - }, - footer: { - style: "dark", - links: [ - { - items: [ - { - label: "Code of Conduct", - href: "https://github.com/conda-incubator/governance/blob/main/CODE_OF_CONDUCT.md", - }, - { - label: "Governance", - href: "https://github.com/conda-incubator/governance/tree/main", - }, - { - label: "Support", - to: "community/introduction#support", - }, - ], - }, - { - items: [ - { - label: "Brand guidelines", - to: "community/design", - }, - { - label: "Contribute", - to: "community/contribute/", - }, - ], - }, - ], - copyright: `Copyright ยฉ ${new Date().getFullYear()} | Made with ๐Ÿ’š by conda - store development team`, - }, - prism: { - theme: lightCodeTheme, - darkTheme: darkCodeTheme, - }, - announcementBar: { - id: 'WIP', - content: - 'โš ๏ธ We are in the process of revamping our docs, some pages may be incomplete or inaccurate. โš ๏ธ', - isCloseable: false, - }, - docs: { - sidebar: { - hideable: true, - }, - }, - }), + themeConfig: + /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ + ({ + // og:image + image: "img/horizontal-logo.png", + metadata: [ + { + name: "keywords", + content: + "conda, python, conda environments, dependencies, reproducibility", + }, + ], + navbar: { + title: "Home", + logo: { + alt: "conda-store logo - conda-store Homepage", + src: "img/logo.svg", + }, + items: [ + { + label: "conda-store", + to: "conda-store/introduction", + position: "left", + }, + { + label: "conda-store UI", + to: "conda-store-ui/introduction", + position: "left", + }, + { + label: "JupyterLab extension", + to: "jupyterlab-conda-store/introduction", + position: "left", + }, + { + label: "Community", + to: "community/introduction", + position: "left", + }, + { + label: "GitHub", + position: "right", + items: [ + { + label: "conda-store", + href: "https://github.com/conda-incubator/conda-store", + }, + { + label: "conda-store-ui", + href: "https://github.com/conda-incubator/conda-store-ui", + }, + { + label: "jupyterlab-conda-store", + href: "https://github.com/conda-incubator/jupyterlab-conda-store", + }, + ], + }, + ], + }, + footer: { + style: "dark", + links: [ + { + items: [ + { + label: "Code of Conduct", + href: "https://github.com/conda-incubator/governance/blob/main/CODE_OF_CONDUCT.md", + }, + { + label: "Governance", + href: "https://github.com/conda-incubator/governance/tree/main", + }, + { + label: "Support", + to: "community/introduction#support", + }, + ], + }, + { + items: [ + { + label: "Brand guidelines", + to: "community/design", + }, + { + label: "Contribute", + to: "community/contribute/", + }, + ], + }, + ], + copyright: `Copyright ยฉ ${new Date().getFullYear()} | Made with ๐Ÿ’š by conda - store development team`, + }, + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme, + }, + announcementBar: { + id: "WIP", + content: + "โš ๏ธ We are in the process of revamping our docs, some pages may be incomplete or inaccurate. โš ๏ธ", + isCloseable: false, + }, + docs: { + sidebar: { + hideable: true, + }, + }, + }), }; module.exports = config; diff --git a/docusaurus-docs/package.json b/docusaurus-docs/package.json index 8bff289c9..b8c50d06c 100644 --- a/docusaurus-docs/package.json +++ b/docusaurus-docs/package.json @@ -1,44 +1,40 @@ { - "name": "docs", - "version": "0.0.0", - "private": true, - "scripts": { - "docusaurus": "docusaurus", - "start": "docusaurus start", - "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy", - "clear": "docusaurus clear", - "serve": "docusaurus serve", - "write-translations": "docusaurus write-translations", - "write-heading-ids": "docusaurus write-heading-ids" - }, - "dependencies": { - "@docusaurus/core": "^3.0.0", - "@docusaurus/preset-classic": "^3.0.0", - "@mdx-js/react": "^3.0.0", - "clsx": "^1.2.1", - "docusaurus-lunr-search": "^3.3.1", - "prism-react-renderer": "^1.3.5", - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "2.4.1" - }, - "browserslist": { - "production": [ - ">0.5%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "engines": { - "node": ">=16.14" - } + "name": "docs", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "^3.0.0", + "@docusaurus/preset-classic": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "clsx": "^1.2.1", + "docusaurus-lunr-search": "^3.3.1", + "prism-react-renderer": "^1.3.5", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "2.4.1" + }, + "browserslist": { + "production": [">0.5%", "not dead", "not op_mini all"], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "engines": { + "node": ">=16.14" + } } diff --git a/docusaurus-docs/sidebarsCommunity.js b/docusaurus-docs/sidebarsCommunity.js index d134d1d8c..6ca73b481 100644 --- a/docusaurus-docs/sidebarsCommunity.js +++ b/docusaurus-docs/sidebarsCommunity.js @@ -6,11 +6,11 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{ type: "autogenerated", dirName: "community" }], + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{ type: "autogenerated", dirName: "community" }], - // Manual sidebar template - /* + // Manual sidebar template + /* tutorialSidebar: [ 'introduction', 'get-started', diff --git a/docusaurus-docs/sidebarsCondaStore.js b/docusaurus-docs/sidebarsCondaStore.js index 65a6c229a..d642604b3 100644 --- a/docusaurus-docs/sidebarsCondaStore.js +++ b/docusaurus-docs/sidebarsCondaStore.js @@ -6,11 +6,11 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{ type: "autogenerated", dirName: "conda-store" }], + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{ type: "autogenerated", dirName: "conda-store" }], - // But you can create a sidebar manually - /* + // But you can create a sidebar manually + /* tutorialSidebar: [ 'intro', 'hello', diff --git a/docusaurus-docs/sidebarsCondaStoreUI.js b/docusaurus-docs/sidebarsCondaStoreUI.js index 444fd80a9..bfb15030e 100644 --- a/docusaurus-docs/sidebarsCondaStoreUI.js +++ b/docusaurus-docs/sidebarsCondaStoreUI.js @@ -6,11 +6,11 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{ type: "autogenerated", dirName: "conda-store-ui" }], + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{ type: "autogenerated", dirName: "conda-store-ui" }], - // But you can create a sidebar manually - /* + // But you can create a sidebar manually + /* tutorialSidebar: [ 'intro', 'hello', diff --git a/docusaurus-docs/sidebarsJupyterLab.js b/docusaurus-docs/sidebarsJupyterLab.js index 129b85bf7..44d640c9a 100644 --- a/docusaurus-docs/sidebarsJupyterLab.js +++ b/docusaurus-docs/sidebarsJupyterLab.js @@ -6,13 +6,13 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [ - { type: "autogenerated", dirName: "jupyterlab-conda-store" }, - ], + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [ + { type: "autogenerated", dirName: "jupyterlab-conda-store" }, + ], - // But you can create a sidebar manually - /* + // But you can create a sidebar manually + /* tutorialSidebar: [ 'intro', 'hello', diff --git a/docusaurus-docs/src/css/custom.css b/docusaurus-docs/src/css/custom.css index 8daf1d6bc..8bf6df9b6 100644 --- a/docusaurus-docs/src/css/custom.css +++ b/docusaurus-docs/src/css/custom.css @@ -12,53 +12,53 @@ /* Override the default Infima variables here. */ :root { - --ifm-color-primary: #298642; - --ifm-color-primary-dark: #206532; - --ifm-color-primary-darker: #144321; - --ifm-color-primary-darkest: #0a2210; - --ifm-color-primary-light: #85cb97; - --ifm-color-primary-lighter: #addcba; - --ifm-color-primary-lightest: #d6eedc; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); - --ifm-breadcrumb-color-active: var(--ifm-color-primary-dark); + --ifm-color-primary: #298642; + --ifm-color-primary-dark: #206532; + --ifm-color-primary-darker: #144321; + --ifm-color-primary-darkest: #0a2210; + --ifm-color-primary-light: #85cb97; + --ifm-color-primary-lighter: #addcba; + --ifm-color-primary-lightest: #d6eedc; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-breadcrumb-color-active: var(--ifm-color-primary-dark); } /* For readability concerns, choose a lighter palette in dark mode. */ [data-theme="dark"] { - --ifm-color-primary: #5cb975; - --ifm-color-primary-dark: #85cb97; - --ifm-color-primary-darker: #addcba; - --ifm-color-primary-darkest: #d6eedc; - --ifm-color-primary-light: #206532; - --ifm-color-primary-lighter: #144321; - --ifm-color-primary-lightest: #0a2210; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ifm-color-primary: #5cb975; + --ifm-color-primary-dark: #85cb97; + --ifm-color-primary-darker: #addcba; + --ifm-color-primary-darkest: #d6eedc; + --ifm-color-primary-light: #206532; + --ifm-color-primary-lighter: #144321; + --ifm-color-primary-lightest: #0a2210; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } div[class^="announcementBar_"] { - font-weight: 500; - color: var(--ifm-color-content); - background-color: var(--ifm-color-primary-light); - border-bottom: 1.5px solid var(--ifm-color-primary-darkest); + font-weight: 500; + color: var(--ifm-color-content); + background-color: var(--ifm-color-primary-light); + border-bottom: 1.5px solid var(--ifm-color-primary-darkest); } div[class^="announcementBarContent_"] { - font-size: 1rem; - padding: 20px 0; + font-size: 1rem; + padding: 20px 0; } img { - border-color: var(--ifm-color-primary); - border-width: 2px; - border-style: solid; + border-color: var(--ifm-color-primary); + border-width: 2px; + border-style: solid; } .footer--dark { - --ifm-footer-background-color: #242526; + --ifm-footer-background-color: #242526; } /* Remove border (only) for conda-store logo in the top navbar */ .navbar__logo img { - border-width: 0; + border-width: 0; } diff --git a/docusaurus-docs/src/pages/index.js b/docusaurus-docs/src/pages/index.js index e1d3bae76..4cc8040c8 100644 --- a/docusaurus-docs/src/pages/index.js +++ b/docusaurus-docs/src/pages/index.js @@ -2,199 +2,203 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import React from "react"; -import clsx from "clsx"; import Link from "@docusaurus/Link"; +import useBaseUrl, { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import Layout from "@theme/Layout"; import CondaStoreLogo from "@site/static/img/logo.svg"; -import useBaseUrl, { useBaseUrlUtils } from "@docusaurus/useBaseUrl"; +import Layout from "@theme/Layout"; +import clsx from "clsx"; +import React from "react"; import styles from "./index.module.css"; function HomepageHeader() { - const { siteConfig } = useDocusaurusContext(); - return ( -
-
- -

{siteConfig.title}

-

{siteConfig.tagline}

-
- - Learn more - -
-
-
- ); + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+ +

{siteConfig.title}

+

{siteConfig.tagline}

+
+ + Learn more + +
+
+
+ ); } const ProjectsList = [ - { - title: "๐Ÿ“ฆ conda-store", - link: "/conda-store/introduction", - description: ( - <>Core library that provides key features through a REST API - ), - }, - { - title: "๐Ÿ’ป conda-store UI", - link: "/conda-store-ui/introduction", - description: ( - <> - User-friendly frontend to access conda-store features in a React - application - - ), - }, - { - title: "๐Ÿช JupyterLab extension", - link: "/jupyterlab-conda-store/introduction", - description: ( - <>JupyterLab interface that provides conda-store-ui frontend - ), - }, + { + title: "๐Ÿ“ฆ conda-store", + link: "/conda-store/introduction", + description: ( + <>Core library that provides key features through a REST API + ), + }, + { + title: "๐Ÿ’ป conda-store UI", + link: "/conda-store-ui/introduction", + description: ( + <> + User-friendly frontend to access conda-store features in a React + application + + ), + }, + { + title: "๐Ÿช JupyterLab extension", + link: "/jupyterlab-conda-store/introduction", + description: ( + <>JupyterLab interface that provides conda-store-ui frontend + ), + }, ]; const FeatureList = [ - { - title: "๐Ÿงถ Flexible & Intuitive UI", - description: ( - <> - Create, update, and manage environments using a user-friendly graphical - UI or YAML editor, available from within JupyterLab or standalone. - - ), - }, - { - title: "๐Ÿ“‹ Reproducible Artifacts", - description: ( - <> - Share fully-reproducible environments with auto-generated artifacts like - lockfiles, YAML files, and tarballs. - - ), - }, - { - title: "๐ŸŒฑ Free and Open Source", - description: ( - <> - A part of the conda (incubator) community, conda-store tools are built - using OSS libraries and developed under a permissive license. - - ), - }, - { - title: "๐Ÿ”€ Version Controlled", - description: ( - <> - Reference or use previous versions or artifacts of your environments - with automatic version-control. - - ), - }, - { - title: "โš–๏ธ Role-based Access Control", - description: ( - <> - Admins can manage users or teams and approve packages and channels to - maintain organizational standards. - - ), - }, - { - title: "๐Ÿ’ป System Agnostic", - description: ( - <> - Run conda-store on any major cloud provider, on-prem, or on local - machines. - - ), - }, + { + title: "๐Ÿงถ Flexible & Intuitive UI", + description: ( + <> + Create, update, and manage environments using a user-friendly graphical + UI or YAML editor, available from within JupyterLab or standalone. + + ), + }, + { + title: "๐Ÿ“‹ Reproducible Artifacts", + description: ( + <> + Share fully-reproducible environments with auto-generated artifacts like + lockfiles, YAML files, and tarballs. + + ), + }, + { + title: "๐ŸŒฑ Free and Open Source", + description: ( + <> + A part of the conda (incubator) community, conda-store tools are built + using OSS libraries and developed under a permissive license. + + ), + }, + { + title: "๐Ÿ”€ Version Controlled", + description: ( + <> + Reference or use previous versions or artifacts of your environments + with automatic version-control. + + ), + }, + { + title: "โš–๏ธ Role-based Access Control", + description: ( + <> + Admins can manage users or teams and approve packages and channels to + maintain organizational standards. + + ), + }, + { + title: "๐Ÿ’ป System Agnostic", + description: ( + <> + Run conda-store on any major cloud provider, on-prem, or on local + machines. + + ), + }, ]; function Project({ Svg, title, description, link }) { - return ( -
-
-

{title}

-

{description}

- Learn more โ†’ -
-
- ); + return ( +
+
+

{title}

+

{description}

+ Learn more โ†’ +
+
+ ); } function HomepageProjects() { - const { siteConfig } = useDocusaurusContext(); - return ( -
-
-
- {ProjectsList.map((props, idx) => ( - - ))} -
-
-
- ); + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+
+ {ProjectsList.map((props, idx) => ( + + ))} +
+
+
+ ); } function Feature({ Svg, title, description }) { - return ( -
-
-

{title}

-

{description}

-
-
- ); + return ( +
+
+

{title}

+

{description}

+
+
+ ); } function HomepageFeatures() { - const { siteConfig } = useDocusaurusContext(); - return ( -
-
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
-
- ); + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); } function HomepageVideo() { - const { siteConfig } = useDocusaurusContext(); - return ( -
-
- -

-
-
- ); + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+ An animated webp showing the conda-store UI and some of its major features. +
+
+
+ ); } export default function Home() { - const { siteConfig } = useDocusaurusContext(); - return ( - - -
- - - -
-
- ); + const { siteConfig } = useDocusaurusContext(); + return ( + + +
+ + + +
+
+ ); } diff --git a/docusaurus-docs/src/pages/index.module.css b/docusaurus-docs/src/pages/index.module.css index 051daf6a3..f0f4be9d3 100644 --- a/docusaurus-docs/src/pages/index.module.css +++ b/docusaurus-docs/src/pages/index.module.css @@ -9,35 +9,35 @@ */ .heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; } @media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } + .heroBanner { + padding: 2rem; + } } .buttons { - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; } .features { - display: flex; - align-items: center; - padding: 2rem 0; - width: 100%; + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; } .logo { - max-width: 15%; + max-width: 15%; } .video { - width: 80%; + width: 80%; } diff --git a/docusaurus-docs/static/openapi.json b/docusaurus-docs/static/openapi.json index 2ba6164e1..0d48374a6 100644 --- a/docusaurus-docs/static/openapi.json +++ b/docusaurus-docs/static/openapi.json @@ -1207,8 +1207,15 @@ "type": "integer" }, "metadata_": { - "title": "Metadata", - "type": "object" + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Metadata" }, "name": { "pattern": "^[A-Za-z0-9-+_@$&?^~.=]+$", @@ -1333,7 +1340,7 @@ }, "info": { "title": "conda-store", - "version": "2024.11.3-dev" + "version": "2025.2.2-dev" }, "openapi": "3.1.0", "paths": { @@ -2731,6 +2738,7 @@ }, "/api/v1/environment/": { "get": { + "deprecated": true, "description": "Retrieve a list of environments.\n\nParameters\n----------\nauth : Authentication\n Authentication instance for the request. Used to get role bindings\n and filter environments returned to those visible by the user making\n the request\nentity : AuthenticationToken\n Token of the user making the request\npaginated_args : PaginatedArgs\n Arguments for controlling pagination of the response\nconda_store : app.CondaStore\n The running conda store application\nsearch : Optional[str]\n If specified, filter by environment names or namespace names containing the\n search term\nnamespace : Optional[str]\n If specified, filter by environments in the given namespace\nname : Optional[str]\n If specified, filter by environments with the given name\nstatus : Optional[schema.BuildStatus]\n If specified, filter by environments with the given status\npackages : Optional[List[str]]\n If specified, filter by environments containing the given package name(s)\nartifact : Optional[schema.BuildArtifactType]\n If specified, filter by environments with the given BuildArtifactType\njwt : Optional[auth_schema.AuthenticationToken]\n If specified, retrieve only the environments accessible to this token; that is,\n only return environments that the user has 'admin', 'editor', and 'viewer'\n role bindings for.\n\nReturns\n-------\nDict\n Paginated JSON response containing the requested environments. Uses limit/offset-based\n pagination.", "operationId": "api_list_environments_v1_api_v1_environment__get", "parameters": [ @@ -5015,65 +5023,6 @@ "conda-store-ui" ] } - }, - "/v2/": { - "get": { - "operationId": "v2_v2__get", - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - } - }, - "summary": "V2", - "tags": [ - "registry" - ] - } - }, - "/v2/{rest}": { - "get": { - "operationId": "list_tags_v2__rest__get", - "parameters": [ - { - "in": "path", - "name": "rest", - "required": true, - "schema": { - "title": "Rest", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "summary": "List Tags", - "tags": [ - "registry" - ] - } } } } diff --git a/examples/docker-without-nfs/assets/conda_store_config.py b/examples/docker-without-nfs/assets/conda_store_config.py index 8fdd6c220..aed734929 100644 --- a/examples/docker-without-nfs/assets/conda_store_config.py +++ b/examples/docker-without-nfs/assets/conda_store_config.py @@ -45,7 +45,6 @@ c.CondaStoreServer.log_level = logging.INFO c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True -c.CondaStoreServer.enable_registry = True c.CondaStoreServer.enable_metrics = True c.CondaStoreServer.address = "0.0.0.0" c.CondaStoreServer.port = 8080 diff --git a/examples/docker/assets/conda_store_config.py b/examples/docker/assets/conda_store_config.py index c14a32ec0..bef8eb019 100644 --- a/examples/docker/assets/conda_store_config.py +++ b/examples/docker/assets/conda_store_config.py @@ -40,7 +40,6 @@ c.CondaStoreServer.log_level = logging.INFO c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True -c.CondaStoreServer.enable_registry = True c.CondaStoreServer.enable_metrics = True c.CondaStoreServer.address = "0.0.0.0" c.CondaStoreServer.port = 8080 diff --git a/examples/kubernetes/files/conda_store_config.py b/examples/kubernetes/files/conda_store_config.py index 6f40fa938..c1a2f2573 100644 --- a/examples/kubernetes/files/conda_store_config.py +++ b/examples/kubernetes/files/conda_store_config.py @@ -34,7 +34,6 @@ c.CondaStoreServer.log_level = logging.INFO c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True -c.CondaStoreServer.enable_registry = True c.CondaStoreServer.enable_metrics = True c.CondaStoreServer.address = "0.0.0.0" c.CondaStoreServer.port = 8080 diff --git a/examples/ubuntu2004/templates/conda_store_config.py.j2 b/examples/ubuntu2004/templates/conda_store_config.py.j2 index 2e874a01c..ca7fe2817 100644 --- a/examples/ubuntu2004/templates/conda_store_config.py.j2 +++ b/examples/ubuntu2004/templates/conda_store_config.py.j2 @@ -30,7 +30,6 @@ c.S3Storage.external_secure = False c.CondaStoreServer.log_level = logging.INFO c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True -c.CondaStoreServer.enable_registry = True c.CondaStoreServer.enable_metrics = True c.CondaStoreServer.address = "0.0.0.0" c.CondaStoreServer.port = 8080 diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 00e5545ab..a9df60100 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -45,9 +45,12 @@ outputs: - {{ pin_subpackage('conda-store-server', min_pin='x.x.x', max_pin='x.x.x') }} test: + requires: + - pip imports: - conda_store commands: + - pip check - conda-store --help - name: conda-store-server @@ -89,19 +92,27 @@ outputs: - pyyaml >=6.0.1 - redis-py - requests + # setuptools>=70 uses local version of packaging (and other deps) without + # pinning them; conda-lock depends on this, but also doesn't pin the setuptools + # version. See https://github.com/pypa/setuptools/issues/4478 for details + - setuptools<70 - sqlalchemy >=2.0,<2.1 - traitlets - uvicorn - yarl - psycopg2 - pymysql + - psycopg2-binary run_constrained: - {{ pin_subpackage('conda-store', min_pin='x.x.x', max_pin='x.x.x') }} test: + requires: + - pip imports: - conda_store_server commands: + - pip check - conda-store-server --help - conda-store-worker --help diff --git a/tests/assets/conda_store_config.py b/tests/assets/conda_store_config.py index 637afc4be..3901aff78 100644 --- a/tests/assets/conda_store_config.py +++ b/tests/assets/conda_store_config.py @@ -45,7 +45,6 @@ c.CondaStoreServer.log_level = logging.INFO c.CondaStoreServer.enable_ui = True c.CondaStoreServer.enable_api = True -c.CondaStoreServer.enable_registry = True c.CondaStoreServer.reload = True c.CondaStoreServer.enable_metrics = True c.CondaStoreServer.address = "0.0.0.0"