Skip to content

Commit

Permalink
Add ability to mock LocalClient (#90)
Browse files Browse the repository at this point in the history
This PR adds the ability to mock LocalClient responses via `-mockfile`
so that we can test vscode <-> tsrelay communication without requiring a
live tailscaled server.

---------

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
Co-authored-by: Tyler Smalley <tyler@tailscale.com>
  • Loading branch information
marwan-at-work and tylersmalley committed Jun 29, 2023
1 parent 7a627b1 commit 2105a74
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 9 deletions.
3 changes: 3 additions & 0 deletions profiles/offline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"MockOffline": true
}
60 changes: 60 additions & 0 deletions profiles/snapshot.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { exec } from 'child_process';
import fs from 'fs';

function executeCommand(command) {
const fullCommand = `tailscale ${command} --json`;
return new Promise((resolve, reject) => {
exec(fullCommand, (error, stdout, stderr) => {
if (error) {
// If command not found in PATH, try with zsh
if (error.code === 127) {
exec(`zsh -i -c "${fullCommand}"`, (zshError, zshStdout, zshStderr) => {
if (zshError) {
reject(zshError);
} else {
resolve(JSON.parse(zshStdout.trim()));
}
});
} else {
reject(error);
}
} else {
try {
const parsedOutput = JSON.parse(stdout.trim());
resolve(parsedOutput);
} catch (parseError) {
resolve({});
}
}
});
});
}

function exportResults(profileName, results) {
const filename = `${profileName}.json`;
fs.writeFileSync(filename, JSON.stringify(results, null, 2));
console.log(`Results exported to ${filename}`);
}

async function runCommands(profileName) {
try {
const results = {};

results.Status = await executeCommand('status');
results.ServeConfig = await executeCommand('serve status');

// Export results to JSON file
exportResults(profileName, results);
} catch (error) {
console.error('Error:', error.message);
throw error;
}
}

const profileName = process.argv[2];

if (profileName) {
runCommands(profileName);
} else {
console.error('Please provide a profile name as an argument.');
}
86 changes: 86 additions & 0 deletions tsrelay/local_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"context"
"encoding/json"
"net"
"os"
"sync"

"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
)

// static check for local client interface implementation
var _ localClient = (*tailscale.LocalClient)(nil)

// localClient is an abstraction of tailscale.LocalClient
type localClient interface {
Status(ctx context.Context) (*ipnstate.Status, error)
GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error)
StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error)
SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error
}

type profile struct {
Status *ipnstate.Status
ServeConfig *ipn.ServeConfig
MockOffline bool
MockAccessDenied bool
}

// NewMockClient returns a mock localClient
// based on the given json file. The format of the file
// is described in the profile struct. Note that SET
// operations update the given input in memory.
func NewMockClient(file string) (localClient, error) {
bts, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var p profile
return &mockClient{p: &p}, json.Unmarshal(bts, &p)
}

type mockClient struct {
sync.Mutex
p *profile
}

// GetServeConfig implements localClient.
func (m *mockClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
if m.p.MockOffline {
return nil, &net.OpError{Op: "dial"}
}
return m.p.ServeConfig, nil
}

// SetServeConfig implements localClient.
func (m *mockClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
if m.p.MockAccessDenied {
return &tailscale.AccessDeniedError{}
}
m.Lock()
defer m.Unlock()
m.p.ServeConfig = config
return nil
}

// Status implements localClient.
func (m *mockClient) Status(ctx context.Context) (*ipnstate.Status, error) {
if m.p.MockOffline || m.p.Status == nil {
return nil, &net.OpError{Op: "dial"}
}
return m.p.Status, nil
}

// StatusWithoutPeers implements localClient.
func (m *mockClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
if m.p.MockOffline || m.p.Status == nil {
return nil, &net.OpError{Op: "dial"}
}
copy := *(m.p.Status)
copy.Peer = nil
return &copy, nil
}
26 changes: 17 additions & 9 deletions tsrelay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import (
)

var (
logfile = flag.String("logfile", "", "send logs to a file instead of stderr")
verbose = flag.Bool("v", false, "verbose logging")
port = flag.Int("port", 0, "port for http server. If 0, one will be chosen")
nonce = flag.String("nonce", "", "nonce for the http server")
socket = flag.String("socket", "", "alternative path for local api socket")
logfile = flag.String("logfile", "", "send logs to a file instead of stderr")
verbose = flag.Bool("v", false, "verbose logging")
port = flag.Int("port", 0, "port for http server. If 0, one will be chosen")
nonce = flag.String("nonce", "", "nonce for the http server")
socket = flag.String("socket", "", "alternative path for local api socket")
mockFile = flag.String("mockfile", "", "a profile file to mock LocalClient responses")
)

// ErrorTypes for signaling
Expand Down Expand Up @@ -152,11 +153,18 @@ func runHTTPServer(ctx context.Context, lggr *logger, port int, nonce string) er
Nonce: nonce,
}
json.NewEncoder(os.Stdout).Encode(sd)
var lc localClient = &tailscale.LocalClient{
Socket: *socket,
}
if *mockFile != "" {
lc, err = NewMockClient(*mockFile)
if err != nil {
return fmt.Errorf("error creating mock client: %w", err)
}
}
s := &http.Server{
Handler: &httpHandler{
lc: tailscale.LocalClient{
Socket: *socket,
},
lc: lc,
nonce: nonce,
l: lggr,
pids: make(map[int]struct{}),
Expand All @@ -179,7 +187,7 @@ func getNonce() string {
type httpHandler struct {
sync.Mutex
nonce string
lc tailscale.LocalClient
lc localClient
l *logger
u websocket.Upgrader
pids map[int]struct{}
Expand Down

0 comments on commit 2105a74

Please sign in to comment.