Skip to content

Commit 14fd212

Browse files
committed
Add golangci-lint with CI integration and fix all lint issues
Add .golangci.yml (v2 format) with pragmatic linter config and golangci-lint-action step to CI workflow. Fix 70 lint issues across the codebase: errcheck for unchecked returns, errorlint for proper error wrapping, octal literal modernization, misspellings, prealloc suggestions, if-else to switch conversions, and removal of unused color variables.
1 parent e956524 commit 14fd212

36 files changed

Lines changed: 218 additions & 169 deletions

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ jobs:
1717
with:
1818
go-version-file: go.mod
1919

20+
- name: Lint
21+
uses: golangci/golangci-lint-action@v7
22+
with:
23+
version: latest
24+
2025
- name: Build
2126
run: go build ./cmd/netalchemy
2227

.golangci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 5m
5+
6+
linters:
7+
enable:
8+
- misspell
9+
- gocritic
10+
- bodyclose
11+
- nilerr
12+
- errorlint
13+
- prealloc
14+
15+
settings:
16+
gocritic:
17+
enabled-tags:
18+
- diagnostic
19+
- style
20+
disabled-checks:
21+
- unnamedResult
22+
- nestingReduce
23+
- sprintfQuotedString
24+
25+
misspell:
26+
locale: US
27+
28+
prealloc:
29+
simple: true
30+
for-loops: true
31+
32+
exclusions:
33+
rules:
34+
- path: _test\.go
35+
linters:
36+
- gocritic
37+
- prealloc
38+
- errorlint
39+
- errcheck
40+
41+
formatters:
42+
enable:
43+
- gofmt
44+
- goimports

pkg/build/checker.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ func expressionMatches(podLabels map[string]string, expr *policy.LabelExpression
322322

323323
// formatLabels formats a label map as {key=value, ...} for display.
324324
func formatLabels(labels map[string]string) string {
325-
var parts []string
325+
parts := make([]string, 0, len(labels))
326326
for k, v := range labels {
327327
parts = append(parts, fmt.Sprintf("%s=%s", k, v))
328328
}

pkg/build/generator.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ func (g *Generator) Generate() (*GenerateResult, error) {
204204
}, nil
205205
}
206206

207-
208207
// mergePolicies merges ingress and egress policies that target the same pod
209208
// selector in the same namespace. This reduces the number of NetworkPolicy
210209
// resources in clusters with many interconnected services.
@@ -254,7 +253,7 @@ func mergeKey(gp *GeneratedPolicy) string {
254253
}
255254
sort.Strings(keys)
256255

257-
var parts []string
256+
parts := make([]string, 0, len(keys))
258257
for _, k := range keys {
259258
parts = append(parts, k+"="+labels[k])
260259
}
@@ -393,18 +392,19 @@ func (g *Generator) createEgressNetworkPolicy(namespace, from string, fromSelect
393392
}
394393

395394
// Set name and pod selector based on source selector
396-
if fromSelector != nil && fromSelector.Type == policy.SelectorTypeLabelSelector && len(fromSelector.Labels) > 0 {
395+
switch {
396+
case fromSelector != nil && fromSelector.Type == policy.SelectorTypeLabelSelector && len(fromSelector.Labels) > 0:
397397
for _, v := range fromSelector.Labels {
398398
np.Name = fmt.Sprintf("egress-from-%s", util.SanitizeK8sName(v))
399399
break
400400
}
401401
np.Spec.PodSelector = metav1.LabelSelector{
402402
MatchLabels: fromSelector.Labels,
403403
}
404-
} else if fromSelector != nil && fromSelector.Type == policy.SelectorTypeNamespace {
404+
case fromSelector != nil && fromSelector.Type == policy.SelectorTypeNamespace:
405405
np.Name = fmt.Sprintf("egress-from-ns-%s", util.SanitizeK8sName(fromSelector.Namespace))
406406
np.Spec.PodSelector = metav1.LabelSelector{}
407-
} else {
407+
default:
408408
np.Name = "egress-allow"
409409
np.Spec.PodSelector = metav1.LabelSelector{}
410410
}
@@ -481,7 +481,8 @@ func (g *Generator) createBaseNetworkPolicy(namespace, to string, toSelector *po
481481
}
482482

483483
// Set name and pod selector based on target selector
484-
if toSelector != nil && toSelector.Type == policy.SelectorTypeLabelSelector && len(toSelector.Labels) > 0 {
484+
switch {
485+
case toSelector != nil && toSelector.Type == policy.SelectorTypeLabelSelector && len(toSelector.Labels) > 0:
485486
// Use the labels for pod selection (works for both same-namespace and cross-namespace)
486487
// Use the first label value for naming
487488
for _, v := range toSelector.Labels {
@@ -491,11 +492,11 @@ func (g *Generator) createBaseNetworkPolicy(namespace, to string, toSelector *po
491492
np.Spec.PodSelector = metav1.LabelSelector{
492493
MatchLabels: toSelector.Labels,
493494
}
494-
} else if toSelector != nil && toSelector.Type == policy.SelectorTypeNamespace {
495+
case toSelector != nil && toSelector.Type == policy.SelectorTypeNamespace:
495496
np.Name = fmt.Sprintf("allow-to-ns-%s", util.SanitizeK8sName(toSelector.Namespace))
496497
// Empty pod selector means all pods in namespace
497498
np.Spec.PodSelector = metav1.LabelSelector{}
498-
} else {
499+
default:
499500
np.Name = "allow-traffic"
500501
np.Spec.PodSelector = metav1.LabelSelector{}
501502
}
@@ -632,7 +633,6 @@ func protocolToK8s(p policy.Protocol) corev1.Protocol {
632633
}
633634
}
634635

635-
636636
// ToYAML converts a GeneratedPolicy to YAML
637637
func (gp *GeneratedPolicy) ToYAML() ([]byte, error) {
638638
return yaml.Marshal(gp.NetworkPolicy)
@@ -654,5 +654,5 @@ func (gp *GeneratedPolicy) WriteToFile(dir string) (string, error) {
654654
header += fmt.Sprintf("# Source rules: %s\n", strings.Join(gp.SourceRules, ", "))
655655
header += "---\n"
656656

657-
return path, os.WriteFile(path, []byte(header+string(data)), 0600)
657+
return path, os.WriteFile(path, []byte(header+string(data)), 0o600)
658658
}

pkg/cli/build.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ Examples:
4242
}
4343

4444
var (
45-
buildPolicyFile string
46-
buildOutput string
47-
buildNamespace string
48-
buildDryRun bool
49-
buildDefaultDeny bool
50-
buildCheckSelectors bool
45+
buildPolicyFile string
46+
buildOutput string
47+
buildNamespace string
48+
buildDryRun bool
49+
buildDefaultDeny bool
50+
buildCheckSelectors bool
5151
)
5252

5353
func init() {
@@ -58,7 +58,7 @@ func init() {
5858
buildCmd.Flags().BoolVar(&buildDefaultDeny, "default-deny", true, "Generate default-deny policy for each namespace (recommended)")
5959
buildCmd.Flags().BoolVar(&buildCheckSelectors, "check-selectors", false, "Query cluster to warn about selectors that don't match running pods/namespaces")
6060

61-
buildCmd.MarkFlagRequired("policy")
61+
_ = buildCmd.MarkFlagRequired("policy")
6262
}
6363

6464
func runBuild(cmd *cobra.Command, args []string) error {
@@ -179,7 +179,7 @@ func runBuild(cmd *cobra.Command, args []string) error {
179179
fmt.Printf("Run without --dry-run to write to: %s\n", colorBold(buildOutput))
180180
} else {
181181
// Create output directory
182-
if err := os.MkdirAll(buildOutput, 0755); err != nil {
182+
if err := os.MkdirAll(buildOutput, 0o755); err != nil {
183183
return fmt.Errorf("failed to create output directory: %w", err)
184184
}
185185

pkg/cli/colors.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,9 @@ var (
1717
colorDeny = color.New(color.FgRed).SprintFunc()
1818

1919
// Diff colors
20-
colorAdded = color.New(color.FgGreen).SprintFunc()
21-
colorRemoved = color.New(color.FgRed).SprintFunc()
22-
colorModified = color.New(color.FgYellow).SprintFunc()
20+
colorAdded = color.New(color.FgGreen).SprintFunc()
2321

2422
// Emphasis
2523
colorBold = color.New(color.Bold).SprintFunc()
2624
colorDim = color.New(color.Faint).SprintFunc()
27-
28-
// Printers for full-line colored output
29-
printSuccess = color.New(color.FgGreen).PrintlnFunc()
30-
printError = color.New(color.FgRed).PrintlnFunc()
31-
printWarning = color.New(color.FgYellow).PrintlnFunc()
32-
printInfo = color.New(color.FgCyan).PrintlnFunc()
3325
)

pkg/cli/define.go

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

33
import (
44
"bufio"
5+
"errors"
56
"fmt"
67
"io"
78
"os"
@@ -17,7 +18,7 @@ import (
1718
// Returns the trimmed input and any error (EOF is returned as-is for caller to handle).
1819
func readLine(reader *bufio.Reader) (string, error) {
1920
input, err := reader.ReadString('\n')
20-
if err != nil && err != io.EOF {
21+
if err != nil && !errors.Is(err, io.EOF) {
2122
return "", fmt.Errorf("failed to read input: %w", err)
2223
}
2324
return strings.TrimSpace(input), err
@@ -26,7 +27,7 @@ func readLine(reader *bufio.Reader) (string, error) {
2627
// readLineOrDefault reads a line, returning defaultVal on EOF or empty input.
2728
func readLineOrDefault(reader *bufio.Reader, defaultVal string) (string, error) {
2829
input, err := readLine(reader)
29-
if err == io.EOF || input == "" {
30+
if errors.Is(err, io.EOF) || input == "" {
3031
return defaultVal, nil
3132
}
3233
if err != nil {
@@ -403,7 +404,6 @@ func formatRuleSummary(r policy.Rule) string {
403404
return fmt.Sprintf("%s → %s%s", from, to, port)
404405
}
405406

406-
407407
// generateRuleName creates a unique name for a rule
408408
func generateRuleName(r policy.Rule, existing map[string]bool) string {
409409
from := util.ShortenSelector(r.From)
@@ -534,7 +534,7 @@ func defineFromObserved(from, output string, interactive bool) error {
534534
return fmt.Errorf("failed to generate YAML: %w", err)
535535
}
536536

537-
if err := os.WriteFile(output, []byte(header+string(yamlData)), 0600); err != nil {
537+
if err := os.WriteFile(output, []byte(header+string(yamlData)), 0o600); err != nil {
538538
return fmt.Errorf("failed to write output file: %w", err)
539539
}
540540

@@ -647,7 +647,7 @@ func defineInteractively(output string) error {
647647
// Name
648648
fmt.Print("Rule name (or 'done' to finish): ")
649649
name, err := readLine(reader)
650-
if err == io.EOF || name == "done" || name == "" {
650+
if errors.Is(err, io.EOF) || name == "done" || name == "" {
651651
break
652652
}
653653
if err != nil {
@@ -659,23 +659,23 @@ func defineInteractively(output string) error {
659659
// Description
660660
fmt.Print("Description: ")
661661
desc, err := readLine(reader)
662-
if err != nil && err != io.EOF {
662+
if err != nil && !errors.Is(err, io.EOF) {
663663
return err
664664
}
665665
rule.Description = desc
666666

667667
// From
668668
fmt.Print("From (e.g., app=grafana, namespace=o11y, 10.0.0.0/8, or empty): ")
669669
from, err := readLine(reader)
670-
if err != nil && err != io.EOF {
670+
if err != nil && !errors.Is(err, io.EOF) {
671671
return err
672672
}
673673
rule.From = from
674674

675675
// To
676676
fmt.Print("To (e.g., app=postgres, namespace=db, or empty): ")
677677
to, err := readLine(reader)
678-
if err != nil && err != io.EOF {
678+
if err != nil && !errors.Is(err, io.EOF) {
679679
return err
680680
}
681681
rule.To = to
@@ -691,7 +691,7 @@ func defineInteractively(output string) error {
691691
// Port
692692
fmt.Print("Port (e.g., 80 or 80-443, or empty): ")
693693
port, err := readLine(reader)
694-
if err != nil && err != io.EOF {
694+
if err != nil && !errors.Is(err, io.EOF) {
695695
return err
696696
}
697697
if port != "" {
@@ -717,10 +717,10 @@ func defineInteractively(output string) error {
717717
}
718718

719719
// Validate
720-
errors := policy.ValidateRule(&rule)
721-
if len(errors) > 0 {
720+
validationErrs := policy.ValidateRule(&rule)
721+
if len(validationErrs) > 0 {
722722
fmt.Println("Warning: Rule has validation errors:")
723-
for _, e := range errors {
723+
for _, e := range validationErrs {
724724
fmt.Printf(" - %s\n", e.Error())
725725
}
726726
fmt.Print("Add anyway? (y/n): ")
@@ -753,7 +753,7 @@ func defineInteractively(output string) error {
753753
return fmt.Errorf("failed to generate YAML: %w", err)
754754
}
755755

756-
if err := os.WriteFile(output, []byte(header+string(yamlData)), 0600); err != nil {
756+
if err := os.WriteFile(output, []byte(header+string(yamlData)), 0o600); err != nil {
757757
return fmt.Errorf("failed to write output file: %w", err)
758758
}
759759

@@ -763,7 +763,7 @@ func defineInteractively(output string) error {
763763

764764
// parsePortRange parses a port or port range string (e.g., "80" or "80-443").
765765
// Returns portMin, portMax, and any error.
766-
func parsePortRange(s string) (int, int, error) {
766+
func parsePortRange(s string) (portMin, portMax int, err error) {
767767
s = strings.TrimSpace(s)
768768
if s == "" {
769769
return 0, 0, nil
@@ -775,27 +775,27 @@ func parsePortRange(s string) (int, int, error) {
775775
return 0, 0, fmt.Errorf("invalid port range format: %s", s)
776776
}
777777

778-
min, err := strconv.Atoi(strings.TrimSpace(parts[0]))
779-
if err != nil {
780-
return 0, 0, fmt.Errorf("invalid port number %q: %w", parts[0], err)
778+
lo, parseErr := strconv.Atoi(strings.TrimSpace(parts[0]))
779+
if parseErr != nil {
780+
return 0, 0, fmt.Errorf("invalid port number %q: %w", parts[0], parseErr)
781781
}
782782

783-
max, err := strconv.Atoi(strings.TrimSpace(parts[1]))
784-
if err != nil {
785-
return 0, 0, fmt.Errorf("invalid port number %q: %w", parts[1], err)
783+
hi, parseErr := strconv.Atoi(strings.TrimSpace(parts[1]))
784+
if parseErr != nil {
785+
return 0, 0, fmt.Errorf("invalid port number %q: %w", parts[1], parseErr)
786786
}
787787

788-
if min < 0 || min > 65535 {
789-
return 0, 0, fmt.Errorf("port %d out of range (0-65535)", min)
788+
if lo < 0 || lo > 65535 {
789+
return 0, 0, fmt.Errorf("port %d out of range (0-65535)", lo)
790790
}
791-
if max < 0 || max > 65535 {
792-
return 0, 0, fmt.Errorf("port %d out of range (0-65535)", max)
791+
if hi < 0 || hi > 65535 {
792+
return 0, 0, fmt.Errorf("port %d out of range (0-65535)", hi)
793793
}
794-
if min > max {
795-
return 0, 0, fmt.Errorf("port range start %d is greater than end %d", min, max)
794+
if lo > hi {
795+
return 0, 0, fmt.Errorf("port range start %d is greater than end %d", lo, hi)
796796
}
797797

798-
return min, max, nil
798+
return lo, hi, nil
799799
}
800800

801801
// Single port

0 commit comments

Comments
 (0)