This commit is contained in:
howardshaw 2023-05-28 12:14:02 +08:00
commit 929cd70212
615 changed files with 114679 additions and 0 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
.vscode/
idea/
.DS_Store
vendor/
_output/
web/build/
web/node_modules/
web/.eslintcache
mysql-data

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
_output
.idea
*.log
tags
*.swp
**/*.pem
**/*.csr
*.db
womtool.jar
mysql-data
*.sqlite3
.vscode
web/build/
web/node_modules/
web/.eslintcache
**/*.DS_Store

230
.golangci.yaml Normal file
View File

@ -0,0 +1,230 @@
# Options for analysis running.
run:
# The default concurrency value is the number of available CPU.
concurrency: 4
# Timeout for analysis, e.g. 30s, 5m.
# Default: 1m
timeout: 5m
# Exit code when at least one issue was found.
# Default: 1
issues-exit-code: 2
# Include test files or not.
# Default: true
tests: false
# List of build tags, all linters use it.
# Default: [].
build-tags:
- mytag
# Which dirs to skip: issues from them won't be reported.
# Can use regexp here: `generated.*`, regexp is applied on full path,
# including the path prefix if one is set.
# Default value is empty list,
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work on Windows.
skip-dirs:
- src/external_libs
- autogenerated_by_my_lib
# Enables skipping of directories:
# - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
# Default: true
skip-dirs-use-default: false
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but there is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
skip-files:
- ".*\\.my\\.go$"
- lib/bad.go
# If set we pass it to "go list -mod={option}". From "go help modules":
# If invoked with -mod=readonly, the go command is disallowed from the implicit
# automatic updating of go.mod described above. Instead, it fails when any changes
# to go.mod are needed. This setting is most useful to check that go.mod does
# not need updates, such as in a continuous integration and testing system.
# If invoked with -mod=vendor, the go command assumes that the vendor
# directory holds the correct copies of dependencies and ignores
# the dependency descriptions in go.mod.
#
# Allowed values: readonly|vendor|mod
# By default, it isn't set.
modules-download-mode: readonly
# Allow multiple parallel golangci-lint instances running.
# If false (default) - golangci-lint acquires file lock on start.
allow-parallel-runners: false
# Define the Go version limit.
# Mainly related to generics support since go1.18.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
go: '1.19'
# output configuration options
output:
# Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions|teamcity
#
# Multiple can be specified by separating them by comma, output can be provided
# for each of them by separating format name and path by colon symbol.
# Output path can be either `stdout`, `stderr` or path to the file to write to.
# Example: "checkstyle:report.xml,json:stdout,colored-line-number"
#
# Default: colored-line-number
format: colored-line-number
# Print lines of code with issue.
# Default: true
print-issued-lines: false
# Print linter name in the end of issue text.
# Default: true
print-linter-name: false
# Make issues output unique by line.
# Default: true
uniq-by-line: false
# Add a prefix to the output file references.
# Default is no prefix.
path-prefix: ""
# Sort results by: filepath, line and column.
sort-results: false
linters-settings:
revive:
rules:
- name: exported
severity: warning
disabled: true
arguments:
- "checkPrivateReceivers"
- "sayRepetitiveInsteadOfStutters"
- "disableStutteringCheck"
goimports:
# A comma-separated list of prefixes, which, if set, checks import paths
# with the given prefixes are grouped after 3rd-party packages.
# Default: ""
local-prefixes: "github.com/Bio-OS/bioos"
linters:
# Disable all linters.
# Default: false
disable-all: true
# Enable specific linter
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable:
- ineffassign
- goimports
- govet
- revive
- unused
# Run only fast linters from enabled linters set (first run won't be fast)
# Default: false
fast: false
issues:
# List of regexps of issue texts to exclude.
#
# But independently of this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`.
# To list all excluded by default patterns execute `golangci-lint run --help`
#
# Default: https://golangci-lint.run/usage/false-positives/#default-exclusions
exclude:
- test/.*
- third_party/.*
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Independently of option `exclude` we use default exclude patterns,
# it can be disabled by this option.
# To list all excluded by default patterns execute `golangci-lint run --help`.
# Default: true.
exclude-use-default: true
# If set to true exclude and exclude-rules regular expressions become case-sensitive.
# Default: false
exclude-case-sensitive: false
# The list of ids of default excludes to include or disable.
# https://golangci-lint.run/usage/false-positives/#default-exclusions
# Default: []
include:
- EXC0001
- EXC0002
- EXC0003
- EXC0004
- EXC0005
- EXC0006
- EXC0007
- EXC0008
- EXC0009
- EXC0010
- EXC0011
- EXC0012
- EXC0013
- EXC0014
- EXC0015
# Maximum issues count per one linter.
# Set to 0 to disable.
# Default: 50
max-issues-per-linter: 0
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing large codebase.
# It's not practical to fix all existing issues at the moment of integration:
# much better don't allow issues in new code.
#
# Default: false.
new: false
# Show only new issues created after git revision `REV`.
# new-from-rev: HEAD
# Show only new issues created in git patch with set file path.
# new-from-patch: path/to/patch/file
# Fix found issues (if it's supported by the linter).
fix: true
severity:
# Set the default severity for issues.
#
# If severity rules are defined and the issues do not match or no severity is provided to the rule
# this will be the default severity applied.
# Severities should match the supported severity names of the selected out format.
# - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity
# - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel
# - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
# - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance
#
# Default value is an empty string.
default-severity: error
# If set to true `severity-rules` regular expressions become case-sensitive.
# Default: false
case-sensitive: true
# When a list of severity rules are provided, severity information will be added to lint issues.
# Severity rules have the same filtering capability as exclude rules
# except you are allowed to specify one matcher per severity rule.
# Only affects out formats that support setting severity information.
#
# Default: []
rules:
- linters:
- dupl
severity: info

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

53
Makefile Normal file
View File

@ -0,0 +1,53 @@
include hack/make-rules/common.mk
include hack/make-rules/copyright.mk
include hack/make-rules/image.mk
include hack/make-rules/golang.mk
include hack/make-rules/grpc.mk
include hack/make-rules/swagger.mk
include hack/make-rules/tools.mk
ROOT_PACKAGE=github.com/Bio-OS/bioos
VERSION ?= $(shell git describe --tags --always --dirty)
GIT_BRANCH ?= $(shell git branch | grep \* | cut -d ' ' -f2)
GIT_COMMIT ?= $(shell git rev-parse HEAD)
GIT_TREE_STATE ?= $(if $(shell git status --porcelain),dirty,clean)
GIT_BUILD_TIME ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GO_VERSION ?= $(shell go version)
.PHONY: build
build: go.build web
.PHONY: image
image: image.build
.PHONY: push
push: image.push
.PHONY: tools
tools:
@$(MAKE) tools.install
.PHONY: swagger
swagger: swagger.run
## web install
.PHONY: web.install
web.install:
npm --prefix=web install
## web dev
.PHONY: web.run
web.run:
npm --prefix=web run dev
## web build
.PHONY: web
web:
@echo "===========> Building web"
npm --prefix=web run build
.PHONY: clean
clean:
@echo "===========> Cleaning all build binary"
@-rm -vrf $(OUTPUT_DIR)

2
NOTICE Normal file
View File

@ -0,0 +1,2 @@
Copyright 2023 Beijing Volcano Engine Technology Ltd.
Copyright 2023 Guangzhou Laboratory

0
OWNERS Normal file
View File

720
README.md Normal file
View File

@ -0,0 +1,720 @@
# About Bio-OS
Bio-OS is an open-source platform for genomics users. It provides a workspace which encapsulates data, workflows, Notebooks, job history, etc. Bio-OS provides both GUI and CLI(Command Line Interface) to quickly setup and run workflows specified by languages like WDL and notebook as well.
## Bio-OS Concept
![](docs/static/bioos.png)
----
## Bio-OS Workspace
Workspace is a complete encapsulation of a bioinformatics research process, including data, environment, code, operational calculation procedures, results, and dashboard as an overview. It is the basic unit that realizes executable, transportable, reproducible, shareable and publishable scientific research and biological application.
![](docs/static/workspace.png)
----
## Architecture :
In Bio-OS, there are three parts in the architecture.
### Control layer:
1. Bioos-web: the front-end component of Bio-OS.
2. Bioctl is the command-line component of Bio-OS. Users can access Bio-OS through bioctl.
3. OpenAPI is used to describe, produce, consume, and visualize RESTful web services. It's a specification standard for REST APIs that defines the structure and syntax.
4. Bioos-apiserver: It mainly realizes the management of workspace, workflow (conforming to WES specification), data, etc. The bioos-server will send requests to the clusters of each computing plane to implement the running workflow
5. IAM(not supported in Version 1.0.0): service that helps you securely control user access
### Workflow Layer
1. Cromwell: open-source cromwell version v85
2. Storage : store workflow input ,output data or log
### Notebook Layer
1. JupyterHub: a multi-user Hub that spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server.
2. Storage : Used to store data from jupyter notebook
![](docs/static/arch.jpg)
## Capabilities
There are four core capabilities which support Bio-OS.
- Data Management
Effectively organize massive scale sample data and easily access public data
- Application Management
Bring together data、workflow、Notebooks、work engine into Sharable、 Traceable and reproducible Workspace
- Resource Management
Autoscaling through cloud-native which supports hybrid scheduling of multi-cloud heterogeneous resources
- Interactive Analysis
Using Built-in Notebook to do tertiary analysis
## Getting Started
### Prerequisites
To run Bio-OS, the following prerequisites must be met:
- A computer with one of the following operating systems:
- CentOS 7.9 +
- Ubuntu 22.04 +
- Internet access
- Resource Required
Minimum 8 cpu and 16G memory
### Installation
There will be two ways to install Bio-OS .
1. Using Docker-compose
- cromwell deployment
Bio-OS V1.0.0 Version does not support cromwell containerized deploymentYou need to install java-jre 11 version on the server and configure application.conf.
```text
include required(classpath("application"))
webservice {
port = 8000
}
workflow-options {
workflow-log-dir = /nfs/bioos-storage/cromwell-workflow-logs
workflow-log-temporary = false
}
call-caching {
enabled = true
invalidate-bad-cache-results = true
}
database {
profile = "slick.jdbc.MySQLProfile$"
db {
driver = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://180.184.37.106:3306/wqw?rewriteBatchedStatements=true&useSSL=false"
port = 3306
user = "public"
password = "Mirrors79"
connectionTimeout = 5000
}
}
backend {
default = "Local"
providers {
Local {
config {
root = "/nfs/bioos-storage/cromwell-executions"
filesystem {
local {
localization: [
"hard-link", "soft-link", "copy"
]
caching {
duplication-strategy: [
"hard-link", "soft-link", "copy"
]
hashing-strategy: "md5"
check-sibling-md5: false
}
}
}
}
}
}
}
```
**Attention**: The workflow-log-dir configuration should be consistent with the apiserver reference storage configuration. Execute the following code to complete the cromwell local deployment, and the apiserver in the subsequent docker-compose.yaml will refer to the cromwell call.
```shell
java -jar -Dconfig.file=/root/cromwell/application.conf -DLOG_LEVEL=INFO -DLOG_MODE=standard /root/cromwell/cromwell.jar server
```
- Bioos local startup
For local container environment testing, you need to install the docker container environment. Our code base provides the'docker-compose.yaml 'file.
Local environment testing can be quickly completed with the following commands.
```shell
# 进入开源 bioos 目录,执行命令
docker-compose up -d
```
**Note**: Local deployment will involve the problem of jupyterhub dynamically obtaining tokens. You can refer to the online deployment part. To update the token, you need to restart the bioos-apiserver container.
2. Online Deployment
Online deployment requires the preparation of a container orchestration engine, and we provide helm deployment packages to quickly complete bioos deployment.
- Deployment based on Kubernetes environment
If there is no local Kubernetes environment, you can try to deploy minikube. For details, please refer to the [official website](https://minikube.sigs.k8s.io/docs/start/).
When installing the container runtime, you have two options:
- Docker - The most common container runtime environment
- Deployment method
```shell
sudo apt update && apt install docker.io -y
```
- Nerdctl - Open source and open operating environment, a perfect alternative to the cncf community
- Deployment method
```shell
wget https://github.com/containerd/nerdctl/releases/download/v1.4.0/nerdctl-full-1.4.0-linux-amd64.tar.gz
tar zxf nerdctl-full-1.4.0-linux-amd64.tar.gz -C /usr/local/
cp /usr/local/lib/systemd/system/*.service /etc/systemd/system/
ln -s /usr/local/bin/nerdctl /usr/bin/docker
systemctl enable buildkit containerd
systemctl restart buildkit containerd
```
Kubernetes environment also has many deployment methods, common deployment forms such as minikube/microk8s/kubeadm/kubespray, considering the simplicity and ease of use, here take minikube as an example to build the local environment, production environment recommends choosing kubespray for high availability deployment.
```shell
# 安装基础依赖
sudo apt update && apt install -y conntrack
sudo sysctl fs.protected_regular=0 #重启之后,记得执行一下,避免 minikube /tmp/juju-mkf6a06118463380f4d96c12aced04598f450743: permission denied 类似报错
mkdir -p /etc/containerd && containerd config default > /etc/containerd/config.toml
wget https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.27.0/crictl-v1.27.0-linux-amd64.tar.gz
tar xvf crictl-v1.27.0-linux-amd64.tar.gz -C /usr/bin/
# 安装 minikube
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# 使用国内源安装 kubernetes
minikube start \
--container-runtime="containerd" \
--image-mirror-country=cn \
--image-repository=registry.cn-hangzhou.aliyuncs.com/google_containers \
--driver=none \
--apiserver-ips='180.184.43.61' # 可选配置如果需要外网访问需要这里配置主机的外网IP
```
Deployment log information:
```shell
root@registry:/home/vagrant# minikube start --container-runtime="containerd" --image-mirror-country=cn --driver=none --image-repository=registry.cn-hangzhou.aliyuncs.com/google_containeres
😄 minikube v1.30.1 on Ubuntu 22.04 (amd64)
✨ Using the none driver based on existing profile
❗ Using the 'containerd' runtime with the 'none' driver is an untested configuration!
❗ Using the 'containerd' runtime with the 'none' driver is an untested configuration!
👍 Starting control plane node minikube in cluster minikube
🔄 Restarting existing none bare metal machine for "minikube" ...
OS release is Ubuntu 22.04.1 LTS
📦 Preparing Kubernetes v1.26.3 on containerd 1.6.12-0ubuntu1 ...
▪ kubelet.resolv-conf=/run/systemd/resolve/resolv.conf
> kubectl.sha256: 64 B / 64 B [-------------------------] 100.00% ? p/s 0s
> kubeadm.sha256: 64 B / 64 B [-------------------------] 100.00% ? p/s 0s
> kubelet.sha256: 64 B / 64 B [-------------------------] 100.00% ? p/s 0s
> kubectl: 45.81 MiB / 45.81 MiB [-------------] 100.00% 5.61 MiB p/s 8.4s
> kubeadm: 44.61 MiB / 44.61 MiB [--------------] 100.00% 2.27 MiB p/s 20s
> kubelet: 115.65 MiB / 115.65 MiB [------------] 100.00% 4.39 MiB p/s 27s
▪ Generating certificates and keys ...
▪ Booting up control plane ...
▪ Configuring RBAC rules ...
🔗 Configuring bridge CNI (Container Networking Interface) ...
🤹 Configuring local host environment ...
❗ The 'none' driver is designed for experts who need to integrate with an existing VM
💡 Most users should use the newer 'docker' driver instead, which does not require root!
📘 For more information, see: https://minikube.sigs.k8s.io/docs/reference/drivers/none/
❗ kubectl and minikube configuration will be stored in /root
❗ To use kubectl or minikube commands as your own user, you may need to relocate them. For example, to overwrite your own settings, run:
▪ sudo mv /root/.kube /root/.minikube $HOME
▪ sudo chown -R $USER $HOME/.kube $HOME/.minikube
💡 This can also be done automatically by setting the env var CHANGE_MINIKUBE_NONE_USER=true
▪ Using image registry.cn-hangzhou.aliyuncs.com/google_containers/storage-provisioner:v5
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
💡 kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
```
After installing minikube, we still need to do some small configuration, such as configuring network plugins
```shell
# 追加 kubectl 快捷方式
echo 'alias kubectl="minikube kubectl --"' >> ~/.bashrc
source ~/.bashrc
# 配置 calico cni 网络
mkdir -p /opt/cni/bin && wget https://github.com/containernetworking/plugins/releases/download/v1.3.0/cni-plugins-linux-amd64-v1.3.0.tgz
tar -xvf cni-plugins-linux-amd64-v1.3.0.tgz -C /opt/cni/bin
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.25.1/manifests/calico.yaml
```
Environmental inspection, the final deployment situation is as follows:
```shell
root@registry:/home/vagrant# kubectl get po -A
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-5857bf8d58-2p25d 1/1 Running 0 7m46s
kube-system calico-node-fk6vd 1/1 Running 0 7m46s
kube-system coredns-567c556887-8r8cx 1/1 Running 0 11m
kube-system etcd-registry 1/1 Running 2 11m
kube-system kube-apiserver-registry 1/1 Running 2 12m
kube-system kube-controller-manager-registry 1/1 Running 2 11m
kube-system kube-proxy-vt2ks 1/1 Running 0 11m
kube-system kube-scheduler-registry 1/1 Running 2 11m
kube-system storage-provisioner 1/1 Running 0 11m
```
Since Bioos needs to use NAS storage, we choose to use the NFS storage solution here. For convenient access, we also need to install an Ingress controller to provide network access support.
**Install the ingress controller**
Ingress deployment can refer to the official website, and you can also refer to the nginx-ingress deployment configuration of kubespray.
```shell
# 安装 ingress 控制器(国内可能镜像下载会有问题)
#kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.1/deploy/static/provider/cloud/deploy.yaml
# 如果镜像下载有问题,可以使用下面的代码替代
# kubectl apply -f https://raw.githubusercontent.com/markthink/helm-charts/main/StorageClass/base/yaml/ingress-nginx.yaml
git clone https://github.com/markthink/helm-charts
cd helm-charts && kubectl apply -f ingress_nginx/
```
Environmental inspection, the final deployment situation is as follows:
```shell
root@registry:/home/vagrant# kubectl get po -A
NAMESPACE NAME READY STATUS RESTARTS AGE
ingress-nginx ingress-nginx-controller-f6c55fdc8-x5dtz 0/1 Running 0 40s
kube-system calico-kube-controllers-5857bf8d58-2p25d 1/1 Running 0 15m
kube-system calico-node-fk6vd 1/1 Running 0 15m
kube-system coredns-567c556887-8r8cx 1/1 Running 0 19m
kube-system etcd-registry 1/1 Running 2 19m
kube-system kube-apiserver-registry 1/1 Running 2 19m
kube-system kube-controller-manager-registry 1/1 Running 2 19m
kube-system kube-proxy-vt2ks 1/1 Running 0 19m
kube-system kube-scheduler-registry 1/1 Running 2 19m
kube-system storage-provisioner 1/1 Running 0 19m
root@registry:/home/vagrant# kubectl get ingressclass -A
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 47s
```
**Install NFS Storage Service**
Use helm to install the nfs storage service (please refer to the remarks for Nfs-server deployment)
Since you can't download foreign (registry.k8s.io) images in China, you need to prepare the helm configuration file values.yaml:
```yaml
image:
nfs:
repository: dyrnq/nfsplugin
tag: v4.2.0
pullPolicy: IfNotPresent
csiProvisioner:
repository: dyrnq/csi-provisioner
tag: v3.3.0
pullPolicy: IfNotPresent
livenessProbe:
repository: dyrnq/livenessprobe
tag: v2.8.0
pullPolicy: IfNotPresent
nodeDriverRegistrar:
repository: dyrnq/csi-node-driver-registrar
tag: v2.6.2
pullPolicy: IfNotPresent
```
```shell
# 安装 helm 部署包
wget https://get.helm.sh/helm-v3.12.0-linux-amd64.tar.gz
tar xvf helm-v3.12.0-linux-amd64.tar.gz \
--strip-components=1 -C /usr/local/bin
# 默认配置一般不需要修改
# https://github.com/kubernetes-csi/csi-driver-nfs/blob/master/charts/v4.2.0/csi-driver-nfs/values.yaml
# 安装 NFS CSI 存储驱动
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system --version v4.2.0 -f values.yaml
```
Use Kustomize to process related variables, [Code link](https://github.com/markthink/helm-charts/tree/main/StorageClass)
```shell
root@opensource-bioos:~/helm-charts/storageclass# tree .
.
├── base
│ ├── kustomization.yaml
│ └── yaml
│ ├── ingress-nginx.yaml
│ └── sc.yaml
└── overlays
├── dev
│ └── kustomization.yaml
└── prod
└── kustomization.yaml
5 directories, 5 files
```
The configuration file directory structure is as above, base is the basic resource configuration file, overlays the related variables according to different environment configuration, enter the directory and execute the following command to view the generation result. Before generating, you need to modify the kustomization.yaml file
```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patches/
# https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/patchesjson6902/
resources:
# - yaml/ingress-nginx.yaml
- yaml/sc.yaml
patches:
- patch: |-
- op: replace
path: /parameters/server
value: 192.168.46.300 #这里填写 nfs server 地址
- op: replace
path: /parameters/share
value: /nfs #这里填写 nfs server 根目录地址
target:
kind: StorageClass
name: nfs-csi
- patch: |-
- op: replace
path: /spec/csi/volumeAttributes/server
value: 192.168.46.300
- op: replace
path: /spec/csi/volumeAttributes/share
value: /nfs
- op: replace
path: /spec/csi/volumeHandle
value: 192.168.46.300#nfs#bioos-storage#
- op: replace
path: /spec/capacity/storage
value: 50Gi
target:
kind: PersistentVolume
name: bioos-storage
- patch: |-
- op: replace
path: /spec/resources/requests/storage
value: 50Gi #这里配置存储块的大小
target:
kind: PersistentVolumeClaim
name: bioos-storage-pvc
```
Fill in the correct server related information and execute the command to check that it is correct
```shell
# kubectl kustomize <kustomization_directory>
cd overlays/dev && kubectl kustomize .
```
Execute the following command to complete the resource creation.
```shell
# kubectl apply -k <kustomization_directory>
cd overlays/dev && kubectl apply -k .
```
**Install Bioos Service**
- Install mysql service
Execute the following command to complete the mysql deployment
```shell
# 注意要保证 nfs-server 共享目录有 777 权限
# https://github.com/bitnami/charts/blob/main/bitnami/mysql/values.yaml
helm install mysql \
--set auth.rootPassword="admin",auth.database=bioos,auth.username=admin,auth.password=admin,global.storageClass=nfs-csi,primary.persistence.size=50Gi \
oci://registry-1.docker.io/bitnamicharts/mysql
```
- Prepare Jupyterhub/cromwell
Bioos uses helm packaging, which is currently divided into four sub-packages. It should be noted that after Jupyterhub is deployed, open the browser to obtain new tokens to continue deploying bioos-server and web services.
The corresponding Helm installation command is as follows:
```shell
# 添加 helm 仓库
helm repo add bioos https://markthink.github.io/helm-charts
helm search repo bioos
```
Search the helm repository using the command
```shell
helm search repo bioos
NAME CHART VERSION APP VERSION DESCRIPTION
bioos/bioos 0.1.0 v1 BioOS UI 前端
bioos/cromwell 0.1.0 1.0.0 A Helm chart for cromwell with local backend
bioos/jupyterhub 2.0.0 3.0.0 Multi-user Jupyter installation
```
Install cromwell to update the values.yaml configuration file:
```yaml
## platformConfig contains information about the environment on which the Chart is being installed
## These values are expected be updated during the platform installation stage
platformConfig:
## Container registry for all images involved in the chart
registryDomain: docker.io
## Container repository for platform components
registryRepository: broadinstitute
## Platform-wide image pull policy
imagePullPolicy: Always
## ImagePullSecret name for all images involved in the chart
imagePullSecret: ""
labels: {}
podLabels: {}
podAnnotations: {}
## Ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}
## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []
image:
name: cromwell
tag: 85
replicaCount: 1
priorityClassName: ""
securityContext:
enabled: false
runAsUser: 65534 # nobody
runAsGroup: 65534 # nobody
fsGroup: 65534 # nobody
hardAntiAffinity: false
livenessProbe:
enabled: true
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 3
successThreshold: 1
readinessProbe:
enabled: true
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 3
successThreshold: 1
startupProbe:
enabled: true
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 10
successThreshold: 1
resources:
limits: {}
requests:
cpu: 100m
memory: 512Mi
config:
dir: /etc/cromwell
file: config
service:
type: ClusterIP
httpPort: 8000
# clusterIP: None
# loadBalancerIP:
# loadBalancerSourceRanges:
# - 10.10.10.0/24
annotations: {}
labels: {}
persistence:
enabled: true
internal: false
pvcName: bioos-storage-pvc
storageClass: "nfs-csi"
pvcSize: 50Gi
accessMode: ReadWriteMany
log:
path: /cromwell-workflow-logs
basePath: /nfs/bioos-storage
executionPath: /cromwell-executions
db:
local:
enabled: false
path: /cromwell-db
mysql:
enabled: true
host: "mysql"
port: 3306
name: "bioos"
username: "root"
password: "admin"
```
install cromwell
```shell
helm install cromwell bioos/cromwell -f values.yaml
```
Setup jupyterhub configure file values.yaml
```yaml
imagePullSecrets: []
hub:
baseUrl: "/jupyterhub/"
allowNamedServers: true
config:
NologinAuthenticator:
username: nobody
JupyterHub:
admin_access: true
authenticator_class: myauthenticator.NologinAuthenticator
tornado_settings:
slow_spawn_timeout: 0
headers:
Access-Control-Allow-Origin: "*"
Content-Security-Policy: "frame-ancestors 'self' http://localhost"
Spawner:
args:
- "--NotebookApp.allow_origin=*"
- '--NotebookApp.tornado_settings={"headers":{"Content-Security-Policy": "frame-ancestors ''self'' http://localhost"}}'
image:
name: bioos/jupyterhub
tag: "v1.0"
networkPolicy:
enabled: false
db:
type: mysql
url: mysql+pymysql://root:admin@mysql:3306/bioos
singleuser:
networkTools:
image:
name: jupyterhub/k8s-network-tools
image:
name: jupyterhub/k8s-singleuser-sample
storage:
type: none
cpu:
limit: 1
guarantee: 1
memory:
limit: 1G
guarantee: 1G
scheduling:
userScheduler:
enabled: false
userPlaceholder:
enabled: false
prePuller:
hook:
enabled: false
continuous:
enabled: false
```
Execute the following commands to install jupyterhub
```shell
helm install jupyterhub bioos/jupyterhub -f values.yaml
```
Open a browser to visit: http://serverIP/jupyterhub, and apply for a new token.
**Install Bioos Service**
You need to update the configuration of bioos. The specific path is as follows:
- You need to update the configuration of bioos. The specific path is as follows: bioos/values.yaml
Modify the configuration and execute the command as follows
```yaml
image:
repository: docker.io
project: bioos
web_name: web:v1.2
apiserver_name: apiserver:v1.2
pullPolicy: IfNotPresent
imagePullSecrets: []
replicaCount: 1
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
rbac:
create: true
podAnnotations: {}
podSecurityContext: {}
securityContext: {}
service:
type: ClusterIP
web_port: 80
api_http_port: 8888
api_grpc_port: 50051
ingress:
enabled: true
className: ""
annotations:
{
# https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#custom-max-body-size
nginx.ingress.kubernetes.io/proxy-body-size: "64m"
}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: ""
paths:
- path: /
pathType: ImplementationSpecific
tls: []
resources: {}
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
notebook:
images:
- name: datascience
version: "1.0"
description: "this is official image"
image: jupyter/datascience-notebook:hub-3.0.0
resources:
- cpu: 1
memory: 1Gi
disk: 20Gi
- cpu: 2
memory: 4Gi
disk: 20Gi
mysql:
hostname: mysql
database: bioos
username: root
password: admin
storage:
pvc: bioos-storage-pvc
mountPath: /app/conf/fs
wes:
endpoint: http://180.184.43.61:8000
jupyterhub:
endpoint: http://180.184.43.61/jupyterhub
adminToken: 6026738d798c495aa01c7831048539d9
```
Update config and run the following command
```shell
helm install demo bioos/bioos -f values.yaml
```
**Environmental inspection**
Execute the command'kubectl get po, pv, pvc, ing 'to view the application deployment status. Bioos uses two pieces of storage, one for Mysql and one for bioos storage, and provides several ingress rules to facilitate subpath reference services. The command status should look like this:
```shell
# kubectl get po,pv,pvc,ing
NAME READY STATUS RESTARTS AGE
pod/apiserver-bd4f89b4c-46vgp 1/1 Running 0 147m
pod/hub-78c55bf4c8-tr8v8 1/1 Running 0 149m
pod/jupyter-nobody 1/1 Running 0 17m
pod/mysql-77b4f7f77c-b9smq 1/1 Running 0 150m
pod/web-7ff595699-rnmkf 1/1 Running 0 147m
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
persistentvolume/bioos-storage 10Gi RWO Delete Bound default/bioos-storage-pvc nfs-csi 150m
persistentvolume/pvc-6b827f42-82e5-40b2-8e74-2c56fb8d836e 10Gi RWO Delete Bound default/mysql-pv-claim nfs-csi 150m
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
persistentvolumeclaim/bioos-storage-pvc Bound bioos-storage 10Gi RWO nfs-csi 150m
persistentvolumeclaim/mysql-pv-claim Bound pvc-6b827f42-82e5-40b2-8e74-2c56fb8d836e 10Gi RWO nfs-csi 150m
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/demo-bioos nginx * 10.211.55.13 80 147m
ingress.networking.k8s.io/jupyter--2fjupyterhub-2f-route nginx * 10.211.55.13 80 149m
ingress.networking.k8s.io/jupyter--2fjupyterhub-2fuser-2fnobody-2f-route nginx * 10.211.55.13 80 17m
```
Open a browser and visit http://serverIP/workspace
This completes the bioos installation.
## Tutorial
### Bio-OS user guide
Please look at the Bio-OS user guide.
### Bio-OS CLI
Please look at the CLI tutorial.
## License
This project is licensed under the Apache-2.0 License.

View File

@ -0,0 +1,23 @@
FROM golang:1.19.8-bullseye AS builder
ARG TARGETOS
ARG TARGETARCH
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /go/src/github.com/Bio-OS/bioos
COPY go.mod .
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN go env -w CGO_ENABLED=1 && BINS=apiserver PLATFORM=${TARGETOS}_${TARGETARCH} make go.build && make tools.install.cfssl && make tools.install.womtool && make generate.certs
FROM debian:bullseye
RUN apt update \
&& apt install -y --no-install-recommends ca-certificates openjdk-11-jre-headless \
&& apt clean \
&& rm -rf /var/lib/apt/lists/*
ARG TARGETOS
ARG TARGETARCH
USER nobody
WORKDIR /app
COPY --from=builder --chown=nobody:nogroup /go/src/github.com/Bio-OS/bioos/conf conf
COPY --from=builder /go/src/github.com/Bio-OS/bioos/_output/platforms/${TARGETOS}/${TARGETARCH}/apiserver .
COPY --from=builder /go/src/github.com/Bio-OS/bioos/womtool.jar .
ENTRYPOINT ["/app/apiserver"]

18
build/bioctl/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM golang:1.19.8-bullseye AS builder
ENV GOPROXY=https://goproxy.cn,direct
ARG TARGETOS
ARG TARGETARCH
WORKDIR /go/src/github.com/Bio-OS/bioos
COPY go.mod .
COPY go.sum .
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN BINS=bioctl PLATFORM=${TARGETOS}_${TARGETARCH} make go.build
FROM debian:bullseye
ARG TARGETOS
ARG TARGETARCH
USER nobody
WORKDIR /app
COPY --from=builder /go/src/github.com/Bio-OS/bioos/_output/platforms/${TARGETOS}/${TARGETARCH}/bioctl .
ENTRYPOINT ["sleep", "1d"]

10
build/web/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM node:16-alpine3.16 as build
WORKDIR /usr/app
COPY /web /usr/app/
RUN npm install && npm run build
FROM nginx:1.21-alpine
COPY --from=build /usr/app/build /usr/app/
COPY --from=build /usr/app/nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

View File

@ -0,0 +1,51 @@
//
// Copyright 2023 Beijing Volcano Engine Technology Ltd.
// Copyright 2023 Guangzhou Laboratory
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"math/rand"
"os"
"time"
ctrl "sigs.k8s.io/controller-runtime"
bioos_server "github.com/Bio-OS/bioos/internal/apiserver"
)
// @title BioOS Apiserver
// @version 1.0
// @description This is bioos apiserver using Hertz.
// @contact.name hertz-contrib
// @contact.url https://github.com/hertz-contrib
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @BasePath /
// @schemes http https
// @securityDefinitions.basic basicAuth
func main() {
rand.Seed(time.Now().UTC().UnixNano())
ctx := ctrl.SetupSignalHandler()
command := bioos_server.NewBioosServerCommand(ctx)
if err := command.Execute(); err != nil {
os.Exit(1)
}
}

31
cmd/bioctl/bioctl.go Normal file
View File

@ -0,0 +1,31 @@
//
// Copyright 2023 Beijing Volcano Engine Technology Ltd.
// Copyright 2023 Guangzhou Laboratory
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"k8s.io/component-base/cli"
"github.com/Bio-OS/bioos/internal/bioctl"
"github.com/Bio-OS/bioos/internal/bioctl/utils"
)
func main() {
command := bioctl.NewDefaultBIOCtlCommand()
if err := cli.RunNoErrOutput(command); err != nil {
utils.CheckErr(err)
}
}

103
conf/apiserver-mongo.yaml Normal file
View File

@ -0,0 +1,103 @@
server:
http:
port: 8888
tls: false
grpc:
port: 50051
tls: true
cert-file: conf/certs/server.pem
key-file: conf/certs/server-key.pem
ca-file: conf/certs/ca.pem
womtool-file: womtool.jar
log:
level: debug
output-path: app.log
max-backups: 3
db:
mongo:
host: MONGO_HOST
username: MONGO_USERNAME
password: MONGO_PASSWORD
port: MONGO_PORT
database: MONGO_DB
connectTimeout: 30s
serverSelectionTimeout: 30s
socketTimeout: 30s
heartbeatInterval: 5s
maxConnIdleTime: 30s
maxPoolSize: 100
minPoolSize: 1
eventBus:
maxRetries: 10
syncPeriod: 15s
batchSize: 5
workers: 5
dequeueTimeout: 5m
runningTimeout: 24h
storage:
fs:
rootPath: /app/conf/fs
auth:
authn:
basic:
ttl: 5
users:
- ID: 1
name: admin
password: admin
groups:
- admin
- users
extensions:
dept:
- deptA
- deptB
role:
- roleA
- roleB
authz:
casbin:
model: conf/model.conf
policy: conf/policy.csv
wes:
endpoint: ''
basePath: /api/ga4gh/wes/v1
timeout: 10
Retry: 1
client:
serverAddr: localhost:50051
serverName: localhost
insecure: false
serverCertFile: conf/certs/server.pem
clientCertFile: conf/certs/client.pem
clientCertKeyFile: conf/certs/client-key.pem
caFile: conf/certs/ca.pem
username: admin
password: admin
method: grpc
timeout: 10
notebook:
officialImages:
- name: sample
version: '1.0'
description: 'this is official images'
image: jupyterhub/k8s-singleuser-sample:2.0.0
updateTime: '2023-01-01T00:00:00Z'
resourceOptions:
- cpu: 1
memory: 1Gi
disk: 20Gi
staticJupyterhub:
endpoint: '' # url format
adminToken: ''
kubernetes:
namespace: ''

107
conf/apiserver-mysql.yaml Normal file
View File

@ -0,0 +1,107 @@
server:
http:
port: 8888
tls: false
grpc:
port: 50051
tls: true
cert-file: conf/certs/server.pem
key-file: conf/certs/server-key.pem
ca-file: conf/certs/ca.pem
womtool-file: womtool.jar
log:
level: debug
output-path: app.log
max-backups: 3
db:
mysql:
host: MYSQL_HOST
username: MYSQL_USERNAME
password: MYSQL_PASSWORD
port: MYSQL_PORT
database: MYSQL_DB
maxOpenConns: 100
maxIdleConns: 1
connMaxLifetime: 1h
connMaxIdletime: 30s
eventBus:
maxRetries: 10
syncPeriod: 15s
batchSize: 5
workers: 5
dequeueTimeout: 5m
runningTimeout: 24h
storage:
fs:
rootPath: /app/conf/fs
auth:
authn:
basic:
ttl: 5
users:
- ID: 1
name: admin
password: admin
groups:
- admin
- users
extensions:
dept:
- deptA
- deptB
role:
- roleA
- roleB
authz:
casbin:
model: conf/model.conf
policy: conf/policy.csv
wes:
endpoint: 'http://cromwell:8000'
basePath: /api/ga4gh/wes/v1
timeout: 10
Retry: 1
client:
serverAddr: localhost:50051
serverName: localhost
insecure: false
serverCertFile: conf/certs/server.pem
clientCertFile: conf/certs/client.pem
clientCertKeyFile: conf/certs/client-key.pem
caFile: conf/certs/ca.pem
username: admin
password: admin
method: grpc
timeout: 10
notebook:
officialImages:
- name: sample
version: '1.0'
description: 'this is official images'
image: jupyterhub/k8s-singleuser-sample:2.0.0
updateTime: '2023-01-01T00:00:00Z'
resourceOptions:
- cpu: 1
memory: 1Gi
disk: 20Gi
- cpu: 2
memory: 4Gi
disk: 20Gi
gpu:
model: 'Nvidia Tesla T4'
memory: 64Gi
card: 1
nodeSelector: {}
staticJupyterhub:
endpoint: '' # url format
adminToken: ''
kubernetes:
namespace: ''

90
conf/apiserver.yaml Normal file
View File

@ -0,0 +1,90 @@
server:
http:
port: 8888
tls: false
grpc:
port: 50051
tls: true
cert-file: conf/certs/server.pem
key-file: conf/certs/server-key.pem
ca-file: conf/certs/ca.pem
womtool-file: womtool.jar
log:
level: debug
output-path: app.log
max-backups: 3
db:
sqlite3:
file: ":memory:"
eventBus:
maxRetries: 10
syncPeriod: 15s
batchSize: 5
workers: 5
dequeueTimeout: 5m
runningTimeout: 24h
storage:
fs:
rootPath: /tmp
auth:
authn:
basic:
ttl: 5
users:
- ID: 1
name: admin
password: admin
groups:
- admin
- users
extensions:
dept:
- deptA
- deptB
role:
- roleA
- roleB
authz:
casbin:
model: conf/model.conf
policy: conf/policy.csv
wes:
endpoint: ''
basePath: /api/ga4gh/wes/v1
timeout: 10
Retry: 1
client:
serverAddr: localhost:50051
serverName: localhost
insecure: false
serverCertFile: conf/certs/server.pem
clientCertFile: conf/certs/client.pem
clientCertKeyFile: conf/certs/client-key.pem
caFile: conf/certs/ca.pem
username: admin
password: admin
method: grpc
timeout: 10
notebook:
officialImages:
- name: sample
version: '1.0'
description: 'this is official images'
image: jupyterhub/k8s-singleuser-sample:2.0.0
updateTime: '2023-01-01T00:00:00Z'
resourceOptions:
- cpu: 1
memory: 1Gi
disk: 20Gi
staticJupyterhub:
endpoint: '' # url format
adminToken: ''
kubernetes:
namespace: ''

15
conf/bioctl.yaml Normal file
View File

@ -0,0 +1,15 @@
client:
serverAddr: localhost:50051
serverName: localhost
insecure: false
serverCertFile: conf/certs/server.pem
clientCertFile: conf/certs/client.pem
clientCertKeyFile: conf/certs/client-key.pem
caFile: conf/certs/ca.pem
username: admin
password: admin
method: grpc
timeout: 10
stream:
format: yaml

36
conf/certs/ca-config.json Normal file
View File

@ -0,0 +1,36 @@
{
"signing": {
"default": {
"expiry": "876000h"
},
"profiles": {
"client": {
"expiry": "175200h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "175200h",
"usages": [
"signing",
"key encipherment",
"client auth",
"server auth"
]
},
"server": {
"expiry": "175200h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
}
}
}
}

16
conf/certs/ca-csr.json Normal file
View File

@ -0,0 +1,16 @@
{
"CN": "ca",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "BeiJing",
"L": "BeiJing",
"O": "Bio",
"OU": "MyTeam"
}
]
}

3
conf/certs/ca.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")"
cfssl gencert -initca ca-csr.json | cfssljson -bare ca

14
conf/certs/certs.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
cd "$(dirname "$0")"
if ! [[ -f ca.pem ]];then
./ca.sh
fi
if ! [[ -f server.pem ]];then
./server.sh
fi
if ! [[ -f client.pem ]];then
./client.sh
fi

View File

@ -0,0 +1,16 @@
{
"CN": "client",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "BeiJing",
"O": "Bio",
"OU": "MyTeam",
"ST": "BeiJing"
}
]
}

8
conf/certs/client.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
cd "$(dirname "$0")"
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=client \
client-csr.json | cfssljson -bare client

View File

@ -0,0 +1,22 @@
{
"CN": "apiservver",
"hosts": [
"127.0.0.1",
"*.bioos.com",
"localhost",
"bioos.com"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "BeiJing",
"O": "Bio",
"OU": "MyTeam",
"ST": "BeiJing"
}
]
}

8
conf/certs/server.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
cd "$(dirname "$0")"
cfssl gencert \
-ca=ca.pem \
-ca-key=ca-key.pem \
-config=ca-config.json \
-profile=server \
server-csr.json | cfssljson -bare server

14
conf/model.conf Normal file
View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "admin"

3
conf/policy.csv Normal file
View File

@ -0,0 +1,3 @@
p, user1, proto.WorkspaceService, GetWorkspace
p, user1, Workspace, Create
p, user1, Workspace, List
1 p user1 proto.WorkspaceService GetWorkspace
2 p user1 Workspace Create
3 p user1 Workspace List

63
docker-compose.yaml Normal file
View File

@ -0,0 +1,63 @@
version: '3'
services:
mysql:
image: mysql:latest
restart: always
environment:
MYSQL_ROOT_PASSWORD: mypassword
MYSQL_DATABASE: bioos
ports:
- "3306:3306"
volumes:
- mysql-storage:/var/lib/mysql
networks:
- bioos
apiserver:
build:
context: .
dockerfile: build/apiserver/Dockerfile
restart: always
environment:
MYSQL_HOST: mysql
MYSQL_USERNAME: root
MYSQL_PASSWORD: mypassword
MYSQL_PORT: "3306"
MYSQL_DB: bioos
command: ["--config", "/app/conf/apiserver-mysql.yaml", "--log-level", "debug"]
ports:
- "8888:8888"
- "50051:50051"
volumes:
- bioos-storage:/app/conf/fs
depends_on:
- mysql
networks:
- bioos
web:
build:
context: .
dockerfile: build/web/Dockerfile
restart: always
depends_on:
- apiserver
ports:
- "8901:80"
networks:
- bioos
cromwell:
image: broadinstitute/cromwell:85
restart: always
command:
- server
ports:
- "8000:8000"
volumes:
- bioos-storage:/nfs/bioos-storage
networks:
- bioos
networks:
bioos:
driver: bridge
volumes:
bioos-storage:
mysql-storage:

4295
docs/docs.go Normal file

File diff suppressed because it is too large Load Diff

BIN
docs/static/arch.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
docs/static/bioos.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

BIN
docs/static/workspace.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

4275
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2799
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

140
examples/client/main.go Normal file
View File

@ -0,0 +1,140 @@
//
// Copyright 2023 Beijing Volcano Engine Technology Ltd.
// Copyright 2023 Guangzhou Laboratory
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"flag"
"fmt"
"log"
"os"
"time"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"golang.org/x/oauth2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/credentials/oauth"
"google.golang.org/grpc/grpclog"
pb "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
)
const (
defaultName = ""
)
var (
addr = flag.String("addr", "localhost:50051", "the address to connect to")
name = flag.String("name", defaultName, "Name to query")
certFile = flag.String("cert-file", "", "cert file name")
keyFile = flag.String("key-file", "", "key file name")
serverCertFile = flag.String("server-cert-file", "", "server cert file name")
caFile = flag.String("ca-file", "", "ca file name")
serverName = flag.String("server-name", "", "server name")
username = flag.String("username", "", "username")
password = flag.String("password", "", "password")
accessToken = flag.String("access-token", "", "oauth access token")
)
type basicAuth struct {
username string
password string
}
func (b basicAuth) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
auth := b.username + ":" + b.password
enc := base64.StdEncoding.EncodeToString([]byte(auth))
return map[string]string{
"authorization": "Basic " + enc,
}, nil
}
func (b basicAuth) RequireTransportSecurity() bool {
return true
}
func main() {
flag.Parse()
creds := insecure.NewCredentials()
if *serverName != "" {
if *serverCertFile != "" {
var err error
creds, err = credentials.NewClientTLSFromFile(*serverCertFile, *serverName)
if err != nil {
grpclog.Fatalf("Failed to create TLS credentials %v", err)
}
} else if *certFile != "" && *keyFile != "" {
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
log.Fatalf("load cert err: %v", err)
}
certPool := x509.NewCertPool()
if *caFile != "" {
ca, _ := os.ReadFile(*caFile)
certPool.AppendCertsFromPEM(ca)
}
creds = credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: *serverName,
RootCAs: certPool,
})
}
}
var auth credentials.PerRPCCredentials
grpcOpts := []grpc.DialOption{
grpc.WithUnaryInterceptor(grpc_prometheus.UnaryClientInterceptor),
grpc.WithStreamInterceptor(grpc_prometheus.StreamClientInterceptor),
grpc.WithTransportCredentials(creds),
}
if *username != "" && *password != "" {
fmt.Printf("using basic auth: %s:%s\n", *username, *password)
auth = &basicAuth{
username: *username,
password: *password,
}
grpcOpts = append(grpcOpts, grpc.WithPerRPCCredentials(auth))
} else if *accessToken != "" {
auth = oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: *accessToken})}
grpcOpts = append(grpcOpts, grpc.WithPerRPCCredentials(auth))
}
// Set up a connection to the server.
conn, err := grpc.Dial(*addr, grpcOpts...)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewWorkspaceServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.GetWorkspace(ctx, &pb.GetWorkspaceRequest{Id: *name})
if err != nil {
log.Fatalf("could not get workspace: %v", err)
}
log.Printf("Workspace: %s", r.GetWorkspace())
}

5
githooks/commit-msg Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
# your commits will be validated after saving and closing the commit message in your text editor.
# install go-gitlint via `go install github.com/llorllale/go-gitlint@latest`
go-gitlint --msg-file="$1"

18
githooks/pre-commit Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env bash
LC_ALL=C
local_branch="$(git rev-parse --abbrev-ref HEAD)"
valid_branch_regex="^(main|develop)$|(feature|feat|release|hotfix)\/[A-Za-z0-9._-]+$|^HEAD$"
message="Branch names should match the regular express: $valid_branch_regex.
Rename your branch to a valid name by: git branch -m oldName newName."
if [[ ! $local_branch =~ $valid_branch_regex ]]
then
echo "$message"
exit 1
fi
cd web
npx lint-staged

205
go.mod Normal file
View File

@ -0,0 +1,205 @@
module github.com/Bio-OS/bioos
go 1.19
require (
github.com/AlecAivazis/survey/v2 v2.3.6
github.com/casbin/casbin/v2 v2.65.2
github.com/casbin/gorm-adapter/v3 v3.15.0
github.com/cloudwego/hertz v0.6.3
github.com/fsnotify/fsnotify v1.6.0
github.com/go-git/go-git/v5 v5.6.1
github.com/go-kratos/kratos/v2 v2.6.1
github.com/go-playground/validator/v10 v10.12.0
github.com/go-resty/resty/v2 v2.7.0
github.com/go-sql-driver/mysql v1.7.0
github.com/google/uuid v1.3.0
github.com/gosuri/uitable v0.0.4
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/hertz-contrib/cors v0.0.0-20230307061136-1fe747ea6a2a
github.com/hertz-contrib/http2 v0.1.6
github.com/hertz-contrib/requestid v1.1.0
github.com/hertz-contrib/swagger v0.0.0-20220711030440-b6402d4709f0
github.com/iancoleman/strcase v0.2.0
github.com/jarcoal/httpmock v1.3.0
github.com/jinzhu/copier v0.3.5
github.com/manifoldco/promptui v0.9.0
github.com/mattn/go-sqlite3 v1.14.16
github.com/mitchellh/mapstructure v1.5.0
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.27.5
github.com/pkg/errors v0.9.1
github.com/rs/xid v1.2.1
github.com/shaj13/go-guardian/v2 v2.11.5
github.com/shaj13/libcache v1.0.0
github.com/spf13/cast v1.5.0
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.10.1
github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2
github.com/swaggo/swag v1.8.12
github.com/vinllen/mgo v0.0.0-20220329061231-e5ecea62f194
go.mongodb.org/mongo-driver v1.8.3
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/net v0.9.0
golang.org/x/oauth2 v0.6.0
golang.org/x/sync v0.1.0
google.golang.org/grpc v1.52.0-dev
google.golang.org/protobuf v1.28.1
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.4.7
gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.6
k8s.io/api v0.26.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
k8s.io/component-base v0.26.1
k8s.io/utils v0.0.0-20221128185143-99ec85e7a448
sigs.k8s.io/controller-runtime v0.14.5
)
require (
cloud.google.com/go/compute v1.8.0 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230417170513-8ee5748c52b5 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bytedance/go-tagexpr/v2 v2.9.2 // indirect
github.com/bytedance/gopkg v0.0.0-20220817015305-b879a72dc90f // indirect
github.com/bytedance/sonic v1.8.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cloudflare/circl v1.3.2 // indirect
github.com/cloudwego/netpoll v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/glebarez/go-sqlite v1.19.1 // indirect
github.com/glebarez/sqlite v1.5.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.8 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/henrylee2cn/ameda v1.4.10 // indirect
github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/microsoft/go-mssqldb v0.17.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/nyaruka/phonenumbers v1.0.56 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.1.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.0 // indirect
github.com/xdg-go/stringprep v1.0.2 // indirect
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
golang.org/x/arch v0.2.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.8.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220804142021-4e6b2dfa6612 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/driver/postgres v1.4.4 // indirect
gorm.io/driver/sqlserver v1.4.1 // indirect
gorm.io/plugin/dbresolver v1.3.0 // indirect
k8s.io/apiextensions-apiserver v0.26.1 // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.19.1 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
exclude cloud.google.com/go/compute v1.9.0

1335
go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
Copyright 2023 Beijing Volcano Engine Technology Ltd.
Copyright 2023 Guangzhou Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

33
hack/make-rules/common.mk Normal file
View File

@ -0,0 +1,33 @@
SHELL := /bin/bash
# include the common make file
COMMON_SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST)))
ifeq ($(origin ROOT_DIR),undefined)
ROOT_DIR := $(abspath $(shell cd $(COMMON_SELF_DIR)/../.. && pwd -P))
endif
ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := _output
$(shell mkdir -p $(OUTPUT_DIR))
endif
GIT_COMMIT:=$(shell git rev-parse HEAD)
# Copy githook scripts when execute makefile
COPY_GITHOOK:=$(shell cp -f githooks/* .git/hooks/)
PLATFORMS ?= darwin_amd64 adrwin_arm64 windows_amd64 linux_amd64 linux_arm64
# Set a specific PLATFORM
ifeq ($(origin PLATFORM), undefined)
ifeq ($(origin GOOS), undefined)
GOOS := $(shell go env GOOS)
endif
ifeq ($(origin GOARCH), undefined)
GOARCH := $(shell go env GOARCH)
endif
PLATFORM := $(GOOS)_$(GOARCH)
IMAGE_PLAT := linux_amd64
else
GOOS := $(word 1, $(subst _, ,$(PLATFORM)))
GOARCH := $(word 2, $(subst _, ,$(PLATFORM)))
IMAGE_PLAT := $(PLATFORM)
endif

View File

@ -0,0 +1,17 @@
.PHONY: gen.add-copyright
gen.add-copyright:
@addlicense -v -f $(ROOT_DIR)/hack/boilerplate/boilerplate.txt \
--ignore "build/**" \
--ignore "conf/**" \
--ignore "docker-compose.yaml" \
--ignore "docs/**" \
--ignore "internal/**" \
--ignore "third_party/**" \
--ignore "vendor/**" \
--ignore "web/node_modules/**" \
--ignore "web/*.js" \
--ignore "web/**/nbv.js" \
--ignore "web/**/prism.js" \
--ignore ".golangci.yaml" \
--ignore ".idea/**" \
--ignore ".git/**" .

63
hack/make-rules/golang.mk Normal file
View File

@ -0,0 +1,63 @@
GO := go
GO_SUPPORTED_VERSIONS ?= 1.13|1.14|1.15|1.16|1.17|1.18|1.19|1.20
GO_LDFLAGS += -X $(ROOT_PACKAGE)/pkg/version.Version=$(VERSION) \
-X $(ROOT_PACKAGE)/pkg/version.Branch=$(GIT_BRANCH) \
-X $(ROOT_PACKAGE)/pkg/version.GitCommit=$(GIT_COMMIT) \
-X $(ROOT_PACKAGE)/pkg/version.GitTreeState=$(GIT_TREE_STATE) \
-X $(ROOT_PACKAGE)/pkg/version.GitBranch=$(GIT_BRANCH) \
-X $(ROOT_PACKAGE)/pkg/version.BuildTime=$(shell date +'%Y-%m-%dT%H:%M:%SZ')
ifneq ($(DLV),)
GO_BUILD_FLAGS += -gcflags "all=-N -l"
LDFLAGS = ""
endif
GO_BUILD_FLAGS += -ldflags "$(GO_LDFLAGS)"
COMMANDS ?= $(wildcard ${ROOT_DIR}/cmd/*)
BINS ?= $(foreach cmd,${COMMANDS},$(notdir ${cmd}))
.PHONY: go.tidy
go.tidy:
@$(GO) mod tidy
.PHONY: go.build.verify
go.build.verify:
@echo "===========> verify go"
@if ! which go &>/dev/null; then echo "Cannot found go compile tool. Please install go tool first."; exit 1; fi
.PHONY: go.build
go.build: go.build.verify go.tidy $(addprefix go.build., $(addprefix $(PLATFORM)., $(BINS)))
.PHONY: go.build.%
go.build.%:
$(eval COMMAND := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Building binary $(COMMAND) $(VERSION) for $(OS) $(ARCH)"
@echo "GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/platforms/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) $(ROOT_PACKAGE)/cmd/$(COMMAND)"
@GOOS=$(OS) GOARCH=$(ARCH) $(GO) build $(GO_BUILD_FLAGS) -o $(OUTPUT_DIR)/platforms/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) $(ROOT_PACKAGE)/cmd/$(COMMAND)
.PHONY: go.lint
go.lint: tools.verify.golangci-lint
@echo "===========> Run golangci-lint"
@golangci-lint run -c $(ROOT_DIR)/.golangci.yaml $(ROOT_DIR)/...
.PHONY: go.run
go.run: tools.install.womtool generate.certs $(addprefix go.run., $(addprefix $(PLATFORM)., apiserver))
.PHONY: generate.certs
generate.certs:
@echo "===========> Generating certs"
@bash ${ROOT_DIR}/conf/certs/certs.sh
.PHONY: go.run.%
go.run.%:
$(eval COMMAND := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Running binary $(COMMAND) $(VERSION) for $(OS) $(ARCH)"
@$(OUTPUT_DIR)/platforms/$(OS)/$(ARCH)/$(COMMAND)$(GO_OUT_EXT) --config conf/apiserver.yaml

22
hack/make-rules/grpc.mk Normal file
View File

@ -0,0 +1,22 @@
.PHONY: protoc.verify
protoc.verify:
@echo "===========> verify protoc"
@if ! which protoc &>/dev/null; then echo "Cannot found protoc compile tool. Please install protoc tool first."; exit 1; fi
@if ! which protoc-gen-go &>/dev/null; then echo "Cannot found protoc-gen-go. Please install protoc-gen-go tool first."; exit 1; fi
@if ! which protoc-gen-go-grpc &>/dev/null; then echo "Cannot found protoc-gen-go-grpc tool. Please install protoc-gen-go-grpc tool first."; exit 1; fi
@if ! which protoc-gen-go-errors &>/dev/null; then echo "Cannot found protoc-gen-go-errors tool. Please install protoc-gen-go-errors tool first."; exit 1; fi
.PHONY: protoc.gen
protoc.gen: protoc.verify tools.verify
@echo "===========> gen grpc code"
@protoc --proto_path=. \
--proto_path=./third_party \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
--go-errors_out=paths=source_relative:. \
--go-http_out=paths=source_relative:. \
internal/context/workspace/interface/grpc/proto/*.proto \
internal/context/submission/interface/grpc/proto/*.proto \
internal/context/notebookserver/interface/grpc/proto/*.proto

55
hack/make-rules/image.mk Normal file
View File

@ -0,0 +1,55 @@
DOCKER := docker
IMAGES_DIR ?= $(wildcard ${ROOT_DIR}/build/*)
IMAGES ?= $(foreach image,${IMAGES_DIR},$(notdir ${image}))
IMAGE_REGISTRY ?= quay.io/bioos
.PHONY: image.verify
image.verify:
@echo "===========> verify docker"
@if ! which docker &>/dev/null; then echo "Cannot found docker. Please install docker first."; exit 1; fi
@if ! docker ps &>/dev/null; then echo "Docker daemon not running. Please start it first.";exit 1;fi
.PHONY: buildx.verify
buildx.verify:
@echo "===========> verify docker buildx"
@if ! docker buildx version &>/dev/null; then echo "Cannot found docker buildx. Please install docker buildx first. Link: https://docs.docker.com/build/architecture/"; exit 1; fi
.PHONY: image.build
image.build: image.verify $(addprefix image.build., $(addprefix $(IMAGE_PLAT)., $(IMAGES)))
.PHONY: image.buildx.multiarch.%
image.buildx.multiarch.%: image.verify buildx.verify
$(eval IMAGE := $(*))
@echo "===========> Building multiple arch docker image $(IMAGE)"
$(DOCKER) buildx build \
--platform linux/amd64,linux/arm64 \
-t $(IMAGE_REGISTRY)/$(IMAGE):$(VERSION) \
-f $(ROOT_DIR)/build/$(IMAGE)/Dockerfile . --push
.PHONY: image.buildx
image.buildx: image.verify $(foreach i,$(IMAGES), $(addprefix image.buildx.multiarch.,$(i)))
.PHONY: image.build.%
image.build.%:
$(eval IMAGE := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Building docker image $(IMAGE) for $(OS) $(ARCH)"
DOCKER_BUILDKIT=1 $(DOCKER) build \
--platform $(OS)/$(ARCH) \
-t $(IMAGE_REGISTRY)/$(IMAGE):$(VERSION) \
-f $(ROOT_DIR)/build/$(IMAGE)/Dockerfile .
.PHONY: image.push
image.push: image.build $(addprefix image.push., $(addprefix $(IMAGE_PLAT)., $(IMAGES)))
.PHONY: image.push.%
image.push.%:
$(eval IMAGE := $(word 2,$(subst ., ,$*)))
$(eval PLATFORM := $(word 1,$(subst ., ,$*)))
$(eval OS := $(word 1,$(subst _, ,$(PLATFORM))))
$(eval ARCH := $(word 2,$(subst _, ,$(PLATFORM))))
@echo "===========> Pushing docker image $(IMAGE) for $(OS) $(ARCH)"
@echo "$(DOCKER) push $(IMAGE_REGISTRY)/$(IMAGE):$(VERSION)"
$(DOCKER) push $(IMAGE_REGISTRY)/$(IMAGE):$(VERSION)

View File

@ -0,0 +1,11 @@
.PHONY: swagger.gen
swagger.gen: install.swagger
@echo "===========> Generating swag API docs"
@swag init --parseDependency --dir ./cmd/apiserver,./internal -g apiserver.go
.PHONY: swagger.fmt
swagger.fmt: install.swagger
@echo "===========> Format swag comments"
@swag fmt --dir ./cmd/apiserver,./internal
.PHONY: swagger.run
swagger.run: swagger.gen swagger.fmt

68
hack/make-rules/tools.mk Normal file
View File

@ -0,0 +1,68 @@
TOOLS ?= swagger addlicense cfssl mockgen gotests git-chglog protoc-gen-go protoc-gen-go-grpc protoc-gen-go-http protoc-gen-go-errors go-gitlint golangci-lint
.PHONY: tools.install
tools.install: $(addprefix tools.install., $(TOOLS))
.PHONY: tools.verify
tools.verify: $(addprefix tools.verify., $(TOOLS))
.PHONY: tools.install.%
tools.install.%:
@echo "===========> Installing $*"
@$(MAKE) install.$*
.PHONY: tools.verify.%
tools.verify.%:
@if ! which $* &>/dev/null; then $(MAKE) tools.install.$*; fi
.PHONY: install.swagger
install.swagger:
@$(GO) install github.com/swaggo/swag/cmd/swag@v1.8.12
.PHONY: install.cfssl
install.cfssl:
@$(GO) install github.com/cloudflare/cfssl/cmd/...@latest
.PHONY: install.go-gitlint
install.go-gitlint:
@$(GO) install github.com/llorllale/go-gitlint@latest
.PHONY: install.git-chglog
install.git-chglog:
@$(GO) install github.com/git-chglog/git-chglog/cmd/git-chglog@latest
.PHONY: install.mockgen
install.mockgen:
@$(GO) install github.com/golang/mock/mockgen@latest
.PHONY: install.gotests
install.gotests:
@$(GO) install github.com/cweill/gotests/gotests@latest
.PHONY: install.addlicense
install.addlicense:
@$(GO) install github.com/google/addlicense@latest
.PHONY: install.protoc-gen-go
install.protoc-gen-go:
@$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@v1.30.0
.PHONY: install.protoc-gen-go-grpc
install.protoc-gen-go-grpc:
@$(GO) install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
.PHONY: install.protoc-gen-go-http
install.protoc-gen-go-http:
@$(GO) install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest
.PHONY: install.protoc-gen-go-errors
install.protoc-gen-go-errors:
@$(GO) install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest
.PHONY: install.golangci-lint
install.golangci-lint:
@$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
.PHONY: install.womtool
install.womtool:
@wget --progress=dot -cP ${ROOT_DIR} -O womtool.jar https://github.com/broadinstitute/cromwell/releases/download/85/womtool-85.jar

View File

@ -0,0 +1,78 @@
package apiserver
import (
"crypto/tls"
"crypto/x509"
"os"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags"
grpc_opentracing "github.com/grpc-ecosystem/go-grpc-middleware/tracing/opentracing"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
"github.com/Bio-OS/bioos/internal/apiserver/options"
middlewaregrpc "github.com/Bio-OS/bioos/pkg/middlewares/grpc"
appserver "github.com/Bio-OS/bioos/pkg/server"
)
func setupGrpcServer(opts *options.Options, registers ...appserver.GRPCRegister) (*grpc.Server, error) {
serverOptions := []grpc.ServerOption{
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
grpc_ctxtags.StreamServerInterceptor(),
grpc_opentracing.StreamServerInterceptor(),
grpc_prometheus.StreamServerInterceptor,
middlewaregrpc.NewAuthStreamServerInterceptor(),
grpc_recovery.StreamServerInterceptor(),
middlewaregrpc.RBACStreamServerChain(),
)),
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
grpc_ctxtags.UnaryServerInterceptor(),
grpc_opentracing.UnaryServerInterceptor(),
grpc_prometheus.UnaryServerInterceptor,
middlewaregrpc.NewAuthUnaryServerInterceptor(),
grpc_recovery.UnaryServerInterceptor(),
middlewaregrpc.RBACUnaryServerChain(),
)),
}
if opts.ServerOption.Grpc.TLS {
tlsCredentials, err := loadTLSCredentials(opts.ServerOption.CertFile, opts.ServerOption.KeyFile, opts.ServerOption.CaFile)
if err != nil {
return nil, err
}
serverOptions = append(serverOptions, grpc.Creds(tlsCredentials))
}
grpcServer := grpc.NewServer(serverOptions...)
for _, r := range registers {
r(grpcServer)
}
reflection.Register(grpcServer)
grpc_prometheus.Register(grpcServer)
hs := health.NewServer()
hs.SetServingStatus("grpc.health.v1.workspaceservice", 1)
healthpb.RegisterHealthServer(grpcServer, hs)
return grpcServer, nil
}
func loadTLSCredentials(certFile, keyFile, caFile string) (credentials.TransportCredentials, error) {
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
ca, err := os.ReadFile(caFile)
if err != nil {
return nil, err
}
certPool.AppendCertsFromPEM(ca)
return credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: certPool,
}), nil
}

178
internal/apiserver/http.go Normal file
View File

@ -0,0 +1,178 @@
package apiserver
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"
"time"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/config"
"github.com/google/uuid"
"github.com/hertz-contrib/cors"
"github.com/hertz-contrib/http2/factory"
"github.com/hertz-contrib/requestid"
"github.com/hertz-contrib/swagger"
swaggerFiles "github.com/swaggo/files"
"golang.org/x/net/http2"
"github.com/Bio-OS/bioos/internal/apiserver/options"
"github.com/Bio-OS/bioos/pkg/consts"
apperrors "github.com/Bio-OS/bioos/pkg/errors"
apphertz "github.com/Bio-OS/bioos/pkg/middlewares/hertz"
"github.com/Bio-OS/bioos/pkg/notebook"
appserver "github.com/Bio-OS/bioos/pkg/server"
"github.com/Bio-OS/bioos/pkg/utils"
"github.com/Bio-OS/bioos/pkg/version"
)
func setupHTTPServer(opts *options.Options, registers ...appserver.RouteRegister) (*server.Hertz, error) {
serverOptions := []config.Option{
server.WithHostPorts(fmt.Sprintf(":%s", opts.ServerOption.Http.Port)),
server.WithMaxRequestBodySize(opts.ServerOption.Http.MaxRequestBodySize),
server.WithALPN(true),
}
if opts.ServerOption.Http.TLS {
cert, err := tls.LoadX509KeyPair(opts.ServerOption.CertFile, opts.ServerOption.KeyFile)
if err != nil {
return nil, fmt.Errorf("fail to load x509 pair: %w", err)
}
certBytes, err := os.ReadFile(opts.ServerOption.CaFile)
if err != nil {
return nil, fmt.Errorf("failed to read ca file: %w", err)
}
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(certBytes)
if !ok {
return nil, fmt.Errorf("failed to parse ca file: %w", err)
}
// set server tls.Config
cfg := &tls.Config{
// add certificate
Certificates: []tls.Certificate{cert},
MaxVersion: tls.VersionTLS13,
// enable client authentication
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
// cipher suites supported
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
},
// set application protocol http2
NextProtos: []string{http2.NextProtoTLS},
}
serverOptions = append(serverOptions, server.WithTLS(cfg))
}
httpServer := server.Default(serverOptions...)
httpServer.AddProtocol(http2.NextProtoTLS, factory.NewServerFactory())
setupMiddlewares(httpServer)
setupRouter(httpServer, opts)
for _, r := range registers {
r.AddRoute(httpServer)
}
return httpServer, nil
}
func setupMiddlewares(h *server.Hertz) {
h.Use(
cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"PUT", "PATCH", "OPTIONS", "GET", "POST", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowWebSockets: true,
AllowWildcard: true,
MaxAge: 12 * time.Hour,
}),
requestid.New(
requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
return uuid.New().String()
}),
// set custom header for request id
requestid.WithCustomHeaderStrKey(consts.XRequestIDKey),
),
apphertz.Logger(),
)
}
func setupRouter(h *server.Hertz, opts *options.Options) {
h.GET("/ping", PingHandler)
h.GET("/version", VersionHandler)
url := swagger.URL("/swagger/doc.json") // The url pointing to API definition
h.GET("/swagger/*any", swagger.WrapHandler(swaggerFiles.Handler, url))
h.GET("/.well-known/configuration", clientConfigHandler(opts))
h.NoRoute(func(ctx context.Context, c *app.RequestContext) {
utils.WriteHertzErrorResponse(c, &apperrors.AppError{Code: apperrors.RouteNotFoundCode, Message: "route not found"})
})
}
type clientConfig struct {
Storage struct {
FSPath []string `json:"fsPath,omitempty"`
} `json:"storage"`
Notebook struct {
OfficialImages []notebook.Image `json:"officialImages"`
ResourceOptions []notebook.ResourceSize `json:"resourceOptions"`
} `json:"notebook"`
}
// GetClientConfig get client configuration
//
// @Summary use to get client configuration
// @Description get client configuration
// @Router /.well-known/configuration [get]
// @Produce application/json
// @Success 200 {object} clientConfig
// @Failure 401 {object} apperrors.AppError "unauthorized"
// @Failure 403 {object} apperrors.AppError "forbidden"
// @Failure 500 {object} apperrors.AppError "internal system error"
func clientConfigHandler(opts *options.Options) app.HandlerFunc {
return func(_ context.Context, ctx *app.RequestContext) {
var resp clientConfig
if opts.StorageOption != nil {
if opts.StorageOption.FileSystem != nil {
// string array is for multi share storage porpose in future
resp.Storage.FSPath = []string{opts.StorageOption.FileSystem.RootPath}
}
}
resp.Notebook.OfficialImages = opts.NotebookOption.OfficialImages
resp.Notebook.ResourceOptions = opts.NotebookOption.ListResourceSizes()
utils.WriteHertzOKResponse(ctx, &resp)
}
}
// PingHandler ping handler
//
// @Summary ping
// @Description ping
// @Accept application/json
// @Produce application/json
// @Router /ping [get]
// @Success 200
func PingHandler(_ context.Context, ctx *app.RequestContext) {
ctx.JSON(http.StatusOK, map[string]string{
"ping": "pong",
})
}
// VersionHandler version handler
//
// @Summary version Summary
// @Description version Description
// @Accept application/json
// @Produce application/json
// @Router /version [get]
// @Success 200
func VersionHandler(_ context.Context, ctx *app.RequestContext) {
ctx.JSON(http.StatusOK, version.Get())
}

View File

@ -0,0 +1,128 @@
package options
import (
"fmt"
"path"
"github.com/fsnotify/fsnotify"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"github.com/Bio-OS/bioos/internal/context/submission/infrastructure/client/wes"
"github.com/Bio-OS/bioos/pkg/auth"
"github.com/Bio-OS/bioos/pkg/client"
"github.com/Bio-OS/bioos/pkg/db"
"github.com/Bio-OS/bioos/pkg/eventbus"
"github.com/Bio-OS/bioos/pkg/log"
"github.com/Bio-OS/bioos/pkg/notebook"
"github.com/Bio-OS/bioos/pkg/server"
"github.com/Bio-OS/bioos/pkg/storage"
)
const (
ConfigFlagName = "config"
)
var cfgFile string
func init() {
pflag.StringVarP(&cfgFile, ConfigFlagName, "c", cfgFile, "Read configuration from specified `FILE`, "+
"support JSON, YAML formats.")
cobra.OnInitialize(initConfig)
}
func initConfig() {
viper.SetConfigName("apiserver") // name of config file (without extension)
if cfgFile != "" { // enable ability to specify config file via flag
viper.SetConfigFile(cfgFile)
configDir := path.Dir(cfgFile)
if configDir != "." {
viper.AddConfigPath(configDir)
}
}
viper.AutomaticEnv() // read in environment variables that match
viper.AddConfigPath("conf")
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME")
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
} else {
fmt.Println(err)
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
}
type Options struct {
Client *client.Options `json:"client" mapstructure:"client"`
ServerOption *server.Options `json:"server" mapstructure:"server"`
LogOption *log.Options `json:"log" mapstructure:"log"`
DBOption *db.Options `json:"db" mapstructure:"db"`
AuthOption *auth.Options `json:"auth" mapstructure:"auth"`
StorageOption *storage.Options `json:"storage" mapstructure:"storage"`
EventBusOption *eventbus.Options `json:"eventBus" mapstructure:"eventBus"`
WesOption *wes.Options `json:"wes" mapstructure:"wes"`
NotebookOption *notebook.Options `json:"notebook" mapstructure:"notebook"`
}
func NewOptions() *Options {
return &Options{
ServerOption: server.NewOptions(),
LogOption: log.NewOptions(),
DBOption: db.NewOptions(),
AuthOption: auth.NewOptions(),
StorageOption: storage.NewOptions(),
EventBusOption: eventbus.NewOptions(),
WesOption: wes.NewOptions(),
NotebookOption: notebook.NewOptions(),
}
}
// Validate validate log options is valid.
func (o *Options) Validate() error {
if err := o.ServerOption.Validate(); err != nil {
return err
}
if err := o.LogOption.Validate(); err != nil {
return err
}
if err := o.DBOption.Validate(); err != nil {
return err
}
if err := o.AuthOption.Validate(); err != nil {
return err
}
if err := o.EventBusOption.Validate(); err != nil {
return err
}
if err := o.StorageOption.Validate(); err != nil {
return err
}
if err := o.Client.Validate(); err != nil {
return err
}
if err := o.WesOption.Validate(); err != nil {
return err
}
if err := o.NotebookOption.Validate(); err != nil {
return err
}
return nil
}
func (o *Options) AddFlags(fs *pflag.FlagSet) {
o.ServerOption.AddFlags(fs)
o.LogOption.AddFlags(fs)
o.DBOption.AddFlags(fs)
o.AuthOption.AddFlags(fs)
o.StorageOption.AddFlags(fs)
o.EventBusOption.AddFlags(fs)
o.WesOption.AddFlags(fs)
o.NotebookOption.AddFlags(fs)
}

View File

@ -0,0 +1,177 @@
package apiserver
import (
"context"
"fmt"
"net"
"time"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
"k8s.io/component-base/featuregate"
_ "github.com/Bio-OS/bioos/docs" // for swagger
"github.com/Bio-OS/bioos/internal/apiserver/options"
notebookserverapp "github.com/Bio-OS/bioos/internal/context/notebookserver/application"
notebookservergrpc "github.com/Bio-OS/bioos/internal/context/notebookserver/interface/grpc"
notebookserverproto "github.com/Bio-OS/bioos/internal/context/notebookserver/interface/grpc/proto"
notebookserverhertz "github.com/Bio-OS/bioos/internal/context/notebookserver/interface/hertz"
submissionapp "github.com/Bio-OS/bioos/internal/context/submission/application"
submissiongrpc "github.com/Bio-OS/bioos/internal/context/submission/interface/grpc"
submissionproto "github.com/Bio-OS/bioos/internal/context/submission/interface/grpc/proto"
submissionhertz "github.com/Bio-OS/bioos/internal/context/submission/interface/hertz"
workspaceapp "github.com/Bio-OS/bioos/internal/context/workspace/application"
workspacegrpc "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
workspacehertz "github.com/Bio-OS/bioos/internal/context/workspace/interface/hertz"
"github.com/Bio-OS/bioos/pkg/log"
"github.com/Bio-OS/bioos/pkg/middlewares"
"github.com/Bio-OS/bioos/pkg/notebook"
"github.com/Bio-OS/bioos/pkg/server"
"github.com/Bio-OS/bioos/pkg/version"
)
const (
component = "bioos-apiserver"
)
func newBioosServerCommand(ctx context.Context, opts *options.Options) *cobra.Command {
return &cobra.Command{
Use: component,
Short: "bioos apiserver",
Long: `bioos apiserver
`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := viper.BindPFlags(cmd.Flags()); err != nil {
return err
}
if err := viper.Unmarshal(opts, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
notebook.ResourceQuantityStringToInt64HookFunc,
))); err != nil {
return err
}
if err := opts.Validate(); err != nil {
return err
}
version.PrintVersionOrContinue()
log.RegisterLogger(opts.LogOption)
defer log.Sync()
middlewares.RegisterAuthenticator(opts.AuthOption.AuthN)
middlewares.RegisterAuthorizer(opts.AuthOption.AuthZ)
log.Infow("server options", "options", opts)
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
log.Debugf("FLAG: --%s=%q", flag.Name, flag.Value)
})
return run(ctx, opts)
},
Args: cobra.ExactArgs(0),
}
}
func run(ctx context.Context, opts *options.Options) error {
log.Infow("called bioos-apiserver")
workspaceService, err := workspaceapp.NewWorkspaceService(ctx, opts)
if err != nil {
return err
}
defer func() {
_ = workspaceService.Close(ctx)
}()
workspaceGRPCService := workspacegrpc.NewServer(workspaceService)
// TODO invoke workspace API by grpc service client after solved backend token
notebookserverService, err := notebookserverapp.NewService(ctx, opts, workspaceGRPCService)
if err != nil {
return fmt.Errorf("new notebook server service fail: %w", err)
}
defer func() {
_ = notebookserverService.Close(ctx)
}()
submissionService, err := submissionapp.NewSubmissionService(ctx, opts)
if err != nil {
return err
}
defer func() {
_ = submissionService.Close(ctx)
}()
lis, err := net.Listen("tcp", fmt.Sprintf(":%s", opts.ServerOption.Grpc.Port))
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
workflowGRPCService := workspacegrpc.NewWorkflowServer(workspaceService)
datamodelGRPCService := workspacegrpc.NewDataModelServer(workspaceService)
notebookGRPCService := workspacegrpc.NewNotebookServer(workspaceService)
submissionGRPCService := submissiongrpc.NewSubmissionServer(submissionService)
versionGRPCService := workspacegrpc.NewVersionServer()
notebookserverGRPCService := notebookservergrpc.NewServer(notebookserverService)
grpcServer, err := setupGrpcServer(
opts,
server.GetGRPCRegister(workspaceproto.RegisterWorkspaceServiceServer, workspaceGRPCService),
server.GetGRPCRegister(workspaceproto.RegisterWorkflowServiceServer, workflowGRPCService),
server.GetGRPCRegister(workspaceproto.RegisterDataModelServiceServer, datamodelGRPCService),
server.GetGRPCRegister(workspaceproto.RegisterNotebookServiceServer, notebookGRPCService),
server.GetGRPCRegister(submissionproto.RegisterSubmissionServiceServer, submissionGRPCService),
server.GetGRPCRegister(workspaceproto.RegisterVersionServiceServer, versionGRPCService),
server.GetGRPCRegister(notebookserverproto.RegisterNotebookServerServiceServer, notebookserverGRPCService),
)
if err != nil {
log.Fatalf("failed to setup grpc server: %v", err)
}
httpServer, err := setupHTTPServer(
opts,
workspacehertz.NewRouteRegister(workspaceService),
submissionhertz.NewRouteRegister(submissionService),
notebookserverhertz.NewRouteRegister(notebookserverService),
)
if err != nil {
log.Fatalf("failed to setup http server: %v", err)
}
eg := errgroup.Group{}
eg.Go(func() error {
err := grpcServer.Serve(lis)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
return err
})
eg.Go(func() error {
httpServer.Spin()
return nil
})
if err := eg.Wait(); err != nil {
log.Fatalf("get error: %v", err)
}
return nil
}
// NewBioosServerCommand instance a bioos server command.
func NewBioosServerCommand(ctx context.Context) *cobra.Command {
opt := options.NewOptions()
cmd := newBioosServerCommand(ctx, opt)
opt.AddFlags(cmd.Flags())
featureGate := featuregate.NewFeatureGate()
featureGate.AddFlag(cmd.Flags())
version.AddFlags(cmd.Flags())
cmd.Flags().AddFlag(pflag.Lookup(options.ConfigFlagName))
return cmd
}

147
internal/bioctl/bioctl.go Normal file
View File

@ -0,0 +1,147 @@
package bioctl
import (
"flag"
"io"
"os"
"reflect"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
cliflag "k8s.io/component-base/cli/flag"
internalcmd "github.com/Bio-OS/bioos/internal/bioctl/cmd"
clidatamodel "github.com/Bio-OS/bioos/internal/bioctl/cmd/data-model"
clisubmission "github.com/Bio-OS/bioos/internal/bioctl/cmd/submission"
cliversion "github.com/Bio-OS/bioos/internal/bioctl/cmd/version"
cliworkflow "github.com/Bio-OS/bioos/internal/bioctl/cmd/workflow"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
)
// NewDefaultBIOCtlCommand return the bioctl cmd.
func NewDefaultBIOCtlCommand() *cobra.Command {
return NewBIOCtlCommandWithArgs(os.Stdin, os.Stdout, os.Stderr)
}
// NewBIOCtlCommandWithArgs initialized a bioctl command.
func NewBIOCtlCommandWithArgs(in io.Reader, out, err io.Writer) *cobra.Command {
command := newBIOCtlCommand()
command.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc)
flags := command.PersistentFlags()
addProfilingFlags(flags)
command.SetGlobalNormalizationFunc(cliflag.WordSepNormalizeFunc)
opt := clioptions.DefaultConfig
command.AddCommand(cliworkspace.NewCmdWorkspace(&opt))
command.AddCommand(cliworkflow.NewCmdWorkflow(&opt))
command.AddCommand(clidatamodel.NewCmdDataModel(&opt))
command.AddCommand(clisubmission.NewCmdSubmission(&opt))
addExample(command)
// version doesn't need Example text
command.AddCommand(cliversion.NewCmdVersion(&opt))
command.PersistentFlags().AddFlag(pflag.Lookup(clioptions.ConfigFlagName))
// read from config file
clioptions.LoadConfigFile(clioptions.CfgFile, &opt)
clioptions.SetConfigEnv()
// define global options
loadConfigFromOptionOrEnv(command, &opt.Client.ServerAddr, "server-addr", "SERVER_ADDR", "Bioos apiserver address")
loadConfigFromOptionOrEnv(command, &opt.Client.Insecure, "insecure", "INSECURE", "Whether enable tls")
loadConfigFromOptionOrEnv(command, &opt.Client.ServerCertFile, "server-cert-file", "SERVER_CERT_FILE", "Server cert file path")
loadConfigFromOptionOrEnv(command, &opt.Client.ServerName, "server-name", "SERVER_NAME", "Server Name")
loadConfigFromOptionOrEnv(command, &opt.Client.ClientCertFile, "client-cert-file", "CLIENT_CERT_FILE", "Client cert file path")
loadConfigFromOptionOrEnv(command, &opt.Client.ClientCertKeyFile, "client-cert-key-file", "CLIENT_CERT_KEY_FILE", "Client key file path")
loadConfigFromOptionOrEnv(command, &opt.Client.CaFile, "ca-file", "CA_FILE", "CA file path")
loadConfigFromOptionOrEnv(command, &opt.Client.Username, "username", "USERNAME", "Username")
loadConfigFromOptionOrEnv(command, &opt.Client.Password, "password", "PASSWORD", "Password")
loadConfigFromOptionOrEnv(command, &opt.Client.AuthToken, "auth-token", "AUTH_TOKEN", "Auth token")
loadConfigFromOptionOrEnv(command, (*string)(&opt.Client.Method), "connect-method", "CONNECT_METHOD", "Cli connect method: grpc or http")
loadConfigFromOptionOrEnv(command, &opt.Client.Timeout, "connect-timeout", "CONNECT_TIMEOUT", "Connect timeout seconds")
// format should only come from command line
command.PersistentFlags().StringVarP((*string)(&opt.Stream.OutputFormat), "output-format", "o", string(opt.Stream.OutputFormat), "Cli output format: json, yaml, table or text")
_ = viper.BindPFlags(command.PersistentFlags())
// read from environment
command.PersistentFlags().AddGoFlagSet(flag.CommandLine)
return command
}
var castFuncMap = map[reflect.Kind]interface{}{
reflect.Bool: cast.ToBool,
reflect.String: cast.ToString,
reflect.Int: cast.ToInt,
}
var pfFuncMap = map[reflect.Kind]interface{}{
reflect.Bool: (*pflag.FlagSet).BoolVar,
reflect.String: (*pflag.FlagSet).StringVar,
reflect.Int: (*pflag.FlagSet).IntVar,
}
// Read config from env or flag, flag > env
func loadConfigFromOptionOrEnv(command *cobra.Command, opt interface{}, optName, envName, usage string) {
optT := reflect.TypeOf(opt)
for optT.Kind() == reflect.Pointer {
optT = optT.Elem()
}
optKind := optT.Kind()
viper.BindEnv(envName)
eVal := viper.Get(envName)
defaultVal := reflect.ValueOf(opt).Elem()
if eVal != nil {
defaultVal = reflect.ValueOf(castFuncMap[optKind]).Call([]reflect.Value{reflect.ValueOf(eVal)})[0]
}
reflect.ValueOf(pfFuncMap[optKind]).Call([]reflect.Value{
reflect.ValueOf(command.PersistentFlags()),
reflect.ValueOf(opt),
reflect.ValueOf(optName),
defaultVal,
reflect.ValueOf(usage),
},
)
}
func newBIOCtlCommand() *cobra.Command {
return &cobra.Command{
Use: "bioctl",
Short: "bioctl command",
Long: `bioctl command`,
Run: runHelp,
PersistentPreRunE: func(*cobra.Command, []string) error {
return initProfiling()
},
PersistentPostRunE: func(*cobra.Command, []string) error {
return flushProfiling()
},
}
}
func runHelp(cmd *cobra.Command, args []string) {
_ = cmd.Help()
}
func addExample(cmd *cobra.Command) {
if !cmd.HasSubCommands() {
if cmd.Example != "" {
return
}
cmd.Example = internalcmd.GenExample(cmd)
return
}
for _, subCommand := range cmd.Commands() {
addExample(subCommand)
}
}

View File

@ -0,0 +1,22 @@
package data_model
import (
"github.com/spf13/cobra"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
func NewCmdDataModel(opt *clioptions.GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "data-model",
Short: "data-model command",
Long: `data-model command`,
Args: cobra.NoArgs,
Run: prompt.SelectSubCommand,
}
cmd.AddCommand(NewCmdImport(opt))
cmd.AddCommand(NewCmdList(opt))
cmd.AddCommand(NewCmdDelete(opt))
return cmd
}

View File

@ -0,0 +1,163 @@
package data_model
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// DeleteOptions is an options to delete a data-model.
type DeleteOptions struct {
WorkspaceName string
Name string
workspaceClient factory.WorkspaceClient
dataModelClient factory.DataModelClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewDeleteOptions returns a reference to a DeleteOptions
func NewDeleteOptions(opt *clioptions.GlobalOptions) *DeleteOptions {
return &DeleteOptions{
options: opt,
}
}
func NewCmdDelete(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewDeleteOptions(opt)
cmd := &cobra.Command{
Use: "delete",
Short: "delete a data-model",
Long: "delete a data-model",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.Name, "name", "n", o.Name, "data-model names to List")
return cmd
}
// Complete completes all the required options.
func (o *DeleteOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.dataModelClient, err = f.DataModelClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the delete options
func (o *DeleteOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the delete data-model command
func (o *DeleteOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
req := &convert.ListDataModelsRequest{
WorkspaceID: workspaceID,
}
remnant := make([]string, 0)
if o.Name != "" {
req.SearchWord = o.Name
remnant = []string{o.Name}
}
listResp, err := o.dataModelClient.ListDataModels(ctx, req)
if err != nil {
return err
}
for _, item := range listResp.Items {
// Delete all data-models
if o.Name == "" {
_, err := o.dataModelClient.DeleteDataModel(ctx, &convert.DeleteDataModelRequest{
ID: item.ID,
WorkspaceID: workspaceID,
})
if err != nil {
remnant = append(remnant, item.Name)
}
// Delete one specified data_model
} else {
if item.Name == o.Name {
_, err := o.dataModelClient.DeleteDataModel(ctx, &convert.DeleteDataModelRequest{
ID: item.ID,
WorkspaceID: workspaceID,
})
if err != nil {
return err
} //remove specified data_model name from remnant
remnant = []string{}
break
}
}
}
if len(remnant) > 0 {
return fmt.Errorf("%v delete failed", remnant)
}
return nil
}
func (o *DeleteOptions) GetPromptArgs() ([]string, error) {
return nil, nil
}
func (o *DeleteOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.Name, err = prompt.PromptOptionalString("DataModel Name", prompt.WithInputMessage("all dataModel will be deleted if no name specified"))
if err != nil {
return err
}
return nil
}
func (o *DeleteOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,187 @@
package data_model
import (
"context"
"fmt"
"os"
"path"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/pkg/consts"
"github.com/Bio-OS/bioos/pkg/utils"
)
// ImportOptions is an options to import a data-model.
type ImportOptions struct {
WorkspaceName string
InputFile string
workspaceClient factory.WorkspaceClient
dataModelClient factory.DataModelClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewImportOptions returns a reference to a ImportOptions
func NewImportOptions(opt *clioptions.GlobalOptions) *ImportOptions {
return &ImportOptions{
options: opt,
}
}
func NewCmdImport(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewImportOptions(opt)
cmd := &cobra.Command{
Use: "import",
Short: "import a data-model",
Long: "import a data-model",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.InputFile, "input-file ", "i", o.InputFile, "the file (only support csv) to import")
return cmd
}
// Complete completes all the required options.
func (o *ImportOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.dataModelClient, err = f.DataModelClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the import options
func (o *ImportOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
if o.InputFile == "" || path.Ext(o.InputFile) != ".csv" {
return fmt.Errorf("you must choose a csv file to import")
}
_, err := os.Stat(o.InputFile)
if err != nil {
return err
}
return nil
}
// Run run the import data-model command
func (o *ImportOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
name := strings.TrimSuffix(path.Base(o.InputFile), path.Ext(o.InputFile))
listResp, err := o.dataModelClient.ListDataModels(ctx, &convert.ListDataModelsRequest{
WorkspaceID: workspaceID,
Types: []string{consts.DataModelTypeEntity},
SearchWord: name,
})
if err != nil {
return err
}
for _, item := range listResp.Items {
if item.Name == name {
_, err = o.dataModelClient.DeleteDataModel(ctx, &convert.DeleteDataModelRequest{
ID: item.ID,
WorkspaceID: workspaceID,
})
if err != nil {
return fmt.Errorf("delete existed dataModel %s failed: %w", name, err)
}
}
}
req := &convert.PatchDataModelRequest{
WorkspaceID: workspaceID,
Name: name,
}
headers, rows, err := utils.ReadDataModelFromCSV(o.InputFile)
if err != nil {
return err
}
req.Headers = headers
req.Rows = make([][]string, len(rows))
for i, data := range rows {
req.Rows[i] = data
}
resp, err := o.dataModelClient.PatchDataModel(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.ID)
return nil
}
func (o *ImportOptions) GetPromptArgs() ([]string, error) {
return nil, nil
}
func (o *ImportOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.InputFile, err = prompt.PromptStringWithValidator("Input File Path", func(ans interface{}) error {
err := survey.Required(ans)
if err != nil {
return err
}
_, err = os.Stat(cast.ToString(ans))
if err != nil {
curPath, _ := os.Getwd()
return fmt.Errorf("%w, (currenct path is [%s])", err, curPath)
}
return nil
}, prompt.WithInputMessage("only support CSV"))
if err != nil {
return err
}
return nil
}
func (o *ImportOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,207 @@
package data_model
import (
"context"
"fmt"
"reflect"
"time"
"github.com/iancoleman/strcase"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/pkg/consts"
)
// ListOptions is an options to List data-models.
type ListOptions struct {
WorkspaceName string
Types []string
Name string
workspaceClient factory.WorkspaceClient
dataModelClient factory.DataModelClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewListOptions returns a reference to a ListOptions
func NewListOptions(opt *clioptions.GlobalOptions) *ListOptions {
return &ListOptions{
options: opt,
}
}
func NewCmdList(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewListOptions(opt)
cmd := &cobra.Command{
Use: "list",
Short: "list data-models",
Long: "list data-models of a specified workspace",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.Name, "name", "n", o.Name, "data-model names to List")
cmd.Flags().StringSliceVarP(&o.Types, "types", "t", o.Types, "data-model types to List")
return cmd
}
// Complete completes all the required options.
func (o *ListOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.dataModelClient, err = f.DataModelClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the List options
func (o *ListOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
for _, t := range o.Types {
if t != consts.DataModelTypeEntity && t != consts.DataModelTypeEntitySet && t != consts.DataModelTypeWorkspace {
return fmt.Errorf("data-model type %s not support", t)
}
}
return nil
}
// Run run the List data-model command
func (o *ListOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
if o.Name != "" {
dataModelID, err := ConvertDataModelNameIntoID(ctx, o.dataModelClient, workspaceID, o.Name)
if err != nil {
return err
}
headers, rows, err := cmd.ParseWholeDataModel(ctx, o.dataModelClient, workspaceID, dataModelID)
if err != nil {
return err
}
if len(rows) > 10000 {
o.formatter.Write(fmt.Sprintf("It will take a while to collect in that the length of dataModel is too big: %d rows", len(rows)))
}
o.formatter.Write(getDataModelStruct(headers, rows))
return nil
}
req := &convert.ListDataModelsRequest{
WorkspaceID: workspaceID,
}
if len(o.Types) > 0 {
req.Types = o.Types
}
resp, err := o.dataModelClient.ListDataModels(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.Items)
return nil
}
func (o *ListOptions) GetPromptArgs() ([]string, error) {
return nil, nil
}
func (o *ListOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.Name, err = prompt.PromptOptionalString("DataModel Name")
if err != nil {
return err
}
if o.Name == "" {
o.Types, err = prompt.PromptStringMultiSelect("DataModel Types", 3,
[]string{consts.DataModelTypeEntity, consts.DataModelTypeEntitySet, consts.DataModelTypeWorkspace},
)
if err != nil {
return err
}
}
return nil
}
func (o *ListOptions) GetDefaultFormat() formatter.Format {
return formatter.TableFormat
}
func ConvertDataModelNameIntoID(ctx context.Context, dataModelClient factory.DataModelClient, wsID, dataModelName string) (string, error) {
resp, err := dataModelClient.ListDataModels(ctx, &convert.ListDataModelsRequest{
WorkspaceID: wsID,
SearchWord: dataModelName,
})
if err != nil {
return "", err
}
for _, item := range resp.Items {
if item.Name == dataModelName {
return item.ID, nil
}
}
return "", fmt.Errorf("no data-model named %s found", dataModelName)
}
func getDataModelStruct(headers []string, rows [][]string) interface{} {
f := make([]reflect.StructField, len(headers))
fields := headers
tOfStr := reflect.TypeOf("")
for i, v := range fields {
f[i] = reflect.StructField{
Name: strcase.ToCamel(reflect.ValueOf(v).Interface().(string)),
Type: tOfStr,
}
}
t := reflect.StructOf(f)
items := reflect.MakeSlice(reflect.SliceOf(t), len(rows), len(rows))
for i, row := range rows {
e := reflect.New(t).Elem()
for j, elem := range row {
e.Field(j).Set(reflect.ValueOf(elem))
}
items.Index(i).Set(e)
}
return items.Interface()
}

View File

@ -0,0 +1,158 @@
package cmd
import (
"bytes"
"context"
"fmt"
"text/template"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
func GetAllRuns(ctx context.Context, submissionClient factory.SubmissionClient, workspaceID string, submissionID string) ([]convert.RunItem, error) {
resp, err := submissionClient.ListRuns(ctx, &convert.ListRunsRequest{
Page: 1,
Size: 100,
WorkspaceID: workspaceID,
SubmissionID: submissionID,
})
if err != nil {
return nil, err
}
currentNum := resp.Size
currentPage := resp.Page
items := resp.Items
for currentNum < resp.Total {
currentNum += len(resp.Items)
currentPage += 1
resp, err = submissionClient.ListRuns(ctx, &convert.ListRunsRequest{
Page: currentPage,
Size: 100,
WorkspaceID: workspaceID,
SubmissionID: submissionID,
})
if err != nil {
return nil, err
}
items = append(items, resp.Items...)
}
return items, nil
}
func ParseWholeDataModel(ctx context.Context, dataModelClient factory.DataModelClient, workspaceID, dataModelID string) ([]string, [][]string, error) {
resp, err := dataModelClient.ListDataModelRows(ctx, &convert.ListDataModelRowsRequest{
Page: 1,
Size: 100,
WorkspaceID: workspaceID,
ID: dataModelID,
})
if err != nil {
return nil, nil, err
}
currentNum := int64(resp.Size)
currentPage := resp.Page
headers := resp.Headers
rows := resp.Rows
for currentNum < resp.Total {
currentNum += int64(resp.Size)
currentPage += 1
resp, err := dataModelClient.ListDataModelRows(ctx, &convert.ListDataModelRowsRequest{
Page: currentPage,
Size: 100,
WorkspaceID: workspaceID,
ID: dataModelID,
})
if err != nil {
return nil, nil, err
}
rows = append(rows, resp.Rows...)
}
return headers, rows, nil
}
func GetWorkspaceName(timeout int, workspaceClient factory.WorkspaceClient) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(timeout))
defer cancel()
resp, err := workspaceClient.ListWorkspaces(ctx, &convert.ListWorkspacesRequest{})
if err != nil {
return "", err
}
if resp.Total == 0 {
return "", fmt.Errorf("no workspace found")
}
var workspaceName string
if resp.Total > 20 {
workspaceName, err = prompt.PromptRequiredString("WorkspaceName", prompt.WithInputMessage(""))
} else {
items := make([]string, len(resp.Items))
for j := range resp.Items {
workspace := resp.Items[j]
items[j] = fmt.Sprintf("%s", workspace.Name)
}
workspaceName, err = prompt.PromptStringSelect("WorkspaceName", 10, items)
}
if err != nil {
return "", err
}
return workspaceName, nil
}
var ExampleTemplate = template.Must(template.New("Example").Parse(
` # Use Command-Line mode:
"bioctl {{.Use}} {{- range .OptionsUsage }} {{ . }} {{- end }}"
# Enter Interactive mode:
1. Firstly input parent Command:
"bioctl {{.ParentName}}"
2. Select Subcommand
"{{.Name}}"
`))
type ExampleData struct {
ParentName string
Name string
Use string
OptionsUsage []string
}
// "# list workspaces: bioctl workspace list"
func GenExample(cmd *cobra.Command) string {
// template.
flags := cmd.Flags()
// cmd.Args
flags.FlagUsages()
data := ExampleData{
ParentName: cmd.Parent().Name(),
Name: cmd.Name(),
Use: cmd.Use,
}
flags.VisitAll(func(flag *pflag.Flag) {
var option string
if flag.Shorthand != "" {
option += fmt.Sprintf("-%s/", flag.Shorthand)
}
option += fmt.Sprintf("--%s [%s]", flag.Name, flag.Value.Type())
data.OptionsUsage = append(data.OptionsUsage, option)
})
var res bytes.Buffer
ExampleTemplate.Execute(&res, data)
return res.String()
}

View File

@ -0,0 +1,128 @@
package submission
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// DeleteOptions is an options to delete a workspace.
type DeleteOptions struct {
WorkspaceName string
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewDeleteOptions returns a reference to a DeleteOptions
func NewDeleteOptions(opt *clioptions.GlobalOptions) *DeleteOptions {
return &DeleteOptions{
options: opt,
}
}
func NewCmdDelete(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewDeleteOptions(opt)
cmd := &cobra.Command{
Use: "delete <submission_id>",
Short: "delete the submission",
Long: "delete the submission",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
return cmd
}
// Complete completes all the required options.
func (o *DeleteOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the delete options
func (o *DeleteOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the delete workspace command
func (o *DeleteOptions) Run(args []string) error {
submissionID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
_, err = o.submissionClient.DeleteSubmission(ctx, &convert.DeleteSubmissionRequest{
WorkspaceID: workspaceID,
ID: submissionID,
})
if err != nil {
return err
}
o.formatter.Write(fmt.Sprintf("submission [%s] will be deleted soon", submissionID))
return nil
}
func (o *DeleteOptions) GetPromptArgs() ([]string, error) {
submissionID, err := prompt.PromptRequiredString("Submission ID")
if err != nil {
return []string{}, err
}
return []string{submissionID}, nil
}
func (o *DeleteOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
return nil
}
func (o *DeleteOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,200 @@
package submission
import (
"context"
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/internal/context/submission/application/query/submission"
"github.com/Bio-OS/bioos/pkg/consts"
)
// ListOptions is an options to List submissions.
type ListOptions struct {
WorkspaceName string
Status []string
Page int32
Size int32
OrderBy string
SearchWords []string
Ids []string
workspaceClient factory.WorkspaceClient
submissionClient factory.SubmissionClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewListOptions returns a reference to a ListOptions
func NewListOptions(opt *clioptions.GlobalOptions) *ListOptions {
return &ListOptions{
options: opt,
}
}
func NewCmdList(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewListOptions(opt)
cmd := &cobra.Command{
Use: "list",
Short: "list submissions",
Long: "list submissions of a specified workspace",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringSliceVar(&o.Status, "status", o.Status, "filtered status")
cmd.Flags().Int32VarP(&o.Page, "page", "p", 1, "The page number")
cmd.Flags().Int32VarP(&o.Size, "size", "s", 10, "The page size")
cmd.Flags().StringVar(&o.OrderBy, "order-by", o.OrderBy, "The order-by field")
cmd.Flags().StringSliceVar(&o.SearchWords, "search-word", o.SearchWords, "The search word")
cmd.Flags().StringSliceVar(&o.Ids, "ids", o.Ids, "The ids of the workspace.")
return cmd
}
// Complete completes all the required options.
func (o *ListOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
} else {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the List options
func (o *ListOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the List submission command
func (o *ListOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
req := &convert.ListSubmissionsRequest{
WorkspaceID: workspaceID,
Page: int(o.Page),
Size: int(o.Size),
}
if o.OrderBy != "" {
req.OrderBy = o.OrderBy
}
if len(o.SearchWords) > 0 {
req.SearchWord = strings.Join(o.SearchWords, consts.QuerySliceDelimiter)
}
if len(o.Status) > 0 {
req.Status = o.Status
}
if len(o.Ids) > 0 {
req.IDs = o.Ids
}
resp, err := o.submissionClient.ListSubmissions(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp)
return nil
}
func (o *ListOptions) GetPromptArgs() ([]string, error) {
return nil, nil
}
func (o *ListOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.Page, err = prompt.PromptRequiredInt32("Page")
if err != nil {
return err
}
o.Size, err = prompt.PromptRequiredInt32("Size")
if err != nil {
return err
}
orderByFields, err := prompt.PromptStringMultiSelect("OrderBy", 2, []string{submission.OrderByName, submission.OrderByStartTime})
if err != nil {
return err
}
if len(orderByFields) > 0 {
for i, field := range orderByFields {
ascending, err := prompt.PromptStringSelect(fmt.Sprintf("%s Ascending", field), 2, []string{consts.ASCOrdering, consts.DESCOrdering})
if err != nil {
return err
}
orderByFields[i] += consts.OrderDelimiter + ascending
}
o.OrderBy = strings.Join(orderByFields, ",")
}
o.Status, err = prompt.PromptStringMultiSelect("Status", 7, []string{consts.SubmissionPending,
consts.SubmissionRunning, consts.SubmissionFailed, consts.SubmissionFinished, consts.SubmissionSucceeded, consts.SubmissionCancelling,
consts.SubmissionCancelled})
if err != nil {
return err
}
o.SearchWords, err = prompt.PromptStringSlice("SearchWords")
if err != nil {
return err
}
o.Ids, err = prompt.PromptStringSlice("IDs")
if err != nil {
return err
}
// correct formatter
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
}
return nil
}
func (o *ListOptions) GetDefaultFormat() formatter.Format {
return formatter.TableFormat
}

View File

@ -0,0 +1,213 @@
package submission
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// LogOptions is an options to log a workspace.
type LogOptions struct {
WorkspaceName string
RunID string
TaskName string
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewLogOptions returns a reference to a LogOptions
func NewLogOptions(opt *clioptions.GlobalOptions) *LogOptions {
return &LogOptions{
options: opt,
}
}
func NewCmdLog(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewLogOptions(opt)
cmd := &cobra.Command{
Use: "log <submission_id>",
Short: "get the log of the submission",
Long: "get the log of the submission",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.RunID, "run-id", "r", o.RunID, "The RunID of the submission.")
cmd.Flags().StringVarP(&o.TaskName, "task-name", "t", o.TaskName, "The TaskName of the submission")
return cmd
}
// Complete completes all the required options.
func (o *LogOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the log options
func (o *LogOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.TaskName != "" {
if o.RunID == "" {
return fmt.Errorf("must specify a run id before specifying a task name ")
}
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the log workspace command
func (o *LogOptions) Run(args []string) error {
submissionID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
if o.RunID != "" {
runResp, err := o.submissionClient.ListRuns(ctx, &convert.ListRunsRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
IDs: []string{o.RunID},
})
if err != nil {
return err
}
if len(runResp.Items) == 0 {
return fmt.Errorf("run: %s not found", o.RunID)
}
if o.TaskName != "" {
taskResp, err := o.submissionClient.ListTasks(ctx, &convert.ListTasksRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
RunID: o.RunID,
})
if err != nil {
return err
}
for _, item := range taskResp.Items {
if item.Name == o.TaskName {
if item.Stdout != "" {
o.formatter.Write(item.Stdout)
} else {
o.formatter.Write(fmt.Sprintf("the Stdout of task [%s] is unavailable now", o.TaskName))
}
if item.Stderr != "" {
o.formatter.Write(item.Stderr)
} else {
o.formatter.Write(fmt.Sprintf("the Stderr of task [%s] is unavailable now", o.TaskName))
}
return nil
}
}
if len(taskResp.Items) == 0 {
return fmt.Errorf("task: %s not found", o.TaskName)
}
return nil
}
if runResp.Items[0].Log != nil {
o.formatter.Write(runResp.Items[0].Log)
} else {
o.formatter.Write(fmt.Sprintf("the log of run [%s] is unavailable now", runResp.Items[0].ID))
}
return nil
}
runs, err := cmd.GetAllRuns(ctx, o.submissionClient, workspaceID, submissionID)
if err != nil {
return err
}
for _, run := range runs {
if run.Log != nil {
o.formatter.Write(struct {
Log *string
RunID string
}{
Log: run.Log,
RunID: run.ID,
})
continue
}
o.formatter.Write(fmt.Sprintf("the log of run [%s] is unavailable now", run.ID))
}
return nil
}
func (o *LogOptions) GetPromptArgs() ([]string, error) {
submissionID, err := prompt.PromptRequiredString("Submission ID")
if err != nil {
return []string{}, err
}
return []string{submissionID}, nil
}
func (o *LogOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.RunID, err = prompt.PromptOptionalString("Run ID")
if err != nil {
return err
}
if o.RunID != "" {
o.TaskName, err = prompt.PromptOptionalString("Task Name")
if err != nil {
return err
}
}
return nil
}
func (o *LogOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,201 @@
package submission
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// OutputOptions is an options to output a workspace.
type OutputOptions struct {
WorkspaceName string
RunID string
TaskName string
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewOutputOptions returns a reference to a OutputOptions
func NewOutputOptions(opt *clioptions.GlobalOptions) *OutputOptions {
return &OutputOptions{
options: opt,
}
}
func NewCmdOutput(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewOutputOptions(opt)
cmd := &cobra.Command{
Use: "output <submission_id>",
Short: "get the output of the submission",
Long: "get the output of the submission",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.RunID, "run-id", "r", o.RunID, "The RunID of the submission.")
cmd.Flags().StringVarP(&o.TaskName, "task-name", "t", o.TaskName, "The TaskName of the submission")
return cmd
}
// Complete completes all the required options.
func (o *OutputOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the output options
func (o *OutputOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the output workspace command
func (o *OutputOptions) Run(args []string) error {
submissionID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
if o.RunID != "" {
runResp, err := o.submissionClient.ListRuns(ctx, &convert.ListRunsRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
IDs: []string{o.RunID},
})
if err != nil {
return err
}
if len(runResp.Items) == 0 {
return fmt.Errorf("run: %s not found", o.TaskName)
}
if o.TaskName != "" {
taskResp, err := o.submissionClient.ListTasks(ctx, &convert.ListTasksRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
RunID: o.RunID,
})
if err != nil {
return err
}
for _, item := range taskResp.Items {
if item.Name == o.TaskName {
if item.Stdout != "" {
o.formatter.Write(item.Stdout)
} else {
o.formatter.Write(fmt.Sprintf("the outputs of task [%s] is unavailable now", o.TaskName))
}
return nil
}
}
if len(taskResp.Items) == 0 {
return fmt.Errorf("task: %s not found", o.TaskName)
}
return nil
}
if runResp.Items[0].Outputs != "" {
o.formatter.Write(runResp.Items[0].Outputs)
} else {
o.formatter.Write(fmt.Sprintf("the outputs of run [%s] is unavailable now", runResp.Items[0].ID))
}
return nil
}
runs, err := cmd.GetAllRuns(ctx, o.submissionClient, workspaceID, submissionID)
if err != nil {
return err
}
for _, run := range runs {
if run.Log != nil {
o.formatter.Write(struct {
RunID string
Outputs string
}{
RunID: run.ID,
Outputs: run.Outputs,
})
continue
}
o.formatter.Write(fmt.Sprintf("the outputs of run [%s] is unavailable now", run.ID))
}
return nil
}
func (o *OutputOptions) GetPromptArgs() ([]string, error) {
submissionID, err := prompt.PromptRequiredString("Submission ID")
if err != nil {
return []string{}, err
}
return []string{submissionID}, nil
}
func (o *OutputOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.RunID, err = prompt.PromptOptionalString("Run ID")
if err != nil {
return err
}
if o.RunID != "" {
o.TaskName, err = prompt.PromptOptionalString("Task Name")
if err != nil {
return err
}
}
return nil
}
func (o *OutputOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,192 @@
package submission
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// QueryOptions is an options to query a workspace.
type QueryOptions struct {
WorkspaceName string
RunID string
TaskName string
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewQueryOptions returns a reference to a QueryOptions
func NewQueryOptions(opt *clioptions.GlobalOptions) *QueryOptions {
return &QueryOptions{
options: opt,
}
}
func NewCmdQuery(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewQueryOptions(opt)
cmd := &cobra.Command{
Use: "query <submission_id>",
Short: "get the status of the submission",
Long: "get the status of the submission",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.RunID, "run-id", "r", o.RunID, "The RunID of the submission.")
cmd.Flags().StringVarP(&o.TaskName, "task-name", "t", o.TaskName, "The TaskName of the submission")
return cmd
}
// Complete completes all the required options.
func (o *QueryOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the query options
func (o *QueryOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.TaskName != "" {
if o.RunID == "" {
return fmt.Errorf("must specify a run id before specifying a task name ")
}
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the query workspace command
func (o *QueryOptions) Run(args []string) error {
submissionID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
resp, err := o.submissionClient.ListSubmissions(ctx, &convert.ListSubmissionsRequest{
WorkspaceID: workspaceID,
IDs: []string{submissionID},
})
if err != nil {
return err
}
if len(resp.Items) == 0 {
return fmt.Errorf("submission: %s not found", submissionID)
}
if o.RunID != "" {
runResp, err := o.submissionClient.ListRuns(ctx, &convert.ListRunsRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
IDs: []string{o.RunID},
})
if err != nil {
return err
}
if len(runResp.Items) == 0 {
return fmt.Errorf("run: %s not found", o.TaskName)
}
if o.TaskName != "" {
taskResp, err := o.submissionClient.ListTasks(ctx, &convert.ListTasksRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
RunID: o.RunID,
})
if err != nil {
return err
}
for _, item := range taskResp.Items {
if item.Name == o.TaskName {
o.formatter.Write(item.Status)
return nil
}
}
return fmt.Errorf("task: %s not found", o.TaskName)
}
o.formatter.Write(runResp.Items[0].Status)
return nil
}
o.formatter.Write(resp.Items[0].Status)
return nil
}
func (o *QueryOptions) GetPromptArgs() ([]string, error) {
submissionID, err := prompt.PromptRequiredString("Submission ID")
if err != nil {
return []string{}, err
}
return []string{submissionID}, nil
}
func (o *QueryOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.RunID, err = prompt.PromptOptionalString("Run ID")
if err != nil {
return err
}
if o.RunID != "" {
o.TaskName, err = prompt.PromptOptionalString("Task Name")
if err != nil {
return err
}
}
return nil
}
func (o *QueryOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,149 @@
package submission
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// StopOptions is an options to stop a workspace.
type StopOptions struct {
WorkspaceName string
RunID string
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewStopOptions returns a reference to a StopOptions
func NewStopOptions(opt *clioptions.GlobalOptions) *StopOptions {
return &StopOptions{
options: opt,
}
}
func NewCmdStop(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewStopOptions(opt)
cmd := &cobra.Command{
Use: "stop <submission_id>",
Short: "stop the submission",
Long: "stop the submission",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.RunID, "run-id", "r", o.RunID, "The RunID of the submission.")
return cmd
}
// Complete completes all the required options.
func (o *StopOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the stop options
func (o *StopOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
return nil
}
// Run run the stop workspace command
func (o *StopOptions) Run(args []string) error {
submissionID := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
if o.RunID != "" {
_, err := o.submissionClient.CancelRun(ctx, &convert.CancelRunRequest{
WorkspaceID: workspaceID,
SubmissionID: submissionID,
ID: o.RunID,
})
if err != nil {
return err
}
o.formatter.Write(fmt.Sprintf("run [%s] will be canceled soon", o.RunID))
return nil
}
_, err = o.submissionClient.CancelSubmission(ctx, &convert.CancelSubmissionRequest{
WorkspaceID: workspaceID,
ID: submissionID,
})
if err != nil {
return err
}
o.formatter.Write(fmt.Sprintf("submission [%s] will be canceled soon", submissionID))
return nil
}
func (o *StopOptions) GetPromptArgs() ([]string, error) {
submissionID, err := prompt.PromptRequiredString("Submission ID")
if err != nil {
return []string{}, err
}
return []string{submissionID}, nil
}
func (o *StopOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.RunID, err = prompt.PromptOptionalString("Run ID")
if err != nil {
return err
}
return nil
}
func (o *StopOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,26 @@
package submission
import (
"github.com/spf13/cobra"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
func NewCmdSubmission(opt *clioptions.GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "submission",
Short: "submission command",
Long: `submission command`,
Args: cobra.NoArgs,
Run: prompt.SelectSubCommand,
}
cmd.AddCommand(NewCmdSubmit(opt))
cmd.AddCommand(NewCmdQuery(opt))
cmd.AddCommand(NewCmdDelete(opt))
cmd.AddCommand(NewCmdStop(opt))
cmd.AddCommand(NewCmdLog(opt))
cmd.AddCommand(NewCmdList(opt))
cmd.AddCommand(NewCmdOutput(opt))
return cmd
}

View File

@ -0,0 +1,324 @@
package submission
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
clidatamodel "github.com/Bio-OS/bioos/internal/bioctl/cmd/data-model"
cliworkflow "github.com/Bio-OS/bioos/internal/bioctl/cmd/workflow"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/pkg/consts"
)
type InputsOutputs struct {
InputsTemplate map[string]interface{}
OutputsTemplate map[string]interface{}
InputsMaterial map[string]interface{}
OutputsMaterial map[string]interface{}
}
const emptyJsonStr = "null"
// SubmitOptions is an options to submit a workspace.
type SubmitOptions struct {
WorkspaceName string
Description string
Type string
DataModelName string
DataModelRowIDs []string
File string
ReadFromCache bool
InputsTemplate string
OutputsTemplate string
InputsMaterial string
OutputsMaterial string
dataModelClient factory.DataModelClient
workflowClient factory.WorkflowClient
submissionClient factory.SubmissionClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewSubmitOptions returns a reference to a SubmitOptions
func NewSubmitOptions(opt *clioptions.GlobalOptions) *SubmitOptions {
return &SubmitOptions{
options: opt,
}
}
func NewCmdSubmit(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewSubmitOptions(opt)
cmd := &cobra.Command{
Use: "submit <workflow_name>",
Short: "submit a workflow",
Long: "submit a workflow",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The workspace name")
cmd.Flags().StringVarP(&o.Description, "description", "d", o.Description, "The description of the submission.")
cmd.Flags().StringVarP(&o.Type, "type", "t", o.Type, "The Type of the submission.")
cmd.Flags().StringVarP(&o.DataModelName, "data-model", "m", o.DataModelName, "The name of the data-model this submission will use.")
cmd.Flags().StringSliceVar(&o.DataModelRowIDs, "data-model-rows", o.DataModelRowIDs, "The rows of the data-model this submission will use.")
cmd.Flags().StringVarP(&o.File, "file", "f", o.File, "The file path of Inputs/Outputs.")
cmd.Flags().BoolVar(&o.ReadFromCache, "call-caching", true, "use previous cache of the submission or not.")
return cmd
}
// Complete completes all the required options.
func (o *SubmitOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
o.submissionClient, err = f.SubmissionClient()
if err != nil {
return err
}
o.dataModelClient, err = f.DataModelClient()
if err != nil {
return err
}
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the submit options
func (o *SubmitOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("need to specify a workspace name")
}
if o.Type == "" {
return fmt.Errorf("submission type cannot be empty")
}
if o.File == "" {
return fmt.Errorf("need to specify a file to declare inputs and outputs")
}
err := o.parseInputsAndOutputsFile()
if err != nil {
return err
}
if o.Type == consts.DataModelTypeSubmission {
if o.InputsTemplate == "" {
return fmt.Errorf("InputsTemplate cannot be empty")
}
if o.DataModelName == "" {
return fmt.Errorf("need to specify a data-model")
}
} else if o.Type == consts.FilePathTypeSubmission {
if o.InputsMaterial == "" {
return fmt.Errorf("InputsMaterial cannot be empty")
}
} else {
return fmt.Errorf("submission type %s not support", o.Type)
}
return nil
}
// Run run the submit workspace command
func (o *SubmitOptions) Run(args []string) error {
workflowName := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
workflowID, err := cliworkflow.ConvertWorkflowNameIntoID(ctx, o.workflowClient, workspaceID, workflowName)
if err != nil {
return err
}
req := &convert.CreateSubmissionRequest{
WorkspaceID: workspaceID,
Name: fmt.Sprintf("%s-history-%s", workflowName, time.Now().Format("2006-01-02-15-04-05")),
WorkflowID: workflowID,
Type: o.Type,
ExposedOptions: convert.ExposedOptions{
ReadFromCache: o.ReadFromCache,
},
}
if o.Type == consts.DataModelTypeSubmission {
dataModelID, err := clidatamodel.ConvertDataModelNameIntoID(ctx, o.dataModelClient, workspaceID, o.DataModelName)
if err != nil {
return err
}
if len(o.DataModelRowIDs) == 0 {
idsResp, err := o.dataModelClient.ListAllDataModelRowIDs(ctx, &convert.ListAllDataModelRowIDsRequest{
WorkspaceID: workspaceID,
ID: dataModelID,
})
if err != nil {
return err
}
o.DataModelRowIDs = idsResp.RowIDs
}
req.Entity = &convert.Entity{
DataModelID: dataModelID,
DataModelRowIDs: o.DataModelRowIDs,
InputsTemplate: o.InputsTemplate,
OutputsTemplate: o.OutputsTemplate,
}
} else if o.Type == consts.FilePathTypeSubmission {
req.InOutMaterial = &convert.InOutMaterial{
InputsMaterial: o.InputsMaterial,
OutputsMaterial: o.OutputsMaterial,
}
}
if o.Description != "" {
req.Description = &o.Description
}
resp, err := o.submissionClient.CreateSubmission(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.ID)
return nil
}
func (o *SubmitOptions) GetPromptArgs() ([]string, error) {
workflowName, err := prompt.PromptRequiredString("Workflow Name")
if err != nil {
return []string{}, err
}
return []string{workflowName}, nil
}
func (o *SubmitOptions) GetPromptOptions() error {
var err error
// TODO put it in GetPromptArgs? enhance interactive user experience
o.WorkspaceName, err = cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return err
}
o.Description, err = prompt.PromptOptionalString("Description")
if err != nil {
return err
}
o.Type, err = prompt.PromptStringSelect("Submission Type", 2, []string{consts.DataModelTypeSubmission, consts.FilePathTypeSubmission})
if err != nil {
return err
}
if o.Type == consts.DataModelTypeSubmission {
o.DataModelName, err = prompt.PromptRequiredString("DataModel Name")
if err != nil {
return err
}
o.DataModelRowIDs, err = prompt.PromptStringSlice(fmt.Sprintf("RowIDs of [%s]", o.DataModelName))
if err != nil {
return err
}
}
o.File, err = prompt.PromptStringWithValidator("Inputs and Outputs FilePath", func(ans interface{}) error {
err := survey.Required(ans)
if err != nil {
return err
}
_, err = os.Stat(cast.ToString(ans))
if err != nil {
curPath, _ := os.Getwd()
return fmt.Errorf("%w, (currenct path is [%s])", err, curPath)
}
return nil
})
if err != nil {
return err
}
o.ReadFromCache, err = prompt.PromptBoolSelect("ReadFromCache")
if err != nil {
return err
}
return nil
}
func (o *SubmitOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
func (o *SubmitOptions) parseInputsAndOutputsFile() error {
bytes, err := os.ReadFile(o.File)
if err != nil {
return err
}
inOuts := InputsOutputs{}
err = json.Unmarshal(bytes, &inOuts)
if err != nil {
return err
}
dataInputsTemplate, _ := json.Marshal(inOuts.InputsTemplate)
o.InputsTemplate = string(dataInputsTemplate)
dataOutputsTemplate, _ := json.Marshal(inOuts.OutputsTemplate)
o.OutputsTemplate = string(dataOutputsTemplate)
dataInputsMaterial, _ := json.Marshal(inOuts.InputsMaterial)
o.InputsMaterial = string(dataInputsMaterial)
dataOutputsMaterial, _ := json.Marshal(inOuts.OutputsMaterial)
o.OutputsMaterial = string(dataOutputsMaterial)
if o.InputsTemplate == emptyJsonStr {
o.InputsTemplate = ""
}
if o.OutputsTemplate == emptyJsonStr {
o.OutputsTemplate = ""
}
if o.InputsMaterial == emptyJsonStr {
o.InputsMaterial = ""
}
if o.OutputsMaterial == emptyJsonStr {
o.OutputsMaterial = ""
}
return nil
}

View File

@ -0,0 +1,101 @@
package version
import (
"context"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/pkg/version"
)
type VersionOptions struct {
formatter formatter.Formatter
versionClient factory.VersionClient
option *clioptions.GlobalOptions
}
// NewVersionOptions returns a reference to a CreateOptions
func NewVersionOptions(opt *clioptions.GlobalOptions) *VersionOptions {
return &VersionOptions{
option: opt,
}
}
func NewCmdVersion(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewVersionOptions(opt)
cmd := &cobra.Command{
Use: "version",
Short: "version command",
Long: `version command`,
Run: func(cmd *cobra.Command, args []string) {
utils.CheckErr(o.Complete())
utils.CheckErr(o.Validate())
utils.CheckErr(o.Run(args))
},
}
return cmd
}
func (o *VersionOptions) Complete() error {
var err error
f := factory.NewFactory(&o.option.Client)
o.versionClient, err = f.VersionClient()
if err != nil {
return err
}
o.formatter = formatter.NewFormatter(o.option.Stream.OutputFormat, o.option.Stream.Output)
return nil
}
func (o *VersionOptions) Validate() error {
//return o.option.Validate()
return nil
}
func (o *VersionOptions) Run(args []string) error {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Duration(o.option.Client.Timeout)*time.Second)
defer cancelFunc()
switch o.formatter.(type) {
case *formatter.Json, *formatter.Yaml:
m := map[string]interface{}{
"clientVersion": version.Get(),
}
info, err := o.versionClient.Version(ctx)
if err != nil {
m["serverVersion"] = err.Error()
} else {
m["serverVersion"] = info
}
o.formatter.Write(m)
case *formatter.Table:
// todo optimize here
o.formatter.Write(version.Get())
info, err := o.versionClient.Version(ctx)
if err != nil {
o.formatter.Write(err.Error())
} else {
o.formatter.Write(info)
}
case *formatter.Text:
o.formatter.Write("clientVersion")
o.formatter.Write(version.Get())
o.formatter.Write("serverVersion")
info, err := o.versionClient.Version(ctx)
if err != nil {
o.formatter.Write(err.Error())
} else {
o.formatter.Write(info)
}
}
return nil
}

View File

@ -0,0 +1,182 @@
package workflow
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/internal/context/workspace/domain/workflow"
)
// CreateOptions is an options to create a workflow.
type CreateOptions struct {
WorkspaceName string
Name string
Description string
Language string
Source string
URL string
Tag string
Token string
MainWorkflowPath string
workflowClient factory.WorkflowClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
func (o *CreateOptions) GetPromptArgs() ([]string, error) {
workflowName, err := prompt.PromptRequiredString("Name")
if err != nil {
return []string{}, err
}
return []string{workflowName}, nil
}
func (o *CreateOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = prompt.PromptRequiredString("Workspace", prompt.WithInputMessage("Name"))
if err != nil {
return err
}
o.Description, err = prompt.PromptOptionalString("Description")
if err != nil {
return err
}
o.Language, err = prompt.PromptStringSelect("Language", 10, []string{workflow.Language})
if err != nil {
return err
}
o.Source, err = prompt.PromptStringSelect("Source", 2, []string{workflow.WorkflowSourceGit, workflow.WorkflowSourceFile})
if err != nil {
return err
}
o.URL, err = prompt.PromptRequiredString("URL")
if err != nil {
return err
}
o.Tag, err = prompt.PromptRequiredString("Tag", prompt.WithInputMessage("tag or branch"))
if err != nil {
return err
}
o.Token, err = prompt.PromptOptionalString("Token")
if err != nil {
return err
}
o.MainWorkflowPath, err = prompt.PromptRequiredString("MainWorkflowPath", prompt.WithInputMessage("relative path"))
if err != nil {
return err
}
return nil
}
func (o *CreateOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
// NewCreateOptions returns a reference to a CreateOptions
func NewCreateOptions(opt *clioptions.GlobalOptions) *CreateOptions {
return &CreateOptions{
options: opt,
}
}
func NewCmdCreate(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewCreateOptions(opt)
cmd := &cobra.Command{
Use: "create",
Short: "create a workflow",
Long: "create a workflow",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The name of the workspace.")
cmd.Flags().StringVarP(&o.Description, "description", "d", o.Description, "The description of the workflow.")
cmd.Flags().StringVarP(&o.Language, "language", "l", o.Language, "The language of the workflow.")
cmd.Flags().StringVar(&o.Source, "source", o.Source, "The source of the workflow.")
cmd.Flags().StringVar(&o.URL, "url", o.URL, "The url of the workflow.")
cmd.Flags().StringVarP(&o.Tag, "tag", o.Tag, "t", "The tag or branch of the workflow.")
cmd.Flags().StringVar(&o.Token, "token", o.Token, "The token to clone repo of the workflow.")
cmd.Flags().StringVarP(&o.MainWorkflowPath, "main-workflow-path", "p", o.MainWorkflowPath, "The main path of the workflow.")
return cmd
}
// Complete completes all the required options.
func (o *CreateOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the create options
func (o *CreateOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.Language != workflow.Language {
return fmt.Errorf("unspport language: %s", o.Language)
}
if o.Source != workflow.WorkflowSourceGit && o.Source != workflow.WorkflowSourceFile {
return fmt.Errorf("unspport source: %s", o.Source)
}
return nil
}
// Run run the create workflow command
func (o *CreateOptions) Run(args []string) error {
workflowName := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
req := &convert.CreateWorkflowRequest{
WorkspaceID: workspaceID,
Name: workflowName,
Description: o.Description,
Language: o.Language,
Source: o.Source,
MainWorkflowPath: o.MainWorkflowPath,
Url: o.URL,
Tag: o.Tag,
Token: o.Token,
}
var resp *convert.CreateWorkflowResponse
resp, err = o.workflowClient.CreateWorkflow(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.ID)
return nil
}

View File

@ -0,0 +1,145 @@
package workflow
import (
"context"
"time"
"github.com/spf13/cobra"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// DeleteOptions is an options to delete a workflow.
type DeleteOptions struct {
WorkspaceName string
workflowClient factory.WorkflowClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
func (o *DeleteOptions) GetPromptArgs() ([]string, error) {
workflowName, err := prompt.PromptRequiredString("Workflow", prompt.WithInputMessage("Name"))
if err != nil {
return []string{}, err
}
return []string{workflowName}, nil
}
func (o *DeleteOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = prompt.PromptRequiredString("Workspace", prompt.WithInputMessage("Name"))
return err
}
func (o *DeleteOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
// NewDeleteOptions returns a reference to a DeleteOptions.
func NewDeleteOptions(opt *clioptions.GlobalOptions) *DeleteOptions {
return &DeleteOptions{
options: opt,
}
}
// NewCmdDelete new a delete workflow cmd.
func NewCmdDelete(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewDeleteOptions(opt)
cmd := &cobra.Command{
Use: "delete",
Short: "delete a workflow",
Long: "delete a workflow",
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The name of the workspace.")
return cmd
}
// Complete completes all the required options.
func (o *DeleteOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the delete options
func (o *DeleteOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
return nil
}
// Run run the create workflow command
func (o *DeleteOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
if len(args) != 0 {
workflowName := args[0]
var workflowID string
workflowID, err = ConvertWorkflowNameIntoID(ctx, o.workflowClient, workspaceID, workflowName)
if err != nil {
return err
}
_, err = o.workflowClient.DeleteWorkflow(ctx, &convert.DeleteWorkflowRequest{
ID: workflowID,
WorkspaceID: workspaceID,
})
} else {
// delete all workflow in special workspace
pageNum := 1
pageSize := 100
for {
var workspaces *convert.ListWorkflowsResponse
workspaces, err = o.workflowClient.ListWorkflow(ctx, &convert.ListWorkflowsRequest{
Page: pageNum,
Size: pageSize,
WorkspaceID: workspaceID,
})
if err != nil {
return err
}
for _, item := range workspaces.Items {
_, err = o.workflowClient.DeleteWorkflow(ctx, &convert.DeleteWorkflowRequest{
ID: item.ID,
WorkspaceID: workspaceID,
})
}
if len(workspaces.Items) < pageSize {
return nil
}
pageNum++
}
}
return err
}

View File

@ -0,0 +1,172 @@
package workflow
import (
"encoding/json"
"fmt"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// ImportOptions is an options to get a workflow.
type ImportOptions struct {
WorkspaceName string
Filepath string
create *CreateOptions
workflowClient factory.WorkflowClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
func (o *ImportOptions) GetPromptArgs() ([]string, error) {
return []string{}, nil
}
func (o *ImportOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = prompt.PromptRequiredString("Workspace", prompt.WithInputMessage("Name"))
if err != nil {
return err
}
o.Filepath, err = prompt.PromptStringWithValidator("Input File Path", func(ans interface{}) error {
err := survey.Required(ans)
if err != nil {
return err
}
_, err = os.Stat(cast.ToString(ans))
if err != nil {
curPath, _ := os.Getwd()
return fmt.Errorf("%w, (currenct path is [%s])", err, curPath)
}
return nil
}, prompt.WithInputMessage("abs path"))
if err != nil {
return err
}
return nil
}
func (o *ImportOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
// NewImportOptions returns a reference to a ImportOptions.
func NewImportOptions(opt *clioptions.GlobalOptions) *ImportOptions {
return &ImportOptions{
options: opt,
}
}
// NewCmdImport new a get workflow cmd.
func NewCmdImport(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewImportOptions(opt)
cmd := &cobra.Command{
Use: "import",
Short: "import a workflow",
Long: "import a workflow",
Example: `# import a workflow
# import a workflow with json format
{"name":"test","description":"test","language":"WDL","source":"git","URL":"https://github.com/xueerli/wdl.git","tag":"main","token":"","mainWorkflowPath":"no.wdl"}
`,
Args: cobra.ExactArgs(0),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.Filepath, "file", "f", o.WorkspaceName, "The file of the git info.")
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The name of workspace")
return cmd
}
// Complete completes all the required options.
func (o *ImportOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the get options
func (o *ImportOptions) Validate() error {
stat, err := os.Stat(o.Filepath)
if err != nil {
return err
}
file, err := os.ReadFile(stat.Name())
if err != nil {
return err
}
template := ImportTemplate{}
err = json.Unmarshal(file, &template)
if err != nil {
return err
}
o.create = &CreateOptions{
WorkspaceName: o.WorkspaceName,
Name: template.Name,
Description: template.Description,
Language: template.Language,
Source: template.Source,
URL: template.URL,
Tag: template.Tag,
Token: template.Token,
MainWorkflowPath: template.MainWorkflowPath,
options: o.options,
}
if err = o.Complete(); err != nil {
return err
}
if err = o.options.Validate(); err != nil {
return err
}
if err = o.create.Complete(); err != nil {
return err
}
return o.create.Validate()
}
type ImportTemplate struct {
Name string `json:"name"`
Description string `json:"description"`
Language string `json:"language"`
Source string `json:"source"`
URL string `json:"URL"`
Tag string `json:"tag"`
Token string `json:"token"`
MainWorkflowPath string `json:"mainWorkflowPath"`
}
// Run run the create workflow command
func (o *ImportOptions) Run(args []string) error {
return o.create.Run([]string{o.create.Name})
}

View File

@ -0,0 +1,213 @@
package workflow
import (
"context"
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/internal/context/workspace/application/query/workflow"
"github.com/Bio-OS/bioos/pkg/consts"
)
// ListOptions is an options to list workflow.
type ListOptions struct {
WorkspaceName string
Page int32
Size int32
OrderBy string
SearchWords []string
Ids []string
workflowClient factory.WorkflowClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
func (o *ListOptions) GetPromptArgs() ([]string, error) {
return []string{}, nil
}
func (o *ListOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = prompt.PromptRequiredString("Workspace", prompt.WithInputMessage("Name"))
if err != nil {
return err
}
o.SearchWords, err = prompt.PromptStringSlice("SearchWords")
if err != nil {
return err
}
orderByFields, err := prompt.PromptStringMultiSelect("OrderBy", 2, []string{workflow.OrderByName, workflow.OrderByCreateTime})
if err != nil {
return err
}
if len(orderByFields) > 0 {
for i, field := range orderByFields {
ascending, err := prompt.PromptStringSelect(fmt.Sprintf("%s Ascending", field), 2, []string{consts.ASCOrdering, consts.DESCOrdering})
if err != nil {
return err
}
orderByFields[i] += consts.OrderDelimiter + ascending
}
o.OrderBy = strings.Join(orderByFields, ",")
}
o.Page, err = prompt.PromptRequiredInt32("Page")
if err != nil {
return err
}
o.Size, err = prompt.PromptRequiredInt32("Size")
if err != nil {
return err
}
o.Ids, err = prompt.PromptStringSlice("IDs")
if err != nil {
return err
}
// correct formatter
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
}
return nil
}
func (o *ListOptions) GetDefaultFormat() formatter.Format {
return formatter.TableFormat
}
// NewListOptions returns a reference to a ListOptions.
func NewListOptions(opt *clioptions.GlobalOptions) *ListOptions {
return &ListOptions{
options: opt,
}
}
// NewCmdList new a list workflow cmd.
func NewCmdList(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewListOptions(opt)
cmd := &cobra.Command{
Use: "list",
Short: "list workflow",
Long: "list workflow",
Args: cobra.ExactArgs(0),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The name of the workspace.")
cmd.Flags().Int32Var(&o.Page, "page", 1, "The page number")
cmd.Flags().Int32Var(&o.Size, "size", 10, "The page size")
cmd.Flags().StringVar(&o.OrderBy, "order-by", o.OrderBy, "The order-by field: Name:desc")
cmd.Flags().StringSliceVar(&o.SearchWords, "search-words", o.SearchWords, "The search word")
cmd.Flags().StringSliceVar(&o.Ids, "ids", o.Ids, "The ids of the workspace.")
return cmd
}
// Complete completes all the required options.
func (o *ListOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
} else {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the list options
func (o *ListOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.WorkspaceName == "" {
return fmt.Errorf("workspace not provide")
}
return nil
}
// Run run the create workflow command
func (o *ListOptions) Run(_ []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
req := &convert.ListWorkflowsRequest{
Page: int(o.Page),
Size: int(o.Size),
WorkspaceID: workspaceID,
}
if o.OrderBy != "" {
req.OrderBy = o.OrderBy
}
if len(o.SearchWords) > 0 {
req.SearchWord = strings.Join(o.SearchWords, consts.QuerySliceDelimiter)
}
if len(o.Ids) > 0 {
req.IDs = strings.Join(o.Ids, consts.QuerySliceDelimiter)
}
resp, err := o.workflowClient.ListWorkflow(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp)
return nil
}
// ConvertWorkflowNameIntoID convert workflow name into id
func ConvertWorkflowNameIntoID(ctx context.Context, workflowClient factory.WorkflowClient, wsID string, name string) (id string, err error) {
pageNum := 1
pageSize := 100
for {
var listWorkflowsResponse *convert.ListWorkflowsResponse
listWorkflowsResponse, err = workflowClient.ListWorkflow(ctx, &convert.ListWorkflowsRequest{
Page: pageNum,
Size: pageSize,
SearchWord: name,
WorkspaceID: wsID,
})
if err != nil {
return
}
for _, item := range listWorkflowsResponse.Items {
if item.Name == name {
id = item.ID
return
}
}
if len(listWorkflowsResponse.Items) < pageSize {
err = fmt.Errorf("workflow: %s not found", name)
return
}
pageNum++
}
}

View File

@ -0,0 +1,146 @@
package workflow
import (
"context"
"time"
"github.com/spf13/cobra"
cliworkspace "github.com/Bio-OS/bioos/internal/bioctl/cmd/workspace"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// UpdateOptions is an options to update workflow.
type UpdateOptions struct {
WorkspaceName string
Name string
Description string
workflowClient factory.WorkflowClient
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
func (o *UpdateOptions) GetPromptArgs() ([]string, error) {
workflowName, err := prompt.PromptRequiredString("Workflow", prompt.WithInputMessage("Name"))
if err != nil {
return nil, err
}
return []string{workflowName}, nil
}
func (o *UpdateOptions) GetPromptOptions() error {
var err error
o.WorkspaceName, err = prompt.PromptRequiredString("Workspace", prompt.WithInputMessage("Name"))
if err != nil {
return err
}
o.Name, err = prompt.PromptRequiredString("Name")
if err != nil {
return err
}
o.Description, err = prompt.PromptOptionalString("Description")
if err != nil {
return err
}
return nil
}
func (o *UpdateOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
// NewUpdateOptions returns a reference to a UpdateOptions.
func NewUpdateOptions(opt *clioptions.GlobalOptions) *UpdateOptions {
return &UpdateOptions{
options: opt,
}
}
// NewCmdUpdate new a update workflow cmd.
func NewCmdUpdate(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewUpdateOptions(opt)
cmd := &cobra.Command{
Use: "update <workflow_name>",
Short: "update a workflow",
Long: "update a workflow",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.WorkspaceName, "workspace", "w", o.WorkspaceName, "The name of the workspace.")
cmd.Flags().StringVarP(&o.Name, "name", "n", o.Name, "The name of the workflow.")
cmd.Flags().StringVarP(&o.Description, "description", "d", o.Description, "The description of the workflow.")
return cmd
}
// Complete completes all the required options.
func (o *UpdateOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workflowClient, err = f.WorkflowClient()
if err != nil {
return err
}
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the list options
func (o *UpdateOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
return nil
}
// Run run the update workflow command
func (o *UpdateOptions) Run(args []string) error {
workflowName := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := cliworkspace.ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, o.WorkspaceName)
if err != nil {
return err
}
workflowID, err := ConvertWorkflowNameIntoID(ctx, o.workflowClient, workspaceID, workflowName)
if err != nil {
return err
}
req := &convert.UpdateWorkflowRequest{
ID: workflowID,
WorkspaceID: workspaceID,
}
if o.Name != "" {
req.Name = o.Name
}
if o.Description != "" {
req.Description = o.Description
}
// todo update api needs to add more fields
_, err = o.workflowClient.UpdateWorkflow(ctx, req)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,24 @@
package workflow
import (
"github.com/spf13/cobra"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
func NewCmdWorkflow(opt *clioptions.GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "workflow",
Short: "workflow command",
Long: `workflow command`,
Args: cobra.NoArgs,
Run: prompt.SelectSubCommand,
}
cmd.AddCommand(NewCmdCreate(opt))
cmd.AddCommand(NewCmdImport(opt))
cmd.AddCommand(NewCmdList(opt))
cmd.AddCommand(NewCmdUpdate(opt))
cmd.AddCommand(NewCmdDelete(opt))
return cmd
}

View File

@ -0,0 +1,137 @@
package workspace
import (
"context"
"fmt"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// CreateOptions is an options to create a workspace.
type CreateOptions struct {
Description string
MountType string
MountPath string
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewCreateOptions returns a reference to a CreateOptions
func NewCreateOptions(opt *clioptions.GlobalOptions) *CreateOptions {
return &CreateOptions{
options: opt,
}
}
func NewCmdCreate(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewCreateOptions(opt)
cmd := &cobra.Command{
Use: "create <workspace_name>",
Short: "create a workspace",
Long: "create a workspace",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.Description, "description", "d", o.Description, "The description of the workspace.")
cmd.Flags().StringVarP(&o.MountType, "mount-type", "t", o.MountType, "The mount type of the workspace Storage.")
cmd.Flags().StringVarP(&o.MountPath, "mount-path", "p", o.MountPath, "The mount path of the workspace Storage.")
return cmd
}
// Complete completes all the required options.
func (o *CreateOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the create options
func (o *CreateOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.MountType != "nfs" {
return fmt.Errorf("workspace storage [%s] not support", o.MountType)
}
return nil
}
// Run run the create workspace command
func (o *CreateOptions) Run(args []string) error {
workspaceName := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
req := &convert.CreateWorkspaceRequest{
Name: workspaceName,
Description: o.Description,
}
if o.MountPath != "" {
req.Storage = &convert.WorkspaceStorage{
NFS: &convert.NFSWorkspaceStorage{
MountPath: o.MountPath,
},
}
}
resp, err := o.workspaceClient.CreateWorkspace(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.Id)
return nil
}
func (o *CreateOptions) GetPromptArgs() ([]string, error) {
workspaceName, err := prompt.PromptRequiredString("Name")
if err != nil {
return []string{}, err
}
return []string{workspaceName}, nil
}
func (o *CreateOptions) GetPromptOptions() error {
var err error
o.Description, err = prompt.PromptRequiredString("Description")
if err != nil {
return err
}
o.MountPath, err = prompt.PromptRequiredString("MountPath", prompt.WithInputMessage("abs path"))
if err != nil {
return err
}
o.MountType, err = prompt.PromptStringSelect("MountType", 1, []string{"nfs"})
if err != nil {
return err
}
return nil
}
func (o *CreateOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,105 @@
package workspace
import (
"context"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
)
// DeleteOptions is an options to delete a workspace.
type DeleteOptions struct {
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewDeleteOptions returns a reference to a DeleteOptions.
func NewDeleteOptions(opt *clioptions.GlobalOptions) *DeleteOptions {
return &DeleteOptions{
options: opt,
}
}
// NewCmdDelete new a delete workspace cmd.
func NewCmdDelete(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewDeleteOptions(opt)
cmd := &cobra.Command{
Use: "delete <workspace_name>",
Short: "delete a workspace",
Long: "delete a workspace",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
return cmd
}
// Complete completes all the required options.
func (o *DeleteOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the delete options
func (o *DeleteOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
return nil
}
// Run run the create workspace command
func (o *DeleteOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, args[0])
if err != nil {
return err
}
_, err = o.workspaceClient.DeleteWorkspace(ctx, &convert.DeleteWorkspaceRequest{
Id: workspaceID,
})
if err != nil {
return err
}
return nil
}
func (o *DeleteOptions) GetPromptArgs() ([]string, error) {
name, err := cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return nil, err
}
return []string{name}, nil
}
func (o *DeleteOptions) GetPromptOptions() error {
return nil
}
func (o *DeleteOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,237 @@
package workspace
import (
"context"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/util/rand"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/pkg/consts"
applog "github.com/Bio-OS/bioos/pkg/log"
"github.com/Bio-OS/bioos/pkg/schema"
"github.com/Bio-OS/bioos/pkg/utils"
)
type ImportOptions struct {
YamlPath string
MountType string
MountPath string
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewImportOptions returns a reference to a ImportOptions
func NewImportOptions(opt *clioptions.GlobalOptions) *ImportOptions {
return &ImportOptions{
options: opt,
}
}
func NewCmdImport(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewImportOptions(opt)
cmd := &cobra.Command{
Use: "import",
Short: "import a workspace",
Long: "import a workspace",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.YamlPath, "yaml", "y", o.YamlPath, "The path of the workspace yaml file")
cmd.Flags().StringVarP(&o.MountType, "mount-type", "t", o.MountType, "The mount type of the workspace Storage.")
cmd.Flags().StringVarP(&o.MountPath, "mount-path", "p", o.MountPath, "The mount path of the workspace Storage.")
return cmd
}
// Complete completes all the required options.
func (o *ImportOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the import options
func (o *ImportOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
if o.YamlPath == "" || path.Ext(o.YamlPath) != ".yaml" {
return fmt.Errorf("you must choose a csv file to import")
}
_, err := os.Stat(o.YamlPath)
if err != nil {
return err
}
if o.MountType != "nfs" {
return fmt.Errorf("workspace storage [%s] not support", o.MountType)
}
return nil
}
// Run run the import workspace command
func (o *ImportOptions) Run(args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
bytes, err := os.ReadFile(o.YamlPath)
if err != nil {
return err
}
schema := &schema.WorkspaceTypedSchema{}
err = yaml.Unmarshal(bytes, &schema)
if err != nil {
return err
}
zipFilePath, err := o.zipFiles(schema)
if err != nil {
return err
}
defer os.Remove(zipFilePath)
req := &convert.ImportWorkspaceRequest{
FilePath: zipFilePath,
MountType: o.MountType,
}
if o.MountPath != "" {
req.MountPath = o.MountPath
}
resp, err := o.workspaceClient.ImportWorkspace(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp.Id)
return nil
}
func (o *ImportOptions) GetPromptArgs() ([]string, error) {
return nil, nil
}
func (o *ImportOptions) GetPromptOptions() error {
var err error
o.YamlPath, err = prompt.PromptStringWithValidator("Workspace YamlPath File Path", func(ans interface{}) error {
err = survey.Required(ans)
if err != nil {
return err
}
_, err = os.Stat(cast.ToString(ans))
if err != nil {
curPath, _ := os.Getwd()
return fmt.Errorf("%w, (currenct path is [%s])", err, curPath)
}
return nil
}, prompt.WithInputMessage("only support YamlPath"))
if err != nil {
return err
}
o.MountPath, err = prompt.PromptRequiredString("MountPath", prompt.WithInputMessage("abs path"))
if err != nil {
return err
}
o.MountType, err = prompt.PromptStringSelect("MountType", 1, []string{"nfs"})
if err != nil {
return err
}
return nil
}
func (o *ImportOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}
func (o *ImportOptions) zipFiles(schema *schema.WorkspaceTypedSchema) (string, error) {
yamlBase := path.Base(o.YamlPath)
yamlFilePrefix := strings.TrimSuffix(yamlBase, path.Ext(o.YamlPath))
dir := fmt.Sprintf("import-%s-%s-%s", yamlFilePrefix, o.MountType, rand.String(10))
os.RemoveAll(dir)
err := os.Mkdir(dir, os.ModePerm)
if err != nil {
applog.Errorf("fail to make tmp dir: %s", err)
return "", err
}
defer os.RemoveAll(dir)
baseDir := filepath.Dir(o.YamlPath)
// copy all files to temp dir
filesMap := map[string]string{o.YamlPath: path.Join(dir, consts.WorkspaceYAMLName)}
for _, dataModel := range schema.DataModels {
filesMap[path.Join(baseDir, dataModel.Path)] = path.Join(dir, dataModel.Path)
}
for _, workflow := range schema.Workflows {
err = filepath.Walk(path.Join(baseDir, workflow.Path), func(p string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
filesMap[p] = path.Join(dir, workflow.Path, path.Base(p))
return nil
})
if err != nil {
return "", err
}
}
for _, artifact := range schema.Notebooks.Artifacts {
filesMap[path.Join(baseDir, artifact.Path)] = path.Join(dir, artifact.Path)
}
for fToRead, fToWrite := range filesMap {
input, err := os.ReadFile(filepath.Clean(fToRead))
if err != nil {
return "", err
}
if _, err = os.Stat(fToWrite); os.IsNotExist(err) {
err = os.MkdirAll(filepath.Dir(fToWrite), os.ModePerm)
if err != nil {
return "", err
}
}
err = os.WriteFile(fToWrite, input, 0660)
if err != nil {
return "", err
}
}
zipFilePath := strings.ReplaceAll(yamlBase, ".yaml", ".zip")
err = utils.ZipDir(dir, zipFilePath)
if err != nil {
return "", err
}
return zipFilePath, nil
}

View File

@ -0,0 +1,196 @@
package workspace
import (
"context"
"fmt"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/internal/context/workspace/application/query/workspace"
"github.com/Bio-OS/bioos/pkg/consts"
)
// ListOptions is an options to list workspaces.
type ListOptions struct {
Page int32
Size int32
OrderBy string
SearchWords []string
Ids []string
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewListOptions returns a reference to a ListOptions.
func NewListOptions(opt *clioptions.GlobalOptions) *ListOptions {
return &ListOptions{
options: opt,
}
}
// NewCmdList new a list workspace cmd.
func NewCmdList(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewListOptions(opt)
cmd := &cobra.Command{
Use: "list",
Short: "list workspaces",
Long: "list workspaces",
Args: cobra.NoArgs,
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().Int32VarP(&o.Page, "page", "p", 1, "The page number")
cmd.Flags().Int32VarP(&o.Size, "size", "s", 10, "The page size")
cmd.Flags().StringVar(&o.OrderBy, "order-by", o.OrderBy, "The order-by field")
cmd.Flags().StringSliceVar(&o.SearchWords, "search-word", o.SearchWords, "The search word")
cmd.Flags().StringSliceVar(&o.Ids, "ids", o.Ids, "The ids of the workspace.")
return cmd
}
// Complete completes all the required options.
func (o *ListOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
} else {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the list options
func (o *ListOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
return nil
}
// Run run the create workspace command
func (o *ListOptions) Run(_ []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
req := &convert.ListWorkspacesRequest{
Page: int(o.Page),
Size: int(o.Size),
}
if len(o.Ids) > 0 {
req.IDs = strings.Join(o.Ids, consts.QuerySliceDelimiter)
}
if o.OrderBy != "" {
req.OrderBy = o.OrderBy
}
if len(o.SearchWords) > 0 {
req.SearchWord = strings.Join(o.SearchWords, consts.QuerySliceDelimiter)
}
resp, err := o.workspaceClient.ListWorkspaces(ctx, req)
if err != nil {
return err
}
o.formatter.Write(resp)
return nil
}
func (o *ListOptions) GetPromptArgs() ([]string, error) {
return []string{}, nil
}
func (o *ListOptions) GetPromptOptions() error {
var err error
o.Page, err = prompt.PromptRequiredInt32("Page")
if err != nil {
return err
}
o.Size, err = prompt.PromptRequiredInt32("Size")
if err != nil {
return err
}
orderByFields, err := prompt.PromptStringMultiSelect("OrderBy", 2, []string{workspace.OrderByName, workspace.OrderByCreateTime})
if err != nil {
return err
}
if len(orderByFields) > 0 {
for i, field := range orderByFields {
ascending, err := prompt.PromptStringSelect(fmt.Sprintf("%s Ascending", field), 2, []string{consts.ASCOrdering, consts.DESCOrdering})
if err != nil {
return err
}
orderByFields[i] += consts.OrderDelimiter + ascending
}
o.OrderBy = strings.Join(orderByFields, ",")
}
o.SearchWords, err = prompt.PromptStringSlice("SearchWords")
if err != nil {
return err
}
o.Ids, err = prompt.PromptStringSlice("IDs")
if err != nil {
return err
}
// correct formatter
if len(o.Ids) > 0 {
o.options.Stream.OutputFormat = formatter.JsonFormat
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
}
return err
}
func (o *ListOptions) GetDefaultFormat() formatter.Format {
return formatter.TableFormat
}
// ConvertWorkspaceNameIntoID convert workspace name into id
func ConvertWorkspaceNameIntoID(ctx context.Context, workspaceClient factory.WorkspaceClient, name string) (id string, err error) {
pageNum := 1
pageSize := 100
for {
var workspaces *convert.ListWorkspacesResponse
workspaces, err = workspaceClient.ListWorkspaces(ctx, &convert.ListWorkspacesRequest{
Page: pageNum,
Size: pageSize,
SearchWord: name,
})
if err != nil {
return
}
for _, item := range workspaces.Items {
if item.Name == name {
id = item.Id
return
}
}
if len(workspaces.Items) < pageSize {
err = fmt.Errorf("workspace: %s not found", name)
return
}
pageNum++
}
}

View File

@ -0,0 +1,133 @@
package workspace
import (
"context"
"time"
"github.com/spf13/cobra"
"github.com/Bio-OS/bioos/internal/bioctl/cmd"
"github.com/Bio-OS/bioos/internal/bioctl/factory"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
// UpdateOptions is an options to update workspaces.
type UpdateOptions struct {
Name string
Description string
workspaceClient factory.WorkspaceClient
formatter formatter.Formatter
options *clioptions.GlobalOptions
}
// NewUpdateOptions returns a reference to a UpdateOptions.
func NewUpdateOptions(opt *clioptions.GlobalOptions) *UpdateOptions {
return &UpdateOptions{
options: opt,
}
}
// NewCmdUpdate new a update workspace cmd.
func NewCmdUpdate(opt *clioptions.GlobalOptions) *cobra.Command {
o := NewUpdateOptions(opt)
cmd := &cobra.Command{
Use: "update <workspace_name>",
Short: "update a workspace",
Long: "update a workspace",
Args: cobra.ExactArgs(1),
Run: clioptions.GetCommonRunFunc(o),
}
cmd.Flags().StringVarP(&o.Name, "name", "n", o.Name, "The name of the workspace.")
cmd.Flags().StringVarP(&o.Description, "description", "d", o.Description, "The description of the workspace.")
return cmd
}
// Complete completes all the required options.
func (o *UpdateOptions) Complete() error {
var err error
f := factory.NewFactory(&o.options.Client)
o.workspaceClient, err = f.WorkspaceClient()
if err != nil {
return err
}
if o.options.Stream.OutputFormat == "" {
o.options.Stream.OutputFormat = o.GetDefaultFormat()
}
o.formatter = formatter.NewFormatter(o.options.Stream.OutputFormat, o.options.Stream.Output)
return nil
}
// Validate validate the list options
func (o *UpdateOptions) Validate() error {
if err := o.options.Validate(); err != nil {
return err
}
return nil
}
// Run run the update workspace command
func (o *UpdateOptions) Run(args []string) error {
workspaceName := args[0]
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(o.options.Client.Timeout))
defer cancel()
workspaceID, err := ConvertWorkspaceNameIntoID(ctx, o.workspaceClient, workspaceName)
if err != nil {
return err
}
req := &convert.UpdateWorkspaceRequest{
ID: workspaceID,
}
if o.Name != "" {
req.Name = &o.Name
}
if o.Description != "" {
req.Description = &o.Description
}
_, err = o.workspaceClient.UpdateWorkspace(ctx, req)
if err != nil {
return err
}
return nil
}
func (o *UpdateOptions) GetPromptArgs() ([]string, error) {
name, err := cmd.GetWorkspaceName(o.options.Client.Timeout, o.workspaceClient)
if err != nil {
return nil, err
}
return []string{name}, nil
}
func (o *UpdateOptions) GetPromptOptions() error {
var err error
o.Name, err = prompt.PromptOptionalString("Name")
if err != nil {
return err
}
o.Description, err = prompt.PromptOptionalString("Description")
if err != nil {
return err
}
return nil
}
func (o *UpdateOptions) GetDefaultFormat() formatter.Format {
return formatter.TextFormat
}

View File

@ -0,0 +1,24 @@
package workspace
import (
"github.com/spf13/cobra"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
)
func NewCmdWorkspace(opt *clioptions.GlobalOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "workspace",
Short: "workspace command",
Long: `workspace command`,
Args: cobra.NoArgs,
Run: prompt.SelectSubCommand,
}
cmd.AddCommand(NewCmdCreate(opt))
cmd.AddCommand(NewCmdList(opt))
cmd.AddCommand(NewCmdDelete(opt))
cmd.AddCommand(NewCmdUpdate(opt))
cmd.AddCommand(NewCmdImport(opt))
return cmd
}

1
internal/bioctl/doc.go Normal file
View File

@ -0,0 +1 @@
package bioctl

View File

@ -0,0 +1,207 @@
package convert
import (
"reflect"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
)
type GetDataModelRequest struct {
WorkspaceID string `path:"workspace_id"`
ID string `path:"id"`
}
func (req *GetDataModelRequest) ToGRPC() *workspaceproto.GetDataModelRequest {
return &workspaceproto.GetDataModelRequest{
Id: req.ID,
WorkspaceID: req.WorkspaceID,
}
}
type GetDataModelResponse struct {
DataModel *DataModel `json:"dataModel,omitempty"`
Headers []string `json:"headers"`
}
func (resp *GetDataModelResponse) FromGRPC(protoResp *workspaceproto.GetDataModelResponse) {
resp.DataModel = &DataModel{
ID: protoResp.GetDataModel().GetId(),
Name: protoResp.GetDataModel().GetName(),
RowCount: protoResp.GetDataModel().GetRowCount(),
Type: protoResp.GetDataModel().GetType(),
}
resp.Headers = protoResp.GetHeaders()
}
type ListDataModelsRequest struct {
WorkspaceID string `path:"workspace_id"`
Types []string `query:"types,omitempty"`
SearchWord string `query:"searchWord"`
IDs []string `query:"ids,omitempty"`
}
func (req *ListDataModelsRequest) ToGRPC() *workspaceproto.ListDataModelsRequest {
return &workspaceproto.ListDataModelsRequest{
WorkspaceID: req.WorkspaceID,
Types: req.Types,
SearchWord: req.SearchWord,
Ids: req.IDs,
}
}
type ListDataModelsResponse struct {
Items []DataModel `json:"Items"`
}
func (resp *ListDataModelsResponse) BriefItems() interface{} {
briefItems := make([]DataModel, len(resp.Items))
for i, item := range resp.Items {
briefItems[i] = item
}
return reflect.ValueOf(briefItems)
}
func (resp *ListDataModelsResponse) FromGRPC(protoResp *workspaceproto.ListDataModelsResponse) {
resp.Items = make([]DataModel, len(protoResp.GetItems()))
for i, item := range protoResp.Items {
resp.Items[i] = DataModel{
ID: item.GetId(),
Name: item.GetName(),
RowCount: item.GetRowCount(),
Type: item.GetType(),
}
}
}
type DataModel struct {
ID string `json:"id"`
Name string `json:"name"`
RowCount int64 `json:"rowCount"`
Type string `json:"type"`
}
type ListDataModelRowsRequest struct {
WorkspaceID string `path:"workspace_id"`
ID string `path:"id"`
Page int32 `query:"page"`
Size int32 `query:"size"`
OrderBy string `query:"orderBy"`
SearchWord string `query:"searchWord"`
InSetIDs []string `query:"inSetIDs"`
RowIDs []string `query:"rowIDs"`
}
func (req *ListDataModelRowsRequest) ToGRPC() *workspaceproto.ListDataModelRowsRequest {
return &workspaceproto.ListDataModelRowsRequest{
WorkspaceID: req.WorkspaceID,
Id: req.ID,
Page: req.Page,
Size: req.Size,
OrderBy: req.OrderBy,
SearchWord: req.SearchWord,
InSetIDs: req.InSetIDs,
RowIDs: req.RowIDs,
}
}
type ListDataModelRowsResponse struct {
Headers []string `json:"headers"`
Rows [][]string `json:"rows"`
Page int32 `json:"page"`
Size int32 `json:"size"`
Total int64 `json:"total"`
}
func (resp *ListDataModelRowsResponse) FromGRPC(protoResp *workspaceproto.ListDataModelRowsResponse) {
resp.Headers = protoResp.GetHeaders()
resp.Rows = make([][]string, len(protoResp.Rows))
for i, r := range protoResp.Rows {
resp.Rows[i] = r.Grids
}
resp.Page = protoResp.GetPage()
resp.Size = protoResp.GetSize()
resp.Total = protoResp.GetTotal()
}
type Row struct {
Grid []Grid `json:"grid"`
}
type Grid struct {
Value []byte `json:"value"`
Type string `json:"type"`
}
type PatchDataModelRequest struct {
WorkspaceID string `path:"workspace_id"`
Name string `json:"name"`
Async bool `json:"async"`
Headers []string `json:"headers"`
Rows [][]string `json:"rows"`
}
func (req *PatchDataModelRequest) ToGRPC() *workspaceproto.PatchDataModelRequest {
out := &workspaceproto.PatchDataModelRequest{
WorkspaceID: req.WorkspaceID,
Name: req.Name,
Async: req.Async,
Headers: req.Headers,
}
out.Rows = make([]*workspaceproto.Row, len(req.Rows))
for i, r := range req.Rows {
out.Rows[i] = &workspaceproto.Row{
Grids: r,
}
}
return out
}
type PatchDataModelResponse struct {
ID string `json:"id"`
}
func (resp *PatchDataModelResponse) FromGRPC(protoResp *workspaceproto.PatchDataModelResponse) {
resp.ID = protoResp.GetId()
}
type DeleteDataModelRequest struct {
ID string `path:"id"`
WorkspaceID string `path:"workspace_id"`
Headers []string `query:"headers,omitempty"`
RowIDs []string `query:"rowIDs,omitempty"`
}
func (req *DeleteDataModelRequest) ToGRPC() *workspaceproto.DeleteDataModelRequest {
return &workspaceproto.DeleteDataModelRequest{
Id: req.ID,
WorkspaceID: req.WorkspaceID,
Headers: req.Headers,
RowIDs: req.RowIDs,
}
}
type DeleteDataModelResponse struct{}
func (resp *DeleteDataModelResponse) FromGRPC(protoResp *workspaceproto.DeleteDataModelResponse) {
return
}
type ListAllDataModelRowIDsRequest struct {
WorkspaceID string `path:"workspace_id"`
ID string `path:"id"`
}
func (req *ListAllDataModelRowIDsRequest) ToGRPC() *workspaceproto.ListAllDataModelRowIDsRequest {
return &workspaceproto.ListAllDataModelRowIDsRequest{
WorkspaceID: req.WorkspaceID,
Id: req.ID,
}
}
type ListAllDataModelRowIDsResponse struct {
RowIDs []string `json:"rowIDs"`
}
func (resp *ListAllDataModelRowIDsResponse) FromGRPC(protoResp *workspaceproto.ListAllDataModelRowIDsResponse) {
resp.RowIDs = protoResp.GetRowIDs()
}

View File

@ -0,0 +1,161 @@
package convert
import submissionproto "github.com/Bio-OS/bioos/internal/context/submission/interface/grpc/proto"
type ListRunsRequest struct {
WorkspaceID string `path:"workspace_id"`
SubmissionID string `path:"submission_id"`
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
SearchWord string `query:"searchWord"`
Status []string `query:"status,omitempty"`
IDs []string `query:"ids,omitempty"`
}
func (req *ListRunsRequest) ToGRPC() *submissionproto.ListRunsRequest {
return &submissionproto.ListRunsRequest{
WorkspaceID: req.WorkspaceID,
SubmissionID: req.SubmissionID,
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
SearchWord: req.SearchWord,
Status: req.Status,
Ids: req.IDs,
}
}
type ListRunsResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Items []RunItem `json:"items"`
}
func (resp *ListRunsResponse) FromGRPC(protoResp *submissionproto.ListRunsResponse) {
resp.Page = int(protoResp.GetPage())
resp.Size = int(protoResp.GetSize())
resp.Total = int(protoResp.GetTotal())
resp.Items = make([]RunItem, len(protoResp.GetItems()))
for i, item := range protoResp.GetItems() {
resp.Items[i] = RunItem{
ID: item.GetId(),
Name: item.GetName(),
Status: item.GetStatus(),
StartTime: item.GetStartTime(),
FinishTime: &item.FinishTime,
Duration: item.GetDuration(),
EngineRunID: item.GetEngineRunID(),
Inputs: item.GetInputs(),
Outputs: item.GetOutputs(),
TaskStatus: Status{
Count: item.GetTaskStatus().GetCount(),
Pending: item.GetTaskStatus().GetPending(),
Succeeded: item.GetTaskStatus().GetSucceeded(),
Failed: item.GetTaskStatus().GetFailed(),
Running: item.GetTaskStatus().GetRunning(),
Cancelling: item.GetTaskStatus().GetCancelling(),
Cancelled: item.GetTaskStatus().GetCancelled(),
Queued: item.GetTaskStatus().GetQueued(),
Initializing: item.GetTaskStatus().GetInitializing(),
},
Log: &item.Log,
Message: &item.Message,
}
}
}
type RunItem struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
StartTime int64 `json:"startTime"`
FinishTime *int64 `json:"finishTime"`
Duration int64 `json:"duration"`
EngineRunID string `json:"engineRunID"`
Inputs string `json:"inputs"`
Outputs string `json:"outputs"`
TaskStatus Status `json:"taskStatus"`
Log *string `json:"log"`
Message *string `json:"message"`
}
type CancelRunRequest struct {
WorkspaceID string `path:"workspace_id"`
SubmissionID string `path:"submission_id"`
ID string `path:"id"`
}
func (req *CancelRunRequest) ToGRPC() *submissionproto.CancelRunRequest {
return &submissionproto.CancelRunRequest{
WorkspaceID: req.WorkspaceID,
SubmissionID: req.SubmissionID,
Id: req.ID,
}
}
type CancelRunResponse struct {
}
func (resp *CancelRunResponse) FromGRPC(protoResp *submissionproto.CancelRunResponse) {
return
}
type ListTasksRequest struct {
WorkspaceID string `path:"workspace_id"`
SubmissionID string `path:"submission_id"`
RunID string `path:"run_id"`
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
}
func (req *ListTasksRequest) ToGRPC() *submissionproto.ListTasksRequest {
return &submissionproto.ListTasksRequest{
WorkspaceID: req.WorkspaceID,
SubmissionID: req.SubmissionID,
RunID: req.RunID,
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
}
}
type ListTasksResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Items []TaskItem `json:"items"`
}
func (resp *ListTasksResponse) FromGRPC(protoResp *submissionproto.ListTasksResponse) {
resp.Page = int(protoResp.GetPage())
resp.Size = int(protoResp.GetSize())
resp.Total = int(protoResp.GetTotal())
resp.Items = make([]TaskItem, len(protoResp.GetItems()))
for i, item := range protoResp.GetItems() {
resp.Items[i] = TaskItem{
Name: item.GetName(),
RunID: item.GetRunID(),
Status: item.GetStatus(),
StartTime: item.GetStartTime(),
FinishTime: &item.FinishTime,
Duration: item.GetDuration(),
Stdout: item.GetStdout(),
Stderr: item.GetStderr(),
}
}
}
type TaskItem struct {
Name string `json:"name"`
RunID string `json:"runID"`
Status string `json:"status"`
StartTime int64 `json:"startTime"`
FinishTime *int64 `json:"finishTime"`
Duration int64 `json:"duration"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}

View File

@ -0,0 +1,263 @@
package convert
import (
"reflect"
"k8s.io/utils/pointer"
submissionproto "github.com/Bio-OS/bioos/internal/context/submission/interface/grpc/proto"
)
type CreateSubmissionRequest struct {
WorkspaceID string `path:"workspace_id"`
Name string `json:"name"`
WorkflowID string `json:"workflowID"`
Description *string `json:"description"`
Type string `json:"type"`
Entity *Entity `json:"entity"`
ExposedOptions ExposedOptions `json:"exposedOptions"`
InOutMaterial *InOutMaterial `json:"inOutMaterial"`
}
func (req *CreateSubmissionRequest) ToGRPC() *submissionproto.CreateSubmissionRequest {
return &submissionproto.CreateSubmissionRequest{
WorkspaceID: req.WorkspaceID,
Name: req.Name,
WorkflowID: req.WorkflowID,
Description: pointer.StringDeref(req.Description, ""),
Type: req.Type,
Entity: &submissionproto.Entity{
DataModelID: req.Entity.DataModelID,
DataModelRowIDs: req.Entity.DataModelRowIDs,
InputsTemplate: req.Entity.InputsTemplate,
OutputsTemplate: req.Entity.OutputsTemplate,
},
ExposedOptions: &submissionproto.ExposedOptions{
ReadFromCache: req.ExposedOptions.ReadFromCache,
},
InOutMaterial: &submissionproto.InOutMaterial{
InputsMaterial: req.InOutMaterial.InputsMaterial,
OutputsMaterial: req.InOutMaterial.OutputsMaterial,
},
}
}
type Entity struct {
DataModelID string `json:"dataModelID"`
DataModelRowIDs []string `json:"dataModelRowIDs"`
/** 输入配置json 序列化后的 string
采用 json 序列化原因基于以下两点考虑
- thrift/接口设计层面不允许 `Value` 类型不确定
- inputs/outputs 层级进行序列化可使得 `bioos-server` 不处理 `Inputs`/`Outputs`( `this.xxx` 索引的输入) 就入库/提交给计算引擎达到透传效果
*/
InputsTemplate string `json:"inputsTemplate"`
/** 输出配置json 序列化后的 string
采用 json 序列化原因基于以下两点考虑
- thrift/接口设计层面不允许 `Value` 类型不确定
- inputs/outputs 层级进行序列化可使得 `bioos-server` 不处理 `Inputs`/`Outputs`( `this.xxx` 索引的输入) 就入库/提交给计算引擎达到透传效果
*/
OutputsTemplate string `json:"outputsTemplate"`
}
type InOutMaterial struct {
InputsMaterial string `json:"inputsMaterial"`
OutputsMaterial string `json:"outputsMaterial"`
}
type ExposedOptions struct {
ReadFromCache bool `json:"readFromCache"`
}
type CreateSubmissionResponse struct {
ID string `json:"id"`
}
func (resp *CreateSubmissionResponse) FromGRPC(protoResp *submissionproto.CreateSubmissionResponse) {
resp.ID = protoResp.GetId()
}
type CheckSubmissionRequest struct {
WorkspaceID string `path:"workspace_id"`
Name string `json:"name"`
}
func (req *CheckSubmissionRequest) ToGRPC() *submissionproto.CheckSubmissionRequest {
return &submissionproto.CheckSubmissionRequest{}
}
type CheckSubmissionResponse struct {
IsNameExist bool `json:"isNameExist"`
}
func (resp *CheckSubmissionResponse) FromGRPC(protoResp *submissionproto.CheckSubmissionResponse) {
resp.IsNameExist = protoResp.GetIsNameExist()
}
type ListSubmissionsRequest struct {
WorkspaceID string `path:"workspace_id"`
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
SearchWord string `query:"searchWord"`
WorkflowID string `query:"workflowID,omitempty"`
Status []string `query:"status,omitempty"`
IDs []string `query:"ids,omitempty"`
}
func (req *ListSubmissionsRequest) ToGRPC() *submissionproto.ListSubmissionsRequest {
return &submissionproto.ListSubmissionsRequest{
WorkspaceID: req.WorkspaceID,
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
SearchWord: req.SearchWord,
WorkflowID: req.WorkflowID,
Status: req.Status,
Ids: req.IDs,
}
}
type ListSubmissionsResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Items []SubmissionItem `json:"items"`
}
type listSubmissionsResponseBriefItems struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
WorkflowID string `json:"workflowID"`
WorkflowVersionID string `json:"versionID"`
}
func (resp *ListSubmissionsResponse) BriefItems() reflect.Value {
briefItems := make([]listSubmissionsResponseBriefItems, len(resp.Items))
for i, item := range resp.Items {
briefItems[i] = listSubmissionsResponseBriefItems{
ID: item.ID,
Name: item.Name,
Status: item.Status,
WorkflowID: item.WorkflowVersion.ID,
WorkflowVersionID: item.WorkflowVersion.VersionID,
}
}
return reflect.ValueOf(briefItems)
}
func (resp *ListSubmissionsResponse) FromGRPC(protoResp *submissionproto.ListSubmissionsResponse) {
resp.Page = int(protoResp.GetPage())
resp.Size = int(protoResp.GetSize())
resp.Total = int(protoResp.GetTotal())
resp.Items = make([]SubmissionItem, len(protoResp.GetItems()))
for i, item := range protoResp.GetItems() {
resp.Items[i] = SubmissionItem{
ID: item.GetId(),
Name: item.GetName(),
Description: &item.Description,
Status: item.GetStatus(),
StartTime: item.GetStartTime(),
FinishTime: &item.FinishTime,
Duration: item.GetDuration(),
WorkflowVersion: WorkflowVersionBrief{
item.GetWorkflowVersion().GetId(),
item.GetWorkflowVersion().GetVersionID(),
},
RunStatus: Status{
Count: item.GetRunStatus().GetCount(),
Pending: item.GetRunStatus().GetPending(),
Succeeded: item.GetRunStatus().GetSucceeded(),
Failed: item.GetRunStatus().GetFailed(),
Running: item.GetRunStatus().GetRunning(),
Cancelling: item.GetRunStatus().GetCancelling(),
Cancelled: item.GetRunStatus().GetCancelled(),
Queued: item.GetRunStatus().GetQueued(),
Initializing: item.GetRunStatus().GetInitializing(),
},
Entity: &Entity{
DataModelID: item.GetEntity().GetDataModelID(),
DataModelRowIDs: item.GetEntity().GetDataModelRowIDs(),
InputsTemplate: item.GetEntity().GetInputsTemplate(),
OutputsTemplate: item.GetEntity().GetOutputsTemplate(),
},
ExposedOptions: ExposedOptions{
ReadFromCache: item.GetExposedOptions().GetReadFromCache(),
},
InOutMaterial: &InOutMaterial{
InputsMaterial: item.GetInOutMaterial().GetInputsMaterial(),
OutputsMaterial: item.GetInOutMaterial().GetOutputsMaterial(),
},
}
}
}
type SubmissionItem struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
Status string `json:"status"`
StartTime int64 `json:"startTime"`
FinishTime *int64 `json:"finishTime"`
Duration int64 `json:"duration"`
WorkflowVersion WorkflowVersionBrief `json:"workflowVersion"`
RunStatus Status `json:"runStatus"`
Entity *Entity `json:"entity"`
ExposedOptions ExposedOptions `json:"exposedOptions"`
InOutMaterial *InOutMaterial `json:"inOutMaterial"`
}
type WorkflowVersionBrief struct {
ID string `json:"id"`
VersionID string `json:"versionID"`
}
type Status struct {
Count int64 `json:"count"`
Pending int64 `json:"pending"`
Succeeded int64 `json:"succeeded"`
Failed int64 `json:"failed"`
Running int64 `json:"running"`
Cancelling int64 `json:"cancelling"`
Cancelled int64 `json:"cancelled"`
Queued int64 `json:"queued"`
Initializing int64 `json:"initializing"`
}
type DeleteSubmissionRequest struct {
WorkspaceID string `path:"workspace_id"`
ID string `path:"id"`
}
func (req *DeleteSubmissionRequest) ToGRPC() *submissionproto.DeleteSubmissionRequest {
return &submissionproto.DeleteSubmissionRequest{
WorkspaceID: req.WorkspaceID,
Id: req.ID,
}
}
type DeleteSubmissionResponse struct {
}
func (resp *DeleteSubmissionResponse) FromGRPC(protoResp *submissionproto.DeleteSubmissionResponse) {
return
}
type CancelSubmissionRequest struct {
WorkspaceID string `path:"workspace_id"`
ID string `path:"id"`
}
func (req *CancelSubmissionRequest) ToGRPC() *submissionproto.CancelSubmissionRequest {
return &submissionproto.CancelSubmissionRequest{
WorkspaceID: req.WorkspaceID,
Id: req.ID,
}
}
type CancelSubmissionResponse struct {
}
func (resp *CancelSubmissionResponse) FromGRPC(protoResp *submissionproto.CancelSubmissionResponse) {
return
}

View File

@ -0,0 +1,68 @@
package convert
import (
"encoding/json"
"errors"
"net/url"
"reflect"
"strings"
"github.com/go-resty/resty/v2"
"github.com/spf13/cast"
)
func AssignToHttpRequest(req interface{}, restyRequest *resty.Request) {
reqV := reflect.ValueOf(req).Elem()
reqT := reqV.Type()
var pathParams map[string]string
var queryParams url.Values
var body map[string]interface{}
for i := 0; i < reqV.NumField(); i++ {
pathTag := reqT.Field(i).Tag.Get("path")
if pathTag != "" {
if pathParams == nil {
pathParams = make(map[string]string)
}
pathParams[pathTag] = cast.ToString(reqV.Field(i).Interface())
}
queryTag := reqT.Field(i).Tag.Get("query")
if queryTag != "" {
if queryParams == nil {
queryParams = make(url.Values)
}
if strings.Contains(queryTag, "omitempty") {
if reqV.Field(i).IsZero() {
continue
}
queryTag = strings.Split(queryTag, ",")[0]
}
queryParams[queryTag] = cast.ToStringSlice(reqV.Field(i).Interface())
}
bodyTag := reqT.Field(i).Tag.Get("json")
if bodyTag != "" {
if body == nil {
body = make(map[string]interface{})
}
if strings.Contains(bodyTag, "omitempty") {
if reqV.Field(i).IsZero() {
continue
}
bodyTag = strings.Split(bodyTag, ",")[0]
}
body[bodyTag] = reqV.Field(i).Interface()
}
}
restyRequest.SetPathParams(pathParams).SetQueryParamsFromValues(queryParams).SetBody(body)
}
func AssignFromHttpResponse(restyResp *resty.Response, resp interface{}) error {
if restyResp.RawResponse != nil && restyResp.RawResponse.StatusCode >= 400 {
return errors.New(string(restyResp.Body()))
}
err := json.Unmarshal(restyResp.Body(), resp)
if err != nil {
return errors.New(string(restyResp.Body()))
}
return nil
}

View File

@ -0,0 +1,421 @@
package convert
import (
"reflect"
"strings"
"time"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
"github.com/Bio-OS/bioos/pkg/consts"
)
type CreateWorkflowRequest struct {
ID string `json:"id"`
WorkspaceID string `path:"workspace-id"`
Name string `json:"name" validate:"required,resName"`
Description string `json:"description" validate:"required,workspaceDesc"`
Language string `json:"language" validate:"required,oneof=WDL"`
Source string `json:"source" validate:"required,oneof=git"`
Url string `json:"url" validate:"required"`
Tag string `json:"tag" validate:"required"`
Token string `json:"token"`
MainWorkflowPath string `json:"mainWorkflowPath" validate:"required"`
}
func (req *CreateWorkflowRequest) ToGRPC() *workspaceproto.CreateWorkflowRequest {
return &workspaceproto.CreateWorkflowRequest{
Id: &req.ID,
WorkspaceID: req.WorkspaceID,
Name: req.Name,
Description: &req.Description,
Language: req.Language,
Source: req.Source,
Url: &req.Url,
Tag: &req.Tag,
Token: &req.Token,
MainWorkflowPath: req.MainWorkflowPath,
}
}
type CreateWorkflowResponse struct {
ID string `json:"id"`
}
func (resp *CreateWorkflowResponse) FromGRPC(protoResp *workspaceproto.CreateWorkflowResponse) {
resp.ID = protoResp.GetId()
}
type GetWorkflowRequest struct {
WorkspaceID string `path:"workspace-id"`
ID string `path:"id"`
}
func (req *GetWorkflowRequest) ToGRPC() *workspaceproto.GetWorkflowRequest {
return &workspaceproto.GetWorkflowRequest{
WorkspaceID: req.WorkspaceID,
Id: req.ID,
}
}
type GetWorkflowResponse struct {
Workflow WorkflowItem `json:"workflow"`
}
func (resp *GetWorkflowResponse) FromGRPC(protoResp *workspaceproto.GetWorkflowResponse) {
resp.Workflow = convertWorkflowItem(protoResp.GetWorkflow())
}
type ListWorkflowsRequest struct {
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
SearchWord string `query:"searchWord"`
IDs string `query:"ids"`
WorkspaceID string `path:"workspace-id"`
}
func (req *ListWorkflowsRequest) ToGRPC() *workspaceproto.ListWorkflowRequest {
return &workspaceproto.ListWorkflowRequest{
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
SearchWord: req.SearchWord,
Ids: strings.Split(req.IDs, consts.QuerySliceDelimiter),
WorkspaceID: req.WorkspaceID,
}
}
type ListWorkflowsResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Items []WorkflowItem `json:"items"`
}
type listWorkflowsResponseBriefItems struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
LatestVersionStatus string `json:"status"`
LatestVersionMessage string `json:"message"`
LatestVersionLanguage string `json:"language"`
LatestVersionLanguageVersion string `json:"languageVersion"`
LatestVersionMainWorkflowPath string `json:"mainWorkflowPath"`
LatestVersionSource string `json:"source"`
}
func (resp *ListWorkflowsResponse) BriefItems() reflect.Value {
briefItems := make([]listWorkflowsResponseBriefItems, len(resp.Items))
for i, item := range resp.Items {
briefItems[i] = listWorkflowsResponseBriefItems{
ID: item.ID,
Name: item.Name,
Description: item.Description,
LatestVersionStatus: item.LatestVersion.Status,
LatestVersionMessage: item.LatestVersion.Message,
LatestVersionLanguage: item.LatestVersion.Language,
LatestVersionLanguageVersion: item.LatestVersion.LanguageVersion,
LatestVersionMainWorkflowPath: item.LatestVersion.MainWorkflowPath,
LatestVersionSource: item.LatestVersion.Source,
}
}
return reflect.ValueOf(briefItems)
}
func (resp *ListWorkflowsResponse) FromGRPC(protoResp *workspaceproto.ListWorkflowResponse) {
resp.Page = int(protoResp.Page)
resp.Size = int(protoResp.Size)
resp.Total = int(protoResp.Total)
resp.Items = make([]WorkflowItem, len(protoResp.Items))
for i := range protoResp.Items {
resp.Items[i] = convertWorkflowItem(protoResp.Items[i])
}
}
type UpdateWorkflowRequest struct {
ID string `path:"id"`
Name string `json:"name"`
Description string `json:"description"`
WorkspaceID string `path:"workspace-id"`
}
func (req *UpdateWorkflowRequest) ToGRPC() *workspaceproto.UpdateWorkflowRequest {
return &workspaceproto.UpdateWorkflowRequest{
Id: req.ID,
Name: &req.Name,
Description: &req.Description,
WorkspaceID: req.WorkspaceID,
}
}
type UpdateWorkflowResponse struct {
}
func (resp *UpdateWorkflowResponse) FromGRPC(protoResp *workspaceproto.UpdateWorkflowResponse) {
return
}
type DeleteWorkflowRequest struct {
ID string `path:"id"`
WorkspaceID string `path:"workspace-id"`
}
func (req *DeleteWorkflowRequest) ToGRPC() *workspaceproto.DeleteWorkflowRequest {
return &workspaceproto.DeleteWorkflowRequest{
Id: req.ID,
WorkspaceID: req.WorkspaceID,
}
}
type DeleteWorkflowResponse struct {
}
func (resp *DeleteWorkflowResponse) FromGRPC(protoResp *workspaceproto.DeleteWorkflowResponse) {
return
}
type GetWorkflowFileRequest struct {
ID string `path:"id"`
WorkspaceID string `path:"workspace-id"`
WorkflowID string `path:"workflow-id"`
}
func (req *GetWorkflowFileRequest) ToGRPC() *workspaceproto.GetWorkflowFileRequest {
return &workspaceproto.GetWorkflowFileRequest{
Id: req.ID,
WorkspaceID: req.WorkspaceID,
WorkflowID: req.WorkflowID,
}
}
type GetWorkflowFileResponse struct {
File *WorkflowFile `json:"file"`
}
func (resp *GetWorkflowFileResponse) FromGRPC(protoResp *workspaceproto.GetWorkflowFileResponse) {
resp.File = &WorkflowFile{
ID: protoResp.GetFile().GetId(),
WorkflowVersionID: protoResp.GetFile().GetWorkflowVersionID(),
Path: protoResp.GetFile().GetPath(),
Content: protoResp.GetFile().GetContent(),
CreatedAt: protoResp.GetFile().GetCreatedAt().AsTime(),
UpdatedAt: protoResp.GetFile().GetUpdatedAt().AsTime(),
}
}
type GetWorkflowVersionRequest struct {
ID string `path:"id"`
WorkspaceID string `path:"workspace-id"`
WorkflowID string `path:"workflow-id"`
}
func (req *GetWorkflowVersionRequest) ToGRPC() *workspaceproto.GetWorkflowVersionRequest {
return &workspaceproto.GetWorkflowVersionRequest{
Id: req.ID,
WorkspaceID: req.WorkspaceID,
WorkflowID: req.WorkflowID,
}
}
type GetWorkflowVersionResponse struct {
Version WorkflowVersion `json:"version"`
}
func (resp *GetWorkflowVersionResponse) FromGRPC(protoResp *workspaceproto.GetWorkflowVersionResponse) {
resp.Version = convertWorkflowVersion(protoResp.GetVersion())
}
type ListWorkflowFilesRequest struct {
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
IDs string `query:"ids"`
WorkspaceID string `path:"workspace-id"`
WorkflowID string `path:"workflow-id"`
WorkflowVersionID string `query:"workflowVersionID,omitempty"`
}
func (req *ListWorkflowFilesRequest) ToGRPC() *workspaceproto.ListWorkflowFilesRequest {
return &workspaceproto.ListWorkflowFilesRequest{
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
Ids: strings.Split(req.IDs, consts.QuerySliceDelimiter),
WorkspaceID: req.WorkspaceID,
WorkflowID: req.WorkflowID,
WorkflowVersionID: &req.WorkflowVersionID,
}
}
type ListWorkflowFilesResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
WorkspaceID string `json:"workspaceID"`
WorkflowID string `json:"workflowID"`
Items []*WorkflowFile `json:"items"`
}
func (resp *ListWorkflowFilesResponse) FromGRPC(protoResp *workspaceproto.ListWorkflowFilesResponse) {
resp.Page = int(protoResp.GetPage())
resp.Size = int(protoResp.GetSize())
resp.Total = int(protoResp.GetTotal())
resp.WorkspaceID = protoResp.GetWorkspaceID()
resp.WorkflowID = protoResp.GetWorkflowID()
resp.Items = make([]*WorkflowFile, len(protoResp.GetFiles()))
for i, f := range protoResp.GetFiles() {
resp.Items[i] = &WorkflowFile{
ID: f.GetId(),
WorkflowVersionID: f.GetWorkflowVersionID(),
Path: f.GetPath(),
Content: f.GetContent(),
CreatedAt: f.GetCreatedAt().AsTime(),
UpdatedAt: f.GetUpdatedAt().AsTime(),
}
}
}
type ListWorkflowVersionsRequest struct {
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
IDs string `query:"ids"`
WorkspaceID string `path:"workspace-id"`
WorkflowID string `path:"workflow-id"`
}
func (req *ListWorkflowVersionsRequest) ToGRPC() *workspaceproto.ListWorkflowVersionsRequest {
return &workspaceproto.ListWorkflowVersionsRequest{
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
Ids: strings.Split(req.IDs, consts.QuerySliceDelimiter),
WorkspaceID: req.WorkspaceID,
WorkflowID: req.WorkflowID,
}
}
type ListWorkflowVersionsResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
WorkspaceID string `json:"workspaceID"`
WorkflowID string `json:"workflowID"`
Items []WorkflowVersion `json:"items"`
}
func (resp *ListWorkflowVersionsResponse) FromGRPC(protoResp *workspaceproto.ListWorkflowVersionsResponse) {
resp.Page = int(protoResp.GetPage())
resp.Size = int(protoResp.GetSize())
resp.Total = int(protoResp.GetTotal())
resp.WorkspaceID = protoResp.GetWorkspaceID()
resp.WorkflowID = protoResp.GetWorkflowID()
resp.Items = make([]WorkflowVersion, len(protoResp.GetItems()))
for i, item := range protoResp.GetItems() {
resp.Items[i] = convertWorkflowVersion(item)
}
}
type WorkflowItem struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
LatestVersion WorkflowVersion `json:"latestVersion"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"UpdatedAt"`
}
type WorkflowVersion struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
Language string `json:"language"`
LanguageVersion string `json:"languageVersion"`
MainWorkflowPath string `json:"mainWorkflowPath"`
Inputs []*WorkflowParam `json:"inputs"`
Outputs []*WorkflowParam `json:"outputs"`
Graph string `json:"graph"`
Metadata map[string]string `json:"metadata"`
Source string `json:"source"`
Files []*WorkflowFileInfo `json:"files"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"UpdatedAt"`
}
type WorkflowFileInfo struct {
ID string `json:"id"`
Path string `json:"path"`
}
type WorkflowParam struct {
Name string `json:"name"`
Type string `json:"type"`
Optional bool `json:"optional"`
Default string `json:"default"`
}
type WorkflowFile struct {
ID string `json:"id"`
WorkflowVersionID string `json:"workflowVersionID"`
Path string `json:"path"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"UpdatedAt"`
}
func convertWorkflowItem(workflow *workspaceproto.Workflow) WorkflowItem {
item := WorkflowItem{
ID: workflow.GetId(),
Name: workflow.GetName(),
Description: workflow.GetDescription(),
LatestVersion: convertWorkflowVersion(workflow.GetLatestVersion()),
CreatedAt: workflow.GetCreatedAt().AsTime(),
UpdatedAt: workflow.GetUpdatedAt().AsTime(),
}
return item
}
func convertWorkflowVersion(version *workspaceproto.WorkflowVersion) WorkflowVersion {
v := WorkflowVersion{
ID: version.GetId(),
Status: version.GetStatus(),
Message: version.GetMessage(),
Language: version.GetLanguage(),
LanguageVersion: version.GetLanguageVersion(),
MainWorkflowPath: version.GetMainWorkflowPath(),
Inputs: make([]*WorkflowParam, len(version.GetInputs())),
Outputs: make([]*WorkflowParam, len(version.GetOutputs())),
Graph: version.GetGraph(),
Metadata: version.GetMetadata(),
Source: version.GetSource(),
Files: make([]*WorkflowFileInfo, len(version.GetFiles())),
CreatedAt: version.GetCreatedAt().AsTime(),
UpdatedAt: version.GetUpdatedAt().AsTime(),
}
for i, param := range version.GetInputs() {
v.Inputs[i] = &WorkflowParam{
Name: param.Name,
Type: param.Type,
Optional: param.Optional,
Default: *param.Default,
}
}
for i, param := range version.GetOutputs() {
v.Outputs[i] = &WorkflowParam{
Name: param.Name,
Type: param.Type,
Optional: param.Optional,
Default: *param.Default,
}
}
for i, f := range version.GetFiles() {
v.Files[i] = &WorkflowFileInfo{
ID: f.Id,
Path: f.Path,
}
}
return v
}

View File

@ -0,0 +1,183 @@
package convert
import (
"reflect"
"strings"
"k8s.io/utils/pointer"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
"github.com/Bio-OS/bioos/pkg/consts"
)
type CreateWorkspaceRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Storage *WorkspaceStorage `json:"storage"`
}
func (req *CreateWorkspaceRequest) ToGRPC() *workspaceproto.CreateWorkspaceRequest {
return &workspaceproto.CreateWorkspaceRequest{
Name: req.Name,
Description: req.Description,
Storage: &workspaceproto.WorkspaceStorage{
Nfs: &workspaceproto.NFSWorkspaceStorage{
MountPath: req.Storage.NFS.MountPath,
},
},
}
}
type CreateWorkspaceResponse struct {
Id string `json:"id"`
}
func (resp *CreateWorkspaceResponse) FromGRPC(protoResp *workspaceproto.CreateWorkspaceResponse) {
resp.Id = protoResp.Id
}
type DeleteWorkspaceRequest struct {
Id string `path:"id"`
}
func (req *DeleteWorkspaceRequest) ToGRPC() *workspaceproto.DeleteWorkspaceRequest {
return &workspaceproto.DeleteWorkspaceRequest{
Id: req.Id,
}
}
type DeleteWorkspaceResponse struct {
}
func (resp *DeleteWorkspaceResponse) FromGRPC(protoResp *workspaceproto.DeleteWorkspaceResponse) {
return
}
type UpdateWorkspaceRequest struct {
ID string `path:"id"`
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
}
func (req *UpdateWorkspaceRequest) ToGRPC() *workspaceproto.UpdateWorkspaceRequest {
return &workspaceproto.UpdateWorkspaceRequest{
Id: req.ID,
Name: pointer.StringDeref(req.Name, ""),
Description: pointer.StringDeref(req.Description, ""),
}
}
type UpdateWorkspaceResponse struct {
}
func (resp *UpdateWorkspaceResponse) FromGRPC(protoResp *workspaceproto.UpdateWorkspaceResponse) {
return
}
type ListWorkspacesRequest struct {
Page int `query:"page"`
Size int `query:"size"`
OrderBy string `query:"orderBy"`
SearchWord string `query:"searchWord"`
IDs string `query:"ids"`
}
func (req *ListWorkspacesRequest) ToGRPC() *workspaceproto.ListWorkspaceRequest {
r := &workspaceproto.ListWorkspaceRequest{
Page: int32(req.Page),
Size: int32(req.Size),
OrderBy: req.OrderBy,
SearchWord: req.SearchWord,
}
if req.IDs != "" {
r.Ids = strings.Split(req.IDs, consts.QuerySliceDelimiter)
}
return r
}
type ListWorkspacesResponse struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
Items []WorkspaceItem `json:"items"`
}
type listWorkspacesBriefItems struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
func (resp *ListWorkspacesResponse) BriefItems() reflect.Value {
briefItems := make([]listWorkspacesBriefItems, len(resp.Items))
for i, item := range resp.Items {
briefItems[i] = struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}{
Id: item.Id,
Name: item.Name,
Description: item.Description,
}
}
return reflect.ValueOf(briefItems)
}
func (resp *ListWorkspacesResponse) FromGRPC(protoResp *workspaceproto.ListWorkspaceResponse) {
resp.Page = int(protoResp.Page)
resp.Size = int(protoResp.Size)
resp.Total = int(protoResp.Total)
resp.Items = make([]WorkspaceItem, len(protoResp.GetItems()))
for i, item := range protoResp.Items {
resp.Items[i] = WorkspaceItem{
Id: item.Id,
Name: item.Name,
Description: item.Description,
Storage: &WorkspaceStorage{
NFS: &NFSWorkspaceStorage{
MountPath: item.Storage.Nfs.MountPath,
},
},
CreateTime: item.CreatedAt.GetSeconds(),
UpdateTime: item.UpdatedAt.GetSeconds(),
}
}
return
}
type WorkspaceItem struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Storage *WorkspaceStorage `json:"storage"`
CreateTime int64 `json:"createTime"`
UpdateTime int64 `json:"updateTime"`
}
type WorkspaceStorage struct {
NFS *NFSWorkspaceStorage `json:"nfs,omitempty"`
}
// For table/text output
func (s WorkspaceStorage) String() string {
return s.NFS.MountPath
}
type NFSWorkspaceStorage struct {
MountPath string `json:"mountPath"`
}
type ImportWorkspaceRequest struct {
FilePath string
MountPath string `query:"mountPath"`
MountType string `query:"mountType"`
}
type ImportWorkspaceResponse struct {
Id string `json:"id"`
}
func (resp *ImportWorkspaceResponse) FromGRPC(protoResp *workspaceproto.ImportWorkspaceResponse) {
resp.Id = protoResp.GetId()
}

View File

@ -0,0 +1,155 @@
package factory
import (
"context"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
)
type DataModelClient interface {
ListDataModels(ctx context.Context, in *convert.ListDataModelsRequest) (*convert.ListDataModelsResponse, error)
GetDataModel(ctx context.Context, in *convert.GetDataModelRequest) (*convert.GetDataModelResponse, error)
ListDataModelRows(ctx context.Context, in *convert.ListDataModelRowsRequest) (*convert.ListDataModelRowsResponse, error)
PatchDataModel(ctx context.Context, in *convert.PatchDataModelRequest) (*convert.PatchDataModelResponse, error)
DeleteDataModel(ctx context.Context, in *convert.DeleteDataModelRequest) (*convert.DeleteDataModelResponse, error)
ListAllDataModelRowIDs(ctx context.Context, in *convert.ListAllDataModelRowIDsRequest) (*convert.ListAllDataModelRowIDsResponse, error)
}
func (g *grpcClient) ListDataModels(ctx context.Context, in *convert.ListDataModelsRequest) (*convert.ListDataModelsResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).ListDataModels(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListDataModelsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) GetDataModel(ctx context.Context, in *convert.GetDataModelRequest) (*convert.GetDataModelResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).GetDataModel(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.GetDataModelResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListDataModelRows(ctx context.Context, in *convert.ListDataModelRowsRequest) (*convert.ListDataModelRowsResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).ListDataModelRows(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListDataModelRowsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) PatchDataModel(ctx context.Context, in *convert.PatchDataModelRequest) (*convert.PatchDataModelResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).PatchDataModel(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.PatchDataModelResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) DeleteDataModel(ctx context.Context, in *convert.DeleteDataModelRequest) (*convert.DeleteDataModelResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).DeleteDataModel(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.DeleteDataModelResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListAllDataModelRowIDs(ctx context.Context, in *convert.ListAllDataModelRowIDsRequest) (*convert.ListAllDataModelRowIDsResponse, error) {
protoResp, err := workspaceproto.NewDataModelServiceClient(g.conn).ListAllDataModelRowIDs(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListAllDataModelRowIDsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (h *httpClient) ListDataModels(ctx context.Context, in *convert.ListDataModelsRequest) (*convert.ListDataModelsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/data_model"))
if err != nil {
return nil, err
}
out := &convert.ListDataModelsResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) GetDataModel(ctx context.Context, in *convert.GetDataModelRequest) (*convert.GetDataModelResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/data_model/{id}"))
if err != nil {
return nil, err
}
out := &convert.GetDataModelResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ListDataModelRows(ctx context.Context, in *convert.ListDataModelRowsRequest) (*convert.ListDataModelRowsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/data_model/{id}/rows"))
if err != nil {
return nil, err
}
out := &convert.ListDataModelRowsResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) PatchDataModel(ctx context.Context, in *convert.PatchDataModelRequest) (*convert.PatchDataModelResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Patch(h.url("workspace/{workspace_id}/data_model"))
if err != nil {
return nil, err
}
out := &convert.PatchDataModelResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) DeleteDataModel(ctx context.Context, in *convert.DeleteDataModelRequest) (*convert.DeleteDataModelResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Delete(h.url("workspace/{workspace_id}/data_model/{id}"))
if err != nil {
return nil, err
}
out := &convert.DeleteDataModelResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ListAllDataModelRowIDs(ctx context.Context, in *convert.ListAllDataModelRowIDsRequest) (*convert.ListAllDataModelRowIDsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/data_model/{id}/rows/ids"))
if err != nil {
return nil, err
}
out := &convert.ListAllDataModelRowIDsResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}

View File

@ -0,0 +1,144 @@
package factory
import (
"context"
"fmt"
"strings"
"github.com/go-resty/resty/v2"
"google.golang.org/grpc"
clioptions "github.com/Bio-OS/bioos/internal/bioctl/options"
"github.com/Bio-OS/bioos/internal/bioctl/utils"
"github.com/Bio-OS/bioos/pkg/client"
pkgutils "github.com/Bio-OS/bioos/pkg/utils"
)
type Factory interface {
WorkspaceClient() (WorkspaceClient, error)
DataModelClient() (DataModelClient, error)
WorkflowClient() (WorkflowClient, error)
VersionClient() (VersionClient, error)
SubmissionClient() (SubmissionClient, error)
}
func NewFactory(opts *clioptions.ClientOptions) Factory {
return factoryImpl{
opts: opts,
}
}
var _ Factory = factoryImpl{}
type factoryImpl struct {
opts *clioptions.ClientOptions
}
func (f factoryImpl) newGrpcClient() (*grpcClient, error) {
conn, err := pkgutils.GrpcDial(f.opts.ConnectInfo, f.opts.AuthInfo)
if err != nil {
utils.CheckErr(err)
}
return &grpcClient{
conn: conn,
}, nil
}
func (f factoryImpl) newHttpClient() (*httpClient, error) {
transport, err := pkgutils.NewTransportWithAuth(f.opts.ConnectInfo, f.opts.AuthInfo)
if err != nil {
return nil, err
}
serverAddr := f.opts.ServerAddr
if f.opts.Insecure {
serverAddr = "http://" + serverAddr
} else {
serverAddr = "https://" + serverAddr
}
return &httpClient{
addr: strings.TrimRight(serverAddr, "/"),
rest: resty.NewWithClient(transport.Client()),
}, err
}
func (f factoryImpl) WorkspaceClient() (WorkspaceClient, error) {
if err := f.opts.Method.Validate(); err != nil {
return nil, err
}
switch f.opts.Method {
case client.GRPCMethod:
return f.newGrpcClient()
case client.HTTPMethod:
return f.newHttpClient()
}
return nil, nil
}
func (f factoryImpl) DataModelClient() (DataModelClient, error) {
if err := f.opts.Method.Validate(); err != nil {
return nil, err
}
switch f.opts.Method {
case client.GRPCMethod:
return f.newGrpcClient()
case client.HTTPMethod:
return f.newHttpClient()
}
return nil, nil
}
func (f factoryImpl) VersionClient() (VersionClient, error) {
if err := f.opts.Method.Validate(); err != nil {
return nil, err
}
switch f.opts.Method {
case client.GRPCMethod:
return f.newGrpcClient()
case client.HTTPMethod:
return f.newHttpClient()
}
return nil, nil
}
func (f factoryImpl) SubmissionClient() (SubmissionClient, error) {
if err := f.opts.Method.Validate(); err != nil {
return nil, err
}
switch f.opts.Method {
case client.GRPCMethod:
return f.newGrpcClient()
case client.HTTPMethod:
return f.newHttpClient()
}
return nil, nil
}
func (f factoryImpl) WorkflowClient() (WorkflowClient, error) {
if err := f.opts.Method.Validate(); err != nil {
return nil, err
}
switch f.opts.Method {
case client.GRPCMethod:
return f.newGrpcClient()
case client.HTTPMethod:
return f.newHttpClient()
}
return nil, nil
}
type grpcClient struct {
conn *grpc.ClientConn
}
type httpClient struct {
addr string
rest *resty.Client
}
func (h *httpClient) restR(ctx context.Context) *resty.Request {
return h.rest.R().SetContext(ctx).ForceContentType("application/json")
}
func (h *httpClient) url(apipath string, pathParams ...interface{}) string {
return strings.Join([]string{h.addr, fmt.Sprintf(apipath, pathParams...)}, "/")
}

View File

@ -0,0 +1,179 @@
package factory
import (
"context"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
submissionproto "github.com/Bio-OS/bioos/internal/context/submission/interface/grpc/proto"
)
type SubmissionClient interface {
ListSubmissions(ctx context.Context, in *convert.ListSubmissionsRequest) (*convert.ListSubmissionsResponse, error)
CreateSubmission(ctx context.Context, in *convert.CreateSubmissionRequest) (*convert.CreateSubmissionResponse, error)
DeleteSubmission(ctx context.Context, in *convert.DeleteSubmissionRequest) (*convert.DeleteSubmissionResponse, error)
CancelSubmission(ctx context.Context, in *convert.CancelSubmissionRequest) (*convert.CancelSubmissionResponse, error)
ListRuns(ctx context.Context, in *convert.ListRunsRequest) (*convert.ListRunsResponse, error)
CancelRun(ctx context.Context, in *convert.CancelRunRequest) (*convert.CancelRunResponse, error)
ListTasks(ctx context.Context, in *convert.ListTasksRequest) (*convert.ListTasksResponse, error)
}
func (g *grpcClient) ListSubmissions(ctx context.Context, in *convert.ListSubmissionsRequest) (*convert.ListSubmissionsResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).ListSubmissions(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListSubmissionsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) CreateSubmission(ctx context.Context, in *convert.CreateSubmissionRequest) (*convert.CreateSubmissionResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).CreateSubmission(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.CreateSubmissionResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) DeleteSubmission(ctx context.Context, in *convert.DeleteSubmissionRequest) (*convert.DeleteSubmissionResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).DeleteSubmission(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.DeleteSubmissionResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) CancelSubmission(ctx context.Context, in *convert.CancelSubmissionRequest) (*convert.CancelSubmissionResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).CancelSubmission(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.CancelSubmissionResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListRuns(ctx context.Context, in *convert.ListRunsRequest) (*convert.ListRunsResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).ListRuns(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListRunsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) CancelRun(ctx context.Context, in *convert.CancelRunRequest) (*convert.CancelRunResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).CancelRun(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.CancelRunResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListTasks(ctx context.Context, in *convert.ListTasksRequest) (*convert.ListTasksResponse, error) {
protoResp, err := submissionproto.NewSubmissionServiceClient(g.conn).ListTasks(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListTasksResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (h *httpClient) ListSubmissions(ctx context.Context, in *convert.ListSubmissionsRequest) (*convert.ListSubmissionsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/submission"))
if err != nil {
return nil, err
}
out := &convert.ListSubmissionsResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) CreateSubmission(ctx context.Context, in *convert.CreateSubmissionRequest) (*convert.CreateSubmissionResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Post(h.url("workspace/{workspace_id}/submission"))
if err != nil {
return nil, err
}
out := &convert.CreateSubmissionResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) DeleteSubmission(ctx context.Context, in *convert.DeleteSubmissionRequest) (*convert.DeleteSubmissionResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Delete(h.url("workspace/{workspace_id}/submission/{id}"))
if err != nil {
return nil, err
}
out := &convert.DeleteSubmissionResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) CancelSubmission(ctx context.Context, in *convert.CancelSubmissionRequest) (*convert.CancelSubmissionResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Post(h.url("workspace/{workspace_id}/submission/{id}/cancel"))
if err != nil {
return nil, err
}
out := &convert.CancelSubmissionResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ListRuns(ctx context.Context, in *convert.ListRunsRequest) (*convert.ListRunsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/submission/{submission_id}/run"))
if err != nil {
return nil, err
}
out := &convert.ListRunsResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) CancelRun(ctx context.Context, in *convert.CancelRunRequest) (*convert.CancelRunResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Post(h.url("workspace/{workspace_id}/submission/{submission_id}/run/{id}/cancel"))
if err != nil {
return nil, err
}
out := &convert.CancelRunResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ListTasks(ctx context.Context, in *convert.ListTasksRequest) (*convert.ListTasksResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace_id}/submission/{submission_id}/run/{run_id}/task"))
if err != nil {
return nil, err
}
out := &convert.ListTasksResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}

View File

@ -0,0 +1,51 @@
package factory
import (
"context"
"encoding/json"
"fmt"
"github.com/cloudwego/hertz/pkg/protocol/consts"
"google.golang.org/protobuf/types/known/emptypb"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
"github.com/Bio-OS/bioos/pkg/version"
)
type VersionClient interface {
Version(ctx context.Context) (*version.Info, error)
}
func (h *httpClient) Version(ctx context.Context) (*version.Info, error) {
req := h.restR(ctx)
httpResp, err := req.Get(h.url("version"))
if err != nil {
return nil, err
}
if httpResp.StatusCode() != consts.StatusOK {
return nil, fmt.Errorf("status code is %d", httpResp.StatusCode())
}
var info *version.Info
err = json.Unmarshal(httpResp.Body(), &info)
return info, err
}
func (g *grpcClient) Version(ctx context.Context) (*version.Info, error) {
cli := workspaceproto.NewVersionServiceClient(g.conn)
response, err := cli.Version(ctx, &emptypb.Empty{})
if err != nil {
return nil, err
}
return &version.Info{
Version: response.Version,
GitBranch: response.GitBranch,
GitCommit: response.GitCommit,
GitTreeState: response.GitTreeState,
BuildTime: response.BuildTime,
GoVersion: response.GoVersion,
Compiler: response.Compiler,
Platform: response.Platform,
}, nil
}

View File

@ -0,0 +1,179 @@
package factory
import (
"context"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
)
type WorkflowClient interface {
GetWorkflow(ctx context.Context, in *convert.GetWorkflowRequest) (*convert.GetWorkflowResponse, error)
GetWorkflowVersion(ctx context.Context, in *convert.GetWorkflowVersionRequest) (*convert.GetWorkflowVersionResponse, error)
ListWorkflowFiles(ctx context.Context, in *convert.ListWorkflowFilesRequest) (*convert.ListWorkflowFilesResponse, error)
CreateWorkflow(ctx context.Context, in *convert.CreateWorkflowRequest) (*convert.CreateWorkflowResponse, error)
DeleteWorkflow(ctx context.Context, in *convert.DeleteWorkflowRequest) (*convert.DeleteWorkflowResponse, error)
UpdateWorkflow(ctx context.Context, in *convert.UpdateWorkflowRequest) (*convert.UpdateWorkflowResponse, error)
ListWorkflow(ctx context.Context, in *convert.ListWorkflowsRequest) (*convert.ListWorkflowsResponse, error)
}
func (g *grpcClient) GetWorkflow(ctx context.Context, in *convert.GetWorkflowRequest) (*convert.GetWorkflowResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).GetWorkflow(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.GetWorkflowResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) GetWorkflowVersion(ctx context.Context, in *convert.GetWorkflowVersionRequest) (*convert.GetWorkflowVersionResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).GetWorkflowVersion(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.GetWorkflowVersionResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListWorkflowFiles(ctx context.Context, in *convert.ListWorkflowFilesRequest) (*convert.ListWorkflowFilesResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).ListWorkflowFiles(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListWorkflowFilesResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) CreateWorkflow(ctx context.Context, in *convert.CreateWorkflowRequest) (*convert.CreateWorkflowResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).CreateWorkflow(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.CreateWorkflowResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) DeleteWorkflow(ctx context.Context, in *convert.DeleteWorkflowRequest) (*convert.DeleteWorkflowResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).DeleteWorkflow(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.DeleteWorkflowResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) UpdateWorkflow(ctx context.Context, in *convert.UpdateWorkflowRequest) (*convert.UpdateWorkflowResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).UpdateWorkflow(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.UpdateWorkflowResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListWorkflow(ctx context.Context, in *convert.ListWorkflowsRequest) (*convert.ListWorkflowsResponse, error) {
protoResp, err := workspaceproto.NewWorkflowServiceClient(g.conn).ListWorkflow(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListWorkflowsResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (h *httpClient) GetWorkflow(ctx context.Context, in *convert.GetWorkflowRequest) (*convert.GetWorkflowResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace-id}/workflow/{workflow-id}/file/{id}"))
if err != nil {
return nil, err
}
out := &convert.GetWorkflowResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) GetWorkflowVersion(ctx context.Context, in *convert.GetWorkflowVersionRequest) (*convert.GetWorkflowVersionResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace-id}/workflow/{workflow-id}/version/{id}"))
if err != nil {
return nil, err
}
out := &convert.GetWorkflowVersionResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) ListWorkflowFiles(ctx context.Context, in *convert.ListWorkflowFilesRequest) (*convert.ListWorkflowFilesResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace-id}/workflow/{workflow-id}/file"))
if err != nil {
return nil, err
}
out := &convert.ListWorkflowFilesResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) CreateWorkflow(ctx context.Context, in *convert.CreateWorkflowRequest) (*convert.CreateWorkflowResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Post(h.url("workspace/{workspace-id}/workflow"))
if err != nil {
return nil, err
}
out := &convert.CreateWorkflowResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) DeleteWorkflow(ctx context.Context, in *convert.DeleteWorkflowRequest) (*convert.DeleteWorkflowResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Delete(h.url("workspace/{workspace-id}/workflow/{id}"))
if err != nil {
return nil, err
}
out := &convert.DeleteWorkflowResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) UpdateWorkflow(ctx context.Context, in *convert.UpdateWorkflowRequest) (*convert.UpdateWorkflowResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Patch(h.url("workspace/{workspace-id}/workflow/{id}"))
if err != nil {
return nil, err
}
out := &convert.UpdateWorkflowResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}
func (h *httpClient) ListWorkflow(ctx context.Context, in *convert.ListWorkflowsRequest) (*convert.ListWorkflowsResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace/{workspace-id}/workflow"))
if err != nil {
return nil, err
}
out := &convert.ListWorkflowsResponse{}
err = convert.AssignFromHttpResponse(httpResp, out)
return out, err
}

View File

@ -0,0 +1,180 @@
package factory
import (
"context"
"io"
"os"
"path"
"github.com/Bio-OS/bioos/internal/bioctl/factory/convert"
workspaceproto "github.com/Bio-OS/bioos/internal/context/workspace/interface/grpc/proto"
)
type WorkspaceClient interface {
CreateWorkspace(ctx context.Context, in *convert.CreateWorkspaceRequest) (*convert.CreateWorkspaceResponse, error)
DeleteWorkspace(ctx context.Context, in *convert.DeleteWorkspaceRequest) (*convert.DeleteWorkspaceResponse, error)
UpdateWorkspace(ctx context.Context, in *convert.UpdateWorkspaceRequest) (*convert.UpdateWorkspaceResponse, error)
ListWorkspaces(ctx context.Context, in *convert.ListWorkspacesRequest) (*convert.ListWorkspacesResponse, error)
ImportWorkspace(ctx context.Context, in *convert.ImportWorkspaceRequest) (*convert.ImportWorkspaceResponse, error)
}
func (g *grpcClient) CreateWorkspace(ctx context.Context, in *convert.CreateWorkspaceRequest) (*convert.CreateWorkspaceResponse, error) {
protoResp, err := workspaceproto.NewWorkspaceServiceClient(g.conn).CreateWorkspace(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.CreateWorkspaceResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) DeleteWorkspace(ctx context.Context, in *convert.DeleteWorkspaceRequest) (*convert.DeleteWorkspaceResponse, error) {
protoResp, err := workspaceproto.NewWorkspaceServiceClient(g.conn).DeleteWorkspace(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.DeleteWorkspaceResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) UpdateWorkspace(ctx context.Context, in *convert.UpdateWorkspaceRequest) (*convert.UpdateWorkspaceResponse, error) {
protoResp, err := workspaceproto.NewWorkspaceServiceClient(g.conn).UpdateWorkspace(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.UpdateWorkspaceResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ListWorkspaces(ctx context.Context, in *convert.ListWorkspacesRequest) (*convert.ListWorkspacesResponse, error) {
protoResp, err := workspaceproto.NewWorkspaceServiceClient(g.conn).ListWorkspace(ctx, in.ToGRPC())
if err != nil {
return nil, err
}
out := &convert.ListWorkspacesResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (g *grpcClient) ImportWorkspace(ctx context.Context, in *convert.ImportWorkspaceRequest) (*convert.ImportWorkspaceResponse, error) {
client := workspaceproto.NewWorkspaceServiceClient(g.conn)
stream, err := client.ImportWorkspace(ctx)
if err != nil {
return nil, err
}
file, err := os.Open(in.FilePath)
defer file.Close()
if err != nil {
return nil, err
}
// Maximum 1KB size per stream.
buf := make([]byte, 1024)
for {
num, err := file.Read(buf)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if err := stream.Send(&workspaceproto.ImportWorkspaceRequest{
FileName: path.Base(in.FilePath),
Content: buf[:num],
Storage: &workspaceproto.WorkspaceStorage{
Nfs: &workspaceproto.NFSWorkspaceStorage{
MountPath: in.MountPath,
},
},
}); err != nil {
return nil, err
}
}
protoResp, err := stream.CloseAndRecv()
if err != nil {
return nil, err
}
out := &convert.ImportWorkspaceResponse{}
out.FromGRPC(protoResp)
return out, nil
}
func (h *httpClient) CreateWorkspace(ctx context.Context, in *convert.CreateWorkspaceRequest) (*convert.CreateWorkspaceResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Post(h.url("workspace"))
if err != nil {
return nil, err
}
out := &convert.CreateWorkspaceResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) DeleteWorkspace(ctx context.Context, in *convert.DeleteWorkspaceRequest) (*convert.DeleteWorkspaceResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Delete(h.url("workspace/{id}"))
if err != nil {
return nil, err
}
out := &convert.DeleteWorkspaceResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) UpdateWorkspace(ctx context.Context, in *convert.UpdateWorkspaceRequest) (*convert.UpdateWorkspaceResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Patch(h.url("workspace/{id}"))
if err != nil {
return nil, err
}
out := &convert.UpdateWorkspaceResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ListWorkspaces(ctx context.Context, in *convert.ListWorkspacesRequest) (*convert.ListWorkspacesResponse, error) {
req := h.restR(ctx)
convert.AssignToHttpRequest(in, req)
httpResp, err := req.Get(h.url("workspace"))
if err != nil {
return nil, err
}
out := &convert.ListWorkspacesResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}
func (h *httpClient) ImportWorkspace(ctx context.Context, in *convert.ImportWorkspaceRequest) (*convert.ImportWorkspaceResponse, error) {
req := h.rest.R().SetContext(ctx).ForceContentType("multipart/form-data").SetQueryParams(map[string]string{
"mountPath": in.MountPath,
"mountType": in.MountType,
})
file, err := os.Open(in.FilePath)
if err != nil {
return nil, err
}
defer file.Close()
req.SetFile("file", path.Base(in.FilePath))
httpResp, err := req.Put(h.url("workspace"))
if err != nil {
return nil, err
}
out := &convert.ImportWorkspaceResponse{}
convert.AssignFromHttpResponse(httpResp, out)
return out, nil
}

View File

@ -0,0 +1,139 @@
package options
import (
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"k8s.io/client-go/util/homedir"
"github.com/Bio-OS/bioos/internal/bioctl/utils"
"github.com/Bio-OS/bioos/internal/bioctl/utils/formatter"
"github.com/Bio-OS/bioos/internal/bioctl/utils/prompt"
"github.com/Bio-OS/bioos/pkg/client"
)
const (
ConfigFlagName = "config"
DefaultConfigName = "bioctl.yaml"
)
var DefaultConfig = GlobalOptions{
Client: ClientOptions{
Insecure: true,
Method: client.HTTPMethod,
Timeout: 30,
},
Stream: StreamOptions{
Input: os.Stdin,
Output: os.Stdout,
ErrorOutput: os.Stderr,
},
}
var CfgFile string
func init() {
pflag.StringVarP(&CfgFile, ConfigFlagName, "c", CfgFile, "Read bioctl configuration from specified `FILE`, "+
"support JSON, YAML formats.")
}
type ClientOptions = client.Options
type GlobalOptions struct {
Client ClientOptions `json:"client" mapstructure:"client"`
Stream StreamOptions `json:"stream" mapstructure:"stream"`
}
func (o GlobalOptions) Validate() error {
if err := o.Client.Validate(); err != nil {
return err
}
if err := o.Stream.Validate(); err != nil {
return err
}
return nil
}
type StreamOptions struct {
// input stream, eg: os.Stdin
Input io.Reader `json:"input,omitempty"`
// output stream, eg: os.Stdout
Output io.Writer `json:"output,omitempty"`
// error stream, eg: os.Stderr
ErrorOutput io.Writer `json:"errorOutput,omitempty"`
// output format, eg: text,table,json
OutputFormat formatter.Format `json:"format,omitempty" mapstructure:"format,omitempty"`
}
func (o StreamOptions) Validate() error {
if err := o.OutputFormat.Validate(); err != nil {
return err
}
return nil
}
// LoadConfigFile load config file
func LoadConfigFile(configFile string, opt *GlobalOptions) {
if configFile != "" {
viper.SetConfigFile(configFile)
} else {
viper.SetConfigName(DefaultConfigName)
}
viper.AddConfigPath(".")
viper.AddConfigPath("conf")
viper.AddConfigPath(filepath.Join(homedir.HomeDir(), ".bioctl"))
// Use config file from the flag.
viper.SetConfigType("yaml") // set the type of the configuration to yaml.
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err != nil {
utils.CheckErr(err)
}
//stream.key should only come from command line, thus delete it here
if viper.Get("stream") != nil {
delete(viper.Get("stream").(map[string]interface{}), "format")
}
if err := viper.Unmarshal(&opt); err != nil {
utils.CheckErr(err)
}
return
}
func SetConfigEnv() {
viper.SetEnvPrefix("BIO") // set ENVIRONMENT variables prefix to BIO.
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
}
type Options interface {
Complete() error
Validate() error
Run(args []string) error
GetPromptArgs() ([]string, error)
GetPromptOptions() error
GetDefaultFormat() formatter.Format
}
func GetCommonRunFunc(o Options) func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, args []string) {
utils.CheckErr(o.Complete())
var err error
if len(args) > 0 {
if args[0] == prompt.NeedPromptFlag {
args, err = o.GetPromptArgs()
utils.CheckErr(err)
utils.CheckErr(o.GetPromptOptions())
}
}
utils.CheckErr(o.Validate())
utils.CheckErr(o.Run(args))
}
}

View File

@ -0,0 +1,89 @@
package bioctl
import (
"fmt"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"github.com/spf13/pflag"
)
var (
profileName string
profileOutput string
)
func addProfilingFlags(flags *pflag.FlagSet) {
flags.StringVar(&profileName, "profile", "none", "Name of profile to capture. One of (none|cpu|heap|goroutine|threadcreate|block|mutex)")
flags.StringVar(&profileOutput, "profile-output", "profile.pprof", "Name of the file to write the profile to")
}
func initProfiling() error {
var (
f *os.File
err error
)
switch profileName {
case "none":
return nil
case "cpu":
f, err = os.Create(profileOutput)
if err != nil {
return err
}
err = pprof.StartCPUProfile(f)
if err != nil {
return err
}
// Block and mutex profiles need a call to Set{Block,Mutex}ProfileRate to
// output anything. We choose to sample all events.
case "block":
runtime.SetBlockProfileRate(1)
case "mutex":
runtime.SetMutexProfileFraction(1)
default:
// Check the profile name is valid.
if profile := pprof.Lookup(profileName); profile == nil {
return fmt.Errorf("unknown profile '%s'", profileName)
}
}
// If the command is interrupted before the end (ctrl-c), flush the
// profiling files
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
f.Close()
flushProfiling()
os.Exit(0)
}()
return nil
}
func flushProfiling() error {
switch profileName {
case "none":
return nil
case "cpu":
pprof.StopCPUProfile()
case "heap":
runtime.GC()
fallthrough
default:
profile := pprof.Lookup(profileName)
if profile == nil {
return nil
}
f, err := os.Create(profileOutput)
if err != nil {
return err
}
defer f.Close()
profile.WriteTo(f, 0)
}
return nil
}

View File

@ -0,0 +1,60 @@
package formatter
import (
"fmt"
"io"
"text/tabwriter"
)
type Formatter interface {
Write(interface{})
}
func NewFormatter(format Format, output io.Writer) Formatter {
switch format {
case TextFormat:
return NewText(output)
case TableFormat:
return NewTable(output)
case JsonFormat:
return NewJson(output)
case YamlFormat:
return NewYaml(output)
default:
return NewText(output)
}
}
func NewText(output io.Writer) Formatter {
return &Text{output}
}
func NewTable(output io.Writer) Formatter {
return &Table{
*tabwriter.NewWriter(output, 0, 8, 0, tableDelimiter[0], 0),
}
}
func NewJson(output io.Writer) Formatter {
return &Json{output}
}
func NewYaml(output io.Writer) Formatter {
return &Yaml{output}
}
type Format string
const (
TextFormat Format = "text"
TableFormat Format = "table"
JsonFormat Format = "json"
YamlFormat Format = "yaml"
)
func (f Format) Validate() error {
switch f {
case TextFormat, TableFormat, JsonFormat, YamlFormat:
return nil
}
return fmt.Errorf("invalid formatter type. Valid formatter types are 'text', 'table', or 'json'")
}

View File

@ -0,0 +1,26 @@
package formatter
import (
"encoding/json"
"fmt"
"io"
)
type Json struct {
writer io.Writer
}
func (f *Json) write(bytes []byte) {
_, _ = fmt.Fprint(f.writer, string(bytes))
}
func (f *Json) newLine() {
_, _ = fmt.Fprintln(f.writer)
}
func (f *Json) Write(o interface{}) {
jsons, _ := json.MarshalIndent(o, "", "\t")
f.write(jsons)
f.newLine()
}

View File

@ -0,0 +1,230 @@
package formatter
import (
"fmt"
"reflect"
"strings"
"text/tabwriter"
)
type Table struct {
writer tabwriter.Writer
}
const (
noPrefix = ""
emptyVal = "-"
prefixNameDivider = "-"
tableDelimiter = "\t"
)
// ListResult only applies for table formatter
type ListResult interface {
BriefItems() reflect.Value
}
var listResponseType = reflect.TypeOf(new(ListResult)).Elem()
func (f *Table) Write(o interface{}) {
oType := reflect.TypeOf(o)
if valid := f.validate(oType); valid {
val := reflect.ValueOf(o)
if !f.isSlice(oType) {
if val.Type().Implements(listResponseType) {
val = o.(ListResult).BriefItems()
}
}
f.writeHeader(val)
f.writeValue(val)
f.writer.Flush()
}
}
func (f *Table) validate(val reflect.Type) bool {
if f.isSlice(val) {
return f.validateStruct(val.Elem())
} else if f.isStruct(val) {
return f.validateStruct(val)
} else {
return true
}
}
func (f *Table) validateStruct(val reflect.Type) bool {
for i := 0; i < val.NumField(); i++ {
structFieldType := val.Field(i).Type
if f.isStruct(structFieldType) {
return false
} else if f.isSlice(structFieldType) && !f.validateSlice(structFieldType.Elem()) {
return false
}
}
return true
}
func (f *Table) validateSlice(val reflect.Type) bool {
if f.isSimpleType(val) {
return true
} else if f.isStruct(val) {
for i := 0; i < val.NumField(); i++ {
structFieldType := val.Field(i).Type
if !f.isSimpleType(structFieldType) {
return false
}
}
}
return true
}
func (f *Table) writeHeader(value reflect.Value) {
f.write(strings.Join(f.getHeader(noPrefix, value.Type()), tableDelimiter))
f.newLine()
}
func (f *Table) getHeader(prefix string, value reflect.Type) []string {
if f.isStruct(value) {
headers := f.getStructHeaders(prefix, value)
return headers
} else if f.isSlice(value) {
headers := f.getHeader(prefix, value.Elem())
return headers
} else {
return []string{prefix + value.Name()}
}
}
func (f *Table) getStructHeaders(prefix string, val reflect.Type) []string {
var headers []string
for i := 0; i < val.NumField(); i++ {
structFieldType := val.Field(i).Type
if f.isSlice(structFieldType) {
headers = append(headers, f.getHeader(prefix+val.Field(i).Name+prefixNameDivider, val.Field(i).Type.Elem())...)
} else {
headers = append(headers, prefix+val.Field(i).Name)
}
}
return headers
}
func (f *Table) writeValue(value reflect.Value) {
val := reflect.Indirect(value)
if f.isSimpleType(value.Type()) {
f.write(fmt.Sprint(value))
} else if f.isStruct(value.Type()) {
maxCollectionSize := f.getMaxSliceSize(value)
if maxCollectionSize == 0 {
f.write(f.getSimpleStructOutput(val))
} else {
f.write(f.getSliceOutput(maxCollectionSize, value))
}
} else {
f.writeCollection(value)
}
}
func (f *Table) writeCollection(value reflect.Value) {
for i := 0; i < value.Len(); i++ {
maxCollectionSize := f.getMaxSliceSize(value.Index(i))
if maxCollectionSize == 0 {
f.write(f.getSimpleStructOutput(value.Index(i)))
} else {
f.write(f.getSliceOutput(maxCollectionSize, value.Index(i)))
}
f.newLine()
}
}
func (f *Table) getMaxSliceSize(value reflect.Value) int {
maxSize := 0
for i := 0; i < value.Type().NumField(); i++ {
field := value.Field(i)
if f.isSlice(field.Type()) && field.Len() > maxSize {
maxSize = field.Len()
}
}
return maxSize
}
func (f *Table) getSliceOutput(maxCollectionSize int, value reflect.Value) string {
val := reflect.Indirect(value)
var outputRowList []string
output := ""
for rowIndex := 0; rowIndex < maxCollectionSize; rowIndex++ {
for structIndex := 0; structIndex < val.Type().NumField(); structIndex++ {
structField := val.Field(structIndex)
if f.isSimpleType(structField.Type()) {
outputRowList = f.appendSimpleValue(structField, outputRowList, rowIndex)
} else if f.isStruct(structField.Type().Elem()) {
outputRowList = f.appendSliceStructRow(structField, outputRowList, rowIndex)
} else if f.isSlice(structField.Type()) {
outputRowList = f.appendSimpleSlice(structField, outputRowList, rowIndex)
} else {
outputRowList = append(outputRowList, fmt.Sprint(structField))
}
}
output += strings.Join(outputRowList, tableDelimiter) + "\n"
outputRowList = outputRowList[:0]
}
return output
}
func (f *Table) appendSliceStructRow(field reflect.Value, outputList []string, row int) []string {
for k := 0; k < field.Type().Elem().NumField(); k++ {
if row >= field.Len() {
outputList = append(outputList, emptyVal)
} else {
outputList = append(outputList, fmt.Sprint(field.Index(row).Field(k)))
}
}
return outputList
}
func (f *Table) appendSimpleSlice(field reflect.Value, outputList []string, row int) []string {
if row >= field.Len() {
outputList = append(outputList, emptyVal)
} else {
outputList = append(outputList, fmt.Sprint(field.Index(row)))
}
return outputList
}
func (f *Table) appendSimpleValue(field reflect.Value, outputList []string, row int) []string {
if row > 0 {
outputList = append(outputList, emptyVal)
} else {
outputList = append(outputList, fmt.Sprint(field))
}
return outputList
}
func (f *Table) getSimpleStructOutput(val reflect.Value) string {
var outputList []string
for i := 0; i < val.Type().NumField(); i++ {
outputList = append(outputList, fmt.Sprint(val.Field(i)))
}
return strings.Join(outputList, tableDelimiter)
}
func (f *Table) isSimpleType(value reflect.Type) bool {
return !(value.Kind() == reflect.Slice || value.Kind() == reflect.Struct)
}
func (f *Table) isSlice(value reflect.Type) bool {
return value.Kind() == reflect.Slice
}
func (f *Table) isStruct(value reflect.Type) bool {
return value.Kind() == reflect.Struct
}
func (f *Table) write(v interface{}) {
_, _ = fmt.Fprint(&f.writer, v)
}
func (f *Table) newLine() {
_, _ = fmt.Fprintln(&f.writer)
}

View File

@ -0,0 +1,95 @@
package formatter
import (
"fmt"
"io"
"reflect"
"sort"
"strings"
)
const delimiter = "\t"
type Text struct {
writer io.Writer
}
func (f *Text) Write(o interface{}) {
f.writeValue(reflect.ValueOf(o))
}
func (f *Text) writeValue(value reflect.Value) {
switch value.Kind() {
case reflect.Struct:
f.writeStruct(value)
case reflect.Ptr:
f.writeValue(value.Elem())
case reflect.Slice:
f.writeSliceValue(value)
default:
f.writeOther(value)
}
}
func (f *Text) writeOther(value reflect.Value) {
f.write(value)
f.newLine()
}
func (f *Text) writeSliceValue(sliceValue reflect.Value) {
for i := 0; i < sliceValue.Len(); i++ {
itemValue := sliceValue.Index(i)
f.writeValue(itemValue)
}
}
func (f *Text) writeStruct(value reflect.Value) {
f.writeSimpleFields(value)
f.writeCompoundFields(value)
}
func (f *Text) writeSimpleFields(value reflect.Value) {
id := strings.ToUpper(value.Type().Name())
f.write(id)
fieldNames := getFieldNames(value)
for _, fieldName := range fieldNames {
field := value.FieldByName(fieldName)
if isSimpleType(field) {
f.write(delimiter)
f.write(fmt.Sprint(field))
}
}
f.newLine()
}
func isSimpleType(value reflect.Value) bool {
return !(value.Kind() == reflect.Struct || value.Kind() == reflect.Slice)
}
func (f *Text) write(v interface{}) {
_, _ = fmt.Fprint(f.writer, v)
}
func (f *Text) newLine() {
_, _ = fmt.Fprintln(f.writer)
}
func getFieldNames(value reflect.Value) []string {
var names []string
valueType := value.Type()
for i := 0; i < value.NumField(); i++ {
names = append(names, valueType.Field(i).Name)
}
sort.Strings(names)
return names
}
func (f *Text) writeCompoundFields(value reflect.Value) {
fieldNames := getFieldNames(value)
for _, fieldName := range fieldNames {
field := value.FieldByName(fieldName)
if !isSimpleType(field) {
f.writeValue(field)
}
}
}

View File

@ -0,0 +1,32 @@
package formatter
import (
"bytes"
"fmt"
"io"
"gopkg.in/yaml.v3"
)
type Yaml struct {
writer io.Writer
}
func (f *Yaml) write(bytes []byte) {
_, _ = fmt.Fprint(f.writer, string(bytes))
}
func (f *Yaml) newLine() {
_, _ = fmt.Fprintln(f.writer)
}
func (f *Yaml) Write(o interface{}) {
var b bytes.Buffer
yamlEncoder := yaml.NewEncoder(&b)
yamlEncoder.SetIndent(2)
_ = yamlEncoder.Encode(o)
f.write(b.Bytes())
f.newLine()
}

View File

@ -0,0 +1,81 @@
package utils
import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"github.com/manifoldco/promptui"
)
const (
DefaultErrorExitCode = 1
)
var fatalErrHandler = fatal
var PromptManualExitSignal = fmt.Errorf("prompt")
func fatal(msg string, code int) {
if len(msg) > 0 {
// add newline if needed
if !strings.HasSuffix(msg, "\n") {
msg += "\n"
}
fmt.Fprint(os.Stderr, msg)
}
os.Exit(code)
}
var ErrExit = fmt.Errorf("exit")
func CheckErr(err error) {
checkErr(err, fatalErrHandler)
}
func checkErr(err error, handleErr func(string, int)) {
if err == nil {
return
}
switch {
case err == ErrExit:
handleErr("", DefaultErrorExitCode)
case err == PromptManualExitSignal:
handleErr("Prompt exit", DefaultErrorExitCode)
default:
msg, ok := StandardErrorMessage(err)
if !ok {
msg = err.Error()
if !strings.HasPrefix(msg, "error: ") {
msg = fmt.Sprintf("error: %s", msg)
}
}
handleErr(msg, DefaultErrorExitCode)
}
}
func StandardErrorMessage(err error) (string, bool) {
switch t := err.(type) {
case *url.Error:
switch {
case strings.Contains(t.Err.Error(), "connection refused"):
host := t.URL
if server, err := url.Parse(t.URL); err == nil {
host = server.Host
}
return fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host), true
}
return fmt.Sprintf("Unable to connect to the server: %v", t.Err), true
}
return "", false
}
func HandlePromptError(err error) error {
if errors.Is(err, promptui.ErrInterrupt) || errors.Is(err, promptui.ErrAbort) {
return PromptManualExitSignal
}
return fmt.Errorf("Prompt failed %v\n", err)
}

View File

@ -0,0 +1,101 @@
package prompt
import (
"fmt"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/Bio-OS/bioos/internal/bioctl/utils"
)
type PromptOptions struct {
message string
}
type PromptOption func(o *PromptOptions)
func WithInputMessage(message string) PromptOption {
return func(o *PromptOptions) {
o.message = message
}
}
func PromptRequiredString(name string, opts ...PromptOption) (string, error) {
return PromptStringWithValidator(name, survey.Required, opts...)
}
func PromptOptionalString(name string, opts ...PromptOption) (string, error) {
return PromptStringWithValidator(name, OptionalInput, opts...)
}
func PromptStringWithValidator(name string, validator survey.Validator, opts ...PromptOption) (string, error) {
res := ""
msg := name
options := &PromptOptions{}
for _, opt := range opts {
opt(options)
}
if options.message != "" {
msg = fmt.Sprintf("%s (%s)", name, options.message)
}
prompt := &survey.Input{
Message: msg,
}
if err := survey.AskOne(prompt, &res, survey.WithValidator(validator), survey.WithIcons(func(icons *survey.IconSet) {
// you can set any icons
icons.Question.Text = SELECTICON
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
//icons.Question.Format = "yellow+hb"
icons.Question.Format = "cyan"
})); err != nil {
fmt.Println(err)
return "", utils.HandlePromptError(err)
}
return res, nil
}
// PromptStringSlice prompt for string slice
func PromptStringSlice(label string) ([]string, error) {
res := make([]string, 0)
var result string
var err error
for result != "quit" {
result, err = PromptRequiredString(fmt.Sprintf("%s ('quit' to exit)", label))
if err != nil {
return nil, err
}
if strings.ToLower(result) != "quit" {
res = append(res, result)
}
}
return res, nil
}
func PromptRequiredInt32(name string, opts ...PromptOption) (int32, error) {
var res int32 = 0
msg := name
options := &PromptOptions{}
for _, opt := range opts {
opt(options)
}
if options.message != "" {
msg = options.message
}
prompt := &survey.Input{
Message: msg,
}
if err := survey.AskOne(prompt, &res, survey.WithValidator(ValidateIntegerNumberInput), survey.WithIcons(func(icons *survey.IconSet) {
// you can set any icons
icons.Question.Text = SELECTICON
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
//icons.Question.Format = "yellow+hb"
icons.Question.Format = "cyan"
})); err != nil {
return 0, utils.HandlePromptError(err)
}
return res, nil
}

Some files were not shown because too many files have changed in this diff Show More