Skip to content

Commit 14859fa

Browse files
committed
feat: add installation script and improve CLI usability
- Add interactive install script (scripts/install.sh) with version selection - Add resource aliases to all commands (create, delete, set) - Update README with simplified installation instructions - Fix HAMi gpumem=0 issue by setting minimal GPU memory (1 MiB) for edit sessions - Enhance CI workflow to run on push/PR and handle non-tag builds
1 parent bf32bdd commit 14859fa

14 files changed

Lines changed: 841 additions & 268 deletions

File tree

.github/workflows/release.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
name: Release
1+
name: CI/Release
22

33
on:
44
push:
5+
branches: [main]
56
tags:
67
- 'v*'
8+
pull_request:
9+
branches: [main]
710

811
permissions:
912
contents: write
@@ -33,17 +36,25 @@ jobs:
3336
with:
3437
go-version: '1.25'
3538

39+
- name: Set version
40+
id: version
41+
run: |
42+
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
43+
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
44+
else
45+
echo "version=dev-${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
46+
fi
47+
3648
- name: Build
3749
env:
3850
GOOS: ${{ matrix.goos }}
3951
GOARCH: ${{ matrix.goarch }}
4052
run: |
41-
VERSION=${GITHUB_REF#refs/tags/}
4253
OUTPUT=sgs-${{ matrix.goos }}-${{ matrix.goarch }}
4354
if [ "${{ matrix.goos }}" = "windows" ]; then
4455
OUTPUT="${OUTPUT}.exe"
4556
fi
46-
go build -ldflags "-X github.com/bacchus-snu/sgs-cli/internal/sgs.Version=${VERSION}" \
57+
go build -ldflags "-X github.com/bacchus-snu/sgs-cli/internal/sgs.Version=${{ steps.version.outputs.version }}" \
4758
-o ${OUTPUT} ./cmd/sgs
4859
4960
- name: Upload artifact
@@ -55,6 +66,7 @@ jobs:
5566
release:
5667
needs: build
5768
runs-on: ubuntu-latest
69+
if: startsWith(github.ref, 'refs/tags/')
5870
steps:
5971
- name: Download all artifacts
6072
uses: actions/download-artifact@v4

README.md

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -51,44 +51,39 @@ The configuration is automatically refreshed if more than 24 hours have passed s
5151

5252
## Installation
5353

54-
### Download Binary
55-
56-
Download the latest binary for your platform from [GitHub Releases](https://github.com/bacchus-snu/sgs-cli/releases):
54+
### Quick Install
5755

5856
```bash
59-
# Linux (amd64)
60-
curl -LO https://github.com/bacchus-snu/sgs-cli/releases/latest/download/sgs-linux-amd64
61-
chmod +x sgs-linux-amd64
62-
sudo mv sgs-linux-amd64 /usr/local/bin/sgs
63-
64-
# macOS (Apple Silicon)
65-
curl -LO https://github.com/bacchus-snu/sgs-cli/releases/latest/download/sgs-darwin-arm64
66-
chmod +x sgs-darwin-arm64
67-
sudo mv sgs-darwin-arm64 /usr/local/bin/sgs
68-
69-
# macOS (Intel)
70-
curl -LO https://github.com/bacchus-snu/sgs-cli/releases/latest/download/sgs-darwin-amd64
71-
chmod +x sgs-darwin-amd64
72-
sudo mv sgs-darwin-amd64 /usr/local/bin/sgs
57+
curl -fsSL https://raw.githubusercontent.com/bacchus-snu/sgs-cli/main/scripts/install.sh | sh
7358
```
7459

75-
### From Source
60+
Or with wget:
7661

7762
```bash
78-
git clone https://github.com/bacchus-snu/sgs-cli.git
79-
cd sgs-cli
80-
make build
63+
wget -qO- https://raw.githubusercontent.com/bacchus-snu/sgs-cli/main/scripts/install.sh | sh
8164
```
8265

83-
### Install to PATH
66+
The installer will:
67+
68+
- Auto-detect your OS and architecture
69+
- Show available versions to choose from
70+
- Let you select installation path (global `/usr/local/bin` or local `~/.local/bin`)
71+
72+
### Manual Download
73+
74+
Download binaries directly from [GitHub Releases](https://github.com/bacchus-snu/sgs-cli/releases).
75+
76+
### From Source
8477

8578
```bash
86-
make install
79+
git clone https://github.com/bacchus-snu/sgs-cli.git
80+
cd sgs-cli
81+
make build && make install
8782
```
8883

8984
### Auto-Update
9085

91-
When running `sgs fetch`, the CLI automatically checks for new versions and offers to update.
86+
The CLI automatically runs `sgs fetch` when any command is executed and the last fetch was more than 24 hours ago. This checks for new versions and offers to update.
9287

9388
## Build
9489

internal/cmd/attach.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import (
1212
)
1313

1414
var attachCmd = &cobra.Command{
15-
Use: "attach <node>/<volume>",
16-
Short: "Attach to an existing session",
15+
Use: "attach <node>/<volume>",
16+
Aliases: []string{"at"},
17+
Short: "Attach to an existing session (at)",
1718
Long: `Attach to an existing session (edit or run pod).
1819
1920
This command waits for the session pod to be ready and then attaches to it.

internal/cmd/cp.go

Lines changed: 85 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,61 @@
11
package cmd
22

33
import (
4+
"bufio"
45
"context"
56
"fmt"
7+
"os"
8+
"strings"
69

710
"github.com/bacchus-snu/sgs-cli/internal/cleanup"
811
"github.com/bacchus-snu/sgs-cli/internal/client"
912
"github.com/bacchus-snu/sgs-cli/internal/volume"
1013
"github.com/spf13/cobra"
1114
)
1215

16+
var cpForce bool
17+
1318
var cpCmd = &cobra.Command{
14-
Use: "cp <source-node>/<source-volume> <dest-node>/<dest-volume>",
15-
Short: "Copy a volume to another location",
16-
Long: `Copy the contents of a source volume to a new destination volume.
19+
Use: "cp <source> <destination>",
20+
Short: "Copy volumes or files/directories between volumes",
21+
Long: `Copy volumes or files/directories between volumes.
1722
18-
The destination volume will be created automatically with the same size and type
19-
as the source volume. If the source is an OS volume, the destination will also
20-
be marked as an OS volume.
23+
Volume copy (entire volume):
24+
sgs cp <node>/<volume> <node>/<volume>
25+
- Copies entire volume contents
26+
- Destination volume is created automatically
27+
- Requires confirmation (use --force to skip)
2128
22-
The source volume must not have an active session. The destination volume must
23-
not already exist.
29+
File/directory copy (specific paths):
30+
sgs cp <node>/<volume>:<path> <node>/<volume>:<path>
31+
- Copies specific files or directories within volumes
32+
- Both source and destination volumes must exist
33+
- For OS volumes, paths are relative to the rootfs (handled internally)
2434
25-
For same-node copies, this uses a single temporary pod that mounts both volumes.
26-
For cross-node copies, this streams data via tar between two temporary pods.
35+
Note: Source and destination must BOTH have paths (file/directory copy) or NEITHER have paths (volume copy).
2736
2837
Examples:
29-
# Copy a volume on the same node
30-
sgs cp ferrari/my-volume ferrari/my-volume-backup
38+
# Copy entire volume (creates destination)
39+
sgs cp ferrari/os-vol porsche/os-vol-backup
40+
41+
# Copy entire volume, skip confirmation
42+
sgs cp --force ferrari/os-vol porsche/os-vol-backup
43+
44+
# Copy specific directory between data volumes
45+
sgs cp ferrari/data:/datasets/mnist porsche/data:/datasets/mnist
3146
32-
# Copy a volume to a different node
33-
sgs cp ferrari/my-volume porsche/my-volume
47+
# Copy from OS volume to data volume
48+
sgs cp ferrari/os-vol:/home/user/code ferrari/data:/backup/code
3449
35-
# Copy an OS volume to create a fresh copy
36-
sgs cp bentley/os-volume ford/os-volume`,
50+
# Copy between different nodes
51+
sgs cp ferrari/data:/models porsche/data:/models`,
3752
Args: cobra.ExactArgs(2),
3853
Run: runCp,
3954
}
4055

4156
func init() {
4257
rootCmd.AddCommand(cpCmd)
58+
cpCmd.Flags().BoolVarP(&cpForce, "force", "f", false, "Skip confirmation prompt (volume copy only)")
4359
}
4460

4561
func runCp(cmd *cobra.Command, args []string) {
@@ -49,31 +65,73 @@ func runCp(cmd *cobra.Command, args []string) {
4965
ctx, cancel := cleanup.InterruptibleContext(context.Background())
5066
defer cancel()
5167

52-
// Parse source
53-
srcNode, srcVolume, err := volume.ParseVolumePath(args[0])
68+
// Parse source and destination using the parser that handles paths
69+
srcPath, err := volume.ParseCopyPath(args[0])
5470
if err != nil {
5571
exitWithError(fmt.Sprintf("invalid source: %s", args[0]), err)
5672
}
5773

58-
// Parse destination
59-
dstNode, dstVolume, err := volume.ParseVolumePath(args[1])
74+
dstPath, err := volume.ParseCopyPath(args[1])
6075
if err != nil {
6176
exitWithError(fmt.Sprintf("invalid destination: %s", args[1]), err)
6277
}
6378

79+
// Validate that both have paths or neither has paths
80+
srcHasPath := srcPath.IsFineGrained()
81+
dstHasPath := dstPath.IsFineGrained()
82+
83+
if srcHasPath != dstHasPath {
84+
exitWithError("invalid copy format: source and destination must both have paths (file/directory copy) or neither have paths (volume copy)", nil)
85+
}
86+
6487
k8sClient, err := client.New()
6588
if err != nil {
6689
exitWithError("failed to create client", err)
6790
}
6891

69-
fmt.Printf("Copying %s/%s to %s/%s...\n", srcNode, srcVolume, dstNode, dstVolume)
92+
opts := volume.CopyOptions{
93+
SrcNode: srcPath.NodeName,
94+
SrcVolume: srcPath.VolumeName,
95+
SrcPath: srcPath.Path,
96+
DstNode: dstPath.NodeName,
97+
DstVolume: dstPath.VolumeName,
98+
DstPath: dstPath.Path,
99+
}
100+
101+
if srcHasPath {
102+
// File/directory copy: both volumes must exist
103+
fmt.Printf("Copying %s/%s:%s to %s/%s:%s...\n",
104+
srcPath.NodeName, srcPath.VolumeName, srcPath.Path,
105+
dstPath.NodeName, dstPath.VolumeName, dstPath.Path)
106+
107+
err = volume.CopyFiles(ctx, k8sClient, opts)
108+
} else {
109+
// Volume copy: destination will be created
110+
srcVolPath := fmt.Sprintf("%s/%s", srcPath.NodeName, srcPath.VolumeName)
111+
dstVolPath := fmt.Sprintf("%s/%s", dstPath.NodeName, dstPath.VolumeName)
112+
113+
// Require confirmation unless --force is set
114+
if !cpForce {
115+
fmt.Printf("This will copy volume '%s' to new volume '%s'.\n", srcVolPath, dstVolPath)
116+
fmt.Printf("Type the destination volume path to confirm: ")
117+
118+
reader := bufio.NewReader(os.Stdin)
119+
input, err := reader.ReadString('\n')
120+
if err != nil {
121+
exitWithError("failed to read input", err)
122+
}
123+
124+
input = strings.TrimSpace(input)
125+
if input != dstVolPath {
126+
fmt.Println("Aborted: confirmation does not match")
127+
os.Exit(1)
128+
}
129+
}
130+
131+
fmt.Printf("Copying %s to %s...\n", srcVolPath, dstVolPath)
132+
err = volume.Copy(ctx, k8sClient, opts)
133+
}
70134

71-
err = volume.Copy(ctx, k8sClient, volume.CopyOptions{
72-
SrcNode: srcNode,
73-
SrcVolume: srcVolume,
74-
DstNode: dstNode,
75-
DstVolume: dstVolume,
76-
})
77135
if err != nil {
78136
// If context was cancelled (interrupt), signal handler already cleaned up and will exit
79137
// Just return to let it handle things

internal/cmd/create.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ var (
3030
)
3131

3232
var createCmd = &cobra.Command{
33-
Use: "create",
34-
Short: "Create a resource",
33+
Use: "create",
34+
Aliases: []string{"cr"},
35+
Short: "Create a resource (cr)",
3536
}
3637

3738
var createVolumeCmd = &cobra.Command{
3839
Use: "volume <node-name>/<volume-name>",
39-
Aliases: []string{"volumes"},
40-
Short: "Create a new volume",
40+
Aliases: []string{"volumes", "vo", "vol"},
41+
Short: "Create a new volume (vo, vol)",
4142
Long: `Create a new persistent volume on a specific node.
4243
4344
There are two types of volumes:
@@ -68,13 +69,13 @@ Examples:
6869

6970
var createSessionCmd = &cobra.Command{
7071
Use: "session <node>/<volume>",
71-
Aliases: []string{"sessions"},
72-
Short: "Create a session on an OS volume",
72+
Aliases: []string{"sessions", "se"},
73+
Short: "Create a session on an OS volume (se)",
7374
Long: `Create a session on an OS volume.
7475
7576
By default (or with --edit): Creates an edit session for interactive use.
7677
- Limited resources (4 CPU, 16GiB memory)
77-
- GPU memory set to 0 (HAMi: drivers accessible, CUDA blocked)
78+
- Minimal GPU memory (1 MiB) for nvidia-smi access, no CUDA compute
7879
- Use --attach to attach to shell immediately
7980
8081
With --run flag: Creates a run session for GPU workloads.
@@ -83,7 +84,7 @@ With --run flag: Creates a run session for GPU workloads.
8384
- CPU/memory automatically calculated based on GPU count
8485
- Use --pin-cpu and --pin-mem to pin resources
8586
86-
You can mount additional volumes using the --mount flag.
87+
You can mount volumes using the --mount flag (both OS and data volumes supported).
8788
8889
Examples:
8990
# Start an edit session

0 commit comments

Comments
 (0)