Skip to content

Commit efd7014

Browse files
chore(internal): codegen related update
1 parent 0ee9d29 commit efd7014

11 files changed

Lines changed: 100 additions & 35 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
2121

2222
steps:
23-
- uses: actions/checkout@v4
23+
- uses: actions/checkout@v6
2424

2525
- name: Setup go
2626
uses: actions/setup-go@v5
@@ -39,7 +39,7 @@ jobs:
3939
runs-on: ${{ github.repository == 'stainless-sdks/hypeman-cli' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
4040
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
4141
steps:
42-
- uses: actions/checkout@v4
42+
- uses: actions/checkout@v6
4343

4444
- name: Setup go
4545
uses: actions/setup-go@v5

.github/workflows/publish-release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v4
18+
uses: actions/checkout@v6
1919
with:
2020
fetch-depth: 0
2121
- name: Set up Go

.github/workflows/release-doctor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
if: github.repository == 'kernel/hypeman-cli' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
1313

1414
steps:
15-
- uses: actions/checkout@v4
15+
- uses: actions/checkout@v6
1616

1717
- name: Check release environment
1818
run: |

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
The official CLI for [Hypeman](https://github.com/kernel/hypeman/).
44

5+
<!-- x-release-please-start-version -->
6+
57
## Installation
68

79
### Installing with Homebrew
@@ -12,22 +14,18 @@ brew install kernel/tap/hypeman
1214

1315
### Installing with Go
1416

15-
<!-- x-release-please-start-version -->
16-
1717
```sh
1818
go install 'github.com/kernel/hypeman-cli/cmd/hypeman@latest'
1919
```
2020

21-
### Running Locally
21+
<!-- x-release-please-end -->
2222

23-
<!-- x-release-please-start-version -->
23+
### Running Locally
2424

2525
```sh
26-
go run cmd/hypeman/main.go
26+
./scripts/run args...
2727
```
2828

29-
<!-- x-release-please-end -->
30-
3129
## Usage
3230

3331
```sh
@@ -265,9 +263,15 @@ hypeman [resource] [command] [flags]
265263

266264
## Global Flags
267265

266+
- `--help` - Show command line usage
268267
- `--debug` - Enable debug logging (includes HTTP request/response details)
269268
- `--version`, `-v` - Show the CLI version
270269

270+
- `--base-url` - Use a custom API backend URL
271+
- `--format` - Change the output format (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`)
272+
- `--format-error` - Change the output format for errors (`auto`, `explore`, `json`, `jsonl`, `pretty`, `raw`, `yaml`)
273+
- `--transform` - Transform the data output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)
274+
- `--transform-error` - Transform the error output using [GJSON syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md)
271275
## Development
272276

273277
### Testing Preview Branches

internal/debugmiddleware/debug_middleware.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package debugmiddleware
22

33
import (
4+
"bytes"
5+
"io"
46
"log"
57
"net/http"
68
"net/http/httputil"
9+
"reflect"
710
"strings"
8-
"sync"
911
)
1012

1113
// For the time being these type definitions are duplicated here so that we can
@@ -17,7 +19,8 @@ type (
1719

1820
const redactedPlaceholder = "<REDACTED>"
1921

20-
// Headers known to contain sensitive information like an API key.
22+
// Headers known to contain sensitive information like an API key. Note that this exclude `Authorization`,
23+
// which is handled specially in `redactRequest` below.
2124
var sensitiveHeaders = []string{}
2225

2326
// RequestLogger is a middleware that logs HTTP requests and responses.
@@ -36,7 +39,11 @@ func NewRequestLogger() *RequestLogger {
3639

3740
func (m *RequestLogger) Middleware() Middleware {
3841
return func(req *http.Request, mn MiddlewareNext) (*http.Response, error) {
39-
if reqBytes, err := httputil.DumpRequest(m.redactRequest(req), true); err == nil {
42+
redacted, err := m.redactRequest(req)
43+
if err != nil {
44+
return nil, err
45+
}
46+
if reqBytes, err := httputil.DumpRequest(redacted, true); err == nil {
4047
m.logger.Printf("Request Content:\n%s\n", reqBytes)
4148
}
4249

@@ -57,42 +64,64 @@ func (m *RequestLogger) Middleware() Middleware {
5764
// purposes. If redaction is necessary, the request is cloned before mutating
5865
// the original and that clone is returned. As a small optimization, the
5966
// original is request is returned unchanged if no redaction is necessary.
60-
func (m *RequestLogger) redactRequest(req *http.Request) *http.Request {
61-
cloneReq := sync.OnceFunc(func() {
62-
req = req.Clone(req.Context())
63-
})
67+
func (m *RequestLogger) redactRequest(req *http.Request) (*http.Request, error) {
68+
redactedHeaders := req.Header.Clone()
6469

6570
// Notably, the clauses below are written so they can redact multiple
6671
// headers of the same name if necessary.
67-
if values := req.Header.Values("Authorization"); len(values) > 0 {
68-
cloneReq()
69-
req.Header.Del("Authorization")
72+
if values := redactedHeaders.Values("Authorization"); len(values) > 0 {
73+
redactedHeaders.Del("Authorization")
7074

7175
for _, value := range values {
7276
// In case we're using something like a bearer token (e.g. `Bearer
7377
// <my_token>`), keep the `Bearer` part for more debugging
7478
// information.
7579
if authKind, _, ok := strings.Cut(value, " "); ok {
76-
req.Header.Add("Authorization", authKind+" "+redactedPlaceholder)
80+
redactedHeaders.Add("Authorization", authKind+" "+redactedPlaceholder)
7781
} else {
78-
req.Header.Add("Authorization", redactedPlaceholder)
82+
redactedHeaders.Add("Authorization", redactedPlaceholder)
7983
}
8084
}
8185
}
8286

8387
for _, header := range m.sensitiveHeaders {
84-
values := req.Header.Values(header)
88+
values := redactedHeaders.Values(header)
8589
if len(values) == 0 {
8690
continue
8791
}
8892

89-
cloneReq()
90-
req.Header.Del(header)
93+
redactedHeaders.Del(header)
9194

9295
for range values {
93-
req.Header.Add(header, redactedPlaceholder)
96+
redactedHeaders.Add(header, redactedPlaceholder)
9497
}
9598
}
9699

97-
return req
100+
if reflect.DeepEqual(req.Header, redactedHeaders) {
101+
return req, nil
102+
}
103+
104+
redacted := req.Clone(req.Context())
105+
redacted.Header = redactedHeaders
106+
var err error
107+
redacted.Body, req.Body, err = cloneBody(req.Body)
108+
return redacted, err
109+
}
110+
111+
// This function returns two copies of an HTTP request body that can each be
112+
// read independently without affecting the other.
113+
// This logic is taken from `drainBody` in net/http/httputil.
114+
func cloneBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
115+
if b == nil || b == http.NoBody {
116+
// No copying needed. Preserve the magic sentinel meaning of NoBody.
117+
return http.NoBody, http.NoBody, nil
118+
}
119+
var buf bytes.Buffer
120+
if _, err = buf.ReadFrom(b); err != nil {
121+
return nil, b, err
122+
}
123+
if err = b.Close(); err != nil {
124+
return nil, b, err
125+
}
126+
return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
98127
}

internal/debugmiddleware/debug_middleware_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package debugmiddleware
22

33
import (
44
"bytes"
5+
"io"
56
"log"
67
"net/http"
78
"net/http/httptest"
@@ -166,4 +167,35 @@ func TestDebugMiddleware(t *testing.T) {
166167
require.True(t, nextMiddlewareRan)
167168
require.Equal(t, 2, strings.Count(logBuf.String(), customAPIKeyHeader+": "+redactedPlaceholder))
168169
})
170+
171+
t.Run("DoesNotConsumeRequestBodyWhenIoReader", func(t *testing.T) {
172+
t.Parallel()
173+
174+
middleware, logBuf := setup()
175+
middleware.sensitiveHeaders = []string{customAPIKeyHeader}
176+
177+
const bodyContent = "test request body content"
178+
bodyReader := strings.NewReader(bodyContent)
179+
180+
req := httptest.NewRequest("POST", "https://example.com", bodyReader)
181+
req.Header.Set("Authorization", secretToken)
182+
183+
var nextMiddlewareRan bool
184+
middleware.Middleware()(req, func(req *http.Request) (*http.Response, error) {
185+
nextMiddlewareRan = true
186+
187+
// The request body should still be fully readable after the middleware runs
188+
body, err := io.ReadAll(req.Body)
189+
require.NoError(t, err)
190+
require.Equal(t, bodyContent, string(body))
191+
192+
// The request sent down through middleware shouldn't be mutated.
193+
require.Equal(t, secretToken, req.Header.Get("Authorization"))
194+
195+
return &http.Response{}, nil
196+
})
197+
198+
require.True(t, nextMiddlewareRan)
199+
require.Contains(t, logBuf.String(), "Authorization: "+redactedPlaceholder)
200+
})
169201
}

internal/requestflag/innerflag.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,19 +192,17 @@ func WithInnerFlags(cmd cli.Command, innerFlagMap map[string][]HasOuterFlag) cli
192192

193193
updatedFlags := make([]cli.Flag, 0, len(cmd.Flags))
194194
for _, flag := range cmd.Flags {
195+
updatedFlags = append(updatedFlags, flag)
195196
for _, name := range flag.Names() {
196197
// Check if this flag has inner flags in our map
197198
innerFlags, hasInnerFlags := innerFlagMap[name]
198199
if !hasInnerFlags {
199-
// No inner flags for this one, add it as-is
200-
updatedFlags = append(updatedFlags, flag)
201200
continue
202201
}
203202

204203
// Mark this inner flag key as used
205204
delete(unusedInnerFlagKeys, name)
206205

207-
updatedFlags = append(updatedFlags, flag)
208206
for _, innerFlag := range innerFlags {
209207
innerFlag.SetOuterFlag(flag)
210208
updatedFlags = append(updatedFlags, innerFlag)

pkg/cmd/cmdutil.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func ShowJSONIterator[T any](stdout *os.File, title string, iter jsonview.Iterat
298298

299299
terminalWidth, terminalHeight, err := term.GetSize(os.Stdout.Fd())
300300
if err != nil {
301-
return err
301+
terminalHeight = 100
302302
}
303303

304304
// Decide whether or not to use a pager based on whether it's a short output or a long output

pkg/cmd/instance.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@ var instancesCreate = requestflag.WithInnerFlags(cli.Command{
8383
&requestflag.Flag[bool]{
8484
Name: "skip-guest-agent",
8585
Usage: "Skip guest-agent installation during boot.\nWhen true, the exec and stat APIs will not work for this instance.\nThe instance will still run, but remote command execution will be unavailable.\n",
86+
Default: false,
8687
BodyPath: "skip_guest_agent",
8788
},
8889
&requestflag.Flag[bool]{
8990
Name: "skip-kernel-headers",
9091
Usage: "Skip kernel headers installation during boot for faster startup.\nWhen true, DKMS (Dynamic Kernel Module Support) will not work,\npreventing compilation of out-of-tree kernel modules (e.g., NVIDIA vGPU drivers).\nRecommended for workloads that don't need kernel module compilation.\n",
92+
Default: false,
9193
BodyPath: "skip_kernel_headers",
9294
},
9395
&requestflag.Flag[int64]{
@@ -193,6 +195,7 @@ var instancesLogs = cli.Command{
193195
&requestflag.Flag[bool]{
194196
Name: "follow",
195197
Usage: "Continue streaming new lines after initial output",
198+
Default: false,
196199
QueryPath: "follow",
197200
},
198201
&requestflag.Flag[string]{
@@ -272,6 +275,7 @@ var instancesStat = cli.Command{
272275
&requestflag.Flag[bool]{
273276
Name: "follow-links",
274277
Usage: "Follow symbolic links (like stat vs lstat)",
278+
Default: false,
275279
QueryPath: "follow_links",
276280
},
277281
},

pkg/cmd/instancevolume.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ var instancesVolumesAttach = cli.Command{
3737
&requestflag.Flag[bool]{
3838
Name: "readonly",
3939
Usage: "Mount as read-only",
40+
Default: false,
4041
BodyPath: "readonly",
4142
},
4243
},

0 commit comments

Comments
 (0)