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

feat: support trace with executables #81

Merged
merged 10 commits into from
Jul 13, 2023
9 changes: 9 additions & 0 deletions internal/executer/executer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"io"
"os"
"os/exec"

"github.com/oras-project/oras-credentials-go/trace"
)

// dockerDesktopHelperName is the name of the docker credentials helper
Expand Down Expand Up @@ -52,7 +54,14 @@ func (c *executable) Execute(ctx context.Context, input io.Reader, action string
cmd := exec.CommandContext(ctx, c.name, action)
cmd.Stdin = input
cmd.Stderr = os.Stderr
trace := trace.ContextExecutableTrace(ctx)
if trace != nil && trace.ExecuteStart != nil {
trace.ExecuteStart(c.name, action)
}
output, err := cmd.Output()
if trace != nil && trace.ExecuteDone != nil {
trace.ExecuteDone(c.name, action, err)
}
if err != nil {
switch execErr := err.(type) {
case *exec.ExitError:
Expand Down
200 changes: 199 additions & 1 deletion native_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,25 @@ limitations under the License.
package credentials

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"testing"

"github.com/oras-project/oras-credentials-go/trace"
"oras.land/oras-go/v2/registry/remote/auth"
)

const (
basicAuthHost = "localhost:2333"
bearerAuthHost = "localhost:6666"
bearerAuthHost = "localhost:666"
exeErrorHost = "localhost:500/exeError"
jsonErrorHost = "localhost:500/jsonError"
noCredentialsHost = "localhost:404"
traceHost = "localhost:808"
testUsername = "test_username"
testPassword = "test_password"
testRefreshToken = "test_token"
Expand Down Expand Up @@ -69,6 +72,17 @@ func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action stri
return []byte("json.Unmarshal failed"), nil
case noCredentialsHost:
return []byte("credentials not found"), errCredentialsNotFound
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "get")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "get", nil)
}
}
return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
default:
return []byte("program failed"), errCommandExited
}
Expand All @@ -81,13 +95,35 @@ func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action stri
switch c.ServerURL {
case basicAuthHost, bearerAuthHost, exeErrorHost:
return nil, nil
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "store")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "store", nil)
}
}
return nil, nil
default:
return []byte("program failed"), errCommandExited
}
case "erase":
switch inS {
case basicAuthHost, bearerAuthHost:
return nil, nil
case traceHost:
traceHook := trace.ContextExecutableTrace(ctx)
if traceHook != nil {
if traceHook.ExecuteStart != nil {
traceHook.ExecuteStart("testExecuter", "erase")
}
if traceHook.ExecuteDone != nil {
traceHook.ExecuteDone("testExecuter", "erase", nil)
}
}
return nil, nil
default:
return []byte("program failed"), errCommandExited
}
Expand Down Expand Up @@ -185,3 +221,165 @@ func TestNewDefaultNativeStore(t *testing.T) {
t.Errorf("NewDefaultNativeStore() = %v, want %v", ok, wantOK)
}
}

func TestNativeStore_trace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// create trace hooks that write to buffer
buffer := bytes.Buffer{}
traceHook := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("test trace, start the execution of executable %s with action %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("test trace, completed the execution of executable %s with action %s and got err %v", executableName, action, err))
},
}
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
// Test ns.Put trace
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("trace test ns.Put fails: %v", err)
}
bufferContent := buffer.String()
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>" {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Get trace
_, err = ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Get fails: %v", err)
}
bufferContent = buffer.String()
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>" {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Delete trace
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Delete fails: %v", err)
}
bufferContent = buffer.String()
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>" {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
}

// This test ensures that a nil trace will not cause an error.
func TestNativeStore_noTrace(t *testing.T) {
Wwwsylvia marked this conversation as resolved.
Show resolved Hide resolved
ns := &nativeStore{
&testExecuter{},
}
// Put
err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("basic auth test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(context.Background(), traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Get fails: %v", err)
}
if cred.Username != testUsername {
t.Fatal("incorrect username")
}
if cred.Password != testPassword {
t.Fatal("incorrect password")
}
// Delete
err = ns.Delete(context.Background(), traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Delete fails: %v", err)
}
}

// This test ensures that an empty trace will not cause an error.
func TestNativeStore_emptyTrace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
traceHook := &trace.ExecutableTrace{}
ctx := trace.WithExecutableTrace(context.Background(), traceHook)
// Put
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("basic auth test ns.Put fails: %v", err)
}
// Get
cred, err := ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Get fails: %v", err)
}
if cred.Username != testUsername {
t.Fatal("incorrect username")
}
if cred.Password != testPassword {
t.Fatal("incorrect password")
}
// Delete
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("basic auth test ns.Delete fails: %v", err)
}
}

func TestNativeStore_multipleTrace(t *testing.T) {
ns := &nativeStore{
&testExecuter{},
}
// create trace hooks that write to buffer
buffer := bytes.Buffer{}
trace1 := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("trace 1 start %s, %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("trace 1 done %s, %s, %v ", executableName, action, err))
},
}
ctx := context.Background()
ctx = trace.WithExecutableTrace(ctx, trace1)
trace2 := &trace.ExecutableTrace{
ExecuteStart: func(executableName string, action string) {
buffer.WriteString(fmt.Sprintf("trace 2 start %s, %s ", executableName, action))
},
ExecuteDone: func(executableName string, action string, err error) {
buffer.WriteString(fmt.Sprintf("trace 2 done %s, %s, %v ", executableName, action, err))
},
}
ctx = trace.WithExecutableTrace(ctx, trace2)
trace3 := &trace.ExecutableTrace{}
ctx = trace.WithExecutableTrace(ctx, trace3)
// Test ns.Put trace
err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
if err != nil {
t.Fatalf("trace test ns.Put fails: %v", err)
}
bufferContent := buffer.String()
if bufferContent != "trace 2 start testExecuter, store trace 1 start testExecuter, store trace 2 done testExecuter, store, <nil> trace 1 done testExecuter, store, <nil> " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Get trace
_, err = ns.Get(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Get fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "trace 2 start testExecuter, get trace 1 start testExecuter, get trace 2 done testExecuter, get, <nil> trace 1 done testExecuter, get, <nil> " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
buffer.Reset()
// Test ns.Delete trace
err = ns.Delete(ctx, traceHost)
if err != nil {
t.Fatalf("trace test ns.Delete fails: %v", err)
}
bufferContent = buffer.String()
if bufferContent != "trace 2 start testExecuter, erase trace 1 start testExecuter, erase trace 2 done testExecuter, erase, <nil> trace 1 done testExecuter, erase, <nil> " {
t.Fatalf("incorrect buffer content: %s", bufferContent)
}
}
96 changes: 96 additions & 0 deletions trace/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
Copyright The ORAS Authors.
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 trace

import (
"context"
)

// executableTraceContextKey is a value key used to retrieve the ExecutableTrace
// from Context.
type executableTraceContextKey struct{}
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved

// ExecutableTrace is a set of hooks used to trace the execution of binary
// executables. Any particular hook may be nil.
type ExecutableTrace struct {
// ExecuteStart is called before the execution of the executable. The
// executableName parameter is the name of the credential helper executable
// used with NativeStore. The action parameter is one of "store", "get" and
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
// "erase".
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
ExecuteStart func(executableName string, action string)

// ExecuteDone is called after the execution of an executable completes.
// The executableName parameter is the name of the credential helper
// executable used with NativeStore. The action parameter is one of "store",
// "get" and "erase". The err parameter is the error (if any) returned from
// the execution.
//
// Reference:
// - https://docs.docker.com/engine/reference/commandline/login#credentials-store
ExecuteDone func(executableName string, action string, err error)
}

// ContextExecutableTrace returns the ExecutableTrace associated with the
// context. If none, it returns nil.
func ContextExecutableTrace(ctx context.Context) *ExecutableTrace {
trace, _ := ctx.Value(executableTraceContextKey{}).(*ExecutableTrace)
wangxiaoxuan273 marked this conversation as resolved.
Show resolved Hide resolved
return trace
}

// WithExecutableTrace takes a Context and an ExecutableTrace, and returns a
// Context with the ExecutableTrace added as a Value. If the Context has a
// previously added trace, the hooks defined in the new trace will be added
// in addition to the previous ones. The recent hooks will be called first.
func WithExecutableTrace(ctx context.Context, trace *ExecutableTrace) context.Context {
if trace == nil {
return ctx
}
if oldTrace := ContextExecutableTrace(ctx); oldTrace != nil {
trace.compose(oldTrace)
}
return context.WithValue(ctx, executableTraceContextKey{}, trace)
}

// compose takes an oldTrace and modifies the existing trace to include
// the hooks defined in the oldTrace. The hooks in the existing trace will
// be called first.
func (trace *ExecutableTrace) compose(oldTrace *ExecutableTrace) {
if oldStart := oldTrace.ExecuteStart; oldStart != nil {
start := trace.ExecuteStart
if start != nil {
trace.ExecuteStart = func(executableName, action string) {
start(executableName, action)
oldStart(executableName, action)
}
} else {
trace.ExecuteStart = oldStart
}
}
if oldDone := oldTrace.ExecuteDone; oldDone != nil {
done := trace.ExecuteDone
if done != nil {
trace.ExecuteDone = func(executableName, action string, err error) {
done(executableName, action, err)
oldDone(executableName, action, err)
}
} else {
trace.ExecuteDone = oldDone
}
}
}