Skip to content
This repository was archived by the owner on Apr 7, 2024. It is now read-only.

Commit eb1a970

Browse files
feat: support trace with executables (#81)
Resolves #63 Signed-off-by: Xiaoxuan Wang <wangxiaoxuan119@gmail.com>
1 parent aff0773 commit eb1a970

3 files changed

Lines changed: 304 additions & 1 deletion

File tree

internal/executer/executer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"io"
2525
"os"
2626
"os/exec"
27+
28+
"github.com/oras-project/oras-credentials-go/trace"
2729
)
2830

2931
// dockerDesktopHelperName is the name of the docker credentials helper
@@ -52,7 +54,14 @@ func (c *executable) Execute(ctx context.Context, input io.Reader, action string
5254
cmd := exec.CommandContext(ctx, c.name, action)
5355
cmd.Stdin = input
5456
cmd.Stderr = os.Stderr
57+
trace := trace.ContextExecutableTrace(ctx)
58+
if trace != nil && trace.ExecuteStart != nil {
59+
trace.ExecuteStart(c.name, action)
60+
}
5561
output, err := cmd.Output()
62+
if trace != nil && trace.ExecuteDone != nil {
63+
trace.ExecuteDone(c.name, action, err)
64+
}
5665
if err != nil {
5766
switch execErr := err.(type) {
5867
case *exec.ExitError:

native_store_test.go

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,25 @@ limitations under the License.
1616
package credentials
1717

1818
import (
19+
"bytes"
1920
"context"
2021
"encoding/json"
2122
"fmt"
2223
"io"
2324
"strings"
2425
"testing"
2526

27+
"github.com/oras-project/oras-credentials-go/trace"
2628
"oras.land/oras-go/v2/registry/remote/auth"
2729
)
2830

2931
const (
3032
basicAuthHost = "localhost:2333"
31-
bearerAuthHost = "localhost:6666"
33+
bearerAuthHost = "localhost:666"
3234
exeErrorHost = "localhost:500/exeError"
3335
jsonErrorHost = "localhost:500/jsonError"
3436
noCredentialsHost = "localhost:404"
37+
traceHost = "localhost:808"
3538
testUsername = "test_username"
3639
testPassword = "test_password"
3740
testRefreshToken = "test_token"
@@ -69,6 +72,17 @@ func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action stri
6972
return []byte("json.Unmarshal failed"), nil
7073
case noCredentialsHost:
7174
return []byte("credentials not found"), errCredentialsNotFound
75+
case traceHost:
76+
traceHook := trace.ContextExecutableTrace(ctx)
77+
if traceHook != nil {
78+
if traceHook.ExecuteStart != nil {
79+
traceHook.ExecuteStart("testExecuter", "get")
80+
}
81+
if traceHook.ExecuteDone != nil {
82+
traceHook.ExecuteDone("testExecuter", "get", nil)
83+
}
84+
}
85+
return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
7286
default:
7387
return []byte("program failed"), errCommandExited
7488
}
@@ -81,13 +95,35 @@ func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action stri
8195
switch c.ServerURL {
8296
case basicAuthHost, bearerAuthHost, exeErrorHost:
8397
return nil, nil
98+
case traceHost:
99+
traceHook := trace.ContextExecutableTrace(ctx)
100+
if traceHook != nil {
101+
if traceHook.ExecuteStart != nil {
102+
traceHook.ExecuteStart("testExecuter", "store")
103+
}
104+
if traceHook.ExecuteDone != nil {
105+
traceHook.ExecuteDone("testExecuter", "store", nil)
106+
}
107+
}
108+
return nil, nil
84109
default:
85110
return []byte("program failed"), errCommandExited
86111
}
87112
case "erase":
88113
switch inS {
89114
case basicAuthHost, bearerAuthHost:
90115
return nil, nil
116+
case traceHost:
117+
traceHook := trace.ContextExecutableTrace(ctx)
118+
if traceHook != nil {
119+
if traceHook.ExecuteStart != nil {
120+
traceHook.ExecuteStart("testExecuter", "erase")
121+
}
122+
if traceHook.ExecuteDone != nil {
123+
traceHook.ExecuteDone("testExecuter", "erase", nil)
124+
}
125+
}
126+
return nil, nil
91127
default:
92128
return []byte("program failed"), errCommandExited
93129
}
@@ -185,3 +221,165 @@ func TestNewDefaultNativeStore(t *testing.T) {
185221
t.Errorf("NewDefaultNativeStore() = %v, want %v", ok, wantOK)
186222
}
187223
}
224+
225+
func TestNativeStore_trace(t *testing.T) {
226+
ns := &nativeStore{
227+
&testExecuter{},
228+
}
229+
// create trace hooks that write to buffer
230+
buffer := bytes.Buffer{}
231+
traceHook := &trace.ExecutableTrace{
232+
ExecuteStart: func(executableName string, action string) {
233+
buffer.WriteString(fmt.Sprintf("test trace, start the execution of executable %s with action %s ", executableName, action))
234+
},
235+
ExecuteDone: func(executableName string, action string, err error) {
236+
buffer.WriteString(fmt.Sprintf("test trace, completed the execution of executable %s with action %s and got err %v", executableName, action, err))
237+
},
238+
}
239+
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
240+
// Test ns.Put trace
241+
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
242+
if err != nil {
243+
t.Fatalf("trace test ns.Put fails: %v", err)
244+
}
245+
bufferContent := buffer.String()
246+
if bufferContent != "test trace, start the execution of executable testExecuter with action store test trace, completed the execution of executable testExecuter with action store and got err <nil>" {
247+
t.Fatalf("incorrect buffer content: %s", bufferContent)
248+
}
249+
buffer.Reset()
250+
// Test ns.Get trace
251+
_, err = ns.Get(ctx, traceHost)
252+
if err != nil {
253+
t.Fatalf("trace test ns.Get fails: %v", err)
254+
}
255+
bufferContent = buffer.String()
256+
if bufferContent != "test trace, start the execution of executable testExecuter with action get test trace, completed the execution of executable testExecuter with action get and got err <nil>" {
257+
t.Fatalf("incorrect buffer content: %s", bufferContent)
258+
}
259+
buffer.Reset()
260+
// Test ns.Delete trace
261+
err = ns.Delete(ctx, traceHost)
262+
if err != nil {
263+
t.Fatalf("trace test ns.Delete fails: %v", err)
264+
}
265+
bufferContent = buffer.String()
266+
if bufferContent != "test trace, start the execution of executable testExecuter with action erase test trace, completed the execution of executable testExecuter with action erase and got err <nil>" {
267+
t.Fatalf("incorrect buffer content: %s", bufferContent)
268+
}
269+
}
270+
271+
// This test ensures that a nil trace will not cause an error.
272+
func TestNativeStore_noTrace(t *testing.T) {
273+
ns := &nativeStore{
274+
&testExecuter{},
275+
}
276+
// Put
277+
err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword})
278+
if err != nil {
279+
t.Fatalf("basic auth test ns.Put fails: %v", err)
280+
}
281+
// Get
282+
cred, err := ns.Get(context.Background(), traceHost)
283+
if err != nil {
284+
t.Fatalf("basic auth test ns.Get fails: %v", err)
285+
}
286+
if cred.Username != testUsername {
287+
t.Fatal("incorrect username")
288+
}
289+
if cred.Password != testPassword {
290+
t.Fatal("incorrect password")
291+
}
292+
// Delete
293+
err = ns.Delete(context.Background(), traceHost)
294+
if err != nil {
295+
t.Fatalf("basic auth test ns.Delete fails: %v", err)
296+
}
297+
}
298+
299+
// This test ensures that an empty trace will not cause an error.
300+
func TestNativeStore_emptyTrace(t *testing.T) {
301+
ns := &nativeStore{
302+
&testExecuter{},
303+
}
304+
traceHook := &trace.ExecutableTrace{}
305+
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
306+
// Put
307+
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
308+
if err != nil {
309+
t.Fatalf("basic auth test ns.Put fails: %v", err)
310+
}
311+
// Get
312+
cred, err := ns.Get(ctx, traceHost)
313+
if err != nil {
314+
t.Fatalf("basic auth test ns.Get fails: %v", err)
315+
}
316+
if cred.Username != testUsername {
317+
t.Fatal("incorrect username")
318+
}
319+
if cred.Password != testPassword {
320+
t.Fatal("incorrect password")
321+
}
322+
// Delete
323+
err = ns.Delete(ctx, traceHost)
324+
if err != nil {
325+
t.Fatalf("basic auth test ns.Delete fails: %v", err)
326+
}
327+
}
328+
329+
func TestNativeStore_multipleTrace(t *testing.T) {
330+
ns := &nativeStore{
331+
&testExecuter{},
332+
}
333+
// create trace hooks that write to buffer
334+
buffer := bytes.Buffer{}
335+
trace1 := &trace.ExecutableTrace{
336+
ExecuteStart: func(executableName string, action string) {
337+
buffer.WriteString(fmt.Sprintf("trace 1 start %s, %s ", executableName, action))
338+
},
339+
ExecuteDone: func(executableName string, action string, err error) {
340+
buffer.WriteString(fmt.Sprintf("trace 1 done %s, %s, %v ", executableName, action, err))
341+
},
342+
}
343+
ctx := context.Background()
344+
ctx = trace.WithExecutableTrace(ctx, trace1)
345+
trace2 := &trace.ExecutableTrace{
346+
ExecuteStart: func(executableName string, action string) {
347+
buffer.WriteString(fmt.Sprintf("trace 2 start %s, %s ", executableName, action))
348+
},
349+
ExecuteDone: func(executableName string, action string, err error) {
350+
buffer.WriteString(fmt.Sprintf("trace 2 done %s, %s, %v ", executableName, action, err))
351+
},
352+
}
353+
ctx = trace.WithExecutableTrace(ctx, trace2)
354+
trace3 := &trace.ExecutableTrace{}
355+
ctx = trace.WithExecutableTrace(ctx, trace3)
356+
// Test ns.Put trace
357+
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
358+
if err != nil {
359+
t.Fatalf("trace test ns.Put fails: %v", err)
360+
}
361+
bufferContent := buffer.String()
362+
if bufferContent != "trace 2 start testExecuter, store trace 1 start testExecuter, store trace 2 done testExecuter, store, <nil> trace 1 done testExecuter, store, <nil> " {
363+
t.Fatalf("incorrect buffer content: %s", bufferContent)
364+
}
365+
buffer.Reset()
366+
// Test ns.Get trace
367+
_, err = ns.Get(ctx, traceHost)
368+
if err != nil {
369+
t.Fatalf("trace test ns.Get fails: %v", err)
370+
}
371+
bufferContent = buffer.String()
372+
if bufferContent != "trace 2 start testExecuter, get trace 1 start testExecuter, get trace 2 done testExecuter, get, <nil> trace 1 done testExecuter, get, <nil> " {
373+
t.Fatalf("incorrect buffer content: %s", bufferContent)
374+
}
375+
buffer.Reset()
376+
// Test ns.Delete trace
377+
err = ns.Delete(ctx, traceHost)
378+
if err != nil {
379+
t.Fatalf("trace test ns.Delete fails: %v", err)
380+
}
381+
bufferContent = buffer.String()
382+
if bufferContent != "trace 2 start testExecuter, erase trace 1 start testExecuter, erase trace 2 done testExecuter, erase, <nil> trace 1 done testExecuter, erase, <nil> " {
383+
t.Fatalf("incorrect buffer content: %s", bufferContent)
384+
}
385+
}

trace/trace.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
Copyright The ORAS Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package trace
17+
18+
import (
19+
"context"
20+
)
21+
22+
// executableTraceContextKey is a value key used to retrieve the ExecutableTrace
23+
// from Context.
24+
type executableTraceContextKey struct{}
25+
26+
// ExecutableTrace is a set of hooks used to trace the execution of binary
27+
// executables. Any particular hook may be nil.
28+
type ExecutableTrace struct {
29+
// ExecuteStart is called before the execution of the executable. The
30+
// executableName parameter is the name of the credential helper executable
31+
// used with NativeStore. The action parameter is one of "store", "get" and
32+
// "erase".
33+
//
34+
// Reference:
35+
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
36+
ExecuteStart func(executableName string, action string)
37+
38+
// ExecuteDone is called after the execution of an executable completes.
39+
// The executableName parameter is the name of the credential helper
40+
// executable used with NativeStore. The action parameter is one of "store",
41+
// "get" and "erase". The err parameter is the error (if any) returned from
42+
// the execution.
43+
//
44+
// Reference:
45+
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
46+
ExecuteDone func(executableName string, action string, err error)
47+
}
48+
49+
// ContextExecutableTrace returns the ExecutableTrace associated with the
50+
// context. If none, it returns nil.
51+
func ContextExecutableTrace(ctx context.Context) *ExecutableTrace {
52+
trace, _ := ctx.Value(executableTraceContextKey{}).(*ExecutableTrace)
53+
return trace
54+
}
55+
56+
// WithExecutableTrace takes a Context and an ExecutableTrace, and returns a
57+
// Context with the ExecutableTrace added as a Value. If the Context has a
58+
// previously added trace, the hooks defined in the new trace will be added
59+
// in addition to the previous ones. The recent hooks will be called first.
60+
func WithExecutableTrace(ctx context.Context, trace *ExecutableTrace) context.Context {
61+
if trace == nil {
62+
return ctx
63+
}
64+
if oldTrace := ContextExecutableTrace(ctx); oldTrace != nil {
65+
trace.compose(oldTrace)
66+
}
67+
return context.WithValue(ctx, executableTraceContextKey{}, trace)
68+
}
69+
70+
// compose takes an oldTrace and modifies the existing trace to include
71+
// the hooks defined in the oldTrace. The hooks in the existing trace will
72+
// be called first.
73+
func (trace *ExecutableTrace) compose(oldTrace *ExecutableTrace) {
74+
if oldStart := oldTrace.ExecuteStart; oldStart != nil {
75+
start := trace.ExecuteStart
76+
if start != nil {
77+
trace.ExecuteStart = func(executableName, action string) {
78+
start(executableName, action)
79+
oldStart(executableName, action)
80+
}
81+
} else {
82+
trace.ExecuteStart = oldStart
83+
}
84+
}
85+
if oldDone := oldTrace.ExecuteDone; oldDone != nil {
86+
done := trace.ExecuteDone
87+
if done != nil {
88+
trace.ExecuteDone = func(executableName, action string, err error) {
89+
done(executableName, action, err)
90+
oldDone(executableName, action, err)
91+
}
92+
} else {
93+
trace.ExecuteDone = oldDone
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)