Skip to content

Commit 0f0ae20

Browse files
authored
Joesirianni/bpop 3615 GitHub action set user agent and enhance error logging (#72)
* chore: Additional debug logging and user-agent * fix glob regression * refactor glob handling * refactor glob handling * Return after applying globbed files
1 parent 7510c67 commit 0f0ae20

File tree

11 files changed

+238
-33
lines changed

11 files changed

+238
-33
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ Bindplane requires a license. You can request a free license [here](https://obse
2828
| token | | The Github token that will be used to read and write to the repo. Usually secrets.GITHUB_TOKEN is sufficient. Requires the `contents.write` permission. Alternatively, you can set `github_url`, which should contain your access token. |
2929
| enable_auto_rollout | `false` | When enabled, the action will trigger a rollout for any configuration that has been updated. |
3030
| tls_ca_cert | | The contents of a TLS certificate authority, usually from a secret. See the [TLS](#tls) section. |
31-
| github_url | | Optional URL to use when cloning the repository. Should be of the form `"https://{GITHUB_ACTOR}:{TOKEN}@{GITHUB_HOST}/{GITHUB_REPOSITORY}.git`. When set, `token` will not be used. |
31+
| github_url | | Optional URL to use when cloning the repository. Should be of the form `"https://{GITHUB_ACTOR}:{TOKEN}@{GITHUB_HOST}/{GITHUB_REPOSITORY}.git". When set, `token` will not be used. |
32+
| user_agent | `bindplane-op-action` | The user agent string to use when making requests to BindPlane. |
3233

3334
## Usage
3435

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ inputs:
3838
description: 'The CA certificate to use when connecting to Bindplane'
3939
github_url:
4040
description: 'The GitHub URL to use when connecting to GitHub'
41+
user_agent:
42+
description: 'The user agent string to use when making requests to BindPlane'
43+
default: 'bindplane-op-action'
4144

4245
runs:
4346
using: 'docker'
@@ -60,3 +63,4 @@ runs:
6063
- ${{ inputs.source_path }}
6164
- ${{ inputs.processor_path }}
6265
- ${{ inputs.github_url }}
66+
- ${{ inputs.user_agent }}

action/action.go

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/observiq/bindplane-op-action/internal/client/config"
1515
"github.com/observiq/bindplane-op-action/internal/client/model"
1616
"github.com/observiq/bindplane-op-action/internal/client/version"
17+
"github.com/observiq/bindplane-op-action/internal/glob"
1718
"github.com/observiq/bindplane-op-action/internal/repo"
1819
"gopkg.in/yaml.v3"
1920

@@ -139,6 +140,13 @@ func WithConfigurationOutputBranch(b string) Option {
139140
}
140141
}
141142

143+
// WithUserAgent sets the user agent for the BindPlane client
144+
func WithUserAgent(ua string) Option {
145+
return func(a *Action) {
146+
a.config.Network.UserAgent = ua
147+
}
148+
}
149+
142150
// New creates a new Action with a configured bindPlane client
143151
func New(logger *zap.Logger, opts ...Option) (*Action, error) {
144152
action := &Action{}
@@ -286,41 +294,60 @@ func (a *Action) Apply() error {
286294

287295
// applyAll takes a file or directory path and applies all resources.
288296
// It recursively walks through all subdirectories and applies YAML files.
297+
// It also supports glob patterns like "*.yaml" or "./resources/*.yaml".
289298
func (a *Action) applyAll(path string) error {
290299
info, err := os.Stat(path)
291-
if err != nil {
300+
switch {
301+
case err != nil:
302+
if glob.ContainsGlobChars(path) {
303+
matches, err := filepath.Glob(path)
304+
if err != nil {
305+
return fmt.Errorf("glob path %s: %w", path, err)
306+
}
307+
if len(matches) == 0 {
308+
return fmt.Errorf("no matching files found when globbing %s", path)
309+
}
310+
311+
a.Logger.Info("Applying globbed resources", zap.String("path", path), zap.Int("matches", len(matches)))
312+
313+
for _, match := range matches {
314+
if err := a.apply(match); err != nil {
315+
return err
316+
}
317+
}
318+
return nil
319+
}
292320
return fmt.Errorf("stat path %s: %w", path, err)
293-
}
294321

295-
if !info.IsDir() {
322+
case !info.IsDir():
296323
return a.apply(path)
297-
}
298324

299-
a.Logger.Info("Walking directory", zap.String("path", path))
325+
default:
326+
a.Logger.Info("Walking directory", zap.String("path", path))
300327

301-
err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
302-
if err != nil {
303-
a.Logger.Error("Error walking path", zap.String("path", p), zap.Error(err))
304-
return err
305-
}
328+
err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
329+
if err != nil {
330+
a.Logger.Error("Error walking path", zap.String("path", p), zap.Error(err))
331+
return err
332+
}
306333

307-
if info.IsDir() {
308-
return nil
309-
}
334+
if info.IsDir() {
335+
return nil
336+
}
310337

311-
ext := filepath.Ext(p)
312-
if ext != ".yaml" && ext != ".yml" {
313-
return nil
314-
}
338+
ext := filepath.Ext(p)
339+
if ext != ".yaml" && ext != ".yml" {
340+
return nil
341+
}
315342

316-
if err := a.apply(p); err != nil {
317-
return err
318-
}
343+
return a.apply(p)
344+
})
319345

346+
if err != nil {
347+
return fmt.Errorf("walk path %s: %w", path, err)
348+
}
320349
return nil
321-
})
322-
323-
return err
350+
}
324351
}
325352

326353
// apply takes a file path and applies it to the BindPlane API.
@@ -337,7 +364,7 @@ func (a *Action) apply(path string) error {
337364
}
338365

339366
if resp == nil {
340-
return fmt.Errorf("nil response from client: %s", BugError)
367+
return fmt.Errorf("nil response from client while applying path %s: %s", path, BugError)
341368
}
342369

343370
for _, s := range resp {

action/action_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,46 @@ func TestWithBindPlaneUsername(t *testing.T) {
109109
}
110110
}
111111

112+
func TestWithUserAgent(t *testing.T) {
113+
cases := []struct {
114+
name string
115+
input string
116+
expect *Action
117+
}{
118+
{
119+
"Set user agent",
120+
"my-custom-agent",
121+
&Action{
122+
config: config.Config{
123+
Network: config.Network{
124+
UserAgent: "my-custom-agent",
125+
},
126+
},
127+
},
128+
},
129+
{
130+
"Set empty user agent",
131+
"",
132+
&Action{
133+
config: config.Config{
134+
Network: config.Network{
135+
UserAgent: "",
136+
},
137+
},
138+
},
139+
},
140+
}
141+
142+
for _, tc := range cases {
143+
t.Run(tc.name, func(t *testing.T) {
144+
a := &Action{}
145+
opt := WithUserAgent(tc.input)
146+
opt(a)
147+
require.Equal(t, tc.expect, a)
148+
})
149+
}
150+
}
151+
112152
func TestWithBindPlanePassword(t *testing.T) {
113153
cases := []struct {
114154
name string

cmd/action/args.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func parseArgs() error {
1818
// Add one to account for arg 0 being the binary name
1919
count := argCount + 1
2020
if len(args) != count {
21-
return fmt.Errorf("Not enough arguments, expected 17, got %d. %s.", len(args), action.BugError)
21+
return fmt.Errorf("Not enough arguments, expected 18, got %d. %s.", len(args), action.BugError)
2222
}
2323

2424
// First arg is always the binary name, so we skip it. We could
@@ -64,6 +64,7 @@ func parseArgs() error {
6464
source_path = args[14]
6565
processor_path = args[15]
6666
github_url = args[16]
67+
user_agent = args[17]
6768

6869
return nil
6970
}

cmd/action/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
// include the binary name itself (which is returned by os.Args[0]).
1717
// When adding new arguments to the action, this number should be updated
1818
// and new global variables should be declared and handled in parseArgs().
19-
const argCount = 16
19+
const argCount = 17
2020

2121
// Global variables will be used when creating the action configuration. These
2222
// are the options set by the user. Their order in parseArgs() is important.
@@ -37,6 +37,7 @@ var (
3737
source_path string
3838
processor_path string
3939
github_url string
40+
user_agent string
4041
)
4142

4243
const (
@@ -100,6 +101,7 @@ func main() {
100101
action.WithBindPlaneUsername(bindplane_username),
101102
action.WithBindPlanePassword(bindplane_password),
102103
action.WithTLSCACert(tls_ca_cert),
104+
action.WithUserAgent(user_agent),
103105

104106
// Base action options for reading resources
105107
// from the repo, to apply to bindplane

cmd/action/validate.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"fmt"
55
"net/url"
66
"os"
7+
"path/filepath"
78

89
"github.com/observiq/bindplane-op-action/action"
910
"github.com/observiq/bindplane-op-action/internal/client/model"
11+
"github.com/observiq/bindplane-op-action/internal/glob"
1012
)
1113

1214
func validate() error {
@@ -136,12 +138,23 @@ func validateFilePaths() error {
136138
continue
137139
}
138140

139-
_, err := os.Stat(path)
140-
if err != nil {
141-
if os.IsNotExist(err) {
142-
return fmt.Errorf("%s path %s does not exist", kind, path)
141+
// Check if the path contains glob pattern characters
142+
if glob.ContainsGlobChars(path) {
143+
matches, err := filepath.Glob(path)
144+
if err != nil {
145+
return fmt.Errorf("globbing %s path %s: %w", kind, path, err)
146+
}
147+
if len(matches) == 0 {
148+
return fmt.Errorf("%s path %s does not match any files", kind, path)
149+
}
150+
} else {
151+
_, err := os.Stat(path)
152+
if err != nil {
153+
if os.IsNotExist(err) {
154+
return fmt.Errorf("%s path %s does not exist", kind, path)
155+
}
156+
return fmt.Errorf("stat %s path %s: %w", kind, path, err)
143157
}
144-
return fmt.Errorf("stat %s path %s: %w", kind, path, err)
145158
}
146159
}
147160

internal/client/client.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import (
1919
const (
2020
KeyHeader = "X-Bindplane-Api-Key"
2121

22-
DefaultTimeout = time.Second * 60
22+
DefaultTimeout = time.Second * 60
23+
DefaultUserAgent = "bindplane-op-action"
2324
)
2425

2526
type BindPlane struct {
@@ -44,6 +45,12 @@ func NewBindPlane(config *config.Config, logger *zap.Logger) (*BindPlane, error)
4445

4546
restryClient.SetBaseURL(fmt.Sprintf("%s/v1", config.Network.RemoteURL))
4647

48+
userAgent := DefaultUserAgent
49+
if config.Network.UserAgent != "" {
50+
userAgent = config.Network.UserAgent
51+
}
52+
restryClient.SetHeader("User-Agent", userAgent)
53+
4754
tlsConfig := &tls.Config{
4855
MinVersion: tls.VersionTLS13,
4956
}

internal/client/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Auth struct {
1414
type Network struct {
1515
RemoteURL string
1616
CertificateAuthority []string
17+
UserAgent string
1718
TLS
1819
}
1920

internal/glob/glob.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package glob
2+
3+
import "strings"
4+
5+
// ContainsGlobChars checks if a path contains glob pattern characters
6+
func ContainsGlobChars(path string) bool {
7+
return strings.ContainsAny(path, "*?[")
8+
}

0 commit comments

Comments
 (0)