Skip to content

Commit 9b08ea2

Browse files
authored
ci: add golangci-lint and fix all reported issues (#6)
Adopt the maratori "golden config" (golangci-lint v2.7.1) and run it in CI via a new lint job in build.yml. Only deviation from the upstream config is local-prefixes set to the module path. Resolve every issue the config reports (108 -> 0) in code, with no config relaxations and no nolint directives: - Extract generic polling helpers (pollUntilReady, pollUntilGone, deleteAndWait, diffStrings) in provider/util.go, collapsing the repetitive per-resource Create/Delete closures. This clears dupl, most govet shadow hits, and gocognit on vmResource.Update. - Split the mock HTTP wiring funcs into one method per handler to bring cognitive complexity under threshold (wireVMs was 62). - Move the mock publicIPCounter onto the Server struct (gochecknoglobals). - Introduce named constants for mode/status strings and magic numbers; add json tags for musttag. - Move validator tests to an external _test package; rename util_test.go to util_internal_test.go (it needs an unexported symbol). Behavior is preserved; the full test suite, go vet, gofmt and the docs-stale check all pass. SG delete now detects terminal-failure status via the shared pollUntilGone helper, matching the other resources.
1 parent 122f77a commit 9b08ea2

37 files changed

Lines changed: 1828 additions & 920 deletions

.github/workflows/build.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ on:
77
branches: [main]
88

99
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-go@v5
15+
with:
16+
go-version: "1.25"
17+
check-latest: true
18+
- name: golangci-lint
19+
uses: golangci/golangci-lint-action@v8
20+
with:
21+
version: v2.7.1
22+
1023
build:
1124
runs-on: ubuntu-latest
1225
steps:

.golangci.yml

Lines changed: 466 additions & 0 deletions
Large diffs are not rendered by default.

internal/client/client.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@ import (
1212
"io"
1313
"net/http"
1414
"net/url"
15+
"slices"
1516
"strconv"
1617
"strings"
1718
"time"
1819
)
1920

2021
const DefaultEndpoint = "https://api.fluence.dev"
2122

23+
const (
24+
// defaultHTTPTimeout bounds every request the client makes.
25+
defaultHTTPTimeout = 60 * time.Second
26+
// maxErrorBodyLen caps how much of an undecodable response body is echoed
27+
// back in an error message.
28+
maxErrorBodyLen = 256
29+
)
30+
2231
type Client struct {
2332
endpoint string
2433
apiKey string
@@ -38,7 +47,7 @@ func New(endpoint, apiKey string, opts ...Option) *Client {
3847
c := &Client{
3948
endpoint: strings.TrimRight(endpoint, "/"),
4049
apiKey: apiKey,
41-
http: &http.Client{Timeout: 60 * time.Second},
50+
http: &http.Client{Timeout: defaultHTTPTimeout},
4251
ua: "terraform-provider-cloudless",
4352
}
4453
for _, o := range opts {
@@ -99,7 +108,7 @@ func (c *Client) do(ctx context.Context, method, path string, query url.Values,
99108
// spec also describes an apiKey scheme with header name `X-API-KEY`. We
100109
// send both so either gateway configuration works.
101110
req.Header.Set("Authorization", "X-API-KEY "+c.apiKey)
102-
req.Header.Set("X-API-KEY", c.apiKey)
111+
req.Header.Set("X-Api-Key", c.apiKey)
103112
req.Header.Set("User-Agent", c.ua)
104113

105114
resp, err := c.http.Do(req)
@@ -125,8 +134,8 @@ func (c *Client) do(ctx context.Context, method, path string, query url.Values,
125134
}
126135

127136
if out != nil && len(respBody) > 0 {
128-
if err := json.Unmarshal(respBody, out); err != nil {
129-
return fmt.Errorf("decode response: %w (body=%q)", err, truncate(string(respBody), 256))
137+
if err = json.Unmarshal(respBody, out); err != nil {
138+
return fmt.Errorf("decode response: %w (body=%q)", err, truncate(string(respBody), maxErrorBodyLen))
130139
}
131140
}
132141
return nil
@@ -443,7 +452,14 @@ func (c *Client) AddVMStorages(ctx context.Context, vmID string, storageIDs []st
443452
}
444453

445454
func (c *Client) RemoveVMStorages(ctx context.Context, vmID string, storageIDs []string) error {
446-
return c.do(ctx, http.MethodPost, "/v2/vms/"+vmID+"/storages/remove", nil, vmStoragesBody{DataDisks: storageIDs}, nil)
455+
return c.do(
456+
ctx,
457+
http.MethodPost,
458+
"/v2/vms/"+vmID+"/storages/remove",
459+
nil,
460+
vmStoragesBody{DataDisks: storageIDs},
461+
nil,
462+
)
447463
}
448464

449465
func (c *Client) AddVMPublicIP(ctx context.Context, vmID, publicIPID string) error {
@@ -490,17 +506,15 @@ func (c *Client) FindVMByInterface(ctx context.Context, interfaceID string) (*VM
490506
// pages, indexing pages 1..N as the server reports them.
491507
const maxIters = 10000 // ~2M VMs at per_page=200; defensive cap if pagination metadata never converges.
492508
nextPage := uint64(1)
493-
for iter := 0; iter < maxIters; iter++ {
509+
for range maxIters {
494510
q := url.Values{"page": {FormatPage(nextPage)}, "per_page": {"200"}}
495511
var resp vmsListResponse
496512
if err := c.do(ctx, http.MethodGet, "/v2/vms", q, nil, &resp); err != nil {
497513
return nil, err
498514
}
499515
for i := range resp.Items {
500-
for _, ni := range resp.Items[i].NetworkInterfaces {
501-
if ni == interfaceID {
502-
return &resp.Items[i], nil
503-
}
516+
if slices.Contains(resp.Items[i].NetworkInterfaces, interfaceID) {
517+
return &resp.Items[i], nil
504518
}
505519
}
506520
if resp.Pagination.CurrentPage >= uint64(resp.Pagination.TotalPages) {
@@ -594,6 +608,7 @@ func (c *Client) ListDatacenters(ctx context.Context) ([]Datacenter, error) {
594608
// callers to do the join themselves.
595609
type EnrichedCluster struct {
596610
Cluster
611+
597612
Region string // = Datacenter.CountryCode
598613
CityCode string
599614
DCSlug string
@@ -731,7 +746,7 @@ func (p *ProtocolKind) UnmarshalJSON(b []byte) error {
731746
p.Ports = v.Ports
732747
return nil
733748
}
734-
return fmt.Errorf("ProtocolKind: empty object")
749+
return errors.New("ProtocolKind: empty object")
735750
}
736751

737752
// Ports is "all" or {exact:{value:N}} or {range:{min:M,max:N}}.
@@ -752,7 +767,7 @@ func (p Ports) MarshalJSON() ([]byte, error) {
752767
if p.RangeMin != nil && p.RangeMax != nil {
753768
return json.Marshal(map[string]any{"range": map[string]any{"min": *p.RangeMin, "max": *p.RangeMax}})
754769
}
755-
return nil, fmt.Errorf("Ports: empty value")
770+
return nil, errors.New("Ports: empty value")
756771
}
757772

758773
func (p *Ports) UnmarshalJSON(b []byte) error {
@@ -761,7 +776,10 @@ func (p *Ports) UnmarshalJSON(b []byte) error {
761776
p.All = true
762777
return nil
763778
}
764-
type rangePart struct{ Min, Max uint16 }
779+
type rangePart struct {
780+
Min uint16 `json:"min"`
781+
Max uint16 `json:"max"`
782+
}
765783
var probe struct {
766784
Exact *struct {
767785
Value uint16 `json:"value"`
@@ -795,7 +813,7 @@ func (r SGRemote) MarshalJSON() ([]byte, error) {
795813
if r.SecurityGroup != nil {
796814
return json.Marshal(map[string]string{"securityGroup": *r.SecurityGroup})
797815
}
798-
return nil, fmt.Errorf("SGRemote: empty")
816+
return nil, errors.New("SGRemote: empty")
799817
}
800818

801819
func (r *SGRemote) UnmarshalJSON(b []byte) error {
@@ -851,7 +869,11 @@ func (c *Client) GetSecurityGroup(ctx context.Context, id string) (*SecurityGrou
851869
return nil, &APIError{StatusCode: http.StatusNotFound, Message: "security group not found"}
852870
}
853871

854-
func (c *Client) UpdateSecurityGroup(ctx context.Context, id string, req UpdateSecurityGroupRequest) (*SecurityGroup, error) {
872+
func (c *Client) UpdateSecurityGroup(
873+
ctx context.Context,
874+
id string,
875+
req UpdateSecurityGroupRequest,
876+
) (*SecurityGroup, error) {
855877
var out SecurityGroup
856878
if err := c.do(ctx, http.MethodPatch, "/v1/security_groups/"+id, nil, req, &out); err != nil {
857879
return nil, err

internal/client/mock/clusters.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func (s *Server) wireClusters() {
1717
}
1818
s.mu.Unlock()
1919

20-
s.mux.HandleFunc("/v1/clusters", func(w http.ResponseWriter, r *http.Request) {
20+
s.mux.HandleFunc("/v1/clusters", func(w http.ResponseWriter, _ *http.Request) {
2121
s.mu.Lock()
2222
defer s.mu.Unlock()
2323
out := []map[string]any{}
@@ -41,7 +41,7 @@ func (s *Server) wireDCs() {
4141
}
4242
s.mu.Unlock()
4343

44-
s.mux.HandleFunc("/v1/datacenters", func(w http.ResponseWriter, r *http.Request) {
44+
s.mux.HandleFunc("/v1/datacenters", func(w http.ResponseWriter, _ *http.Request) {
4545
s.mu.Lock()
4646
defer s.mu.Unlock()
4747
out := []map[string]any{}
@@ -64,6 +64,9 @@ func (s *Server) SeedCluster(id, name, dcID string) {
6464
s.clusterMap[id] = map[string]any{"id": id, "name": name, "dc_id": dcID}
6565
}
6666

67+
// mockDatacenterTier is the fixed tier value seeded for mock datacenter rows.
68+
const mockDatacenterTier = 3
69+
6770
// SeedDatacenter registers a datacenter row.
6871
func (s *Server) SeedDatacenter(id, country, city, slug string) {
6972
s.mu.Lock()
@@ -73,6 +76,6 @@ func (s *Server) SeedDatacenter(id, country, city, slug string) {
7376
}
7477
s.dcMap[id] = map[string]any{
7578
"id": id, "countryCode": country, "cityCode": city,
76-
"index": 0, "tier": 3, "certifications": []string{}, "slug": slug,
79+
"index": 0, "tier": mockDatacenterTier, "certifications": []string{}, "slug": slug,
7780
}
7881
}

0 commit comments

Comments
 (0)