├── .code.yml
├── .github
└── workflows
│ └── go.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── Makefile
├── PROJECT
├── README.md
├── api
└── v1alpha1
│ ├── groupversion_info.go
│ ├── jupytergateway_types.go
│ ├── jupyterkernel_types.go
│ ├── jupyterkernelspec_types.go
│ ├── jupyterkerneltemplate_types.go
│ ├── jupyternotebook_types.go
│ └── zz_generated.deepcopy.go
├── cli
├── README.md
├── cmd
│ └── root.go
└── main.go
├── config
├── certmanager
│ ├── certificate.yaml
│ ├── kustomization.yaml
│ └── kustomizeconfig.yaml
├── crd
│ ├── bases
│ │ ├── kubeflow.tkestack.io_jupytergateways.yaml
│ │ ├── kubeflow.tkestack.io_jupyterkernels.yaml
│ │ ├── kubeflow.tkestack.io_jupyterkernelspecs.yaml
│ │ ├── kubeflow.tkestack.io_jupyterkerneltemplates.yaml
│ │ └── kubeflow.tkestack.io_jupyternotebooks.yaml
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── patches
│ │ ├── cainjection_in_jupytergateways.yaml
│ │ ├── cainjection_in_jupyterkernels.yaml
│ │ ├── cainjection_in_jupyterkernelspecs.yaml
│ │ ├── cainjection_in_jupyterkerneltemplates.yaml
│ │ ├── cainjection_in_jupyternotebooks.yaml
│ │ ├── webhook_in_jupytergateways.yaml
│ │ ├── webhook_in_jupyterkernels.yaml
│ │ ├── webhook_in_jupyterkernelspecs.yaml
│ │ ├── webhook_in_jupyterkerneltemplates.yaml
│ │ └── webhook_in_jupyternotebooks.yaml
├── default
│ └── kustomization.yaml
├── manager
│ ├── kustomization.yaml
│ └── manager.yaml
├── prometheus
│ ├── kustomization.yaml
│ └── monitor.yaml
├── rbac
│ ├── auth_proxy_client_clusterrole.yaml
│ ├── auth_proxy_role.yaml
│ ├── auth_proxy_role_binding.yaml
│ ├── auth_proxy_service.yaml
│ ├── jupytergateway_editor_role.yaml
│ ├── jupytergateway_viewer_role.yaml
│ ├── jupyterkernel_editor_role.yaml
│ ├── jupyterkernel_viewer_role.yaml
│ ├── jupyterkernelspec_editor_role.yaml
│ ├── jupyterkernelspec_viewer_role.yaml
│ ├── jupyterkerneltemplate_editor_role.yaml
│ ├── jupyterkerneltemplate_viewer_role.yaml
│ ├── jupyternotebook_editor_role.yaml
│ ├── jupyternotebook_viewer_role.yaml
│ ├── kustomization.yaml
│ ├── leader_election_role.yaml
│ ├── leader_election_role_binding.yaml
│ ├── role.yaml
│ └── role_binding.yaml
├── samples
│ ├── kubeflow.tkestack.io_v1alpha1_jupytergateway-kernels.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkernel.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkernelspec-custom-launcher.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
│ └── kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
└── webhook
│ ├── kustomization.yaml
│ ├── kustomizeconfig.yaml
│ └── service.yaml
├── controllers
├── jupytergateway_controller.go
├── jupyterkernel_controller.go
├── jupyterkernelspec_controller.go
├── jupyterkerneltemplate_controller.go
├── jupyternotebook_controller.go
├── jupyternotebook_controller_test.go
└── suite_test.go
├── docs
├── README.md
├── api
│ ├── autogen
│ │ ├── config.yaml
│ │ └── templates
│ │ │ ├── gv_details.tpl
│ │ │ ├── gv_list.tpl
│ │ │ ├── type.tpl
│ │ │ └── type_members.tpl
│ └── generated.asciidoc
├── design.md
├── images
│ ├── arch.jpeg
│ ├── elastic.jpeg
│ ├── gateway.png
│ ├── jupyter.jpeg
│ ├── kubeflow.jpeg
│ ├── multiuser.jpeg
│ ├── overview.jpeg
│ └── uml.jpeg
├── kernel.md
└── quick-start.md
├── examples
├── elastic-with-custom-kernels
│ ├── kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
│ ├── kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
│ └── kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
├── elastic
│ ├── kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
│ └── kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
└── simple-deployments
│ └── kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
├── go.mod
├── go.sum
├── hack
├── add-license.sh
├── boilerplate.go.txt
├── enterprise_gateway
│ └── prepare.yaml
└── license.txt
├── main.go
└── pkg
├── gateway
├── generate.go
└── reconcile.go
├── kernel
├── generate.go
└── reconcile.go
├── kernelspec
├── generate.go
├── reconcile.go
└── types.go
└── notebook
├── generate.go
├── generate_test.go
├── reconcile.go
└── reconcile_test.go
/.code.yml:
--------------------------------------------------------------------------------
1 | source:
2 | third_party_source:
3 | filepath_regex: [".*/cmd/.*",
4 | ".*/examples/.*",
5 | ".*/hack/.*",
6 | ".*/installer/.*",
7 | ".*/pkg/.*",
8 | ".*/third_party/.*",
9 | ".*/build/.*",
10 | ".*/test/.*"]
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v2
18 | with:
19 | go-version: 1.17
20 |
21 | - name: Reuse go mod cache
22 | uses: actions/cache@v2
23 | with:
24 | path: ~/go/pkg/mod
25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
26 | restore-keys: |
27 | ${{ runner.os }}-go-
28 |
29 | - name: Build the operator
30 | run: go build -v ./main.go
31 |
32 | - name: Build the kubeflow-launcher
33 | run: go build -v ./cli/main.go
34 |
35 | test:
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v2
39 |
40 | - name: Set up Go
41 | uses: actions/setup-go@v2
42 | with:
43 | go-version: 1.17
44 |
45 | - name: Reuse go mod cache
46 | uses: actions/cache@v2
47 | with:
48 | path: ~/go/pkg/mod
49 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
50 | restore-keys: |
51 | ${{ runner.os }}-go-
52 |
53 | - name: Test
54 | run: |
55 | curl -L -O "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.0/kubebuilder_2.3.0_linux_amd64.tar.gz"
56 | tar -zxvf kubebuilder_2.3.0_linux_amd64.tar.gz
57 | sudo mv kubebuilder_2.3.0_linux_amd64 /usr/local/kubebuilder
58 | export PATH=$PATH:/usr/local/kubebuilder/bin
59 | go test -v ./...
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 | bin
9 |
10 | # Test binary, build with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 |
16 | # Kubernetes Generated files - skip generated files, except for vendored files
17 |
18 | !vendor/**/zz_generated.*
19 |
20 | # editor and IDE paraphernalia
21 | .idea
22 | *.swp
23 | *.swo
24 | *~
25 |
26 | vendor/
27 | .vscode/
28 | /elastic-jupyter-operator
29 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "enterprise_gateway"]
2 | path = enterprise_gateway
3 | url = git@github.com:skai-x/enterprise_gateway.git
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [Unreleased](https://github.com/tkestack/elastic-jupyter-operator/tree/HEAD)
4 |
5 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/v0.2.0...HEAD)
6 |
7 | **Merged pull requests:**
8 |
9 | - \[typo\] fix typo in README.md [\#40](https://github.com/tkestack/elastic-jupyter-operator/pull/40) ([mkkb473](https://github.com/mkkb473))
10 |
11 | ## [v0.2.0](https://github.com/tkestack/elastic-jupyter-operator/tree/v0.2.0) (2021-09-28)
12 |
13 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/v0.1.1...v0.2.0)
14 |
15 | **Implemented enhancements:**
16 |
17 | - feat\(notebook\): Avoid token in the URL [\#34](https://github.com/tkestack/elastic-jupyter-operator/issues/34)
18 | - chore: Support CI with GitHub Actions [\#24](https://github.com/tkestack/elastic-jupyter-operator/issues/24)
19 |
20 | **Fixed bugs:**
21 |
22 | - \[feat\] Set the kernel owner reference to gateway [\#18](https://github.com/tkestack/elastic-jupyter-operator/issues/18)
23 |
24 | **Closed issues:**
25 |
26 | - kernel: Support cullout [\#28](https://github.com/tkestack/elastic-jupyter-operator/issues/28)
27 | - \[feat\] Design Kernel CRD to ease the maintanance [\#4](https://github.com/tkestack/elastic-jupyter-operator/issues/4)
28 |
29 | **Merged pull requests:**
30 |
31 | - fix: Fix the jupyter kernel spec name [\#39](https://github.com/tkestack/elastic-jupyter-operator/pull/39) ([gaocegege](https://github.com/gaocegege))
32 | - feat: Add docs and new api doc [\#38](https://github.com/tkestack/elastic-jupyter-operator/pull/38) ([gaocegege](https://github.com/gaocegege))
33 | - feat\(notebook\): Support auth configuration [\#36](https://github.com/tkestack/elastic-jupyter-operator/pull/36) ([gaocegege](https://github.com/gaocegege))
34 | - feat\(examples\): Add examples for simple jupyter [\#35](https://github.com/tkestack/elastic-jupyter-operator/pull/35) ([gaocegege](https://github.com/gaocegege))
35 | - feat: Support cullout [\#33](https://github.com/tkestack/elastic-jupyter-operator/pull/33) ([gaocegege](https://github.com/gaocegege))
36 | - feat: Add loglevel [\#32](https://github.com/tkestack/elastic-jupyter-operator/pull/32) ([gaocegege](https://github.com/gaocegege))
37 | - feat\(launcher\): Set owner for kernel [\#31](https://github.com/tkestack/elastic-jupyter-operator/pull/31) ([gaocegege](https://github.com/gaocegege))
38 | - fix: Resize the image in README [\#30](https://github.com/tkestack/elastic-jupyter-operator/pull/30) ([gaocegege](https://github.com/gaocegege))
39 | - chore: Update docs [\#29](https://github.com/tkestack/elastic-jupyter-operator/pull/29) ([gaocegege](https://github.com/gaocegege))
40 | - chore: Add go mod cache [\#27](https://github.com/tkestack/elastic-jupyter-operator/pull/27) ([gaocegege](https://github.com/gaocegege))
41 | - chore: Add CLI README and print events when there is any problem creating gateway CR [\#23](https://github.com/tkestack/elastic-jupyter-operator/pull/23) ([gaocegege](https://github.com/gaocegege))
42 |
43 | ## [v0.1.1](https://github.com/tkestack/elastic-jupyter-operator/tree/v0.1.1) (2021-06-02)
44 |
45 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/v0.1.0...v0.1.1)
46 |
47 | **Closed issues:**
48 |
49 | - \[feat\] Support Launching Notebooks with CRD Directly [\#8](https://github.com/tkestack/elastic-jupyter-operator/issues/8)
50 |
51 | **Merged pull requests:**
52 |
53 | - feat\(kernel\): Add Kernel CRD [\#19](https://github.com/tkestack/elastic-jupyter-operator/pull/19) ([gaocegege](https://github.com/gaocegege))
54 | - feat: Add Kernel Launcher and KernelTemplate and KernelSpec CRD [\#17](https://github.com/tkestack/elastic-jupyter-operator/pull/17) ([gaocegege](https://github.com/gaocegege))
55 | - chore: Add docs for kernel [\#11](https://github.com/tkestack/elastic-jupyter-operator/pull/11) ([gaocegege](https://github.com/gaocegege))
56 |
57 | ## [v0.1.0](https://github.com/tkestack/elastic-jupyter-operator/tree/v0.1.0) (2021-04-22)
58 |
59 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/v0.1.0-rc.1...v0.1.0)
60 |
61 | **Closed issues:**
62 |
63 | - \[bug\] Kernel completed when run [\#13](https://github.com/tkestack/elastic-jupyter-operator/issues/13)
64 | - \[feat\] Use 2.5.0 enterprise gateway [\#9](https://github.com/tkestack/elastic-jupyter-operator/issues/9)
65 |
66 | **Merged pull requests:**
67 |
68 | - fix: Add testcases to notebook and fix generate logic for notebook deployment [\#16](https://github.com/tkestack/elastic-jupyter-operator/pull/16) ([Mirrored90](https://github.com/Mirrored90))
69 | - feat\(notebook\): Support Launching Notebooks with CRD Directly [\#15](https://github.com/tkestack/elastic-jupyter-operator/pull/15) ([Mirrored90](https://github.com/Mirrored90))
70 | - feat: Add a new CRD KernelSpec [\#12](https://github.com/tkestack/elastic-jupyter-operator/pull/12) ([gaocegege](https://github.com/gaocegege))
71 | - feat\(gateway\): change dev tag to 2.5.0 [\#10](https://github.com/tkestack/elastic-jupyter-operator/pull/10) ([Mirrored90](https://github.com/Mirrored90))
72 | - fix: Fix readme [\#7](https://github.com/tkestack/elastic-jupyter-operator/pull/7) ([pokerfaceSad](https://github.com/pokerfaceSad))
73 |
74 | ## [v0.1.0-rc.1](https://github.com/tkestack/elastic-jupyter-operator/tree/v0.1.0-rc.1) (2021-04-02)
75 |
76 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/v0.1.0-rc.0...v0.1.0-rc.1)
77 |
78 | **Merged pull requests:**
79 |
80 | - fix: Fix ignore file [\#6](https://github.com/tkestack/elastic-jupyter-operator/pull/6) ([gaocegege](https://github.com/gaocegege))
81 |
82 | ## [v0.1.0-rc.0](https://github.com/tkestack/elastic-jupyter-operator/tree/v0.1.0-rc.0) (2021-03-01)
83 |
84 | [Full Changelog](https://github.com/tkestack/elastic-jupyter-operator/compare/c194c0ee41da0d42bf156827f53066c4b259e557...v0.1.0-rc.0)
85 |
86 | **Merged pull requests:**
87 |
88 | - feat: Update docs [\#5](https://github.com/tkestack/elastic-jupyter-operator/pull/5) ([gaocegege](https://github.com/gaocegege))
89 | - fix: Update readme [\#2](https://github.com/tkestack/elastic-jupyter-operator/pull/2) ([gaocegege](https://github.com/gaocegege))
90 | - fix: Fix rbac issues [\#1](https://github.com/tkestack/elastic-jupyter-operator/pull/1) ([gaocegege](https://github.com/gaocegege))
91 |
92 |
93 |
94 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
95 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build the elastic-jupyter-operator binary
2 | FROM golang:1.17.6-alpine as builder
3 |
4 | WORKDIR /workspace
5 | # Copy the Go Modules manifests
6 | COPY go.mod go.mod
7 | COPY go.sum go.sum
8 |
9 | # Download libs first to use docker buildx caching
10 | RUN go mod download
11 | RUN go mod verify
12 |
13 | # Copy the go source
14 | COPY main.go main.go
15 | COPY api/ api/
16 | COPY controllers/ controllers/
17 | COPY pkg/ pkg/
18 |
19 | # Build
20 | RUN CGO_ENABLED=0 go build -a -o elastic-jupyter-operator main.go
21 |
22 | # Use distroless as minimal base image to package the elastic-jupyter-operator binary
23 | # Refer to https://github.com/GoogleContainerTools/distroless for more details
24 | FROM gcr.io/distroless/static:nonroot
25 |
26 | LABEL org.opencontainers.image.source https://github.com/tkestack/elastic-jupyter-operator
27 |
28 | WORKDIR /
29 | COPY --from=builder /workspace/elastic-jupyter-operator .
30 | USER nonroot:nonroot
31 |
32 | ENTRYPOINT ["/elastic-jupyter-operator"]
33 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 |
2 | # Image URL to use all building/pushing image targets
3 | IMG ?= ghcr.io/skai-x/elastic-jupyter-operator:latest
4 | REGISTRY_IMG ?= ghcr.io/skai-x/enterprise-gateway:latest
5 | REGISTRY_K8S_IMG ?= ghcr.io/skai-x/enterprise-gateway-with-kernel-spec:latest
6 | KERNEL_PY_IMG ?= ghcr.io/skai-x/jupyter-kernel-py:2.6.0
7 | KERNEL_R_IMG ?= ghcr.io/skai-x/jupyter-kernel-r:2.6.0
8 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
9 | CRD_OPTIONS ?= "crd:trivialVersions=true"
10 |
11 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
12 | ifeq (,$(shell go env GOBIN))
13 | GOBIN=$(shell go env GOPATH)/bin
14 | else
15 | GOBIN=$(shell go env GOBIN)
16 | endif
17 |
18 | all: manager
19 |
20 | # Run tests
21 | test: generate fmt vet manifests
22 | go test ./... -coverprofile cover.out
23 |
24 | # Build manager binary
25 | manager: generate fmt vet
26 | go build -o bin/elastic-jupyter-operator main.go
27 |
28 | # Run against the configured Kubernetes cluster in ~/.kube/config
29 | run: generate fmt vet manifests
30 | go run ./main.go
31 |
32 | # Install CRDs into a cluster
33 | install: manifests kustomize
34 | $(KUSTOMIZE) build config/crd | kubectl apply -f -
35 |
36 | # Uninstall CRDs from a cluster
37 | uninstall: manifests kustomize
38 | $(KUSTOMIZE) build config/crd | kubectl delete -f -
39 |
40 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config
41 | deploy: manifests kustomize
42 | cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
43 | $(KUSTOMIZE) build config/default | kubectl apply -f -
44 |
45 | # Generate manifests e.g. CRD, RBAC etc.
46 | manifests: controller-gen
47 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
48 |
49 | # Run go fmt against code
50 | fmt:
51 | go fmt ./...
52 |
53 | # Run go vet against code
54 | vet:
55 | go vet ./...
56 |
57 | # Generate code
58 | generate: controller-gen api-reference
59 | $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
60 |
61 | api-reference: install-tools ## Generate API reference documentation
62 | $(GOBIN)/crd-ref-docs \
63 | --source-path ./api/v1alpha1 \
64 | --config ./docs/api/autogen/config.yaml \
65 | --templates-dir ./docs/api/autogen/templates \
66 | --output-path ./docs/api/generated.asciidoc \
67 | --max-depth 30
68 |
69 | # Build the docker image
70 | docker-build: test
71 | docker build . -t ${IMG}
72 |
73 | # Push the docker image
74 | docker-push:
75 | docker buildx build --push --platform linux/amd64,linux/arm64 --tag ${IMG} .
76 |
77 | # find or download controller-gen
78 | # download controller-gen if necessary
79 | controller-gen:
80 | ifeq (, $(shell which controller-gen))
81 | @{ \
82 | set -e ;\
83 | CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
84 | cd $$CONTROLLER_GEN_TMP_DIR ;\
85 | go mod init tmp ;\
86 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.5.0 ;\
87 | rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
88 | }
89 | CONTROLLER_GEN=$(GOBIN)/controller-gen
90 | else
91 | CONTROLLER_GEN=$(shell which controller-gen)
92 | endif
93 |
94 | kustomize:
95 | ifeq (, $(shell which kustomize))
96 | @{ \
97 | set -e ;\
98 | KUSTOMIZE_GEN_TMP_DIR=$$(mktemp -d) ;\
99 | cd $$KUSTOMIZE_GEN_TMP_DIR ;\
100 | go mod init tmp ;\
101 | go get sigs.k8s.io/kustomize/kustomize/v3@v3.5.4 ;\
102 | rm -rf $$KUSTOMIZE_GEN_TMP_DIR ;\
103 | }
104 | KUSTOMIZE=$(GOBIN)/kustomize
105 | else
106 | KUSTOMIZE=$(shell which kustomize)
107 | endif
108 |
109 | install-tools:
110 | go get github.com/elastic/crd-ref-docs
111 |
112 | enterprise-gateway:
113 | cd enterprise_gateway && python setup.py bdist_wheel \
114 | && rm -rf *.egg-info && cd - && \
115 | docker buildx build --push --platform linux/amd64,linux/arm64 --tag ${REGISTRY_IMG} -f enterprise_gateway/etc/docker/enterprise-gateway/Dockerfile .
116 |
117 | enterprise-gateway-k8s:
118 | cd enterprise_gateway && make kernelspecs && \
119 | python setup.py bdist_wheel \
120 | && rm -rf *.egg-info && cd - && \
121 | docker buildx build --push --platform linux/amd64,linux/arm64 --tag ${REGISTRY_K8S_IMG} -f enterprise_gateway/etc/docker/enterprise-gateway-k8s/Dockerfile .
122 |
123 | kernel: kernel-py kernel-r
124 |
125 | kernel-py:
126 | docker buildx build --push --platform linux/amd64,linux/arm64 --tag ${KERNEL_PY_IMG} -f enterprise_gateway/etc/docker/kernel-py/Dockerfile .
127 |
128 | kernel-r:
129 | docker buildx build --push --platform linux/amd64,linux/arm64 --tag ${KERNEL_R_IMG} -f enterprise_gateway/etc/docker/kernel-r/Dockerfile .
--------------------------------------------------------------------------------
/PROJECT:
--------------------------------------------------------------------------------
1 | layout: go.kubebuilder.io/v2
2 | projectName: elastic-jupyter-operator
3 | repo: github.com/tkestack/elastic-jupyter-operator
4 | resources:
5 | - group: kubeflow.tkestack.io
6 | kind: JupyterNotebook
7 | version: v1alpha1
8 | - group: kubeflow.tkestack.io
9 | kind: JupyterGateway
10 | version: v1alpha1
11 | - group: kubeflow.tkestack.io
12 | kind: JupyterKernelSpec
13 | version: v1alpha1
14 | version: 3-alpha
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elastic-jupyter-operator
2 |
3 | Elastic Jupyter Notebooks on Kubernetes
4 |
5 | ## Motivation
6 |
7 | Jupyter is a free, open-source, interactive web tool known as a computational notebook, which researchers can use to combine software code, computational output, explanatory text, and multimedia resources in a single document.
8 |
9 | For data scientists and machine learning engineers, Jupyter has emerged as a de facto standard. At the same time, there has been growing criticism that the way notebooks are being used leads to low resource utilization.
10 |
11 | GPU and other hardware resources will be bound to the specified notebooks even if the data scientists do not need them currently. This project proposes some Kubernetes CRDs to solve these problems.
12 |
13 | ## Introduction
14 |
15 | elastic-jupyter-operator provides elastic Jupyter notebook services with these features:
16 |
17 | - Provide users the out-of-box Jupyter notebooks on Kubernetes.
18 | - Autoscale Jupyter kernels when the kernels are not used within the given time frame to increase the resource utilization.
19 | - Customize the kernel configuration in runtime without restarting the notebook.
20 |
21 |

22 | Figure 1. elastic-jupyter-operator
23 |
24 | 
25 | Figure 2. Other Jupyter on Kubernetes solutions
26 |
27 | ## Deploy
28 |
29 | ```bash
30 | kubectl apply -f ./hack/enterprise_gateway/prepare.yaml
31 | make deploy
32 | ```
33 |
34 | ## Quickstart
35 |
36 | You can follow the [quickstart](./docs/quick-start.md) to create the notebook server and kernel in Kubernetes like this:
37 |
38 | ```bash
39 | NAME READY STATUS RESTARTS AGE
40 | jovyan-fd191444-b08c-4668-ba4e-3748a54a0ac1-5789574d66-tb5cm 1/1 Running 0 146m
41 | jupytergateway-sample-858bbc8d5c-xds44 1/1 Running 0 3h46m
42 | jupyternotebook-sample-5bf7d9d9fb-pdv9b 1/1 Running 10 77d
43 | ```
44 |
45 | There are three pods running in the demo:
46 |
47 | - `jupyternotebook-sample-5bf7d9d9fb-pdv9b` is the notebook server
48 | - `jupytergateway-sample-858bbc8d5c-xds44` is the jupyter gateway to support remote kernels
49 | - `jovyan-fd191444-b08c-4668-ba4e-3748a54a0ac1-5789574d66-tb5cm` is the remote kernel
50 |
51 | The kernel will be deleted if the notebook does not use it in 10 mins. And it will be recreated if there is any new run in the notebook.
52 |
53 | ## Community
54 |
55 | Please join [![Discord][discord-badge]][discord-url]
56 |
57 | [discord-badge]: https://img.shields.io/discord/913359799058587658?logo=Discord&style=flat-square
58 | [discord-url]: https://discord.gg/NJsd4guhPM
59 |
60 | ## Design
61 |
62 | Please refer to [design doc](docs/design.md)
63 |
64 | ## API Documentation
65 |
66 | Please refer to [API doc](docs/api/generated.asciidoc)
67 |
68 | ## Special Thanks
69 |
70 | - [jupyter/enterprise_gateway](https://github.com/jupyter/enterprise_gateway) which implements the logic to run kernels remotely
71 |
--------------------------------------------------------------------------------
/api/v1alpha1/groupversion_info.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | // Package v1alpha1 contains API Schema definitions for the kubeflow.tkestack.io v1alpha1 API group
18 | // +kubebuilder:object:generate=true
19 | // +groupName=kubeflow.tkestack.io
20 | package v1alpha1
21 |
22 | import (
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | "sigs.k8s.io/controller-runtime/pkg/scheme"
25 | )
26 |
27 | var (
28 | // GroupVersion is group version used to register these objects
29 | GroupVersion = schema.GroupVersion{Group: "kubeflow.tkestack.io", Version: "v1alpha1"}
30 |
31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme
32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
33 |
34 | // AddToScheme adds the types in this group-version to the given scheme.
35 | AddToScheme = SchemeBuilder.AddToScheme
36 | )
37 |
--------------------------------------------------------------------------------
/api/v1alpha1/jupytergateway_types.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package v1alpha1
18 |
19 | import (
20 | appsv1 "k8s.io/api/apps/v1"
21 | v1 "k8s.io/api/core/v1"
22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | )
24 |
25 | // JupyterGatewaySpec defines the desired state of JupyterGateway
26 | type JupyterGatewaySpec struct {
27 | // Knernels defines the kernels in the gateway.
28 | // We will add kernels at runtime, thus we do not make it a type.
29 | Kernels []string `json:"kernels,omitempty"`
30 | // DefaultKernel defines the default kernel in the gateway.
31 | DefaultKernel *string `json:"defaultKernel,omitempty"`
32 | // Timeout (in seconds) after which a kernel is considered idle and
33 | // ready to be culled. Values of 0 or lower disable culling. Very
34 | // short timeouts may result in kernels being culled for users
35 | // with poor network connections.
36 | // Ref https://jupyter-notebook.readthedocs.io/en/stable/config.html
37 | CullIdleTimeout *int32 `json:"cullIdleTimeout,omitempty"`
38 |
39 | // The interval (in seconds) on which to check for idle kernels
40 | // exceeding the cull timeout value.
41 | CullInterval *int32 `json:"cullInterval,omitempty"`
42 |
43 | LogLevel *LogLevel `json:"logLevel,omitempty"`
44 |
45 | // Compute Resources required by this container.
46 | // Cannot be updated.
47 | // More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/
48 | // +optional
49 | Resources *v1.ResourceRequirements `json:"resources,omitempty"`
50 |
51 | // Docker image name.
52 | // More info: https://kubernetes.io/docs/concepts/containers/images
53 | // This field defaults to ghcr.io/skai-x/enterprise-gateway:2.6.0
54 | // +optional
55 | Image string `json:"image,omitempty"`
56 |
57 | // ClusterRole for the gateway, which is used to create the kernel pods in the cluster. Defaults to enterprise-gateway-controller (created at startup).
58 | ClusterRole *string `json:"clusterRole,omitempty"`
59 | }
60 |
61 | type LogLevel string
62 |
63 | const (
64 | LogLevelDebug = "DEBUG"
65 | LogLevelInfo = "INFO"
66 | LogLevelWarning = "WARNING"
67 | )
68 |
69 | // JupyterGatewayStatus defines the observed state of JupyterGateway
70 | type JupyterGatewayStatus struct {
71 | appsv1.DeploymentStatus `json:",inline"`
72 | }
73 |
74 | // +kubebuilder:object:root=true
75 | // +kubebuilder:subresource:status
76 |
77 | // JupyterGateway is the Schema for the jupytergateways API
78 | type JupyterGateway struct {
79 | metav1.TypeMeta `json:",inline"`
80 | metav1.ObjectMeta `json:"metadata,omitempty"`
81 |
82 | Spec JupyterGatewaySpec `json:"spec,omitempty"`
83 | Status JupyterGatewayStatus `json:"status,omitempty"`
84 | }
85 |
86 | // +kubebuilder:object:root=true
87 |
88 | // JupyterGatewayList contains a list of JupyterGateway
89 | type JupyterGatewayList struct {
90 | metav1.TypeMeta `json:",inline"`
91 | metav1.ListMeta `json:"metadata,omitempty"`
92 | Items []JupyterGateway `json:"items"`
93 | }
94 |
95 | func init() {
96 | SchemeBuilder.Register(&JupyterGateway{}, &JupyterGatewayList{})
97 | }
98 |
--------------------------------------------------------------------------------
/api/v1alpha1/jupyterkernel_types.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package v1alpha1
18 |
19 | import (
20 | v1 "k8s.io/api/core/v1"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | )
23 |
24 | // JupyterKernelSpec defines the desired state of JupyterKernel
25 | type JupyterKernelCRDSpec struct {
26 | Template v1.PodTemplateSpec `json:"template,omitempty"`
27 | }
28 |
29 | // JupyterKernelStatus defines the observed state of JupyterKernel
30 | type JupyterKernelStatus struct {
31 | // Conditions is an array of current observed job conditions.
32 | Conditions []JupyterKernelCondition `json:"conditions"`
33 |
34 | // Represents time when the job was acknowledged by the job controller.
35 | // It is not guaranteed to be set in happens-before order across separate operations.
36 | // It is represented in RFC3339 form and is in UTC.
37 | StartTime *metav1.Time `json:"startTime,omitempty"`
38 |
39 | // Represents time when the job was completed. It is not guaranteed to
40 | // be set in happens-before order across separate operations.
41 | // It is represented in RFC3339 form and is in UTC.
42 | CompletionTime *metav1.Time `json:"completionTime,omitempty"`
43 |
44 | // Represents last time when the job was reconciled. It is not guaranteed to
45 | // be set in happens-before order across separate operations.
46 | // It is represented in RFC3339 form and is in UTC.
47 | LastReconcileTime *metav1.Time `json:"lastReconcileTime,omitempty"`
48 | }
49 |
50 | type JupyterKernelCondition struct {
51 | // Type of job condition.
52 | Type JupyterKernelConditionType `json:"type"`
53 | // Status of the condition, one of True, False, Unknown.
54 | Status v1.ConditionStatus `json:"status"`
55 | // The reason for the condition's last transition.
56 | Reason string `json:"reason,omitempty"`
57 | // A human readable message indicating details about the transition.
58 | Message string `json:"message,omitempty"`
59 | // The last time this condition was updated.
60 | LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"`
61 | // Last time the condition transitioned from one status to another.
62 | LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
63 | }
64 |
65 | type JupyterKernelConditionType string
66 |
67 | const (
68 | JupyterKernelRunning JupyterKernelConditionType = "Running"
69 | JupyterKernelFailed JupyterKernelConditionType = "Failed"
70 | JupyterKernelSucceeded JupyterKernelConditionType = "Succeeded"
71 | )
72 |
73 | // +kubebuilder:object:root=true
74 | // +kubebuilder:subresource:status
75 | // +kubebuilder:pruning:PreserveUnknownFields
76 |
77 | // JupyterKernel is the Schema for the jupyterkernels API
78 | type JupyterKernel struct {
79 | metav1.TypeMeta `json:",inline"`
80 | metav1.ObjectMeta `json:"metadata,omitempty"`
81 |
82 | Spec JupyterKernelCRDSpec `json:"spec,omitempty"`
83 | Status JupyterKernelStatus `json:"status,omitempty"`
84 | }
85 |
86 | // +kubebuilder:object:root=true
87 |
88 | // JupyterKernelList contains a list of JupyterKernel
89 | type JupyterKernelList struct {
90 | metav1.TypeMeta `json:",inline"`
91 | metav1.ListMeta `json:"metadata,omitempty"`
92 | Items []JupyterKernel `json:"items"`
93 | }
94 |
95 | func init() {
96 | SchemeBuilder.Register(&JupyterKernel{}, &JupyterKernelList{})
97 | }
98 |
--------------------------------------------------------------------------------
/api/v1alpha1/jupyterkernelspec_types.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package v1alpha1
18 |
19 | import (
20 | v1 "k8s.io/api/core/v1"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | )
23 |
24 | // JupyterKernelSpecSpec defines the desired state of JupyterKernelSpec
25 | type JupyterKernelSpecSpec struct {
26 | Language string `json:"language,omitempty"`
27 | DisplayName string `json:"displayName,omitempty"`
28 | Image string `json:"image,omitempty"`
29 | Env []v1.EnvVar `json:"env,omitempty"`
30 | Command []string `json:"command,omitempty"`
31 | ClassName string `json:"className,omitempty"`
32 |
33 | Template *v1.ObjectReference `json:"template,omitempty"`
34 | // TODO(gaocegege): Support resources and so on.
35 | }
36 |
37 | // JupyterKernelSpecStatus defines the observed state of JupyterKernelSpec
38 | type JupyterKernelSpecStatus struct {
39 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
40 | // Important: Run "make" to regenerate code after modifying this file
41 | }
42 |
43 | // +kubebuilder:object:root=true
44 | // +kubebuilder:subresource:status
45 |
46 | // JupyterKernelSpec is the Schema for the jupyterkernelspecs API
47 | type JupyterKernelSpec struct {
48 | metav1.TypeMeta `json:",inline"`
49 | metav1.ObjectMeta `json:"metadata,omitempty"`
50 |
51 | Spec JupyterKernelSpecSpec `json:"spec,omitempty"`
52 | Status JupyterKernelSpecStatus `json:"status,omitempty"`
53 | }
54 |
55 | // +kubebuilder:object:root=true
56 |
57 | // JupyterKernelSpecList contains a list of JupyterKernelSpec
58 | type JupyterKernelSpecList struct {
59 | metav1.TypeMeta `json:",inline"`
60 | metav1.ListMeta `json:"metadata,omitempty"`
61 | Items []JupyterKernelSpec `json:"items"`
62 | }
63 |
64 | func init() {
65 | SchemeBuilder.Register(&JupyterKernelSpec{}, &JupyterKernelSpecList{})
66 | }
67 |
--------------------------------------------------------------------------------
/api/v1alpha1/jupyterkerneltemplate_types.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package v1alpha1
18 |
19 | import (
20 | v1 "k8s.io/api/core/v1"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | )
23 |
24 | // JupyterKernelTemplateSpec defines the desired state of JupyterKernelTemplate
25 | type JupyterKernelTemplateSpec struct {
26 | Template *v1.PodTemplateSpec `json:"template,omitempty"`
27 | }
28 |
29 | // JupyterKernelTemplateStatus defines the observed state of JupyterKernelTemplate
30 | type JupyterKernelTemplateStatus struct {
31 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
32 | // Important: Run "make" to regenerate code after modifying this file
33 | }
34 |
35 | // +kubebuilder:object:root=true
36 | // +kubebuilder:subresource:status
37 |
38 | // JupyterKernelTemplate is the Schema for the jupyterkerneltemplates API
39 | type JupyterKernelTemplate struct {
40 | metav1.TypeMeta `json:",inline"`
41 | metav1.ObjectMeta `json:"metadata,omitempty"`
42 |
43 | Spec JupyterKernelTemplateSpec `json:"spec,omitempty"`
44 | Status JupyterKernelTemplateStatus `json:"status,omitempty"`
45 | }
46 |
47 | // +kubebuilder:object:root=true
48 |
49 | // JupyterKernelTemplateList contains a list of JupyterKernelTemplate
50 | type JupyterKernelTemplateList struct {
51 | metav1.TypeMeta `json:",inline"`
52 | metav1.ListMeta `json:"metadata,omitempty"`
53 | Items []JupyterKernelTemplate `json:"items"`
54 | }
55 |
56 | func init() {
57 | SchemeBuilder.Register(&JupyterKernelTemplate{}, &JupyterKernelTemplateList{})
58 | }
59 |
--------------------------------------------------------------------------------
/api/v1alpha1/jupyternotebook_types.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package v1alpha1
18 |
19 | import (
20 | v1 "k8s.io/api/core/v1"
21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22 | )
23 |
24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
26 |
27 | // JupyterNotebookSpec defines the desired state of JupyterNotebook
28 | type JupyterNotebookSpec struct {
29 | Gateway *v1.ObjectReference `json:"gateway,omitempty"`
30 | Auth *JupyterAuth `json:"auth,omitempty"`
31 |
32 | Template *v1.PodTemplateSpec `json:"template,omitempty"`
33 | }
34 |
35 | // JupyterAuth defines how to deal with jupyter notebook tokens or passwords.
36 | // https://jupyter-notebook.readthedocs.io/en/stable/security.html
37 | type JupyterAuth struct {
38 | // TODO(gaocegege): Is this field necessary since we make Token and Password a pointer?
39 | Mode ModeJupyterAuth `json:"mode,omitempty"`
40 | Token *string `json:"token,omitempty"`
41 | Password *string `json:"password,omitempty"`
42 | }
43 |
44 | type ModeJupyterAuth string
45 |
46 | const (
47 | ModeJupyterAuthEnable ModeJupyterAuth = "enable"
48 | // ModeJupyterAuthDisable disables authentication altogether by setting the token
49 | // and password to empty strings, but this is NOT RECOMMENDED, unless authentication
50 | // or access restrictions are handled at a different layer in your web application
51 | ModeJupyterAuthDisable ModeJupyterAuth = "disable"
52 | )
53 |
54 | // JupyterNotebookStatus defines the observed state of JupyterNotebook
55 | type JupyterNotebookStatus struct {
56 | }
57 |
58 | // +kubebuilder:object:root=true
59 | // +kubebuilder:subresource:status
60 |
61 | // JupyterNotebook is the Schema for the jupyternotebooks API
62 | type JupyterNotebook struct {
63 | metav1.TypeMeta `json:",inline"`
64 | metav1.ObjectMeta `json:"metadata,omitempty"`
65 |
66 | Spec JupyterNotebookSpec `json:"spec,omitempty"`
67 | Status JupyterNotebookStatus `json:"status,omitempty"`
68 | }
69 |
70 | // +kubebuilder:object:root=true
71 |
72 | // JupyterNotebookList contains a list of JupyterNotebook
73 | type JupyterNotebookList struct {
74 | metav1.TypeMeta `json:",inline"`
75 | metav1.ListMeta `json:"metadata,omitempty"`
76 | Items []JupyterNotebook `json:"items"`
77 | }
78 |
79 | func init() {
80 | SchemeBuilder.Register(&JupyterNotebook{}, &JupyterNotebookList{})
81 | }
82 |
--------------------------------------------------------------------------------
/cli/README.md:
--------------------------------------------------------------------------------
1 | # Kubeflow Launcher
2 |
3 | Kubeflow launcher is used in enterprise gateway image to launch kernels.
4 |
--------------------------------------------------------------------------------
/cli/cmd/root.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2021 NAME HERE
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package cmd
17 |
18 | import (
19 | "context"
20 | "fmt"
21 | "os"
22 |
23 | "github.com/spf13/cobra"
24 | v1 "k8s.io/api/core/v1"
25 | "k8s.io/apimachinery/pkg/types"
26 | "k8s.io/client-go/kubernetes/scheme"
27 | "sigs.k8s.io/controller-runtime/pkg/client"
28 | "sigs.k8s.io/controller-runtime/pkg/client/config"
29 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
30 | "sigs.k8s.io/controller-runtime/pkg/log/zap"
31 |
32 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
33 | )
34 |
35 | const (
36 | envKernelPodName = "KERNEL_POD_NAME"
37 | envKernelImage = "KERNEL_IMAGE"
38 | envKernelNamespace = "KERNEL_NAMESPACE"
39 | envKernelID = "KERNEL_ID"
40 | envKernelLanguage = "KERNEL_LANGUAGE"
41 | envKernelName = "KERNEL_NAME"
42 | envKernelSpark = "KERNEL_SPARK_CONTEXT_INIT_MODE"
43 | envKernelUsername = "KERNEL_USERNAME"
44 |
45 | envPortRange = "EG_PORT_RANGE"
46 | envResponseAddress = "EG_RESPONSE_ADDRESS"
47 | envPublicKey = "EG_PUBLIC_KEY"
48 | envGatewayName = "EG_NAME"
49 | envGatewayNamespace = "EG_NAMESPACE"
50 |
51 | labelKernelID = "kernel_id"
52 | )
53 |
54 | var (
55 | kernelID, portRange, responseAddr,
56 | publicKey, sparkContextInitMode,
57 | kernelTemplateName, kernelTemplateNamespace string
58 | gatewayName string
59 | verbose bool
60 | )
61 |
62 | // rootCmd represents the base command when called without any subcommands
63 | var rootCmd = &cobra.Command{
64 | Use: "kubeflow-launcher",
65 | Short: "Launch kernels",
66 | Long: `Launch kernels in the jupyter enterprise gateway`,
67 | Run: func(cmd *cobra.Command, args []string) {
68 | logger := zap.New(zap.UseDevMode(verbose))
69 |
70 | gatewayNamespace := os.Getenv(envGatewayNamespace)
71 | gatewayName := os.Getenv(envGatewayName)
72 |
73 | if gatewayName == "" || gatewayNamespace == "" {
74 | panic(fmt.Errorf("failed to get the gateway name or namespace from the env var"))
75 | }
76 | if kernelTemplateName == "" || kernelTemplateNamespace == "" {
77 | panic(fmt.Errorf("failed to get the template's name or namespace"))
78 | }
79 |
80 | logger.Info("Launching the kernel",
81 | "kernelID", kernelID, "responseAddr", responseAddr,
82 | "kernelTemplateName", kernelTemplateName,
83 | "kernelTemplateNamespace", kernelTemplateNamespace,
84 | "gatewayName", gatewayName,
85 | "gatewayNamespace", gatewayNamespace,
86 | )
87 |
88 | if err := v1alpha1.AddToScheme(scheme.Scheme); err != nil {
89 | panic(err)
90 | }
91 |
92 | cfg, err := config.GetConfig()
93 | if err != nil {
94 | panic(err)
95 | }
96 |
97 | cli, err := client.New(cfg, client.Options{
98 | Scheme: scheme.Scheme,
99 | })
100 | if err != nil {
101 | panic(err)
102 | }
103 |
104 | kt := &v1alpha1.JupyterKernelTemplate{}
105 | if err := cli.Get(context.TODO(), client.ObjectKey{
106 | Namespace: kernelTemplateNamespace,
107 | Name: kernelTemplateName,
108 | }, kt); err != nil {
109 | panic(err)
110 | }
111 |
112 | kernel := &v1alpha1.JupyterKernel{
113 | ObjectMeta: kt.Spec.Template.ObjectMeta,
114 | Spec: v1alpha1.JupyterKernelCRDSpec{
115 | Template: *kt.Spec.Template,
116 | },
117 | }
118 |
119 | // Set image from the kernel spec.
120 | image := os.Getenv(envKernelImage)
121 | if image != "" && len(kernel.Spec.Template.Spec.Containers) != 0 {
122 | kernel.Spec.Template.Spec.Containers[0].Image = image
123 | }
124 |
125 | kernel.Name = os.Getenv(envKernelPodName)
126 | kernel.Namespace = os.Getenv(envKernelNamespace)
127 | if kernel.Spec.Template.Labels == nil {
128 | kernel.Spec.Template.Labels = make(map[string]string)
129 | }
130 | // We cannot rely on it because of
131 | // https://github.com/kubernetes-sigs/controller-tools/issues/448
132 | kernel.Spec.Template.Labels[labelKernelID] = kernelID
133 |
134 | // Set the environment variables.
135 | if kernel.Spec.Template.Spec.Containers[0].Env == nil {
136 | kernel.Spec.Template.Spec.Containers[0].Env = make([]v1.EnvVar, 0)
137 | }
138 | kernel.Spec.Template.Spec.Containers[0].Env = append(
139 | kernel.Spec.Template.Spec.Containers[0].Env,
140 | v1.EnvVar{
141 | Name: envPortRange,
142 | Value: portRange,
143 | },
144 | v1.EnvVar{
145 | Name: envResponseAddress,
146 | Value: responseAddr,
147 | },
148 | v1.EnvVar{
149 | Name: envPublicKey,
150 | Value: publicKey,
151 | },
152 | v1.EnvVar{
153 | Name: envKernelID,
154 | Value: kernelID,
155 | },
156 | v1.EnvVar{
157 | Name: envKernelLanguage,
158 | Value: os.Getenv(envKernelLanguage),
159 | },
160 | v1.EnvVar{
161 | Name: envKernelName,
162 | Value: os.Getenv(envKernelName),
163 | },
164 | v1.EnvVar{
165 | Name: envKernelNamespace,
166 | Value: os.Getenv(envKernelNamespace),
167 | },
168 | v1.EnvVar{
169 | Name: envKernelSpark,
170 | Value: sparkContextInitMode,
171 | },
172 | v1.EnvVar{
173 | Name: envKernelUsername,
174 | Value: os.Getenv(envKernelUsername),
175 | },
176 | )
177 |
178 | gateway := &v1alpha1.JupyterGateway{}
179 | if err := cli.Get(context.TODO(), types.NamespacedName{
180 | Name: gatewayName,
181 | Namespace: gatewayNamespace,
182 | }, gateway); err != nil {
183 | panic(err)
184 | }
185 |
186 | if err := controllerutil.SetControllerReference(
187 | gateway, kernel, scheme.Scheme); err != nil {
188 | panic(err)
189 | }
190 |
191 | logger.Info("Creating the kernel", "kernel", kernel)
192 | if err := cli.Create(context.TODO(), kernel); err != nil {
193 | panic(err)
194 | }
195 | },
196 | }
197 |
198 | // Execute adds all child commands to the root command and sets flags appropriately.
199 | // This is called by main.main(). It only needs to happen once to the rootCmd.
200 | func Execute() {
201 | if err := rootCmd.Execute(); err != nil {
202 | fmt.Println(err)
203 | os.Exit(1)
204 | }
205 | }
206 |
207 | func init() {
208 | // Cobra also supports local flags, which will only run
209 | // when this action is called directly.
210 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
211 |
212 | rootCmd.Flags().StringVar(&kernelID,
213 | "RemoteProcessProxy.kernel-id", "", "kernel id")
214 | rootCmd.Flags().StringVar(&portRange,
215 | "RemoteProcessProxy.port-range", "", "port range")
216 | rootCmd.Flags().StringVar(&responseAddr,
217 | "RemoteProcessProxy.response-address", "", "response address")
218 | rootCmd.Flags().StringVar(&publicKey,
219 | "RemoteProcessProxy.public-key", "", "public key")
220 | rootCmd.Flags().StringVar(&sparkContextInitMode,
221 | "RemoteProcessProxy.spark-context-initialization-mode",
222 | "", "spark context init mode")
223 |
224 | rootCmd.Flags().StringVar(&kernelTemplateName,
225 | "kernel-template-name", "", "kernel template CRD name")
226 | rootCmd.Flags().StringVar(&kernelTemplateNamespace,
227 | "kernel-template-namespace", "", "kernel template CRD namesapce")
228 |
229 | rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Set verbose")
230 | }
231 |
--------------------------------------------------------------------------------
/cli/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2021 NAME HERE
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 | package main
17 |
18 | import "github.com/tkestack/elastic-jupyter-operator/cli/cmd"
19 |
20 | func main() {
21 | // kubeflow-launcher is a launcher for Kubeflow.
22 | cmd.Execute()
23 | }
24 |
--------------------------------------------------------------------------------
/config/certmanager/certificate.yaml:
--------------------------------------------------------------------------------
1 | # The following manifests contain a self-signed issuer CR and a certificate CR.
2 | # More document can be found at https://docs.cert-manager.io
3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for
4 | # breaking changes
5 | apiVersion: cert-manager.io/v1alpha2
6 | kind: Issuer
7 | metadata:
8 | name: selfsigned-issuer
9 | namespace: system
10 | spec:
11 | selfSigned: {}
12 | ---
13 | apiVersion: cert-manager.io/v1alpha2
14 | kind: Certificate
15 | metadata:
16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
17 | namespace: system
18 | spec:
19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize
20 | dnsNames:
21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc
22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local
23 | issuerRef:
24 | kind: Issuer
25 | name: selfsigned-issuer
26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize
27 |
--------------------------------------------------------------------------------
/config/certmanager/kustomization.yaml:
--------------------------------------------------------------------------------
1 | resources:
2 | - certificate.yaml
3 |
4 | configurations:
5 | - kustomizeconfig.yaml
6 |
--------------------------------------------------------------------------------
/config/certmanager/kustomizeconfig.yaml:
--------------------------------------------------------------------------------
1 | # This configuration is for teaching kustomize how to update name ref and var substitution
2 | nameReference:
3 | - kind: Issuer
4 | group: cert-manager.io
5 | fieldSpecs:
6 | - kind: Certificate
7 | group: cert-manager.io
8 | path: spec/issuerRef/name
9 |
10 | varReference:
11 | - kind: Certificate
12 | group: cert-manager.io
13 | path: spec/commonName
14 | - kind: Certificate
15 | group: cert-manager.io
16 | path: spec/dnsNames
17 |
--------------------------------------------------------------------------------
/config/crd/bases/kubeflow.tkestack.io_jupytergateways.yaml:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | apiVersion: apiextensions.k8s.io/v1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | controller-gen.kubebuilder.io/version: v0.5.0
8 | creationTimestamp: null
9 | name: jupytergateways.kubeflow.tkestack.io
10 | spec:
11 | group: kubeflow.tkestack.io
12 | names:
13 | kind: JupyterGateway
14 | listKind: JupyterGatewayList
15 | plural: jupytergateways
16 | singular: jupytergateway
17 | scope: Namespaced
18 | versions:
19 | - name: v1alpha1
20 | schema:
21 | openAPIV3Schema:
22 | description: JupyterGateway is the Schema for the jupytergateways API
23 | properties:
24 | apiVersion:
25 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
26 | type: string
27 | kind:
28 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
29 | type: string
30 | metadata:
31 | type: object
32 | spec:
33 | description: JupyterGatewaySpec defines the desired state of JupyterGateway
34 | properties:
35 | clusterRole:
36 | description: ClusterRole for the gateway, which is used to create the kernel pods in the cluster. Defaults to enterprise-gateway-controller (created at startup).
37 | type: string
38 | cullIdleTimeout:
39 | description: Timeout (in seconds) after which a kernel is considered idle and ready to be culled. Values of 0 or lower disable culling. Very short timeouts may result in kernels being culled for users with poor network connections. Ref https://jupyter-notebook.readthedocs.io/en/stable/config.html
40 | format: int32
41 | type: integer
42 | cullInterval:
43 | description: The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value.
44 | format: int32
45 | type: integer
46 | defaultKernel:
47 | description: DefaultKernel defines the default kernel in the gateway.
48 | type: string
49 | image:
50 | description: 'Docker image name. More info: https://kubernetes.io/docs/concepts/containers/images This field defaults to ghcr.io/skai-x/enterprise-gateway:2.6.0'
51 | type: string
52 | kernels:
53 | description: Knernels defines the kernels in the gateway. We will add kernels at runtime, thus we do not make it a type.
54 | items:
55 | type: string
56 | type: array
57 | logLevel:
58 | type: string
59 | resources:
60 | description: 'Compute Resources required by this container. Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/'
61 | properties:
62 | limits:
63 | additionalProperties:
64 | anyOf:
65 | - type: integer
66 | - type: string
67 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
68 | x-kubernetes-int-or-string: true
69 | description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/'
70 | type: object
71 | requests:
72 | additionalProperties:
73 | anyOf:
74 | - type: integer
75 | - type: string
76 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
77 | x-kubernetes-int-or-string: true
78 | description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/'
79 | type: object
80 | type: object
81 | type: object
82 | status:
83 | description: JupyterGatewayStatus defines the observed state of JupyterGateway
84 | properties:
85 | availableReplicas:
86 | description: Total number of available pods (ready for at least minReadySeconds) targeted by this deployment.
87 | format: int32
88 | type: integer
89 | collisionCount:
90 | description: Count of hash collisions for the Deployment. The Deployment controller uses this field as a collision avoidance mechanism when it needs to create the name for the newest ReplicaSet.
91 | format: int32
92 | type: integer
93 | conditions:
94 | description: Represents the latest available observations of a deployment's current state.
95 | items:
96 | description: DeploymentCondition describes the state of a deployment at a certain point.
97 | properties:
98 | lastTransitionTime:
99 | description: Last time the condition transitioned from one status to another.
100 | format: date-time
101 | type: string
102 | lastUpdateTime:
103 | description: The last time this condition was updated.
104 | format: date-time
105 | type: string
106 | message:
107 | description: A human readable message indicating details about the transition.
108 | type: string
109 | reason:
110 | description: The reason for the condition's last transition.
111 | type: string
112 | status:
113 | description: Status of the condition, one of True, False, Unknown.
114 | type: string
115 | type:
116 | description: Type of deployment condition.
117 | type: string
118 | required:
119 | - status
120 | - type
121 | type: object
122 | type: array
123 | observedGeneration:
124 | description: The generation observed by the deployment controller.
125 | format: int64
126 | type: integer
127 | readyReplicas:
128 | description: Total number of ready pods targeted by this deployment.
129 | format: int32
130 | type: integer
131 | replicas:
132 | description: Total number of non-terminated pods targeted by this deployment (their labels match the selector).
133 | format: int32
134 | type: integer
135 | unavailableReplicas:
136 | description: Total number of unavailable pods targeted by this deployment. This is the total number of pods that are still required for the deployment to have 100% available capacity. They may either be pods that are running but not yet available or pods that still have not been created.
137 | format: int32
138 | type: integer
139 | updatedReplicas:
140 | description: Total number of non-terminated pods targeted by this deployment that have the desired template spec.
141 | format: int32
142 | type: integer
143 | type: object
144 | type: object
145 | served: true
146 | storage: true
147 | subresources:
148 | status: {}
149 | status:
150 | acceptedNames:
151 | kind: ""
152 | plural: ""
153 | conditions: []
154 | storedVersions: []
155 |
--------------------------------------------------------------------------------
/config/crd/bases/kubeflow.tkestack.io_jupyterkernelspecs.yaml:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | apiVersion: apiextensions.k8s.io/v1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | controller-gen.kubebuilder.io/version: v0.5.0
8 | creationTimestamp: null
9 | name: jupyterkernelspecs.kubeflow.tkestack.io
10 | spec:
11 | group: kubeflow.tkestack.io
12 | names:
13 | kind: JupyterKernelSpec
14 | listKind: JupyterKernelSpecList
15 | plural: jupyterkernelspecs
16 | singular: jupyterkernelspec
17 | scope: Namespaced
18 | versions:
19 | - name: v1alpha1
20 | schema:
21 | openAPIV3Schema:
22 | description: JupyterKernelSpec is the Schema for the jupyterkernelspecs API
23 | properties:
24 | apiVersion:
25 | description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
26 | type: string
27 | kind:
28 | description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
29 | type: string
30 | metadata:
31 | type: object
32 | spec:
33 | description: JupyterKernelSpecSpec defines the desired state of JupyterKernelSpec
34 | properties:
35 | className:
36 | type: string
37 | command:
38 | items:
39 | type: string
40 | type: array
41 | displayName:
42 | type: string
43 | env:
44 | items:
45 | description: EnvVar represents an environment variable present in a Container.
46 | properties:
47 | name:
48 | description: Name of the environment variable. Must be a C_IDENTIFIER.
49 | type: string
50 | value:
51 | description: 'Variable references $(VAR_NAME) are expanded using the previous defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. The $(VAR_NAME) syntax can be escaped with a double $$, ie: $$(VAR_NAME). Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".'
52 | type: string
53 | valueFrom:
54 | description: Source for the environment variable's value. Cannot be used if value is not empty.
55 | properties:
56 | configMapKeyRef:
57 | description: Selects a key of a ConfigMap.
58 | properties:
59 | key:
60 | description: The key to select.
61 | type: string
62 | name:
63 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
64 | type: string
65 | optional:
66 | description: Specify whether the ConfigMap or its key must be defined
67 | type: boolean
68 | required:
69 | - key
70 | type: object
71 | fieldRef:
72 | description: 'Selects a field of the pod: supports metadata.name, metadata.namespace, metadata.labels, metadata.annotations, spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.'
73 | properties:
74 | apiVersion:
75 | description: Version of the schema the FieldPath is written in terms of, defaults to "v1".
76 | type: string
77 | fieldPath:
78 | description: Path of the field to select in the specified API version.
79 | type: string
80 | required:
81 | - fieldPath
82 | type: object
83 | resourceFieldRef:
84 | description: 'Selects a resource of the container: only resources limits and requests (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.'
85 | properties:
86 | containerName:
87 | description: 'Container name: required for volumes, optional for env vars'
88 | type: string
89 | divisor:
90 | anyOf:
91 | - type: integer
92 | - type: string
93 | description: Specifies the output format of the exposed resources, defaults to "1"
94 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
95 | x-kubernetes-int-or-string: true
96 | resource:
97 | description: 'Required: resource to select'
98 | type: string
99 | required:
100 | - resource
101 | type: object
102 | secretKeyRef:
103 | description: Selects a key of a secret in the pod's namespace
104 | properties:
105 | key:
106 | description: The key of the secret to select from. Must be a valid secret key.
107 | type: string
108 | name:
109 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
110 | type: string
111 | optional:
112 | description: Specify whether the Secret or its key must be defined
113 | type: boolean
114 | required:
115 | - key
116 | type: object
117 | type: object
118 | required:
119 | - name
120 | type: object
121 | type: array
122 | image:
123 | type: string
124 | language:
125 | type: string
126 | template:
127 | description: 'ObjectReference contains enough information to let you inspect or modify the referred object. --- New uses of this type are discouraged because of difficulty describing its usage when embedded in APIs. 1. Ignored fields. It includes many fields which are not generally honored. For instance, ResourceVersion and FieldPath are both very rarely valid in actual usage. 2. Invalid usage help. It is impossible to add specific help for individual usage. In most embedded usages, there are particular restrictions like, "must refer only to types A and B" or "UID not honored" or "name must be restricted". Those cannot be well described when embedded. 3. Inconsistent validation. Because the usages are different, the validation rules are different by usage, which makes it hard for users to predict what will happen. 4. The fields are both imprecise and overly precise. Kind is not a precise mapping to a URL. This can produce ambiguity during interpretation and require a REST mapping. In most cases, the dependency is on the group,resource tuple and the version of the actual struct is irrelevant. 5. We cannot easily change it. Because this type is embedded in many locations, updates to this type will affect numerous schemas. Don''t make new APIs embed an underspecified API type they do not control. Instead of using this type, create a locally provided and used type that is well-focused on your reference. For example, ServiceReferences for admission registration: https://github.com/kubernetes/api/blob/release-1.17/admissionregistration/v1/types.go#L533 .'
128 | properties:
129 | apiVersion:
130 | description: API version of the referent.
131 | type: string
132 | fieldPath:
133 | description: 'If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: "spec.containers{name}" (where "name" refers to the name of the container that triggered the event) or if no container name is specified "spec.containers[2]" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object. TODO: this design is not final and this field is subject to change in the future.'
134 | type: string
135 | kind:
136 | description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
137 | type: string
138 | name:
139 | description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names'
140 | type: string
141 | namespace:
142 | description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/'
143 | type: string
144 | resourceVersion:
145 | description: 'Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency'
146 | type: string
147 | uid:
148 | description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids'
149 | type: string
150 | type: object
151 | type: object
152 | status:
153 | description: JupyterKernelSpecStatus defines the observed state of JupyterKernelSpec
154 | type: object
155 | type: object
156 | served: true
157 | storage: true
158 | subresources:
159 | status: {}
160 | status:
161 | acceptedNames:
162 | kind: ""
163 | plural: ""
164 | conditions: []
165 | storedVersions: []
166 |
--------------------------------------------------------------------------------
/config/crd/kustomization.yaml:
--------------------------------------------------------------------------------
1 | # This kustomization.yaml is not intended to be run by itself,
2 | # since it depends on service name and namespace that are out of this kustomize package.
3 | # It should be run by config/default
4 | resources:
5 | - bases/kubeflow.tkestack.io_jupyternotebooks.yaml
6 | - bases/kubeflow.tkestack.io_jupytergateways.yaml
7 | - bases/kubeflow.tkestack.io_jupyterkernelspecs.yaml
8 | - bases/kubeflow.tkestack.io_jupyterkerneltemplates.yaml
9 | - bases/kubeflow.tkestack.io_jupyterkernels.yaml
10 | # +kubebuilder:scaffold:crdkustomizeresource
11 |
12 | patchesStrategicMerge:
13 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
14 | # patches here are for enabling the conversion webhook for each CRD
15 | #- patches/webhook_in_jupyternotebooks.yaml
16 | #- patches/webhook_in_jupytergateways.yaml
17 | #- patches/webhook_in_jupyterkernelspecs.yaml
18 | #- patches/webhook_in_jupyterkerneltemplates.yaml
19 | #- patches/webhook_in_jupyterkernels.yaml
20 | # +kubebuilder:scaffold:crdkustomizewebhookpatch
21 |
22 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
23 | # patches here are for enabling the CA injection for each CRD
24 | #- patches/cainjection_in_jupyternotebooks.yaml
25 | #- patches/cainjection_in_jupytergateways.yaml
26 | #- patches/cainjection_in_jupyterkernelspecs.yaml
27 | #- patches/cainjection_in_jupyterkerneltemplates.yaml
28 | #- patches/cainjection_in_jupyterkernels.yaml
29 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch
30 |
31 | # the following config is for teaching kustomize how to do kustomization for CRDs.
32 | configurations:
33 | - kustomizeconfig.yaml
34 |
--------------------------------------------------------------------------------
/config/crd/kustomizeconfig.yaml:
--------------------------------------------------------------------------------
1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD
2 | nameReference:
3 | - kind: Service
4 | version: v1
5 | fieldSpecs:
6 | - kind: CustomResourceDefinition
7 | group: apiextensions.k8s.io
8 | path: spec/conversion/webhookClientConfig/service/name
9 |
10 | namespace:
11 | - kind: CustomResourceDefinition
12 | group: apiextensions.k8s.io
13 | path: spec/conversion/webhookClientConfig/service/namespace
14 | create: false
15 |
16 | varReference:
17 | - path: metadata/annotations
18 |
--------------------------------------------------------------------------------
/config/crd/patches/cainjection_in_jupytergateways.yaml:
--------------------------------------------------------------------------------
1 | # The following patch adds a directive for certmanager to inject CA into the CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
8 | name: jupytergateways.kubeflow.tkestack.io
9 |
--------------------------------------------------------------------------------
/config/crd/patches/cainjection_in_jupyterkernels.yaml:
--------------------------------------------------------------------------------
1 | # The following patch adds a directive for certmanager to inject CA into the CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
8 | name: jupyterkernels.kubeflow.tkestack.io
9 |
--------------------------------------------------------------------------------
/config/crd/patches/cainjection_in_jupyterkernelspecs.yaml:
--------------------------------------------------------------------------------
1 | # The following patch adds a directive for certmanager to inject CA into the CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
8 | name: jupyterkernelspecs.kubeflow.tkestack.io
9 |
--------------------------------------------------------------------------------
/config/crd/patches/cainjection_in_jupyterkerneltemplates.yaml:
--------------------------------------------------------------------------------
1 | # The following patch adds a directive for certmanager to inject CA into the CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
8 | name: jupyterkerneltemplates.kubeflow.tkestack.io
9 |
--------------------------------------------------------------------------------
/config/crd/patches/cainjection_in_jupyternotebooks.yaml:
--------------------------------------------------------------------------------
1 | # The following patch adds a directive for certmanager to inject CA into the CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | annotations:
7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
8 | name: jupyternotebooks.kubeflow.tkestack.io
9 |
--------------------------------------------------------------------------------
/config/crd/patches/webhook_in_jupytergateways.yaml:
--------------------------------------------------------------------------------
1 | # The following patch enables conversion webhook for CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | name: jupytergateways.kubeflow.tkestack.io
7 | spec:
8 | conversion:
9 | strategy: Webhook
10 | webhookClientConfig:
11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
13 | caBundle: Cg==
14 | service:
15 | namespace: system
16 | name: webhook-service
17 | path: /convert
18 |
--------------------------------------------------------------------------------
/config/crd/patches/webhook_in_jupyterkernels.yaml:
--------------------------------------------------------------------------------
1 | # The following patch enables conversion webhook for CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | name: jupyterkernels.kubeflow.tkestack.io
7 | spec:
8 | conversion:
9 | strategy: Webhook
10 | webhookClientConfig:
11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
13 | caBundle: Cg==
14 | service:
15 | namespace: system
16 | name: webhook-service
17 | path: /convert
18 |
--------------------------------------------------------------------------------
/config/crd/patches/webhook_in_jupyterkernelspecs.yaml:
--------------------------------------------------------------------------------
1 | # The following patch enables conversion webhook for CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | name: jupyterkernelspecs.kubeflow.tkestack.io
7 | spec:
8 | conversion:
9 | strategy: Webhook
10 | webhookClientConfig:
11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
13 | caBundle: Cg==
14 | service:
15 | namespace: system
16 | name: webhook-service
17 | path: /convert
18 |
--------------------------------------------------------------------------------
/config/crd/patches/webhook_in_jupyterkerneltemplates.yaml:
--------------------------------------------------------------------------------
1 | # The following patch enables conversion webhook for CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | name: jupyterkerneltemplates.kubeflow.tkestack.io
7 | spec:
8 | conversion:
9 | strategy: Webhook
10 | webhookClientConfig:
11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
13 | caBundle: Cg==
14 | service:
15 | namespace: system
16 | name: webhook-service
17 | path: /convert
18 |
--------------------------------------------------------------------------------
/config/crd/patches/webhook_in_jupyternotebooks.yaml:
--------------------------------------------------------------------------------
1 | # The following patch enables conversion webhook for CRD
2 | # CRD conversion requires k8s 1.13 or later.
3 | apiVersion: apiextensions.k8s.io/v1beta1
4 | kind: CustomResourceDefinition
5 | metadata:
6 | name: jupyternotebooks.kubeflow.tkestack.io
7 | spec:
8 | conversion:
9 | strategy: Webhook
10 | webhookClientConfig:
11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
13 | caBundle: Cg==
14 | service:
15 | namespace: system
16 | name: webhook-service
17 | path: /convert
18 |
--------------------------------------------------------------------------------
/config/default/kustomization.yaml:
--------------------------------------------------------------------------------
1 | # Adds namespace to all resources.
2 | namespace: elastic-jupyter-operator-system
3 |
4 | # Value of this field is prepended to the
5 | # names of all resources, e.g. a deployment named
6 | # "wordpress" becomes "alices-wordpress".
7 | # Note that it should also match with the prefix (text before '-') of the namespace
8 | # field above.
9 | namePrefix: elastic-jupyter-operator-
10 |
11 | # Labels to add to all resources and selectors.
12 | #commonLabels:
13 | # someName: someValue
14 |
15 | bases:
16 | - ../crd
17 | - ../rbac
18 | - ../manager
19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
20 | # crd/kustomization.yaml
21 | #- ../webhook
22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
23 | #- ../certmanager
24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
25 | #- ../prometheus
26 |
27 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
28 | # crd/kustomization.yaml
29 | #- manager_webhook_patch.yaml
30 |
31 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
32 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
33 | # 'CERTMANAGER' needs to be enabled to use ca injection
34 | #- webhookcainjection_patch.yaml
35 |
36 | # the following config is for teaching kustomize how to do var substitution
37 | # vars:
38 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
39 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
40 | # objref:
41 | # kind: Certificate
42 | # group: cert-manager.io
43 | # version: v1alpha2
44 | # name: serving-cert # this name should match the one in certificate.yaml
45 | # fieldref:
46 | # fieldpath: metadata.namespace
47 | #- name: CERTIFICATE_NAME
48 | # objref:
49 | # kind: Certificate
50 | # group: cert-manager.io
51 | # version: v1alpha2
52 | # name: serving-cert # this name should match the one in certificate.yaml
53 | #- name: SERVICE_NAMESPACE # namespace of the service
54 | # objref:
55 | # kind: Service
56 | # version: v1
57 | # name: webhook-service
58 | # fieldref:
59 | # fieldpath: metadata.namespace
60 | #- name: SERVICE_NAME
61 | # objref:
62 | # kind: Service
63 | # version: v1
64 | # name: webhook-service
65 |
--------------------------------------------------------------------------------
/config/manager/kustomization.yaml:
--------------------------------------------------------------------------------
1 | resources:
2 | - manager.yaml
3 | apiVersion: kustomize.config.k8s.io/v1beta1
4 | kind: Kustomization
5 | images:
6 | - name: controller
7 | newName: ghcr.io/skai-x/elastic-jupyter-operator
8 | newTag: latest
9 |
--------------------------------------------------------------------------------
/config/manager/manager.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Namespace
3 | metadata:
4 | labels:
5 | control-plane: controller-manager
6 | name: system
7 | ---
8 | apiVersion: apps/v1
9 | kind: Deployment
10 | metadata:
11 | name: controller-manager
12 | namespace: system
13 | labels:
14 | control-plane: controller-manager
15 | spec:
16 | selector:
17 | matchLabels:
18 | control-plane: controller-manager
19 | replicas: 1
20 | template:
21 | metadata:
22 | labels:
23 | control-plane: controller-manager
24 | spec:
25 | containers:
26 | - command:
27 | - /elastic-jupyter-operator
28 | args:
29 | - --enable-leader-election
30 | image: ghcr.io/skai-x/elastic-jupyter-operator:latest
31 | name: manager
32 | resources:
33 | limits:
34 | cpu: 100m
35 | memory: 30Mi
36 | requests:
37 | cpu: 100m
38 | memory: 20Mi
39 | terminationGracePeriodSeconds: 10
40 |
--------------------------------------------------------------------------------
/config/prometheus/kustomization.yaml:
--------------------------------------------------------------------------------
1 | resources:
2 | - monitor.yaml
3 |
--------------------------------------------------------------------------------
/config/prometheus/monitor.yaml:
--------------------------------------------------------------------------------
1 |
2 | # Prometheus Monitor Service (Metrics)
3 | apiVersion: monitoring.coreos.com/v1
4 | kind: ServiceMonitor
5 | metadata:
6 | labels:
7 | control-plane: controller-manager
8 | name: controller-manager-metrics-monitor
9 | namespace: system
10 | spec:
11 | endpoints:
12 | - path: /metrics
13 | port: https
14 | selector:
15 | matchLabels:
16 | control-plane: controller-manager
17 |
--------------------------------------------------------------------------------
/config/rbac/auth_proxy_client_clusterrole.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRole
3 | metadata:
4 | name: metrics-reader
5 | rules:
6 | - nonResourceURLs: ["/metrics"]
7 | verbs: ["get"]
8 |
--------------------------------------------------------------------------------
/config/rbac/auth_proxy_role.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRole
3 | metadata:
4 | name: proxy-role
5 | rules:
6 | - apiGroups: ["authentication.k8s.io"]
7 | resources:
8 | - tokenreviews
9 | verbs: ["create"]
10 | - apiGroups: ["authorization.k8s.io"]
11 | resources:
12 | - subjectaccessreviews
13 | verbs: ["create"]
14 |
--------------------------------------------------------------------------------
/config/rbac/auth_proxy_role_binding.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRoleBinding
3 | metadata:
4 | name: proxy-rolebinding
5 | roleRef:
6 | apiGroup: rbac.authorization.k8s.io
7 | kind: ClusterRole
8 | name: proxy-role
9 | subjects:
10 | - kind: ServiceAccount
11 | name: default
12 | namespace: system
13 |
--------------------------------------------------------------------------------
/config/rbac/auth_proxy_service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | labels:
5 | control-plane: controller-manager
6 | name: controller-manager-metrics-service
7 | namespace: system
8 | spec:
9 | ports:
10 | - name: https
11 | port: 8443
12 | targetPort: https
13 | selector:
14 | control-plane: controller-manager
15 |
--------------------------------------------------------------------------------
/config/rbac/jupytergateway_editor_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to edit jupytergateways.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupytergateway-editor-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupytergateways
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - kubeflow.tkestack.io
21 | resources:
22 | - jupytergateways/status
23 | verbs:
24 | - get
25 |
--------------------------------------------------------------------------------
/config/rbac/jupytergateway_viewer_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to view jupytergateways.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupytergateway-viewer-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupytergateways
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - apiGroups:
16 | - kubeflow.tkestack.io
17 | resources:
18 | - jupytergateways/status
19 | verbs:
20 | - get
21 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkernel_editor_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to edit jupyterkernels.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkernel-editor-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkernels
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - kubeflow.tkestack.io
21 | resources:
22 | - jupyterkernels/status
23 | verbs:
24 | - get
25 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkernel_viewer_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to view jupyterkernels.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkernel-viewer-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkernels
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - apiGroups:
16 | - kubeflow.tkestack.io
17 | resources:
18 | - jupyterkernels/status
19 | verbs:
20 | - get
21 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkernelspec_editor_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to edit jupyterkernelspecs.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkernelspec-editor-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkernelspecs
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - kubeflow.tkestack.io
21 | resources:
22 | - jupyterkernelspecs/status
23 | verbs:
24 | - get
25 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkernelspec_viewer_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to view jupyterkernelspecs.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkernelspec-viewer-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkernelspecs
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - apiGroups:
16 | - kubeflow.tkestack.io
17 | resources:
18 | - jupyterkernelspecs/status
19 | verbs:
20 | - get
21 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkerneltemplate_editor_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to edit jupyterkerneltemplates.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkerneltemplate-editor-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkerneltemplates
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - kubeflow.tkestack.io
21 | resources:
22 | - jupyterkerneltemplates/status
23 | verbs:
24 | - get
25 |
--------------------------------------------------------------------------------
/config/rbac/jupyterkerneltemplate_viewer_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to view jupyterkerneltemplates.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyterkerneltemplate-viewer-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyterkerneltemplates
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - apiGroups:
16 | - kubeflow.tkestack.io
17 | resources:
18 | - jupyterkerneltemplates/status
19 | verbs:
20 | - get
21 |
--------------------------------------------------------------------------------
/config/rbac/jupyternotebook_editor_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to edit jupyternotebooks.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyternotebook-editor-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyternotebooks
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - kubeflow.tkestack.io
21 | resources:
22 | - jupyternotebooks/status
23 | verbs:
24 | - get
25 |
--------------------------------------------------------------------------------
/config/rbac/jupyternotebook_viewer_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions for end users to view jupyternotebooks.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: jupyternotebook-viewer-role
6 | rules:
7 | - apiGroups:
8 | - kubeflow.tkestack.io
9 | resources:
10 | - jupyternotebooks
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - apiGroups:
16 | - kubeflow.tkestack.io
17 | resources:
18 | - jupyternotebooks/status
19 | verbs:
20 | - get
21 |
--------------------------------------------------------------------------------
/config/rbac/kustomization.yaml:
--------------------------------------------------------------------------------
1 | resources:
2 | - role.yaml
3 | - role_binding.yaml
4 | - leader_election_role.yaml
5 | - leader_election_role_binding.yaml
6 | # Comment the following 4 lines if you want to disable
7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy)
8 | # which protects your /metrics endpoint.
9 | # - auth_proxy_service.yaml
10 | # - auth_proxy_role.yaml
11 | # - auth_proxy_role_binding.yaml
12 | # - auth_proxy_client_clusterrole.yaml
13 |
--------------------------------------------------------------------------------
/config/rbac/leader_election_role.yaml:
--------------------------------------------------------------------------------
1 | # permissions to do leader election.
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: Role
4 | metadata:
5 | name: leader-election-role
6 | rules:
7 | - apiGroups:
8 | - ""
9 | resources:
10 | - configmaps
11 | verbs:
12 | - get
13 | - list
14 | - watch
15 | - create
16 | - update
17 | - patch
18 | - delete
19 | - apiGroups:
20 | - ""
21 | resources:
22 | - configmaps/status
23 | verbs:
24 | - get
25 | - update
26 | - patch
27 | - apiGroups:
28 | - ""
29 | resources:
30 | - events
31 | verbs:
32 | - create
33 | - patch
34 |
--------------------------------------------------------------------------------
/config/rbac/leader_election_role_binding.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: RoleBinding
3 | metadata:
4 | name: leader-election-rolebinding
5 | roleRef:
6 | apiGroup: rbac.authorization.k8s.io
7 | kind: Role
8 | name: leader-election-role
9 | subjects:
10 | - kind: ServiceAccount
11 | name: default
12 | namespace: system
13 |
--------------------------------------------------------------------------------
/config/rbac/role.yaml:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | apiVersion: rbac.authorization.k8s.io/v1
4 | kind: ClusterRole
5 | metadata:
6 | creationTimestamp: null
7 | name: manager-role
8 | rules:
9 | - apiGroups:
10 | - ""
11 | resources:
12 | - configmaps
13 | - events
14 | - namespaces
15 | - persistentvolumeclaims
16 | - persistentvolumes
17 | - pods
18 | - secrets
19 | - serviceaccounts
20 | - services
21 | verbs:
22 | - create
23 | - delete
24 | - get
25 | - list
26 | - patch
27 | - update
28 | - watch
29 | - apiGroups:
30 | - apps
31 | resources:
32 | - deployments
33 | verbs:
34 | - create
35 | - delete
36 | - get
37 | - list
38 | - patch
39 | - update
40 | - watch
41 | - apiGroups:
42 | - kubeflow.tkestack.io
43 | resources:
44 | - jupytergateways
45 | verbs:
46 | - create
47 | - delete
48 | - get
49 | - list
50 | - patch
51 | - update
52 | - watch
53 | - apiGroups:
54 | - kubeflow.tkestack.io
55 | resources:
56 | - jupytergateways/status
57 | verbs:
58 | - get
59 | - patch
60 | - update
61 | - apiGroups:
62 | - kubeflow.tkestack.io
63 | resources:
64 | - jupyterkernels
65 | verbs:
66 | - create
67 | - delete
68 | - get
69 | - list
70 | - patch
71 | - update
72 | - watch
73 | - apiGroups:
74 | - kubeflow.tkestack.io
75 | resources:
76 | - jupyterkernels/status
77 | verbs:
78 | - get
79 | - patch
80 | - update
81 | - apiGroups:
82 | - kubeflow.tkestack.io
83 | resources:
84 | - jupyterkernelspecs
85 | verbs:
86 | - create
87 | - delete
88 | - get
89 | - list
90 | - patch
91 | - update
92 | - watch
93 | - apiGroups:
94 | - kubeflow.tkestack.io
95 | resources:
96 | - jupyterkernelspecs/status
97 | verbs:
98 | - get
99 | - patch
100 | - update
101 | - apiGroups:
102 | - kubeflow.tkestack.io
103 | resources:
104 | - jupyterkerneltemplates
105 | verbs:
106 | - create
107 | - delete
108 | - get
109 | - list
110 | - patch
111 | - update
112 | - watch
113 | - apiGroups:
114 | - kubeflow.tkestack.io
115 | resources:
116 | - jupyterkerneltemplates/status
117 | verbs:
118 | - get
119 | - patch
120 | - update
121 | - apiGroups:
122 | - kubeflow.tkestack.io
123 | resources:
124 | - jupyternotebooks
125 | verbs:
126 | - create
127 | - delete
128 | - get
129 | - list
130 | - patch
131 | - update
132 | - watch
133 | - apiGroups:
134 | - kubeflow.tkestack.io
135 | resources:
136 | - jupyternotebooks/status
137 | verbs:
138 | - get
139 | - patch
140 | - update
141 | - apiGroups:
142 | - rbac.authorization.k8s.io
143 | resources:
144 | - rolebindings
145 | verbs:
146 | - create
147 | - delete
148 | - get
149 | - list
150 | - patch
151 | - update
152 | - watch
153 |
--------------------------------------------------------------------------------
/config/rbac/role_binding.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: rbac.authorization.k8s.io/v1
2 | kind: ClusterRoleBinding
3 | metadata:
4 | name: manager-rolebinding
5 | roleRef:
6 | apiGroup: rbac.authorization.k8s.io
7 | kind: ClusterRole
8 | name: manager-role
9 | subjects:
10 | - kind: ServiceAccount
11 | name: default
12 | namespace: system
13 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupytergateway-kernels.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterGateway
3 | metadata:
4 | name: jupytergateway-sample
5 | spec:
6 | cullIdleTimeout: 10
7 | cullInterval: 10
8 | logLevel: DEBUG
9 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
10 | kernels:
11 | - python-kubernetes
12 | resources:
13 | requests:
14 | memory: "64Mi"
15 | cpu: "250m"
16 | limits:
17 | memory: "128Mi"
18 | cpu: "500m"
19 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterGateway
3 | metadata:
4 | name: jupytergateway-sample
5 | spec:
6 | cullIdleTimeout: 3600
7 | resources:
8 | requests:
9 | memory: "64Mi"
10 | cpu: "250m"
11 | limits:
12 | memory: "128Mi"
13 | cpu: "500m"
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupyterkernel.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernel
3 | metadata:
4 | name: jupyterkernel-sample
5 | spec:
6 | template:
7 | metadata:
8 | app: enterprise-gateway
9 | component: kernel
10 | spec:
11 | restartPolicy: Never
12 | containers:
13 | - name: kernel
14 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
15 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec-custom-launcher.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernelSpec
3 | metadata:
4 | name: python-kubernetes
5 | spec:
6 | language: Python
7 | displayName: "Python on Kubernetes as a JupyterKernelSpec"
8 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
9 | className: enterprise_gateway.services.processproxies.kubeflow.KubeflowProcessProxy
10 | template:
11 | namespace: default
12 | name: jupyterkerneltemplate-sample
13 | command:
14 | # Use the default scripts to launch the kernel.
15 | - "kubeflow-launcher"
16 | - "--verbose"
17 | - "--RemoteProcessProxy.kernel-id"
18 | - "{kernel_id}"
19 | - "--RemoteProcessProxy.port-range"
20 | - "{port_range}"
21 | - "--RemoteProcessProxy.response-address"
22 | - "{response_address}"
23 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernelSpec
3 | metadata:
4 | name: python-kubernetes
5 | spec:
6 | language: Python
7 | displayName: "Python on Kubernetes as a JupyterKernelSpec"
8 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
9 | command:
10 | - "python"
11 | # Use the default scripts to launch the kernel.
12 | - "/usr/local/share/jupyter/kernels/python_kubernetes/scripts/launch_kubernetes.py"
13 | - "--RemoteProcessProxy.kernel-id"
14 | - "{kernel_id}"
15 | - "--RemoteProcessProxy.port-range"
16 | - "{port_range}"
17 | - "--RemoteProcessProxy.response-address"
18 | - "{response_address}"
19 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernelTemplate
3 | metadata:
4 | name: jupyterkerneltemplate-sample
5 | spec:
6 | template:
7 | metadata:
8 | app: enterprise-gateway
9 | component: kernel
10 | spec:
11 | restartPolicy: Always
12 | containers:
13 | - name: kernel
14 |
--------------------------------------------------------------------------------
/config/samples/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterNotebook
3 | metadata:
4 | name: jupyternotebook-sample
5 | spec:
6 | gateway:
7 | name: jupytergateway-sample
8 | namespace: default
9 | # resources:
10 | # requests:
11 | # memory: "64Mi"
12 | # cpu: "250m"
13 | # limits:
14 | # memory: "128Mi"
15 | # cpu: "500m"
16 |
--------------------------------------------------------------------------------
/config/webhook/kustomization.yaml:
--------------------------------------------------------------------------------
1 | resources:
2 | - manifests.yaml
3 | - service.yaml
4 |
5 | configurations:
6 | - kustomizeconfig.yaml
7 |
--------------------------------------------------------------------------------
/config/webhook/kustomizeconfig.yaml:
--------------------------------------------------------------------------------
1 | # the following config is for teaching kustomize where to look at when substituting vars.
2 | # It requires kustomize v2.1.0 or newer to work properly.
3 | nameReference:
4 | - kind: Service
5 | version: v1
6 | fieldSpecs:
7 | - kind: MutatingWebhookConfiguration
8 | group: admissionregistration.k8s.io
9 | path: webhooks/clientConfig/service/name
10 | - kind: ValidatingWebhookConfiguration
11 | group: admissionregistration.k8s.io
12 | path: webhooks/clientConfig/service/name
13 |
14 | namespace:
15 | - kind: MutatingWebhookConfiguration
16 | group: admissionregistration.k8s.io
17 | path: webhooks/clientConfig/service/namespace
18 | create: true
19 | - kind: ValidatingWebhookConfiguration
20 | group: admissionregistration.k8s.io
21 | path: webhooks/clientConfig/service/namespace
22 | create: true
23 |
24 | varReference:
25 | - path: metadata/annotations
26 |
--------------------------------------------------------------------------------
/config/webhook/service.yaml:
--------------------------------------------------------------------------------
1 |
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: webhook-service
6 | namespace: system
7 | spec:
8 | ports:
9 | - port: 443
10 | targetPort: 9443
11 | selector:
12 | control-plane: controller-manager
13 |
--------------------------------------------------------------------------------
/controllers/jupytergateway_controller.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2021.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package controllers
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | appsv1 "k8s.io/api/apps/v1"
24 | "k8s.io/apimachinery/pkg/api/errors"
25 | "k8s.io/apimachinery/pkg/runtime"
26 | "k8s.io/client-go/tools/record"
27 | ctrl "sigs.k8s.io/controller-runtime"
28 | "sigs.k8s.io/controller-runtime/pkg/client"
29 | "sigs.k8s.io/controller-runtime/pkg/handler"
30 | "sigs.k8s.io/controller-runtime/pkg/source"
31 |
32 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
33 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
34 | "github.com/tkestack/elastic-jupyter-operator/pkg/gateway"
35 | )
36 |
37 | // JupyterGatewayReconciler reconciles a JupyterGateway object
38 | type JupyterGatewayReconciler struct {
39 | client.Client
40 | Log logr.Logger
41 | Recorder record.EventRecorder
42 | Scheme *runtime.Scheme
43 | }
44 |
45 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupytergateways,verbs=get;list;watch;create;update;patch;delete
46 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupytergateways/status,verbs=get;update;patch
47 | // +kubebuilder:rbac:groups="apps",resources=deployments,verbs=get;list;watch;create;update;patch;delete
48 | // +kubebuilder:rbac:groups="",resources=pods;namespaces;services;serviceaccounts;configmaps;secrets;persistentvolumes;persistentvolumeclaims;events,verbs=get;list;watch;create;update;create;patch;delete
49 | // +kubebuilder:rbac:groups="rbac.authorization.k8s.io",resources=rolebindings,verbs=get;create;update;patch;list;watch;delete
50 |
51 | func (r *JupyterGatewayReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
52 | _ = context.Background()
53 | _ = r.Log.WithValues("jupytergateway", req.NamespacedName)
54 |
55 | original := &v1alpha1.JupyterGateway{}
56 |
57 | err := r.Get(context.TODO(), req.NamespacedName, original)
58 | if err != nil {
59 | if errors.IsNotFound(err) {
60 | // Object not found, return. Created objects are automatically garbage collected.
61 | // For additional cleanup logic use finalizers.
62 | return ctrl.Result{}, nil
63 | }
64 | // Error reading the object - requeue the request.
65 | r.Log.Error(err, "Failed to get the object, requeuing the request")
66 | return ctrl.Result{}, err
67 | }
68 | instance := original.DeepCopy()
69 |
70 | gr, err := gateway.NewReconciler(r.Client, r.Log, r.Recorder, r.Scheme, instance)
71 | if err != nil {
72 | return ctrl.Result{}, err
73 | }
74 | if err := gr.Reconcile(); err != nil {
75 | return ctrl.Result{}, err
76 | }
77 | return ctrl.Result{}, nil
78 | }
79 |
80 | func (r *JupyterGatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
81 | return ctrl.NewControllerManagedBy(mgr).
82 | For(&kubeflowtkestackiov1alpha1.JupyterGateway{}).
83 | Watches(&source.Kind{Type: &appsv1.Deployment{}},
84 | &handler.EnqueueRequestForOwner{
85 | IsController: true,
86 | OwnerType: &v1alpha1.JupyterGateway{},
87 | }).
88 | Complete(r)
89 | }
90 |
--------------------------------------------------------------------------------
/controllers/jupyterkernel_controller.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package controllers
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | "k8s.io/apimachinery/pkg/api/errors"
24 | "k8s.io/apimachinery/pkg/runtime"
25 | "k8s.io/client-go/tools/record"
26 | ctrl "sigs.k8s.io/controller-runtime"
27 | "sigs.k8s.io/controller-runtime/pkg/client"
28 |
29 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
30 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
31 | "github.com/tkestack/elastic-jupyter-operator/pkg/kernel"
32 | )
33 |
34 | // JupyterKernelReconciler reconciles a JupyterKernel object
35 | type JupyterKernelReconciler struct {
36 | client.Client
37 | Log logr.Logger
38 | Scheme *runtime.Scheme
39 | Recorder record.EventRecorder
40 | }
41 |
42 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkernels,verbs=get;list;watch;create;update;patch;delete
43 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkernels/status,verbs=get;update;patch
44 |
45 | func (r *JupyterKernelReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
46 | _ = context.Background()
47 | _ = r.Log.WithValues("jupyterkernel", req.NamespacedName)
48 |
49 | original := &v1alpha1.JupyterKernel{}
50 |
51 | err := r.Get(context.TODO(), req.NamespacedName, original)
52 | if err != nil {
53 | if errors.IsNotFound(err) {
54 | // Object not found, return. Created objects are automatically garbage collected.
55 | // For additional cleanup logic use finalizers.
56 | return ctrl.Result{}, nil
57 | }
58 | // Error reading the object - requeue the request.
59 | r.Log.Error(err, "Failed to get the object, requeuing the request")
60 | return ctrl.Result{}, err
61 | }
62 | instance := original.DeepCopy()
63 |
64 | gr, err := kernel.NewReconciler(r.Client, r.Log, r.Recorder, r.Scheme, instance)
65 | if err != nil {
66 | return ctrl.Result{}, err
67 | }
68 | if err := gr.Reconcile(); err != nil {
69 | return ctrl.Result{}, err
70 | }
71 |
72 | return ctrl.Result{}, nil
73 | }
74 |
75 | func (r *JupyterKernelReconciler) SetupWithManager(mgr ctrl.Manager) error {
76 | return ctrl.NewControllerManagedBy(mgr).
77 | For(&kubeflowtkestackiov1alpha1.JupyterKernel{}).
78 | Complete(r)
79 | }
80 |
--------------------------------------------------------------------------------
/controllers/jupyterkernelspec_controller.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package controllers
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | "k8s.io/apimachinery/pkg/api/errors"
24 | "k8s.io/apimachinery/pkg/runtime"
25 | "k8s.io/client-go/tools/record"
26 | ctrl "sigs.k8s.io/controller-runtime"
27 | "sigs.k8s.io/controller-runtime/pkg/client"
28 |
29 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
30 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
31 | "github.com/tkestack/elastic-jupyter-operator/pkg/kernelspec"
32 | )
33 |
34 | // JupyterKernelSpecReconciler reconciles a JupyterKernelSpec object
35 | type JupyterKernelSpecReconciler struct {
36 | client.Client
37 | Log logr.Logger
38 | Recorder record.EventRecorder
39 | Scheme *runtime.Scheme
40 | }
41 |
42 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkernelspecs,verbs=get;list;watch;create;update;patch;delete
43 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkernelspecs/status,verbs=get;update;patch
44 |
45 | func (r *JupyterKernelSpecReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
46 | _ = context.Background()
47 | _ = r.Log.WithValues("jupyterkernelspec", req.NamespacedName)
48 |
49 | original := &v1alpha1.JupyterKernelSpec{}
50 |
51 | err := r.Get(context.TODO(), req.NamespacedName, original)
52 | if err != nil {
53 | if errors.IsNotFound(err) {
54 | // Object not found, return. Created objects are automatically garbage collected.
55 | // For additional cleanup logic use finalizers.
56 | return ctrl.Result{}, nil
57 | }
58 | // Error reading the object - requeue the request.
59 | r.Log.Error(err, "Failed to get the object, requeuing the request")
60 | return ctrl.Result{}, err
61 | }
62 | instance := original.DeepCopy()
63 |
64 | gr, err := kernelspec.NewReconciler(r.Client, r.Log, r.Recorder, r.Scheme, instance)
65 | if err != nil {
66 | return ctrl.Result{}, err
67 | }
68 | if err := gr.Reconcile(); err != nil {
69 | return ctrl.Result{}, err
70 | }
71 |
72 | return ctrl.Result{}, nil
73 | }
74 |
75 | func (r *JupyterKernelSpecReconciler) SetupWithManager(mgr ctrl.Manager) error {
76 | return ctrl.NewControllerManagedBy(mgr).
77 | For(&kubeflowtkestackiov1alpha1.JupyterKernelSpec{}).
78 | Complete(r)
79 | }
80 |
--------------------------------------------------------------------------------
/controllers/jupyterkerneltemplate_controller.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package controllers
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | "k8s.io/apimachinery/pkg/runtime"
24 | ctrl "sigs.k8s.io/controller-runtime"
25 | "sigs.k8s.io/controller-runtime/pkg/client"
26 |
27 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
28 | )
29 |
30 | // JupyterKernelTemplateReconciler reconciles a JupyterKernelTemplate object
31 | type JupyterKernelTemplateReconciler struct {
32 | client.Client
33 | Log logr.Logger
34 | Scheme *runtime.Scheme
35 | }
36 |
37 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkerneltemplates,verbs=get;list;watch;create;update;patch;delete
38 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyterkerneltemplates/status,verbs=get;update;patch
39 |
40 | func (r *JupyterKernelTemplateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
41 | _ = context.Background()
42 | _ = r.Log.WithValues("jupyterkerneltemplate", req.NamespacedName)
43 |
44 | // your logic here
45 |
46 | return ctrl.Result{}, nil
47 | }
48 |
49 | func (r *JupyterKernelTemplateReconciler) SetupWithManager(mgr ctrl.Manager) error {
50 | return ctrl.NewControllerManagedBy(mgr).
51 | For(&kubeflowtkestackiov1alpha1.JupyterKernelTemplate{}).
52 | Complete(r)
53 | }
54 |
--------------------------------------------------------------------------------
/controllers/jupyternotebook_controller.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2021.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package controllers
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | appsv1 "k8s.io/api/apps/v1"
24 | "k8s.io/apimachinery/pkg/api/errors"
25 | "k8s.io/apimachinery/pkg/runtime"
26 | "k8s.io/client-go/tools/record"
27 | ctrl "sigs.k8s.io/controller-runtime"
28 | "sigs.k8s.io/controller-runtime/pkg/client"
29 | "sigs.k8s.io/controller-runtime/pkg/handler"
30 | "sigs.k8s.io/controller-runtime/pkg/source"
31 |
32 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
33 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
34 | "github.com/tkestack/elastic-jupyter-operator/pkg/notebook"
35 | )
36 |
37 | // JupyterNotebookReconciler reconciles a JupyterNotebook object
38 | type JupyterNotebookReconciler struct {
39 | client.Client
40 | Log logr.Logger
41 | Recorder record.EventRecorder
42 | Scheme *runtime.Scheme
43 | }
44 |
45 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyternotebooks,verbs=get;list;watch;create;update;patch;delete
46 | // +kubebuilder:rbac:groups=kubeflow.tkestack.io,resources=jupyternotebooks/status,verbs=get;update;patch
47 |
48 | func (r *JupyterNotebookReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
49 | _ = context.Background()
50 | _ = r.Log.WithValues("jupyternotebook", req.NamespacedName)
51 |
52 | original := &v1alpha1.JupyterNotebook{}
53 |
54 | err := r.Get(context.TODO(), req.NamespacedName, original)
55 | if err != nil {
56 | if errors.IsNotFound(err) {
57 | // Object not found, return. Created objects are automatically garbage collected.
58 | // For additional cleanup logic use finalizers.
59 | return ctrl.Result{}, nil
60 | }
61 | // Error reading the object - requeue the request.
62 | r.Log.Error(err, "Failed to get the object, requeuing the request")
63 | return ctrl.Result{}, err
64 | }
65 | instance := original.DeepCopy()
66 |
67 | gr, err := notebook.NewReconciler(r.Client, r.Log, r.Recorder, r.Scheme, instance)
68 | if err != nil {
69 | return ctrl.Result{}, err
70 | }
71 | if err := gr.Reconcile(); err != nil {
72 | return ctrl.Result{}, err
73 | }
74 | return ctrl.Result{}, nil
75 | }
76 |
77 | func (r *JupyterNotebookReconciler) SetupWithManager(mgr ctrl.Manager) error {
78 | return ctrl.NewControllerManagedBy(mgr).
79 | For(&kubeflowtkestackiov1alpha1.JupyterNotebook{}).
80 | Watches(&source.Kind{Type: &appsv1.Deployment{}},
81 | &handler.EnqueueRequestForOwner{
82 | IsController: true,
83 | OwnerType: &v1alpha1.JupyterNotebook{},
84 | }).
85 | Complete(r)
86 | }
87 |
--------------------------------------------------------------------------------
/controllers/jupyternotebook_controller_test.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | . "github.com/onsi/ginkgo"
8 | . "github.com/onsi/gomega"
9 | v1 "k8s.io/api/core/v1"
10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | "k8s.io/apimachinery/pkg/types"
12 |
13 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
14 | )
15 |
16 | var _ = Describe("JupyterNotebook controller", func() {
17 | const (
18 | JupyterNotebookName = "jupyternotebook-sample"
19 | JupyterNotebookNamespace = "default"
20 | DefaultContainerName = "notebook"
21 | DefaultImage = "busysandbox"
22 |
23 | timeout = time.Second * 10
24 | duration = time.Second * 10
25 | interval = time.Millisecond * 250
26 | )
27 |
28 | var (
29 | podSpec = v1.PodSpec{
30 | Containers: []v1.Container{
31 | {
32 | Name: DefaultContainerName,
33 | Image: DefaultImage,
34 | ImagePullPolicy: v1.PullIfNotPresent,
35 | },
36 | },
37 | }
38 |
39 | notebookWithTemplate = &kubeflowtkestackiov1alpha1.JupyterNotebook{
40 | TypeMeta: metav1.TypeMeta{
41 | APIVersion: "kubeflow.tkestack.io/v1alpha1",
42 | Kind: "JupyterNotebook",
43 | },
44 | ObjectMeta: metav1.ObjectMeta{
45 | Name: JupyterNotebookName,
46 | Namespace: JupyterNotebookNamespace,
47 | },
48 | Spec: kubeflowtkestackiov1alpha1.JupyterNotebookSpec{
49 | Template: &v1.PodTemplateSpec{
50 | Spec: podSpec,
51 | },
52 | },
53 | }
54 |
55 | key = types.NamespacedName{
56 | Name: JupyterNotebookName,
57 | Namespace: JupyterNotebookNamespace,
58 | }
59 | )
60 |
61 | Context("JupyterNotebook only have template", func() {
62 | It("Should create successfully", func() {
63 | Expect(k8sClient.Create(context.Background(), notebookWithTemplate)).Should(Succeed())
64 | By("Expecting container name")
65 | Eventually(func() string {
66 | actual := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
67 | if err := k8sClient.Get(context.Background(), key, actual); err == nil {
68 | return actual.Spec.Template.Spec.Containers[0].Name
69 | }
70 | return ""
71 | }, timeout, interval).Should(Equal(DefaultContainerName))
72 | })
73 |
74 | It("Should update successfully", func() {
75 | name := "NewName"
76 | actual := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
77 | Expect(k8sClient.Get(context.Background(), key, actual)).Should(Succeed())
78 | actual.Spec.Template.Name = name
79 | Expect(k8sClient.Update(context.Background(), actual)).Should(Succeed())
80 |
81 | By("Expecting template name")
82 | Eventually(func() string {
83 | notebook := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
84 | if err := k8sClient.Get(context.Background(), key, notebook); err == nil {
85 | return actual.Spec.Template.Name
86 | }
87 | return ""
88 | }, timeout, interval).Should(Equal(name))
89 | })
90 |
91 | It("Should delete successfully", func() {
92 | By("Expecting to delete successfully")
93 | Eventually(func() error {
94 | actual := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
95 | k8sClient.Get(context.Background(), key, actual)
96 | return k8sClient.Delete(context.Background(), actual)
97 | }, timeout, interval).Should(Succeed())
98 |
99 | By("Expecting to delete finish")
100 | Eventually(func() error {
101 | actual := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
102 | return k8sClient.Get(context.Background(), key, actual)
103 | }, timeout, interval).ShouldNot(Succeed())
104 | })
105 | })
106 | })
107 |
--------------------------------------------------------------------------------
/controllers/suite_test.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2021.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package controllers
18 |
19 | import (
20 | "path/filepath"
21 | "testing"
22 |
23 | . "github.com/onsi/ginkgo"
24 | . "github.com/onsi/gomega"
25 | "k8s.io/client-go/kubernetes/scheme"
26 | "k8s.io/client-go/rest"
27 | ctrl "sigs.k8s.io/controller-runtime"
28 | "sigs.k8s.io/controller-runtime/pkg/client"
29 | "sigs.k8s.io/controller-runtime/pkg/envtest"
30 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
31 | logf "sigs.k8s.io/controller-runtime/pkg/log"
32 | "sigs.k8s.io/controller-runtime/pkg/log/zap"
33 |
34 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
35 | // +kubebuilder:scaffold:imports
36 | )
37 |
38 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to
39 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
40 |
41 | var cfg *rest.Config
42 | var k8sClient client.Client
43 | var testEnv *envtest.Environment
44 |
45 | func TestAPIs(t *testing.T) {
46 | RegisterFailHandler(Fail)
47 |
48 | RunSpecsWithDefaultAndCustomReporters(t,
49 | "Controller Suite",
50 | []Reporter{printer.NewlineReporter{}})
51 | }
52 |
53 | var _ = BeforeSuite(func(done Done) {
54 | logf.SetLogger(zap.New(zap.UseDevMode(true)))
55 |
56 | By("bootstrapping test environment")
57 | testEnv = &envtest.Environment{
58 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
59 | }
60 |
61 | var err error
62 | cfg, err = testEnv.Start()
63 | Expect(err).ToNot(HaveOccurred())
64 | Expect(cfg).ToNot(BeNil())
65 |
66 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
67 | Expect(err).NotTo(HaveOccurred())
68 |
69 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
70 | Expect(err).NotTo(HaveOccurred())
71 |
72 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
73 | Expect(err).NotTo(HaveOccurred())
74 |
75 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
76 | Expect(err).NotTo(HaveOccurred())
77 |
78 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
79 | Expect(err).NotTo(HaveOccurred())
80 |
81 | // +kubebuilder:scaffold:scheme
82 |
83 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
84 | Scheme: scheme.Scheme,
85 | })
86 | Expect(err).ToNot(HaveOccurred())
87 |
88 | err = (&JupyterNotebookReconciler{
89 | Client: k8sManager.GetClient(),
90 | Log: ctrl.Log.WithName("controllers").WithName("JupyterNotebook"),
91 | Recorder: k8sManager.GetEventRecorderFor("JupyterNotebook"),
92 | Scheme: k8sManager.GetScheme(),
93 | }).SetupWithManager(k8sManager)
94 | Expect(err).ToNot(HaveOccurred())
95 |
96 | go func() {
97 | err = k8sManager.Start(ctrl.SetupSignalHandler())
98 | Expect(err).ToNot(HaveOccurred())
99 | }()
100 |
101 | k8sClient = k8sManager.GetClient()
102 | Expect(k8sClient).ToNot(BeNil())
103 |
104 | close(done)
105 | }, 60)
106 |
107 | var _ = AfterSuite(func() {
108 | By("tearing down the test environment")
109 | err := testEnv.Stop()
110 | Expect(err).ToNot(HaveOccurred())
111 | })
112 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Elastic Jupyter Notebooks on Kubernetes: the Cloud Native Way
2 |
3 |
4 | Running [Jupyter Notebook][] on Kubernetes is common, but it's not easy. The notebook server runs kernels on the host by default. However, it is necessary to run remote kernels if [Jupyter Notebook][] is deployed and used on Kubernetes.
5 |
6 | Deep learning model training is a good example. It requires lots of resources, usually some GPUs. Meanwhile, GPU resources are expensive to use. Thus users expect sharing GPUs between notebooks.
7 |
8 | ## State of the art
9 |
10 | There are some existing [Jupyter Notebook][] operators in Kubernetes community, such as [Kubeflow jupyter operator](https://github.com/kubeflow/kubeflow/tree/master/components/notebook-controller). These projects just deploy the [Jupyter Notebook][] as a deployment directly on Kubernetes. The GPU utilization does not meet our expectation, because the GPUs are allocated by users statically.
11 |
12 | [Jupyter Enterprise Gateway][] could help us improve the utilization by running the notebook server processes and kernel processes separately. But there are some limitations. [Jupyter Enterprise Gateway][] is designed to be used on different resource managers, e.g. Yarn, Kubernetes, etc. Thus it is not Kubernetes native. Maintaining such a gateway and multiple notebook servers/kernels is not easy.
13 |
14 | 
15 |
16 | Besides this, customizing the kernel specifications requires [rebooting the enterprise gateway](https://github.com/jupyter/enterprise_gateway/blob/master/etc/docker/enterprise-gateway/Dockerfile#L30) on Kubernetes, because the kernel specifications are hard-coded in the image.
17 |
18 | Last, the resources used by the kernel can not be updated easily. The Kernel YAML template is defined as [jinja2 template](https://github.com/jupyter/enterprise_gateway/blob/master/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2). It is also hard-coded in the image.
19 |
20 | To solve these problems, we implemented a new operator [elastic-jupyter-operator][] based on Kubernetes and [Jupyter Enterprise Gateway][], to make it easy to deploy and use elastic [Jupyter Notebook][] on Kubernetes. You can manage the notebook server and kernels on Kubernetes in a declarative way via the CustomResourceDefinitions (CRDs), instead of getting trouble with the containers and networking things.
21 |
22 | ## Quick start
23 |
24 | First you need to clone the repository and install the operator. Five CustomResourceDefinitions (CRDs) are installed in the cluster: `JupyterGateway`, `JupyterNotebook`, `JupyterKernel`, `JupyterKernelTemplate` and `JupyterKernelSpec`.
25 |
26 | ```yaml
27 | git clone git@github.com:tkestack/elastic-jupyter-operator.git
28 | kubectl apply -f ./hack/enterprise_gateway/prepare.yaml
29 | make deploy
30 | ```
31 |
32 | ### Remote kernels
33 |
34 | Users can create the elastic [Jupyter Notebook][] on Kubernetes by creating `JupyterNotebook` and `JupyterGateway`.
35 |
36 | ```bash
37 | $ cat ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
38 | apiVersion: kubeflow.tkestack.io/v1alpha1
39 | kind: JupyterNotebook
40 | metadata:
41 | name: jupyternotebook-elastic
42 | spec:
43 | gateway:
44 | name: jupytergateway-elastic
45 | namespace: default
46 | auth:
47 | mode: disable
48 |
49 | $ cat ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
50 | apiVersion: kubeflow.tkestack.io/v1alpha1
51 | kind: JupyterGateway
52 | metadata:
53 | name: jupytergateway-elastic
54 | spec:
55 | cullIdleTimeout: 3600
56 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
57 |
58 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
59 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
60 | $ kubectl port-forward deploy/jupyternotebook-elastic 8888:8888
61 | ```
62 |
63 | When the code is executed in the notebook page, there will be a new kernel pod created in the cluster.
64 |
65 | ```
66 | NAME READY STATUS RESTARTS AGE
67 | kernel-219cfd49-89ad-428c-8e0d-3e61e15d79a7 1/1 Running 0 170m
68 | jupytergateway-elastic-868d8f465c-8mg44 1/1 Running 0 3h
69 | jupyternotebook-elastic-787d94bb4b-xdwnc 1/1 Running 0 3h10m
70 | ```
71 |
72 | ### Remote kernels with custom configuration
73 |
74 | If you want to custom the kernel deployment, for example. you want to update the resource requirements of the python kernel or use different images for the kernel, you can deploy the jupyter notebooks and gateways with custom kernels.
75 |
76 | First, you need to create the JupyterKernelSpec CR, which is used to generate the [Jupyter kernelspec](https://jupyter-client.readthedocs.io/en/stable/kernels.html).
77 |
78 | ```yaml
79 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
80 | apiVersion: kubeflow.tkestack.io/v1alpha1
81 | kind: JupyterKernelSpec
82 | metadata:
83 | name: python-kubernetes
84 | spec:
85 | language: Python
86 | displayName: "Python on Kubernetes as a JupyterKernelSpec"
87 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
88 | className: enterprise_gateway.services.processproxies.kubeflow.KubeflowProcessProxy
89 | # Use the template defined in JupyterKernelTemplate CR.
90 | template:
91 | namespace: default
92 | name: jupyterkerneltemplate-elastic-with-custom-kernels
93 | command:
94 | # Use the default scripts to launch the kernel.
95 | - "kubeflow-launcher"
96 | - "--verbose"
97 | - "--RemoteProcessProxy.kernel-id"
98 | - "{kernel_id}"
99 | - "--RemoteProcessProxy.port-range"
100 | - "{port_range}"
101 | - "--RemoteProcessProxy.response-address"
102 | - "{response_address}"
103 |
104 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
105 | apiVersion: kubeflow.tkestack.io/v1alpha1
106 | kind: JupyterKernelTemplate
107 | metadata:
108 | name: jupyterkerneltemplate-elastic-with-custom-kernels
109 | spec:
110 | template:
111 | metadata:
112 | app: enterprise-gateway
113 | component: kernel
114 | spec:
115 | restartPolicy: Always
116 | containers:
117 | - name: kernel
118 |
119 | $ kubectl apply -f ./examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
120 | $ kubectl apply -f ./examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
121 | ```
122 |
123 | There will be a configmap created with the given CR, and it will be mounted into the gateway.
124 |
125 | ```yaml
126 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
127 | apiVersion: kubeflow.tkestack.io/v1alpha1
128 | kind: JupyterGateway
129 | metadata:
130 | name: jupytergateway-elastic-with-custom-kernels
131 | spec:
132 | cullIdleTimeout: 10
133 | cullInterval: 10
134 | logLevel: DEBUG
135 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
136 | # Use the kernel which is defined in JupyterKernelSpec CR.
137 | kernels:
138 | - python-kubernetes
139 |
140 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
141 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
142 | $ kubectl port-forward deploy/jupyternotebook-elastic-with-custom-kernels 8888:8888
143 | ```
144 |
145 | ## Design and implementation
146 |
147 | [elastic-jupyter-operator][] reuses the [Jupyter Enterprise Gateway][] to support remote execution of Jupyter notebooks. The request will be sent to the notebook server process first when users execute the code in the browser. But the request cannot be processed since there is no kernel to execute it. The notebook server will issue a request then to the gateway to create a new kernel. The gateway creates the `JupyterKernel` CR via our custom `KubeflowProcessProxy` in the gateway's source code. The operator watches the `JupyterKernel` CR and creates the corresponding kernel pod in Kubernetes. Then the execution result will be sent back to the notebook server via ZeroMQ.
148 |
149 | 
150 |
151 | The gateway monitors the kernels and culls the idle kernels. The operator also monitors them to restart the kernel if the kernel is not ready.
152 |
153 | ## Summary
154 |
155 | There are still too many features to be covered in this document. But the basic features are listed below:
156 |
157 | - Remote Jupyter kernel execution with custom configuration
158 | - Declarative way to manage Jupyter Notebook and Geteway
159 | - Support adding/removing kernel specs dynamically
160 | - Support custom kernel image, command, resource requirements and so on
161 |
162 | ## License
163 |
164 | - This article is licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/).
165 | - Please contact me for commercial use.
166 |
167 |
168 | [Jupyter Enterprise Gateway]: https://jupyter.org/enterprise_gateway/
169 | [Jupyter Notebook]: https://jupyter.org/
170 | [elastic-jupyter-operator]: https://github.com/tkestack/elastic-jupyter-operator
171 |
--------------------------------------------------------------------------------
/docs/api/autogen/config.yaml:
--------------------------------------------------------------------------------
1 | render:
2 | kubernetesVersion: "1.20"
--------------------------------------------------------------------------------
/docs/api/autogen/templates/gv_details.tpl:
--------------------------------------------------------------------------------
1 | {{- define "gvDetails" -}}
2 | {{- $gv := . -}}
3 | [id="{{ asciidocGroupVersionID $gv | asciidocRenderAnchorID }}"]
4 | == {{ $gv.GroupVersionString }}
5 |
6 | {{ $gv.Doc }}
7 |
8 | {{- if $gv.Kinds }}
9 | .Resource Types
10 | {{- range $gv.SortedKinds }}
11 | - {{ $gv.TypeForKind . | asciidocRenderTypeLink }}
12 | {{- end }}
13 | {{ end }}
14 |
15 | === Definitions
16 | {{ range $gv.SortedTypes }}
17 | {{ template "type" . }}
18 | {{ end }}
19 |
20 | {{- end -}}
--------------------------------------------------------------------------------
/docs/api/autogen/templates/gv_list.tpl:
--------------------------------------------------------------------------------
1 | {{- define "gvList" -}}
2 | {{- $groupVersions := . -}}
3 |
4 | // Generated documentation. Please do not edit.
5 | :anchor_prefix: k8s-api
6 |
7 | [id="{p}-api-reference"]
8 | = API Reference
9 |
10 | .Packages
11 | {{- range $groupVersions }}
12 | - {{ asciidocRenderGVLink . }}
13 | {{- end }}
14 |
15 | {{ range $groupVersions }}
16 | {{ template "gvDetails" . }}
17 | {{ end }}
18 |
19 | {{- end -}}
--------------------------------------------------------------------------------
/docs/api/autogen/templates/type.tpl:
--------------------------------------------------------------------------------
1 | {{- define "type" -}}
2 | {{- $type := . -}}
3 | {{- if asciidocShouldRenderType $type -}}
4 |
5 | [id="{{ asciidocTypeID $type | asciidocRenderAnchorID }}"]
6 | ==== {{ $type.Name }} {{ if $type.IsAlias }}({{ asciidocRenderTypeLink $type.UnderlyingType }}) {{ end }}
7 |
8 | {{ $type.Doc }}
9 |
10 | {{ if $type.References -}}
11 | .Appears In:
12 | ****
13 | {{- range $type.SortedReferences }}
14 | - {{ asciidocRenderTypeLink . }}
15 | {{- end }}
16 | ****
17 | {{- end }}
18 |
19 | {{ if $type.Members -}}
20 | [cols="25a,75a", options="header"]
21 | |===
22 | | Field | Description
23 | {{ if $type.GVK -}}
24 | | *`apiVersion`* __string__ | `{{ $type.GVK.Group }}/{{ $type.GVK.Version }}`
25 | | *`kind`* __string__ | `{{ $type.GVK.Kind }}`
26 | {{ end -}}
27 |
28 | {{ range $type.Members -}}
29 | | *`{{ .Name }}`* __{{ asciidocRenderType .Type }}__ | {{ template "type_members" . }}
30 | {{ end -}}
31 | |===
32 | {{ end -}}
33 |
34 | {{- end -}}
35 | {{- end -}}
--------------------------------------------------------------------------------
/docs/api/autogen/templates/type_members.tpl:
--------------------------------------------------------------------------------
1 | {{- define "type_members" -}}
2 | {{- $field := . -}}
3 | {{- if eq $field.Name "metadata" -}}
4 | Refer to Kubernetes API documentation for fields of `metadata`.
5 | {{ else -}}
6 | {{ $field.Doc }}
7 | {{- end -}}
8 | {{- end -}}
--------------------------------------------------------------------------------
/docs/design.md:
--------------------------------------------------------------------------------
1 | There are 5 CRDs defined in elastic-jupyter-operator:
2 |
3 | - jupytergateways.kubeflow.tkestack.io
4 | - jupyterkernels.kubeflow.tkestack.io
5 | - jupyterkernelspecs.kubeflow.tkestack.io
6 | - jupyterkerneltemplates.kubeflow.tkestack.io
7 | - jupyternotebooks.kubeflow.tkestack.io
8 |
9 | elastic-jupyter-operator 的架构如图所示,`JupyterGateway` 和 `JupyterNotebook` 是两个 CRD。其中 Notebook 是 Jupyter Notebook 的前端服务,负责面向用户提供用户界面,并且与后端服务通过 HTTPS 和 Websocket 进行通信,处理用户的计算请求。
10 |
11 | Gateway 是对应的后端服务。它负责处理来自 Notebook CR 的请求,通过调用 Kubernetes 的 API 按需创建出真正负责处理用户计算任务的 Kernel。
12 |
13 | 
--------------------------------------------------------------------------------
/docs/images/arch.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/arch.jpeg
--------------------------------------------------------------------------------
/docs/images/elastic.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/elastic.jpeg
--------------------------------------------------------------------------------
/docs/images/gateway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/gateway.png
--------------------------------------------------------------------------------
/docs/images/jupyter.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/jupyter.jpeg
--------------------------------------------------------------------------------
/docs/images/kubeflow.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/kubeflow.jpeg
--------------------------------------------------------------------------------
/docs/images/multiuser.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/multiuser.jpeg
--------------------------------------------------------------------------------
/docs/images/overview.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/overview.jpeg
--------------------------------------------------------------------------------
/docs/images/uml.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skai-x/elastic-jupyter-operator/941f006c729cdaf8412e2a846a1c49ec1ce54d23/docs/images/uml.jpeg
--------------------------------------------------------------------------------
/docs/kernel.md:
--------------------------------------------------------------------------------
1 | # Design Proposal for Jupyter Kernel CRD
2 |
3 | Authors:
4 | - Ce Gao
5 |
6 | ## Background
7 |
8 | There are two CustomResourceDefinitions `JupyterNotebook` and `JupyterGateway` for users. They are used to create Notebook instances and [Jupyter Enterprise Gateways](https://github.com/jupyter/enterprise_gateway). But it is still hard to configure the kernels.
9 |
10 | ## Motivation
11 |
12 | When we configure kernels with Jupyter Enterprise Gateway, we need to provide the kernel json format configburation files. Currently, these configurations is packaged with the gateway image. The kernel specification is shown here:
13 |
14 | ```
15 | kernel.json
16 | logo-64x64.png
17 | scripts/
18 | kernel-pod.yaml.j2
19 | launch_kubernetes.py
20 | ```
21 |
22 | The scripts directory is copied from the source code of enterprise gateway, and the kernel.json looks like:
23 |
24 | ```json
25 | {
26 | "language": "python",
27 | "display_name": "Python on Kubernetes with Tensorflow",
28 | "metadata": {
29 | "process_proxy": {
30 | "class_name": "enterprise_gateway.services.processproxies.k8s.KubernetesProcessProxy",
31 | "config": {
32 | "image_name": "elyra/kernel-tf-py:VERSION"
33 | }
34 | }
35 | },
36 | "env": {
37 | },
38 | "argv": [
39 | "python",
40 | "/usr/local/share/jupyter/kernels/python_tf_kubernetes/scripts/launch_kubernetes.py",
41 | "--RemoteProcessProxy.kernel-id",
42 | "{kernel_id}",
43 | "--RemoteProcessProxy.response-address",
44 | "{response_address}"
45 | ]
46 | }
47 | ```
48 |
49 | It is hand-written by enterprise-gateway maintainers. All kernel specs are copied into the enterprise-gateway docker image:
50 |
51 | ```dockerfile
52 | ADD jupyter_enterprise_gateway_kernelspecs*.tar.gz /usr/local/share/jupyter/kernels/
53 | ```
54 |
55 | Thus it is hard to update them on the fly.
56 |
57 | ## Goals
58 |
59 | This proposal is to allow users to:
60 |
61 | - Configure kernels on the fly
62 | - Manage kernels on Kubernetes easily
63 |
64 | ## Non-goals
65 |
66 | This proposal is not to:
67 |
68 | - Make the design the default implementaion, which means that the current design and implementation will not change.
69 |
70 | ## Implementation
71 |
72 | The design proposal involves four main changes to elastic jupyter operator and enterprise gateway:
73 |
74 | - Kernel CRD (new): It is used to manage kernels on Kubernetes
75 | - KernelSpec CRD (new): It is used to configure the kernel specs in runtime
76 | - KernelTemplate CRD (new): It is used to configure kernel in runtime
77 | - JupyterGateway CRD: Changes is made to support updating kernels on the fly
78 | - Kubeflow Kernel Launcher (new): It is used to launch Kernel CRD inside the gateway
79 | - Kubeflow Process Proxy (new): It is used to manage kernels in enterprise gateway
80 |
81 | ### Kernel CRD
82 |
83 | ```yaml
84 | spec:
85 | ID: "{{ kernel_id }}"
86 | restartPolicy: Never
87 | serviceAccountName: "{{ kernel_service_account_name }}"
88 | securityContext: ...
89 | environments:
90 | respondAddress: "{{ eg_response_address }}"
91 | language: "{{ kernel_language }}"
92 | ```
93 |
94 | ### KernelTemplate CRD
95 |
96 | KernelTemplate CRD is used to replace [etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2](https://github.com/jupyter/enterprise_gateway/blob/master/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2). Its definition looks like:
97 |
98 | ```yaml
99 | apiVersion: kubeflow.tkestack.io/v1alpha1
100 | kind: JupyterKernelTemplate
101 | metadata:
102 | name: jupyterkerneltemplate-sample
103 | spec:
104 | template:
105 | metadata:
106 | app: enterprise-gateway
107 | component: kernel
108 | template:
109 | spec:
110 | restartPolicy: Never
111 | containers:
112 | - name: kernel
113 | ```
114 |
115 | ### KernelSpec CRD
116 |
117 | > The primary vehicle for indicating a given kernel should be handled in a different manner is the kernel specification, otherwise known as the kernel spec. Enterprise Gateway leverages the natively extensible metadata stanza to introduce a new stanza named process_proxy.
118 | >
119 | > The process_proxy stanza identifies the class that provides the kernel’s process abstraction (while allowing for future extensions). This class then provides the kernel’s lifecycle management operations relative to the managed resource or functional equivalent.
120 | >
121 | > Here’s an example of a kernel specification that uses the DistributedProcessProxy class for its abstraction:
122 | >
123 | ```json
124 | {
125 | "language": "scala",
126 | "display_name": "Spark - Scala (YARN Client Mode)",
127 | "metadata": {
128 | "process_proxy": {
129 | "class_name": "enterprise_gateway.services.processproxies.distributed.DistributedProcessProxy"
130 | }
131 | },
132 | "env": {
133 | "SPARK_HOME": "/usr/hdp/current/spark2-client",
134 | "__TOREE_SPARK_OPTS__": "--master yarn --deploy-mode client --name ${KERNEL_ID:-ERROR__NO__KERNEL_ID}",
135 | "__TOREE_OPTS__": "",
136 | "LAUNCH_OPTS": "",
137 | "DEFAULT_INTERPRETER": "Scala"
138 | },
139 | "argv": [
140 | "/usr/local/share/jupyter/kernels/spark_scala_yarn_client/bin/run.sh",
141 | "--RemoteProcessProxy.kernel-id",
142 | "{kernel_id}",
143 | "--RemoteProcessProxy.response-address",
144 | "{response_address}",
145 | "--RemoteProcessProxy.public-key",
146 | "{public_key}"
147 | ]
148 | }
149 | ```
150 |
151 | The kernel specifications are placed in the docker image at build time, which is not easy to maintain on the fly. The kernelspec CRD is defined to support dynamic update. The CRD specification looks like this:
152 |
153 | ```yaml
154 | spec:
155 | language: Python
156 | displayName: "Python on Kubernetes with Tensorflow"
157 | image: elyra/kernel-tf-py:VERSION
158 | envs: ...
159 | command:
160 | - "python",
161 | - "/usr/local/share/jupyter/scripts/launch_kubernetes.py",
162 | - "--RemoteProcessProxy.kernel-id",
163 | - "{kernel_id}",
164 | - "--RemoteProcessProxy.response-address",
165 | - "{response_address}"
166 | ```
167 |
168 | When a JupyterKernelSpec CR is created, we will create the corresponding configmap. And the configmap will be used as a mount volume in the gateway.
169 |
170 | Besides this, The KernelSpec CRD maintains a object reference to one KernelTemplate CRD. It is used to generate commands.
171 |
172 | ```diff
173 | {
174 | "language": "Python",
175 | "display_name": "Python on Kubernetes as a JupyterKernelSpec",
176 | "metadata": {
177 | "process_proxy": {
178 | "class_name": "enterprise_gateway.services.processproxies.k8s.KubernetesProcessProxy"
179 | },
180 | "config": {
181 | "image_name": "ghcr.io/skai-x/jupyter-kernel-py:2.6.0"
182 | }
183 | },
184 | "argv": [
185 | "kubeflow-launcher",
186 | "--RemoteProcessProxy.kernel-id",
187 | "{kernel_id}",
188 | "--RemoteProcessProxy.port-range",
189 | "{port_range}",
190 | "--RemoteProcessProxy.response-address",
191 | "{response_address}",
192 | + "--kernel-template-name",
193 | + "jupyterkerneltemplate-sample",
194 | + "--kernel-template-namespace",
195 | + "default"
196 | ]
197 | }
198 | ```
199 |
200 | ### JupyterGateway CRD
201 |
202 | The specification generation logic needs to be changed to support the new JupyterKernelSpec CRD.
203 |
204 | ```yaml
205 | spec:
206 | kernels:
207 | - python
208 | - r
209 | - dask
210 | ...
211 | ```
212 |
213 | When `kernels` are defined in the spec, we should get the jupyter kernelspec CRs from the kubernetes api server, then mount the configmaps as volumes into the gateway container.
214 |
215 | ### Kernel Launcher
216 |
217 | The [kernel launcher](https://github.com/tkestack/elastic-jupyter-operator/tree/master/cli) is introduced to replace [etc/kernel-launchers/kubernetes/scripts/launch_kubernetes.py](https://github.com/jupyter/enterprise_gateway/blob/master/etc/kernel-launchers/kubernetes/scripts/launch_kubernetes.py).
218 |
219 | The launcher gets the corresponding KernelTemplate CRD and creates the kernel in the cluster.
220 |
221 | ## Reference
222 |
223 | - [Jupyter Enterprise Gateway System Architecture](https://jupyter-enterprise-gateway.readthedocs.io/en/latest/system-architecture.html)
224 |
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | ## Quickstart
2 |
3 | ### Simple deployment
4 |
5 | You can create a simple Jupyter notebook with all components in one pod, like this:
6 |
7 | ```yaml
8 | $ cat ./examples/simple-deployments/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
9 | apiVersion: kubeflow.tkestack.io/v1alpha1
10 | kind: JupyterNotebook
11 | metadata:
12 | name: jupyternotebook-simple
13 | spec:
14 | auth:
15 | mode: disable
16 | template:
17 | metadata:
18 | labels:
19 | notebook: simple
20 | spec:
21 | containers:
22 | - name: notebook
23 | image: jupyter/base-notebook:python-3.9.7
24 | command: ["tini", "-g", "--", "start-notebook.sh"]
25 |
26 | $ kubectl apply -f ./examples/simple-deployments/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
27 | $ kubectl port-forward deploy/jupyternotebook-simple 8888:8888
28 | ```
29 |
30 | Then you can open the URL `http://127.0.0.1:8888/` to get the simple Jupyter notebook instance. The deployment follows the architecture below:
31 |
32 | 
33 |
34 |
35 | ## Elastic deployment
36 |
37 | elastic-jupyter-operator supports running Jupyter kernels in separate pods. In this example, we will create the notebook and gateway.
38 |
39 | ```yaml
40 | $ cat ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
41 | apiVersion: kubeflow.tkestack.io/v1alpha1
42 | kind: JupyterNotebook
43 | metadata:
44 | name: jupyternotebook-elastic
45 | spec:
46 | gateway:
47 | name: jupytergateway-elastic
48 | namespace: default
49 | auth:
50 | mode: disable
51 |
52 | $ cat ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
53 | apiVersion: kubeflow.tkestack.io/v1alpha1
54 | kind: JupyterGateway
55 | metadata:
56 | name: jupytergateway-elastic
57 | spec:
58 | cullIdleTimeout: 3600
59 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
60 |
61 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
62 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
63 | $ kubectl port-forward deploy/jupyternotebook-elastic 8888:8888
64 | ```
65 |
66 | When users run the code in the browser, there will be a new kernel pod created in the cluster.
67 |
68 | ```
69 | NAME READY STATUS RESTARTS AGE
70 | jovyan-219cfd49-89ad-428c-8e0d-3e61e15d79a7 1/1 Running 0 170m
71 | jupytergateway-elastic-868d8f465c-8mg44 1/1 Running 0 3h
72 | jupyternotebook-elastic-787d94bb4b-xdwnc 1/1 Running 0 3h10m
73 | ```
74 |
75 | ### Elastic deployment with custom kernel
76 |
77 | If you want to custom the kernel deployment, for example. you want to update the resource requirements of the python kernel or use different images for the kernel, you can deploy the jupyter notebooks and gateways with custom kernels.
78 |
79 | First, you need to create the JupyterKernelSpec CR, which is used to generate the [Jupyter kernelspec](https://jupyter-client.readthedocs.io/en/stable/kernels.html).
80 |
81 | ```yaml
82 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
83 | apiVersion: kubeflow.tkestack.io/v1alpha1
84 | kind: JupyterKernelSpec
85 | metadata:
86 | name: python-kubernetes
87 | spec:
88 | language: Python
89 | displayName: "Python on Kubernetes as a JupyterKernelSpec"
90 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
91 | className: enterprise_gateway.services.processproxies.kubeflow.KubeflowProcessProxy
92 | # Use the template defined in JupyterKernelTemplate CR.
93 | template:
94 | namespace: default
95 | name: jupyterkerneltemplate-elastic-with-custom-kernels
96 | command:
97 | # Use the default scripts to launch the kernel.
98 | - "kubeflow-launcher"
99 | - "--verbose"
100 | - "--RemoteProcessProxy.kernel-id"
101 | - "{kernel_id}"
102 | - "--RemoteProcessProxy.port-range"
103 | - "{port_range}"
104 | - "--RemoteProcessProxy.response-address"
105 | - "{response_address}"
106 |
107 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
108 | apiVersion: kubeflow.tkestack.io/v1alpha1
109 | kind: JupyterKernelTemplate
110 | metadata:
111 | name: jupyterkerneltemplate-elastic-with-custom-kernels
112 | spec:
113 | template:
114 | metadata:
115 | app: enterprise-gateway
116 | component: kernel
117 | spec:
118 | restartPolicy: Always
119 | containers:
120 | - name: kernel
121 |
122 | $ kubectl apply -f ./examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml
123 | $ kubectl apply -f ./examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml
124 | ```
125 |
126 | There will be a configmap created with the given CR, and it will be mounted into the gateway.
127 |
128 | ```yaml
129 | $ cat examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
130 | apiVersion: kubeflow.tkestack.io/v1alpha1
131 | kind: JupyterGateway
132 | metadata:
133 | name: jupytergateway-elastic-with-custom-kernels
134 | spec:
135 | cullIdleTimeout: 10
136 | cullInterval: 10
137 | logLevel: DEBUG
138 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
139 | # Use the kernel which is defined in JupyterKernelSpec CR.
140 | kernels:
141 | - python-kubernetes
142 |
143 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml
144 | $ kubectl apply -f ./examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml
145 | $ kubectl port-forward deploy/jupyternotebook-elastic-with-custom-kernels 8888:8888
146 | ```
147 |
--------------------------------------------------------------------------------
/examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterGateway
3 | metadata:
4 | name: jupytergateway-elastic-with-custom-kernels
5 | spec:
6 | cullIdleTimeout: 10
7 | cullInterval: 10
8 | logLevel: DEBUG
9 | image: ghcr.io/skai-x/enterprise-gateway:2.6.0
10 | # Use the kernel which is defined in JupyterKernelSpec CR.
11 | kernels:
12 | - python-kubernetes
13 |
--------------------------------------------------------------------------------
/examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkernelspec.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernelSpec
3 | metadata:
4 | name: python-kubernetes
5 | spec:
6 | language: Python
7 | displayName: "Elastic Python Kernel on Kubernetes"
8 | image: ghcr.io/skai-x/jupyter-kernel-py:2.6.0
9 | className: enterprise_gateway.services.processproxies.kubeflow.KubeflowProcessProxy
10 | # Use the template defined in JupyterKernelTemplate CR.
11 | template:
12 | namespace: default
13 | name: jupyterkerneltemplate-elastic-with-custom-kernels
14 | command:
15 | # Use the default scripts to launch the kernel.
16 | - "kubeflow-launcher"
17 | - "--verbose"
18 | - "--RemoteProcessProxy.kernel-id"
19 | - "{kernel_id}"
20 | - "--RemoteProcessProxy.port-range"
21 | - "{port_range}"
22 | - "--RemoteProcessProxy.response-address"
23 | - "{response_address}"
24 | - "--RemoteProcessProxy.public-key"
25 | - "{public_key}"
26 |
--------------------------------------------------------------------------------
/examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyterkerneltemplate.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterKernelTemplate
3 | metadata:
4 | name: jupyterkerneltemplate-elastic-with-custom-kernels
5 | spec:
6 | template:
7 | metadata:
8 | app: enterprise-gateway
9 | component: kernel
10 | spec:
11 | restartPolicy: Always
12 | containers:
13 | - name: kernel
14 |
--------------------------------------------------------------------------------
/examples/elastic-with-custom-kernels/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterNotebook
3 | metadata:
4 | name: jupyternotebook-elastic-with-custom-kernels
5 | spec:
6 | gateway:
7 | name: jupytergateway-elastic-with-custom-kernels
8 | namespace: default
9 | # Disable the password and token based auth in this example,
10 | # please do not do it in PROD.
11 | auth:
12 | mode: disable
13 |
--------------------------------------------------------------------------------
/examples/elastic/kubeflow.tkestack.io_v1alpha1_jupytergateway.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterGateway
3 | metadata:
4 | name: jupytergateway-elastic
5 | spec:
6 | # Timeout (in seconds) after which a kernel is considered idle and ready to be culled.
7 | cullIdleTimeout: 3600
8 | image: ghcr.io/skai-x/enterprise-gateway-with-kernel-spec:latest
9 |
--------------------------------------------------------------------------------
/examples/elastic/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterNotebook
3 | metadata:
4 | name: jupyternotebook-elastic
5 | spec:
6 | gateway:
7 | name: jupytergateway-elastic
8 | namespace: default
9 | # Disable the password and token based auth in this example,
10 | # please do not do it in PROD.
11 | auth:
12 | mode: disable
13 |
--------------------------------------------------------------------------------
/examples/simple-deployments/kubeflow.tkestack.io_v1alpha1_jupyternotebook.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kubeflow.tkestack.io/v1alpha1
2 | kind: JupyterNotebook
3 | metadata:
4 | name: jupyternotebook-simple
5 | spec:
6 | # Disable the password and token based auth in this example,
7 | # please do not do it in PROD.
8 | auth:
9 | mode: disable
10 | template:
11 | metadata:
12 | labels:
13 | notebook: simple
14 | spec:
15 | containers:
16 | - name: notebook
17 | image: jupyter/base-notebook:python-3.9.7
18 | command: ["tini", "-g", "--", "start-notebook.sh"]
19 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tkestack/elastic-jupyter-operator
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/go-logr/logr v0.1.0
7 | github.com/onsi/ginkgo v1.12.1
8 | github.com/onsi/gomega v1.10.1
9 | github.com/spf13/cobra v0.0.5
10 | k8s.io/api v0.18.6
11 | k8s.io/apimachinery v0.18.6
12 | k8s.io/client-go v0.18.6
13 | sigs.k8s.io/controller-runtime v0.6.4
14 | )
15 |
16 | require (
17 | cloud.google.com/go v0.38.0 // indirect
18 | github.com/BurntSushi/toml v0.3.1 // indirect
19 | github.com/beorn7/perks v1.0.0 // indirect
20 | github.com/davecgh/go-spew v1.1.1 // indirect
21 | github.com/evanphx/json-patch v4.9.0+incompatible // indirect
22 | github.com/fsnotify/fsnotify v1.4.9 // indirect
23 | github.com/go-logr/zapr v0.1.0 // indirect
24 | github.com/gogo/protobuf v1.3.1 // indirect
25 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
26 | github.com/golang/protobuf v1.4.2 // indirect
27 | github.com/google/go-cmp v0.4.0 // indirect
28 | github.com/google/gofuzz v1.1.0 // indirect
29 | github.com/google/uuid v1.1.1 // indirect
30 | github.com/googleapis/gnostic v0.3.1 // indirect
31 | github.com/hashicorp/golang-lru v0.5.4 // indirect
32 | github.com/imdario/mergo v0.3.9 // indirect
33 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
34 | github.com/json-iterator/go v1.1.10 // indirect
35 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
37 | github.com/modern-go/reflect2 v1.0.1 // indirect
38 | github.com/nxadm/tail v1.4.4 // indirect
39 | github.com/pkg/errors v0.8.1 // indirect
40 | github.com/prometheus/client_golang v1.0.0 // indirect
41 | github.com/prometheus/client_model v0.2.0 // indirect
42 | github.com/prometheus/common v0.4.1 // indirect
43 | github.com/prometheus/procfs v0.0.11 // indirect
44 | github.com/spf13/pflag v1.0.5 // indirect
45 | go.uber.org/atomic v1.5.0 // indirect
46 | go.uber.org/multierr v1.4.0 // indirect
47 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect
48 | go.uber.org/zap v1.13.0 // indirect
49 | golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 // indirect
50 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de // indirect
51 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect
52 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect
53 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
54 | golang.org/x/text v0.3.3 // indirect
55 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
56 | golang.org/x/tools v0.0.0-20191114161115-faa69481e761 // indirect
57 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
58 | gomodules.xyz/jsonpatch/v2 v2.0.1 // indirect
59 | google.golang.org/appengine v1.5.0 // indirect
60 | google.golang.org/protobuf v1.23.0 // indirect
61 | gopkg.in/inf.v0 v0.9.1 // indirect
62 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
63 | gopkg.in/yaml.v2 v2.3.0 // indirect
64 | honnef.co/go/tools v0.0.1-2019.2.3 // indirect
65 | k8s.io/apiextensions-apiserver v0.18.6 // indirect
66 | k8s.io/klog v1.0.0 // indirect
67 | k8s.io/klog/v2 v2.0.0 // indirect
68 | k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 // indirect
69 | k8s.io/utils v0.0.0-20200603063816-c1c6865ac451 // indirect
70 | sigs.k8s.io/structured-merge-diff/v3 v3.0.0 // indirect
71 | sigs.k8s.io/yaml v1.2.0 // indirect
72 | )
73 |
--------------------------------------------------------------------------------
/hack/add-license.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/..
4 |
5 | cd ${SCRIPT_ROOT}
6 | addlicense -f ./hack/license.txt *.go pkg/**/*.go
7 | cd - >/dev/null
8 |
--------------------------------------------------------------------------------
/hack/boilerplate.go.txt:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 |
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 |
10 | // https://opensource.org/licenses/Apache-2.0
11 |
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
--------------------------------------------------------------------------------
/hack/enterprise_gateway/prepare.yaml:
--------------------------------------------------------------------------------
1 | # This file defines the Kubernetes objects necessary for Enterprise Gateway to run within Kubernetes.
2 | #
3 | apiVersion: v1
4 | kind: Namespace
5 | metadata:
6 | name: enterprise-gateway
7 | labels:
8 | app: enterprise-gateway
9 | ---
10 | apiVersion: v1
11 | kind: ServiceAccount
12 | metadata:
13 | name: enterprise-gateway-sa
14 | namespace: enterprise-gateway
15 | labels:
16 | app: enterprise-gateway
17 | component: enterprise-gateway
18 | ---
19 | apiVersion: rbac.authorization.k8s.io/v1
20 | kind: ClusterRole
21 | metadata:
22 | name: enterprise-gateway-controller
23 | labels:
24 | app: enterprise-gateway
25 | component: enterprise-gateway
26 | rules:
27 | - apiGroups: [""]
28 | resources: ["pods", "namespaces", "services", "configmaps", "secrets", "persistentvolumes", "persistentvolumeclaims"]
29 | verbs: ["get", "watch", "list", "create", "delete"]
30 | - apiGroups: ["rbac.authorization.k8s.io"]
31 | resources: ["rolebindings"]
32 | verbs: ["get", "list", "create", "delete"]
33 | - apiGroups: ["kubeflow.tkestack.io"]
34 | resources: ["jupyterkerneltemplates", "jupyterkernels", "jupytergateways"]
35 | verbs: ["get", "list", "create", "delete"]
36 | ---
37 | apiVersion: rbac.authorization.k8s.io/v1
38 | kind: ClusterRole
39 | metadata:
40 | # Referenced by EG_KERNEL_CLUSTER_ROLE below
41 | name: kernel-controller
42 | labels:
43 | app: enterprise-gateway
44 | component: kernel
45 | rules:
46 | - apiGroups: [""]
47 | resources: ["pods"]
48 | verbs: ["get", "watch", "list", "create", "delete"]
49 | ---
50 | apiVersion: rbac.authorization.k8s.io/v1
51 | kind: ClusterRoleBinding
52 | metadata:
53 | name: enterprise-gateway-controller
54 | labels:
55 | app: enterprise-gateway
56 | component: enterprise-gateway
57 | subjects:
58 | - kind: ServiceAccount
59 | name: enterprise-gateway-sa
60 | namespace: enterprise-gateway
61 | roleRef:
62 | kind: ClusterRole
63 | name: enterprise-gateway-controller
64 | apiGroup: rbac.authorization.k8s.io
65 |
--------------------------------------------------------------------------------
/hack/license.txt:
--------------------------------------------------------------------------------
1 | Tencent is pleased to support the open source community by making TKEStack
2 | available.
3 |
4 | Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | this file except in compliance with the License. You may obtain a copy of the
8 | License at
9 |
10 | https://opensource.org/licenses/Apache-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | specific language governing permissions and limitations under the License.
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2021.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | package main
18 |
19 | import (
20 | "flag"
21 | "os"
22 |
23 | "k8s.io/apimachinery/pkg/runtime"
24 | utilruntime "k8s.io/apimachinery/pkg/util/runtime"
25 | clientgoscheme "k8s.io/client-go/kubernetes/scheme"
26 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
27 | ctrl "sigs.k8s.io/controller-runtime"
28 | "sigs.k8s.io/controller-runtime/pkg/log/zap"
29 |
30 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
31 | "github.com/tkestack/elastic-jupyter-operator/controllers"
32 | // +kubebuilder:scaffold:imports
33 | )
34 |
35 | var (
36 | scheme = runtime.NewScheme()
37 | setupLog = ctrl.Log.WithName("setup")
38 | )
39 |
40 | func init() {
41 | utilruntime.Must(clientgoscheme.AddToScheme(scheme))
42 |
43 | utilruntime.Must(kubeflowtkestackiov1alpha1.AddToScheme(scheme))
44 | // +kubebuilder:scaffold:scheme
45 | }
46 |
47 | func main() {
48 | var metricsAddr string
49 | var enableLeaderElection bool
50 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
51 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
52 | "Enable leader election for controller manager. "+
53 | "Enabling this will ensure there is only one active controller manager.")
54 | flag.Parse()
55 |
56 | ctrl.SetLogger(zap.New(zap.UseDevMode(true)))
57 |
58 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
59 | Scheme: scheme,
60 | MetricsBindAddress: metricsAddr,
61 | Port: 9443,
62 | LeaderElection: enableLeaderElection,
63 | LeaderElectionID: "82ec55e3.kubeflow.tkestack.io",
64 | })
65 | if err != nil {
66 | setupLog.Error(err, "unable to start manager")
67 | os.Exit(1)
68 | }
69 |
70 | if err = (&controllers.JupyterNotebookReconciler{
71 | Client: mgr.GetClient(),
72 | Log: ctrl.Log.WithName("controllers").WithName("JupyterNotebook"),
73 | Recorder: mgr.GetEventRecorderFor("JupyterNotebook"),
74 | Scheme: mgr.GetScheme(),
75 | }).SetupWithManager(mgr); err != nil {
76 | setupLog.Error(err, "unable to create controller", "controller", "JupyterNotebook")
77 | os.Exit(1)
78 | }
79 | if err = (&controllers.JupyterGatewayReconciler{
80 | Client: mgr.GetClient(),
81 | Recorder: mgr.GetEventRecorderFor("JupyterGateway"),
82 | Log: ctrl.Log.WithName("controllers").WithName("JupyterGateway"),
83 | Scheme: mgr.GetScheme(),
84 | }).SetupWithManager(mgr); err != nil {
85 | setupLog.Error(err, "unable to create controller", "controller", "JupyterGateway")
86 | os.Exit(1)
87 | }
88 | if err = (&controllers.JupyterKernelSpecReconciler{
89 | Client: mgr.GetClient(),
90 | Recorder: mgr.GetEventRecorderFor("JupyterKernelSpec"),
91 | Log: ctrl.Log.WithName("controllers").WithName("JupyterKernelSpec"),
92 | Scheme: mgr.GetScheme(),
93 | }).SetupWithManager(mgr); err != nil {
94 | setupLog.Error(err, "unable to create controller", "controller", "JupyterKernelSpec")
95 | os.Exit(1)
96 | }
97 | if err = (&controllers.JupyterKernelTemplateReconciler{
98 | Client: mgr.GetClient(),
99 | Log: ctrl.Log.WithName("controllers").WithName("JupyterKernelTemplate"),
100 | Scheme: mgr.GetScheme(),
101 | }).SetupWithManager(mgr); err != nil {
102 | setupLog.Error(err, "unable to create controller", "controller", "JupyterKernelTemplate")
103 | os.Exit(1)
104 | }
105 | if err = (&controllers.JupyterKernelReconciler{
106 | Client: mgr.GetClient(),
107 | Log: ctrl.Log.WithName("controllers").WithName("JupyterKernel"),
108 | Scheme: mgr.GetScheme(),
109 | }).SetupWithManager(mgr); err != nil {
110 | setupLog.Error(err, "unable to create controller", "controller", "JupyterKernel")
111 | os.Exit(1)
112 | }
113 | // +kubebuilder:scaffold:builder
114 |
115 | setupLog.Info("starting manager")
116 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
117 | setupLog.Error(err, "problem running manager")
118 | os.Exit(1)
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/pkg/gateway/generate.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Tencent is pleased to support the open source community by making TKEStack
3 | * available.
4 | *
5 | * Copyright (C) 2012-2020 Tencent. All Rights Reserved.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
8 | * this file except in compliance with the License. You may obtain a copy of the
9 | * License at
10 | *
11 | * https://opensource.org/licenses/Apache-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 | * WARRANTIES OF ANY KIND, either express or implied. See the License for the
16 | * specific language governing permissions and limitations under the License.
17 | */
18 |
19 | package gateway
20 |
21 | import (
22 | "context"
23 | "fmt"
24 | "strconv"
25 | "strings"
26 |
27 | appsv1 "k8s.io/api/apps/v1"
28 | v1 "k8s.io/api/core/v1"
29 | rbacv1 "k8s.io/api/rbac/v1"
30 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 | "k8s.io/apimachinery/pkg/types"
32 | "sigs.k8s.io/controller-runtime/pkg/client"
33 |
34 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
35 | )
36 |
37 | const (
38 | defaultImage = "ghcr.io/skai-x/enterprise-gateway:2.6.0"
39 | defaultContainerName = "gateway"
40 | defaultKernelImage = "ghcr.io/skai-x/jupyter-kernel-py:2.6.0"
41 | defaultPortName = "gateway"
42 | defaultKernel = "python_kubernetes"
43 | defaultPort = 8888
44 | defaultGatewayClusterRole = "enterprise-gateway-controller"
45 | defaultServiceAccount = "enterprise-gateway-sa"
46 |
47 | LabelGateway = "gateway"
48 | LabelNS = "namespace"
49 |
50 | cullTimeoutOpt = "--MappingKernelManager.cull_idle_timeout"
51 | cullInterval = "--MappingKernelManager.cull_interval"
52 |
53 | defaultKernelPath = "/usr/local/share/jupyter/kernels/"
54 | defaultKernels = "'r_kubernetes','python_kubernetes','python_tf_kubernetes','python_tf_gpu_kubernetes','scala_kubernetes','spark_r_kubernetes','spark_python_kubernetes','spark_scala_kubernetes'"
55 | )
56 |
57 | // generator defines the generator which is used to generate
58 | // desired specs.
59 | type generator struct {
60 | gateway *v1alpha1.JupyterGateway
61 | cli client.Client
62 | }
63 |
64 | // newGenerator creates a new Generator.
65 | func newGenerator(c client.Client, gateway *v1alpha1.JupyterGateway) (
66 | *generator, error) {
67 | if gateway == nil {
68 | return nil, fmt.Errorf("Got nil when initializing Generator")
69 | }
70 | g := &generator{
71 | gateway: gateway,
72 | cli: c,
73 | }
74 |
75 | return g, nil
76 | }
77 |
78 | // DesiredServiceWithoutOwner returns desired service without
79 | // owner.
80 | func (g generator) DesiredServiceWithoutOwner() *v1.Service {
81 | labels := g.labels()
82 | s := &v1.Service{
83 | ObjectMeta: metav1.ObjectMeta{
84 | Namespace: g.gateway.Namespace,
85 | Name: g.gateway.Name,
86 | Labels: labels,
87 | },
88 | Spec: v1.ServiceSpec{
89 | Selector: labels,
90 | Type: v1.ServiceTypeClusterIP,
91 | SessionAffinity: v1.ServiceAffinityClientIP,
92 | Ports: []v1.ServicePort{
93 | {
94 | Name: defaultPortName,
95 | Port: defaultPort,
96 | Protocol: v1.ProtocolTCP,
97 | },
98 | },
99 | },
100 | }
101 | return s
102 | }
103 |
104 | func (g generator) DesiredRoleBinding(
105 | sa *v1.ServiceAccount) *rbacv1.RoleBinding {
106 | labels := g.labels()
107 | crb := &rbacv1.RoleBinding{
108 | ObjectMeta: metav1.ObjectMeta{
109 | Namespace: g.gateway.Namespace,
110 | Name: g.gateway.Name,
111 | Labels: labels,
112 | },
113 | Subjects: []rbacv1.Subject{
114 | {
115 | Kind: "ServiceAccount",
116 | Name: sa.Name,
117 | Namespace: sa.Namespace,
118 | },
119 | },
120 | RoleRef: rbacv1.RoleRef{
121 | Name: defaultGatewayClusterRole,
122 | Kind: "ClusterRole",
123 | APIGroup: "rbac.authorization.k8s.io",
124 | },
125 | }
126 | return crb
127 | }
128 |
129 | func (g generator) DesiredServiceAccountWithoutOwner() *v1.ServiceAccount {
130 | labels := g.labels()
131 | sa := &v1.ServiceAccount{
132 | TypeMeta: metav1.TypeMeta{
133 | Kind: "ServiceAccount",
134 | },
135 | ObjectMeta: metav1.ObjectMeta{
136 | Namespace: g.gateway.Namespace,
137 | Name: g.gateway.Name,
138 | Labels: labels,
139 | },
140 | }
141 | return sa
142 | }
143 |
144 | // DesiredDeploymentWithoutOwner returns the desired deployment
145 | // without owner.
146 | func (g generator) DesiredDeploymentWithoutOwner(
147 | sa string) (*appsv1.Deployment, error) {
148 | // Generate volumes with the kernelspec CR.
149 | volumes, err := g.volumes()
150 | if err != nil {
151 | return nil, err
152 | }
153 |
154 | labels := g.labels()
155 | selector := &metav1.LabelSelector{
156 | MatchLabels: labels,
157 | }
158 | d := &appsv1.Deployment{
159 | ObjectMeta: metav1.ObjectMeta{
160 | Namespace: g.gateway.Namespace,
161 | Name: g.gateway.Name,
162 | Labels: labels,
163 | },
164 | Spec: appsv1.DeploymentSpec{
165 | Selector: selector,
166 | Template: v1.PodTemplateSpec{
167 | ObjectMeta: metav1.ObjectMeta{
168 | Labels: labels,
169 | },
170 | Spec: v1.PodSpec{
171 | ServiceAccountName: sa,
172 | Volumes: volumes,
173 | Containers: []v1.Container{
174 | {
175 | Name: defaultContainerName,
176 | Image: defaultImage,
177 | ImagePullPolicy: v1.PullIfNotPresent,
178 | Ports: []v1.ContainerPort{
179 | {
180 | Name: defaultPortName,
181 | ContainerPort: defaultPort,
182 | Protocol: v1.ProtocolTCP,
183 | },
184 | },
185 | Command: []string{"/usr/local/bin/start-enterprise-gateway.sh"},
186 | VolumeMounts: g.volumeMounts(volumes),
187 | Env: []v1.EnvVar{
188 | {
189 | Name: "EG_DEFAULT_KERNEL_NAME",
190 | Value: g.defaultKernel(),
191 | },
192 | {
193 | Name: "EG_KERNEL_CLUSTER_ROLE",
194 | Value: g.defaultClusterRole(),
195 | },
196 | {
197 | Name: "EG_KERNEL_WHITELIST",
198 | Value: g.kernels(),
199 | },
200 | {
201 | Name: "EG_PORT",
202 | Value: strconv.Itoa(defaultPort),
203 | },
204 | // --EnterpriseGatewayApp.port_range=
205 | // Specifies the lower and upper port numbers from which ports are created. The
206 | // bounded values are separated by '..' (e.g., 33245..34245 specifies a range
207 | // of 1000 ports to be randomly selected). A range of zero (e.g., 33245..33245
208 | // or 0..0) disables port-range enforcement. (EG_PORT_RANGE env var)
209 | {
210 | Name: "EG_PORT_RANGE",
211 | Value: "0..0",
212 | },
213 | {
214 | Name: "EG_NAMESPACE",
215 | Value: g.gateway.Namespace,
216 | },
217 | {
218 | Name: "EG_NAME",
219 | Value: g.gateway.Name,
220 | },
221 | {
222 | // TODO(gaocegege): Make it configurable.
223 | Name: "EG_SHARED_NAMESPACE",
224 | Value: "true",
225 | },
226 | {
227 | // TODO(gaocegege): Make it configurable.
228 | Name: "EG_MIRROR_WORKING_DIRS",
229 | Value: "false",
230 | },
231 | {
232 | Name: "EG_CULL_IDLE_TIMEOUT",
233 | Value: "3600",
234 | },
235 | {
236 | Name: "EG_KERNEL_LAUNCH_TIMEOUT",
237 | Value: "60",
238 | },
239 | {
240 | Name: "EG_KERNEL_IMAGE",
241 | Value: defaultKernelImage,
242 | },
243 | },
244 | },
245 | },
246 | },
247 | },
248 | },
249 | }
250 |
251 | if g.gateway.Spec.Image != "" {
252 | d.Spec.Template.Spec.Containers[0].Image = g.gateway.Spec.Image
253 | }
254 |
255 | if g.gateway.Spec.LogLevel != nil {
256 | env := v1.EnvVar{
257 | Name: "EG_LOG_LEVEL",
258 | Value: string(*g.gateway.Spec.LogLevel),
259 | }
260 | d.Spec.Template.Spec.Containers[0].Env = append(
261 | d.Spec.Template.Spec.Containers[0].Env, env)
262 | }
263 |
264 | if g.gateway.Spec.CullIdleTimeout != nil {
265 | env := v1.EnvVar{
266 | Name: "EG_CULL_IDLE_TIMEOUT",
267 | Value: strconv.Itoa(int(*g.gateway.Spec.CullIdleTimeout)),
268 | }
269 | d.Spec.Template.Spec.Containers[0].Env = append(
270 | d.Spec.Template.Spec.Containers[0].Env, env)
271 | }
272 | if g.gateway.Spec.CullInterval != nil {
273 | env := v1.EnvVar{
274 | Name: "EG_CULL_INTERVAL",
275 | Value: strconv.Itoa(int(*g.gateway.Spec.CullInterval)),
276 | }
277 | d.Spec.Template.Spec.Containers[0].Env = append(
278 | d.Spec.Template.Spec.Containers[0].Env, env)
279 | }
280 | if g.gateway.Spec.Resources != nil {
281 | d.Spec.Template.Spec.Containers[0].Resources = *g.gateway.Spec.Resources
282 | }
283 |
284 | return d, nil
285 | }
286 |
287 | func (g generator) volumeMounts(
288 | volumes []v1.Volume) []v1.VolumeMount {
289 | volumeMounts := []v1.VolumeMount{}
290 | for _, v := range volumes {
291 | volumeMounts = append(volumeMounts, v1.VolumeMount{
292 | Name: v.Name,
293 | ReadOnly: true,
294 | MountPath: fmt.Sprintf("%s/%s", defaultKernelPath, v.Name),
295 | })
296 | }
297 | return volumeMounts
298 | }
299 |
300 | func (g generator) volumes() ([]v1.Volume, error) {
301 | volumes := []v1.Volume{}
302 | for _, k := range g.gateway.Spec.Kernels {
303 | ks := &v1alpha1.JupyterKernelSpec{}
304 | if err := g.cli.Get(context.TODO(), types.NamespacedName{
305 | Namespace: g.gateway.Namespace,
306 | Name: k,
307 | }, ks); err != nil {
308 | return nil, err
309 | }
310 |
311 | volumes = append(volumes, v1.Volume{
312 | Name: k,
313 | VolumeSource: v1.VolumeSource{
314 | ConfigMap: &v1.ConfigMapVolumeSource{
315 | LocalObjectReference: v1.LocalObjectReference{Name: k},
316 | },
317 | },
318 | })
319 | }
320 | return volumes, nil
321 | }
322 |
323 | func (g generator) defaultClusterRole() string {
324 | if g.gateway.Spec.ClusterRole != nil {
325 | return *g.gateway.Spec.ClusterRole
326 | }
327 | return defaultGatewayClusterRole
328 | }
329 |
330 | func (g generator) labels() map[string]string {
331 | return map[string]string{
332 | LabelNS: g.gateway.Namespace,
333 | LabelGateway: g.gateway.Name,
334 | }
335 | }
336 |
337 | func (g generator) kernels() string {
338 | if g.gateway.Spec.Kernels != nil {
339 | ks := []string{}
340 | for _, k := range g.gateway.Spec.Kernels {
341 | ks = append(ks, fmt.Sprintf("'%s'", k))
342 | }
343 | return strings.Join(ks, ",")
344 | }
345 | return defaultKernels
346 | }
347 |
348 | func (g generator) defaultKernel() string {
349 | if g.gateway.Spec.DefaultKernel != nil {
350 | return *g.gateway.Spec.DefaultKernel
351 | }
352 | return defaultKernel
353 | }
354 |
--------------------------------------------------------------------------------
/pkg/gateway/reconcile.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 | //
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 | //
10 | // https://opensource.org/licenses/Apache-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package gateway
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | appsv1 "k8s.io/api/apps/v1"
24 | v1 "k8s.io/api/core/v1"
25 | rbacv1 "k8s.io/api/rbac/v1"
26 | "k8s.io/apimachinery/pkg/api/equality"
27 | "k8s.io/apimachinery/pkg/api/errors"
28 | "k8s.io/apimachinery/pkg/runtime"
29 | "k8s.io/apimachinery/pkg/types"
30 | "k8s.io/client-go/tools/record"
31 | "sigs.k8s.io/controller-runtime/pkg/client"
32 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
33 |
34 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
35 | )
36 |
37 | type Reconciler struct {
38 | cli client.Client
39 | log logr.Logger
40 | recorder record.EventRecorder
41 | scheme *runtime.Scheme
42 |
43 | instance *v1alpha1.JupyterGateway
44 | gen *generator
45 | }
46 |
47 | func NewReconciler(cli client.Client, l logr.Logger,
48 | r record.EventRecorder, s *runtime.Scheme,
49 | i *v1alpha1.JupyterGateway) (*Reconciler, error) {
50 | g, err := newGenerator(cli, i)
51 | if err != nil {
52 | return nil, err
53 | }
54 | return &Reconciler{
55 | cli: cli,
56 | log: l,
57 | recorder: r,
58 | scheme: s,
59 | instance: i,
60 | gen: g,
61 | }, nil
62 | }
63 |
64 | func (r Reconciler) Reconcile() error {
65 | serviceAccountName, err := r.reconcileRBAC()
66 | if err != nil {
67 | return err
68 | }
69 | if err := r.reconcileDeployment(serviceAccountName); err != nil {
70 | return err
71 | }
72 | if err := r.reconcileService(); err != nil {
73 | return err
74 | }
75 | return nil
76 | }
77 |
78 | func (r Reconciler) reconcileRBAC() (string, error) {
79 | sa, err := r.reconcileServiceAccount()
80 | if err != nil {
81 | return "", err
82 | }
83 | if err := r.reconcileRoleBinding(sa); err != nil {
84 | return "", err
85 | }
86 | return sa.Name, nil
87 | }
88 |
89 | func (r Reconciler) reconcileRoleBinding(
90 | sa *v1.ServiceAccount) error {
91 | desired := r.gen.DesiredRoleBinding(sa)
92 |
93 | if err := controllerutil.SetControllerReference(
94 | r.instance, desired, r.scheme); err != nil {
95 | r.log.Error(err,
96 | "Set controller reference error, requeuing the request")
97 | return err
98 | }
99 |
100 | actual := &rbacv1.RoleBinding{}
101 | err := r.cli.Get(context.TODO(),
102 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
103 | if err != nil && errors.IsNotFound(err) {
104 | r.log.Info("Creating rolebinding",
105 | "namespace", desired.Namespace, "name", desired.Name)
106 |
107 | if err := r.cli.Create(context.TODO(), desired); err != nil {
108 | r.log.Error(err, "Failed to create the rolebinding",
109 | "rolebinding", desired.Name)
110 | return err
111 | }
112 | } else if err != nil {
113 | r.log.Error(err, "failed to get the expected rolebinding",
114 | "rolebinding", desired.Name)
115 | return err
116 | }
117 | return nil
118 | }
119 |
120 | func (r Reconciler) reconcileServiceAccount() (*v1.ServiceAccount, error) {
121 | desired := r.gen.DesiredServiceAccountWithoutOwner()
122 |
123 | if err := controllerutil.SetControllerReference(
124 | r.instance, desired, r.scheme); err != nil {
125 | r.log.Error(err,
126 | "Set controller reference error, requeuing the request")
127 | return nil, err
128 | }
129 |
130 | actual := &v1.ServiceAccount{}
131 | err := r.cli.Get(context.TODO(),
132 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
133 | if err != nil && errors.IsNotFound(err) {
134 | r.log.Info("Creating serviceaccount", "namespace", desired.Namespace, "name", desired.Name)
135 |
136 | if err := r.cli.Create(context.TODO(), desired); err != nil {
137 | r.log.Error(err, "Failed to create the serviceaccount",
138 | "serviceaccount", desired.Name)
139 | return nil, err
140 | }
141 | } else if err != nil {
142 | r.log.Error(err, "failed to get the expected serviceaccount",
143 | "serviceaccount", desired.Name)
144 | return nil, err
145 | }
146 | // When the sa is created, actual is nil. Thus actual cannot be used to build rolebinding.
147 | return desired, nil
148 | }
149 |
150 | func (r Reconciler) reconcileService() error {
151 | desired := r.gen.DesiredServiceWithoutOwner()
152 |
153 | if err := controllerutil.SetControllerReference(
154 | r.instance, desired, r.scheme); err != nil {
155 | r.log.Error(err,
156 | "Set controller reference error, requeuing the request")
157 | return err
158 | }
159 |
160 | actual := &v1.Service{}
161 | err := r.cli.Get(context.TODO(),
162 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
163 | if err != nil && errors.IsNotFound(err) {
164 | r.log.Info("Creating service", "namespace", desired.Namespace, "name", desired.Name)
165 |
166 | if err := r.cli.Create(context.TODO(), desired); err != nil {
167 | r.log.Error(err, "Failed to create the serivce",
168 | "service", desired.Name)
169 | return err
170 | }
171 | } else if err != nil {
172 | r.log.Error(err, "failed to get the expected service",
173 | "service", desired.Name)
174 | return err
175 | }
176 | return nil
177 | }
178 |
179 | func (r Reconciler) reconcileDeployment(sa string) error {
180 | desired, err := r.gen.DesiredDeploymentWithoutOwner(sa)
181 | if err != nil {
182 | r.recorder.Event(r.instance, v1.EventTypeWarning, "FailedToGenerate", err.Error())
183 | return err
184 | }
185 |
186 | if err := controllerutil.SetControllerReference(
187 | r.instance, desired, r.scheme); err != nil {
188 | r.log.Error(err,
189 | "Set controller reference error, requeuing the request")
190 | return err
191 | }
192 |
193 | actual := &appsv1.Deployment{}
194 | err = r.cli.Get(context.TODO(),
195 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
196 | if err != nil && errors.IsNotFound(err) {
197 | r.log.Info("Creating deployment", "namespace", desired.Namespace, "name", desired.Name)
198 |
199 | if err := r.cli.Create(context.TODO(), desired); err != nil {
200 | r.log.Error(err, "Failed to create the deployment",
201 | "deployment", desired.Name)
202 | r.recorder.Event(r.instance, v1.EventTypeWarning, "FailedToCreate", err.Error())
203 | return err
204 | }
205 | } else if err != nil {
206 | r.log.Error(err, "failed to get the expected deployment",
207 | "deployment", desired.Name)
208 | r.recorder.Event(r.instance, v1.EventTypeWarning, "FailedToGet", err.Error())
209 | return err
210 | }
211 |
212 | if !equality.Semantic.DeepEqual(r.instance.Status.DeploymentStatus, actual.Status) {
213 | r.instance.Status.DeploymentStatus = actual.Status
214 | if err := r.cli.Status().Update(context.TODO(), r.instance); err != nil {
215 | r.log.Error(err, "failed to update status",
216 | "namespace", r.instance.Namespace,
217 | "jupytergateway", r.instance.Name)
218 | r.recorder.Event(r.instance, v1.EventTypeWarning, "FailedToUpdateStatus", err.Error())
219 | return err
220 | }
221 | }
222 | return nil
223 | }
224 |
--------------------------------------------------------------------------------
/pkg/kernel/generate.go:
--------------------------------------------------------------------------------
1 | package kernel
2 |
3 | import (
4 | "fmt"
5 |
6 | appsv1 "k8s.io/api/apps/v1"
7 | v1 "k8s.io/api/core/v1"
8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9 |
10 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
11 | )
12 |
13 | const (
14 | labelNS = "namespace"
15 | labelKernel = "kernel"
16 | envKernelID = "KERNEL_ID"
17 | labelKernelID = "kernel_id"
18 | )
19 |
20 | // generator defines the generator which is used to generate
21 | // desired specs.
22 | type generator struct {
23 | k *v1alpha1.JupyterKernel
24 | }
25 |
26 | // newGenerator creates a new Generator.
27 | func newGenerator(k *v1alpha1.JupyterKernel) (
28 | *generator, error) {
29 | if k == nil {
30 | return nil, fmt.Errorf("Got nil when initializing Generator")
31 | }
32 | g := &generator{
33 | k: k,
34 | }
35 |
36 | return g, nil
37 | }
38 |
39 | func (g generator) DesiredDeployment() (*appsv1.Deployment, error) {
40 | labels := g.labels()
41 |
42 | d := &appsv1.Deployment{
43 | ObjectMeta: metav1.ObjectMeta{
44 | Name: g.k.Name,
45 | Namespace: g.k.Namespace,
46 | Labels: labels,
47 | },
48 | Spec: appsv1.DeploymentSpec{
49 | Template: g.k.Spec.Template,
50 | Selector: &metav1.LabelSelector{
51 | MatchLabels: labels,
52 | },
53 | },
54 | }
55 |
56 | if d.Spec.Template.Labels == nil {
57 | d.Spec.Template.Labels = make(map[string]string)
58 | }
59 | // Set the labels to the pod template.
60 | for k, v := range labels {
61 | d.Spec.Template.Labels[k] = v
62 | }
63 |
64 | // Update the metadata.
65 | g.hackLabelID(&d.Spec.Template)
66 |
67 | return d, nil
68 | }
69 |
70 | func (g generator) labels() map[string]string {
71 | return map[string]string{
72 | labelNS: g.k.Namespace,
73 | labelKernel: g.k.Name,
74 | }
75 | }
76 |
77 | // hackLabelID copies the ID from environment variables to
78 | // metadata.
79 | // TODO(gaocegege): Use newer version of controller-tools to avoid it.
80 | // https://github.com/kubernetes-sigs/controller-tools/issues/448
81 | func (g generator) hackLabelID(pod *v1.PodTemplateSpec) {
82 | if pod.Spec.Containers == nil || len(pod.Spec.Containers) == 0 {
83 | return
84 | }
85 | for _, env := range pod.Spec.Containers[0].Env {
86 | if env.Name == envKernelID {
87 | if pod.Labels == nil {
88 | pod.Labels = make(map[string]string)
89 | }
90 | pod.Labels[labelKernelID] = env.Value
91 | return
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/kernel/reconcile.go:
--------------------------------------------------------------------------------
1 | package kernel
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/go-logr/logr"
7 | appsv1 "k8s.io/api/apps/v1"
8 | "k8s.io/apimachinery/pkg/api/errors"
9 | "k8s.io/apimachinery/pkg/runtime"
10 | "k8s.io/apimachinery/pkg/types"
11 | "k8s.io/client-go/tools/record"
12 | "sigs.k8s.io/controller-runtime/pkg/client"
13 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
14 |
15 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
16 | )
17 |
18 | type Reconciler struct {
19 | cli client.Client
20 | log logr.Logger
21 | recorder record.EventRecorder
22 | scheme *runtime.Scheme
23 |
24 | instance *v1alpha1.JupyterKernel
25 | gen *generator
26 | }
27 |
28 | func NewReconciler(cli client.Client, l logr.Logger,
29 | r record.EventRecorder, s *runtime.Scheme,
30 | i *v1alpha1.JupyterKernel) (*Reconciler, error) {
31 | g, err := newGenerator(i)
32 | if err != nil {
33 | return nil, err
34 | }
35 | return &Reconciler{
36 | cli: cli,
37 | log: l,
38 | recorder: r,
39 | scheme: s,
40 | instance: i,
41 | gen: g,
42 | }, nil
43 | }
44 |
45 | func (r Reconciler) Reconcile() error {
46 | if err := r.reconcileDeployment(); err != nil {
47 | return err
48 | }
49 |
50 | return nil
51 | }
52 |
53 | func (r Reconciler) reconcileDeployment() error {
54 | desired, err := r.gen.DesiredDeployment()
55 | if err != nil {
56 | return err
57 | }
58 |
59 | if err := controllerutil.SetControllerReference(
60 | r.instance, desired, r.scheme); err != nil {
61 | r.log.Error(err,
62 | "Set controller reference error, requeuing the request")
63 | return err
64 | }
65 |
66 | actual := &appsv1.Deployment{}
67 | err = r.cli.Get(context.TODO(),
68 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
69 | if err != nil && errors.IsNotFound(err) {
70 | r.log.Info("Creating deployment", "namespace", desired.Namespace, "name", desired.Name)
71 |
72 | if err := r.cli.Create(context.TODO(), desired); err != nil {
73 | r.log.Error(err, "Failed to create the deployment",
74 | "deployment", desired.Name)
75 | return err
76 | }
77 | } else if err != nil {
78 | r.log.Error(err, "failed to get the expected deployment",
79 | "deployment", desired.Name)
80 | return err
81 | }
82 |
83 | // TODO(gaocegege): Update status.
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/kernelspec/generate.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 | //
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 | //
10 | // https://opensource.org/licenses/Apache-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 | // Tencent is pleased to support the open source community by making TKEStack
17 | // available.
18 | //
19 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
20 | //
21 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
22 | // this file except in compliance with the License. You may obtain a copy of the
23 | // License at
24 | //
25 | // https://opensource.org/licenses/Apache-2.0
26 | //
27 | // Unless required by applicable law or agreed to in writing, software
28 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
29 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
30 | // specific language governing permissions and limitations under the License.
31 |
32 | package kernelspec
33 |
34 | import (
35 | "encoding/json"
36 | "fmt"
37 |
38 | v1 "k8s.io/api/core/v1"
39 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40 |
41 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
42 | )
43 |
44 | const (
45 | LabelKernelSpec = "kernelspec"
46 | LabelNS = "namespace"
47 |
48 | fileName = "kernel.json"
49 | defaultClassName = "enterprise_gateway.services.processproxies.k8s.KubernetesProcessProxy"
50 |
51 | keyKernelTemplateName = "--kernel-template-name"
52 | keyKernelTemplateNamespace = "--kernel-template-namespace"
53 | )
54 |
55 | // generator defines the generator which is used to generate
56 | // desired specs.
57 | type generator struct {
58 | kernelSpec *v1alpha1.JupyterKernelSpec
59 | }
60 |
61 | // newGenerator creates a new Generator.
62 | func newGenerator(kernelSpec *v1alpha1.JupyterKernelSpec) (
63 | *generator, error) {
64 | if kernelSpec == nil {
65 | return nil, fmt.Errorf("Got nil when initializing Generator")
66 | }
67 | g := &generator{
68 | kernelSpec: kernelSpec,
69 | }
70 |
71 | return g, nil
72 | }
73 |
74 | func (g generator) DesiredConfigmapWithoutOwner() (*v1.ConfigMap, error) {
75 | labels := g.labels()
76 |
77 | jsonConfig, err := g.desiredJSON()
78 | if err != nil {
79 | return nil, err
80 | }
81 |
82 | cm := &v1.ConfigMap{
83 | ObjectMeta: metav1.ObjectMeta{
84 | Namespace: g.kernelSpec.Namespace,
85 | Name: g.kernelSpec.Name,
86 | Labels: labels,
87 | },
88 | Data: map[string]string{
89 | fileName: jsonConfig,
90 | },
91 | }
92 |
93 | return cm, nil
94 | }
95 |
96 | func (g generator) desiredJSON() (string, error) {
97 | c := &kernelConfig{
98 | Language: g.kernelSpec.Spec.Language,
99 | DisplayName: g.kernelSpec.Spec.DisplayName,
100 | Metadata: metadata{
101 | ProcessProxy: processProxy{
102 | ClassName: defaultClassName,
103 | Config: config{
104 | ImageName: g.kernelSpec.Spec.Image,
105 | },
106 | },
107 | },
108 | Argv: g.kernelSpec.Spec.Command,
109 | }
110 |
111 | // Set the class name to desired.
112 | if g.kernelSpec.Spec.ClassName != "" {
113 | c.Metadata.ProcessProxy.ClassName = g.kernelSpec.Spec.ClassName
114 | }
115 | // Set the namespace and name for the jupyter kernel spec.
116 | c.Argv = append(c.Argv,
117 | keyKernelTemplateName, g.kernelSpec.Spec.Template.Name,
118 | keyKernelTemplateNamespace, g.kernelSpec.Spec.Template.Namespace)
119 | v, err := json.Marshal(c)
120 | return string(v), err
121 | }
122 |
123 | func (g generator) labels() map[string]string {
124 | return map[string]string{
125 | LabelNS: g.kernelSpec.Namespace,
126 | LabelKernelSpec: g.kernelSpec.Name,
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/pkg/kernelspec/reconcile.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 | //
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 | //
10 | // https://opensource.org/licenses/Apache-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package kernelspec
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | v1 "k8s.io/api/core/v1"
24 | "k8s.io/apimachinery/pkg/api/errors"
25 | "k8s.io/apimachinery/pkg/runtime"
26 | "k8s.io/apimachinery/pkg/types"
27 | "k8s.io/client-go/tools/record"
28 | "sigs.k8s.io/controller-runtime/pkg/client"
29 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
30 |
31 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
32 | )
33 |
34 | type Reconciler struct {
35 | cli client.Client
36 | log logr.Logger
37 | recorder record.EventRecorder
38 | scheme *runtime.Scheme
39 |
40 | instance *v1alpha1.JupyterKernelSpec
41 | gen *generator
42 | }
43 |
44 | func NewReconciler(cli client.Client, l logr.Logger,
45 | r record.EventRecorder, s *runtime.Scheme,
46 | i *v1alpha1.JupyterKernelSpec) (*Reconciler, error) {
47 | g, err := newGenerator(i)
48 | if err != nil {
49 | return nil, err
50 | }
51 | return &Reconciler{
52 | cli: cli,
53 | log: l,
54 | recorder: r,
55 | scheme: s,
56 | instance: i,
57 | gen: g,
58 | }, nil
59 | }
60 |
61 | func (r Reconciler) Reconcile() error {
62 | if err := r.reconcileConfigmap(); err != nil {
63 | return err
64 | }
65 |
66 | return nil
67 | }
68 |
69 | func (r Reconciler) reconcileConfigmap() error {
70 | desired, err := r.gen.DesiredConfigmapWithoutOwner()
71 | if err != nil {
72 | return err
73 | }
74 |
75 | if err = controllerutil.SetControllerReference(
76 | r.instance, desired, r.scheme); err != nil {
77 | r.log.Error(err,
78 | "Set controller reference error, requeuing the request")
79 | return err
80 | }
81 |
82 | actual := &v1.ConfigMap{}
83 | err = r.cli.Get(context.TODO(),
84 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
85 | if err != nil && errors.IsNotFound(err) {
86 | r.log.Info("Creating confimap", "namespace", desired.Namespace, "name", desired.Name)
87 |
88 | if err := r.cli.Create(context.TODO(), desired); err != nil {
89 | r.log.Error(err, "Failed to create the confimap",
90 | "confimap", desired.Name)
91 | return err
92 | }
93 | } else if err != nil {
94 | r.log.Error(err, "failed to get the expected confimap",
95 | "confimap", desired.Name)
96 | return err
97 | }
98 | return nil
99 | }
100 |
--------------------------------------------------------------------------------
/pkg/kernelspec/types.go:
--------------------------------------------------------------------------------
1 | package kernelspec
2 |
3 | type kernelConfig struct {
4 | Language string `json:"language,omitempty"`
5 | DisplayName string `json:"display_name,omitempty"`
6 | Metadata metadata `json:"metadata,omitempty"`
7 | Argv []string `json:"argv,omitempty"`
8 | }
9 |
10 | type metadata struct {
11 | ProcessProxy processProxy `json:"process_proxy,omitempty"`
12 | }
13 |
14 | type processProxy struct {
15 | ClassName string `json:"class_name,omitempty"`
16 | Config config `json:"config,omitempty"`
17 | }
18 |
19 | type config struct {
20 | ImageName string `json:"image_name,omitempty"`
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/notebook/generate.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 | //
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 | //
10 | // https://opensource.org/licenses/Apache-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package notebook
18 |
19 | import (
20 | "fmt"
21 |
22 | appsv1 "k8s.io/api/apps/v1"
23 | v1 "k8s.io/api/core/v1"
24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25 | "k8s.io/apimachinery/pkg/util/intstr"
26 |
27 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
28 | )
29 |
30 | const (
31 | defaultImage = "jupyter/base-notebook:python-3.9.7"
32 | defaultContainerName = "notebook"
33 | defaultPortName = "notebook"
34 | defaultPort = 8888
35 |
36 | LabelNotebook = "notebook"
37 | LabelNS = "namespace"
38 |
39 | argumentGatewayURL = "--gateway-url"
40 | argumentNotebookToken = "--NotebookApp.token"
41 | argumentNotebookPassword = "--NotebookApp.password"
42 | )
43 |
44 | type generator struct {
45 | nb *v1alpha1.JupyterNotebook
46 | }
47 |
48 | // newGenerator creates a new Generator.
49 | func newGenerator(nb *v1alpha1.JupyterNotebook) (
50 | *generator, error) {
51 | if nb == nil {
52 | return nil, fmt.Errorf("the notebook is null")
53 | }
54 | g := &generator{
55 | nb: nb,
56 | }
57 |
58 | return g, nil
59 | }
60 |
61 | func (g generator) DesiredDeploymentWithoutOwner() (*appsv1.Deployment, error) {
62 | if g.nb.Spec.Template == nil && g.nb.Spec.Gateway == nil {
63 | return nil, fmt.Errorf("no gateway and template applied")
64 | }
65 |
66 | podSpec := v1.PodSpec{}
67 | podLabels := g.labels()
68 | podAnnotations := g.annotations()
69 | labels := g.labels()
70 | annotations := g.annotations()
71 | selector := &metav1.LabelSelector{
72 | MatchLabels: labels,
73 | }
74 | terminationGracePeriodSeconds := int64(30)
75 |
76 | if g.nb.Spec.Template != nil {
77 | if g.nb.Spec.Template.Labels != nil {
78 | for k, v := range g.nb.Spec.Template.Labels {
79 | podLabels[k] = v
80 | }
81 | }
82 | if g.nb.Spec.Template.Annotations != nil {
83 | for k, v := range g.nb.Spec.Template.Annotations {
84 | podAnnotations[k] = v
85 | }
86 | }
87 | podSpec = completePodSpec(&g.nb.Spec.Template.Spec)
88 | } else {
89 | podSpec = v1.PodSpec{
90 | Containers: []v1.Container{
91 | {
92 | Name: defaultContainerName,
93 | Image: defaultImage,
94 | ImagePullPolicy: v1.PullIfNotPresent,
95 | TerminationMessagePath: v1.TerminationMessagePathDefault,
96 | TerminationMessagePolicy: v1.TerminationMessageReadFile,
97 | Args: []string{
98 | "start-notebook.sh",
99 | },
100 | Ports: []v1.ContainerPort{
101 | {
102 | Name: defaultPortName,
103 | ContainerPort: defaultPort,
104 | Protocol: v1.ProtocolTCP,
105 | },
106 | },
107 | },
108 | },
109 | RestartPolicy: v1.RestartPolicyAlways,
110 | TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
111 | DNSPolicy: v1.DNSClusterFirst,
112 | SecurityContext: &v1.PodSecurityContext{},
113 | SchedulerName: v1.DefaultSchedulerName,
114 | }
115 | }
116 |
117 | replicas := int32(1)
118 | revisionHistoryLimit := int32(10)
119 | progressDeadlineSeconds := int32(600)
120 | maxUnavailable := intstr.FromInt(25)
121 |
122 | d := &appsv1.Deployment{
123 | ObjectMeta: metav1.ObjectMeta{
124 | Namespace: g.nb.Namespace,
125 | Name: g.nb.Name,
126 | Labels: labels,
127 | Annotations: annotations,
128 | },
129 | Spec: appsv1.DeploymentSpec{
130 | Replicas: &replicas,
131 | Selector: selector,
132 | Template: v1.PodTemplateSpec{
133 | ObjectMeta: metav1.ObjectMeta{
134 | Labels: podLabels,
135 | Annotations: podAnnotations,
136 | },
137 | Spec: podSpec,
138 | },
139 | Strategy: appsv1.DeploymentStrategy{
140 | Type: appsv1.DeploymentStrategyType(appsv1.RollingUpdateDaemonSetStrategyType),
141 | RollingUpdate: &appsv1.RollingUpdateDeployment{
142 | MaxUnavailable: &maxUnavailable,
143 | MaxSurge: &maxUnavailable,
144 | },
145 | },
146 | RevisionHistoryLimit: &revisionHistoryLimit,
147 | ProgressDeadlineSeconds: &progressDeadlineSeconds,
148 | },
149 | }
150 |
151 | if g.nb.Spec.Gateway != nil {
152 | gatewayURL := fmt.Sprintf("http://%s.%s:%d",
153 | g.nb.Spec.Gateway.Name, g.nb.Spec.Gateway.Namespace, defaultPort)
154 | d.Spec.Template.Spec.Containers[0].Args = append(
155 | d.Spec.Template.Spec.Containers[0].Args, argumentGatewayURL, gatewayURL)
156 | }
157 |
158 | // Set the auth configuration to notebook instance.
159 | if g.nb.Spec.Auth != nil {
160 | auth := g.nb.Spec.Auth
161 | // Set the token and password to empty.
162 | if auth.Mode == v1alpha1.ModeJupyterAuthDisable {
163 | d.Spec.Template.Spec.Containers[0].Args = append(
164 | d.Spec.Template.Spec.Containers[0].Args,
165 | argumentNotebookToken, "",
166 | argumentNotebookPassword, "",
167 | )
168 | } else {
169 | if auth.Token != nil {
170 | d.Spec.Template.Spec.Containers[0].Args = append(
171 | d.Spec.Template.Spec.Containers[0].Args,
172 | argumentNotebookToken, *auth.Token,
173 | )
174 | }
175 | if auth.Password != nil {
176 | d.Spec.Template.Spec.Containers[0].Args = append(
177 | d.Spec.Template.Spec.Containers[0].Args,
178 | argumentNotebookPassword, *auth.Password,
179 | )
180 | }
181 | }
182 | }
183 |
184 | return d, nil
185 | }
186 |
187 | func (g generator) labels() map[string]string {
188 | return map[string]string{
189 | LabelNS: g.nb.Namespace,
190 | LabelNotebook: g.nb.Name,
191 | }
192 | }
193 |
194 | func (g generator) annotations() map[string]string {
195 | return map[string]string{}
196 | }
197 |
198 | func completePodSpec(old *v1.PodSpec) v1.PodSpec {
199 | new := old.DeepCopy()
200 | for i := range new.Containers {
201 | if new.Containers[i].TerminationMessagePath == "" {
202 | new.Containers[i].TerminationMessagePath = v1.TerminationMessagePathDefault
203 | }
204 | if new.Containers[i].TerminationMessagePolicy == v1.TerminationMessagePolicy("") {
205 | new.Containers[i].TerminationMessagePolicy = v1.TerminationMessageReadFile
206 | }
207 | if new.Containers[i].ImagePullPolicy == v1.PullPolicy("") {
208 | new.Containers[i].ImagePullPolicy = v1.PullIfNotPresent
209 | }
210 | }
211 |
212 | if new.RestartPolicy == v1.RestartPolicy("") {
213 | new.RestartPolicy = v1.RestartPolicyAlways
214 | }
215 |
216 | if new.TerminationGracePeriodSeconds == nil {
217 | d := int64(v1.DefaultTerminationGracePeriodSeconds)
218 | new.TerminationGracePeriodSeconds = &d
219 | }
220 |
221 | if new.DNSPolicy == v1.DNSPolicy("") {
222 | new.DNSPolicy = v1.DNSClusterFirst
223 | }
224 |
225 | if new.SecurityContext == nil {
226 | new.SecurityContext = &v1.PodSecurityContext{}
227 | }
228 |
229 | if new.SchedulerName == "" {
230 | new.SchedulerName = v1.DefaultSchedulerName
231 | }
232 |
233 | return *new
234 | }
235 |
--------------------------------------------------------------------------------
/pkg/notebook/generate_test.go:
--------------------------------------------------------------------------------
1 | package notebook
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "reflect"
7 | "testing"
8 |
9 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
10 | v1 "k8s.io/api/core/v1"
11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12 | )
13 |
14 | const (
15 | JupyterNotebookName = "jupyternotebook-sample"
16 | JupyterNotebookNamespace = "default"
17 | DefaultContainerName = "notebook"
18 | DefaultImage = "busysandbox"
19 | DefaultImageWithGateway = "jupyter/base-notebook:python-3.9.7"
20 | GatewayName = "gateway"
21 | GatewayNamespace = "default"
22 | )
23 |
24 | var (
25 | podSpec = v1.PodSpec{
26 | Containers: []v1.Container{
27 | {
28 | Name: DefaultContainerName,
29 | Image: DefaultImage,
30 | },
31 | },
32 | }
33 |
34 | emptyNotebook = &v1alpha1.JupyterNotebook{
35 | ObjectMeta: metav1.ObjectMeta{
36 | Name: JupyterNotebookName,
37 | Namespace: JupyterNotebookNamespace,
38 | },
39 | }
40 |
41 | notebookWithTemplate = &v1alpha1.JupyterNotebook{
42 | ObjectMeta: metav1.ObjectMeta{
43 | Name: JupyterNotebookName,
44 | Namespace: JupyterNotebookNamespace,
45 | },
46 | Spec: v1alpha1.JupyterNotebookSpec{
47 | Template: &v1.PodTemplateSpec{
48 | ObjectMeta: metav1.ObjectMeta{
49 | Labels: map[string]string{
50 | "app": "notebook",
51 | "custom-label": "yes",
52 | },
53 | Annotations: map[string]string{"custom-annotation": "yes"},
54 | },
55 | Spec: podSpec,
56 | },
57 | },
58 | }
59 |
60 | notebookWithGateway = &v1alpha1.JupyterNotebook{
61 | ObjectMeta: metav1.ObjectMeta{
62 | Name: JupyterNotebookName,
63 | Namespace: JupyterNotebookNamespace,
64 | },
65 | Spec: v1alpha1.JupyterNotebookSpec{
66 | Gateway: &v1.ObjectReference{
67 | Kind: "JupyterGateway",
68 | Namespace: GatewayNamespace,
69 | Name: GatewayName,
70 | },
71 | },
72 | }
73 |
74 | testPwd = "test"
75 | notebookWithAuthPassword = &v1alpha1.JupyterNotebook{
76 | ObjectMeta: metav1.ObjectMeta{
77 | Name: JupyterNotebookName,
78 | Namespace: JupyterNotebookNamespace,
79 | },
80 | Spec: v1alpha1.JupyterNotebookSpec{
81 | Auth: &v1alpha1.JupyterAuth{
82 | Password: &testPwd,
83 | },
84 | Template: &v1.PodTemplateSpec{
85 | ObjectMeta: metav1.ObjectMeta{
86 | Labels: map[string]string{"app": "notebook"},
87 | },
88 | Spec: podSpec,
89 | },
90 | },
91 | }
92 |
93 | completeNotebook = &v1alpha1.JupyterNotebook{
94 | ObjectMeta: metav1.ObjectMeta{
95 | Name: JupyterNotebookName,
96 | Namespace: JupyterNotebookNamespace,
97 | },
98 | Spec: v1alpha1.JupyterNotebookSpec{
99 | Gateway: &v1.ObjectReference{
100 | Kind: "JupyterGateway",
101 | Namespace: GatewayNamespace,
102 | Name: GatewayName,
103 | },
104 | Template: &v1.PodTemplateSpec{
105 | Spec: podSpec,
106 | },
107 | },
108 | }
109 | )
110 |
111 | func TestGenerate(t *testing.T) {
112 | type test struct {
113 | input *v1alpha1.JupyterNotebook
114 | expectedErr error
115 | expectedGen *generator
116 | }
117 |
118 | tests := []test{
119 | {input: nil, expectedErr: errors.New("the notebook is null"), expectedGen: nil},
120 | {input: notebookWithTemplate, expectedErr: nil, expectedGen: &generator{nb: notebookWithTemplate}},
121 | {input: notebookWithGateway, expectedErr: nil, expectedGen: &generator{nb: notebookWithGateway}},
122 | {input: completeNotebook, expectedErr: nil, expectedGen: &generator{nb: completeNotebook}},
123 | {input: emptyNotebook, expectedErr: nil, expectedGen: &generator{nb: emptyNotebook}},
124 | }
125 |
126 | for _, tc := range tests {
127 | gen, err := newGenerator(tc.input)
128 | if !reflect.DeepEqual(tc.expectedErr, err) {
129 | t.Errorf("expected: %v, got: %v", tc.expectedErr, err)
130 | }
131 | if err == nil && !reflect.DeepEqual(tc.expectedGen, gen) {
132 | t.Errorf("expected: %v, got: %v", tc.expectedGen, gen)
133 | }
134 | }
135 | }
136 |
137 | func TestDesiredDeploymentWithoutOwner(t *testing.T) {
138 | type test struct {
139 | gen *generator
140 | expectedErr error
141 | expectedImage string
142 | expectedLabels map[string]string
143 | expectedAnnotations map[string]string
144 | expectedArgs []string
145 | }
146 |
147 | tests := []test{
148 | {
149 | gen: &generator{nb: notebookWithTemplate},
150 | expectedErr: nil,
151 | expectedImage: DefaultImage,
152 | expectedLabels: map[string]string{
153 | "app": "notebook",
154 | "custom-label": "yes",
155 | },
156 | expectedAnnotations: map[string]string{
157 | "custom-annotation": "yes",
158 | },
159 | expectedArgs: nil,
160 | },
161 | {
162 | gen: &generator{nb: notebookWithGateway},
163 | expectedErr: nil,
164 | expectedImage: DefaultImageWithGateway,
165 | expectedArgs: []string{
166 | "start-notebook.sh",
167 | argumentGatewayURL,
168 | fmt.Sprintf("http://%s.%s:%d", GatewayName, GatewayNamespace, 8888),
169 | },
170 | },
171 | {
172 | gen: &generator{nb: completeNotebook},
173 | expectedErr: nil, expectedImage: DefaultImage,
174 | expectedArgs: []string{
175 | argumentGatewayURL,
176 | fmt.Sprintf("http://%s.%s:%d", GatewayName, GatewayNamespace, 8888),
177 | },
178 | },
179 | {
180 | gen: &generator{nb: emptyNotebook},
181 | expectedErr: errors.New("no gateway and template applied"),
182 | },
183 | {
184 | gen: &generator{nb: notebookWithAuthPassword},
185 | expectedErr: nil, expectedImage: DefaultImage,
186 | expectedArgs: []string{argumentNotebookPassword, *notebookWithAuthPassword.Spec.Auth.Password},
187 | },
188 | }
189 |
190 | for i, tc := range tests {
191 | d, err := tc.gen.DesiredDeploymentWithoutOwner()
192 | if !reflect.DeepEqual(tc.expectedErr, err) {
193 | t.Errorf("expected: %v, got: %v", tc.expectedErr, err)
194 | }
195 | if err == nil && !reflect.DeepEqual(tc.expectedImage, d.Spec.Template.Spec.Containers[0].Image) {
196 | t.Errorf("expected: %v, got: %v", tc.expectedImage, d.Spec.Template.Spec.Containers[0].Image)
197 | }
198 | if err == nil && !reflect.DeepEqual(tc.expectedArgs, d.Spec.Template.Spec.Containers[0].Args) {
199 | t.Errorf("i= %d expected: %v, got: %v", i, tc.expectedArgs, d.Spec.Template.Spec.Containers[0].Args)
200 | }
201 | for k, v := range tc.expectedLabels {
202 | if v != d.Spec.Template.Labels[k] {
203 | t.Errorf("expected: %v, got: %v", v, d.Labels[k])
204 | }
205 | }
206 | for k, v := range tc.expectedAnnotations {
207 | if v != d.Spec.Template.Annotations[k] {
208 | t.Errorf("expected: %v, got: %v", v, d.Annotations[k])
209 | }
210 | }
211 | }
212 | }
213 |
214 | func TestLable(t *testing.T) {
215 | type test struct {
216 | gen *generator
217 | expected string
218 | }
219 |
220 | tests := []test{
221 | {gen: &generator{nb: notebookWithTemplate}, expected: JupyterNotebookName},
222 | }
223 |
224 | for _, tc := range tests {
225 | mp := tc.gen.labels()
226 | if !reflect.DeepEqual(tc.expected, mp["notebook"]) {
227 | t.Errorf("expected: %v, got: %v", tc.expected, mp["notebook"])
228 | }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/pkg/notebook/reconcile.go:
--------------------------------------------------------------------------------
1 | // Tencent is pleased to support the open source community by making TKEStack
2 | // available.
3 | //
4 | // Copyright (C) 2012-2020 Tencent. All Rights Reserved.
5 | //
6 | // Licensed under the Apache License, Version 2.0 (the "License"); you may not use
7 | // this file except in compliance with the License. You may obtain a copy of the
8 | // License at
9 | //
10 | // https://opensource.org/licenses/Apache-2.0
11 | //
12 | // Unless required by applicable law or agreed to in writing, software
13 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | // WARRANTIES OF ANY KIND, either express or implied. See the License for the
15 | // specific language governing permissions and limitations under the License.
16 |
17 | package notebook
18 |
19 | import (
20 | "context"
21 |
22 | "github.com/go-logr/logr"
23 | appsv1 "k8s.io/api/apps/v1"
24 | "k8s.io/apimachinery/pkg/api/equality"
25 | "k8s.io/apimachinery/pkg/api/errors"
26 | "k8s.io/apimachinery/pkg/runtime"
27 | "k8s.io/apimachinery/pkg/types"
28 | "k8s.io/client-go/tools/record"
29 | "sigs.k8s.io/controller-runtime/pkg/client"
30 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
31 |
32 | "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
33 | )
34 |
35 | type Reconciler struct {
36 | cli client.Client
37 | log logr.Logger
38 | recorder record.EventRecorder
39 | scheme *runtime.Scheme
40 |
41 | instance *v1alpha1.JupyterNotebook
42 | gen *generator
43 | }
44 |
45 | func NewReconciler(cli client.Client,
46 | l logr.Logger,
47 | r record.EventRecorder, s *runtime.Scheme,
48 | i *v1alpha1.JupyterNotebook) (*Reconciler, error) {
49 | g, err := newGenerator(i)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return &Reconciler{
54 | cli: cli,
55 | log: l,
56 | recorder: r,
57 | scheme: s,
58 | instance: i,
59 | gen: g,
60 | }, nil
61 | }
62 |
63 | func (r Reconciler) Reconcile() error {
64 | if err := r.reconcileDeployment(); err != nil {
65 | return err
66 | }
67 | return nil
68 | }
69 |
70 | func (r Reconciler) reconcileDeployment() error {
71 | desired, err := r.gen.DesiredDeploymentWithoutOwner()
72 | if err != nil {
73 | return err
74 | }
75 |
76 | if err := controllerutil.SetControllerReference(
77 | r.instance, desired, r.scheme); err != nil {
78 | r.log.Error(err,
79 | "Set controller reference error, requeuing the request")
80 | return err
81 | }
82 |
83 | actual := &appsv1.Deployment{}
84 | err = r.cli.Get(context.TODO(),
85 | types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, actual)
86 |
87 | // Create deployment if not found
88 | if err != nil && errors.IsNotFound(err) {
89 | r.log.Info("Creating deployment", "namespace", desired.Namespace, "name", desired.Name)
90 | if err := r.cli.Create(context.TODO(), desired); err != nil {
91 | r.log.Error(err, "Failed to create the deployment",
92 | "deployment", desired.Name)
93 | return err
94 | }
95 | } else if err != nil {
96 | r.log.Error(err, "failed to get the expected deployment",
97 | "deployment", desired.Name)
98 | return err
99 | }
100 |
101 | // Update deployment from desired to actural
102 | if !equality.Semantic.DeepEqual(desired.Spec, actual.Spec) {
103 | if err := r.cli.Update(context.TODO(), desired); err != nil {
104 | r.log.Error(err, "Failed to update deployment")
105 | return err
106 | } else {
107 | r.log.Info("deployment updated")
108 | }
109 | }
110 |
111 | return nil
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/notebook/reconcile_test.go:
--------------------------------------------------------------------------------
1 | package notebook
2 |
3 | import (
4 | "context"
5 | "path/filepath"
6 | "testing"
7 | "time"
8 |
9 | . "github.com/onsi/ginkgo"
10 | . "github.com/onsi/gomega"
11 | "k8s.io/apimachinery/pkg/runtime"
12 | "k8s.io/apimachinery/pkg/types"
13 | "k8s.io/client-go/kubernetes/scheme"
14 | "k8s.io/client-go/rest"
15 | "k8s.io/client-go/tools/record"
16 | ctrl "sigs.k8s.io/controller-runtime"
17 | "sigs.k8s.io/controller-runtime/pkg/client"
18 | "sigs.k8s.io/controller-runtime/pkg/envtest"
19 | "sigs.k8s.io/controller-runtime/pkg/envtest/printer"
20 | logf "sigs.k8s.io/controller-runtime/pkg/log"
21 | "sigs.k8s.io/controller-runtime/pkg/log/zap"
22 | "sigs.k8s.io/controller-runtime/pkg/manager"
23 |
24 | kubeflowtkestackiov1alpha1 "github.com/tkestack/elastic-jupyter-operator/api/v1alpha1"
25 | // +kubebuilder:scaffold:imports
26 | )
27 |
28 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to
29 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
30 | var (
31 | cfg *rest.Config
32 | k8sClient client.Client
33 | k8sManager manager.Manager
34 | testEnv *envtest.Environment
35 | s *runtime.Scheme
36 |
37 | log = ctrl.Log.WithName("controllers").WithName("JupyterNotebook")
38 | rec = record.NewFakeRecorder(1024 * 1024)
39 | )
40 |
41 | const (
42 | timeout = time.Second * 10
43 | duration = time.Second * 10
44 | interval = time.Millisecond * 250
45 | )
46 |
47 | func TestAPIs(t *testing.T) {
48 | RegisterFailHandler(Fail)
49 |
50 | RunSpecsWithDefaultAndCustomReporters(t,
51 | "Noteboook Suite",
52 | []Reporter{printer.NewlineReporter{}})
53 | }
54 |
55 | var _ = BeforeSuite(func(done Done) {
56 | logf.SetLogger(zap.New(zap.UseDevMode(true)))
57 |
58 | By("bootstrapping test environment")
59 | testEnv = &envtest.Environment{
60 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
61 | }
62 |
63 | var err error
64 | cfg, err = testEnv.Start()
65 | Expect(err).ToNot(HaveOccurred())
66 | Expect(cfg).ToNot(BeNil())
67 |
68 | err = kubeflowtkestackiov1alpha1.AddToScheme(scheme.Scheme)
69 | Expect(err).NotTo(HaveOccurred())
70 |
71 | // +kubebuilder:scaffold:scheme
72 |
73 | k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{
74 | Scheme: scheme.Scheme,
75 | })
76 | Expect(err).ToNot(HaveOccurred())
77 |
78 | go func() {
79 | err = k8sManager.Start(ctrl.SetupSignalHandler())
80 | Expect(err).ToNot(HaveOccurred())
81 | }()
82 |
83 | k8sClient = k8sManager.GetClient()
84 | Expect(k8sClient).ToNot(BeNil())
85 | s = k8sManager.GetScheme()
86 | Expect(s).ToNot(BeNil())
87 |
88 | close(done)
89 | }, 60)
90 |
91 | var _ = Describe("JupyterNotebook controller", func() {
92 |
93 | Context("Nil JupyterNotebook", func() {
94 | It("Should fail to NewReconciler", func() {
95 | _, err := NewReconciler(k8sClient, log, rec, s, nil)
96 | Expect(err).To(HaveOccurred())
97 | })
98 | })
99 |
100 | Context("JupyterNotebook without template and notebook", func() {
101 | It("Should fail to reconcileDeployment", func() {
102 | var r *Reconciler
103 | var err error
104 | r, err = NewReconciler(k8sClient, log, rec, s, emptyNotebook)
105 | Expect(err).ToNot(HaveOccurred())
106 | Expect(r).ToNot(BeNil())
107 | err = r.reconcileDeployment()
108 | Expect(err).To(HaveOccurred())
109 | })
110 | })
111 |
112 | Context("JupyterNotebook only have template", func() {
113 | It("Should reconcile deployment as desired", func() {
114 | var r *Reconciler
115 | var err error
116 | r, err = NewReconciler(k8sClient, log, rec, s, notebookWithTemplate)
117 | Expect(err).ToNot(HaveOccurred())
118 | Expect(r).ToNot(BeNil())
119 |
120 | err = r.cli.Create(context.TODO(), notebookWithTemplate)
121 | Expect(err).ToNot(HaveOccurred())
122 |
123 | err = r.reconcileDeployment()
124 | Expect(err).ToNot(HaveOccurred())
125 |
126 | By("Expecting template name")
127 | Eventually(func() string {
128 | actual := &kubeflowtkestackiov1alpha1.JupyterNotebook{}
129 | if err := k8sClient.Get(context.Background(),
130 | types.NamespacedName{Name: notebookWithTemplate.GetName(), Namespace: notebookWithTemplate.GetNamespace()}, actual); err == nil {
131 | return actual.Spec.Template.Spec.Containers[0].Name
132 | }
133 | return ""
134 | }, timeout, interval).Should(Equal(notebookWithTemplate.Spec.Template.Spec.Containers[0].Name))
135 |
136 | err = r.Reconcile()
137 | Expect(err).ToNot(HaveOccurred())
138 | })
139 | })
140 | })
141 |
142 | var _ = AfterSuite(func() {
143 | By("tearing down the test environment")
144 | err := testEnv.Stop()
145 | Expect(err).ToNot(HaveOccurred())
146 | })
147 |
--------------------------------------------------------------------------------