From 0a84c933feaee7415beb80acdf9912c39e9344e7 Mon Sep 17 00:00:00 2001 From: choldgraf Date: Tue, 21 Jan 2025 09:45:45 -0800 Subject: [PATCH 01/10] BLOG: Post about cross-project contribution --- .../blog/2025/jupyter-book-cors/featured.png | Bin 0 -> 37892 bytes content/blog/2025/jupyter-book-cors/index.md | 46 ++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 content/blog/2025/jupyter-book-cors/featured.png create mode 100644 content/blog/2025/jupyter-book-cors/index.md diff --git a/content/blog/2025/jupyter-book-cors/featured.png b/content/blog/2025/jupyter-book-cors/featured.png new file mode 100644 index 0000000000000000000000000000000000000000..11e3eafea8b9e47b6f717c148a1c3961fb56e490 GIT binary patch literal 37892 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 + +## Getting Jupyter Book to discover Jupyter Hub + +As we began developing this workflow, we realized that there was a blocker in the JupyterHub and BinderHub ecosystem that needed to be fixed. We needed a way to **ask a JupyterHub whether it had an unauthenticated end-point for service discovery**. Basically, a way to ask a hub "what kind of hub are you, and how can we launch an interactive session on you?" Doing this is simple-enough - JupyterHub already has a way of reporting its version and application type, which allows us to infer how to launch interactive sessions. But, we hit a snag in an HTML context. + +By default, JupyterHub disallows certain kinds of [Cross-Origin Resource Sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) (CORS) requests, in order to restrict other applications from abusing a JupyterHub's API. If you hit parts of a JupyterHub API from _the command line_, things work fine. But if you do the same thing via JavaScript from a website, the request is disallowed. This was a problem if we want Jupyter Book (a web application) to be able to make requests of JupyterHub's API. + +So, we realized that we needed to make an **upstream contribution in JupyterHub** in order to **enable an interaction between JupyterHub and Jupyter Book**. In this case, it was a relatively simple fix: allowing CORS requests for the specific API endpoint we needed (which is a very lightweight endpoint that is not vulnerable to security risks, and is broadly useful to make accessible)[^1]. That resulted in two PRs: + +- https://github.com/jupyterhub/jupyterhub/pull/4966 allows CORS requests for the API that was needed for service discovery in JupyterHub. +- https://github.com/jupyterhub/binderhub/pull/1906 enables this workflow on a BinderHub so that its services can be discovered. +- https://github.com/jupyter-book/myst-theme/pull/503 adds new launch button functionality to [Jupyter Book 2](https://next.jupyterbook.org) that allows readers to bring their own Binder / JupyterHub links for launching. (this is what necessitated the above two PRs) + +[^1]: This actually required an interesting bit of team discussion that was much easier with a few 2i2c staff on the JupyterHub team. The original request from Angus got interpreted as opening up the _entire hub API_ to external requests (which is a bad idea!) but we were able to quickly clarify and discuss with the JupyterHub team that this was only about a very specific API endpoint. This is the kind of communication loop that often goes haywire when you have people contributing to a project without historical relationships to the project's maintainers. + +As a result of this upstream contribution loop, JupyterHub can now accept API requests at its "service discovery" endpoint, which means that Jupyter Book (and any other web application) can more easily learn about a hub's capabilities and version. + +We wanted to share this short vignette because it's a good reflection of the kind of value that 2i2c tries to provide, given its role in helping to build and enhance networks of infrastructure, domain communities, and open source communities.In this case, we enabled a _cross-project_ workflow that required knowledge of each project and a vision for how they could be used together in a way that was greater than the some of its parts. + +We think there's a lot more potential in these kinds of workflows, and are eager to continue our work to identify and enhance community-centric infrastructure for interactive computing. + From 603892ae1ea2ac3c32ce7afcd173d2a657128bd0 Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 21 Jan 2025 20:12:26 +0000 Subject: [PATCH 02/10] wip: change all links to JB2 --- content/blog/2025/jupyter-book-cors/index.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/content/blog/2025/jupyter-book-cors/index.md b/content/blog/2025/jupyter-book-cors/index.md index e127e4340..a69a9bf12 100644 --- a/content/blog/2025/jupyter-book-cors/index.md +++ b/content/blog/2025/jupyter-book-cors/index.md @@ -10,17 +10,17 @@ draft: false A key challenge in the open source space is that projects are often independent and autonomous, with relatively few formal ways to collaborate and coordinate efforts. While this usually isn't a big deal, it means that there is a missed opportunity to grow the impact of an ecosystem because it requires coordinated development among multiple stakeholders within it. -This is one of the reasons we created 2i2c's open community hub platform. By deploying a single platform that utilizes entirely open infrastructure that we contribute back to, we have visibility over a variety of projects along with the need to combine them together for a specific end-user outcome. One-such development scenario recently came up involving [Jupyter Book](https://jupyterbook.org/) and [JupyterHub](https://jupyterhub.org/). +This is one of the reasons we created 2i2c's open community hub platform. By deploying a single platform that utilizes entirely open infrastructure that we contribute back to, we have visibility over a variety of projects along with the need to combine them together for a specific end-user outcome. One-such development scenario recently came up involving [Jupyter Book 2][jb2] and [JupyterHub](https://jupyterhub.org/). ## Allowing readers to "bring their own Binders" -We've recently been working to integrate [Jupyter Book](https://jupyterbook.org/) workflows with our community hubs for a more seamless experience (for example, having book pages link back to interactive cloud sessions that allow users to interact with the content). We imagine a network of Jupyter Books that all build upon the same core infrastructures (JupyterHub, Binder, etc) for cloud-based computing. Our hope is to allow a user to _bring their own Binder_ with them so that they can interact with another book's content with their own cloud infrastructure. For example: +We've recently been working to integrate [Jupyter Book 2][jb2] workflows with our community hubs for a more seamless experience (for example, having book pages link back to interactive cloud sessions that allow users to interact with the content). We imagine a network of Jupyter Books that all build upon the same core infrastructures (JupyterHub, Binder, etc) for cloud-based computing. Our hope is to allow a user to _bring their own Binder_ with them so that they can interact with another book's content with their own cloud infrastructure. For example: - A student with access to `binder.myuniversity.edu` could read a Jupyter Book created by a professor at `otheruniversity.edu`. - The Jupyter Book is defined with a [Binder specification](https://repo2docker.readthedocs.io/en/latest/specification.html) that has a recipe for re-building the environment needed to run te book's content. - From the professor's book, the student can choose to launch an interactive Binder sessions on _their university's Binder_, allowing them to interact with the book's content on their own infrastructure. -We want a workflow like this to be as seamless and un-complicated as possible. We also want it to follow the same fundamental workflow as the [nbgitpuller-based launch buttons](https://docs.2i2c.org/community/content/). Along the way, we realized that we needed to coordinate development across [Jupyter Book](https://jupyterbook.org/), [JupyterHub](https://jupyter.readthedocs.io), and [BinderHub](https://binderhub.readthedocs.io). +We want a workflow like this to be as seamless and un-complicated as possible. We also want it to follow the same fundamental workflow as the [nbgitpuller-based launch buttons](https://docs.2i2c.org/community/content/). Along the way, we realized that we needed to coordinate development across [Jupyter Book 2][jb2]], [JupyterHub](https://jupyter.readthedocs.io), and [BinderHub](https://binderhub.readthedocs.io). {{< figure src="./images/featured.png" caption="The three projects (Jupyter Book, BinderHub, and JupyterHub) that needed to work together to enable 'bring your own binderhub' workflows." >}} @@ -34,7 +34,7 @@ So, we realized that we needed to make an **upstream contribution in JupyterHub* - https://github.com/jupyterhub/jupyterhub/pull/4966 allows CORS requests for the API that was needed for service discovery in JupyterHub. - https://github.com/jupyterhub/binderhub/pull/1906 enables this workflow on a BinderHub so that its services can be discovered. -- https://github.com/jupyter-book/myst-theme/pull/503 adds new launch button functionality to [Jupyter Book 2](https://next.jupyterbook.org) that allows readers to bring their own Binder / JupyterHub links for launching. (this is what necessitated the above two PRs) +- https://github.com/jupyter-book/myst-theme/pull/503 adds new launch button functionality to [Jupyter Book 2][jb2] that allows readers to bring their own Binder / JupyterHub links for launching. (this is what necessitated the above two PRs) [^1]: This actually required an interesting bit of team discussion that was much easier with a few 2i2c staff on the JupyterHub team. The original request from Angus got interpreted as opening up the _entire hub API_ to external requests (which is a bad idea!) but we were able to quickly clarify and discuss with the JupyterHub team that this was only about a very specific API endpoint. This is the kind of communication loop that often goes haywire when you have people contributing to a project without historical relationships to the project's maintainers. @@ -44,3 +44,5 @@ We wanted to share this short vignette because it's a good reflection of the kin We think there's a lot more potential in these kinds of workflows, and are eager to continue our work to identify and enhance community-centric infrastructure for interactive computing. +[jb2]: https://next.jupyterbook.org/ + From 373d3b96e0de27c38f6b96ea7b4cfc3e2f9d26ab Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 21 Jan 2025 20:15:29 +0000 Subject: [PATCH 03/10] wip: small typos / rewording --- content/blog/2025/jupyter-book-cors/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/blog/2025/jupyter-book-cors/index.md b/content/blog/2025/jupyter-book-cors/index.md index a69a9bf12..7df39c647 100644 --- a/content/blog/2025/jupyter-book-cors/index.md +++ b/content/blog/2025/jupyter-book-cors/index.md @@ -36,11 +36,11 @@ So, we realized that we needed to make an **upstream contribution in JupyterHub* - https://github.com/jupyterhub/binderhub/pull/1906 enables this workflow on a BinderHub so that its services can be discovered. - https://github.com/jupyter-book/myst-theme/pull/503 adds new launch button functionality to [Jupyter Book 2][jb2] that allows readers to bring their own Binder / JupyterHub links for launching. (this is what necessitated the above two PRs) -[^1]: This actually required an interesting bit of team discussion that was much easier with a few 2i2c staff on the JupyterHub team. The original request from Angus got interpreted as opening up the _entire hub API_ to external requests (which is a bad idea!) but we were able to quickly clarify and discuss with the JupyterHub team that this was only about a very specific API endpoint. This is the kind of communication loop that often goes haywire when you have people contributing to a project without historical relationships to the project's maintainers. +[^1]: This actually required an interesting bit of team discussion that was much easier with a few 2i2c staff on the JupyterHub team. The original request from Angus was interpreted as opening up the _entire hub API_ to external requests (which is a bad idea!) but we were able to quickly discuss this with the JupyterHub team to clarify that this was only about a very specific API endpoint. This is the kind of communication loop that often goes haywire when you have people contributing to a project without historical relationships to the project's maintainers. As a result of this upstream contribution loop, JupyterHub can now accept API requests at its "service discovery" endpoint, which means that Jupyter Book (and any other web application) can more easily learn about a hub's capabilities and version. -We wanted to share this short vignette because it's a good reflection of the kind of value that 2i2c tries to provide, given its role in helping to build and enhance networks of infrastructure, domain communities, and open source communities.In this case, we enabled a _cross-project_ workflow that required knowledge of each project and a vision for how they could be used together in a way that was greater than the some of its parts. +We wanted to share this short vignette because it's a good reflection of the kind of value that 2i2c tries to provide, given its role in helping to build and enhance networks of infrastructure, domain communities, and open source communities. In this case, we enabled a _cross-project_ workflow that required knowledge of each project, and a vision for how they could be used together in a way that exceeded the sum of their parts. We think there's a lot more potential in these kinds of workflows, and are eager to continue our work to identify and enhance community-centric infrastructure for interactive computing. From 77b6ff4219f1549aa012b9bcb60abed324d85a3b Mon Sep 17 00:00:00 2001 From: Angus Hollands Date: Tue, 21 Jan 2025 20:23:34 +0000 Subject: [PATCH 04/10] fix: correct image path --- content/blog/2025/jupyter-book-cors/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/blog/2025/jupyter-book-cors/index.md b/content/blog/2025/jupyter-book-cors/index.md index 7df39c647..e759eb7c2 100644 --- a/content/blog/2025/jupyter-book-cors/index.md +++ b/content/blog/2025/jupyter-book-cors/index.md @@ -22,7 +22,7 @@ We've recently been working to integrate [Jupyter Book 2][jb2] workflows with ou We want a workflow like this to be as seamless and un-complicated as possible. We also want it to follow the same fundamental workflow as the [nbgitpuller-based launch buttons](https://docs.2i2c.org/community/content/). Along the way, we realized that we needed to coordinate development across [Jupyter Book 2][jb2]], [JupyterHub](https://jupyter.readthedocs.io), and [BinderHub](https://binderhub.readthedocs.io). -{{< figure src="./images/featured.png" caption="The three projects (Jupyter Book, BinderHub, and JupyterHub) that needed to work together to enable 'bring your own binderhub' workflows." >}} +{{< figure src="./featured.png" caption="The three projects (Jupyter Book, BinderHub, and JupyterHub) that needed to work together to enable 'bring your own binderhub' workflows." >}} ## Getting Jupyter Book to discover Jupyter Hub From 404a1c59cee28e6c9ecefcf0b74212bb0d532d2d Mon Sep 17 00:00:00 2001 From: Chris Holdgraf Date: Thu, 23 Jan 2025 15:30:19 -0800 Subject: [PATCH 05/10] ADD commitment to open licenses (#359) * ADD commitment to open licenses * Apply comments * back to hello --- config/_default/menus.toml | 12 +++ content/blog/2025/binder-singlenode/index.md | 1 + .../blog/2025/community-ownership/index.md | 75 +++++++++++++++++++ content/open-technology/index.md | 19 +++++ 4 files changed, 107 insertions(+) create mode 100644 content/blog/2025/binder-singlenode/index.md create mode 100644 content/blog/2025/community-ownership/index.md create mode 100644 content/open-technology/index.md diff --git a/config/_default/menus.toml b/config/_default/menus.toml index cda7833af..ad029acc0 100644 --- a/config/_default/menus.toml +++ b/config/_default/menus.toml @@ -14,10 +14,22 @@ weight = 11 [[main]] + name = "Service Principles" + weight = 12 + +[[main]] + parent = "Service Principles" name = "Right to Replicate" url = "right-to-replicate/" weight = 12 +[[main]] + parent = "Service Principles" + name = "Commitment to Open Technology" + url = "open-technology/" + weight = 12 + + [[main]] name = "About" identifier = "about" diff --git a/content/blog/2025/binder-singlenode/index.md b/content/blog/2025/binder-singlenode/index.md new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/content/blog/2025/binder-singlenode/index.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/blog/2025/community-ownership/index.md b/content/blog/2025/community-ownership/index.md new file mode 100644 index 000000000..ad1045339 --- /dev/null +++ b/content/blog/2025/community-ownership/index.md @@ -0,0 +1,75 @@ +--- +title: "Announcing our formal commitment to open technology" +date: "2025-01-15" +banner: + image: "" +authors: ["Yuvi Panda", "Chris Holdgraf"] +tags: [open-source] +categories: [organization] +featured: false +draft: false +--- + +In this post, we're sharing our [Commitment to Open Technology](../../../open-technology/index.md). It is focused on _software licenses_ for reasons we'll describe below. We hope that it clarifies what kind of licenses we'll use, and assures our communities that we will not change our stance towards open source technology in the future. This ensures 2i2c's long-term commitment to community-owned and open infrastructure. + +Being a platform and service provider gives us a lot of power, and also introduces a potential source of _lock-in_ for our member communities. While 2i2c's organizational mission and culture are strongly aligned with open infrastructure, we believe it's important to encode commitments like these in a formal way to provide both transparency and accountability to our member communities. + +## Our commitment to open technology + +Below we copy the original language of this policy from our [Commitment to Open Technology](../../../open-technology/index.md): + + + +_Definitions of MUST, MUST NOT, SHOULD, MAY, etc are defined in [RFC 2119](https://tools.ietf.org/html/rfc2119)_ + +1. All engineering artifacts (code, documentation, etc) produced by 2i2c's engineering team MUST be licensed under an open source license approved by a non-profit organization that is not 2i2c. +2. Open Source Projects originating at 2i2c, or stewarded by 2i2c, MUST NOT require a [Contributor Licensing Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) that includes Copyright Assignment to 2i2c. +3. The list of external organizations that define licenses we accept are + 1. [the Open Source Initiative](https://opensource.org/) + 2. the [Organization for Ethical Source](https://ethicalsource.dev/). +4. Modifying (1), (2), or (3) MUST be done through a 2/3 majority vote of 2i2c staff. + +## What does this commitment mean? + +In plain language, here's what this commitment means: + +1. We'll only use open source licenses that have been approved by standard non-profits that are broadly recognized by the tech industry. +2. For anything we build, we won't require contributors to give up the rights to their contributions via CLAs, so that it is much harder for 2i2c to change our licenses in the future. +3. Changing this policy will require organization-wide agreement, and in the future we'll give authority over this policy to a group of people representing our member communities. + +## Why are licenses and CLAs important? + +Many organizations claim to be committed to open infrastructure, while retaining the ability to _change this commitment in the future when it is in their interests_. A classic example of this is a "bait and switch" that looks something like this: + +1. A company releases software under an open source license and professes to build an open source community around it. +2. However, they retain the rights to all of the code in their projects through a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) with copyright assignment. This generally means that contributors must _give up the rights to their contribution_ in order to make that contribution. +3. Once their product has gained traction and it is in their interests, the company can _change the license_ to whatever they wish (even one that is not open source) because they retain the rights to all contributions in the codebase. +4. They then leverage this new position as owners of a proprietary project to extract business value or grow their position in a market. + +Think this sounds unlikely? Here are just a few recent examples of companies that have switched their license after many years of releasing their technology under an open source license: + +- [Redis](https://redis.io/blog/redis-adopts-dual-source-available-licensing/) +- [Hashicorp / Terraform](https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license) +- [Elastic Search](https://en.wikipedia.org/wiki/Elasticsearch#Licensing_changes) + +We want to ensure our communities that 2i2c is not headed down this path, in order to give them confidence in treating us as a long-term service partner. + +## What does this change about 2i2c's open source commitment? + +In short: nothing. These are already the principles that 2i2c was committed to from its inception, and already implied via our [Right to Replicate](../../../right-to-replicate/). However, we wanted to make these commitments more formally in order to give ourselves more accountability to sticking with them, and to provide more transparency for our community members and stakeholders. + +## Who is this for? + +We imagine three audiences for this policy: + +1. **2i2c present and future staff** who want to ensure that their organization remains committed to our open principles. This document provides a sense of psychological safety to have bold discussions about structuring our approach to open source. +2. **Member communities and 2i2c stakeholders** who need to have an understanding of the guarantees that we provide in order to trust 2i2c as a service developer and provider. This is similar to the effect our [Right to Replicate](/right-to-replicate) has. +3. **Open source communities** who need to understand our long-term commitment and goals around open technology in order to trust as a peer and collaborator within open source communities. + +## We'd love feedback + +We hope that these ideas both clarify our intent and the reason that we think it's important. We'd love feedback about early refinements to these principles in order to make them more effective, as well as ways that we can provide more community oversight and participation in evolving these policies moving forward. If you have any thoughts to share, please send feedback via e-mail [hello@2i2c.org](mailto:hello@2i2c.org). + +--- + +**Acknowledgements**: _The creation of this policy and the rationale behind it was led by [Yuvi Panda](../../../authors/yuvi-panda/) with feedback from 2i2c's team. This blog post was co-written with [Chris Holdgraf](../../../authors/chris-holdgraf)._ diff --git a/content/open-technology/index.md b/content/open-technology/index.md new file mode 100644 index 000000000..0d7d5d69d --- /dev/null +++ b/content/open-technology/index.md @@ -0,0 +1,19 @@ +# Our commitment to open technology + +This page describes 2i2c's commitment to developing and contributing to infrastructure that is open source now and into the future. More context can be found [in this blog post](../blog/2025/community-ownership/index.md). + +_Definitions of MUST, MUST NOT, SHOULD, MAY, etc are defined in [RFC 2119](https://tools.ietf.org/html/rfc2119)_ + +1. All engineering artifacts (code, documentation, etc) produced by 2i2c's engineering team MUST be licensed under an open source license approved by a non-profit organization that is not 2i2c.[^os-license] +2. Open Source Projects originating at 2i2c, or stewarded by 2i2c, MUST NOT require a [Contributor Licensing Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) that includes Copyright Assignment to 2i2c. [^cla] +3. The list of external organizations that define licenses we accept are[^licensors]: + 1. [the Open Source Initiative](https://opensource.org/) + 2. the [Organization for Ethical Source](https://ethicalsource.dev/). +4. Modifying (1), (2), or (3) MUST be done through a 2/3 majority vote of 2i2c staff. [^governance] + +[^os-license]: This constrains us from writing proprietary engineering code or creating proprietary products. Note that this only applies to 2i2c artifacts, and not those that are created by our member communities. +[^licensors]: This constrains us from creating a new non profit that rubberstamps a license that is fundamentally proprietary, while still allowing for experimentation with licenses that try to innovate on OSI. +[^governance]: This sets an intentionally-high bar for modifying this policy. In 2025 we aim to re-establish our steering council along with a community advisory board that is tasked with defining policy like this. In the interrim, we choose `2/3 of the organization` to be a reasonably high bar for changing this policy +[^cla]: Protects from the most common "bait and switch" licensing problem, where being the sole copyright owner of a project allows us to change the license in the future because we've retained ownership over all the code. + +These commitments ensure 2i2c's ongoing support for open source technology and community-owned infrastructure, and constrain us from _changing_ this commitment in a way that would harm our communities. We hope that this builds trust with our communities to rely on 2i2c as a provider and a steward of their infrastructure. [See this blog post for more context](../blog/2025/community-ownership/index.md) From e837534b8e238aa7cb7940089251b367cdb961b7 Mon Sep 17 00:00:00 2001 From: Sarah Gibson <44771837+sgibson91@users.noreply.github.com> Date: Mon, 27 Jan 2025 18:17:24 +0000 Subject: [PATCH 06/10] Update Sarah's author page (#360) --- content/authors/sarah-gibson/_index.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/content/authors/sarah-gibson/_index.md b/content/authors/sarah-gibson/_index.md index d4d47d7de..29e0ff84d 100644 --- a/content/authors/sarah-gibson/_index.md +++ b/content/authors/sarah-gibson/_index.md @@ -19,7 +19,7 @@ organizations: - name: Project Jupyter url: "https://jupyter.org" - name: The Turing Way - url: "https://the-turing-way.netlify.app/" + url: "https://the-turing-way.org/" # Short bio (displayed in user profile at end of posts) bio: @@ -36,9 +36,6 @@ social: - icon: envelope icon_pack: fas link: 'mailto:sgibson@2i2c.org' -- icon: twitter - icon_pack: fab - link: https://twitter.com/drsarahlgibson - icon: github icon_pack: fab link: https://github.com/sgibson91 @@ -50,10 +47,11 @@ social: # Set this to `[]` or comment out if you are not using People widget. user_groups: - Product and Services Team + --- Sarah Gibson is an Open Source Infrastructure Engineer at 2i2c, an open source contributor and advocate. -She holds more than two years of experience as a Research Engineer at a national institute for data science and artificial intelligence, as well as holding a core contributor role in the open source projects [Binder](https://jupyter.org/binder), [JupyterHub](https://jupyter.org/hub), and [_The Turing Way_](https://the-turing-way.netlify.app/). +She holds more than two years of experience as a Research Engineer at a national institute for data science and artificial intelligence, as well as holding a core contributor role in the open source projects [Binder](https://jupyter.org/binder), [JupyterHub](https://jupyter.org/hub), and [_The Turing Way_](https://the-turing-way.org). Sarah is passionate about working with domain experts to leverage cloud computing in order to accelerate cutting-edge, data-intensive research and disseminating the results in an open, reproducible and reusable manner. Sarah holds a Fellowship with the [Software Sustainability Institute](https://software.ac.uk) and advocates for best software practices in research. From 051f0791071488b074c3f9673bc2db5e0aecea77 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Mon, 27 Jan 2025 13:46:08 -0800 Subject: [PATCH 07/10] Update Yuvi's role on the website to match his current role (#362) While technically 'tech lead' has been my official role for a long time, I believe I've actually only 'lived' it for the last 9-12 months. Now I feel comfortable actually putting that on the website. --- content/authors/yuvi-panda/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/authors/yuvi-panda/_index.md b/content/authors/yuvi-panda/_index.md index c1702958a..5f36ccaa5 100644 --- a/content/authors/yuvi-panda/_index.md +++ b/content/authors/yuvi-panda/_index.md @@ -9,7 +9,7 @@ authors: # Is this the primary user of the site? superuser: true -role: Senior Open Source Infrastructure Engineer +role: Tech Lead and Co-Founder # Organizations/Affiliations organizations: [] From 43127a51319c2003081df42335b10bf24992d1de Mon Sep 17 00:00:00 2001 From: Sarah Gibson Date: Tue, 28 Jan 2025 10:07:18 +0000 Subject: [PATCH 08/10] Add a blog post to announce the per-user storage quota feature --- .../blog/2025/per-user-storage-quota/index.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 content/blog/2025/per-user-storage-quota/index.md diff --git a/content/blog/2025/per-user-storage-quota/index.md b/content/blog/2025/per-user-storage-quota/index.md new file mode 100644 index 000000000..7976ef405 --- /dev/null +++ b/content/blog/2025/per-user-storage-quota/index.md @@ -0,0 +1,46 @@ +--- +title: "Enforcing per-user storage quotas with `jupyterhub-home-nfs`" +subtitle: "" +summary: "" +authors: ["Sarah Gibson"] +tags: [] +categories: [] +date: 2025-01-28T09:57:28+00:00 +lastmod: 2025-01-07T15:18:37-08:00 +featured: false +draft: false + +# Featured image +# To use, add an image named `featured.jpg/png` to your page's folder. +# Focal points: Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight. +image: + caption: "" + focal_point: "" + preview_only: false + +# Projects (optional). +# Associate this post with one or more of your projects. +# Simply enter your project's folder or file name without extension. +# E.g. `projects = ["internal-project"]` references `content/project/deep-learning/index.md`. +# Otherwise, set `projects = []`. +projects: ["nasa-veda"] +--- + +When sharing a storage disk between users, as is usually the case in a JupyterHub deployment, it is important to put in guardrails so that one user cannot eat up the whole storage capacity from the rest of the users. +To this end, 2i2c in close collaboration with [Development Seed](https://developmentseed.org) have developed the [`jupyterhub-home-nfs` project](https://github.com/2i2c-org/jupyterhub-home-nfs) which is a Helm chart that permits enforcing per-user quotas on the storage space. + +{{% callout note %}} +Note that this feature is currently available to AWS hosted hubs only and will be rolled out to other cloud providers in the future. +{{% /callout %}} + +Under the hood, the Helm chart runs [NFS Ganesha](https://github.com/nfs-ganesha/nfs-ganesha) as an in-cluster NFS server, backed by XFS as the underlying filesystem. Storage quota is enforced through XFS's native quota management utility `xfs_quota`. + +Since this feature moves our infrastructure away from managed filesystems (such as AWS's Elastic File System), we have also developed monitoring and alerting mechanisms that will let us know when the disks are getting full, and automated back-ups for disaster recovery. + +If you would like to try this on your 2i2c-managed hub, [please get in touch](https://docs.2i2c.org/support/). + +This project can also be used with _any_ Kubernetes-based JupyterHub, as per our [Right to Replicate policy](https://2i2c.org/right-to-replicate/), so please try it out on your own deployment and let us know what you think! + +## Credit + +This project was developed and deployed in collaboration with [Tarashish Mishra](https://developmentseed.org/team/tarashish-mishra/) from [Development Seed](https://developmentseed.org) From a9e87e73b2f18d98cc9fa4015ea8258ecb5c830c Mon Sep 17 00:00:00 2001 From: Sarah Gibson Date: Tue, 28 Jan 2025 10:11:45 +0000 Subject: [PATCH 09/10] Update last modified date --- content/blog/2025/per-user-storage-quota/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/blog/2025/per-user-storage-quota/index.md b/content/blog/2025/per-user-storage-quota/index.md index 7976ef405..a6701aed3 100644 --- a/content/blog/2025/per-user-storage-quota/index.md +++ b/content/blog/2025/per-user-storage-quota/index.md @@ -6,7 +6,7 @@ authors: ["Sarah Gibson"] tags: [] categories: [] date: 2025-01-28T09:57:28+00:00 -lastmod: 2025-01-07T15:18:37-08:00 +lastmod: 2025-01-28T10:10:14+00:00 featured: false draft: false From d804399804aa12fb6d045cb99f40efaf23f9beea Mon Sep 17 00:00:00 2001 From: Jenny Wong Date: Tue, 28 Jan 2025 12:03:33 +0000 Subject: [PATCH 10/10] Apply suggestions from code review --- content/blog/2025/per-user-storage-quota/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/content/blog/2025/per-user-storage-quota/index.md b/content/blog/2025/per-user-storage-quota/index.md index a6701aed3..e9b5f0b10 100644 --- a/content/blog/2025/per-user-storage-quota/index.md +++ b/content/blog/2025/per-user-storage-quota/index.md @@ -3,8 +3,8 @@ title: "Enforcing per-user storage quotas with `jupyterhub-home-nfs`" subtitle: "" summary: "" authors: ["Sarah Gibson"] -tags: [] -categories: [] +tags: [open-source] +categories: [impact] date: 2025-01-28T09:57:28+00:00 lastmod: 2025-01-28T10:10:14+00:00 featured: false @@ -33,9 +33,9 @@ To this end, 2i2c in close collaboration with [Development Seed](https://develop Note that this feature is currently available to AWS hosted hubs only and will be rolled out to other cloud providers in the future. {{% /callout %}} -Under the hood, the Helm chart runs [NFS Ganesha](https://github.com/nfs-ganesha/nfs-ganesha) as an in-cluster NFS server, backed by XFS as the underlying filesystem. Storage quota is enforced through XFS's native quota management utility `xfs_quota`. +Under the hood, the Helm chart runs [NFS Ganesha](https://github.com/nfs-ganesha/nfs-ganesha) as an in-cluster NFS server, backed by [XFS](https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/7/html/storage_administration_guide/ch-xfs) as the underlying filesystem. Storage quota is enforced through XFS's native quota management utility `xfs_quota`. -Since this feature moves our infrastructure away from managed filesystems (such as AWS's Elastic File System), we have also developed monitoring and alerting mechanisms that will let us know when the disks are getting full, and automated back-ups for disaster recovery. +Since this feature moves our infrastructure away from managed filesystems (such as AWS's Elastic File System) that cannot support per-user storage quotas, we have also developed monitoring and alerting mechanisms that will let us know when the disks are getting full, and automated back-ups for disaster recovery. If you would like to try this on your 2i2c-managed hub, [please get in touch](https://docs.2i2c.org/support/).