├── mm ├── images ├── arch-01.png ├── arch-02.png ├── lab-01.png ├── lab-02.png ├── user-guide.png ├── vscode-01.png ├── user-guide-1.png ├── user-guide-2.png ├── user-guide-3.png ├── user-guide-4.png ├── user-guide-5.png ├── user-guide-6.png ├── user-guide-7.png ├── user-guide-8.png ├── user-guide-9.png ├── installation-1.png ├── installation-10.png ├── installation-11.png ├── installation-12.png ├── installation-13.png ├── installation-14.png ├── installation-15.png ├── installation-16.png ├── installation-17.png ├── installation-18.png ├── installation-19.png ├── installation-2.png ├── installation-20.png ├── installation-21.png ├── installation-3.png ├── installation-4.png ├── installation-6.png ├── installation-7.png ├── installation-8.png ├── installation-9.png ├── user-guide-10.png ├── user-guide-11.png ├── user-guide-12.png ├── user-guide-13.png ├── user-guide-14.png ├── user-guide-15.png ├── user-guide-16.png ├── user-guide-18.png ├── user-guide-19.png ├── user-guide-20.png ├── user-guide-21.png ├── user-guide-22.png ├── user-guide-23.png ├── user-guide-24.png ├── user-guide-25.png ├── user-guide-26.png ├── user-guide-27.png ├── user-guide-28.png └── installation-22.jpeg ├── .gitignore ├── hack ├── builder │ └── Dockerfile └── boilerplate.go.txt ├── deploy ├── device-jupyter-admin.yaml ├── device-vscode-admin.yaml ├── user-admin.yaml ├── time-slicing-gpu.yaml ├── mysql-instance.yaml ├── reverse-proxy.yaml └── mysql-operator.yaml ├── pkg ├── apis │ └── open-hydra-api │ │ ├── course │ │ └── core │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── dataset │ │ └── core │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── device │ │ └── core │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── setting │ │ └── core │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── summary │ │ └── core │ │ │ └── v1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ │ └── user │ │ └── core │ │ └── v1 │ │ ├── doc.go │ │ ├── types.go │ │ ├── register.go │ │ └── zz_generated.deepcopy.go ├── util │ ├── util_suite_test.go │ ├── k8s.go │ ├── zip.go │ ├── utils.go │ └── util_test.go ├── open-hydra │ ├── k8s │ │ ├── k8s_suite_test.go │ │ ├── abstraction.go │ │ └── faker.go │ ├── open-hydra_suite_test.go │ ├── api-resource.go │ ├── apis │ │ └── api.go │ ├── setting.go │ ├── summary-handler.go │ ├── handler.go │ └── comment.go ├── database │ ├── auth-plugin │ │ ├── keystone │ │ │ └── train │ │ │ │ ├── train_suite_test.go │ │ │ │ └── schema.go │ │ └── mysql-default.go │ ├── abstraction.go │ ├── etcd.go │ └── faker.go └── apiserver │ ├── apiserver.go │ └── service-register.go ├── Dockerfile ├── cmd └── open-hydra-server │ ├── app │ ├── config │ │ ├── config_suite_test.go │ │ └── config_test.go │ ├── option │ │ └── options.go │ └── open-hydra-server.go │ └── main.go ├── asserts ├── mysql-aio-cluster.yaml ├── config.yaml ├── config-keystone.yaml └── mysql-deploy-operator.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── go.yml │ ├── build-image.yml │ └── e2e-test-on-pr.yml ├── docs ├── trouble-shooting.md ├── trouble-shooting-en.md ├── keystone-interfacing.md ├── keystone-interfacing-en.md ├── user-guide.md └── dev-guide.md ├── code-of-conduct.md ├── rp.yaml ├── go.mod └── Makefile /mm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/arch-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/arch-01.png -------------------------------------------------------------------------------- /images/arch-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/arch-02.png -------------------------------------------------------------------------------- /images/lab-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/lab-01.png -------------------------------------------------------------------------------- /images/lab-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/lab-02.png -------------------------------------------------------------------------------- /images/user-guide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide.png -------------------------------------------------------------------------------- /images/vscode-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/vscode-01.png -------------------------------------------------------------------------------- /images/user-guide-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-1.png -------------------------------------------------------------------------------- /images/user-guide-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-2.png -------------------------------------------------------------------------------- /images/user-guide-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-3.png -------------------------------------------------------------------------------- /images/user-guide-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-4.png -------------------------------------------------------------------------------- /images/user-guide-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-5.png -------------------------------------------------------------------------------- /images/user-guide-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-6.png -------------------------------------------------------------------------------- /images/user-guide-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-7.png -------------------------------------------------------------------------------- /images/user-guide-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-8.png -------------------------------------------------------------------------------- /images/user-guide-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-9.png -------------------------------------------------------------------------------- /images/installation-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-1.png -------------------------------------------------------------------------------- /images/installation-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-10.png -------------------------------------------------------------------------------- /images/installation-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-11.png -------------------------------------------------------------------------------- /images/installation-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-12.png -------------------------------------------------------------------------------- /images/installation-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-13.png -------------------------------------------------------------------------------- /images/installation-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-14.png -------------------------------------------------------------------------------- /images/installation-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-15.png -------------------------------------------------------------------------------- /images/installation-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-16.png -------------------------------------------------------------------------------- /images/installation-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-17.png -------------------------------------------------------------------------------- /images/installation-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-18.png -------------------------------------------------------------------------------- /images/installation-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-19.png -------------------------------------------------------------------------------- /images/installation-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-2.png -------------------------------------------------------------------------------- /images/installation-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-20.png -------------------------------------------------------------------------------- /images/installation-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-21.png -------------------------------------------------------------------------------- /images/installation-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-3.png -------------------------------------------------------------------------------- /images/installation-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-4.png -------------------------------------------------------------------------------- /images/installation-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-6.png -------------------------------------------------------------------------------- /images/installation-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-7.png -------------------------------------------------------------------------------- /images/installation-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-8.png -------------------------------------------------------------------------------- /images/installation-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-9.png -------------------------------------------------------------------------------- /images/user-guide-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-10.png -------------------------------------------------------------------------------- /images/user-guide-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-11.png -------------------------------------------------------------------------------- /images/user-guide-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-12.png -------------------------------------------------------------------------------- /images/user-guide-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-13.png -------------------------------------------------------------------------------- /images/user-guide-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-14.png -------------------------------------------------------------------------------- /images/user-guide-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-15.png -------------------------------------------------------------------------------- /images/user-guide-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-16.png -------------------------------------------------------------------------------- /images/user-guide-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-18.png -------------------------------------------------------------------------------- /images/user-guide-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-19.png -------------------------------------------------------------------------------- /images/user-guide-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-20.png -------------------------------------------------------------------------------- /images/user-guide-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-21.png -------------------------------------------------------------------------------- /images/user-guide-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-22.png -------------------------------------------------------------------------------- /images/user-guide-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-23.png -------------------------------------------------------------------------------- /images/user-guide-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-24.png -------------------------------------------------------------------------------- /images/user-guide-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-25.png -------------------------------------------------------------------------------- /images/user-guide-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-26.png -------------------------------------------------------------------------------- /images/user-guide-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-27.png -------------------------------------------------------------------------------- /images/user-guide-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/user-guide-28.png -------------------------------------------------------------------------------- /images/installation-22.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhydra/open-hydra/HEAD/images/installation-22.jpeg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmd/open-hydra-server/open-hydra-server 2 | .idea 3 | .vscode 4 | coverage.out 5 | *__debug_bin 6 | *__debug_bin* 7 | -------------------------------------------------------------------------------- /hack/builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rockylinux:8.8 2 | 3 | ARG APP 4 | COPY cmd/open-hydra-server/open-hydra-server /usr/bin/ 5 | EXPOSE 443 6 | WORKDIR /usr/bin -------------------------------------------------------------------------------- /deploy/device-jupyter-admin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: open-hydra-server.openhydra.io/v1 2 | kind: Device 3 | metadata: 4 | name: admin 5 | spec: 6 | openHydraUsername: admin 7 | sandboxName: jupyter-lab -------------------------------------------------------------------------------- /deploy/device-vscode-admin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: open-hydra-server.openhydra.io/v1 2 | kind: Device 3 | metadata: 4 | name: admin 5 | spec: 6 | sandboxName: vscode 7 | openHydraUsername: admin 8 | 9 | -------------------------------------------------------------------------------- /deploy/user-admin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: open-hydra-server.openhydra.io/v1 2 | kind: OpenHydraUser 3 | metadata: 4 | name: admin 5 | spec: 6 | chineseName: admin 7 | description: admin 8 | password: openhydra 9 | role: 1 -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/course/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/dataset/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/device/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/setting/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/summary/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/user/core/v1/doc.go: -------------------------------------------------------------------------------- 1 | // +k8s:deepcopy-gen=package 2 | // +k8s:defaulter-gen=TypeMeta 3 | 4 | // +groupName=open-hydra-server.openhydra.io 5 | // +versionName=v1 6 | // +k8s:openapi-gen=true 7 | // Package v1 is the v1 version of the API. 8 | package v1 9 | -------------------------------------------------------------------------------- /pkg/util/util_suite_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestUtil(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Util Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/open-hydra/k8s/k8s_suite_test.go: -------------------------------------------------------------------------------- 1 | package k8s_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestK8s(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "K8s Suite") 13 | } 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.8-alpine3.19 as build 2 | WORKDIR /openhydra 3 | COPY . /openhydra 4 | RUN apk add make bash which && make go-build 5 | 6 | FROM rockylinux:8.8 7 | COPY --from=build /openhydra/cmd/open-hydra-server/open-hydra-server /usr/bin/ 8 | EXPOSE 443 9 | WORKDIR /usr/bin -------------------------------------------------------------------------------- /pkg/open-hydra/open-hydra_suite_test.go: -------------------------------------------------------------------------------- 1 | package openhydra_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestOpenHydra(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "OpenHydra Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cmd/open-hydra-server/app/config/config_suite_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Config Suite") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/database/auth-plugin/keystone/train/train_suite_test.go: -------------------------------------------------------------------------------- 1 | package train_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestTrain(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Train Suite") 13 | } 14 | -------------------------------------------------------------------------------- /cmd/open-hydra-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "open-hydra/cmd/open-hydra-server/app" 7 | ) 8 | 9 | var version string 10 | 11 | func main() { 12 | // add a comment for test 13 | cmd := app.NewCommand(version) 14 | if err := cmd.Execute(); err != nil { 15 | os.Exit(1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /deploy/time-slicing-gpu.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: time-slicing-config-all 5 | namespace: gpu-operator 6 | data: 7 | any: |- 8 | version: v1 9 | flags: 10 | migStrategy: none 11 | sharing: 12 | timeSlicing: 13 | resources: 14 | - name: nvidia.com/gpu 15 | replicas: 4 -------------------------------------------------------------------------------- /asserts/mysql-aio-cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | stringData: 3 | rootHost: "%" 4 | rootPassword: "root" 5 | rootUser: "root" 6 | kind: Secret 7 | metadata: 8 | name: mypwds 9 | namespace: mysql-operator 10 | type: Opaque 11 | --- 12 | apiVersion: mysql.oracle.com/v2 13 | kind: InnoDBCluster 14 | metadata: 15 | name: mycluster 16 | namespace: mysql-operator 17 | spec: 18 | imagePullPolicy: IfNotPresent 19 | secretName: mypwds 20 | tlsUseSelfSigned: true 21 | instances: 1 22 | router: 23 | instances: 1 24 | -------------------------------------------------------------------------------- /deploy/mysql-instance.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | rootHost: JQ== 4 | rootPassword: b3Blbmh5ZHJh 5 | rootUser: cm9vdA== 6 | kind: Secret 7 | metadata: 8 | name: mypwds 9 | namespace: mysql-operator 10 | type: Opaque 11 | 12 | --- 13 | 14 | apiVersion: mysql.oracle.com/v2 15 | kind: InnoDBCluster 16 | metadata: 17 | name: mycluster 18 | namespace: mysql-operator 19 | spec: 20 | imagePullPolicy: IfNotPresent 21 | secretName: mypwds 22 | tlsUseSelfSigned: true 23 | instances: 1 24 | router: 25 | instances: 1 -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | */ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /pkg/util/k8s.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/apimachinery/pkg/conversion" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | 9 | "open-hydra/cmd/open-hydra-server/app/option" 10 | 11 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func FillKindAndApiVersion(mType *metaV1.TypeMeta, kind string) { 15 | mType.Kind = kind 16 | mType.APIVersion = fmt.Sprintf("%s/%s", option.GroupVersion.Group, option.GroupVersion.Version) 17 | } 18 | 19 | func FillObjectGVK(obj schema.ObjectKind) { 20 | obj.SetGroupVersionKind(schema.GroupVersionKind{ 21 | Group: option.GroupVersion.Group, 22 | Kind: GetObjectKind(obj), 23 | Version: option.GroupVersion.Version, 24 | }) 25 | } 26 | 27 | func GetObjectKind(obj schema.ObjectKind) string { 28 | v, err := conversion.EnforcePtr(obj) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return v.Type().Name() 33 | } 34 | -------------------------------------------------------------------------------- /asserts/config.yaml: -------------------------------------------------------------------------------- 1 | podAllocatableLimit: -1 2 | defaultCpuPerDevice: 2000 3 | defaultRamPerDevice: 8192 4 | defaultGpuPerDevice: 0 5 | datasetBasePath: /mnt/public-dataset 6 | datasetVolumeType: hostpath 7 | jupyterLabHostBaseDir: /mnt/jupyter-lab 8 | imageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/jupyter:Python-3.8.18-dual-lan" 9 | vscodeImageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/vscode:1.85.1" 10 | defaultGpuDriver: nvidia.com/gpu 11 | serverIP: "localhost" 12 | patchResourceNotRelease: true 13 | disableAuth: true 14 | mysqlConfig: 15 | address: mycluster-instances.mysql-operator.svc 16 | port: 3306 17 | username: root 18 | password: openhydra 19 | databaseName: openhydra 20 | protocol: tcp 21 | leaderElection: 22 | leaderElect: false 23 | leaseDuration: 30s 24 | renewDeadline: 15s 25 | retryPeriod: 5s 26 | resourceLock: endpointsleases 27 | resourceName: open-hydra-api-leader-lock 28 | resourceNamespace: default -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["main"] 9 | paths: 10 | - "**.go" 11 | pull_request: 12 | branches: ["main"] 13 | paths: 14 | - "**.go" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Install ginkgo 23 | run: | 24 | go install github.com/onsi/ginkgo/v2/ginkgo 25 | go get github.com/onsi/gomega/... 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v4 29 | with: 30 | go-version: "1.21.4" 31 | 32 | - name: Build 33 | run: make go-build 34 | 35 | - name: Test 36 | run: make test-all 37 | - name: Upload coverage reports to Codecov 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | -------------------------------------------------------------------------------- /pkg/util/zip.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // ZipDirectory creates a zip file from the specified directory. 12 | func ZipDir(source, target string) error { 13 | destinationFile, err := os.Create(target) 14 | if err != nil { 15 | return err 16 | } 17 | myZip := zip.NewWriter(destinationFile) 18 | err = filepath.Walk(source, func(filePath string, info os.FileInfo, err error) error { 19 | if info.IsDir() { 20 | return nil 21 | } 22 | if err != nil { 23 | return err 24 | } 25 | relPath := strings.TrimPrefix(filePath, filepath.Dir(source)) 26 | zipFile, err := myZip.Create(relPath) 27 | if err != nil { 28 | return err 29 | } 30 | fsFile, err := os.Open(filePath) 31 | if err != nil { 32 | return err 33 | } 34 | _, err = io.Copy(zipFile, fsFile) 35 | if err != nil { 36 | return err 37 | } 38 | return nil 39 | }) 40 | if err != nil { 41 | return err 42 | } 43 | err = myZip.Close() 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /asserts/config-keystone.yaml: -------------------------------------------------------------------------------- 1 | podAllocatableLimit: -1 2 | defaultCpuPerDevice: 2000 3 | defaultRamPerDevice: 8192 4 | defaultGpuPerDevice: 0 5 | datasetBasePath: /mnt/public-dataset 6 | datasetVolumeType: hostpath 7 | jupyterLabHostBaseDir: /mnt/jupyter-lab 8 | imageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/jupyter:Python-3.8.18-dual-lan" 9 | vscodeImageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/vscode:1.85.1" 10 | defaultGpuDriver: nvidia.com/gpu 11 | serverIP: "localhost" 12 | patchResourceNotRelease: true 13 | disableAuth: true 14 | mysqlConfig: 15 | address: mycluster-instances.mysql-operator.svc 16 | port: 3306 17 | username: root 18 | password: openhydra 19 | databaseName: openhydra 20 | protocol: tcp 21 | leaderElection: 22 | leaderElect: false 23 | leaseDuration: 30s 24 | renewDeadline: 15s 25 | retryPeriod: 5s 26 | resourceLock: endpointsleases 27 | resourceName: open-hydra-api-leader-lock 28 | resourceNamespace: default 29 | authDelegateConfig: 30 | keystoneConfig: 31 | endpoint: http://keystone.openhydra.svc:5000 32 | username: admin 33 | password: admin 34 | domainId: default 35 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/dataset/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:nonNamespaced 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // +kubebuilder:subresource:status 11 | // +k8s:openapi-gen=true 12 | // +resource:path=datasets,strategy=DatasetStrategy,shortname=dst 13 | // Dataset is the Schema for the Dataset API 14 | type Dataset struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata,omitempty"` 17 | Spec DatasetSpec `json:"spec,omitempty"` 18 | Status DatasetStatus `json:"status,omitempty"` 19 | } 20 | 21 | // DatasetStatus defines the observed state of Device of cluster 22 | type DatasetStatus struct { 23 | } 24 | 25 | type DatasetSpec struct { 26 | Description string `json:"description,omitempty"` 27 | LastUpdate metav1.Time `json:"lastUpdate"` 28 | } 29 | 30 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 31 | // +k8s:openapi-gen=true 32 | type DatasetList struct { 33 | metav1.TypeMeta `json:",inline"` 34 | metav1.ListMeta `json:"metadata,omitempty"` 35 | Items []Dataset `json:"items"` 36 | } 37 | -------------------------------------------------------------------------------- /docs/trouble-shooting.md: -------------------------------------------------------------------------------- 1 | # 错误解决 2 | 3 | 本文档描述了如何解决一些常见的错误的方法 4 | 5 | ## usb 安装系统后正常启动但是在 openhydra 全部完成前服务器挂了 6 | 7 | * 这种情况多半无法自动恢复,我们需要一下步骤来完全重置后重新安装 8 | 9 | ```bash 10 | # 登陆到服务器上 11 | # 切换到 root 账号 12 | # 开始重置 13 | $ systemctl stop kubelet 14 | $ systemctl disable kubelet 15 | # 停止并禁用 openhydra 安装服务,因为如果 /etc/kubernetes 不存在的话 systemd 服务会在服务器重启后自动运行 16 | # 会将事情复杂化,所以我们需要停止和禁用服务以简化事情 17 | $ systemctl stop maas 18 | $ systemctl disable maas 19 | # 如果 kubeadm reset -f 这个命令各卡住超过 2 分钟, 则直接重启服务器即可 20 | $ kubeadm reset -f 21 | # 清理 kubeclipper 数据库 22 | $ kcctl clean -Af 23 | # 检查是否有僵尸容器,如果之前重启过服务器那么大概率不会有僵尸容器,直接跳过这一步 24 | $ ctr -n k8s.io container list 25 | # 注意 如果上面的命令返回有容器则需要重启服务器 26 | $ reboot 27 | # 重启后 28 | $ kubeadm reset -f 29 | $ rm -rf /etc/kubernetes 30 | 31 | # 开始重新安装 32 | $ systemctl start maas 33 | 34 | # 查看日志 35 | $ journalctl -u maas -f 36 | ``` 37 | 38 | ## 如何更新 open-hydra server 的镜像 39 | 40 | * 登陆 open-hydra server 的服务器运行以下命令 41 | 42 | ```bash 43 | # 手动下载镜像 44 | $ ctr -n k8s.io i pull registry.cn-shanghai.aliyuncs.com/openhydra/open-hydra-server:latest 45 | # 重启 open-hydra server 46 | $ kubectl scale deployment open-hydra-server --replicas=0 -n open-hydra 47 | # 等待 3 秒后 48 | $ kubectl scale deployment open-hydra-server --replicas=1 -n open-hydra 49 | ``` 50 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # OpenHydra Code of Conduct 2 | 3 | ## Best Practices of Committing Code 4 | 5 | - As gopher, make sure you already read [the conduct of Go language](https://golang.org/conduct) and [the instruction of writing Go](https://golang.org/doc/effective_go.html). 6 | - Fork the project under your GitHub account and make the changes you want there. 7 | - Execute 'make checkfmt' for every piece of new code. 8 | - Every pulling request (PR) would be better constructed with only one commit, which could help code reviewer to go through your code efficiently, also helpful for every follower of this project to understand what happens in this PR. If you need to make any further code change to address the comments from reviewers, which means some new commits will be generated under this PR, you need to use 'git rebase' to combine those commits together. 9 | - Every PR should only solve one problem or provide one feature. Don't put several different fixes into one PR. 10 | - At lease two code reviewers should involve into code reviewing process. 11 | - Please introduce new third-party packages as little as possible to reduce the vendor dependency of this project. For example, don't import a full unit converting package but only use one function from it. For this case, you'd better write that function by yourself. -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/setting/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "open-hydra/pkg/open-hydra/apis" 5 | 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | // +genclient 10 | // +genclient:nonNamespaced 11 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 12 | // +kubebuilder:subresource:status 13 | // +k8s:openapi-gen=true 14 | // +resource:path=settings,strategy=SettingStrategy,shortname=st 15 | // Setting is the Schema for the Dataset API 16 | type Setting struct { 17 | metav1.TypeMeta `json:",inline"` 18 | metav1.ObjectMeta `json:"metadata,omitempty"` 19 | Spec SettingSpec `json:"spec,omitempty"` 20 | Status SettingStatus `json:"status,omitempty"` 21 | } 22 | 23 | // SettingStatus defines the observed state of Device of cluster 24 | type SettingStatus struct { 25 | } 26 | 27 | type SettingSpec struct { 28 | DefaultGpuPerDevice uint8 `json:"default_gpu_per_device" yaml:"defaultGpuPerDevice"` 29 | PluginList apis.PluginList `json:"plugin_list,omitempty" yaml:"pluginList,omitempty"` 30 | } 31 | 32 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 33 | // +k8s:openapi-gen=true 34 | type SettingList struct { 35 | metav1.TypeMeta `json:",inline"` 36 | metav1.ListMeta `json:"metadata,omitempty"` 37 | Items []SettingSpec `json:"items"` 38 | } 39 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/user/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:nonNamespaced 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // +kubebuilder:subresource:status 11 | // +k8s:openapi-gen=true 12 | // +resource:path=openhydrausers,strategy=openhydraStrategy,shortname=ohuser 13 | // OpenHydraUser is the Schema for the OpenHydraUser API 14 | type OpenHydraUser struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata,omitempty"` 17 | Spec OpenHydraUserSpec `json:"spec,omitempty"` 18 | Status OpenHydraUserStatus `json:"status,omitempty"` 19 | } 20 | 21 | // OpenHydraUserSpecUserStatus defines the observed state of Device of cluster 22 | type OpenHydraUserStatus struct { 23 | } 24 | 25 | type OpenHydraUserSpec struct { 26 | ChineseName string `json:"chineseName,omitempty"` 27 | Description string `json:"description,omitempty"` 28 | Password string `json:"password"` 29 | Email string `json:"email,omitempty"` 30 | Role int `json:"role"` 31 | } 32 | 33 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 34 | // +k8s:openapi-gen=true 35 | type OpenHydraUserList struct { 36 | metav1.TypeMeta `json:",inline"` 37 | metav1.ListMeta `json:"metadata,omitempty"` 38 | Items []OpenHydraUser `json:"items"` 39 | } 40 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/course/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:nonNamespaced 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // +kubebuilder:subresource:status 11 | // +k8s:openapi-gen=true 12 | // +resource:path=courses,strategy=CourseStrategy,shortname=dst 13 | // Course is the Schema for the Course API 14 | type Course struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata,omitempty"` 17 | Spec CourseSpec `json:"spec,omitempty"` 18 | Status CourseStatus `json:"status,omitempty"` 19 | } 20 | 21 | // CourseStatus defines the observed state of Device of cluster 22 | type CourseStatus struct { 23 | } 24 | 25 | type CourseSpec struct { 26 | CreatedBy string `json:"createdBy,omitempty"` 27 | Description string `json:"description,omitempty"` 28 | LastUpdate metav1.Time `json:"lastUpdate"` 29 | Level int `json:"level,omitempty"` 30 | SandboxName string `json:"sandboxName,omitempty"` 31 | Size int64 `json:"size,omitempty"` 32 | } 33 | 34 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 35 | // +k8s:openapi-gen=true 36 | type CourseList struct { 37 | metav1.TypeMeta `json:",inline"` 38 | metav1.ListMeta `json:"metadata,omitempty"` 39 | Items []Course `json:"items"` 40 | } 41 | -------------------------------------------------------------------------------- /cmd/open-hydra-server/app/option/options.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/apimachinery/pkg/runtime/serializer" 8 | genericOptions "k8s.io/apiserver/pkg/server/options" 9 | "k8s.io/client-go/util/homedir" 10 | ) 11 | 12 | var Scheme = runtime.NewScheme() 13 | var Codecs = serializer.NewCodecFactory(Scheme) 14 | var GroupVersion = schema.GroupVersion{Group: "open-hydra-server.openhydra.io", Version: "v1"} 15 | 16 | type OpenHydraServerOption struct { 17 | ConfigFile string 18 | KubeConfigFile string 19 | } 20 | 21 | func NewDefaultOpenHydraServerOption() *OpenHydraServerOption { 22 | return &OpenHydraServerOption{} 23 | } 24 | 25 | func NewDefaultApiServerOption() *genericOptions.RecommendedOptions { 26 | return genericOptions.NewRecommendedOptions("", Codecs.LegacyCodec(GroupVersion)) 27 | } 28 | 29 | func (option *OpenHydraServerOption) BindFlags(fs *pflag.FlagSet) { 30 | fs.StringVar(&option.ConfigFile, "open-hydra-server-config", homedir.HomeDir()+"/.open-hydra-server/config.yaml", "config file location") 31 | fs.StringVar(&option.KubeConfigFile, "kube-config", homedir.HomeDir()+"/.kube/config", "kube-config file location") 32 | } 33 | 34 | type Options struct { 35 | OpenHydraServerOption *OpenHydraServerOption 36 | ApiServerOption *genericOptions.RecommendedOptions 37 | } 38 | 39 | func (options *Options) BindFlags(fs *pflag.FlagSet) { 40 | options.OpenHydraServerOption.BindFlags(fs) 41 | options.ApiServerOption.AddFlags(fs) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/database/abstraction.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | xCourseV1 "open-hydra/pkg/apis/open-hydra-api/course/core/v1" 5 | xDatasetV1 "open-hydra/pkg/apis/open-hydra-api/dataset/core/v1" 6 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 7 | ) 8 | 9 | type IDataBase interface { 10 | IDataBaseDataset 11 | IDataBaseUser 12 | IDataBaseCourse 13 | InitDb() error 14 | } 15 | 16 | type IDataBaseUser interface { 17 | // Create a new user 18 | CreateUser(user *xUserV1.OpenHydraUser) error 19 | // Get a user by name 20 | GetUser(name string) (*xUserV1.OpenHydraUser, error) 21 | // Update a user 22 | UpdateUser(user *xUserV1.OpenHydraUser) error 23 | // Delete a user 24 | DeleteUser(name string) error 25 | // List all users 26 | ListUsers() (xUserV1.OpenHydraUserList, error) 27 | // Login a user 28 | LoginUser(name, password string) (*xUserV1.OpenHydraUser, error) 29 | } 30 | 31 | type IDataBaseDataset interface { 32 | // Create a new dataset 33 | CreateDataset(dataset *xDatasetV1.Dataset) error 34 | // Get a dataset by name 35 | GetDataset(name string) (*xDatasetV1.Dataset, error) 36 | // Update a dataset 37 | UpdateDataset(dataset *xDatasetV1.Dataset) error 38 | // Delete a dataset 39 | DeleteDataset(name string) error 40 | // List all datasets 41 | ListDatasets() (xDatasetV1.DatasetList, error) 42 | } 43 | 44 | type IDataBaseCourse interface { 45 | // Create a new course 46 | CreateCourse(course *xCourseV1.Course) error 47 | // Get a course by name 48 | GetCourse(name string) (*xCourseV1.Course, error) 49 | // Update a course 50 | UpdateCourse(course *xCourseV1.Course) error 51 | // Delete a course 52 | DeleteCourse(name string) error 53 | // List all courses 54 | ListCourses() (xCourseV1.CourseList, error) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/summary/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +genclient:nonNamespaced 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // +kubebuilder:subresource:status 11 | // +k8s:openapi-gen=true 12 | // +resource:path=sumups,strategy=sumupStrategy,shortname=xsu 13 | // SumUp is the Schema for the cluster summary API 14 | type SumUp struct { 15 | metav1.TypeMeta `json:",inline"` 16 | metav1.ObjectMeta `json:"metadata,omitempty"` 17 | Spec SumUpSpec `json:"spec,omitempty"` 18 | Status SumUpStatus `json:"status,omitempty"` 19 | } 20 | 21 | // SumUpStatus defines the observed state of Device of cluster 22 | type SumUpStatus struct { 23 | } 24 | 25 | type SumUpSpec struct { 26 | PodAllocatable int `json:"podAllocatable"` 27 | PodAllocated int `json:"podAllocated"` 28 | GpuAllocatable string `json:"gpuAllocatable"` 29 | GpuAllocated string `json:"gpuAllocated"` 30 | DefaultCpuPerDevice string `json:"defaultCpuPerDevice"` 31 | DefaultRamPerDevice string `json:"defaultRamPerDevice"` 32 | DefaultGpuPerDevice uint8 `json:"defaultGpuPerDevice"` 33 | TotalLine uint16 `json:"totalLine"` 34 | GpuResourceSumUp map[string]GpuResourceSumUp `json:"gpuResourceSumUp"` 35 | } 36 | 37 | type GpuResourceSumUp struct { 38 | Allocated int64 `json:"allocated"` 39 | Allocatable int64 `json:"allocatable"` 40 | } 41 | 42 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 43 | // +k8s:openapi-gen=true 44 | type SumUpList struct { 45 | metav1.TypeMeta `json:",inline"` 46 | metav1.ListMeta `json:"metadata,omitempty"` 47 | Items []SumUp `json:"items"` 48 | } 49 | -------------------------------------------------------------------------------- /pkg/open-hydra/api-resource.go: -------------------------------------------------------------------------------- 1 | package openhydra 2 | 3 | import ( 4 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | const ( 8 | OpenHydraUserKind = "OpenHydraUser" 9 | DeviceKind = "Device" 10 | SumUpKind = "SumUp" 11 | OpenHydraUserPath = "openhydrausers" 12 | DevicePath = "devices" 13 | SumUpPath = "sumups" 14 | DatasetPath = "datasets" 15 | DatasetKind = "Dataset" 16 | SettingKind = "Setting" 17 | SettingPath = "settings" 18 | CourseKind = "Course" 19 | CoursePath = "courses" 20 | ) 21 | 22 | // we should register the api resource here 23 | func ApiResources() []metaV1.APIResource { 24 | return []metaV1.APIResource{ 25 | { 26 | Name: OpenHydraUserPath, 27 | SingularName: "xuser", 28 | Namespaced: false, 29 | Kind: OpenHydraUserKind, 30 | Verbs: metaV1.Verbs{"get", "list", "watch", "create", "update", "delete", "patch"}, 31 | }, 32 | { 33 | Name: DevicePath, 34 | SingularName: "dev", 35 | Namespaced: false, 36 | Kind: DeviceKind, 37 | Verbs: metaV1.Verbs{"get", "list", "watch", "create", "update", "delete", "patch"}, 38 | }, 39 | { 40 | Name: SumUpPath, 41 | SingularName: "xsu", 42 | Namespaced: false, 43 | Kind: SumUpKind, 44 | Verbs: metaV1.Verbs{"get"}, 45 | }, 46 | { 47 | Name: DatasetPath, 48 | SingularName: "xds", 49 | Namespaced: false, 50 | Kind: DatasetKind, 51 | Verbs: metaV1.Verbs{"get", "list", "watch", "create", "update", "delete"}, 52 | }, 53 | { 54 | Name: SettingPath, 55 | SingularName: "setting", 56 | Namespaced: false, 57 | Kind: SettingKind, 58 | Verbs: metaV1.Verbs{"get", "update"}, 59 | }, 60 | { 61 | Name: CoursePath, 62 | SingularName: "course", 63 | Namespaced: false, 64 | Kind: CourseKind, 65 | Verbs: metaV1.Verbs{"get", "list", "watch", "create", "update", "delete"}, 66 | }, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/open-hydra/apis/api.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | // +k8s:openapi-gen=true 4 | type VolumeMount struct { 5 | Name string `json:"name"` 6 | MountPath string `json:"mount_path"` 7 | SourcePath string `json:"source_path"` 8 | ReadOnly bool `json:"read_only"` 9 | } 10 | 11 | // +k8s:openapi-gen=true 12 | type Volume struct { 13 | EmptyDir *EmptyDir `json:"empty_dir,omitempty"` 14 | HostPath *HostPath `json:"host_path,omitempty"` 15 | } 16 | 17 | // +k8s:openapi-gen=true 18 | type EmptyDir struct { 19 | Medium string `json:"medium"` 20 | SizeLimit uint64 `json:"size_limit"` 21 | Name string `json:"name"` 22 | } 23 | 24 | // +k8s:openapi-gen=true 25 | type HostPath struct { 26 | Name string `json:"name"` 27 | Path string `json:"path"` 28 | Type string `json:"type"` 29 | } 30 | 31 | type GpuSet struct { 32 | GpuDriverName string `json:"gpu_driver_name"` 33 | Gpu uint8 `json:"gpu"` 34 | } 35 | 36 | // +k8s:openapi-gen=true 37 | type Sandbox struct { 38 | CPUImageName string `json:"cpuImageName,omitempty"` 39 | GPUImageSet map[string]string `json:"gpuImageSet,omitempty"` 40 | Command []string `json:"command,omitempty"` 41 | Args []string `json:"args,omitempty"` 42 | DisplayTitle string `json:"display_title,omitempty"` 43 | Description string `json:"description,omitempty"` 44 | DevelopmentInfo []string `json:"developmentInfo,omitempty"` 45 | Status string `json:"status,omitempty"` 46 | Ports []SandboxPort `json:"ports,omitempty"` 47 | VolumeMounts []VolumeMount `json:"volume_mounts,omitempty"` 48 | Volumes []Volume `json:"volumes,omitempty"` 49 | IconName string `json:"icon_name,omitempty"` 50 | } 51 | 52 | type SandboxPort struct { 53 | Port uint16 `json:"port"` 54 | Name string `json:"name"` 55 | } 56 | 57 | // +k8s:openapi-gen=true 58 | type PluginList struct { 59 | DefaultSandbox string `json:"defaultSandbox"` 60 | Sandboxes map[string]Sandbox `json:"sandboxes"` 61 | } 62 | -------------------------------------------------------------------------------- /docs/trouble-shooting-en.md: -------------------------------------------------------------------------------- 1 | # trouble shooting 2 | 3 | This document describes how to solve some common errors 4 | 5 | ## openhydra installation interrupted 6 | 7 | * This situation is mostly unrecoverable automatically. We need the following steps to completely reset and reinstall 8 | 9 | ```bash 10 | # Login to the server 11 | # Switch to the root account 12 | # Start reset 13 | $ systemctl stop kubelet 14 | $ systemctl disable kubelet 15 | # stop and disable openhydra installation service 16 | # because the systemd service will run automatically after the server reboot if /etc/kubernetes is not exists 17 | # so to avoid chaos, we need to stop and disable the service to make thing easier 18 | $ systemctl stop maas 19 | $ systemctl disable maas 20 | # If the kubeadm reset -f command is stuck for more than 2 minutes, just restart the server 21 | $ kubeadm reset -f 22 | # Clean up the kubeclipper database 23 | $ kcctl clean -Af 24 | # Check for zombie containers, if you reboot you server previously then most likely you don't have any zombie containers remaining just skip this step 25 | $ ctr -n k8s.io container list 26 | # If the above command returns some containers, you need to restart the server or you can skip rebooting server 27 | # because clear all the containers and task for containerd is really a big job, so to easy things up, just reboot the server 28 | $ reboot 29 | # reset again 30 | $ kubeadm reset -f 31 | # remove kubernetes configuration 32 | $ rm -rf /etc/kubernetes 33 | 34 | # Start reinstallation of openhydra by rest 35 | $ systemctl start maas 36 | 37 | # check the log 38 | $ journalctl -u maas -f 39 | ``` 40 | 41 | ## How to update the image of open-hydra server 42 | 43 | * Login to the server running open-hydra server and run the following command 44 | 45 | ```bash 46 | # Manually download the image 47 | $ ctr -n k8s.io i pull registry.cn-shanghai.aliyuncs.com/openhydra/open-hydra-server:latest 48 | # Restart open-hydra server 49 | $ kubectl scale deployment open-hydra-server --replicas=0 -n open-hydra 50 | # Wait for 3 seconds 51 | $ kubectl scale deployment open-hydra-server --replicas=1 -n open-hydra 52 | ``` 53 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | name: build-image 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "v*" 9 | paths: 10 | - "**.go" 11 | 12 | jobs: 13 | build: 14 | name: build-image 15 | runs-on: ubuntu-latest 16 | env: 17 | GO111MODULE: on 18 | steps: 19 | - name: Set up Go 1.21.4 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: 1.21.4 23 | id: go 24 | 25 | - name: Check out code into the Go module directory 26 | uses: actions/checkout@v3 27 | with: 28 | fetch-depth: 0 29 | 30 | - name: Extract Tag 31 | id: extract_tag 32 | run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\//} 33 | 34 | - name: Extract branch name 35 | shell: bash 36 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 37 | id: extract_branch 38 | 39 | - name: Set up docker buildx 40 | id: buildx 41 | uses: docker/setup-buildx-action@v2 42 | with: 43 | version: latest 44 | 45 | - name: Available platforms 46 | run: echo ${{ steps.buildx.outputs.platforms }} 47 | 48 | - name: Docker login 49 | env: 50 | DOCKER_USERNAME: ${{ secrets.ALIYUN_REGISTRY_USERNAME }} 51 | DOCKER_PASSWORD: ${{ secrets.ALIYUN_REGISTRY_PASSWORD }} 52 | run: | 53 | echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin registry.cn-shanghai.aliyuncs.com 54 | 55 | - name: Run buildx 56 | run: | 57 | tag=latest 58 | if [[ "${{ steps.extract_branch.outputs.branch }}" == "master" ]];then 59 | tag=latest 60 | elif [[ "${{ steps.extract_tag.outputs.version }}" == v* ]]; then 61 | tag="${{ steps.extract_tag.outputs.version }}" 62 | fi 63 | 64 | echo extract_tag ${{ steps.extract_tag.outputs.version }} 65 | echo extract_branch ${{ steps.extract_branch.outputs.branch }} 66 | echo current tag is ${tag} 67 | 68 | docker buildx build --platform linux/amd64,linux/arm64 --push -t registry.cn-shanghai.aliyuncs.com/openhydra/open-hydra-server:${tag} . 69 | -------------------------------------------------------------------------------- /docs/keystone-interfacing.md: -------------------------------------------------------------------------------- 1 | # openhydra 最小对接 keystone(alpha 功能) 2 | 3 | 本文档描述了如何将 openhydra 的验证和 keystone 对接,我们目前只支持将 openhydra 用户信息存放到 keystone 的 user 结构体中 4 | 5 | ## 开始之前 6 | 7 | * openhydra 已经正常运行中 8 | * keystone 服务已经部署好了,并正常工作 9 | * 注意在和 keystone 对接后,会以 keystone 中而 username 做为用户在 openhydra 10 | 11 | ## 修改 openhydra 配置文件 12 | 13 | 登陆到服务器上并运行以下命令 14 | 15 | ```bash 16 | # 备份 openhydra 配置文件 17 | $ kubectl get cm -n open-hydra open-hydra-config -o yaml > open-hydra-config.yaml 18 | # 修改 openhydra 配置文件 19 | $ kubectl edit cm -n open-hydra open-hydra-config 20 | # 在 resourceNamespace 下加入以下配置后保存退出 21 | # 替换 keystone-user 为你的 keystone 的账号和密码,我们这里建议您使用 admin 账号 22 | authDelegateConfig: 23 | keystoneConfig: 24 | endpoint: http://[keystone ip]:[port] 25 | username: keystone-user 26 | password: keystone-user-password 27 | domainId: default 28 | 29 | # 重启 openhydra server 30 | $ kubectl scale deployment -n open-hydra open-hydra-server --replicas=0 31 | $ kubectl scale deployment -n open-hydra open-hydra-server --replicas=1 32 | ``` 33 | 34 | ## 检验结果 35 | 36 | 在服务器上运行以下命令,注意当您集成 keystone 后,如果您尝试删除 admin 和 service 账号的操作是会被拒绝的 37 | 38 | ```bash 39 | # 列出所有用户 40 | $ kubectl get openhydrausers 41 | # 输出 42 | NAME AGE 43 | 595af254b5fe491fb7fa2c0a42cb299b 44 | 19ff8aee587f4dbeabff2fb88003c6d7 45 | 0c3eee2a36da4c2d8ba8b3a073eaeec3 46 | e521cf70761d432ba09d093257ac7e35 47 | 4bce15e7abc240c3860bca2c0aaa271b 48 | d75eaa9345984278ad6e6106f48c0d30 49 | 50 | # 创建用户 51 | # 由于 keystone 使用 id 作为唯一的主键会和 k8s 理念冲突,所以则中做法,将 id 变为登陆名,创建的时候一切照旧,但是返回看到的时候是 keystone 中的 id 作为登陆 id 52 | $ cat < user1.yaml 53 | apiVersion: open-hydra-server.openhydra.io/v1 54 | kind: OpenHydraUser 55 | metadata: 56 | name: user1 57 | spec: 58 | password: Openhydra123 59 | role: 2 60 | EOF 61 | 62 | # 验证结果 63 | $ kubectl get openhydrausers -o custom-columns=Name:metadata.name,User:.spec.chineseName 64 | # 输出结果 65 | Name User 66 | ....... 67 | 18ca18f419e54de7b040617b5c065c7f user1 68 | 69 | 70 | # 尝试登陆 71 | # 打开 openhydra 访问页面,输入用户名 18ca18f419e54de7b040617b5c065c7f 和密码 Openhydra123 72 | 73 | # 删除用户 74 | $ kubectl delete openhydrauser 18ca18f419e54de7b040617b5c065c7f 75 | 76 | ``` -------------------------------------------------------------------------------- /rp.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: reverse-proxy-jlab 7 | namespace: open-hydra 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: my-app 13 | template: 14 | metadata: 15 | labels: 16 | app: my-app 17 | spec: 18 | containers: 19 | - name: my-container 20 | image: docker.io/library/nginx:1.24.0-alpine 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - containerPort: 80 24 | volumeMounts: 25 | - name: nginx-conf 26 | mountPath: /etc/nginx/nginx.conf 27 | subPath: nginx.conf 28 | readOnly: true 29 | volumes: 30 | - name: nginx-conf 31 | configMap: 32 | name: nginx-conf-jlab 33 | items: 34 | - key: nginx.conf 35 | path: nginx.conf 36 | 37 | --- 38 | 39 | apiVersion: v1 40 | kind: Service 41 | metadata: 42 | name: reverse-proxy 43 | namespace: open-hydra 44 | spec: 45 | ipFamilies: 46 | - IPv4 47 | ipFamilyPolicy: SingleStack 48 | ports: 49 | - port: 8888 50 | protocol: TCP 51 | targetPort: 8888 52 | selector: 53 | app: my-app 54 | sessionAffinity: None 55 | type: ClusterIP 56 | 57 | --- 58 | 59 | apiVersion: v1 60 | data: 61 | nginx.conf: | 62 | # /etc/nginx/nginx.conf 63 | 64 | user nginx; 65 | worker_processes 1; 66 | 67 | events { 68 | worker_connections 1024; 69 | } 70 | 71 | http { 72 | upstream upstream_jupyter { 73 | server openhydra-service-aes-admin.open-hydra.svc:8888; 74 | keepalive 32; 75 | } 76 | server { 77 | listen 8888; 78 | server_name test.com; 79 | location / { 80 | proxy_set_header Host $host; 81 | proxy_set_header X-Real-IP $remote_addr; 82 | proxy_hide_header "X-Frame-Options"; 83 | proxy_pass http://upstream_jupyter; 84 | proxy_http_version 1.1; 85 | proxy_set_header Upgrade "websocket"; 86 | proxy_set_header Connection "Upgrade"; 87 | proxy_read_timeout 86400; 88 | } 89 | } 90 | } 91 | kind: ConfigMap 92 | metadata: 93 | name: nginx-conf-jlab 94 | namespace: open-hydra -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/device/core/v1/types.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | coreV1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | ) 7 | 8 | // +genclient 9 | // +genclient:nonNamespaced 10 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 11 | // +kubebuilder:subresource:status 12 | // +k8s:openapi-gen=true 13 | // +resource:path=devices,strategy=DeviceStrategy,shortname=dev 14 | // Device is the Schema for the Device API 15 | type Device struct { 16 | metav1.TypeMeta `json:",inline"` 17 | metav1.ObjectMeta `json:"metadata,omitempty"` 18 | Spec DeviceSpec `json:"spec,omitempty"` 19 | Status DeviceStatus `json:"status,omitempty"` 20 | } 21 | 22 | // DeviceStatus defines the observed state of Device of cluster 23 | type DeviceStatus struct { 24 | } 25 | 26 | type DeviceSpec struct { 27 | DeviceName string `json:"deviceName,omitempty"` 28 | DeviceNamespace string `json:"deviceNamespace,omitempty"` 29 | DeviceType string `json:"deviceType,omitempty"` 30 | DeviceIP string `json:"deviceIP,omitempty"` 31 | DeviceCpu string `json:"deviceCpu,omitempty"` 32 | DeviceRam string `json:"deviceRam,omitempty"` 33 | DeviceGpu uint8 `json:"deviceGpu,omitempty"` 34 | DeviceStatus string `json:"deviceStatus,omitempty"` 35 | GpuDriver string `json:"gpuDriver,omitempty"` 36 | OpenHydraUsername string `json:"openHydraUsername,omitempty"` 37 | OpenHydraProjectId string `json:"openHydraProjectId,omitempty"` 38 | Role int `json:"role,omitempty"` 39 | ChineseName string `json:"chineseName,omitempty"` 40 | LineNo string `json:"lineNo,omitempty"` 41 | UsePublicDataSet bool `json:"usePublicDataSet,omitempty"` 42 | SandboxURLs string `json:"sandboxURLs,omitempty"` 43 | SandboxName string `json:"sandboxName,omitempty"` 44 | Affinity *coreV1.Affinity `json:"affinity,omitempty"` 45 | } 46 | 47 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 48 | // +k8s:openapi-gen=true 49 | type DeviceList struct { 50 | metav1.TypeMeta `json:",inline"` 51 | metav1.ListMeta `json:"metadata,omitempty"` 52 | Items []Device `json:"items"` 53 | } 54 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/course/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &Course{}, 61 | &CourseList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/device/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &Device{}, 61 | &DeviceList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/summary/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &SumUp{}, 61 | &SumUpList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/dataset/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &Dataset{}, 61 | &DatasetList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/setting/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &Setting{}, 61 | &SettingList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/user/core/v1/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | // Code generated by register-gen. DO NOT EDIT. 17 | 18 | package v1 19 | 20 | import ( 21 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/apimachinery/pkg/runtime" 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | ) 25 | 26 | // GroupName specifies the group name used to register the objects. 27 | const GroupName = "open-hydra-server.openhydra.io" 28 | 29 | // GroupVersion specifies the group and the version used to register the objects. 30 | var GroupVersion = v1.GroupVersion{Group: GroupName, Version: "v1"} 31 | 32 | // SchemeGroupVersion is group version used to register these objects 33 | // Deprecated: use GroupVersion instead. 34 | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} 35 | 36 | // Resource takes an unqualified resource and returns a Group qualified GroupResource 37 | func Resource(resource string) schema.GroupResource { 38 | return SchemeGroupVersion.WithResource(resource).GroupResource() 39 | } 40 | 41 | var ( 42 | // localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes. 43 | SchemeBuilder runtime.SchemeBuilder 44 | localSchemeBuilder = &SchemeBuilder 45 | // Depreciated: use Install instead 46 | AddToScheme = localSchemeBuilder.AddToScheme 47 | Install = localSchemeBuilder.AddToScheme 48 | ) 49 | 50 | func init() { 51 | // We only register manually written functions here. The registration of the 52 | // generated functions takes place in the generated files. The separation 53 | // makes the code compile even when the generated files are missing. 54 | localSchemeBuilder.Register(addKnownTypes) 55 | } 56 | 57 | // Adds the list of known types to Scheme. 58 | func addKnownTypes(scheme *runtime.Scheme) error { 59 | scheme.AddKnownTypes(SchemeGroupVersion, 60 | &OpenHydraUser{}, 61 | &OpenHydraUserList{}, 62 | ) 63 | // AddToGroupVersion allows the serialization of client types like ListOptions. 64 | v1.AddToGroupVersion(scheme, SchemeGroupVersion) 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /pkg/open-hydra/k8s/abstraction.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "open-hydra/pkg/open-hydra/apis" 5 | 6 | appsV1 "k8s.io/api/apps/v1" 7 | coreV1 "k8s.io/api/core/v1" 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | type DeploymentParameters struct { 12 | CpuMemorySet CpuMemorySet 13 | Image string 14 | Namespace string 15 | Username string 16 | SandboxName string 17 | VolumeMounts []apis.VolumeMount 18 | GpuSet apis.GpuSet 19 | Client *kubernetes.Clientset 20 | Command []string 21 | Args []string 22 | Ports map[string]int 23 | Volumes []apis.Volume 24 | Affinity *coreV1.Affinity 25 | CustomLabels map[string]string 26 | } 27 | 28 | type IOpenHydraK8sHelper interface { 29 | ListDeploymentWithLabel(label, namespace string, client *kubernetes.Clientset) ([]appsV1.Deployment, error) 30 | ListPodWithLabel(label, namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) 31 | ListPod(namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) 32 | GetUserPods(label, namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) 33 | ListDeployment(namespace string, client *kubernetes.Clientset) ([]appsV1.Deployment, error) 34 | ListService(namespace string, client *kubernetes.Clientset) ([]coreV1.Service, error) 35 | DeleteUserDeployment(label, namespace string, client *kubernetes.Clientset) error 36 | CreateDeployment(deployParameter *DeploymentParameters) error 37 | CreateService(namespace, userName, ideType string, client *kubernetes.Clientset, ports map[string]int) error 38 | DeleteUserService(label, namespace string, client *kubernetes.Clientset) error 39 | GetUserService(label, namespace string, client *kubernetes.Clientset) (*coreV1.Service, error) 40 | DeleteUserReplicaSet(label, namespace string, client *kubernetes.Clientset) error 41 | DeleteUserPod(label, namespace string, client *kubernetes.Clientset) error 42 | GetConfigMap(name, namespace string) (*coreV1.ConfigMap, error) 43 | UpdateConfigMap(name, namespace string, data map[string]string) error 44 | RunInformers(stopChan <-chan struct{}) 45 | } 46 | 47 | func NewDefaultK8sHelper(clientSet *kubernetes.Clientset, stopChan <-chan struct{}) IOpenHydraK8sHelper { 48 | helper := &DefaultHelper{ 49 | clientSet: clientSet, 50 | } 51 | helper.InitInformer() 52 | helper.RunInformers(stopChan) 53 | return helper 54 | } 55 | 56 | func NewDefaultK8sHelperWithFake() *Fake { 57 | fake := &Fake{} 58 | fake.Init() 59 | return fake 60 | } 61 | 62 | type CpuMemorySet struct { 63 | CpuRequest string 64 | CpuLimit string 65 | MemoryRequest string 66 | MemoryLimit string 67 | } 68 | -------------------------------------------------------------------------------- /deploy/reverse-proxy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: reverse-proxy 7 | namespace: open-hydra 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: my-app 13 | template: 14 | metadata: 15 | labels: 16 | app: my-app 17 | spec: 18 | containers: 19 | - name: my-container 20 | image: docker.io/library/nginx:1.24.0-alpine 21 | imagePullPolicy: IfNotPresent 22 | ports: 23 | - containerPort: 80 24 | volumeMounts: 25 | - name: k8s-certs 26 | mountPath: /etc/kubernetes/pki 27 | readOnly: true 28 | - name: nginx-conf 29 | mountPath: /etc/nginx/nginx.conf 30 | subPath: nginx.conf 31 | readOnly: true 32 | volumes: 33 | - hostPath: 34 | path: /etc/kubernetes/pki 35 | type: DirectoryOrCreate 36 | name: k8s-certs 37 | - name: nginx-conf 38 | configMap: 39 | name: nginx-conf 40 | items: 41 | - key: nginx.conf 42 | path: nginx.conf 43 | 44 | --- 45 | 46 | apiVersion: v1 47 | kind: Service 48 | metadata: 49 | name: reverse-proxy 50 | namespace: open-hydra 51 | spec: 52 | ipFamilies: 53 | - IPv4 54 | ipFamilyPolicy: SingleStack 55 | ports: 56 | - port: 80 57 | protocol: TCP 58 | targetPort: 80 59 | selector: 60 | app: my-app 61 | sessionAffinity: None 62 | type: ClusterIP 63 | 64 | --- 65 | 66 | apiVersion: v1 67 | data: 68 | nginx.conf: | 69 | # /etc/nginx/nginx.conf 70 | 71 | user nginx; 72 | worker_processes 1; 73 | 74 | events { 75 | worker_connections 1024; 76 | } 77 | 78 | http { 79 | upstream api { 80 | server kubernetes.default.svc:443; 81 | } 82 | server { 83 | listen 80; 84 | server_name myapi.myk8s.com; 85 | location / { 86 | root /usr/local/nginx/html; 87 | index index.htm index.html; 88 | } 89 | location /api/ { 90 | client_max_body_size 200M; 91 | rewrite ^/api(/.*)$ $1 break; 92 | proxy_pass https://api; 93 | proxy_ssl_certificate /etc/kubernetes/pki/apiserver-kubelet-client.crt; 94 | proxy_ssl_certificate_key /etc/kubernetes/pki/apiserver-kubelet-client.key; 95 | proxy_ssl_session_reuse on; 96 | } 97 | } 98 | } 99 | kind: ConfigMap 100 | metadata: 101 | name: nginx-conf 102 | namespace: open-hydra -------------------------------------------------------------------------------- /pkg/database/etcd.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "open-hydra/cmd/open-hydra-server/app/config" 5 | xCourseV1 "open-hydra/pkg/apis/open-hydra-api/course/core/v1" 6 | xDatasetV1 "open-hydra/pkg/apis/open-hydra-api/dataset/core/v1" 7 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 8 | ) 9 | 10 | type Etcd struct { 11 | Config *config.OpenHydraServerConfig 12 | } 13 | 14 | // implements IDataBaseUser creates a new user 15 | func (db *Etcd) CreateUser(user *xUserV1.OpenHydraUser) error { 16 | return nil 17 | } 18 | 19 | // implements IDataBaseUser gets a user by name 20 | func (db *Etcd) GetUser(name string) (*xUserV1.OpenHydraUser, error) { 21 | return nil, nil 22 | } 23 | 24 | // implements IDataBaseUser updates a user 25 | func (db *Etcd) UpdateUser(user *xUserV1.OpenHydraUser) error { 26 | return nil 27 | } 28 | 29 | // implements IDataBaseUser deletes a user 30 | func (db *Etcd) DeleteUser(name string) error { 31 | return nil 32 | } 33 | 34 | // implements IDataBaseUser lists all users 35 | func (db *Etcd) ListUsers() (xUserV1.OpenHydraUserList, error) { 36 | return xUserV1.OpenHydraUserList{}, nil 37 | } 38 | 39 | // implements IDataBaseDataset creates a new dataset 40 | func (db *Etcd) CreateDataset(dataset *xDatasetV1.Dataset) error { 41 | return nil 42 | } 43 | 44 | // implements IDataBaseDataset gets a dataset by name 45 | func (db *Etcd) GetDataset(name string) (*xDatasetV1.Dataset, error) { 46 | return nil, nil 47 | } 48 | 49 | // implements IDataBaseDataset updates a dataset 50 | func (db *Etcd) UpdateDataset(dataset *xDatasetV1.Dataset) error { 51 | return nil 52 | } 53 | 54 | // implements IDataBaseDataset deletes a dataset 55 | func (db *Etcd) DeleteDataset(name string) error { 56 | return nil 57 | } 58 | 59 | // implements IDataBaseDataset lists all datasets 60 | func (db *Etcd) ListDatasets() (xDatasetV1.DatasetList, error) { 61 | return xDatasetV1.DatasetList{}, nil 62 | } 63 | 64 | func (db *Etcd) LoginUser(name, password string) (*xUserV1.OpenHydraUser, error) { 65 | return nil, nil 66 | } 67 | 68 | func (db *Etcd) InitDb() error { 69 | return nil 70 | } 71 | 72 | // implements IDataBaseCourse creates a new course 73 | func (db *Etcd) CreateCourse(course *xCourseV1.Course) error { 74 | return nil 75 | } 76 | 77 | // implements IDataBaseCourse gets a course by name 78 | func (db *Etcd) GetCourse(name string) (*xCourseV1.Course, error) { 79 | return nil, nil 80 | } 81 | 82 | // implements IDataBaseCourse updates a course 83 | func (db *Etcd) UpdateCourse(course *xCourseV1.Course) error { 84 | return nil 85 | } 86 | 87 | // implements IDataBaseCourse deletes a course 88 | func (db *Etcd) DeleteCourse(name string) error { 89 | return nil 90 | } 91 | 92 | // implements IDataBaseCourse lists all courses 93 | func (db *Etcd) ListCourses() (xCourseV1.CourseList, error) { 94 | return xCourseV1.CourseList{}, nil 95 | } 96 | -------------------------------------------------------------------------------- /docs/keystone-interfacing-en.md: -------------------------------------------------------------------------------- 1 | # openhydra interfacing with keystone (alpha feature) 2 | 3 | This document describes how to interface openhydra with keystone for authentication. Currently, we only support storing openhydra user information in the keystone user structure. 4 | 5 | ## Before you begin 6 | 7 | * openhydra is running normally 8 | * The keystone service has been deployed and is working properly 9 | * Note that after interfacing with keystone, the username in keystone will be used as the account in openhydra 10 | 11 | ## Modify the openhydra configuration file 12 | 13 | Log in to the server and run the following command 14 | 15 | ```bash 16 | # Backup the openhydra configuration file 17 | $ kubectl get cm -n open-hydra open-hydra-config -o yaml > open-hydra-config.yaml 18 | # Modify the openhydra configuration file 19 | $ kubectl edit cm -n open-hydra open-hydra-config 20 | # Add the following configuration under the key "resourceNamespace" and save and exit 21 | # Replace keystone-user with your keystone account and password. We recommend that you use the admin account here 22 | authDelegateConfig: 23 | keystoneConfig: 24 | endpoint: http://[keystone ip]:[port] 25 | username: keystone-user 26 | password: keystone-user-password 27 | domainId: default 28 | 29 | # Restart the openhydra server 30 | $ kubectl scale deployment -n open-hydra open-hydra-server --replicas=0 31 | $ kubectl scale deployment -n open-hydra open-hydra-server --replicas=1 32 | ``` 33 | 34 | ## Verify the results 35 | 36 | Run the following command on the server. Note that if you try to delete the admin and service accounts after integrating with keystone, the operation will be rejected 37 | 38 | ```bash 39 | # List all users 40 | $ kubectl get openhydrausers 41 | # Output 42 | NAME AGE 43 | 595af254b5fe491fb7fa2c0a42cb299b 44 | 19ff8aee587f4dbeabff2fb88003c6d7 45 | 0c3eee2a36da4c2d8ba8b3a073eaeec3 46 | e521cf70761d432ba09d093257ac7e35 47 | 4bce15e7abc240c3860bca2c0aaa271b 48 | d75eaa9345984278ad6e6106f48c0d30 49 | 50 | # Creating a user 51 | # Since keystone uses the id as the unique primary key, which conflicts with the k8s concept, we will change the id to the login name. When creating a user, 52 | # everything is the same as usual, but when you see the return, the id in keystone will be used as the account 53 | $ cat < user1.yaml 54 | apiVersion: open-hydra-server.openhydra.io/v1 55 | kind: OpenHydraUser 56 | metadata: 57 | name: user1 58 | spec: 59 | password: Openhydra123 60 | role: 2 61 | EOF 62 | 63 | 64 | # Verify the results 65 | $ kubectl get openhydrausers -o custom-columns=Name:metadata.name,User:.spec.chineseName 66 | # Output 67 | Name User 68 | ....... 69 | 18ca18f419e54de7b040617b5c065c7f user1 70 | 71 | 72 | # Attempt to log in 73 | # Open the openhydra access page and enter the username 18ca18f419e54de7b040617b5c065c7f and password Openhydra123 74 | 75 | # Delete user 76 | # note openhydra will refused to delete user admin for security reasons when use keystone as the authentication plugin 77 | $ kubectl delete openhydrauser 18ca18f419e54de7b040617b5c065c7f 78 | ``` -------------------------------------------------------------------------------- /pkg/database/auth-plugin/keystone/train/schema.go: -------------------------------------------------------------------------------- 1 | package train 2 | 3 | import ( 4 | v1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 5 | ) 6 | 7 | // token issue response body 8 | type TokenResponse struct { 9 | Token Token `json:"token"` 10 | } 11 | 12 | type Token struct { 13 | Methods []string `json:"methods"` 14 | User User `json:"user"` 15 | AuditIds []string `json:"audit_ids"` 16 | ExpiresAt string `json:"expires_at"` 17 | IssuedAt string `json:"issued_at"` 18 | Domain Domain `json:"domain"` 19 | Roles []Role `json:"roles"` 20 | Catalog []Catalog `json:"catalog"` 21 | } 22 | 23 | type Role struct { 24 | ID string `json:"id"` 25 | Name string `json:"name"` 26 | } 27 | 28 | type Catalog struct { 29 | Endpoints []Endpoint `json:"endpoints"` 30 | ID string `json:"id"` 31 | Type string `json:"type"` 32 | Name string `json:"name"` 33 | } 34 | 35 | type Endpoint struct { 36 | ID string `json:"id"` 37 | Interface string `json:"interface"` 38 | RegionID string `json:"region_id"` 39 | URL string `json:"url"` 40 | Region string `json:"region"` 41 | } 42 | 43 | // keystone token issue request body 44 | type AuthRequest struct { 45 | Auth AuthDetails `json:"auth"` 46 | } 47 | 48 | type AuthDetails struct { 49 | Identity IdentityDetails `json:"identity"` 50 | Scope *ScopeDetails `json:"scope,omitempty"` 51 | } 52 | 53 | type IdentityDetails struct { 54 | Methods []string `json:"methods"` 55 | Password PasswordDetails `json:"password"` 56 | } 57 | 58 | type PasswordDetails struct { 59 | User User `json:"user"` 60 | } 61 | 62 | type Domain struct { 63 | Id string `json:"id"` 64 | Name string `json:"name"` 65 | } 66 | 67 | type ScopeDetails struct { 68 | Domain DomainDetails `json:"domain"` 69 | } 70 | 71 | type DomainDetails struct { 72 | Id string `json:"id"` 73 | } 74 | 75 | type User struct { 76 | ID string `json:"id,omitempty"` 77 | Name string `json:"name,omitempty"` 78 | DomainID string `json:"domain_id,omitempty"` 79 | ProjectID string `json:"project_id,omitempty"` 80 | Enabled bool `json:"enabled,omitempty"` 81 | PasswordExpiresAt string `json:"password_expires_at,omitempty"` 82 | Password string `json:"password,omitempty"` 83 | Domain *Domain `json:"domain,omitempty"` 84 | Options *Options `json:"options,omitempty"` 85 | Links *Links `json:"links,omitempty"` 86 | OpenhydraUser *v1.OpenHydraUser `json:"openhydra,omitempty"` 87 | Email string `json:"email,omitempty"` 88 | } 89 | 90 | type Options struct { 91 | IgnoreChangePasswordUponFirstUse bool `json:"ignore_change_password_upon_first_use,omitempty"` 92 | IgnorePasswordExpiry bool `json:"ignore_password_expiry,omitempty"` 93 | IgnoreLockoutFailureAttempts bool `json:"ignore_lockout_failure_attempts,omitempty"` 94 | LockPassword bool `json:"lock_password,omitempty"` 95 | } 96 | 97 | type Links struct { 98 | Self string `json:"self"` 99 | } 100 | 101 | type UserContainer struct { 102 | Users []User `json:"users"` 103 | } 104 | -------------------------------------------------------------------------------- /cmd/open-hydra-server/app/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "open-hydra/pkg/util" 5 | 6 | . "github.com/onsi/ginkgo/v2" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("open-hydra config test", func() { 11 | 12 | configFile := "/tmp/open-hydra-config.yaml" 13 | configFile2 := "/tmp/open-hydra-config2.yaml" 14 | kubeConfigPath := "/tmp/kube-config.yaml" 15 | testConfig := DefaultConfig() 16 | testConfig2 := *testConfig 17 | testConfig2.MySqlConfig.Character = "utf8mb4" 18 | testConfig2.MySqlConfig.Collation = "utf8mb4_general_ci" 19 | testConfig2.LeaderElection = nil 20 | testKubeConfig := `apiVersion: v1 21 | clusters: 22 | - cluster: 23 | certificate-authority-data: test 24 | server: https://apiserver.cluster.local:6443 25 | name: test-cluster 26 | contexts: 27 | - context: 28 | cluster: test-cluster 29 | user: kubernetes-admin 30 | name: kubernetes-admin@test-cluster 31 | current-context: kubernetes-admin@test-cluster 32 | kind: Config 33 | preferences: {} 34 | users: 35 | - name: kubernetes-admin 36 | user: 37 | client-certificate-data: test 38 | client-key-data: test` 39 | 40 | BeforeEach(func() { 41 | err := util.WriteFileWithNosec(kubeConfigPath, []byte(testKubeConfig)) 42 | Expect(err).To(BeNil()) 43 | }) 44 | 45 | Describe("config file read write test", func() { 46 | It("write and read to config file no error", func() { 47 | err := WriteConfig(configFile, testConfig) 48 | Expect(err).To(BeNil()) 49 | targetConfig, err := LoadConfig(configFile, kubeConfigPath) 50 | Expect(err).To(BeNil()) 51 | Expect(targetConfig.DefaultCpuPerDevice).To(Equal(testConfig.DefaultCpuPerDevice)) 52 | Expect(targetConfig.DefaultRamPerDevice).To(Equal(testConfig.DefaultRamPerDevice)) 53 | Expect(targetConfig.DefaultGpuPerDevice).To(Equal(testConfig.DefaultGpuPerDevice)) 54 | Expect(targetConfig.MySqlConfig.Address).To(Equal("mysql.svc.cluster.local")) 55 | Expect(targetConfig.MySqlConfig.Port).To(Equal(uint16(3306))) 56 | Expect(targetConfig.MySqlConfig.Username).To(Equal("root")) 57 | Expect(targetConfig.MySqlConfig.Password).To(Equal("root")) 58 | Expect(targetConfig.MySqlConfig.DataBaseName).To(Equal("open-hydra")) 59 | Expect(targetConfig.MySqlConfig.Protocol).To(Equal("tcp")) 60 | Expect(targetConfig.MySqlConfig.Character).To(Equal("utf8mb4")) 61 | Expect(targetConfig.MySqlConfig.Collation).To(Equal("utf8mb4_general_ci")) 62 | Expect(targetConfig.EtcdConfig).To(Equal(DefaultEtcdConfig())) 63 | Expect(targetConfig.MaximumPortsPerSandbox).To(Equal(uint8(3))) 64 | }) 65 | It("test default value will not overwrite by empty value ", func() { 66 | err := WriteConfig(configFile2, &testConfig2) 67 | Expect(err).To(BeNil()) 68 | targetConfig, err := LoadConfig(configFile2, kubeConfigPath) 69 | Expect(err).To(BeNil()) 70 | Expect(targetConfig.LeaderElection).To(Equal(testConfig.LeaderElection)) 71 | Expect(targetConfig.AuthDelegateConfig).To(BeNil()) 72 | Expect(targetConfig.MySqlConfig.Character).To(Equal("utf8mb4")) 73 | Expect(targetConfig.MySqlConfig.Collation).To(Equal("utf8mb4_general_ci")) 74 | Expect(len(targetConfig.GpuResourceKeys)).To(Equal(2)) 75 | Expect(targetConfig.GpuResourceKeys[0]).To(Equal("nvidia.com/gpu")) 76 | Expect(targetConfig.GpuResourceKeys[1]).To(Equal("amd.com/gpu")) 77 | }) 78 | AfterEach(func() { 79 | _ = util.DeleteFile(configFile) 80 | _ = util.DeleteFile(configFile2) 81 | }) 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /pkg/apiserver/apiserver.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net" 7 | 8 | "open-hydra/cmd/open-hydra-server/app/config" 9 | "open-hydra/cmd/open-hydra-server/app/option" 10 | openHydraDeviceV1 "open-hydra/pkg/apis/open-hydra-api/device/core/v1" 11 | openHydraUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 12 | openHydraOpenApi "open-hydra/pkg/generated/apis/openapi" 13 | 14 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/apimachinery/pkg/runtime/schema" 16 | utilRuntime "k8s.io/apimachinery/pkg/util/runtime" 17 | "k8s.io/apiserver/pkg/endpoints/openapi" 18 | genericApiServer "k8s.io/apiserver/pkg/server" 19 | ) 20 | 21 | func init() { 22 | utilRuntime.Must(openHydraUserV1.AddToScheme(option.Scheme)) 23 | utilRuntime.Must(openHydraDeviceV1.AddToScheme(option.Scheme)) 24 | 25 | // Setting VersionPriority is critical in the InstallAPIGroup call (done in New()) 26 | utilRuntime.Must(option.Scheme.SetVersionPriority(option.GroupVersion)) 27 | metaV1.AddToGroupVersion(option.Scheme, option.GroupVersion) 28 | 29 | // add a comment for ci test 30 | // TODO(devdattakulkarni) -- Following comments coming from sample-apiserver. 31 | // Leaving them for now. 32 | // TODO: keep the generic API server from wanting this 33 | unVersioned := schema.GroupVersion{Group: "", Version: "v1"} 34 | option.Scheme.AddUnversionedTypes(unVersioned, 35 | &metaV1.Status{}, 36 | &metaV1.APIVersions{}, 37 | &metaV1.APIGroupList{}, 38 | &metaV1.APIGroup{}, 39 | &metaV1.APIResourceList{}, 40 | ) 41 | 42 | // Start collecting provenance 43 | //go provenance.CollectProvenance() 44 | } 45 | 46 | func RunApiServer(recommendOption *option.Options, config *config.OpenHydraServerConfig, stopChan <-chan struct{}) error { 47 | if err := recommendOption.ApiServerOption.SecureServing.MaybeDefaultWithSelfSignedCerts("0.0.0.0", nil, []net.IP{net.ParseIP("0.0.0.0")}); err != nil { 48 | return err 49 | } 50 | recommendedConfig := genericApiServer.NewRecommendedConfig(option.Codecs) 51 | 52 | if err := recommendOption.ApiServerOption.ApplyTo(recommendedConfig); err != nil { 53 | return err 54 | } 55 | 56 | completedConfig := recommendedConfig.Complete() 57 | completedConfig.EnableDiscovery = false 58 | completedConfig.OpenAPIConfig = genericApiServer.DefaultOpenAPIConfig(openHydraOpenApi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(option.Scheme)) 59 | completedConfig.OpenAPIConfig.Info.Title = "open-hydra-server.openhydra.io" 60 | completedConfig.OpenAPIConfig.Info.Version = "2" 61 | completedConfig.OpenAPIV3Config = genericApiServer.DefaultOpenAPIV3Config(openHydraOpenApi.GetOpenAPIDefinitions, openapi.NewDefinitionNamer(option.Scheme)) 62 | completedConfig.OpenAPIV3Config.Info.Title = "open-hydra-server.openhydra.io" 63 | completedConfig.OpenAPIV3Config.Info.Version = "3" 64 | 65 | gApiServer, err := completedConfig.New("open-hydra api-server", genericApiServer.NewEmptyDelegate()) 66 | if err != nil { 67 | slog.Error("Failed to create genericApiServer", "error", err) 68 | return err 69 | 70 | } 71 | 72 | installApiGroup := genericApiServer.NewDefaultAPIGroupInfo(option.GroupVersion.Group, option.Scheme, metaV1.ParameterCodec, option.Codecs) 73 | 74 | err = gApiServer.InstallAPIGroup(&installApiGroup) 75 | if err != nil { 76 | slog.Error(fmt.Sprintf("Failed to install API group: %v", err)) 77 | return err 78 | } 79 | 80 | // register the discovery service 81 | registerDiscoveryService(gApiServer) 82 | // register the api resource 83 | err = registerApiResource(gApiServer, config, stopChan) 84 | if err != nil { 85 | slog.Error("Failed to register api resource", "error", err) 86 | return err 87 | } 88 | 89 | return gApiServer.PrepareRun().Run(stopChan) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/setting/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Setting) DeepCopyInto(out *Setting) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Setting. 38 | func (in *Setting) DeepCopy() *Setting { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Setting) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Setting) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *SettingList) DeepCopyInto(out *SettingList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]SettingSpec, len(*in)) 63 | copy(*out, *in) 64 | } 65 | return 66 | } 67 | 68 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingList. 69 | func (in *SettingList) DeepCopy() *SettingList { 70 | if in == nil { 71 | return nil 72 | } 73 | out := new(SettingList) 74 | in.DeepCopyInto(out) 75 | return out 76 | } 77 | 78 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 79 | func (in *SettingList) DeepCopyObject() runtime.Object { 80 | if c := in.DeepCopy(); c != nil { 81 | return c 82 | } 83 | return nil 84 | } 85 | 86 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 87 | func (in *SettingSpec) DeepCopyInto(out *SettingSpec) { 88 | *out = *in 89 | return 90 | } 91 | 92 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingSpec. 93 | func (in *SettingSpec) DeepCopy() *SettingSpec { 94 | if in == nil { 95 | return nil 96 | } 97 | out := new(SettingSpec) 98 | in.DeepCopyInto(out) 99 | return out 100 | } 101 | 102 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 103 | func (in *SettingStatus) DeepCopyInto(out *SettingStatus) { 104 | *out = *in 105 | return 106 | } 107 | 108 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SettingStatus. 109 | func (in *SettingStatus) DeepCopy() *SettingStatus { 110 | if in == nil { 111 | return nil 112 | } 113 | out := new(SettingStatus) 114 | in.DeepCopyInto(out) 115 | return out 116 | } 117 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/summary/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *SumUp) DeepCopyInto(out *SumUp) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SumUp. 38 | func (in *SumUp) DeepCopy() *SumUp { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(SumUp) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *SumUp) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *SumUpList) DeepCopyInto(out *SumUpList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]SumUp, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SumUpList. 71 | func (in *SumUpList) DeepCopy() *SumUpList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(SumUpList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *SumUpList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *SumUpSpec) DeepCopyInto(out *SumUpSpec) { 90 | *out = *in 91 | return 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SumUpSpec. 95 | func (in *SumUpSpec) DeepCopy() *SumUpSpec { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(SumUpSpec) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *SumUpStatus) DeepCopyInto(out *SumUpStatus) { 106 | *out = *in 107 | return 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SumUpStatus. 111 | func (in *SumUpStatus) DeepCopy() *SumUpStatus { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(SumUpStatus) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | -------------------------------------------------------------------------------- /pkg/open-hydra/setting.go: -------------------------------------------------------------------------------- 1 | package openhydra 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | xSetting "open-hydra/pkg/apis/open-hydra-api/setting/core/v1" 7 | 8 | "open-hydra/pkg/util" 9 | 10 | "github.com/emicklei/go-restful/v3" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | func (builder *OpenHydraRouteBuilder) AddGetSettingRoute() { 15 | path := "/" + SettingPath + "/{name}" 16 | builder.addPathAuthorization(path, http.MethodGet, 1) 17 | builder.RootWS.Route(builder.RootWS.GET(path).Operation("getSetting").To(builder.GetSettingRouteHandler). 18 | Returns(http.StatusInternalServerError, "internal server error", ""). 19 | Returns(http.StatusBadRequest, "bad request", ""). 20 | Returns(http.StatusOK, "OK", xSetting.Setting{})) 21 | } 22 | 23 | func (builder *OpenHydraRouteBuilder) GetSettingRouteHandler(request *restful.Request, response *restful.Response) { 24 | serverConfig, err := builder.GetServerConfigFromConfigMap() 25 | if err != nil { 26 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, 27 | fmt.Sprintf("Failed to get server config: %v", err)) 28 | return 29 | } 30 | 31 | result := xSetting.Setting{} 32 | util.FillKindAndApiVersion(&result.TypeMeta, SettingKind) 33 | result.Name = request.PathParameter("name") 34 | result.Spec = xSetting.SettingSpec{} 35 | result.Spec.DefaultGpuPerDevice = serverConfig.DefaultGpuPerDevice 36 | // now get all plugins from configmap 37 | cm, err := builder.k8sHelper.GetConfigMap("openhydra-plugin", OpenhydraNamespace) 38 | if err != nil { 39 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to get configmap: %v", err)) 40 | return 41 | } 42 | 43 | plugins, err := ParseJsonToPluginList(cm.Data["plugins"]) 44 | if err != nil { 45 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to unmarshal json: %v", err)) 46 | return 47 | } 48 | result.Spec.PluginList = plugins 49 | response.WriteHeaderAndEntity(http.StatusOK, result) 50 | } 51 | 52 | func (builder *OpenHydraRouteBuilder) AddUpdateSettingRoute() { 53 | path := "/" + SettingPath + "/{name}" 54 | builder.addPathAuthorization(path, http.MethodPut, 1) 55 | builder.RootWS.Route(builder.RootWS.PUT(path).Operation("createSetting").To(builder.UpdateSettingRouteHandler). 56 | Returns(http.StatusInternalServerError, "internal server error", ""). 57 | Returns(http.StatusBadRequest, "bad request", ""). 58 | Returns(http.StatusOK, "OK", xSetting.Setting{})) 59 | } 60 | 61 | func (builder *OpenHydraRouteBuilder) UpdateSettingRouteHandler(request *restful.Request, response *restful.Response) { 62 | setting := xSetting.Setting{} 63 | err := request.ReadEntity(&setting) 64 | if err != nil { 65 | writeHttpResponseAndLogError(response, http.StatusBadRequest, fmt.Sprintf("Failed to read request entity: %v", err)) 66 | return 67 | } 68 | setting.Name = request.PathParameter("name") 69 | 70 | serverConfig, err := builder.GetServerConfigFromConfigMap() 71 | if err != nil { 72 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, 73 | fmt.Sprintf("Failed to get server config: %v", err)) 74 | return 75 | } 76 | 77 | util.FillKindAndApiVersion(&setting.TypeMeta, SettingKind) 78 | serverConfig.DefaultGpuPerDevice = setting.Spec.DefaultGpuPerDevice 79 | 80 | configJson, err := yaml.Marshal(serverConfig) 81 | if err != nil { 82 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to marshal yaml: %v", err)) 83 | return 84 | } 85 | 86 | err = builder.k8sHelper.UpdateConfigMap("open-hydra-config", OpenhydraNamespace, map[string]string{"config.yaml": string(configJson)}) 87 | if err != nil { 88 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Failed to update configmap: %v", err)) 89 | return 90 | } 91 | 92 | response.WriteHeaderAndEntity(http.StatusOK, setting) 93 | } 94 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/device/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Device) DeepCopyInto(out *Device) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Device. 38 | func (in *Device) DeepCopy() *Device { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Device) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Device) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *DeviceList) DeepCopyInto(out *DeviceList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]Device, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceList. 71 | func (in *DeviceList) DeepCopy() *DeviceList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(DeviceList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *DeviceList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *DeviceSpec) DeepCopyInto(out *DeviceSpec) { 90 | *out = *in 91 | return 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceSpec. 95 | func (in *DeviceSpec) DeepCopy() *DeviceSpec { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(DeviceSpec) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *DeviceStatus) DeepCopyInto(out *DeviceStatus) { 106 | *out = *in 107 | return 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceStatus. 111 | func (in *DeviceStatus) DeepCopy() *DeviceStatus { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(DeviceStatus) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/course/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Course) DeepCopyInto(out *Course) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Course. 38 | func (in *Course) DeepCopy() *Course { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Course) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Course) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *CourseList) DeepCopyInto(out *CourseList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]Course, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CourseList. 71 | func (in *CourseList) DeepCopy() *CourseList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(CourseList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *CourseList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *CourseSpec) DeepCopyInto(out *CourseSpec) { 90 | *out = *in 91 | in.LastUpdate.DeepCopyInto(&out.LastUpdate) 92 | return 93 | } 94 | 95 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CourseSpec. 96 | func (in *CourseSpec) DeepCopy() *CourseSpec { 97 | if in == nil { 98 | return nil 99 | } 100 | out := new(CourseSpec) 101 | in.DeepCopyInto(out) 102 | return out 103 | } 104 | 105 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 106 | func (in *CourseStatus) DeepCopyInto(out *CourseStatus) { 107 | *out = *in 108 | return 109 | } 110 | 111 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CourseStatus. 112 | func (in *CourseStatus) DeepCopy() *CourseStatus { 113 | if in == nil { 114 | return nil 115 | } 116 | out := new(CourseStatus) 117 | in.DeepCopyInto(out) 118 | return out 119 | } 120 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/dataset/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *Dataset) DeepCopyInto(out *Dataset) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | in.Spec.DeepCopyInto(&out.Spec) 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Dataset. 38 | func (in *Dataset) DeepCopy() *Dataset { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(Dataset) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *Dataset) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *DatasetList) DeepCopyInto(out *DatasetList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]Dataset, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetList. 71 | func (in *DatasetList) DeepCopy() *DatasetList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(DatasetList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *DatasetList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *DatasetSpec) DeepCopyInto(out *DatasetSpec) { 90 | *out = *in 91 | in.LastUpdate.DeepCopyInto(&out.LastUpdate) 92 | return 93 | } 94 | 95 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetSpec. 96 | func (in *DatasetSpec) DeepCopy() *DatasetSpec { 97 | if in == nil { 98 | return nil 99 | } 100 | out := new(DatasetSpec) 101 | in.DeepCopyInto(out) 102 | return out 103 | } 104 | 105 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 106 | func (in *DatasetStatus) DeepCopyInto(out *DatasetStatus) { 107 | *out = *in 108 | return 109 | } 110 | 111 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DatasetStatus. 112 | func (in *DatasetStatus) DeepCopy() *DatasetStatus { 113 | if in == nil { 114 | return nil 115 | } 116 | out := new(DatasetStatus) 117 | in.DeepCopyInto(out) 118 | return out 119 | } 120 | -------------------------------------------------------------------------------- /pkg/apis/open-hydra-api/user/core/v1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright 2022. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-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, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | // Code generated by deepcopy-gen. DO NOT EDIT. 20 | 21 | package v1 22 | 23 | import ( 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | ) 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *OpenHydraUser) DeepCopyInto(out *OpenHydraUser) { 29 | *out = *in 30 | out.TypeMeta = in.TypeMeta 31 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 32 | out.Spec = in.Spec 33 | out.Status = in.Status 34 | return 35 | } 36 | 37 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenHydraUser. 38 | func (in *OpenHydraUser) DeepCopy() *OpenHydraUser { 39 | if in == nil { 40 | return nil 41 | } 42 | out := new(OpenHydraUser) 43 | in.DeepCopyInto(out) 44 | return out 45 | } 46 | 47 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 48 | func (in *OpenHydraUser) DeepCopyObject() runtime.Object { 49 | if c := in.DeepCopy(); c != nil { 50 | return c 51 | } 52 | return nil 53 | } 54 | 55 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 56 | func (in *OpenHydraUserList) DeepCopyInto(out *OpenHydraUserList) { 57 | *out = *in 58 | out.TypeMeta = in.TypeMeta 59 | in.ListMeta.DeepCopyInto(&out.ListMeta) 60 | if in.Items != nil { 61 | in, out := &in.Items, &out.Items 62 | *out = make([]OpenHydraUser, len(*in)) 63 | for i := range *in { 64 | (*in)[i].DeepCopyInto(&(*out)[i]) 65 | } 66 | } 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenHydraUserList. 71 | func (in *OpenHydraUserList) DeepCopy() *OpenHydraUserList { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(OpenHydraUserList) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *OpenHydraUserList) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *OpenHydraUserSpec) DeepCopyInto(out *OpenHydraUserSpec) { 90 | *out = *in 91 | return 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenHydraUserSpec. 95 | func (in *OpenHydraUserSpec) DeepCopy() *OpenHydraUserSpec { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(OpenHydraUserSpec) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *OpenHydraUserStatus) DeepCopyInto(out *OpenHydraUserStatus) { 106 | *out = *in 107 | return 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenHydraUserStatus. 111 | func (in *OpenHydraUserStatus) DeepCopy() *OpenHydraUserStatus { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(OpenHydraUserStatus) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | -------------------------------------------------------------------------------- /pkg/util/utils.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net" 12 | "net/http" 13 | "os" 14 | "strconv" 15 | "time" 16 | 17 | "github.com/emicklei/go-restful" 18 | ) 19 | 20 | func ReadTxtFile(url string) ([]byte, error) { 21 | file, err := os.Open(url) 22 | if err != nil { 23 | return []byte(""), err 24 | } 25 | defer file.Close() 26 | stringData, err := io.ReadAll(file) 27 | if err != nil { 28 | return []byte(""), err 29 | } 30 | return stringData, nil 31 | } 32 | 33 | // write file 34 | func WriteFileWithNosec(pathName string, data []byte) error { 35 | // #nosec G306, Expect WriteFile permissions to be 0600 or less 36 | return os.WriteFile(pathName, data, 0644) 37 | } 38 | 39 | func CreateDirIfNotExists(dirLocation string) error { 40 | if _, err := os.Stat(dirLocation); os.IsNotExist(err) { 41 | return os.MkdirAll(dirLocation, os.ModeDir|0755) 42 | } 43 | return nil 44 | } 45 | 46 | func DeleteDirs(filePath string) error { 47 | return os.RemoveAll(filePath) 48 | } 49 | 50 | func DeleteFile(filePath string) error { 51 | return os.Remove(filePath) 52 | } 53 | 54 | func CommonRequest(requestUrl, httpMethod, nameServer string, postBody json.RawMessage, header map[string]string, skipTlsCheck, disableKeepAlive bool, timeout time.Duration) ([]byte, http.Header, int, error) { 55 | var req *http.Request 56 | var reqErr error 57 | 58 | req, reqErr = http.NewRequest(httpMethod, requestUrl, bytes.NewReader(postBody)) 59 | if reqErr != nil { 60 | return []byte{}, nil, http.StatusInternalServerError, reqErr 61 | } 62 | req.Header.Set("Content-Type", "application/json; charset=UTF-8") 63 | for key, val := range header { 64 | req.Header.Set(key, val) 65 | } 66 | client := &http.Client{ 67 | Timeout: 1000 * time.Second, 68 | } 69 | // client := &http.Client{ 70 | // Timeout: timeout, 71 | // } 72 | tr := &http.Transport{ 73 | DisableKeepAlives: disableKeepAlive, 74 | } 75 | if skipTlsCheck { 76 | tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 77 | } 78 | 79 | if nameServer != "" { 80 | r := &net.Resolver{ 81 | PreferGo: true, 82 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 83 | d := net.Dialer{Timeout: 1 * time.Second} 84 | return d.DialContext(ctx, "udp", nameServer) 85 | }, 86 | } 87 | tr.DialContext = (&net.Dialer{ 88 | Resolver: r, 89 | }).DialContext 90 | } 91 | client.Transport = tr 92 | resp, respErr := client.Do(req) 93 | if respErr != nil { 94 | return []byte{}, nil, http.StatusInternalServerError, respErr 95 | } 96 | defer resp.Body.Close() 97 | body, readBodyErr := io.ReadAll(resp.Body) 98 | if readBodyErr != nil { 99 | return []byte{}, nil, http.StatusInternalServerError, readBodyErr 100 | } 101 | return body, resp.Header, resp.StatusCode, nil 102 | } 103 | 104 | func StartMockServer(port int, handlerLoader func(*restful.WebService), stopChan chan struct{}) error { 105 | 106 | svcContainer := restful.NewContainer() 107 | ws2 := new(restful.WebService) 108 | if handlerLoader != nil { 109 | handlerLoader(ws2) 110 | } 111 | 112 | svcContainer.Add(ws2) 113 | 114 | httpServer := http.Server{ 115 | Handler: svcContainer, 116 | } 117 | 118 | httpServer.Addr = ":" + strconv.Itoa(port) 119 | err := httpServer.ListenAndServe() 120 | if err != nil { 121 | return err 122 | } 123 | <-stopChan 124 | httpServer.Close() 125 | return nil 126 | } 127 | 128 | func GetStringValueOrDefault(targetDescription, targetValue, defaultValue string) string { 129 | slog.Debug(fmt.Sprintf("'%s' is not set fall backup to default value: %s", targetDescription, defaultValue)) 130 | if targetValue != "" { 131 | return targetValue 132 | } 133 | return defaultValue 134 | } 135 | 136 | func CopyFile(src, dst string) error { 137 | sourceFile, err := os.Open(src) 138 | if err != nil { 139 | return err 140 | } 141 | defer sourceFile.Close() 142 | 143 | destinationFile, err := os.Create(dst) 144 | if err != nil { 145 | return err 146 | } 147 | defer destinationFile.Close() 148 | 149 | _, err = io.Copy(destinationFile, sourceFile) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | return nil 155 | } -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "open-hydra/cmd/open-hydra-server/app/option" 9 | xDeviceV1 "open-hydra/pkg/apis/open-hydra-api/device/core/v1" 10 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 11 | "time" 12 | 13 | "github.com/emicklei/go-restful" 14 | . "github.com/onsi/ginkgo/v2" 15 | . "github.com/onsi/gomega" 16 | ) 17 | 18 | type TestStruct struct { 19 | Name string 20 | TotalCount int 21 | } 22 | 23 | var testRouter = func(ws *restful.WebService) { 24 | ws.Route(ws.GET("/route1-test").To(func(request *restful.Request, response *restful.Response) { 25 | response.AddHeader("test", "test") 26 | response.WriteAsJson(&TestStruct{Name: "test", TotalCount: 2}) 27 | })) 28 | } 29 | 30 | var _ = Describe("open-hydra-server util test", func() { 31 | BeforeEach(func() { 32 | }) 33 | Describe("FillKinkAndApiVersion test", func() { 34 | It("should be expected", func() { 35 | device := xDeviceV1.Device{} 36 | FillKindAndApiVersion(&device.TypeMeta, "OpenHydraUser") 37 | Expect(device.Kind).To(Equal("OpenHydraUser")) 38 | Expect(device.APIVersion).To(Equal(fmt.Sprintf("%s/%s", option.GroupVersion.Group, option.GroupVersion.Version))) 39 | }) 40 | 41 | AfterEach(func() { 42 | }) 43 | }) 44 | 45 | Describe("FillObjectGVK test", func() { 46 | It("should be expected", func() { 47 | xUser := &xUserV1.OpenHydraUser{} 48 | FillObjectGVK(xUser) 49 | Expect(xUser.APIVersion, fmt.Sprintf("%s/%s", option.GroupVersion.Group, option.GroupVersion.Version)) 50 | Expect(xUser.Kind, "OpenHydraUser") 51 | }) 52 | AfterEach(func() { 53 | }) 54 | }) 55 | 56 | Describe("FillObjectGVK test", func() { 57 | It("should be expected", func() { 58 | xUser := &xUserV1.OpenHydraUser{} 59 | FillObjectGVK(xUser) 60 | Expect(xUser.APIVersion, fmt.Sprintf("%s/%s", option.GroupVersion.Group, option.GroupVersion.Version)) 61 | Expect(xUser.Kind, "OpenHydraUser") 62 | }) 63 | AfterEach(func() { 64 | }) 65 | }) 66 | 67 | Describe("ZipDir test", func() { 68 | It("should be expected", func() { 69 | err := CreateDirIfNotExists("/tmp/test") 70 | Expect(err).To(BeNil()) 71 | err = CreateDirIfNotExists("/tmp/test/test1") 72 | Expect(err).To(BeNil()) 73 | err = WriteFileWithNosec("/tmp/test/test1/test.txt", []byte("test")) 74 | Expect(err).To(BeNil()) 75 | err = ZipDir("/tmp/test", "/tmp/test.zip") 76 | Expect(err).To(BeNil()) 77 | }) 78 | AfterEach(func() { 79 | DeleteDirs("/tmp/test") 80 | DeleteFile("/tmp/test.zip") 81 | }) 82 | }) 83 | 84 | Describe("CommonRequest test", func() { 85 | It("should be expected", func() { 86 | stopChan := make(chan struct{}, 1) 87 | go StartMockServer(20080, testRouter, stopChan) 88 | time.Sleep(2 * time.Second) 89 | defer func() { 90 | stopChan <- struct{}{} 91 | }() 92 | rawResult, header, httpCode, err := CommonRequest("http://localhost:20080/route1-test", http.MethodGet, "", json.RawMessage{}, map[string]string{}, true, true, 3*time.Second) 93 | Expect(err).To(BeNil()) 94 | Expect(httpCode).To(Equal(http.StatusOK)) 95 | var result TestStruct 96 | err = json.Unmarshal(rawResult, &result) 97 | Expect(err).To(BeNil()) 98 | Expect(result.Name).To(Equal("test")) 99 | Expect(result.TotalCount).To(Equal(2)) 100 | headerValue := header.Get("test") 101 | Expect(headerValue).To(Equal("test")) 102 | }) 103 | AfterEach(func() { 104 | }) 105 | }) 106 | 107 | Describe("StartMockServer test", func() { 108 | It("should be expected", func() { 109 | stopChan := make(chan struct{}, 1) 110 | go StartMockServer(28080, testRouter, stopChan) 111 | time.Sleep(2 * time.Second) 112 | defer close(stopChan) 113 | // code check tcp port 114 | conn, err := net.DialTimeout("tcp", "localhost:28080", 3*time.Second) 115 | Expect(err).To(BeNil()) 116 | defer conn.Close() 117 | }) 118 | AfterEach(func() { 119 | }) 120 | }) 121 | 122 | Describe("GetStringValueOrDefault test", func() { 123 | It("should be expected", func() { 124 | value := GetStringValueOrDefault("test", "test", "default") 125 | Expect(value).To(Equal("test")) 126 | value = GetStringValueOrDefault("", "", "default") 127 | Expect(value).To(Equal("default")) 128 | }) 129 | }) 130 | 131 | }) 132 | -------------------------------------------------------------------------------- /pkg/apiserver/service-register.go: -------------------------------------------------------------------------------- 1 | package apiserver 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/http" 7 | 8 | "open-hydra/cmd/open-hydra-server/app/config" 9 | "open-hydra/cmd/open-hydra-server/app/option" 10 | "open-hydra/pkg/database" 11 | openHydraHandler "open-hydra/pkg/open-hydra" 12 | openHydraK8s "open-hydra/pkg/open-hydra/k8s" 13 | 14 | "github.com/emicklei/go-restful/v3" 15 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | genericApiServer "k8s.io/apiserver/pkg/server" 17 | "k8s.io/client-go/kubernetes" 18 | ) 19 | 20 | const ( 21 | httpStatusNotFoundMessage = "Not Found" 22 | ) 23 | 24 | func registerDiscoveryService(apiServer *genericApiServer.GenericAPIServer) { 25 | ws := getWebService() 26 | ws.Path("/apis").Consumes(restful.MIME_JSON, restful.MIME_XML).Produces(restful.MIME_JSON, restful.MIME_XML) 27 | ws.Route(ws.GET("/").Operation("getApis").To(func(request *restful.Request, response *restful.Response) { 28 | apiGroup := metaV1.APIGroup{ 29 | Name: option.GroupVersion.Group, 30 | PreferredVersion: metaV1.GroupVersionForDiscovery{ 31 | GroupVersion: option.GroupVersion.Group + "/" + option.GroupVersion.Version, 32 | Version: option.GroupVersion.Version, 33 | }, 34 | } 35 | apiGroup.Versions = append(apiGroup.Versions, metaV1.GroupVersionForDiscovery{ 36 | GroupVersion: option.GroupVersion.Group + "/" + option.GroupVersion.Version, 37 | Version: option.GroupVersion.Version, 38 | }) 39 | apiGroup.ServerAddressByClientCIDRs = append(apiGroup.ServerAddressByClientCIDRs, metaV1.ServerAddressByClientCIDR{ 40 | ClientCIDR: "0.0.0.0/0", 41 | ServerAddress: "", 42 | }) 43 | apiGroup.Kind = "APIGroup" 44 | response.WriteAsJson(apiGroup) 45 | }).Returns(http.StatusOK, "OK", metaV1.APIGroup{}).Returns(http.StatusNotFound, httpStatusNotFoundMessage, "")) 46 | apiServer.Handler.GoRestfulContainer.Add(ws) 47 | } 48 | 49 | func registerApiResource(apiServer *genericApiServer.GenericAPIServer, config *config.OpenHydraServerConfig, stopChan <-chan struct{}) error { 50 | ws := getWebService() 51 | resourceIndexPathTemplate := "/apis/%s/%s" 52 | resourceIndexPath := fmt.Sprintf(resourceIndexPathTemplate, option.GroupVersion.Group, option.GroupVersion.Version) 53 | ws.Path(resourceIndexPath).Consumes(restful.MIME_JSON, restful.MIME_XML).Produces(restful.MIME_JSON, restful.MIME_XML) 54 | ws.Route(ws.GET("/").Operation("getAllKinds").To(func(request *restful.Request, response *restful.Response) { 55 | list := &metaV1.APIResourceList{} 56 | 57 | list.Kind = "APIResourceList" 58 | list.GroupVersion = option.GroupVersion.Group + "/" + option.GroupVersion.Version 59 | list.APIVersion = option.GroupVersion.Version 60 | list.APIResources = openHydraHandler.ApiResources() 61 | response.WriteAsJson(list) 62 | }).Returns(http.StatusOK, "OK", metaV1.APIResource{}).Returns(http.StatusNotFound, httpStatusNotFoundMessage, "")) 63 | 64 | var db database.IDataBase 65 | switch config.DBType { 66 | case "mysql": 67 | db = database.NewMysql(config) 68 | case "etcd": 69 | db = &database.Etcd{Config: config} 70 | default: 71 | return fmt.Errorf("unknown db type %s", config.DBType) 72 | } 73 | 74 | err := db.InitDb() 75 | if err != nil { 76 | slog.Error("Failed to init db", "error", err) 77 | return err 78 | } 79 | 80 | kubeClient, err := kubernetes.NewForConfig(config.KubeConfig) 81 | if err != nil { 82 | slog.Error("Failed to create kube client", "error", err) 83 | return err 84 | } 85 | 86 | RBuilder := openHydraHandler.NewOpenHydraRouteBuilder(db, ws, kubeClient, openHydraK8s.NewDefaultK8sHelper(kubeClient, stopChan), config) 87 | RBuilder.AddXUserListRoute() 88 | RBuilder.AddXUserCreateRoute() 89 | RBuilder.AddXUserGetRoute() 90 | RBuilder.AddXUserUpdateRoute() 91 | RBuilder.AddXUserDeleteRoute() 92 | RBuilder.AddDeviceListRoute() 93 | RBuilder.AddDeviceCreateRoute() 94 | RBuilder.AddDeviceGetRoute() 95 | RBuilder.AddDeviceUpdateRoute() 96 | RBuilder.AddDeviceDeleteRoute() 97 | RBuilder.AddSummaryGetRoute() 98 | RBuilder.AddDatasetListRoute() 99 | RBuilder.AddDatasetCreateRoute() 100 | RBuilder.AddDatasetGetRoute() 101 | RBuilder.AddDatasetUpdateRoute() 102 | RBuilder.AddDatasetDeleteRoute() 103 | RBuilder.AddXUserLoginRoute() 104 | RBuilder.AddGetSettingRoute() 105 | RBuilder.AddUpdateSettingRoute() 106 | RBuilder.AddCourseListRoute() 107 | RBuilder.AddCourseGetRoute() 108 | RBuilder.AddCourseCreateRoute() 109 | RBuilder.AddCourseUpdateRoute() 110 | RBuilder.AddCourseDeleteRoute() 111 | if !config.DisableAuth { 112 | ws.Filter(RBuilder.Filter) 113 | } 114 | apiServer.Handler.GoRestfulContainer.Add(ws) 115 | return nil 116 | } 117 | 118 | func getWebService() *restful.WebService { 119 | ws := new(restful.WebService) 120 | ws.Path("/apis") 121 | ws.Consumes("*/*") 122 | ws.Produces(restful.MIME_JSON, restful.MIME_XML) 123 | ws.ApiVersion(option.GroupVersion.Group) 124 | return ws 125 | } 126 | -------------------------------------------------------------------------------- /.github/workflows/e2e-test-on-pr.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: E2e-test 5 | 6 | on: 7 | pull_request: 8 | branches: ["main"] 9 | paths: 10 | - "**.go" 11 | 12 | jobs: 13 | create-cluster: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Create k8s Kind Cluster 17 | uses: helm/kind-action@v1 18 | 19 | - name: checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Deploy local path sc 25 | run: | 26 | mkdir -p /opt/local-path-provisioner 27 | kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.26/deploy/local-path-storage.yaml 28 | sleep 5 29 | kubectl get sc 30 | kubectl describe node 31 | 32 | - name: Deploy mysql 33 | run: | 34 | kubectl create -f deploy/mysql-operator-crds.yaml 35 | kubectl create -f deploy/mysql-operator.yaml 36 | kubectl wait --for=condition=available --timeout=120s deployment/mysql-operator -n mysql-operator 37 | echo "wait 30 sec for mysql operator" 38 | sleep 30 39 | kubectl create -f deploy/mysql-instance.yaml 40 | echo "wait 60 sec for pod of mysql instance to show up" 41 | sleep 60 42 | kubectl get pods -n mysql-operator 43 | echo "Describe pod of mysql instance" 44 | kubectl describe pods mycluster-0 -n mysql-operator 45 | echo "Print log of container fixdatadir" 46 | kubectl logs mycluster-0 -n mysql-operator -c fixdatadir 47 | echo "Print log of container initconf" 48 | kubectl logs mycluster-0 -n mysql-operator -c initconf 49 | echo "Print log of container initmysql" 50 | kubectl logs mycluster-0 -n mysql-operator -c initmysql 51 | kubectl wait pods mycluster-0 -n mysql-operator --for condition=Ready --timeout=120s 52 | 53 | - name: Deploy openhydra 54 | run: | 55 | mkdir /mnt/public-dataset 56 | mkdir /mnt/public-course 57 | mkdir /mnt/jupyter-lab 58 | mkdir /mnt/public-vscode 59 | sed -i 's/defaultCpuPerDevice: 2000/defaultCpuPerDevice: 1000/g' deploy/install-open-hydra.yaml 60 | sed -i 's/defaultRamPerDevice: 8192/defaultRamPerDevice: 2048/g' deploy/install-open-hydra.yaml 61 | kubectl create -f deploy/install-open-hydra.yaml 62 | kubectl wait --for=condition=available --timeout=180s deployment/open-hydra-server -n open-hydra 63 | 64 | - name: Test openhydra 65 | run: | 66 | echo "Attempting to test openhydra..." 67 | echo "Creating user-admin..." 68 | kubectl create -f deploy/user-admin.yaml 69 | result=$(kubectl get openhydrausers admin | wc -l) 70 | if [ "$result" != 2 ]; then 71 | echo "Failed to create user-admin..." 72 | exit -1 73 | fi 74 | echo "Creating device for admin..." 75 | kubectl create -f deploy/device-jupyter-admin.yaml 76 | kubectl wait --for=condition=available --timeout=600s deployment/openhydra-deploy-admin -n open-hydra 77 | echo "Waiting 10 seconds for the device to be ready..." 78 | sleep 10 79 | address=$(kubectl get devices admin -o jsonpath="{@.spec.jupyterLabUrl}") 80 | result=$(curl -s -o /dev/null -w "%{http_code}" $address) 81 | if [ "$result" != 302 ]; then 82 | echo "Failed to create device for admin..." 83 | exit -1 84 | fi 85 | echo "Deleting device for admin..." 86 | kubectl delete -f deploy/device-jupyter-admin.yaml 87 | kubectl wait --for=delete --timeout=600s deployment/openhydra-deploy-admin -n open-hydra 88 | # wait 90s for the pod to release the resources 89 | echo "Waiting 90 seconds for the device to be deleted..." 90 | sleep 90 91 | echo "Creating vscode device for admin..." 92 | kubectl create -f deploy/device-vscode-admin.yaml 93 | kubectl wait --for=condition=available --timeout=600s deployment/openhydra-deploy-admin -n open-hydra 94 | echo "Waiting 10 seconds for the device to be ready..." 95 | sleep 10 96 | address=$(kubectl get devices admin -o jsonpath="{@.spec.vsCodeUrl}") 97 | result=$(curl -s -o /dev/null -w "%{http_code}" $address) 98 | if [ "$result" != 200 ]; then 99 | exit -1 100 | fi 101 | echo "Deleting vscode device for admin..." 102 | kubectl delete -f deploy/device-vscode-admin.yaml 103 | kubectl wait --for=delete --timeout=600s deployment/openhydra-deploy-admin -n open-hydra 104 | echo "Test passed..." 105 | -------------------------------------------------------------------------------- /pkg/database/auth-plugin/mysql-default.go: -------------------------------------------------------------------------------- 1 | package auth_plugin 2 | 3 | import ( 4 | "database/sql" 5 | stdErr "errors" 6 | "fmt" 7 | "log/slog" 8 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 9 | "open-hydra/pkg/util" 10 | 11 | "k8s.io/apimachinery/pkg/api/errors" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | ) 14 | 15 | type DefaultMysqlAuthPlugin struct { 16 | Db func() (*sql.DB, error) 17 | } 18 | 19 | func (db *DefaultMysqlAuthPlugin) CreateUser(user *xUserV1.OpenHydraUser) error { 20 | inst, err := db.Db() 21 | if err != nil { 22 | return err 23 | } 24 | defer inst.Close() 25 | _, err = inst.Exec("INSERT INTO user (username, email, password, ch_name, description, role) VALUES (?, ?, ?, ?, ?, ?)", user.Name, user.Spec.Email, user.Spec.Password, user.Spec.ChineseName, user.Spec.Description, user.Spec.Role) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // GetUser implements IDataBaseUser gets a user by name 34 | func (db *DefaultMysqlAuthPlugin) GetUser(name string) (*xUserV1.OpenHydraUser, error) { 35 | inst, err := db.Db() 36 | if err != nil { 37 | return nil, err 38 | } 39 | var user xUserV1.OpenHydraUser 40 | util.FillObjectGVK(&user) 41 | row := inst.QueryRow("SELECT username, email, password, ch_name, description, role FROM user WHERE username = ?", name) 42 | err = row.Scan(&user.Name, &user.Spec.Email, &user.Spec.Password, &user.Spec.ChineseName, &user.Spec.Description, &user.Spec.Role) 43 | if err != nil { 44 | if stdErr.Is(err, sql.ErrNoRows) { 45 | user.GetResourceVersion() 46 | return nil, errors.NewNotFound(schema.GroupResource{Group: xUserV1.GroupName, Resource: util.GetObjectKind(&user)}, name) 47 | } 48 | slog.Error(fmt.Sprintf("Failed to query user %s from database", name), err) 49 | return nil, err 50 | } 51 | return &user, nil 52 | } 53 | 54 | // UpdateUser implements IDataBaseUser updates a user 55 | func (db *DefaultMysqlAuthPlugin) UpdateUser(user *xUserV1.OpenHydraUser) error { 56 | inst, err := db.Db() 57 | if err != nil { 58 | return err 59 | } 60 | _, err = inst.Exec("UPDATE user SET email = ?, password = ?, ch_name = ?, description = ?, role = ? WHERE username = ?", user.Spec.Email, user.Spec.Password, user.Spec.ChineseName, user.Spec.Description, user.Spec.Role, user.Name) 61 | if err != nil { 62 | slog.Error(fmt.Sprintf("Failed to update user %s from database", user.Name), err) 63 | return err 64 | } 65 | return nil 66 | } 67 | 68 | // DeleteUser implements IDataBaseUser deletes a user 69 | func (db *DefaultMysqlAuthPlugin) DeleteUser(name string) error { 70 | inst, err := db.Db() 71 | if err != nil { 72 | return err 73 | } 74 | result, err := inst.Exec("DELETE FROM user WHERE username = ?", name) 75 | if err != nil { 76 | slog.Error(fmt.Sprintf("Failed to delete user %s from database", name), err) 77 | return err 78 | } 79 | affected, err := result.RowsAffected() 80 | if err != nil { 81 | slog.Error(fmt.Sprintf("Failed get delete user %s result", name), err) 82 | return err 83 | } 84 | if affected == 0 { 85 | return errors.NewNotFound(schema.GroupResource{Group: xUserV1.GroupName, Resource: "OpenHydraUser"}, name) 86 | } 87 | return nil 88 | } 89 | 90 | // ListUsers implements IDataBaseUser lists all users 91 | func (db *DefaultMysqlAuthPlugin) ListUsers() (xUserV1.OpenHydraUserList, error) { 92 | inst, err := db.Db() 93 | if err != nil { 94 | return xUserV1.OpenHydraUserList{}, err 95 | } 96 | rows, err := inst.Query("SELECT username, email, password, ch_name, description, role FROM user") 97 | if err != nil { 98 | return xUserV1.OpenHydraUserList{}, err 99 | } 100 | defer rows.Close() 101 | result := xUserV1.OpenHydraUserList{} 102 | for rows.Next() { 103 | var user xUserV1.OpenHydraUser 104 | util.FillObjectGVK(&user) 105 | err = rows.Scan(&user.Name, &user.Spec.Email, &user.Spec.Password, &user.Spec.ChineseName, &user.Spec.Description, &user.Spec.Role) 106 | if err != nil { 107 | return xUserV1.OpenHydraUserList{}, err 108 | } 109 | result.Items = append(result.Items, user) 110 | } 111 | 112 | return result, nil 113 | } 114 | 115 | func (db *DefaultMysqlAuthPlugin) LoginUser(name, password string) (*xUserV1.OpenHydraUser, error) { 116 | inst, err := db.Db() 117 | if err != nil { 118 | return nil, err 119 | } 120 | defer inst.Close() 121 | rows, err := inst.Query("SELECT username, email, password, ch_name, description, role FROM user WHERE username = ? AND password = ?", name, password) 122 | if err != nil { 123 | return nil, err 124 | } 125 | var user xUserV1.OpenHydraUser 126 | util.FillObjectGVK(&user) 127 | for rows.Next() { 128 | err = rows.Scan(&user.Name, &user.Spec.Email, &user.Spec.Password, &user.Spec.ChineseName, &user.Spec.Description, &user.Spec.Role) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | 134 | if user.Name == "" { 135 | return nil, fmt.Errorf("user %s not found", name) 136 | } 137 | 138 | return &user, nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/open-hydra/summary-handler.go: -------------------------------------------------------------------------------- 1 | package openhydra 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | 9 | coreV1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/resource" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | 13 | xSumUpV1 "open-hydra/pkg/apis/open-hydra-api/summary/core/v1" 14 | "open-hydra/pkg/util" 15 | 16 | "github.com/emicklei/go-restful/v3" 17 | ) 18 | 19 | func (builder *OpenHydraRouteBuilder) AddSummaryGetRoute() { 20 | path := "/" + SumUpPath 21 | builder.addPathAuthorization(path, http.MethodGet, 3) 22 | builder.RootWS.Route(builder.RootWS.GET(path).Operation("getSummary").To(builder.SummaryGetRouteHandler). 23 | Returns(http.StatusInternalServerError, "internal server error", ""). 24 | Returns(http.StatusForbidden, "forbidden", ""). 25 | Returns(http.StatusUnauthorized, "unauthorized", ""). 26 | Returns(http.StatusOK, "OK", xSumUpV1.SumUp{})) 27 | } 28 | 29 | func (builder *OpenHydraRouteBuilder) SummaryGetRouteHandler(request *restful.Request, response *restful.Response) { 30 | nodeList, err := builder.kubeClient.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) 31 | if err != nil { 32 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Error getting node list: %v", err)) 33 | return 34 | } 35 | // TODO may be all namespace 36 | pods, err := builder.k8sHelper.ListPod(OpenhydraNamespace, builder.kubeClient) 37 | if err != nil { 38 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Error getting pod list: %v", err)) 39 | return 40 | } 41 | 42 | sumUp, err := builder.SumUpGpuResources(pods, nodeList) 43 | if err != nil { 44 | writeHttpResponseAndLogError(response, http.StatusInternalServerError, fmt.Sprintf("Error sum up gpu resources: %v", err)) 45 | return 46 | } 47 | 48 | util.FillObjectGVK(sumUp) 49 | _ = response.WriteEntity(sumUp) 50 | } 51 | 52 | func (builder *OpenHydraRouteBuilder) SumUpGpuResources(pods []coreV1.Pod, nodeList *coreV1.NodeList) (*xSumUpV1.SumUp, error) { 53 | 54 | serverConfig, err := builder.GetServerConfigFromConfigMap() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | defRAM := resource.NewQuantity(int64(serverConfig.DefaultRamPerDevice*(1<<20)), resource.BinarySI).String() 60 | defCPU := resource.NewMilliQuantity(int64(serverConfig.DefaultCpuPerDevice), resource.DecimalSI).String() 61 | 62 | gpuAllocatable := resource.NewQuantity(0, resource.DecimalSI) 63 | gpuAllocated := resource.NewQuantity(0, resource.DecimalSI) 64 | podAllocated := 0 65 | gpuResourceCount := map[string]struct { 66 | Allocatable *resource.Quantity 67 | Allocated *resource.Quantity 68 | }{} 69 | var totalLine uint16 70 | if len(serverConfig.GpuResourceKeys) == 0 { 71 | // warn 72 | slog.Warn("gpu resource key is empty, so total gpu number will be 0 and all device use gpu will fall back to default") 73 | } else { 74 | 75 | for _, node := range nodeList.Items { 76 | for _, gpuResourceKey := range serverConfig.GpuResourceKeys { 77 | gpu := node.Status.Allocatable[coreV1.ResourceName(gpuResourceKey)] 78 | if _, found := gpuResourceCount[gpuResourceKey]; found { 79 | gpuResourceCount[gpuResourceKey].Allocatable.Add(gpu) 80 | } else { 81 | gpuResourceCount[gpuResourceKey] = struct { 82 | Allocatable *resource.Quantity 83 | Allocated *resource.Quantity 84 | }{ 85 | Allocatable: &gpu, 86 | Allocated: resource.NewQuantity(0, resource.DecimalSI), 87 | } 88 | } 89 | } 90 | } 91 | for _, pod := range pods { 92 | for _, ctr := range pod.Spec.Containers { 93 | for _, gpuResourceKey := range serverConfig.GpuResourceKeys { 94 | gpuRequests := ctr.Resources.Requests[coreV1.ResourceName(gpuResourceKey)] 95 | gpuResourceCount[gpuResourceKey].Allocated.Add(gpuRequests) 96 | } 97 | } 98 | if pod.Status.Phase == coreV1.PodPending { 99 | totalLine++ 100 | } 101 | } 102 | } 103 | 104 | // sum up all gpu device allocation status 105 | gpuDriverSumUp := map[string]xSumUpV1.GpuResourceSumUp{} 106 | for key, allocationStatus := range gpuResourceCount { 107 | gpuAllocatable.Add(*allocationStatus.Allocatable) 108 | gpuAllocated.Add(*allocationStatus.Allocated) 109 | gpuDriverSumUp[key] = xSumUpV1.GpuResourceSumUp{ 110 | Allocatable: allocationStatus.Allocatable.Value(), 111 | Allocated: allocationStatus.Allocated.Value(), 112 | } 113 | } 114 | 115 | podAllocatable := 0 116 | if serverConfig.DefaultGpuPerDevice != 0 { 117 | podAllocatable = int(gpuAllocatable.Value() / int64(serverConfig.DefaultGpuPerDevice)) 118 | } 119 | 120 | return &xSumUpV1.SumUp{ 121 | ObjectMeta: metav1.ObjectMeta{ 122 | CreationTimestamp: metav1.Now(), 123 | }, 124 | Spec: xSumUpV1.SumUpSpec{ 125 | PodAllocatable: podAllocatable, 126 | PodAllocated: podAllocated, 127 | GpuAllocatable: gpuAllocatable.String(), 128 | GpuAllocated: gpuAllocated.String(), 129 | DefaultCpuPerDevice: defCPU, 130 | DefaultRamPerDevice: defRAM, 131 | DefaultGpuPerDevice: serverConfig.DefaultGpuPerDevice, 132 | TotalLine: totalLine, 133 | GpuResourceSumUp: gpuDriverSumUp, 134 | }, 135 | }, nil 136 | } 137 | -------------------------------------------------------------------------------- /docs/user-guide.md: -------------------------------------------------------------------------------- 1 | # OpenHydra用户手册 2 | 3 | ## 平台概述 4 | OpenHydra 是一款由管理端、学生端组成的综合人工智能教育实验管理平台,以Web UI的方式为学生提供即开即用,即关即停,轻量可靠的AI实验平台,学生和老师可以通过便捷的启用CPU或GPU实训环境来完成人工智能课程的实操练习。通过OpenHydra平台,教师和学生可以直接参考学习,并参与到人工智能相关的项目开发中,从而培养用AI解决真实问题的能力。 5 | 6 | ## 管理端 7 | 只有【教师角色】账号可以登陆管理端,教师账号可通过管理端进行管理账户、设备、数据集和课程资源。 8 | 9 | ### 用户管理 10 | 用户管理包括创建用户和删除用户等功能。 11 | 12 | #### 查看账户列表 13 | 1. 用户登陆管理端,点击“账号管理”,进入账户管理页面; 14 | 2. 查看账号列表,账号列表信息包括:账号、姓名、角色、描述和“删除”操作,用户可设置列表中要展示的列; 15 | ![Alt text](../images/user-guide.png) 16 | 17 | #### 创建用户 18 | 1. 用户登陆管理端,点击“账号管理”,进入账号管理页面; 19 | ![Alt text](../images/user-guide-1.png) 20 | 2. 点击“创建账号”按钮,在弹出的创建账号页面中输入以下内容: 21 | - 在‘账号’输入框中输入“账号”; 22 | - 在‘姓名’输入框中输入“姓名”; 23 | - 在‘密码’输入框中输入“密码”; 24 | - 在‘确认密码’输入框中输入“确认密码”,确认密码要和密码一致; 25 | - 在‘角色’下拉框中选择“角色”; 26 | - 在‘描述’输入框中输入“描述”,最大字符数不超过100个字符; 27 | ![Alt text](../images/user-guide-2.png) 28 | 3. 点击“确认”按钮,成功创建账号并保存密码; 29 | 4. 点击"取消"按钮,取消创建账号并返回到账号管理页面中; 30 | 31 | #### 删除用户 32 | 1. 用户登陆管理端,点击“账号管理”,进入账号管理页面; 33 | 2. 选择一个账号,点击其后的“删除”按钮,弹出提示确认框; 34 | 3. 点击“确定”按钮,成功删除账号; 35 | 4. 点击"取消"按钮,取消删除账号并返回到账号管理页面中; 36 | ![Alt text](../images/user-guide-3.png) 37 | 38 | ### 设备管理 39 | 设备管理包括设备启动和关闭设备等功能。 40 | 41 | #### 查看设备管理列表 42 | 1. 用户登陆管理端,点击“设备管理”,进入设备管理页面; 43 | 2. 查看设备管理列表,设备管理列表信息包括:IP地址、账号、启动时间、角色、姓名、容器状态、容器类型、jupyterLabUr、vsCodeUrl和操作按钮“启动”“关闭”,用户可设置列表中要展示的列; 44 | ![Alt text](../images/user-guide-4.png) 45 | 46 | #### 启动设备 47 | 1. 用户登陆管理端,点击“设备管理”,进入设备管理页面; 48 | 2. 选择一个设备,点击其后的“启动”按钮,在弹出的创建账户页面中输入以下内容: 49 | 在‘启动容器’多选框中选择“CPU实验室”和“Jupyter Lab”; 50 | 注:只能通过管理端启动GPU实验室,学生端启动的均为CPU实验室; 51 | ![Alt text](../images/user-guide-5.png) 52 | ![Alt text](../images/user-guide-6.png) 53 | 3. 点击“确认”按钮,成功启动设备; 54 | 注: 55 | + 管理端和学生端的实验室设备均启动; 56 | + 管理端可通过jupyterLabUr、vsCodeUrl进入操作; 57 | + 学生端实验室设备被启动,用户可进入; 58 | 1. 点击"取消"按钮,取消启动设备并返回到设备管理页面中; 59 | 60 | #### 关闭设备 61 | 1. 用户登陆管理端,点击“设备管理”,进入设备管理页面; 62 | 2. 选择一个已启动的设备,点击其后的“关闭”按钮,弹出的提示确认框; 63 | 3. 点击“确认”按钮,成功关闭设备; 64 | 注:管理端和学生端的实验室设备均被关闭; 65 | 4. 点击"取消"按钮,取消关闭设备并返回到设备管理页面中; 66 | ![Alt text](../images/user-guide-7.png) 67 | 68 | ### 数据集管理 69 | 数据集管理包括数据集上传和删除用户等功能。 70 | 71 | #### 查看数据集管理列表 72 | 1. 用户登陆管理端,点击“数据集管理”,进入数据集管理页面; 73 | 2. 查看数据集管理列表,数据集管理列表信息包括:数据集、描述和操作按钮“删除”,用户可设置列表中要展示的列; 74 | ![Alt text](../images/user-guide-8.png) 75 | 76 | #### 上传数据集 77 | 1. 用户登陆管理端,点击“数据管理”,进入数据管理页面; 78 | 2. 点击“上传数据集”按钮,在弹出的上传数据集页面中输入以下内容: 79 | - 在‘数据集名称’输入框中输入“数据集名称”; 80 | - 在‘选择文件’上传框中选择“上传文件”,只允许选择zip类型文件上传; 81 | - 在‘描述’输入框中输入“描述”,最大字符数不超过100个字符; 82 | ![Alt text](../images/user-guide-9.png) 83 | 3. 点击“确认”按钮,成功上传数据集; 84 | 4. 点击"取消"按钮,取消上传数据集并返回到数据集管理页面中; 85 | 86 | #### 删除数据集 87 | 1. 用户登陆管理端,点击“数据集管理”,进入数据集管理页面; 88 | 2. 选择一个数据集,点击其后的“删除”按钮,弹出提示确认框; 89 | ![Alt text](../images/user-guide-10.png) 90 | 3. 点击“确定”按钮,成功删除数据集; 91 | 4. 点击"取消"按钮,取消删除数据集并返回到数据集管理页面中; 92 | 93 | ### 课程资源管理 94 | 课程资源管理包括课程资源上传和删除课程资源等功能。 95 | 96 | #### 查看课程资源管理列表 97 | 1. 用户登陆管理端,点击“课程资源管理”,进入课程资源管理页面; 98 | 2. 查看课程资源管理列表,课程资源管理列表信息包括:课程资源、描述和操作按钮“删除”,用户可设置列表中要展示的列; 99 | ![Alt text](../images/user-guide-11.png) 100 | 101 | #### 上传课程资源 102 | 1. 用户登陆管理端,点击“课程资源管理”,进入课程资源管理页面; 103 | 2. 点击“上传课程资源”按钮,在弹出的上传课程资源页面中输入以下内容: 104 | - 在‘课程资源名称’输入框中输入“课程资源名称”; 105 | - 在‘选择文件’上传框中选择“上传文件”,只允许选择zip类型文件上传; 106 | - 在‘描述’输入框中输入“描述”,最大字符数不超过100个字符; 107 | ![Alt text](../images/user-guide-12.png) 108 | 3. 点击“确认”按钮,成功上传课程资源; 109 | 4. 点击"取消"按钮,取消上传课程资源并返回到课程资源管理页面中; 110 | 111 | #### 删除课程资源 112 | 1. 用户登陆管理端,点击“课程资源管理”,进入课程资源管理页面; 113 | 2. 选择一个课程资源,点击其后的“删除”按钮,弹出提示确认框; 114 | ![Alt text](../images/user-guide-13.png) 115 | 3. 点击“确定”按钮,成功删除课程资源; 116 | 4. 点击"取消"按钮,取消删除课程资源并返回到课程资源管理页面中; 117 | 118 | ### 平台配置 119 | 平台配置提供编辑配置功能。 120 | 121 | #### 编辑平台配置 122 | 1. 用户登陆管理端,点击“平台配置”,进入平台配置页面; 123 | 2. 点击“编辑”按钮,在弹出的基本信息中输入以下内容: 124 | - 在‘设备类型’下拉框中选择“CPU”; 125 | ![Alt text](../images/user-guide-14.png) 126 | 3. 点击“提交”按钮,成功编辑平台设置; 127 | 4. 点击"取消"按钮,取消编辑平台设置并返回到平台配置页面中; 128 | 129 | ## 学生端 130 | 学生端是为【学生角色】提供登录的平台,学生账户可以通过学生端进行学习和课程资源的使用。 131 | 132 | ### 实验环境 133 | #### 开启实验环境 134 | 1. 用户登陆学生端,进入实验环境页面; 135 | ![Alt text](../images/user-guide-15.png) 136 | 2. 点击“开启实验环境”按钮,xedu实验环境开启,easy train和Jupyter Lab可选择; 137 | ![Alt text](../images/user-guide-16.png) 138 | ![Alt text](../images/user-guide-18.png) 139 | 140 | #### 开启vscode实验环境 141 | 1. 用户登陆学生端,进入实验环境页面; 142 | 2. 点击“开启vscode实验环境”按钮,xedu实验环境开启,仅VScode环境可选择; 143 | ![Alt text](../images/user-guide-19.png) 144 | 145 | #### 体验easy train环境 146 | 前提:存在开启中的实验环境; 147 | 1. 用户登陆学生端,进入实验环境页面; 148 | 2. 点击“easy train”按钮,进入easy train页面; 149 | 3. 根据训练步骤进行操作训练; 150 | 151 | #### 体验Jupyter Lab环境 152 | 1. 用户登陆学生端,进入实验环境页面; 153 | 2. 点击“Jupyter Lab”按钮,进入Jupyter Lab页面; 154 | 3. 查看文件夹,点击进入课程资源文件夹; 155 | ![Alt text](../images/user-guide-20.png) 156 | ![Alt text](../images/user-guide-21.png) 157 | 4. 打开课程资源文件,查看课程资源内容 158 | ![Alt text](../images/user-guide-22.png) 159 | 5. 点击创建新文件 160 | ![Alt text](../images/user-guide-23.png) 161 | 6. 输入代码并执行 162 | ![Alt text](../images/user-guide-24.png) 163 | 7. 执行完成,输出结果并保存文件 164 | ![Alt text](../images/user-guide-25.png) 165 | 166 | 167 | #### 体验VScode环境 168 | 1. 用户登陆学生端,进入实验环境页面; 169 | 2. 点击“vscode”按钮,进入vscode页面; 170 | 3. 点击“next section”跳过创建工程; 171 | ![Alt text](../images/user-guide-26.png) 172 | 4. 点击“New File”按钮创建新文件或打开文件夹 173 | ![Alt text](../images/user-guide-27.png) 174 | 175 | #### 关闭实验环境 176 | 前提:存在开启中的实验环境; 177 | 1. 用户登陆学生端,进入实验环境页面; 178 | 2. 点击“关闭实验环境”按钮,弹出提示确认框; 179 | ![Alt text](../images/user-guide-28.png) 180 | 3. 点击“确定”按钮,成功关闭实验环境; 181 | 4. 点击"取消"按钮,取消关闭实验环境并返回到实验环境页面中; 182 | 183 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module open-hydra 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 7 | github.com/emicklei/go-restful v2.16.0+incompatible 8 | github.com/emicklei/go-restful/v3 v3.11.0 9 | github.com/go-sql-driver/mysql v1.7.1 10 | github.com/google/uuid v1.3.0 11 | github.com/onsi/ginkgo/v2 v2.13.2 12 | github.com/onsi/gomega v1.30.0 13 | github.com/spf13/cobra v1.7.0 14 | github.com/spf13/pflag v1.0.5 15 | golang.org/x/sync v0.5.0 16 | golang.org/x/text v0.13.0 17 | gopkg.in/yaml.v2 v2.4.0 18 | k8s.io/api v0.29.0 19 | k8s.io/apimachinery v0.29.0 20 | k8s.io/apiserver v0.29.0 21 | k8s.io/client-go v0.29.0 22 | k8s.io/kube-openapi v0.0.0-20231214164306-ab13479f8bf8 23 | sigs.k8s.io/controller-runtime v0.16.3 24 | ) 25 | 26 | require ( 27 | github.com/NYTimes/gziphandler v1.1.1 // indirect 28 | github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect 29 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/blang/semver/v4 v4.0.0 // indirect 32 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 33 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 34 | github.com/coreos/go-semver v0.3.1 // indirect 35 | github.com/coreos/go-systemd/v22 v22.5.0 // indirect 36 | github.com/davecgh/go-spew v1.1.1 // indirect 37 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 38 | github.com/felixge/httpsnoop v1.0.3 // indirect 39 | github.com/fsnotify/fsnotify v1.7.0 // indirect 40 | github.com/go-logr/logr v1.3.0 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 43 | github.com/go-openapi/jsonreference v0.20.2 // indirect 44 | github.com/go-openapi/swag v0.22.3 // indirect 45 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 46 | github.com/gogo/protobuf v1.3.2 // indirect 47 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 48 | github.com/golang/protobuf v1.5.3 // indirect 49 | github.com/google/cel-go v0.17.7 // indirect 50 | github.com/google/gnostic-models v0.6.8 // indirect 51 | github.com/google/go-cmp v0.6.0 // indirect 52 | github.com/google/gofuzz v1.2.0 // indirect 53 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect 54 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 55 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect 56 | github.com/imdario/mergo v0.3.6 // indirect 57 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 58 | github.com/josharian/intern v1.0.0 // indirect 59 | github.com/json-iterator/go v1.1.12 // indirect 60 | github.com/mailru/easyjson v0.7.7 // indirect 61 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 63 | github.com/modern-go/reflect2 v1.0.2 // indirect 64 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 65 | github.com/pkg/errors v0.9.1 // indirect 66 | github.com/prometheus/client_golang v1.16.0 // indirect 67 | github.com/prometheus/client_model v0.4.0 // indirect 68 | github.com/prometheus/common v0.44.0 // indirect 69 | github.com/prometheus/procfs v0.10.1 // indirect 70 | github.com/stoewer/go-strcase v1.2.0 // indirect 71 | go.etcd.io/etcd/api/v3 v3.5.10 // indirect 72 | go.etcd.io/etcd/client/pkg/v3 v3.5.10 // indirect 73 | go.etcd.io/etcd/client/v3 v3.5.10 // indirect 74 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0 // indirect 75 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect 76 | go.opentelemetry.io/otel v1.19.0 // indirect 77 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect 78 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect 79 | go.opentelemetry.io/otel/metric v1.19.0 // indirect 80 | go.opentelemetry.io/otel/sdk v1.19.0 // indirect 81 | go.opentelemetry.io/otel/trace v1.19.0 // indirect 82 | go.opentelemetry.io/proto/otlp v1.0.0 // indirect 83 | go.uber.org/multierr v1.11.0 // indirect 84 | go.uber.org/zap v1.25.0 // indirect 85 | golang.org/x/crypto v0.14.0 // indirect 86 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect 87 | golang.org/x/net v0.17.0 // indirect 88 | golang.org/x/oauth2 v0.10.0 // indirect 89 | golang.org/x/sys v0.14.0 // indirect 90 | golang.org/x/term v0.13.0 // indirect 91 | golang.org/x/time v0.3.0 // indirect 92 | golang.org/x/tools v0.14.0 // indirect 93 | google.golang.org/appengine v1.6.7 // indirect 94 | google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect 95 | google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect 96 | google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect 97 | google.golang.org/grpc v1.58.3 // indirect 98 | google.golang.org/protobuf v1.31.0 // indirect 99 | gopkg.in/inf.v0 v0.9.1 // indirect 100 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 101 | gopkg.in/yaml.v3 v3.0.1 // indirect 102 | k8s.io/component-base v0.29.0 // indirect 103 | k8s.io/klog/v2 v2.110.1 // indirect 104 | k8s.io/kms v0.29.0 // indirect 105 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect 106 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 // indirect 107 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 108 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 109 | sigs.k8s.io/yaml v1.3.0 // indirect 110 | ) 111 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | REGISTRY ?= registry.cn-shanghai.aliyuncs.com/openhydra 4 | TAG ?= 5 | 6 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 7 | ifeq (,$(shell go env GOBIN)) 8 | GOBIN=$(shell go env GOPATH)/bin 9 | else 10 | GOBIN=$(shell go env GOBIN) 11 | endif 12 | 13 | GOOS ?= $(shell go env GOHOSTOS) 14 | GOARCH ?= $(shell go env GOARCH) 15 | COMMIT_REF ?= $(shell git rev-parse --verify HEAD) 16 | BOILERPLATE_DIR = $(shell pwd)/hack 17 | GOPROXY ?= https://goproxy.cn,direct 18 | GOVERSION ?= 1.21.0 19 | IMAGETAG ?= $(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --verify HEAD)-$(shell date -u '+%Y%m%d%I%M%S') 20 | 21 | APP ?= 22 | ifeq ($(APP),) 23 | apps = $(shell ls cmd) 24 | else 25 | apps = $(APP) 26 | endif 27 | 28 | RACE ?= 29 | ifeq ($(RACE),on) 30 | race = "-race" 31 | endif 32 | 33 | TAG ?= 34 | ifeq ($(TAG),) 35 | TAG = $(COMMIT_REF) 36 | endif 37 | 38 | .PHONY: update-openapi 39 | update-openapi: 40 | $(GOBIN)/openapi-gen --input-dirs open-hydra/pkg/open-hydra/apis,open-hydra/pkg/apis/open-hydra-api/course/core/v1,open-hydra/pkg/apis/open-hydra-api/setting/core/v1,open-hydra/pkg/apis/open-hydra-api/summary/core/v1,open-hydra/pkg/apis/open-hydra-api/device/core/v1,open-hydra/pkg/apis/open-hydra-api/user/core/v1,open-hydra/pkg/apis/open-hydra-api/dataset/core/v1,k8s.io/apimachinery/pkg/util/intstr,k8s.io/apimachinery/pkg/api/resource,k8s.io/apimachinery/pkg/apis/meta/v1,k8s.io/apimachinery/pkg/runtime,k8s.io/api/core/v1,k8s.io/apimachinery/pkg/apis/meta/v1 \ 41 | --output-package open-hydra/pkg/generated/apis/openapi --output-base ./.. --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 42 | 43 | .PHONY: gen-device-deepcopy-set 44 | gen-device-deepcopy-set: 45 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/device/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/device/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 46 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/device/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/device/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 47 | 48 | .PHONY: gen-dataset-deepcopy-set 49 | gen-dataset-deepcopy-set: 50 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/dataset/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/dataset/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 51 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/dataset/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/dataset/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 52 | 53 | .PHONY: gen-course-deepcopy-set 54 | gen-course-deepcopy-set: 55 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/course/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/course/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 56 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/course/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/course/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 57 | 58 | .PHONY: gen-user-deepcopy-set 59 | gen-user-deepcopy-set: 60 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/user/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/user/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 61 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/user/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/user/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 62 | 63 | .PHONY: gen-setting-deepcopy-set 64 | gen-setting-deepcopy-set: 65 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/setting/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/setting/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 66 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/setting/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/setting/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 67 | 68 | .PHONY: gen-summary-deepcopy-set 69 | gen-summary-deepcopy-set: 70 | $(GOBIN)/deepcopy-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/summary/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/summary/core/v1 --output-base ./.. -O zz_generated.deepcopy --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 71 | $(GOBIN)/register-gen --input-dirs open-hydra/pkg/apis/open-hydra-api/summary/core/v1 --output-package open-hydra/pkg/apis/open-hydra-api/summary/core/v1 --output-base ./.. -O register --go-header-file $(BOILERPLATE_DIR)/boilerplate.go.txt 72 | 73 | .PHONY: gen-all-deepcopy-set 74 | gen-all-deepcopy-set: gen-device-deepcopy-set gen-dataset-deepcopy-set gen-user-deepcopy-set gen-summary-deepcopy-set gen-setting-deepcopy-set gen-course-deepcopy-set 75 | 76 | .PHONY: test-all 77 | test-all: 78 | ginkgo -r -v --cover --coverprofile=coverage.out 79 | 80 | .PHONY: fmt 81 | fmt: 82 | gofmt -w pkg cmd 83 | 84 | .PHONY: vet 85 | vet: 86 | go vet ./... 87 | 88 | .PHONY: go-build 89 | go-build: 90 | CGO_ENABLED=0 GOARCH=$(GOARCH) go build -o cmd/open-hydra-server/open-hydra-server -ldflags "-X 'main.version=${TAG}'" cmd/open-hydra-server/main.go 91 | 92 | .PHONY: image 93 | image: go-build 94 | docker build -t $(REGISTRY)/open-hydra-server:$(IMAGETAG) --load . 95 | 96 | .PHONY: image-no-container 97 | image-no-container: go-build 98 | docker build -f hack/builder/Dockerfile -t $(REGISTRY)/open-hydra-server:$(IMAGETAG) --load . 99 | -------------------------------------------------------------------------------- /pkg/database/faker.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | xCourseV1 "open-hydra/pkg/apis/open-hydra-api/course/core/v1" 6 | xDatasetV1 "open-hydra/pkg/apis/open-hydra-api/dataset/core/v1" 7 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 8 | "open-hydra/pkg/util" 9 | 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ) 13 | 14 | // faker is for test purpose only 15 | type Faker struct { 16 | fakeUsers map[string]*xUserV1.OpenHydraUser 17 | fakeDatasets map[string]*xDatasetV1.Dataset 18 | fakeCourses map[string]*xCourseV1.Course 19 | } 20 | 21 | func (f *Faker) Init() { 22 | f.fakeUsers = make(map[string]*xUserV1.OpenHydraUser) 23 | f.fakeDatasets = make(map[string]*xDatasetV1.Dataset) 24 | f.fakeCourses = make(map[string]*xCourseV1.Course) 25 | } 26 | 27 | // implements IDataBaseUser creates a new user 28 | func (db *Faker) CreateUser(user *xUserV1.OpenHydraUser) error { 29 | if _, found := db.fakeUsers[user.Name]; found { 30 | return fmt.Errorf("user %s already exists", user.Name) 31 | } 32 | db.fakeUsers[user.Name] = user 33 | return nil 34 | } 35 | 36 | // implements IDataBaseUser gets a user by name 37 | func (db *Faker) GetUser(name string) (*xUserV1.OpenHydraUser, error) { 38 | if user, found := db.fakeUsers[name]; found { 39 | return user, nil 40 | } 41 | return nil, fmt.Errorf("user %s not found", name) 42 | } 43 | 44 | // implements IDataBaseUser updates a user 45 | func (db *Faker) UpdateUser(user *xUserV1.OpenHydraUser) error { 46 | if _, found := db.fakeUsers[user.Name]; !found { 47 | return fmt.Errorf("user %s not found", user.Name) 48 | } 49 | db.fakeUsers[user.Name] = user 50 | return nil 51 | } 52 | 53 | // implements IDataBaseUser deletes a user 54 | func (db *Faker) DeleteUser(name string) error { 55 | delete(db.fakeUsers, name) 56 | return nil 57 | } 58 | 59 | // implements IDataBaseUser lists all users 60 | func (db *Faker) ListUsers() (xUserV1.OpenHydraUserList, error) { 61 | result := xUserV1.OpenHydraUserList{} 62 | result.Kind = "List" 63 | result.APIVersion = "v1" 64 | for _, user := range db.fakeUsers { 65 | result.Items = append(result.Items, *user) 66 | } 67 | return result, nil 68 | } 69 | 70 | // implements IDataBaseDataset creates a new dataset 71 | func (db *Faker) CreateDataset(dataset *xDatasetV1.Dataset) error { 72 | if _, found := db.fakeDatasets[dataset.Name]; found { 73 | return fmt.Errorf("dataset %s already exists", dataset.Name) 74 | } 75 | db.fakeDatasets[dataset.Name] = dataset 76 | return nil 77 | } 78 | 79 | // implements IDataBaseDataset gets a dataset by name 80 | func (db *Faker) GetDataset(name string) (*xDatasetV1.Dataset, error) { 81 | if dataset, found := db.fakeDatasets[name]; found { 82 | return dataset, nil 83 | } 84 | var dataset xDatasetV1.Dataset 85 | util.FillObjectGVK(&dataset) 86 | return nil, errors.NewNotFound(schema.GroupResource{Group: xUserV1.GroupName, Resource: util.GetObjectKind(&dataset)}, name) 87 | } 88 | 89 | // implements IDataBaseDataset updates a dataset 90 | func (db *Faker) UpdateDataset(dataset *xDatasetV1.Dataset) error { 91 | if _, found := db.fakeDatasets[dataset.Name]; !found { 92 | return fmt.Errorf("dataset %s not found", dataset.Name) 93 | } 94 | db.fakeDatasets[dataset.Name] = dataset 95 | return nil 96 | } 97 | 98 | // implements IDataBaseDataset deletes a dataset 99 | func (db *Faker) DeleteDataset(name string) error { 100 | delete(db.fakeDatasets, name) 101 | return nil 102 | } 103 | 104 | // implements IDataBaseDataset lists all datasets 105 | func (db *Faker) ListDatasets() (xDatasetV1.DatasetList, error) { 106 | result := xDatasetV1.DatasetList{} 107 | result.Kind = "List" 108 | result.APIVersion = "v1" 109 | for _, dataset := range db.fakeDatasets { 110 | result.Items = append(result.Items, *dataset) 111 | } 112 | return result, nil 113 | } 114 | 115 | func (db *Faker) LoginUser(name, password string) (*xUserV1.OpenHydraUser, error) { 116 | if user, found := db.fakeUsers[name]; found { 117 | if user.Spec.Password == password { 118 | return user, nil 119 | } 120 | return nil, fmt.Errorf("wrong password") 121 | } 122 | return nil, fmt.Errorf("user %s not found", name) 123 | } 124 | 125 | func (db *Faker) InitDb() error { 126 | return nil 127 | } 128 | 129 | // implements IDataBaseCourse creates a new course 130 | func (db *Faker) CreateCourse(course *xCourseV1.Course) error { 131 | if _, found := db.fakeCourses[course.Name]; found { 132 | return fmt.Errorf("course %s already exists", course.Name) 133 | } 134 | db.fakeCourses[course.Name] = course 135 | return nil 136 | } 137 | 138 | // implements IDataBaseCourse gets a course by name 139 | func (db *Faker) GetCourse(name string) (*xCourseV1.Course, error) { 140 | if course, found := db.fakeCourses[name]; found { 141 | return course, nil 142 | } 143 | var course xDatasetV1.Dataset 144 | util.FillObjectGVK(&course) 145 | return nil, errors.NewNotFound(schema.GroupResource{Group: xUserV1.GroupName, Resource: util.GetObjectKind(&course)}, name) 146 | } 147 | 148 | // implements IDataBaseCourse updates a course 149 | func (db *Faker) UpdateCourse(course *xCourseV1.Course) error { 150 | if _, found := db.fakeCourses[course.Name]; !found { 151 | return fmt.Errorf("course %s not found", course.Name) 152 | } 153 | db.fakeCourses[course.Name] = course 154 | return nil 155 | } 156 | 157 | // implements IDataBaseCourse deletes a course 158 | func (db *Faker) DeleteCourse(name string) error { 159 | delete(db.fakeCourses, name) 160 | return nil 161 | } 162 | 163 | // implements IDataBaseCourse lists all courses 164 | // add a comment for ci test 165 | func (db *Faker) ListCourses() (xCourseV1.CourseList, error) { 166 | result := xCourseV1.CourseList{} 167 | result.Kind = "List" 168 | result.APIVersion = "v1" 169 | for _, course := range db.fakeCourses { 170 | result.Items = append(result.Items, *course) 171 | } 172 | return result, nil 173 | } 174 | -------------------------------------------------------------------------------- /cmd/open-hydra-server/app/open-hydra-server.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "open-hydra/cmd/open-hydra-server/app/config" 8 | "open-hydra/cmd/open-hydra-server/app/option" 9 | "open-hydra/pkg/apiserver" 10 | "strings" 11 | 12 | "github.com/common-nighthawk/go-figure" 13 | "github.com/google/uuid" 14 | "github.com/spf13/cobra" 15 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/client-go/kubernetes" 17 | "k8s.io/client-go/tools/leaderelection" 18 | "k8s.io/client-go/tools/leaderelection/resourcelock" 19 | "sigs.k8s.io/controller-runtime/pkg/manager/signals" 20 | ) 21 | 22 | func NewCommand(version string) *cobra.Command { 23 | options := &option.Options{ 24 | OpenHydraServerOption: option.NewDefaultOpenHydraServerOption(), 25 | ApiServerOption: option.NewDefaultApiServerOption(), 26 | } 27 | cmd := &cobra.Command{ 28 | Use: "open-hydra-server", 29 | Example: figure.NewColorFigure("OpenHydra", "", "green", true).String(), 30 | } 31 | 32 | runCmd := &cobra.Command{ 33 | Use: "run", 34 | Short: "Launch a open-hydra-server api with configuration file", 35 | Long: "run subcommand will launch a open-hydra-serverr", 36 | Example: "open-hydra-server run", 37 | RunE: run(options, signals.SetupSignalHandler().Done()), 38 | } 39 | 40 | // add version api 41 | 42 | verCmd := &cobra.Command{ 43 | Use: "version", 44 | Short: "Print version and exit", 45 | Long: "version subcommand will print version and exit", 46 | Example: "open-hydra-server version", 47 | Run: func(_ *cobra.Command, _ []string) { 48 | fmt.Println("version:", version) 49 | }, 50 | } 51 | 52 | options.BindFlags(runCmd.Flags()) 53 | cmd.AddCommand(runCmd, verCmd) 54 | 55 | return cmd 56 | } 57 | 58 | func run(options *option.Options, stopCh <-chan struct{}) func(cmd *cobra.Command, args []string) error { 59 | return func(cmd *cobra.Command, args []string) error { 60 | ctx, cancel := context.WithCancel(context.Background()) 61 | defer cancel() 62 | 63 | go func() { 64 | <-stopCh 65 | slog.Info("Received termination, signaling shutdown.") 66 | cancel() 67 | }() 68 | 69 | openHydraConfig, err := config.LoadConfig(options.OpenHydraServerOption.ConfigFile, options.OpenHydraServerOption.KubeConfigFile) 70 | if err != nil { 71 | slog.Error(fmt.Sprintf("Failed to load open-hydra-server config file: %v", err)) 72 | return err 73 | } 74 | 75 | errMsg := checkConfig(openHydraConfig) 76 | if len(errMsg) > 0 { 77 | slog.Error(fmt.Sprintf("Failed to check open-hydra-server config file: %v", strings.Join(errMsg, ","))) 78 | } 79 | 80 | onLeaderRun := func(ctx context.Context) { 81 | err := apiserver.RunApiServer(options, openHydraConfig, ctx.Done()) 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | if openHydraConfig.LeaderElection.LeaderElect { 87 | kubeClient, err := kubernetes.NewForConfig(openHydraConfig.KubeConfig) 88 | if err != nil { 89 | panic(err) 90 | } 91 | id := uuid.New().String() 92 | lock := &resourcelock.LeaseLock{ 93 | LeaseMeta: metaV1.ObjectMeta{ 94 | Name: openHydraConfig.LeaderElection.ResourceName, 95 | Namespace: openHydraConfig.LeaderElection.ResourceNamespace, 96 | }, 97 | Client: kubeClient.CoordinationV1(), 98 | LockConfig: resourcelock.ResourceLockConfig{ 99 | Identity: id, 100 | }, 101 | } 102 | 103 | // start the leader election loop 104 | leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ 105 | Lock: lock, 106 | ReleaseOnCancel: true, 107 | LeaseDuration: openHydraConfig.LeaderElection.LeaseDuration, 108 | RenewDeadline: openHydraConfig.LeaderElection.RenewDeadline, 109 | RetryPeriod: openHydraConfig.LeaderElection.RetryPeriod, 110 | Callbacks: leaderelection.LeaderCallbacks{ 111 | OnStartedLeading: onLeaderRun, 112 | OnStoppedLeading: func() { 113 | slog.Info(fmt.Sprintf("leader lost: %s", id)) 114 | }, 115 | OnNewLeader: func(identity string) { 116 | // we're notified when new leader elected 117 | if identity == id { 118 | // I just got the lock 119 | return 120 | } 121 | slog.Info(fmt.Sprintf("new leader elected: %s", identity)) 122 | }, 123 | }, 124 | }) 125 | 126 | return nil 127 | } else { 128 | onLeaderRun(ctx) 129 | } 130 | return nil 131 | } 132 | } 133 | 134 | func checkConfig(config *config.OpenHydraServerConfig) []string { 135 | var errMsg []string 136 | err := checkDBConfig(config) 137 | if err != nil { 138 | errMsg = append(errMsg, err.Error()) 139 | } 140 | return errMsg 141 | } 142 | 143 | func checkDBConfig(config *config.OpenHydraServerConfig) error { 144 | // add a comment for testing 145 | if config.MySqlConfig == nil || config.EtcdConfig == nil { 146 | return fmt.Errorf("both mysql and etcd config are nil, at least one of them should be set") 147 | } 148 | 149 | oneDBConfigIsOk := false 150 | if config.MySqlConfig != nil { 151 | if config.MySqlConfig.Address == "" { 152 | return fmt.Errorf("mysql address is empty") 153 | } 154 | 155 | if config.MySqlConfig.Port == 0 { 156 | return fmt.Errorf("mysql port is empty") 157 | } 158 | 159 | if config.MySqlConfig.Username == "" { 160 | return fmt.Errorf("mysql username is empty") 161 | } 162 | 163 | if config.MySqlConfig.Password == "" { 164 | return fmt.Errorf("mysql password is empty") 165 | } 166 | config.DBType = "mysql" 167 | oneDBConfigIsOk = true 168 | } 169 | 170 | if config.EtcdConfig != nil && !oneDBConfigIsOk { 171 | if len(config.EtcdConfig.Endpoints) == 0 { 172 | return fmt.Errorf("etcd endpoints is empty") 173 | } 174 | 175 | if strings.Contains(config.EtcdConfig.Endpoints[0], "https") { 176 | if config.EtcdConfig.CAFile == "" { 177 | return fmt.Errorf("etcd ca file is empty") 178 | } 179 | 180 | if config.EtcdConfig.CertFile == "" { 181 | return fmt.Errorf("etcd cert file is empty") 182 | } 183 | 184 | if config.EtcdConfig.KeyFile == "" { 185 | return fmt.Errorf("etcd key file is empty") 186 | } 187 | } 188 | config.DBType = "etcd" 189 | oneDBConfigIsOk = true 190 | } 191 | 192 | if !oneDBConfigIsOk { 193 | return fmt.Errorf("both mysql and etcd config are empty, at least one of them should be set") 194 | } 195 | 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /pkg/open-hydra/handler.go: -------------------------------------------------------------------------------- 1 | package openhydra 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "open-hydra/cmd/open-hydra-server/app/config" 9 | "open-hydra/cmd/open-hydra-server/app/option" 10 | xDeviceV1 "open-hydra/pkg/apis/open-hydra-api/device/core/v1" 11 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 12 | "open-hydra/pkg/database" 13 | openHydraK8s "open-hydra/pkg/open-hydra/k8s" 14 | "strings" 15 | 16 | "github.com/emicklei/go-restful/v3" 17 | "gopkg.in/yaml.v2" 18 | "k8s.io/client-go/kubernetes" 19 | ) 20 | 21 | const ( 22 | openHydraHeaderUser = "Open-Hydra-User" 23 | openHydraHeaderRole = "Open-Hydra-Role" 24 | openHydraAuthStringHeader = "Open-Hydra-Auth" 25 | ) 26 | 27 | type CacheDevices map[string]*xDeviceV1.Device 28 | 29 | type OpenHydraRouteBuilder struct { 30 | Database database.IDataBase 31 | //Config *config.OpenHydraServerConfig 32 | RootWS *restful.WebService 33 | // this is local cache relation between user and device 34 | CacheDevices CacheDevices 35 | kubeClient *kubernetes.Clientset 36 | k8sHelper openHydraK8s.IOpenHydraK8sHelper 37 | authorizationMap map[string]map[string]int 38 | cfg *config.OpenHydraServerConfig 39 | } 40 | 41 | func NewOpenHydraRouteBuilder(db database.IDataBase, rootWS *restful.WebService, client *kubernetes.Clientset, k8sHelper openHydraK8s.IOpenHydraK8sHelper, cfg *config.OpenHydraServerConfig) *OpenHydraRouteBuilder { 42 | return &OpenHydraRouteBuilder{ 43 | Database: db, 44 | //Config: config, 45 | RootWS: rootWS, 46 | CacheDevices: map[string]*xDeviceV1.Device{}, 47 | kubeClient: client, 48 | authorizationMap: make(map[string]map[string]int), 49 | k8sHelper: k8sHelper, 50 | cfg: cfg, 51 | } 52 | } 53 | 54 | func (builder *OpenHydraRouteBuilder) Filter(r1 *restful.Request, r2 *restful.Response, fc *restful.FilterChain) { 55 | // here you can put your authentication and authorization logic 56 | if builder.AuthAndAuthorization(r1, r2) { 57 | fc.ProcessFilter(r1, r2) 58 | } 59 | } 60 | 61 | func (builder *OpenHydraRouteBuilder) AuthAndAuthorization(r1 *restful.Request, r2 *restful.Response) bool { 62 | if strings.HasPrefix(r1.Request.URL.Path, fmt.Sprintf("/apis/%s/v1/%s/login/", option.GroupVersion.Group, OpenHydraUserPath)) { 63 | // login does not need authentication and authorization 64 | return true 65 | } 66 | 67 | switch r1.Request.URL.Path { 68 | case "/apis", "/apis/", fmt.Sprintf("/apis/%s", option.GroupVersion.Group), fmt.Sprintf("/apis/%s/", option.GroupVersion.Group), fmt.Sprintf("/apis/%s/v1", option.GroupVersion.Group), fmt.Sprintf("/apis/%s/v1/", option.GroupVersion.Group): 69 | slog.Info(fmt.Sprintf("skip authentication and authorization for path: %s", r1.Request.URL.Path)) 70 | return true 71 | } 72 | 73 | basicAuth := r1.Request.Header.Get(openHydraAuthStringHeader) 74 | if basicAuth == "" { 75 | writeHttpResponseAndLogError(r2, http.StatusUnauthorized, fmt.Sprintf("no auth header found for path: %s", r1.Request.URL.Path)) 76 | return false 77 | } 78 | 79 | authTypeAndValue := strings.Split(basicAuth, " ") 80 | if len(authTypeAndValue) != 2 { 81 | writeHttpResponseAndLogError(r2, http.StatusUnauthorized, "format is not recognized") 82 | return false 83 | } 84 | 85 | if authTypeAndValue[0] != "Bearer" { 86 | writeHttpResponseAndLogError(r2, http.StatusUnauthorized, "only support Bearer") 87 | return false 88 | } 89 | 90 | credSet, err := base64.StdEncoding.DecodeString(authTypeAndValue[1]) 91 | if err != nil { 92 | writeHttpResponseAndLogError(r2, http.StatusUnauthorized, "decode base64 failed") 93 | return false 94 | } 95 | 96 | userAndPass := strings.Split(string(credSet), ":") 97 | if len(userAndPass) != 2 { 98 | writeHttpResponseAndLogError(r2, http.StatusUnauthorized, "auth format is not recognized") 99 | return false 100 | } 101 | 102 | user, err := builder.Database.LoginUser(userAndPass[0], userAndPass[1]) 103 | if err != nil { 104 | writeHttpResponseAndLogError(r2, http.StatusInternalServerError, "login failed") 105 | return false 106 | } 107 | 108 | if !builder.authorization(r1, user) { 109 | writeHttpResponseAndLogError(r2, http.StatusForbidden, fmt.Sprintf("user: %s do not have the right to access path: %s", user.Name, r1.Request.URL.Path)) 110 | return false 111 | } else { 112 | r1.Request.Header.Set(openHydraHeaderUser, user.Name) 113 | r1.Request.Header.Set(openHydraHeaderRole, fmt.Sprintf("%d", user.Spec.Role)) 114 | } 115 | 116 | return true 117 | } 118 | 119 | func (builder *OpenHydraRouteBuilder) authorization(r1 *restful.Request, user *xUserV1.OpenHydraUser) bool { 120 | relPath := strings.ReplaceAll(r1.SelectedRoutePath(), fmt.Sprintf("/apis/%s/v1", option.GroupVersion.Group), "") 121 | if _, found := builder.authorizationMap[relPath]; !found { 122 | slog.Warn(fmt.Sprintf("no authorization found for path: %s", relPath)) 123 | return false 124 | } 125 | 126 | if _, found := builder.authorizationMap[relPath][r1.Request.Method]; !found { 127 | slog.Warn(fmt.Sprintf("no authorization found for path: %s with http method: %s", relPath, r1.Request.Method)) 128 | return false 129 | } 130 | 131 | // simply xor it 132 | // user.role=1 | route.required=3 | 1 == user.role that saids it gets right to access the route 133 | return user.Spec.Role == builder.authorizationMap[relPath][r1.Request.Method]&user.Spec.Role 134 | } 135 | 136 | func (builder *OpenHydraRouteBuilder) addPathAuthorization(relPath, httpMethod string, requiredRole int) { 137 | if _, found := builder.authorizationMap[relPath]; !found { 138 | builder.authorizationMap[relPath] = make(map[string]int) 139 | builder.authorizationMap[relPath][httpMethod] = requiredRole 140 | } else { 141 | builder.authorizationMap[relPath][httpMethod] = requiredRole 142 | } 143 | } 144 | 145 | func (builder *OpenHydraRouteBuilder) GetServerConfigFromConfigMap() (*config.OpenHydraServerConfig, error) { 146 | configMap, err := builder.k8sHelper.GetConfigMap("open-hydra-config", "open-hydra") 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | if configMap.Data == nil { 152 | return nil, fmt.Errorf("config map data is empty") 153 | } 154 | 155 | serverConfig := config.DefaultConfig() 156 | 157 | err = yaml.Unmarshal([]byte(configMap.Data["config.yaml"]), serverConfig) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return serverConfig, nil 163 | } 164 | -------------------------------------------------------------------------------- /deploy/mysql-operator.yaml: -------------------------------------------------------------------------------- 1 | # The main role for the operator 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: mysql-operator 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["pods"] 9 | verbs: ["get", "list", "watch", "patch"] 10 | - apiGroups: [""] 11 | resources: ["pods/status"] 12 | verbs: ["get", "patch", "update", "watch"] 13 | # Kopf needs patch on secrets or the sidecar will throw 14 | # The operator needs this verb to be able to pass it to the sidecar 15 | - apiGroups: [""] 16 | resources: ["secrets"] 17 | verbs: ["get", "create", "list", "watch", "patch"] 18 | - apiGroups: [""] 19 | resources: ["configmaps"] 20 | verbs: ["get", "create", "update", "list", "watch", "patch", "delete"] 21 | - apiGroups: [""] 22 | resources: ["services"] 23 | verbs: ["get", "create", "list", "update", "delete", "patch"] 24 | - apiGroups: [""] 25 | resources: ["serviceaccounts"] 26 | verbs: ["get", "create"] 27 | - apiGroups: [""] 28 | resources: ["events"] 29 | verbs: ["create", "patch", "update"] 30 | - apiGroups: ["rbac.authorization.k8s.io"] 31 | resources: ["rolebindings"] 32 | verbs: ["get", "create"] 33 | - apiGroups: ["policy"] 34 | resources: ["poddisruptionbudgets"] 35 | verbs: ["get", "create"] 36 | - apiGroups: ["batch"] 37 | resources: ["jobs"] 38 | verbs: ["create"] 39 | - apiGroups: ["batch"] 40 | resources: ["cronjobs"] 41 | verbs: ["get", "create", "update", "delete"] 42 | - apiGroups: ["apps"] 43 | resources: ["deployments", "statefulsets"] 44 | verbs: ["get", "create", "patch", "update", "watch", "delete"] 45 | - apiGroups: ["mysql.oracle.com"] 46 | resources: ["*"] 47 | verbs: ["*"] 48 | - apiGroups: ["zalando.org"] 49 | resources: ["*"] 50 | verbs: ["get", "patch", "list", "watch"] 51 | # Kopf: runtime observation of namespaces & CRDs (addition/deletion). 52 | - apiGroups: [apiextensions.k8s.io] 53 | resources: [customresourcedefinitions] 54 | verbs: [list, watch] 55 | - apiGroups: [""] 56 | resources: [namespaces] 57 | verbs: [list, watch] 58 | - apiGroups: ["monitoring.coreos.com"] 59 | resources: ["servicemonitors"] 60 | verbs: ["get", "create", "patch", "update", "delete"] 61 | --- 62 | # role for the server sidecar 63 | apiVersion: rbac.authorization.k8s.io/v1 64 | kind: ClusterRole 65 | metadata: 66 | name: mysql-sidecar 67 | rules: 68 | - apiGroups: [""] 69 | resources: ["pods"] 70 | verbs: ["get", "list", "watch", "patch"] 71 | - apiGroups: [""] 72 | resources: ["pods/status"] 73 | verbs: ["get", "patch", "update", "watch"] 74 | # Kopf needs patch on secrets or the sidecar will throw 75 | - apiGroups: [""] 76 | resources: ["secrets"] 77 | verbs: ["get", "create", "list", "watch", "patch"] 78 | - apiGroups: [""] 79 | resources: ["configmaps"] 80 | verbs: ["get", "create", "list", "watch", "patch"] 81 | - apiGroups: [""] 82 | resources: ["services"] 83 | verbs: ["get", "create", "list", "update"] 84 | - apiGroups: [""] 85 | resources: ["serviceaccounts"] 86 | verbs: ["get", "create"] 87 | - apiGroups: [""] 88 | resources: ["events"] 89 | verbs: ["create", "patch", "update"] 90 | - apiGroups: ["apps"] 91 | resources: ["deployments"] 92 | verbs: ["get", "patch"] 93 | - apiGroups: ["mysql.oracle.com"] 94 | resources: ["innodbclusters"] 95 | verbs: ["get", "watch", "list"] 96 | - apiGroups: ["mysql.oracle.com"] 97 | resources: ["mysqlbackups"] 98 | verbs: ["create", "get", "list", "patch", "update", "watch", "delete"] 99 | - apiGroups: ["mysql.oracle.com"] 100 | resources: ["mysqlbackups/status"] 101 | verbs: ["get", "patch", "update", "watch"] 102 | --- 103 | # Give access to the operator 104 | apiVersion: rbac.authorization.k8s.io/v1 105 | kind: ClusterRoleBinding 106 | metadata: 107 | name: mysql-operator-rolebinding 108 | subjects: 109 | - kind: ServiceAccount 110 | name: mysql-operator-sa 111 | namespace: mysql-operator 112 | # TODO The following entry is for dev purposes only 113 | #- kind: Group 114 | # name: system:serviceaccounts 115 | # apiGroup: rbac.authorization.k8s.io 116 | roleRef: 117 | kind: ClusterRole 118 | name: mysql-operator 119 | apiGroup: rbac.authorization.k8s.io 120 | --- 121 | apiVersion: zalando.org/v1 122 | kind: ClusterKopfPeering 123 | metadata: 124 | name: mysql-operator 125 | --- 126 | apiVersion: v1 127 | kind: Namespace 128 | metadata: 129 | name: mysql-operator 130 | --- 131 | apiVersion: v1 132 | kind: ServiceAccount 133 | metadata: 134 | name: mysql-operator-sa 135 | namespace: mysql-operator 136 | --- 137 | apiVersion: apps/v1 138 | kind: Deployment 139 | metadata: 140 | name: mysql-operator 141 | namespace: mysql-operator 142 | labels: 143 | version: "1.0" 144 | app.kubernetes.io/name: mysql-operator 145 | app.kubernetes.io/instance: mysql-operator 146 | app.kubernetes.io/version: 8.2.0-2.1.1 147 | app.kubernetes.io/component: controller 148 | app.kubernetes.io/managed-by: mysql-operator 149 | app.kubernetes.io/created-by: mysql-operator 150 | spec: 151 | replicas: 1 152 | selector: 153 | matchLabels: 154 | name: mysql-operator 155 | template: 156 | metadata: 157 | labels: 158 | name: mysql-operator 159 | spec: 160 | containers: 161 | - name: mysql-operator 162 | image: container-registry.oracle.com/mysql/community-operator:8.2.0-2.1.1 163 | imagePullPolicy: IfNotPresent 164 | args: 165 | [ 166 | "mysqlsh", 167 | "--log-level=@INFO", 168 | "--pym", 169 | "mysqloperator", 170 | "operator", 171 | ] 172 | env: 173 | - name: MYSQLSH_USER_CONFIG_HOME 174 | value: /mysqlsh 175 | - name: MYSQLSH_CREDENTIAL_STORE_SAVE_PASSWORDS 176 | value: never 177 | readinessProbe: 178 | exec: 179 | command: 180 | - cat 181 | - /tmp/mysql-operator-ready 182 | initialDelaySeconds: 1 183 | periodSeconds: 3 184 | volumeMounts: 185 | - name: mysqlsh-home 186 | mountPath: /mysqlsh 187 | - name: tmpdir 188 | mountPath: /tmp 189 | securityContext: 190 | runAsUser: 2 191 | allowPrivilegeEscalation: false 192 | privileged: false 193 | readOnlyRootFilesystem: true 194 | volumes: 195 | - name: mysqlsh-home 196 | emptyDir: {} 197 | - name: tmpdir 198 | emptyDir: {} 199 | serviceAccountName: mysql-operator-sa 200 | -------------------------------------------------------------------------------- /asserts/mysql-deploy-operator.yaml: -------------------------------------------------------------------------------- 1 | # The main role for the operator 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: mysql-operator 6 | rules: 7 | - apiGroups: [""] 8 | resources: ["pods"] 9 | verbs: ["get", "list", "watch", "patch"] 10 | - apiGroups: [""] 11 | resources: ["pods/status"] 12 | verbs: ["get", "patch", "update", "watch"] 13 | # Kopf needs patch on secrets or the sidecar will throw 14 | # The operator needs this verb to be able to pass it to the sidecar 15 | - apiGroups: [""] 16 | resources: ["secrets"] 17 | verbs: ["get", "create", "list", "watch", "patch"] 18 | - apiGroups: [""] 19 | resources: ["configmaps"] 20 | verbs: ["get", "create", "update", "list", "watch", "patch", "delete"] 21 | - apiGroups: [""] 22 | resources: ["services"] 23 | verbs: ["get", "create", "list", "update", "delete", "patch"] 24 | - apiGroups: [""] 25 | resources: ["serviceaccounts"] 26 | verbs: ["get", "create"] 27 | - apiGroups: [""] 28 | resources: ["events"] 29 | verbs: ["create", "patch", "update"] 30 | - apiGroups: ["rbac.authorization.k8s.io"] 31 | resources: ["rolebindings"] 32 | verbs: ["get", "create"] 33 | - apiGroups: ["policy"] 34 | resources: ["poddisruptionbudgets"] 35 | verbs: ["get", "create"] 36 | - apiGroups: ["batch"] 37 | resources: ["jobs"] 38 | verbs: ["create"] 39 | - apiGroups: ["batch"] 40 | resources: ["cronjobs"] 41 | verbs: ["get", "create", "update", "delete"] 42 | - apiGroups: ["apps"] 43 | resources: ["deployments", "statefulsets"] 44 | verbs: ["get", "create", "patch", "update", "watch", "delete"] 45 | - apiGroups: ["mysql.oracle.com"] 46 | resources: ["*"] 47 | verbs: ["*"] 48 | - apiGroups: ["zalando.org"] 49 | resources: ["*"] 50 | verbs: ["get", "patch", "list", "watch"] 51 | # Kopf: runtime observation of namespaces & CRDs (addition/deletion). 52 | - apiGroups: [apiextensions.k8s.io] 53 | resources: [customresourcedefinitions] 54 | verbs: [list, watch] 55 | - apiGroups: [""] 56 | resources: [namespaces] 57 | verbs: [list, watch] 58 | - apiGroups: ["monitoring.coreos.com"] 59 | resources: ["servicemonitors"] 60 | verbs: ["get", "create", "patch", "update", "delete"] 61 | --- 62 | # role for the server sidecar 63 | apiVersion: rbac.authorization.k8s.io/v1 64 | kind: ClusterRole 65 | metadata: 66 | name: mysql-sidecar 67 | rules: 68 | - apiGroups: [""] 69 | resources: ["pods"] 70 | verbs: ["get", "list", "watch", "patch"] 71 | - apiGroups: [""] 72 | resources: ["pods/status"] 73 | verbs: ["get", "patch", "update", "watch"] 74 | # Kopf needs patch on secrets or the sidecar will throw 75 | - apiGroups: [""] 76 | resources: ["secrets"] 77 | verbs: ["get", "create", "list", "watch", "patch"] 78 | - apiGroups: [""] 79 | resources: ["configmaps"] 80 | verbs: ["get", "create", "list", "watch", "patch"] 81 | - apiGroups: [""] 82 | resources: ["services"] 83 | verbs: ["get", "create", "list", "update"] 84 | - apiGroups: [""] 85 | resources: ["serviceaccounts"] 86 | verbs: ["get", "create"] 87 | - apiGroups: [""] 88 | resources: ["events"] 89 | verbs: ["create", "patch", "update"] 90 | - apiGroups: ["apps"] 91 | resources: ["deployments"] 92 | verbs: ["get", "patch"] 93 | - apiGroups: ["mysql.oracle.com"] 94 | resources: ["innodbclusters"] 95 | verbs: ["get", "watch", "list"] 96 | - apiGroups: ["mysql.oracle.com"] 97 | resources: ["mysqlbackups"] 98 | verbs: ["create", "get", "list", "patch", "update", "watch", "delete"] 99 | - apiGroups: ["mysql.oracle.com"] 100 | resources: ["mysqlbackups/status"] 101 | verbs: ["get", "patch", "update", "watch"] 102 | --- 103 | # Give access to the operator 104 | apiVersion: rbac.authorization.k8s.io/v1 105 | kind: ClusterRoleBinding 106 | metadata: 107 | name: mysql-operator-rolebinding 108 | subjects: 109 | - kind: ServiceAccount 110 | name: mysql-operator-sa 111 | namespace: mysql-operator 112 | # TODO The following entry is for dev purposes only 113 | #- kind: Group 114 | # name: system:serviceaccounts 115 | # apiGroup: rbac.authorization.k8s.io 116 | roleRef: 117 | kind: ClusterRole 118 | name: mysql-operator 119 | apiGroup: rbac.authorization.k8s.io 120 | --- 121 | apiVersion: zalando.org/v1 122 | kind: ClusterKopfPeering 123 | metadata: 124 | name: mysql-operator 125 | --- 126 | apiVersion: v1 127 | kind: Namespace 128 | metadata: 129 | name: mysql-operator 130 | --- 131 | apiVersion: v1 132 | kind: ServiceAccount 133 | metadata: 134 | name: mysql-operator-sa 135 | namespace: mysql-operator 136 | --- 137 | apiVersion: apps/v1 138 | kind: Deployment 139 | metadata: 140 | name: mysql-operator 141 | namespace: mysql-operator 142 | labels: 143 | version: "1.0" 144 | app.kubernetes.io/name: mysql-operator 145 | app.kubernetes.io/instance: mysql-operator 146 | app.kubernetes.io/version: 8.2.0-2.1.1 147 | app.kubernetes.io/component: controller 148 | app.kubernetes.io/managed-by: mysql-operator 149 | app.kubernetes.io/created-by: mysql-operator 150 | spec: 151 | replicas: 1 152 | selector: 153 | matchLabels: 154 | name: mysql-operator 155 | template: 156 | metadata: 157 | labels: 158 | name: mysql-operator 159 | spec: 160 | containers: 161 | - name: mysql-operator 162 | image: container-registry.oracle.com/mysql/community-operator:8.2.0-2.1.1 163 | imagePullPolicy: IfNotPresent 164 | args: 165 | [ 166 | "mysqlsh", 167 | "--log-level=@INFO", 168 | "--pym", 169 | "mysqloperator", 170 | "operator", 171 | ] 172 | env: 173 | - name: MYSQLSH_USER_CONFIG_HOME 174 | value: /mysqlsh 175 | - name: MYSQLSH_CREDENTIAL_STORE_SAVE_PASSWORDS 176 | value: never 177 | readinessProbe: 178 | exec: 179 | command: 180 | - cat 181 | - /tmp/mysql-operator-ready 182 | initialDelaySeconds: 1 183 | periodSeconds: 3 184 | volumeMounts: 185 | - name: mysqlsh-home 186 | mountPath: /mysqlsh 187 | - name: tmpdir 188 | mountPath: /tmp 189 | securityContext: 190 | runAsUser: 2 191 | allowPrivilegeEscalation: false 192 | privileged: false 193 | readOnlyRootFilesystem: true 194 | volumes: 195 | - name: mysqlsh-home 196 | emptyDir: {} 197 | - name: tmpdir 198 | emptyDir: {} 199 | serviceAccountName: mysql-operator-sa 200 | -------------------------------------------------------------------------------- /pkg/open-hydra/comment.go: -------------------------------------------------------------------------------- 1 | package openhydra 2 | 3 | import ( 4 | "encoding/json" 5 | stdErr "errors" 6 | "fmt" 7 | "log/slog" 8 | "strings" 9 | 10 | "open-hydra/cmd/open-hydra-server/app/config" 11 | xDeviceV1 "open-hydra/pkg/apis/open-hydra-api/device/core/v1" 12 | xUserV1 "open-hydra/pkg/apis/open-hydra-api/user/core/v1" 13 | "open-hydra/pkg/open-hydra/apis" 14 | "open-hydra/pkg/open-hydra/k8s" 15 | "open-hydra/pkg/util" 16 | 17 | "github.com/emicklei/go-restful/v3" 18 | coreV1 "k8s.io/api/core/v1" 19 | "k8s.io/apimachinery/pkg/api/errors" 20 | "k8s.io/apimachinery/pkg/api/resource" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | const OpenhydraNamespace = "open-hydra" 25 | 26 | type HttpErrMsg struct { 27 | Error string `json:"errMsg"` 28 | } 29 | 30 | func writeHttpResponseAndLogError(response *restful.Response, httpStatusCode int, err string) { 31 | slog.Error(err) 32 | response.WriteHeader(httpStatusCode) 33 | response.WriteAsJson(HttpErrMsg{Error: err}) 34 | } 35 | 36 | func reasonAndCodeForError(err error) (metav1.StatusReason, int32) { 37 | if status, ok := err.(errors.APIStatus); ok || stdErr.As(err, &status) { 38 | return status.Status().Reason, status.Status().Code 39 | } 40 | return metav1.StatusReasonUnknown, 500 41 | } 42 | 43 | func writeAPIStatusError(response *restful.Response, err error) { 44 | _, code := reasonAndCodeForError(err) 45 | writeHttpResponseAndLogError(response, int(code), err.Error()) 46 | } 47 | 48 | func combineDeviceList(pods []coreV1.Pod, services []coreV1.Service, users xUserV1.OpenHydraUserList, config *config.OpenHydraServerConfig) []xDeviceV1.Device { 49 | podFlat := make(map[string]coreV1.Pod) 50 | 51 | // now wo are going combine user and pod 52 | // first we put all pod into a map with label app as key 53 | for _, pod := range pods { 54 | if _, found := pod.Labels[k8s.OpenHydraUserLabelKey]; !found { 55 | continue 56 | } 57 | podFlat[pod.Labels[k8s.OpenHydraUserLabelKey]] = pod 58 | } 59 | 60 | serviceFlat := make(map[string]coreV1.Service) 61 | for _, service := range services { 62 | if _, found := service.Labels[k8s.OpenHydraUserLabelKey]; !found { 63 | continue 64 | } 65 | serviceFlat[service.Labels[k8s.OpenHydraUserLabelKey]] = service 66 | } 67 | 68 | var result []xDeviceV1.Device 69 | 70 | for _, user := range users.Items { 71 | 72 | device := xDeviceV1.Device{} 73 | util.FillKindAndApiVersion(&device.TypeMeta, "Device") 74 | device.Name = user.Name 75 | device.Namespace = user.Namespace 76 | device.Spec.Role = user.Spec.Role 77 | device.Spec.ChineseName = user.Spec.ChineseName 78 | if _, found := podFlat[user.Name]; found { 79 | // only fill up device if we found a pod 80 | device.Labels = podFlat[user.Name].Labels 81 | device.Spec.DeviceCpu = podFlat[user.Name].Spec.Containers[0].Resources.Limits.Cpu().String() 82 | device.Spec.DeviceRam = podFlat[user.Name].Spec.Containers[0].Resources.Limits.Memory().String() 83 | device.Spec.DeviceIP = podFlat[user.Name].Status.PodIP 84 | device.Spec.DeviceName = podFlat[user.Name].Name 85 | device.Spec.DeviceNamespace = podFlat[user.Name].Namespace 86 | if _, foundGpuDriver := podFlat[user.Name].Spec.Containers[0].Resources.Requests[coreV1.ResourceName(config.DefaultGpuDriver)]; foundGpuDriver { 87 | device.Spec.DeviceType = "gpu" 88 | device.Spec.GpuDriver = config.DefaultGpuDriver 89 | device.Spec.DeviceGpu = uint8(podFlat[user.Name].Spec.Containers[0].Resources.Requests.Name(coreV1.ResourceName(config.DefaultGpuDriver), resource.DecimalSI).Value()) 90 | } else { 91 | device.Spec.DeviceType = "cpu" 92 | } 93 | device.Spec.OpenHydraUsername = user.Name 94 | // Todo: add line no 95 | device.Spec.LineNo = "0" 96 | device.CreationTimestamp = podFlat[user.Name].CreationTimestamp 97 | device.Spec.DeviceStatus = string(podFlat[user.Name].Status.Phase) 98 | if podFlat[user.Name].DeletionTimestamp != nil { 99 | device.Spec.DeviceStatus = "Terminating" 100 | } 101 | if _, found := podFlat[user.Name].Labels[k8s.OpenHydraSandboxKey]; found { 102 | device.Spec.SandboxName = podFlat[user.Name].Labels[k8s.OpenHydraSandboxKey] 103 | } 104 | } 105 | 106 | if _, found := serviceFlat[user.Name]; found { 107 | var portURLs []string 108 | for _, port := range serviceFlat[user.Name].Spec.Ports { 109 | portURLs = append(portURLs, combineUrl(config.ServerIP, user.Name, port.Name, port.NodePort, config.EnableJupyterLabBaseURL, config)) 110 | } 111 | device.Spec.SandboxURLs = strings.Join(portURLs, ",") 112 | } 113 | result = append(result, device) 114 | } 115 | return result 116 | } 117 | 118 | func combineUrl(serverAddress, username, portName string, port int32, enableJupyterLabBaseURL bool, config *config.OpenHydraServerConfig) string { 119 | addressSet := strings.Split(serverAddress, ",") 120 | if len(addressSet) <= 1 { 121 | if enableJupyterLabBaseURL { 122 | if _, ok := config.ApplyPortNameForIngress[portName]; ok { 123 | return fmt.Sprintf("http://%s:%d/%s/%s", serverAddress, config.IngressPort, fmt.Sprintf("%s-%s", username, portName), config.ApplyPortNameForIngress[portName]) 124 | } else { 125 | return fmt.Sprintf("http://%s:%d/%s", serverAddress, config.IngressPort, username) 126 | } 127 | } else { 128 | return fmt.Sprintf("http://%s:%d", serverAddress, port) 129 | } 130 | } 131 | var result []string 132 | for _, address := range addressSet { 133 | result = append(result, fmt.Sprintf("http://%s:%d", address, port)) 134 | } 135 | return strings.Join(result, ",") 136 | } 137 | 138 | func ParseJsonToPluginList(jsonData string) (apis.PluginList, error) { 139 | var plugins apis.PluginList 140 | err := json.Unmarshal([]byte(jsonData), &plugins) 141 | if err != nil { 142 | return plugins, err 143 | } 144 | return plugins, nil 145 | } 146 | 147 | func preCreateUserDir(volumes []apis.Volume, username string, config *config.OpenHydraServerConfig) error { 148 | for index, volume := range volumes { 149 | if volume.HostPath == nil { 150 | continue 151 | } 152 | dirToCreate := volume.HostPath.Path 153 | if strings.Contains(volume.HostPath.Path, "{username}") || strings.Contains(volume.HostPath.Path, "{workspace}") { 154 | // only private dir needs to be create on pod booting 155 | dirToCreate = strings.Replace(dirToCreate, "{username}", username, -1) 156 | dirToCreate = strings.Replace(dirToCreate, "{workspace}", config.WorkspacePath, -1) 157 | err := util.CreateDirIfNotExists(dirToCreate) 158 | if err != nil { 159 | return err 160 | } 161 | volumes[index].HostPath.Path = dirToCreate 162 | 163 | // copy a file read me to the workspace 164 | err = util.CopyFile(fmt.Sprintf("%s/%s", config.WorkspacePath, "README.md"), fmt.Sprintf("%s/%s", dirToCreate, "README.md")) 165 | if err != nil { 166 | slog.Error("copy readme failed", "msg", err) 167 | } 168 | 169 | continue 170 | } 171 | if strings.Contains(volume.HostPath.Path, "{dataset-public}") { 172 | dirToCreate = strings.Replace(dirToCreate, "{dataset-public}", config.PublicDatasetBasePath, -1) 173 | } 174 | if strings.Contains(volume.HostPath.Path, "{course-public}") { 175 | dirToCreate = strings.Replace(dirToCreate, "{course-public}", config.PublicCourseBasePath, -1) 176 | } 177 | 178 | volumes[index].HostPath.Path = dirToCreate 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /docs/dev-guide.md: -------------------------------------------------------------------------------- 1 | # open-hydra server is a api server for handling open-hydra requests 2 | 3 | ## how to debug it locally without actually deploy it to k8s 4 | 5 | * k8s cluster 1.23+ 6 | * mysql-operator 7 | * golang 1.21.4+ 8 | 9 | ```bash 10 | # create service account 11 | $ kubectl create sa admin 12 | $ kubectl create rolebinding my-service-account-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:admin 13 | $ kubectl create rolebinding -n kube-system my-service-account-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:admin 14 | $ kubectl create clusterrolebinding my-service-account-cluster-admin --clusterrole=cluster-admin --serviceaccount=default:admin 15 | 16 | # create a pod with service account to fake the pod environment locally 17 | apiVersion: v1 18 | kind: Pod 19 | metadata: 20 | name: my-pod 21 | spec: 22 | serviceAccountName: admin 23 | containers: 24 | - name: my-container 25 | image: centos 26 | command: ["init"] 27 | 28 | # copy ca.crt namespace token in /var/run/secrets/kubernetes.io/serviceaccount to local 29 | $ kubectl exec -it my-pod -- bash 30 | 31 | # create mysql cluster with operator 32 | $ kubectl apply -f deploy/mysql-operator-crds.yaml 33 | $ kubectl apply -f deploy/mysql-operator.yaml 34 | 35 | # ensure mysql operator it's ready 36 | # then create mysql cluster 37 | $ kubectl apply -f deploy/mysql-instance.yaml 38 | 39 | # check see if mysql cluster is ready 40 | $ kubectl get pods -n mysql-operator 41 | NAME READY STATUS RESTARTS AGE 42 | mycluster-0 2/2 Running 0 18h 43 | mycluster-router-5d74f97d5b-wpqj2 1/1 Running 0 17h 44 | 45 | # expose mysql service 46 | $ kubectl expose pod mycluster-0 -n mysql-operator --type=NodePort 47 | 48 | # create a dir .open-hydra-server for open-hydra-server config 49 | $ mkdir .open-hydra-server 50 | $ cd .open-hydra-server 51 | $ vi config.yaml 52 | # input following setting 53 | podAllocatableLimit: -1 # not pod limit is set will use pod limit of k8s node by default 54 | defaultCpuPerDevice: 2000 # cpu per pod default 2 55 | defaultRamPerDevice: 8192 # memory per pod default 8Gi 56 | defaultGpuPerDevice: 0 # gpu per pod default 0, keep it 0 unless you have tons of gpus 57 | datasetBasePath: /mnt/public-dataset # where dataset keep on server dir 58 | datasetVolumeType: hostpath # so far we only support hostpath 59 | jupyterLabHostBaseDir: /mnt/jupyter-lab # where user custom code of jupyter-lab on server dir 60 | imageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/jupyter:Python-3.8.18" 61 | vscodeImageRepo: "registry.cn-shanghai.aliyuncs.com/openhydra/vscode:1.85.1" 62 | defaultGpuDriver: nvidia.com/gpu 63 | serverIP: "172.16.151.70" 64 | patchResourceNotRelease: true 65 | disableAuth: true 66 | mysqlConfig: 67 | address: mycluster-instances.mysql-operator.svc 68 | port: 3306 69 | username: root 70 | password: openhydra 71 | databaseName: openhydra 72 | protocol: tcp 73 | leaderElection: 74 | leaderElect: false 75 | leaseDuration: 30s 76 | renewDeadline: 15s 77 | retryPeriod: 5s 78 | resourceLock: endpointsleases 79 | resourceName: open-hydra-api-leader-lock 80 | resourceNamespace: open-hydra 81 | 82 | 83 | # debug it 84 | # checkout out .vscode/launch.json if you use vscode 85 | 86 | ``` 87 | 88 | ## db create table(option) 89 | 90 | * It's ok not to create table manually, table will be created automatically when you start the server by `mysql.go` 91 | 92 | ```sql 93 | # 创建 user 表 94 | CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255), role INT , ch_name NVARCHAR(255) , description NVARCHAR(255) , email VARCHAR(255) , password VARCHAR(255) , UNIQUE (username) ); 95 | 96 | # 创建 Dataset 表 97 | CREATE TABLE dataset ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), description NVARCHAR(255) , last_update DATETIME , create_time DATETIME , UNIQUE (name) ); 98 | 99 | # 创建 Course 表 100 | CREATE TABLE course ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), description NVARCHAR(255) , created_by NVARCHAR(255) , last_update DATETIME , create_time DATETIME , UNIQUE (name) ); 101 | ``` 102 | 103 | ## debug route with curl 104 | 105 | * if disableAuth=false, you need to use basic auth to access apiserver by adding `--header 'open-hydra-auth: Bearer xxxxxxx'` you can get your access token(xxxxxxx) by running command `echo -n 'admin:admin' | base64 -w 0` 106 | 107 | ```bash 108 | # create a user without basic auth 109 | $ curl -k --location -XPOST 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/openhydrausers' \ 110 | --header 'Content-Type: application/json' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key \ 111 | --data-raw '{ 112 | "metadata": { 113 | "name": "user1" 114 | }, 115 | "spec": { 116 | "chineseName": "first user, 117 | "description": "user1", 118 | "password": "password", 119 | "email": "student1@gmail.com", 120 | "role": 1 121 | }, 122 | "status": {} 123 | }' 124 | 125 | 126 | # list users without basic auth 127 | $ curl -k --location 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/openhydrausers' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key 128 | 129 | # create device for user1 130 | $ curl -k --location -XPOST 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/devices' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key \ 131 | --header 'Content-Type: application/json' \ 132 | --data '{ 133 | "metadata": { 134 | "name": "user3" 135 | }, 136 | "spec": { 137 | "openHydraUsername": "user3" 138 | } 139 | }' 140 | 141 | # delete device for user1 142 | $ curl -k --location -XDELETE 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/devices/user1' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key 143 | 144 | # login 145 | $ curl -k --location -XPOST 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/openhydrausers/login/user1' \ 146 | --header 'Content-Type: application/json' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key \ 147 | --data-raw '{ 148 | "metadata": { 149 | "name": "user1" 150 | }, 151 | "spec": { 152 | "password": "password" 153 | } 154 | }' 155 | 156 | # update gpu settting 157 | $ curl -k --location -XPUT 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/settings/default' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key \ 4:01:49 PM 158 | --header 'Content-Type: application/json' \ 159 | --data '{ 160 | "metadata": { 161 | "name": "default" 162 | }, 163 | "spec": { 164 | "default_gpu_per_device": 0 165 | } 166 | }' 167 | 168 | # get gpu setting 169 | curl -k --location 'https://localhost:10443/apis/open-hydra-server.openhydra.io/v1/settings/default' --cert pki/apiserver-kubelet-client.crt --key pki/apiserver-kubelet-client.key 170 | 171 | # output 172 | {"metadata":{"name":"default"},"spec":{"default_gpu_per_device":0},"status":{}} 173 | ``` 174 | 175 | ## try manage everything with kubectl 176 | 177 | ```bash 178 | # when 'disableAuth' set to 'true' you can use kubectl to manage everything 179 | $ vi user2.yaml 180 | apiVersion: open-hydra-server.openhydra.io/v1 181 | kind: OpenHydraUser 182 | metadata: 183 | name: user2 184 | spec: 185 | chineseName: 2nd users 186 | description: user2 187 | email: user2@gmail.com 188 | password: password 189 | role: 2 190 | 191 | # create user 192 | $ kubectl apply -f user2.yaml 193 | # list user 194 | $ kubectl get openhydrausers 195 | 196 | # create device 197 | $ vi device1.yaml 198 | apiVersion: open-hydra-server.openhydra.io/v1 199 | kind: Device 200 | metadata: 201 | name: user2 202 | spec: 203 | studentName: user2 204 | 205 | # create device for user2 206 | $ kubectl create -f device1.yaml 207 | 208 | # list device 209 | $ kubectl get devices 210 | ``` 211 | 212 | ## reverse proxy 213 | 214 | deploy a reverse proxy to access open-hydra-server api directly but not secure, you should use it in your local environment 215 | 216 | ```bash 217 | # deploy reverse proxy 218 | $ kubectl create -f deploy/reverse-proxy.yaml 219 | 220 | # check it 221 | $ kubectl get svc -n open-hydra 222 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 223 | reverse-proxy ClusterIp 10.98.192.222 80:80/TCP 1m 224 | 225 | # check reverse proxy working 226 | $ curl http://reverse-proxy.open-hydra.svc/api/apis 227 | 228 | # check it over cluster ip 229 | $ curl http://10.98.192.222/api/apis 230 | ``` 231 | 232 | ## code commit 233 | 234 | * Please see [code of conduct](../code-of-conduct.md) before you commit your code 235 | 236 | ## modify api 237 | 238 | * After you modify you api property you should always run `make update-openapi` to update you openapi spec 239 | -------------------------------------------------------------------------------- /pkg/open-hydra/k8s/faker.go: -------------------------------------------------------------------------------- 1 | package k8s 2 | 3 | import ( 4 | "fmt" 5 | "open-hydra/cmd/open-hydra-server/app/config" 6 | 7 | "gopkg.in/yaml.v2" 8 | appsV1 "k8s.io/api/apps/v1" 9 | coreV1 "k8s.io/api/core/v1" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | type Fake struct { 15 | namespacedPod map[string][]coreV1.Pod 16 | namespacedDeploy map[string][]appsV1.Deployment 17 | namespacedService map[string][]coreV1.Service 18 | labelPod map[string][]coreV1.Pod 19 | labelDeploy map[string][]appsV1.Deployment 20 | labelService map[string][]coreV1.Service 21 | ServerConfig *config.OpenHydraServerConfig 22 | } 23 | 24 | func (f *Fake) Init() { 25 | f.namespacedPod = make(map[string][]coreV1.Pod) 26 | f.namespacedDeploy = make(map[string][]appsV1.Deployment) 27 | f.namespacedService = make(map[string][]coreV1.Service) 28 | f.labelPod = make(map[string][]coreV1.Pod) 29 | f.labelDeploy = make(map[string][]appsV1.Deployment) 30 | f.labelService = make(map[string][]coreV1.Service) 31 | f.ServerConfig = config.DefaultConfig() 32 | } 33 | 34 | func (f *Fake) ListDeploymentWithLabel(label, namespace string, client *kubernetes.Clientset) ([]appsV1.Deployment, error) { 35 | var result []appsV1.Deployment 36 | if _, ok := f.labelDeploy[label]; ok { 37 | result = f.labelDeploy[label] 38 | } 39 | return result, nil 40 | } 41 | func (f *Fake) ListPodWithLabel(label, namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) { 42 | var result []coreV1.Pod 43 | if _, ok := f.labelPod[label]; ok { 44 | result = f.labelPod[label] 45 | } 46 | return result, nil 47 | } 48 | func (f *Fake) ListPod(namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) { 49 | var result []coreV1.Pod 50 | if _, ok := f.namespacedPod[namespace]; ok { 51 | result = f.namespacedPod[namespace] 52 | } 53 | return result, nil 54 | } 55 | func (f *Fake) GetUserPods(label, namespace string, client *kubernetes.Clientset) ([]coreV1.Pod, error) { 56 | var result []coreV1.Pod 57 | if _, ok := f.labelPod[label]; ok { 58 | result = f.labelPod[label] 59 | } 60 | return result, nil 61 | } 62 | func (f *Fake) ListDeployment(namespace string, client *kubernetes.Clientset) ([]appsV1.Deployment, error) { 63 | var result []appsV1.Deployment 64 | if _, ok := f.namespacedDeploy[namespace]; ok { 65 | result = f.namespacedDeploy[namespace] 66 | } 67 | return result, nil 68 | } 69 | func (f *Fake) ListService(namespace string, client *kubernetes.Clientset) ([]coreV1.Service, error) { 70 | var result []coreV1.Service 71 | if _, ok := f.namespacedService[namespace]; ok { 72 | result = f.namespacedService[namespace] 73 | } 74 | return result, nil 75 | } 76 | func (f *Fake) DeleteUserDeployment(label, namespace string, client *kubernetes.Clientset) error { 77 | delete(f.labelDeploy, label) 78 | return nil 79 | } 80 | func (f *Fake) CreateDeployment(deployParameter *DeploymentParameters) error { 81 | label := fmt.Sprintf("%s=%s", OpenHydraUserLabelKey, deployParameter.Username) 82 | f.labelDeploy[label] = append(f.labelDeploy[label], appsV1.Deployment{}) 83 | f.namespacedDeploy[deployParameter.Namespace] = append(f.namespacedDeploy[deployParameter.Namespace], appsV1.Deployment{}) 84 | f.labelPod[label] = append(f.labelPod[label], coreV1.Pod{ 85 | ObjectMeta: v1.ObjectMeta{ 86 | Labels: map[string]string{ 87 | OpenHydraUserLabelKey: deployParameter.Username, 88 | OpenHydraSandboxKey: deployParameter.SandboxName, 89 | }, 90 | }, 91 | }) 92 | return nil 93 | } 94 | func (f *Fake) CreateService(namespace, studentID, ideType string, client *kubernetes.Clientset, ports map[string]int) error { 95 | label := fmt.Sprintf("%s=%s", OpenHydraUserLabelKey, studentID) 96 | f.labelService[label] = append(f.labelService[label], coreV1.Service{}) 97 | f.namespacedService[namespace] = append(f.namespacedService[namespace], coreV1.Service{}) 98 | return nil 99 | } 100 | func (f *Fake) DeleteUserService(label, namespace string, client *kubernetes.Clientset) error { 101 | delete(f.labelService, label) 102 | return nil 103 | } 104 | func (f *Fake) GetUserService(label, namespace string, client *kubernetes.Clientset) (*coreV1.Service, error) { 105 | var result *coreV1.Service 106 | if _, ok := f.labelService[label]; ok { 107 | result = &f.labelService[label][0] 108 | } else { 109 | return nil, fmt.Errorf("service not found") 110 | } 111 | return result, nil 112 | } 113 | func (f *Fake) DeleteUserReplicaSet(label, namespace string, client *kubernetes.Clientset) error { 114 | return nil 115 | } 116 | func (f *Fake) DeleteUserPod(label, namespace string, client *kubernetes.Clientset) error { 117 | delete(f.labelPod, label) 118 | return nil 119 | } 120 | 121 | func (f *Fake) GetConfigMap(name, namespace string) (*coreV1.ConfigMap, error) { 122 | if name == "openhydra-plugin" { 123 | return &coreV1.ConfigMap{ 124 | Data: map[string]string{ 125 | "plugins": `{ 126 | "defaultSandbox": "test", 127 | "sandboxes":{ 128 | "test": { 129 | "display_title": "test", 130 | "cpuImageName": "test", 131 | "gpuImageSet": { 132 | "nvidia.com/gpu": "nvidia-gpu-image", 133 | "amd.com/gpu": "" 134 | }, 135 | "icon_name": "test1.png", 136 | "command": ["test"], 137 | "description": "test", 138 | "developmentInfo": ["test"], 139 | "status": "test", 140 | "ports": [ 141 | 8888 142 | ], 143 | "volume_mounts": [ 144 | { 145 | "name": "jupyter-lab", 146 | "mount_path": "/root/notebook", 147 | "source_path": "/mnt/jupyter-lab" 148 | }, 149 | { 150 | "name": "public-dataset", 151 | "mount_path": "/root/notebook/dataset-public", 152 | "source_path": "/mnt/public-dataset" 153 | }, 154 | { 155 | "name": "public-course", 156 | "mount_path": "/mnt/public-course", 157 | "source_path": "/mnt/public-course" 158 | } 159 | ] 160 | }, 161 | "jupyter-lab": { 162 | "display_title": "jupyter-lab", 163 | "cpuImageName": "jupyter-lab-test", 164 | "gpuImageSet": { 165 | "nvidia.com/gpu": "nvidia-gpu-image", 166 | "amd.com/gpu": "" 167 | }, 168 | "icon_name": "test2.png", 169 | "command": ["jupyter-lab-test"], 170 | "description": "jupyter-lab-test", 171 | "developmentInfo": ["jupyter-lab-test"], 172 | "status": "running", 173 | "ports": [ 174 | 8888 175 | ], 176 | "volume_mounts": [ 177 | { 178 | "name": "jupyter-lab", 179 | "mount_path": "/root/notebook", 180 | "source_path": "/mnt/jupyter-lab" 181 | }, 182 | { 183 | "name": "public-dataset", 184 | "mount_path": "/root/notebook/dataset-public", 185 | "source_path": "/mnt/public-dataset" 186 | }, 187 | { 188 | "name": "public-course", 189 | "mount_path": "/mnt/public-course", 190 | "source_path": "/mnt/public-course" 191 | } 192 | ] 193 | }, 194 | "jupyter-lab-lot-ports": { 195 | "display_title": "jupyter-lab-lot-ports", 196 | "cpuImageName": "jupyter-lab-test", 197 | "gpuImageSet": { 198 | "nvidia.com/gpu": "nvidia-gpu-image", 199 | "amd.com/gpu": "" 200 | }, 201 | "icon_name": "test3.png", 202 | "command": ["jupyter-lab-test"], 203 | "description": "jupyter-lab-test", 204 | "developmentInfo": ["jupyter-lab-test"], 205 | "status": "running", 206 | "ports": [ 207 | 8888, 208 | 8889, 209 | 8890, 210 | 8891 211 | ], 212 | "volume_mounts": [ 213 | { 214 | "name": "jupyter-lab", 215 | "mount_path": "/root/notebook", 216 | "source_path": "/mnt/jupyter-lab" 217 | }, 218 | { 219 | "name": "public-dataset", 220 | "mount_path": "/root/notebook/dataset-public", 221 | "source_path": "/mnt/public-dataset" 222 | }, 223 | { 224 | "name": "public-course", 225 | "mount_path": "/mnt/public-course", 226 | "source_path": "/mnt/public-course" 227 | } 228 | ] 229 | }, 230 | "jupyter-lab-not-ports": { 231 | "display_title": "jupyter-lab-test", 232 | "cpuImageName": "jupyter-lab-test", 233 | "gpuImageSet": { 234 | "nvidia.com/gpu": "nvidia-gpu-image", 235 | "amd.com/gpu": "" 236 | }, 237 | "icon_name": "test4.png", 238 | "command": ["jupyter-lab-test"], 239 | "description": "jupyter-lab-test", 240 | "developmentInfo": ["jupyter-lab-test"], 241 | "status": "running" 242 | } 243 | }}`, 244 | }, 245 | }, nil 246 | } else { 247 | yamlData, err := yaml.Marshal(f.ServerConfig) 248 | if err != nil { 249 | return nil, err 250 | } 251 | return &coreV1.ConfigMap{ 252 | Data: map[string]string{ 253 | "config.yaml": string(yamlData), 254 | }, 255 | }, nil 256 | } 257 | } 258 | 259 | func (help *Fake) UpdateConfigMap(name, namespace string, data map[string]string) error { 260 | marshaledConfig := &config.OpenHydraServerConfig{} 261 | err := yaml.Unmarshal([]byte(data["config.yaml"]), marshaledConfig) 262 | if err != nil { 263 | return err 264 | } 265 | help.ServerConfig = marshaledConfig 266 | return nil 267 | } 268 | 269 | func (help *Fake) RunInformers(stopChan <-chan struct{}) { 270 | } 271 | --------------------------------------------------------------------------------