diff --git a/config/clusters/2i2c/binderhub-ui-demo.values.yaml b/config/clusters/2i2c/binderhub-ui-demo.values.yaml new file mode 100644 index 0000000000..592170871d --- /dev/null +++ b/config/clusters/2i2c/binderhub-ui-demo.values.yaml @@ -0,0 +1,114 @@ +jupyterhub: + ingress: + hosts: + - hub.binderhub-ui-demo.2i2c.cloud + tls: + - secretName: https-auto-tls + hosts: + - hub.binderhub-ui-demo.2i2c.cloud + custom: + 2i2c: + add_staff_user_ids_to_admin_users: true + add_staff_user_ids_of_type: "google" + jupyterhubConfigurator: + enabled: false + binderhubUI: + enabled: true + homepage: + templateVars: + org: + name: Demo binderhub UI with binderhub-service + url: https://2i2c.org + logo_url: https://2i2c.org/media/logo.png + designed_by: + name: 2i2c + url: https://2i2c.org + operated_by: + name: 2i2c + url: https://2i2c.org + funded_by: + name: "" + url: "" + singleuserAdmin: + extraVolumeMounts: [] + singleuser: + storage: + type: none + extraVolumeMounts: [] + initContainers: [] + hub: + redirectToServer: false + services: + binder: + oauth_no_confirm: true + oauth_redirect_uri: https://binderhub-ui-demo.2i2c.cloud/oauth_callback + loadRoles: + binder: + services: + - binder + scopes: + - servers + - read:users # admin:users is required if authentication isn't enabled + user: + scopes: + - self + # Admin users will by default have access:services, so this is only + # observed to be required for non-admin users. + - access:services!service=binder + config: + BinderSpawnerMixin: + auth_enabled: true + JupyterHub: + authenticator_class: cilogon + CILogonOAuthenticator: + oauth_callback_url: "https://hub.binderhub-ui-demo.2i2c.cloud/hub/oauth_callback" + allowed_idps: + http://google.com/accounts/o8/id: + username_derivation: + username_claim: "email" +binderhub-service: + enabled: true + ingress: + enabled: true + hosts: + - binderhub-ui-demo.2i2c.cloud + tls: + - secretName: binder-https-auto-tls + hosts: + - binderhub-ui-demo.2i2c.cloud + config: + BinderHub: + base_url: / + hub_url: https://hub.binderhub-ui-demo.2i2c.cloud + badge_base_url: https://binderhub-ui-demo.2i2c.cloud + auth_enabled: true + enable_api_only_mode: false + image_prefix: us-central1-docker.pkg.dev/two-eye-two-see/binderhub-ui-demo-registry/binderhub-service- + extraConfig: + # FIXME: set KubernetesBuildExecutor.push_secret again + # without this for some reason the build pods + # search after the wrong secret name (i.e. the default name) + # set by binderhub in KubernetesBuildExecutor.push_secret + 01-binderhub-service-set-push-secret: | + import os + c.KubernetesBuildExecutor.push_secret = os.environ["PUSH_SECRET_NAME"] + extraEnv: + - name: JUPYTERHUB_API_TOKEN + valueFrom: + secretKeyRef: + name: '{{ include "jupyterhub.hub.fullname" . }}' + key: hub.services.binder.apiToken + - name: JUPYTERHUB_CLIENT_ID + value: "service-binder" + - name: JUPYTERHUB_API_URL + value: "https://hub.binderhub-ui-demo.2i2c.cloud/hub/api" + # Without this, the redirect URL to /hub/api/... gets + # appended to binderhub's URL instead of the hub's + - name: JUPYTERHUB_BASE_URL + value: "https://hub.binderhub-ui-demo.2i2c.cloud/" + - name: JUPYTERHUB_OAUTH_CALLBACK_URL + value: "https://binderhub-ui-demo.2i2c.cloud/oauth_callback" + # The password to the registry is stored encrypted in the hub's encrypted config file + buildPodsRegistryCredentials: + server: "https://us-central1-docker.pkg.dev" + username: "_json_key" diff --git a/config/clusters/2i2c/cluster.yaml b/config/clusters/2i2c/cluster.yaml index c37b44a659..e20ad4a2f9 100644 --- a/config/clusters/2i2c/cluster.yaml +++ b/config/clusters/2i2c/cluster.yaml @@ -41,6 +41,14 @@ hubs: - basehub-common.values.yaml - imagebuilding-demo.values.yaml - enc-imagebuilding-demo.secret.values.yaml + - name: binderhub-ui-demo + display_name: "2i2c Binderhub UI demo" + domain: hub.binderhub-ui-demo-demo.2i2c.cloud + helm_chart: basehub + helm_chart_values_files: + - basehub-common.values.yaml + - binderhub-ui-demo.values.yaml + - enc-binderhub-ui-demo.secret.values.yaml - name: demo display_name: "2i2c demo" domain: demo.2i2c.cloud diff --git a/config/clusters/2i2c/enc-binderhub-ui-demo.secret.values.yaml b/config/clusters/2i2c/enc-binderhub-ui-demo.secret.values.yaml new file mode 100644 index 0000000000..81ed9cee1a --- /dev/null +++ b/config/clusters/2i2c/enc-binderhub-ui-demo.secret.values.yaml @@ -0,0 +1,23 @@ +binderhub-service: + buildPodsRegistryCredentials: + password: ENC[AES256_GCM,data:oIqMepLt8hmsRTLhbZvDigfozMw9mIW4sYn+JWamN2k/HrQWq9OkAJr17UznADanwh4ClXwHMhjUTAf77HlpdHfnFReerIzvD+DscPnp3oFMIXU50Z2iZAUdz8PwOUm5UYk2/Tj8W/gi3bs/XJsPL4udI7GMn6T38d9NdExZKA98wb2Ka24rV5SnDAhLvMgCYCDvLgGZu7fqcv9UluavhN5T1bybDJcaOHQZePnbhB+QV1j/HHWy8AnwHjFoGd2ZiErzh9NF4nNhINVpgsWAo2VMZWbFrfqgrKylzDQpPEdkuEmFZI0WhFJHMXhKW4Ab0Pp+O7Agb9ol6LYpV4O8Ji+8FmtQp+09Ch8TsK9JjPAAVKRrx1upLhcCMzqvhJXagFSUPi625AOnxxtZ33VIFqTzIzNvhCab8BU5KDQ3RdJkaHHfs5N2I/+6F/YRQWkq3naz10pDVmeZ+wPNPlCYLCCYmwiKhuHvnNB9K+tloV1PPncZxcTLJMp9QghmiMLt+nZ1M7ofnXnR5CBqYKaHxqtJ5uVU3qxeR2lUm1PunfN7CY/BmpdsBiJblflseSTJy5XCE1qACwY68H0X6+VKt9FcNrVZZrmsoeXmyuzqiWG+PgUEV6W/RSzSCjt8dQqKeKUNdh2An9fONGqipNqfLnHKFppcwi+cCg7kQzwhTFS2mgmTLuJgzu3MGaCsBbWFOSOeNy8P//Htdk3UzUPlJV9Q6KNFTGzHuHWTyZGXTY+mrmG1YZ4QJvOAvve5Foq3L7f/YV9J+50l86aQCrdBZDOeN0FVfw8MamOt/0VeX2IdzDKgSL2cXjAlq9nEXSOZTqZ2wDFNUl7sKPD6Y/eh4+GbsA4V6+rJ28IaqWNBOaIFeM77Jwejc+C4umCW1q961ojR2WHJWmQv0521VnMk/GWOJIIyUWR6qDsyDXBqdTdnW4AiAuTfFissnrQfEVyqw180x+8L1+vvN9hMfOutNgdyQO1AR38/qYNuWENSoefZkoHoYb+k+Bo8if/CK+P9VzdnQtELx+SYkUm9I5ZtBoeR3HJtvYkwi84tOFM9RNQBR8LyDIViRf1x9dIdJDhMFldVDY/ubci+EpVTVT5qOyuTRSlKio7boi/aO6JP2fGPLHdUW87ViprRQko3NfBpYsH5GxEeiF2mLv39g6RCHlaqLK5Ax2/bXoE30GRZz7NF5mNVa0lFFQaPS0HM/Xr1vzU/HzFlfwdBtQIJ08qT2qRptn8qKTmByH5FZNa4NBu+d4rHhUuku0+W4iMHHM2ZSqlGODr5ZtMwQb1uNiBg5bJdeRJoCx3CN8Xb5qS9rZ/+5pGDwJqEhHDoJBL55bDKvS40mEU60HGJfvcAUj3GNl9Dmj/IhDuSoX3Uwj/ZgMlEn4S16DmL/lMzdXoqmLbkknXOc8wW6CJKIErQts8BpgP4VpOwNCdHLMcNkh6tSm+rPCShg+i0REj/4SACPNGiMvTBy133AXZuYCKKWrJsVbSgnk9xDp1JSppnnJyxdMyWDe6L4Ebathg13o4Bs8iNrnOhnTtu2UDaKawHi9A/WBuhGG0/vjZ8/1Zd1kxHtNF7H6NmHA0c7XFRvqqHxyK/GCCnpTTUwEzbG+AumVAFGl8RXQOi6OdCuvNxzX5bsWtxOdFJPc9s4ZsICduSeo9hemS5sqwuNrQKohFCjEGOmg4ET0R6nA8JChN4QvO53KqonWamuCKwtErh+IeIucuWWPN5sPfEcaPCUw6kezNZA5JcwR1FzpGnhweImU+GKVomK1C8GhlxTsvlpnPlHJXE7pr66CwWTqaiRkIejdVQViD2U0fPQqBdvWn360Eafgt5GkzUcMb1ylUKWUBw1rOe57QSV3R9XQrywvgQmgjMx6Vh6etNbmi2bUw0CCy0BkQH4Yoat/FM9HcCyMkxB4pq0q6sPVI4NJKZdEAzKbwSZjcyzcUvTR3RWLtJDy/QBguoO3f3q55ofvdFhkDhVpRK4cEX7YpbFpEBQcnFkyPQMSPQN3Gc6QvaLS3yktX4uhS0NhIcknvkeLXNBnQDYWwE6uCEQsVVQD27ehB+dGwMzzKhTGwLL90xH1Lv0u1vONTPLRCVmak8eVEiS0iDbVM4rtpHLgCZ/Bu772yvzFdGDjUX3yMRPFuJbeVhzPRT9pXneCNGOngVV8ijSXn51Y19+7DYqRjzAi/DYX3LY9FyVi1UhLLPeT+wfxOfter/cts+lZMPDCfcSjTqD+DNYT3psGz+F19InjoYVy4xRlggOvo3GPLRnn4gbaGiGh4i4JK1VM4O/j637UqoRaWlMBuYd2aN2HnvMTnflYnr0SoNP7s28930bICmWMKnOeCjkaLPrJe/KisZRobA5FvysRU7Cv4UCq4//ejVqSV6LNve5I+EN5f/X0FkRCN4B3YyskajLweC8bnnxxLItGxV8XdCV134rACob8fUl2xf8HGGnmn+rYJxTNO2GTIlYzRAdwfydMhOT8XabB0safuLHUj/T91EDypKZJ0LDH/s4fVBHHJCybPORik7rYLdkPwsVieCKsCCClqQQPOkTjTVwImxFfYS4/xTjKT+UUsdQMS+TueZhPRWFe99SHdIX3a7d0eYA/VMFYEyAtXY719e5BOSRs4qgxA49dRT2AfyQV7RWW+qccWKopGSa2STsJfBeU/+fAFcEvejFwCYQsGG7RFOyxIR1DxI9cdI0o74agZtvZsIDij1bpmwoAh7kCjTPVNXY0LVzIGUKLAoZYfvEcGDTLHxRnrFoCnzycRvPxocK3nOsanehR+ZQUx94+Mtn378f1txtwL65G6HeKPk/M3QUAjBiypjXnvlG/Ea4TzvS6/iEBkr9HFx5fwaps2YQulnb8AtkVRFRw94V3g3KV/Zbl4NNdhAwWZc8Qxr99tQ3Wkmv7Fv2uJ0SihpcaEjjJPzZEkbD47khYpCgwfjKOId2ENNUj0PuUl1cl1x9cZgF+0cz2ySIYlFvRms9nufTQ8eczg2cV7lWZPNn53ZpNpFqrQ5/FcYmXGUSCXrsaHY+NP4KkCJmDRt0SLNBZPAa09I5WQUgx/qi07W20ApEu9jzyyG2WpMvId8OCbRJ7CYAEKjWbj4052sA6VsZMfWkBBzYK5PtdTmor9cPkt1Km4YwRMZkPFeciJxfFKIamtAY3/UCmpLOASc7H4QIl0bizLUpYxpORAUPdLGSATPiQqXzvvj,iv:O57KfiTHSPiNYyrN+rJG5weJdfsrZOUO0fyMHUuIKKk=,tag:GCvuufg1uyZw6DIg17rEdw==,type:str] +jupyterhub: + hub: + config: + CILogonOAuthenticator: + client_id: ENC[AES256_GCM,data:tEZMVs9TkdzJaAqsbF+uNX2uwhTCnsuJnxcwJD3rSB0FXt481yze4Rs8t/5Ef/npccE=,iv:bJzGDmlZArcO3yfUO9mCQDDZI7jMDjeC66U7zvBWSKI=,tag:Xt8OKd/iFHik6a6QesR7wg==,type:str] + client_secret: ENC[AES256_GCM,data:AwgZagRLArMGhZji3jon4YVjfblBmp3wyTmqvOFg7mZs5LMiFNrlnAtTQ8oqv5O4QQfyeMGZhustlnmmWMW9Vqtxkax3zljiPfAA0zfy/Ie4t0085Nw=,iv:vwPzC67oR7vzOHEyDMs2PDc19mRRPMkIjSDScT6M0Cc=,tag:imEiXTjo7KGB9Wl2G8eyMQ==,type:str] +sops: + kms: [] + gcp_kms: + - resource_id: projects/two-eye-two-see/locations/global/keyRings/sops-keys/cryptoKeys/similar-hubs + created_at: "2023-09-18T19:00:41Z" + enc: CiUA4OM7eFioG9yDgVwKtc0cYrU65GNcqMSDuUgnuXuq3KW9dRI6EkkAq2nhVV2TFrZOq5jktjMd4TQF1lwH/08tAyGd3vMfBmdd3Xdy3bAUUHhrPXcK6QabMRYdXPzQzgB+oBGaqOsJO7D7jT9NpeCn + azure_kv: [] + hc_vault: [] + age: [] + lastmodified: "2024-05-28T11:32:40Z" + mac: ENC[AES256_GCM,data:DHS9gRya0PThBNrc+2ImEnzLLGIKGVpCkZIn70AjxgY5XTM8oPCChsF72n7b08BEDE6muncb/52Baqbi01nkmDJZLn/TAgIhMmJHmsFYyLxwAQ4IefKOWUtm+zYjRj8ybZxwV3ZItVnoyC9bS+PIC76+HFe8hmK2dzCPt44Sjbc=,iv:AGZy4gL5eb8C15JN+6xBlg7Dctt7DJGrwVytnrtgVMw=,tag:mlbhJCJljDunsXy5FM4B8Q==,type:str] + pgp: [] + unencrypted_suffix: _unencrypted + version: 3.8.1 diff --git a/helm-charts/basehub/Chart.yaml b/helm-charts/basehub/Chart.yaml index 8cbd4d190e..801066c876 100644 --- a/helm-charts/basehub/Chart.yaml +++ b/helm-charts/basehub/Chart.yaml @@ -15,6 +15,6 @@ dependencies: version: 3.3.7 repository: https://jupyterhub.github.io/helm-chart/ - name: binderhub-service - version: 0.1.0-0.dev.git.110.hd833d08 + version: 0.1.0-0.dev.git.240.hdaf17cc repository: https://2i2c.org/binderhub-service/ condition: binderhub-service.enabled diff --git a/helm-charts/basehub/values.schema.yaml b/helm-charts/basehub/values.schema.yaml index bf8fec4315..f638c04201 100644 --- a/helm-charts/basehub/values.schema.yaml +++ b/helm-charts/basehub/values.schema.yaml @@ -516,3 +516,11 @@ properties: properties: enabled: type: boolean + binderhubUI: + type: object + additionalProperties: false + required: + - enabled + properties: + enabled: + type: boolean diff --git a/helm-charts/basehub/values.yaml b/helm-charts/basehub/values.yaml index 4f376ff40f..45bba46d74 100644 --- a/helm-charts/basehub/values.yaml +++ b/helm-charts/basehub/values.yaml @@ -11,6 +11,12 @@ adminServiceAccount: binderhub-service: enabled: false + ingress: + enabled: false + ingressClassName: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-body-size: 256m + cert-manager.io/cluster-issuer: letsencrypt-prod nodeSelector: hub.jupyter.org/node-purpose: core service: @@ -820,6 +826,125 @@ jupyterhub: limits: memory: 2Gi extraConfig: + # This is copy-pasted exactly from https://github.com/jupyterhub/binderhub/blob/c6c5dc8fe73f81ca538c47b420b33f317c3aa8ae/helm-chart/binderhub/values.yaml#L87 + # Should be updated every time the upstream code changes + 0-binderspawnermixin: | + """ + Helpers for creating BinderSpawners + + FIXME: + This file is defined in binderhub/binderspawner_mixin.py + and is copied to helm-chart/binderhub/values.yaml + by ci/check_embedded_chart_code.py + + The BinderHub repo is just used as the distribution mechanism for this spawner, + BinderHub itself doesn't require this code. + + Longer term options include: + - Move BinderSpawnerMixin to a separate Python package and include it in the Z2JH Hub + image + - Override the Z2JH hub with a custom image built in this repository + - Duplicate the code here and in binderhub/binderspawner_mixin.py + """ + + from tornado import web + from traitlets import Bool, Unicode + from traitlets.config import Configurable + + + class BinderSpawnerMixin(Configurable): + """ + Mixin to convert a JupyterHub container spawner to a BinderHub spawner + + Container spawner must support the following properties that will be set + via spawn options: + - image: Container image to launch + - token: JupyterHub API token + """ + + def __init__(self, *args, **kwargs): + # Is this right? Is it possible to having multiple inheritance with both + # classes using traitlets? + # https://stackoverflow.com/questions/9575409/calling-parent-class-init-with-multiple-inheritance-whats-the-right-way + # https://github.com/ipython/traitlets/pull/175 + super().__init__(*args, **kwargs) + + auth_enabled = Bool( + False, + help=""" + Enable authenticated binderhub setup. + + Requires `jupyterhub-singleuser` to be available inside the repositories + being built. + """, + config=True, + ) + + cors_allow_origin = Unicode( + "", + help=""" + Origins that can access the spawned notebooks. + + Sets the Access-Control-Allow-Origin header in the spawned + notebooks. Set to '*' to allow any origin to access spawned + notebook servers. + + See also BinderHub.cors_allow_origin in binderhub config + for controlling CORS policy for the BinderHub API endpoint. + """, + config=True, + ) + + def get_args(self): + if self.auth_enabled: + args = super().get_args() + else: + args = [ + "--ip=0.0.0.0", + f"--port={self.port}", + f"--NotebookApp.base_url={self.server.base_url}", + f"--NotebookApp.token={self.user_options['token']}", + "--NotebookApp.trust_xheaders=True", + ] + if self.default_url: + args.append(f"--NotebookApp.default_url={self.default_url}") + + if self.cors_allow_origin: + args.append("--NotebookApp.allow_origin=" + self.cors_allow_origin) + # allow_origin=* doesn't properly allow cross-origin requests to single files + # see https://github.com/jupyter/notebook/pull/5898 + if self.cors_allow_origin == "*": + args.append("--NotebookApp.allow_origin_pat=.*") + args += self.args + # ServerApp compatibility: duplicate NotebookApp args + for arg in list(args): + if arg.startswith("--NotebookApp."): + args.append(arg.replace("--NotebookApp.", "--ServerApp.")) + return args + + def start(self): + if not self.auth_enabled: + if "token" not in self.user_options: + raise web.HTTPError(400, "token required") + if "image" not in self.user_options: + raise web.HTTPError(400, "image required") + if "image" in self.user_options: + self.image = self.user_options["image"] + return super().start() + + def get_env(self): + env = super().get_env() + if "repo_url" in self.user_options: + env["BINDER_REPO_URL"] = self.user_options["repo_url"] + for key in ( + "binder_ref_url", + "binder_launch_host", + "binder_persistent_request", + "binder_request", + ): + if key in self.user_options: + env[key.upper()] = self.user_options[key] + return env 01-custom-theme: | # adds a JupyterHub template path and updates template variables @@ -861,6 +986,8 @@ jupyterhub: } c.JupyterHub.services.append(jhc_service) + if get_config("custom.binderhubUI.enabled"): + spawner_base_classes = [BinderSpawnerMixin, KubeSpawner] class BaseHubSpawner(*spawner_base_classes): def start(self, *args, **kwargs): @@ -883,6 +1010,7 @@ jupyterhub: self.service_account = admin_service_account return super().start(*args, **kwargs) + c.JupyterHub.spawner_class = BaseHubSpawner diff --git a/terraform/gcp/projects/pilot-hubs.tfvars b/terraform/gcp/projects/pilot-hubs.tfvars index 8f3cfbf616..918807f310 100644 --- a/terraform/gcp/projects/pilot-hubs.tfvars +++ b/terraform/gcp/projects/pilot-hubs.tfvars @@ -77,4 +77,5 @@ hub_cloud_permissions = { container_repos = [ "binder-staging", + "binderhub-ui-demo" ]