Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@ Flags:
-c, --cluster string set the cluster
-h, --help help for info

Global Flags:
--config string set the location of the config file (YAML or JSON)

##### status

Show the status reported by the OSCAR Manager API, including node metrics, deployment readiness and MinIO statistics.

```
Usage:
oscar-cli cluster status [flags]

Aliases:
status, s

Flags:
-c, --cluster string set the cluster
-o, --output string output format (yaml or json) (default "yaml")
-h, --help help for status

Global Flags:
--config string set the location of the config file (YAML or JSON)
```
Expand Down Expand Up @@ -214,7 +233,7 @@ Flags:
--owner string GitHub owner that hosts the curated services (default "grycap")
--path string subdirectory inside the repository that contains the services
--ref string Git reference (branch, tag, or commit) to query (default "main")
-n, --name string override the OSCAR service name during deployment
-n, --name string override the OSCAR service and primary bucket names during deployment
--repo string GitHub repository that hosts the curated services (default "oscar-hub")

Global Flags:
Expand Down
55 changes: 44 additions & 11 deletions cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,19 +194,19 @@ func overrideServiceName(svc *types.Service, newName string) {
if override == "" {
return
}
original := strings.TrimSpace(svc.Name)
if original == "" {
svc.Name = override
return
}
if original == override {
return
originalName := strings.TrimSpace(svc.Name)
primaryBucket := primaryBucketName(svc)
oldBucket := originalName
if primaryBucket != "" {
oldBucket = primaryBucket
}

renameStoragePaths(&svc.Input, original, override)
renameStoragePaths(&svc.Output, original, override)
if svc.Mount.Path != "" {
svc.Mount.Path = replacePathBucket(svc.Mount.Path, original, override)
if strings.TrimSpace(oldBucket) != "" && !strings.EqualFold(oldBucket, override) {
renameStoragePaths(&svc.Input, oldBucket, override)
renameStoragePaths(&svc.Output, oldBucket, override)
if svc.Mount.Path != "" {
svc.Mount.Path = replacePathBucket(svc.Mount.Path, oldBucket, override)
}
}

svc.Name = override
Expand Down Expand Up @@ -258,3 +258,36 @@ func replacePathBucket(path, oldName, newName string) string {
}
return builder
}

func primaryBucketName(svc *types.Service) string {
if svc == nil {
return ""
}
for _, cfg := range svc.Output {
if bucket := bucketFromPath(cfg.Path); bucket != "" {
return bucket
}
}
for _, cfg := range svc.Input {
if bucket := bucketFromPath(cfg.Path); bucket != "" {
return bucket
}
}
if bucket := bucketFromPath(svc.Mount.Path); bucket != "" {
return bucket
}
return ""
}

func bucketFromPath(path string) string {
if strings.TrimSpace(path) == "" {
return ""
}
trimmed := strings.Trim(path, " ")
trimmed = strings.Trim(trimmed, "/")
if trimmed == "" {
return ""
}
parts := strings.SplitN(trimmed, "/", 2)
return parts[0]
}
30 changes: 30 additions & 0 deletions cmd/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,36 @@ func TestOverrideServiceNameUpdatesPaths(t *testing.T) {
}
}

func TestOverrideServiceNameUpdatesPathsWhenBucketDiffersFromServiceName(t *testing.T) {
svc := &types.Service{
Name: "demo",
Input: []types.StorageIOConfig{
{Path: "workflow/in"},
{Path: "other/in"},
},
Output: []types.StorageIOConfig{{Path: "workflow"}},
Mount: types.StorageIOConfig{Path: "workflow/mount"},
}

overrideServiceName(svc, "demo-new")

if svc.Name != "demo-new" {
t.Fatalf("expected service name demo-new, got %s", svc.Name)
}
if got := svc.Input[0].Path; got != "demo-new/in" {
t.Fatalf("expected first input path demo-new/in, got %s", got)
}
if got := svc.Input[1].Path; got != "other/in" {
t.Fatalf("unexpected path rewrite: %s", got)
}
if got := svc.Output[0].Path; got != "demo-new" {
t.Fatalf("expected output path demo-new, got %s", got)
}
if got := svc.Mount.Path; got != "demo-new/mount" {
t.Fatalf("expected mount path demo-new/mount, got %s", got)
}
}

func TestReplacePathBucket(t *testing.T) {
cases := []struct {
name string
Expand Down
1 change: 1 addition & 0 deletions cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func makeClusterCmd() *cobra.Command {
clusterCmd.AddCommand(makeClusterAddCmd())
clusterCmd.AddCommand(makeClusterRemoveCmd())
clusterCmd.AddCommand(makeClusterInfoCmd())
clusterCmd.AddCommand(makeClusterStatusCmd())
clusterCmd.AddCommand(makeClusterListCmd())
clusterCmd.AddCommand(makeClusterDefaultCmd())

Expand Down
82 changes: 82 additions & 0 deletions cmd/cluster_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright (C) GRyCAP - I3M - UPV

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 cmd

import (
"encoding/json"
"fmt"

"github.com/goccy/go-yaml"
"github.com/grycap/oscar-cli/pkg/cluster"
"github.com/grycap/oscar-cli/pkg/config"
"github.com/spf13/cobra"
)

func clusterStatusFunc(cmd *cobra.Command, args []string) error {
conf, err := config.ReadConfig(configPath)
if err != nil {
return err
}

clusterName, err := getCluster(cmd, conf)
if err != nil {
return err
}

status, err := conf.Oscar[clusterName].GetClusterStatus()
if err != nil {
return err
}

output, _ := cmd.Flags().GetString("output")
switch output {
case "json":
if err := clusterInfoPrintJSON(cmd, status); err != nil {
return err
}
default:
outputyaml, err := yaml.Marshal(status)
if err != nil {
return fmt.Errorf("failed to serialize cluster status: %w", err)
}
fmt.Print(string(outputyaml))

}

return nil
}

func clusterInfoPrintJSON(cmd *cobra.Command, objects cluster.StatusInfo) error {
encoder := json.NewEncoder(cmd.OutOrStdout())
encoder.SetIndent("", " ")
return encoder.Encode(objects)
}

func makeClusterStatusCmd() *cobra.Command {
clusterStatusCmd := &cobra.Command{
Use: "status",
Short: "Show status information of an OSCAR cluster",
Args: cobra.NoArgs,
Aliases: []string{"s"},
RunE: clusterStatusFunc,
}

clusterStatusCmd.Flags().StringP("cluster", "c", "", "set the cluster")
clusterStatusCmd.Flags().StringP("output", "o", "table", "output format (json)")

return clusterStatusCmd
}
101 changes: 101 additions & 0 deletions cmd/cluster_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package cmd

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"

"github.com/grycap/oscar-cli/pkg/cluster"
)

func TestClusterStatusCommandPrintsStatus(t *testing.T) {
expected := cluster.StatusInfo{
Cluster: cluster.ClusterStatus{
NodesCount: 1,
Metrics: cluster.ClusterMetrics{
CPU: cluster.CPUMetrics{
TotalFreeCores: 2,
MaxFreeOnNodeCores: 2,
},
},
},
Oscar: cluster.OscarStatus{
DeploymentName: "oscar-manager",
Deployment: cluster.OscarDeployment{
CreationTimestamp: time.Unix(1700000000, 0).UTC(),
},
},
MinIO: cluster.MinioStatus{
BucketsCount: 4,
TotalObjects: 10,
},
}

const (
username = "user"
password = "pass"
)

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/system/status" {
http.NotFound(w, r)
return
}
gotUser, gotPass, ok := r.BasicAuth()
if !ok || gotUser != username || gotPass != password {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := json.NewEncoder(w).Encode(expected); err != nil {
t.Fatalf("encoding status: %v", err)
}
}))
defer server.Close()

configFile := writeConfigFile(t, "alpha", server.URL)

stdout, stderr, err := runCommand(t,
"cluster", "--config", configFile,
"status",
"--cluster", "alpha",
)
if err != nil {
t.Fatalf("cluster status command returned error: %v", err)
}
if stderr != "" {
t.Fatalf("expected empty stderr, got %q", stderr)
}
if !strings.Contains(stdout, "nodes_count: 1") {
t.Fatalf("expected nodes_count in output, got %q", stdout)
}
if !strings.Contains(stdout, "deployment_name: oscar-manager") {
t.Fatalf("expected deployment_name in output, got %q", stdout)
}
if !strings.Contains(stdout, "buckets_count: 4") {
t.Fatalf("expected buckets_count in output, got %q", stdout)
}
}

func TestClusterStatusCommandError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
}))
defer server.Close()

configFile := writeConfigFile(t, "alpha", server.URL)

_, _, err := runCommand(t,
"cluster", "--config", configFile,
"status",
"--cluster", "alpha",
)
if err == nil {
t.Fatalf("expected error, got nil")
}
if err.Error() != "boom\n" {
t.Fatalf("expected boom error, got %v", err)
}
}
4 changes: 2 additions & 2 deletions cmd/hub_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func hubDeployFunc(cmd *cobra.Command, args []string, opts *hubDeployOptions) er
}

if opts.name != "" {
serviceDef.Name = opts.name
overrideServiceName(serviceDef, opts.name)
}

action := "Creating"
Expand Down Expand Up @@ -128,7 +128,7 @@ func makeHubDeployCmd() *cobra.Command {
cmd.Flags().StringVar(&opts.rootPath, "path", opts.rootPath, "subdirectory inside the repository that contains the services")
cmd.Flags().StringVar(&opts.ref, "ref", opts.ref, "Git reference (branch, tag, or commit) to query")
cmd.Flags().StringVar(&opts.apiBase, "api-base", "", "override the GitHub API base URL")
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "override the OSCAR service name during deployment")
cmd.Flags().StringVarP(&opts.name, "name", "n", "", "override the OSCAR service and primary bucket names during deployment")
cmd.Flags().StringVar(&opts.localPath, "local-path", "", "use a local directory containing the RO-Crate metadata instead of fetching it from GitHub")
cmd.Flags().StringP("cluster", "c", "", "set the cluster")

Expand Down
24 changes: 24 additions & 0 deletions cmd/hub_deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ functions:
name: Cowsay
image: ghcr.io/demo/cowsay:latest
script: script.sh
input:
- storage_provider: minio.default
path: cowsay/in
output:
- storage_provider: minio.default
path: cowsay
mount:
storage_provider: minio.default
path: cowsay/mount
`
scriptContent = "#!/bin/bash\necho moo\n"
)
Expand Down Expand Up @@ -98,6 +107,21 @@ default: test
if applied.Name != override {
t.Fatalf("expected service name %s, got %s", override, applied.Name)
}
if len(applied.Input) != 1 {
t.Fatalf("expected 1 input entry, got %d", len(applied.Input))
}
if got := applied.Input[0].Path; got != "alt-cowsay/in" {
t.Fatalf("expected input path alt-cowsay/in, got %s", got)
}
if len(applied.Output) != 1 {
t.Fatalf("expected 1 output entry, got %d", len(applied.Output))
}
if got := applied.Output[0].Path; got != "alt-cowsay" {
t.Fatalf("expected output path alt-cowsay, got %s", got)
}
if got := applied.Mount.Path; got != "alt-cowsay/mount" {
t.Fatalf("expected mount path alt-cowsay/mount, got %s", got)
}
if applied.Script != scriptContent {
t.Fatalf("expected script content %q, got %q", scriptContent, applied.Script)
}
Expand Down
Loading