Skip to content

Commit 69eb68d

Browse files
oscar-cli version 2.1.0 (#99)
* fix: include bucket_name in UpdateBucket payload * feat: decode service run output (#97) * feat: decode service run output * docs: document decoded service run output * feat: add federation commands (#87) * Update service lib (#98) * feat: add federation commands * update oscar-cli to service version of 4.1.0 --------- Co-authored-by: Germán Moltó <gmolto@dsic.upv.es>
1 parent 3e9a914 commit 69eb68d

8 files changed

Lines changed: 289 additions & 122 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ Aliases:
342342

343343
Flags:
344344
-c, --cluster string set the cluster
345+
--decode-output decode the last base64-encoded line in the response and ignore logs
345346
-e, --endpoint string endpoint of a non registered cluster
346347
-f, --file-input string input file for the request
347348
-h, --help help for run

cmd/service_run.go

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"io"
2525
"io/ioutil"
2626
"os"
27+
"strings"
2728

2829
"github.com/grycap/oscar-cli/v2/pkg/config"
2930
"github.com/grycap/oscar-cli/v2/pkg/service"
@@ -58,6 +59,7 @@ func serviceRunFunc(cmd *cobra.Command, args []string) error {
5859
inputFile, _ := cmd.Flags().GetString("file-input")
5960
textInput, _ := cmd.Flags().GetString("text-input")
6061
outputFile, _ := cmd.Flags().GetString("output")
62+
decodeOutput, _ := cmd.Flags().GetBool("decode-output")
6163
if inputFile == "" && textInput == "" {
6264
return errors.New("you must specify \"--file-input\" or \"--text-input\" flag")
6365
}
@@ -110,28 +112,28 @@ func serviceRunFunc(cmd *cobra.Command, args []string) error {
110112
return errors.New("unable to copy the response")
111113
}
112114

113-
// Decode the result body
114-
tmpfile.Seek(0, 0)
115-
decoder := base64.NewDecoder(base64.StdEncoding, tmpfile)
116-
117-
// Parse output (store file if --output is set)
118-
var out *os.File
119-
120-
if outputFile != "" {
121-
// Create the file if --output is set
122-
out, err = os.Create(outputFile)
115+
if decodeOutput {
116+
tmpfile.Seek(0, 0)
117+
response, err := io.ReadAll(tmpfile)
123118
if err != nil {
124-
return fmt.Errorf("unable to create the file \"%s\"", outputFile)
119+
return errors.New("unable to read the response")
125120
}
126-
} else {
127-
// Create a temporary file
128-
out, err = ioutil.TempFile("", "")
121+
decoded, err := decodeLastBase64Line(response)
129122
if err != nil {
130-
return errors.New("unable to create a temporary file to decode the result")
123+
return err
131124
}
132-
defer os.Remove(out.Name())
125+
return writeServiceRunOutput(outputFile, bytes.NewReader(decoded))
126+
}
127+
128+
// Decode the result body
129+
tmpfile.Seek(0, 0)
130+
decoder := base64.NewDecoder(base64.StdEncoding, tmpfile)
131+
132+
out, err := createServiceRunOutput(outputFile)
133+
if err != nil {
134+
return err
133135
}
134-
defer out.Close()
136+
defer closeServiceRunOutput(out, outputFile)
135137

136138
// Copy the decoder stream into out
137139
_, err = io.Copy(out, decoder)
@@ -158,6 +160,68 @@ func serviceRunFunc(cmd *cobra.Command, args []string) error {
158160
return nil
159161
}
160162

163+
func decodeLastBase64Line(response []byte) ([]byte, error) {
164+
lines := strings.Split(string(response), "\n")
165+
decoder := base64.StdEncoding.Strict()
166+
167+
for i := len(lines) - 1; i >= 0; i-- {
168+
line := strings.TrimSpace(lines[i])
169+
if line == "" {
170+
continue
171+
}
172+
decoded, err := decoder.DecodeString(line)
173+
if err == nil {
174+
return decoded, nil
175+
}
176+
}
177+
178+
return nil, errors.New("unable to find base64-encoded output in the response")
179+
}
180+
181+
func writeServiceRunOutput(outputFile string, input io.Reader) error {
182+
out, err := createServiceRunOutput(outputFile)
183+
if err != nil {
184+
return err
185+
}
186+
defer closeServiceRunOutput(out, outputFile)
187+
188+
if _, err := io.Copy(out, input); err != nil {
189+
return errors.New("unable to copy the response")
190+
}
191+
192+
if outputFile == "" {
193+
out.Seek(0, 0)
194+
if _, err := io.Copy(os.Stdout, out); err != nil {
195+
return errors.New("unable to print the result")
196+
}
197+
}
198+
199+
return nil
200+
}
201+
202+
func createServiceRunOutput(outputFile string) (*os.File, error) {
203+
if outputFile != "" {
204+
out, err := os.Create(outputFile)
205+
if err != nil {
206+
return nil, fmt.Errorf("unable to create the file \"%s\"", outputFile)
207+
}
208+
return out, nil
209+
}
210+
211+
out, err := ioutil.TempFile("", "")
212+
if err != nil {
213+
return nil, errors.New("unable to create a temporary file to decode the result")
214+
}
215+
return out, nil
216+
}
217+
218+
func closeServiceRunOutput(out *os.File, outputFile string) {
219+
if outputFile == "" {
220+
os.Remove(out.Name())
221+
}
222+
out.Close()
223+
}
224+
161225
func makeServiceRunCmd() *cobra.Command {
162226
serviceRunCmd := &cobra.Command{
163227
Use: "run SERVICE_NAME {--file-input | --text-input}",
@@ -173,6 +237,7 @@ func makeServiceRunCmd() *cobra.Command {
173237
serviceRunCmd.Flags().StringP("file-input", "f", "", "input file for the request")
174238
serviceRunCmd.Flags().StringP("text-input", "i", "", "text input string for the request")
175239
serviceRunCmd.Flags().StringP("output", "o", "", "file path to store the output")
240+
serviceRunCmd.Flags().Bool("decode-output", false, "decode the last base64-encoded line in the response and ignore logs")
176241

177242
return serviceRunCmd
178243
}

cmd/service_run_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,120 @@ func TestServiceRunCommandFileInput(t *testing.T) {
148148
}
149149
}
150150

151+
func TestServiceRunCommandDecodeOutputIgnoresLogs(t *testing.T) {
152+
const (
153+
clusterName = "run-decode-cluster"
154+
serviceName = "decoder"
155+
serviceToken = "decode-token"
156+
payload = "ping"
157+
expected = "decoded result\nwith multiple lines\n"
158+
)
159+
160+
response := strings.Join([]string{
161+
"2026-05-28 06:38:44,746 - supervisor - INFO - Reading storage configuration",
162+
"2026-05-28 06:38:55,157 - supervisor - INFO - Creating response",
163+
base64.StdEncoding.EncodeToString([]byte(expected)),
164+
"",
165+
}, "\n")
166+
167+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168+
switch {
169+
case r.Method == http.MethodGet && r.URL.Path == "/system/services/"+serviceName:
170+
w.Header().Set("Content-Type", "application/json")
171+
if err := json.NewEncoder(w).Encode(&types.Service{
172+
Name: serviceName,
173+
Token: serviceToken,
174+
}); err != nil {
175+
t.Fatalf("encoding service response: %v", err)
176+
}
177+
case r.Method == http.MethodPost && r.URL.Path == "/run/"+serviceName:
178+
w.WriteHeader(http.StatusOK)
179+
fmt.Fprint(w, response)
180+
default:
181+
http.NotFound(w, r)
182+
}
183+
}))
184+
defer server.Close()
185+
186+
configFile := writeConfigFile(t, clusterName, server.URL)
187+
outputFile := filepath.Join(t.TempDir(), "result.txt")
188+
189+
stdout, stderr, err := runCommand(t,
190+
"service", "--config", configFile,
191+
"run", serviceName,
192+
"--cluster", clusterName,
193+
"--text-input", payload,
194+
"--output", outputFile,
195+
"--decode-output",
196+
)
197+
if err != nil {
198+
t.Fatalf("service run command returned error: %v", err)
199+
}
200+
if stdout != "" {
201+
t.Fatalf("expected empty stdout when output file is set, got %q", stdout)
202+
}
203+
if stderr != "" {
204+
t.Fatalf("expected empty stderr, got %q", stderr)
205+
}
206+
207+
content, err := os.ReadFile(outputFile)
208+
if err != nil {
209+
t.Fatalf("reading output file: %v", err)
210+
}
211+
if string(content) != expected {
212+
t.Fatalf("expected decoded output %q, got %q", expected, content)
213+
}
214+
}
215+
216+
func TestServiceRunCommandWithoutDecodeOutputKeepsRawResponseWithLogs(t *testing.T) {
217+
const (
218+
clusterName = "run-raw-cluster"
219+
serviceName = "raw"
220+
serviceToken = "raw-token"
221+
payload = "ping"
222+
expected = "decoded result"
223+
)
224+
225+
response := "log line\n" + base64.StdEncoding.EncodeToString([]byte(expected)) + "\n"
226+
227+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
228+
switch {
229+
case r.Method == http.MethodGet && r.URL.Path == "/system/services/"+serviceName:
230+
w.Header().Set("Content-Type", "application/json")
231+
if err := json.NewEncoder(w).Encode(&types.Service{
232+
Name: serviceName,
233+
Token: serviceToken,
234+
}); err != nil {
235+
t.Fatalf("encoding service response: %v", err)
236+
}
237+
case r.Method == http.MethodPost && r.URL.Path == "/run/"+serviceName:
238+
w.WriteHeader(http.StatusOK)
239+
fmt.Fprint(w, response)
240+
default:
241+
http.NotFound(w, r)
242+
}
243+
}))
244+
defer server.Close()
245+
246+
configFile := writeConfigFile(t, clusterName, server.URL)
247+
248+
stdout, stderr, err := runCommand(t,
249+
"service", "--config", configFile,
250+
"run", serviceName,
251+
"--cluster", clusterName,
252+
"--text-input", payload,
253+
)
254+
if err != nil {
255+
t.Fatalf("service run command returned error: %v", err)
256+
}
257+
if stderr != "" {
258+
t.Fatalf("expected empty stderr, got %q", stderr)
259+
}
260+
if stdout != response {
261+
t.Fatalf("expected raw response %q, got %q", response, stdout)
262+
}
263+
}
264+
151265
func TestServiceRunCommandInputValidation(t *testing.T) {
152266
const clusterName = "run-validate-cluster"
153267

go.mod

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ require (
1111
github.com/json-iterator/go v1.1.12 // indirect
1212
github.com/mattn/go-isatty v0.0.20 // indirect
1313
github.com/schollz/progressbar/v3 v3.13.1
14-
github.com/spf13/cobra v1.10.1
15-
golang.org/x/net v0.48.0 // indirect
16-
golang.org/x/sys v0.39.0 // indirect
17-
golang.org/x/text v0.32.0 // indirect
18-
k8s.io/api v0.34.2 // indirect
19-
k8s.io/klog/v2 v2.130.1 // indirect
14+
github.com/spf13/cobra v1.10.2
15+
golang.org/x/net v0.51.0 // indirect
16+
golang.org/x/sys v0.42.0 // indirect
17+
golang.org/x/text v0.34.0 // indirect
18+
k8s.io/api v0.35.3 // indirect
19+
k8s.io/klog/v2 v2.140.0 // indirect
2020
)
2121

2222
require (
2323
github.com/gdamore/tcell/v2 v2.8.1
24-
github.com/golang-jwt/jwt/v5 v5.2.2
25-
github.com/grycap/oscar/v4 v4.0.0
24+
github.com/golang-jwt/jwt/v5 v5.3.0
25+
github.com/grycap/oscar/v4 v4.1.0
2626
github.com/indigo-dc/liboidcagent-go v0.3.0
2727
github.com/rivo/tview v0.42.0
28-
golang.org/x/term v0.38.0
28+
golang.org/x/term v0.40.0
2929
gopkg.in/yaml.v3 v3.0.1
3030
)
3131

@@ -35,8 +35,8 @@ require (
3535
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
3636
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
3737
github.com/gdamore/encoding v1.0.1 // indirect
38-
github.com/go-openapi/jsonpointer v0.22.3 // indirect
39-
github.com/go-openapi/jsonreference v0.21.3 // indirect
38+
github.com/go-openapi/jsonpointer v0.22.4 // indirect
39+
github.com/go-openapi/jsonreference v0.21.4 // indirect
4040
github.com/go-openapi/swag v0.25.4 // indirect
4141
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
4242
github.com/go-openapi/swag/conv v0.25.4 // indirect
@@ -49,7 +49,6 @@ require (
4949
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
5050
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
5151
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
52-
github.com/gogo/protobuf v1.3.2 // indirect
5352
github.com/google/gnostic-models v0.7.1 // indirect
5453
github.com/google/uuid v1.6.0 // indirect
5554
github.com/grycap/cdmi-client-go v0.1.1 // indirect
@@ -65,25 +64,25 @@ require (
6564
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
6665
github.com/rivo/uniseg v0.4.7 // indirect
6766
github.com/spf13/pflag v1.0.10 // indirect
68-
github.com/stretchr/testify v1.11.1 // indirect
6967
github.com/x448/float16 v0.8.4 // indirect
7068
go.yaml.in/yaml/v2 v2.4.3 // indirect
7169
go.yaml.in/yaml/v3 v3.0.4 // indirect
72-
golang.org/x/crypto v0.46.0 // indirect
73-
golang.org/x/oauth2 v0.34.0 // indirect
70+
golang.org/x/crypto v0.48.0 // indirect
71+
golang.org/x/oauth2 v0.35.0 // indirect
7472
golang.org/x/time v0.14.0 // indirect
7573
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
76-
google.golang.org/protobuf v1.36.10 // indirect
74+
google.golang.org/protobuf v1.36.11 // indirect
7775
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
7876
gopkg.in/inf.v0 v0.9.1 // indirect
79-
k8s.io/apimachinery v0.34.2 // indirect
80-
k8s.io/client-go v0.34.2 // indirect
81-
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
82-
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
83-
sigs.k8s.io/controller-runtime v0.22.4 // indirect
77+
k8s.io/apimachinery v0.35.3 // indirect
78+
k8s.io/client-go v0.35.3 // indirect
79+
k8s.io/component-helpers v0.35.3 // indirect
80+
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
81+
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
82+
sigs.k8s.io/controller-runtime v0.23.3 // indirect
8483
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
85-
sigs.k8s.io/kueue v0.15.0 // indirect
84+
sigs.k8s.io/kueue v0.17.2 // indirect
8685
sigs.k8s.io/randfill v1.0.0 // indirect
87-
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
86+
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
8887
sigs.k8s.io/yaml v1.6.0 // indirect
8988
)

0 commit comments

Comments
 (0)