Skip to content

Commit a59ec95

Browse files
committed
feat: Adding latest tag and short sha. Fixing blank deployment.
1 parent 919bada commit a59ec95

5 files changed

Lines changed: 173 additions & 181 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
{
22
"name": "bridge",
33
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
4+
"features": {
5+
"ghcr.io/devcontainers/features/aws-cli:1": {}
6+
},
47
"mounts": [
5-
"source=bridge-bashhistory,target=/commandhistory,type=volume"
8+
"source=bridge-bashhistory,target=/commandhistory,type=volume",
9+
"source=${localEnv:HOME}/.aws,target=/tmp/aws,type=bind,readonly"
610
],
711
"postCreateCommand": "sudo chown $(id -u) /commandhistory",
812
"remoteEnv": {
9-
"HISTFILE": "/commandhistory/.shell_history"
13+
"HISTFILE": "/commandhistory/.shell_history",
14+
"AWS_CONFIG_FILE": "/tmp/aws/config",
15+
"AWS_SHARED_CREDENTIALS_FILE": "/tmp/aws/credentials"
1016
}
1117
}

.github/workflows/bridge-image.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,17 @@ jobs:
9898
username: ${{ github.actor }}
9999
password: ${{ secrets.GITHUB_TOKEN }}
100100

101+
- uses: actions/checkout@v4
102+
101103
- name: Determine tags
102104
id: tags
103105
run: |
106+
SHORT_SHA=$(git rev-parse --short HEAD)
104107
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
105-
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF#refs/tags/},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_OUTPUT
108+
VERSION="${GITHUB_REF#refs/tags/}"
109+
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${SHORT_SHA}" >> $GITHUB_OUTPUT
106110
else
107-
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:edge" >> $GITHUB_OUTPUT
111+
echo "tags=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:edge,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest,${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${SHORT_SHA}" >> $GITHUB_OUTPUT
108112
fi
109113
110114
- name: Create manifest list and push

pkg/commands/create.go

Lines changed: 74 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,8 @@ func runCreate(ctx context.Context, c *cli.Command) error {
159159
featureRef = devFeatureRef
160160
}
161161

162-
w := c.Root().Writer
163162
r := c.Root().Reader
164-
p := interact.NewPrinter(w)
163+
p := interact.NewPrinter(c.Root().Writer)
165164

166165
// Step 1: Resolve device identity.
167166
deviceID, err := identity.GetDeviceID()
@@ -176,117 +175,95 @@ func runCreate(ctx context.Context, c *cli.Command) error {
176175
var adm admin.Admin
177176
var existingBridges []*admin.BridgeInfo
178177

179-
err = interact.RunSteps(ctx, []interact.Step{
180-
{
181-
Title: "Connecting to bridge administrator...",
182-
Run: func(ctx context.Context) error {
183-
remote, dialErr := admin.NewRemote(adminAddr)
184-
if dialErr != nil {
185-
return errAdminUnavailable{}
186-
}
187-
// Probe with a quick ListBridges call to verify the admin is up.
188-
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
189-
defer cancel()
190-
bridges, probeErr := remote.ListBridges(probeCtx, deviceID)
191-
if probeErr != nil {
192-
remote.Close()
193-
return errAdminUnavailable{}
194-
}
195-
existingBridges = bridges
196-
adm = remote
197-
return nil
198-
},
199-
},
200-
})
201-
202-
if _, ok := err.(errAdminUnavailable); ok {
203-
err = nil
178+
sp := interact.NewSpinner("Connecting to bridge administrator...")
179+
go sp.Start(ctx)
180+
181+
remote, dialErr := admin.NewRemote(adminAddr)
182+
if dialErr == nil {
183+
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
184+
bridges, probeErr := remote.ListBridges(probeCtx, deviceID)
185+
cancel()
186+
if probeErr == nil {
187+
existingBridges = bridges
188+
adm = remote
189+
} else {
190+
remote.Close()
191+
}
192+
}
193+
sp.Stop()
204194

195+
if adm == nil {
205196
// Remote admin not available — offer local fallback.
206197
if !yes {
207198
p.Newline()
208199
p.Warn("No bridge administrator found in the cluster.")
209200
p.Info(fmt.Sprintf("Should Bridge use your local credentials for cluster %q instead.", kubeContext))
210-
fmt.Fprintf(w, "Continue? [y/N] ")
201+
p.Prompt("Continue? [y/N] ")
211202

212-
answer := promptYN(r)
213-
if answer != "y" && answer != "yes" {
214-
fmt.Fprintf(w, "Aborted.\n")
203+
if answer := promptYN(r); answer != "y" && answer != "yes" {
204+
p.Println("Aborted.")
215205
return nil
216206
}
217207
}
218208

219-
err = interact.RunSteps(ctx, []interact.Step{
220-
{
221-
Title: "Initializing local administrator...",
222-
Run: func(ctx context.Context) error {
223-
localAdm, localErr := admin.NewLocal(admin.LocalConfig{
224-
ProxyImage: proxyImage,
225-
})
226-
if localErr != nil {
227-
return fmt.Errorf("failed to initialize: %w", localErr)
228-
}
229-
adm = localAdm
230-
bridges, listErr := localAdm.ListBridges(ctx, deviceID)
231-
if listErr != nil {
232-
slog.Warn("Failed to list existing bridges", "error", listErr)
233-
} else {
234-
existingBridges = bridges
235-
}
236-
return nil
237-
},
238-
},
209+
sp = interact.NewSpinner("Initializing local administrator...")
210+
go sp.Start(ctx)
211+
212+
localAdm, localErr := admin.NewLocal(admin.LocalConfig{
213+
ProxyImage: proxyImage,
239214
})
240-
if err != nil {
241-
return err
215+
if localErr != nil {
216+
sp.Stop()
217+
return fmt.Errorf("failed to initialize: %w", localErr)
242218
}
243-
} else if err != nil {
244-
return err
219+
adm = localAdm
220+
bridges, listErr := localAdm.ListBridges(ctx, deviceID)
221+
if listErr != nil {
222+
slog.Warn("Failed to list existing bridges", "error", listErr)
223+
} else {
224+
existingBridges = bridges
225+
}
226+
227+
sp.Stop()
245228
}
246229
defer adm.Close()
247230

248231
// Step 3: Check for existing bridges.
249-
var conflictingBridge *admin.BridgeInfo
250232
if !yes {
251233
for _, bridge := range existingBridges {
252234
if bridge.SourceDeployment == deploymentName {
253-
conflictingBridge = bridge
235+
p.Newline()
236+
p.Warn("An existing bridge already exists:")
237+
p.KeyValue("Name", bridge.SourceDeployment)
238+
p.KeyValue("Created", bridge.CreatedAt)
239+
p.KeyValue("Context", kubeContext)
240+
p.Newline()
241+
p.Muted("This will tear down the existing bridge and recreate it.")
242+
p.Prompt("Continue? [y/N] ")
243+
244+
if answer := promptYN(r); answer != "y" && answer != "yes" {
245+
p.Println("Aborted.")
246+
return nil
247+
}
248+
yes = true
254249
break
255250
}
256251
}
257252
}
258253

259-
if conflictingBridge != nil {
260-
p.Newline()
261-
p.Warn("An existing bridge already exists:")
262-
p.KeyValue("Name", conflictingBridge.SourceDeployment)
263-
p.KeyValue("Created", conflictingBridge.CreatedAt)
264-
p.KeyValue("Context", kubeContext)
265-
p.Newline()
266-
p.Muted("This will tear down the existing bridge and recreate it.")
267-
fmt.Fprintf(w, "Continue? [y/N] ")
268-
269-
answer := promptYN(r)
270-
if answer != "y" && answer != "yes" {
271-
fmt.Fprintf(w, "Aborted.\n")
272-
return nil
273-
}
274-
yes = true
275-
}
276-
277254
// Step 4: Create bridge.
278255
var createResp *admin.CreateResponse
279256

280-
err = interact.RunWithSpinner(ctx, "Creating bridge...", func(ctx context.Context) error {
281-
var createErr error
282-
createResp, createErr = adm.CreateBridge(ctx, admin.CreateRequest{
283-
DeviceID: deviceID,
284-
SourceDeployment: deploymentName,
285-
SourceNamespace: sourceNamespace,
286-
Force: yes,
287-
})
288-
return createErr
257+
sp = interact.NewSpinner("Creating bridge...")
258+
go sp.Start(ctx)
259+
260+
createResp, err = adm.CreateBridge(ctx, admin.CreateRequest{
261+
DeviceID: deviceID,
262+
SourceDeployment: deploymentName,
263+
SourceNamespace: sourceNamespace,
264+
Force: yes,
289265
})
266+
sp.Stop()
290267
if err != nil {
291268
return err
292269
}
@@ -306,12 +283,12 @@ func runCreate(ctx context.Context, c *cli.Command) error {
306283
if err != nil {
307284
return err
308285
}
309-
dcConfigPath, err := generateDevcontainerConfig(p, w, deploymentName, baseConfig, featureRef, c.Int("listen"), createResp)
286+
dcConfigPath, err := generateDevcontainerConfig(p, baseConfig, featureRef, c.Int("listen"), createResp)
310287
if err != nil {
311288
return err
312289
}
313290
if connectFlag {
314-
return startDevcontainer(ctx, dcConfigPath, r, w)
291+
return startDevcontainer(ctx, p, dcConfigPath, r)
315292
}
316293
}
317294

@@ -329,11 +306,8 @@ func promptYN(r io.Reader) string {
329306
// It respects the KUBECONFIG env var by bind-mounting it into the container,
330307
// unless the base config already sets containerEnv.KUBECONFIG.
331308
// Returns the path to the generated config.
332-
func generateDevcontainerConfig(p *interact.Printer, w io.Writer, deploymentName, baseConfigPath, featureRef string, appPort int, resp *admin.CreateResponse) (string, error) {
333-
dcName := deploymentName
334-
if dcName == "" {
335-
dcName = "proxy"
336-
}
309+
func generateDevcontainerConfig(p interact.Printer, baseConfigPath, featureRef string, appPort int, resp *admin.CreateResponse) (string, error) {
310+
dcName := resp.DeploymentName
337311

338312
// Place the generated config under the .devcontainer/ directory that contains
339313
// the base config. If the base config isn't already in a .devcontainer/ folder,
@@ -581,21 +555,24 @@ func currentKubeContext() string {
581555
}
582556

583557
// startDevcontainer starts the devcontainer and attaches an interactive shell.
584-
func startDevcontainer(ctx context.Context, dcConfigPath string, r io.Reader, w io.Writer) error {
558+
func startDevcontainer(ctx context.Context, p interact.Printer, dcConfigPath string, r io.Reader) error {
585559
// <workspace>/.devcontainer/bridge-<name>/devcontainer.json → <workspace>
586560
workspaceFolder := filepath.Dir(filepath.Dir(filepath.Dir(dcConfigPath)))
587561
dcClient := &devcontainer.Client{
588562
WorkspaceFolder: workspaceFolder,
589563
ConfigPath: dcConfigPath,
590564
Stdin: r,
591-
Stdout: w,
592-
Stderr: w,
565+
Stdout: os.Stdout,
566+
Stderr: os.Stderr,
593567
}
594568

595569
slog.Debug("Starting devcontainer", "config", dcConfigPath, "workspace", workspaceFolder)
596-
err := interact.RunWithSpinner(ctx, "Starting devcontainer...", func(ctx context.Context) error {
597-
return dcClient.Up(ctx)
598-
})
570+
571+
sp := interact.NewSpinner("Starting devcontainer...")
572+
go sp.Start(ctx)
573+
574+
err := dcClient.Up(ctx)
575+
sp.Stop()
599576
if err != nil {
600577
return fmt.Errorf("failed to start devcontainer: %w", err)
601578
}

pkg/interact/printer.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,48 @@ import (
66
"strings"
77
)
88

9-
// Printer writes themed, styled messages to a writer.
10-
type Printer struct {
9+
// Printer provides styled terminal output.
10+
type Printer interface {
11+
Success(msg string)
12+
Warn(msg string)
13+
Info(msg string)
14+
Errorf(format string, a ...any)
15+
Header(msg string)
16+
KeyValue(key, value string)
17+
Muted(msg string)
18+
Newline()
19+
// Prompt prints a message without a trailing newline, for user input.
20+
Prompt(msg string)
21+
// Println writes an unstyled message with a trailing newline.
22+
Println(msg string)
23+
}
24+
25+
type printer struct {
1126
w io.Writer
1227
theme *Theme
1328
}
1429

1530
// NewPrinter returns a Printer that writes styled output to w.
16-
func NewPrinter(w io.Writer) *Printer {
17-
return &Printer{w: w, theme: NewTheme()}
31+
func NewPrinter(w io.Writer) Printer {
32+
return &printer{w: w, theme: NewTheme()}
1833
}
1934

20-
// Success prints a green check-mark message.
21-
func (p *Printer) Success(msg string) {
35+
func (p *printer) Success(msg string) {
2236
fmt.Fprintf(p.w, "%s %s\n", p.theme.Success.Render("✓"), p.theme.Bold.Render(msg))
2337
}
2438

25-
// Warn prints a yellow warning message.
26-
func (p *Printer) Warn(msg string) {
39+
func (p *printer) Warn(msg string) {
2740
fmt.Fprintf(p.w, "%s %s\n", p.theme.Warning.Render("!"), p.theme.Warning.Render(msg))
2841
}
2942

30-
// Info prints a blue info message.
31-
func (p *Printer) Info(msg string) {
43+
func (p *printer) Info(msg string) {
3244
fmt.Fprintf(p.w, "%s %s\n", p.theme.Info.Render("→"), msg)
3345
}
3446

3547
// Errorf prints a red error message with formatting. If the message contains
3648
// newlines, only the first line is styled to avoid lipgloss mangling
3749
// multi-line output (e.g. devcontainer build logs).
38-
func (p *Printer) Errorf(format string, a ...any) {
50+
func (p *printer) Errorf(format string, a ...any) {
3951
msg := fmt.Sprintf(format, a...)
4052
first, rest, _ := strings.Cut(msg, "\n")
4153
fmt.Fprintf(p.w, "%s %s\n", p.theme.Error.Render("✗"), p.theme.Error.Render(first))
@@ -44,22 +56,26 @@ func (p *Printer) Errorf(format string, a ...any) {
4456
}
4557
}
4658

47-
// Header prints a bold, underlined header.
48-
func (p *Printer) Header(msg string) {
59+
func (p *printer) Header(msg string) {
4960
fmt.Fprintf(p.w, "%s\n", p.theme.Header.Render(msg))
5061
}
5162

52-
// KeyValue prints a dimmed key with a bold value.
53-
func (p *Printer) KeyValue(key, value string) {
63+
func (p *printer) KeyValue(key, value string) {
5464
fmt.Fprintf(p.w, " %s %s\n", p.theme.Key.Render(key+":"), p.theme.Value.Render(value))
5565
}
5666

57-
// Muted prints a dimmed message.
58-
func (p *Printer) Muted(msg string) {
67+
func (p *printer) Muted(msg string) {
5968
fmt.Fprintf(p.w, "%s\n", p.theme.Muted.Render(msg))
6069
}
6170

62-
// Newline prints a blank line.
63-
func (p *Printer) Newline() {
71+
func (p *printer) Newline() {
6472
fmt.Fprintln(p.w)
6573
}
74+
75+
func (p *printer) Prompt(msg string) {
76+
fmt.Fprint(p.w, msg)
77+
}
78+
79+
func (p *printer) Println(msg string) {
80+
fmt.Fprintln(p.w, msg)
81+
}

0 commit comments

Comments
 (0)