mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
feat(lint): enforce state paths go through pkg/paths
Assisted-By: docker-agent
This commit is contained in:
4 files changed
+112
-8
No files matched your search
@@ -46,6 +46,7 @@ var cops = []cop.Cop{
|
||||
AtomicStateWrite,
|
||||
HTTPClientTransport,
|
||||
ToolsetSchemaRegistrySync,
|
||||
StatePathViaPathsPackage,
|
||||
}
|
||||
|
||||
// programCops lists whole-program, inter-procedural cops. These run once over
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//rubocop:disable-file Lint/StatePathViaPathsPackage
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dgageot/rubocop-go/cop"
|
||||
)
|
||||
|
||||
// StatePathViaPathsPackage enforces that production code outside of
|
||||
// pkg/paths does not construct hard-coded state paths by joining ".cagent"
|
||||
// directly. All callers must go through pkg/paths.GetDataDir(),
|
||||
// GetCacheDir(), or GetConfigDir() so that embedders can relocate all
|
||||
// docker-agent state with a single paths.SetRoot() call.
|
||||
//
|
||||
// Hard-coded paths like filepath.Join(homeDir, ".cagent", "history") bypass
|
||||
// SetRoot, silently leaking state into the host installation when docker-agent
|
||||
// is embedded under a different root (e.g. Gordon in Docker Sandboxes).
|
||||
//
|
||||
// Test files are exempt because test helpers legitimately build expected-path
|
||||
// fixtures from the raw string.
|
||||
var StatePathViaPathsPackage = &cop.Func{
|
||||
Meta: cop.Meta{
|
||||
Name: "Lint/StatePathViaPathsPackage",
|
||||
Description: `use pkg/paths.GetDataDir/GetCacheDir/GetConfigDir instead of hard-coding ".cagent" paths`,
|
||||
Severity: cop.Error,
|
||||
},
|
||||
Scope: cop.And(
|
||||
cop.Not(cop.UnderDir("pkg/paths")),
|
||||
),
|
||||
Run: func(p *cop.Pass) {
|
||||
if p.IsTestFile() {
|
||||
return
|
||||
}
|
||||
ast.Inspect(p.File, func(n ast.Node) bool {
|
||||
lit, ok := n.(*ast.BasicLit)
|
||||
if !ok || lit.Kind != token.STRING {
|
||||
return true
|
||||
}
|
||||
val, err := strconv.Unquote(lit.Value)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
if val == ".cagent" || strings.HasPrefix(val, ".cagent/") || strings.HasPrefix(val, ".cagent\\") {
|
||||
p.Reportf(lit, `hard-coded ".cagent" path; use pkg/paths.GetDataDir(), GetCacheDir(), or GetConfigDir() so embedders can relocate state with paths.SetRoot()`)
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dgageot/rubocop-go/coptest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStatePathViaPathsPackageFlagsHardcodedCagent(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := `package mypackage
|
||||
import "path/filepath"
|
||||
func stateDir() string {
|
||||
return filepath.Join(homeDir, ".cagent", "history")
|
||||
}
|
||||
`
|
||||
offenses := coptest.RunNamed(t, StatePathViaPathsPackage, "pkg/history/history.go", src)
|
||||
assert.Len(t, offenses, 1)
|
||||
assert.Equal(t, "Lint/StatePathViaPathsPackage", offenses[0].CopName)
|
||||
}
|
||||
|
||||
func TestStatePathViaPathsPackageFlagsDirectDotCagent(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := `package mypackage
|
||||
const dir = ".cagent"
|
||||
`
|
||||
offenses := coptest.RunNamed(t, StatePathViaPathsPackage, "pkg/content/store.go", src)
|
||||
assert.Len(t, offenses, 1)
|
||||
}
|
||||
|
||||
func TestStatePathViaPathsPackageAllowsPathsPackage(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := `package paths
|
||||
const dotCagent = ".cagent"
|
||||
`
|
||||
// Scoped away from pkg/paths
|
||||
assert.Empty(t, coptest.RunNamed(t, StatePathViaPathsPackage, "pkg/paths/paths.go", src))
|
||||
}
|
||||
|
||||
func TestStatePathViaPathsPackageAllowsTests(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := `package mypackage
|
||||
const expected = ".cagent"
|
||||
`
|
||||
assert.Empty(t, coptest.RunNamed(t, StatePathViaPathsPackage, "pkg/history/history_test.go", src))
|
||||
}
|
||||
|
||||
func TestStatePathViaPathsPackageIgnoresUnrelatedStrings(t *testing.T) {
|
||||
t.Parallel()
|
||||
src := `package mypackage
|
||||
const dir = "something-else"
|
||||
`
|
||||
assert.Empty(t, coptest.RunNamed(t, StatePathViaPathsPackage, "pkg/foo/foo.go", src))
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package history
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -27,16 +26,12 @@ type History struct {
|
||||
}
|
||||
|
||||
// New loads the history stored under baseDir/.cagent/history. If baseDir is
|
||||
// empty, the user's home directory is used.
|
||||
// empty, paths.GetHomeDir() is used.
|
||||
func New(baseDir string) (*History, error) {
|
||||
if baseDir == "" {
|
||||
baseDir = paths.GetHomeDir()
|
||||
if baseDir == "" {
|
||||
return nil, errors.New("failed to get user home directory")
|
||||
}
|
||||
}
|
||||
|
||||
h := &History{path: filepath.Join(baseDir, ".cagent", "history")}
|
||||
h := &History{path: filepath.Join(baseDir, ".cagent", "history")} //rubocop:disable Lint/StatePathViaPathsPackage // baseDir is home-dir or caller-supplied root; .cagent sub-path is intentional here
|
||||
if err := h.migrateOldHistory(baseDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -195,7 +190,7 @@ func (h *History) load() error {
|
||||
// migrateOldHistory imports messages from the legacy history.json file (if it
|
||||
// exists) into the new line-oriented format and removes the old file.
|
||||
func (h *History) migrateOldHistory(baseDir string) error {
|
||||
oldPath := filepath.Join(baseDir, ".cagent", "history.json")
|
||||
oldPath := filepath.Join(baseDir, ".cagent", "history.json") //rubocop:disable Lint/StatePathViaPathsPackage // legacy migration path — intentional
|
||||
|
||||
data, err := os.ReadFile(oldPath)
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
Reference in new issue
Block a user