| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "go/ast" |
| 5 | "go/parser" |
| 6 | "go/token" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "sort" |
| 10 | "strconv" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | ) |
| 14 | |
| 15 | // Region HA spec 2 §25: the provider boundary. Spec 2's core names no |
| 16 | // cloud provider; its AWS parts are adapters in files named for AWS. Each |
| 17 | // plan task that adds a core file lists it here in the same commit. |
| 18 | |
| 19 | // regionHACoreFiles are spec 2's provider-neutral files. None may import |
| 20 | // an AWS package. |
| 21 | var regionHACoreFiles = []string{ |
| 22 | "internal/lbhealth/lbhealth.go", |
| 23 | "internal/hostactors/roles.go", |
| 24 | "internal/boxchoice/policy.go", |
| 25 | "internal/host/boxaddress.go", |
| 26 | "internal/edgesfile/private.go", |
| 27 | "internal/edge/lbhealth.go", |
| 28 | "internal/console/apiboxroles.go", |
| 29 | "system_roles.go", |
| 30 | "internal/console/nocapacity.go", |
| 31 | "internal/hostops/chosen.go", |
| 32 | "internal/console/regioncapacity.go", |
| 33 | "internal/hostenv/hostenv.go", |
| 34 | "internal/hostenv/cli.go", |
| 35 | "internal/hostenv/write_linux.go", |
| 36 | "internal/hostenv/write_other.go", |
| 37 | "internal/host/installchannel.go", |
| 38 | "internal/hostactors/membershipread.go", |
| 39 | "internal/hostactors/groupref.go", |
| 40 | "internal/hostactors/autoenroll.go", |
| 41 | "internal/console/enrollpass.go", |
| 42 | "internal/host/joinmode.go", |
| 43 | "internal/host/boxreference.go", |
| 44 | "internal/status/fleet.go", |
| 45 | "internal/host/joinroot.go", |
| 46 | "internal/host/joining.go", |
| 47 | } |
| 48 | |
| 49 | // regionHAAdapterFiles are spec 2's AWS adapters. Each is named for AWS so |
| 50 | // a reader can see where the provider lives. |
| 51 | var regionHAAdapterFiles = []string{ |
| 52 | "internal/host/awsaddr.go", |
| 53 | "internal/console/awsregionmetric.go", |
| 54 | "internal/hostenv/awsssm.go", |
| 55 | "internal/hostactors/awsmembership/awsmembership.go", |
| 56 | "internal/console/awsmembership.go", |
| 57 | } |
| 58 | |
| 59 | // regionHAPackages are the packages spec 2 touches. Every S3 client built |
| 60 | // in them goes through s3compat.Options(), so CARLOS_S3_ENDPOINT applies. |
| 61 | var regionHAPackages = []string{ |
| 62 | "internal/activator", |
| 63 | "internal/boxchoice", |
| 64 | "internal/console", |
| 65 | "internal/edge", |
| 66 | "internal/edgesfile", |
| 67 | "internal/host", |
| 68 | "internal/hostactors", |
| 69 | "internal/hostactors/awsmembership", |
| 70 | "internal/hostenv", |
| 71 | "internal/hostops", |
| 72 | "internal/lbhealth", |
| 73 | } |
| 74 | |
| 75 | // regionHAS3Exceptions are S3 clients on main that skip s3compat, each |
| 76 | // with the discussion that fixes it outside spec 2. A listed file that has |
| 77 | // since been fixed passes. |
| 78 | var regionHAS3Exceptions = map[string]string{ |
| 79 | "internal/host/claimproduction_marker.go": "#298", |
| 80 | } |
| 81 | |
| 82 | func TestRegionHACoreImportsNoAWS(t *testing.T) { |
| 83 | for _, f := range regionHACoreFiles { |
| 84 | file, err := parser.ParseFile(token.NewFileSet(), f, nil, parser.ImportsOnly) |
| 85 | if err != nil { |
| 86 | t.Fatalf("%s: %v", f, err) |
| 87 | } |
| 88 | for _, imp := range file.Imports { |
| 89 | path, _ := strconv.Unquote(imp.Path.Value) |
| 90 | if strings.HasPrefix(path, "github.com/aws/") { |
| 91 | t.Errorf("%s imports %s: spec 2's core names no provider (§25); move it to an AWS adapter", f, path) |
| 92 | } |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | func TestRegionHAAdaptersAreNamedForAWS(t *testing.T) { |
| 98 | for _, f := range regionHAAdapterFiles { |
| 99 | if _, err := os.Stat(f); err != nil { |
| 100 | t.Errorf("%s: %v", f, err) |
| 101 | } |
| 102 | if !strings.Contains(filepath.Base(f), "aws") && !strings.Contains(filepath.Dir(f), "aws") { |
| 103 | t.Errorf("%s is an AWS adapter but its name does not say so", f) |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | // s3NewFromConfigCalls returns the line of each s3.NewFromConfig call in |
| 109 | // src that does not pass s3compat.Options() as an argument. |
| 110 | func s3NewFromConfigCalls(t *testing.T, path string) []int { |
| 111 | t.Helper() |
| 112 | fset := token.NewFileSet() |
| 113 | f, err := parser.ParseFile(fset, path, nil, 0) |
| 114 | if err != nil { |
| 115 | t.Fatalf("%s: %v", path, err) |
| 116 | } |
| 117 | s3Names, compatNames := map[string]bool{}, map[string]bool{} |
| 118 | for _, imp := range f.Imports { |
| 119 | p, _ := strconv.Unquote(imp.Path.Value) |
| 120 | name := filepath.Base(p) |
| 121 | if imp.Name != nil { |
| 122 | name = imp.Name.Name |
| 123 | } |
| 124 | switch p { |
| 125 | case "github.com/aws/aws-sdk-go-v2/service/s3": |
| 126 | s3Names[name] = true |
| 127 | case "github.com/carlosframework/platform/internal/s3compat": |
| 128 | compatNames[name] = true |
| 129 | } |
| 130 | } |
| 131 | var bad []int |
| 132 | ast.Inspect(f, func(n ast.Node) bool { |
| 133 | call, ok := n.(*ast.CallExpr) |
| 134 | if !ok { |
| 135 | return true |
| 136 | } |
| 137 | sel, ok := call.Fun.(*ast.SelectorExpr) |
| 138 | if !ok || sel.Sel.Name != "NewFromConfig" { |
| 139 | return true |
| 140 | } |
| 141 | pkg, ok := sel.X.(*ast.Ident) |
| 142 | if !ok || !s3Names[pkg.Name] { |
| 143 | return true |
| 144 | } |
| 145 | for _, arg := range call.Args { |
| 146 | inner, ok := arg.(*ast.CallExpr) |
| 147 | if !ok { |
| 148 | continue |
| 149 | } |
| 150 | isel, ok := inner.Fun.(*ast.SelectorExpr) |
| 151 | if !ok || isel.Sel.Name != "Options" { |
| 152 | continue |
| 153 | } |
| 154 | if id, ok := isel.X.(*ast.Ident); ok && compatNames[id.Name] { |
| 155 | return true |
| 156 | } |
| 157 | } |
| 158 | bad = append(bad, fset.Position(call.Pos()).Line) |
| 159 | return true |
| 160 | }) |
| 161 | return bad |
| 162 | } |
| 163 | |
| 164 | func TestRegionHAS3ClientsUseS3Compat(t *testing.T) { |
| 165 | for _, dir := range regionHAPackages { |
| 166 | paths, err := filepath.Glob(filepath.Join(dir, "*.go")) |
| 167 | if err != nil { |
| 168 | t.Fatal(err) |
| 169 | } |
| 170 | sort.Strings(paths) |
| 171 | for _, p := range paths { |
| 172 | if strings.HasSuffix(p, "_test.go") { |
| 173 | continue |
| 174 | } |
| 175 | if _, ok := regionHAS3Exceptions[filepath.ToSlash(p)]; ok { |
| 176 | continue |
| 177 | } |
| 178 | for _, line := range s3NewFromConfigCalls(t, p) { |
| 179 | t.Errorf("%s:%d builds an S3 client without s3compat.Options() (§25): CARLOS_S3_ENDPOINT would not apply", p, line) |
| 180 | } |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 | |
| 185 | // The checker itself: a bare client is caught, one with Options() is not, |
| 186 | // whatever the import names. |
| 187 | func TestRegionHAS3CheckerCatchesABareClient(t *testing.T) { |
| 188 | dir := t.TempDir() |
| 189 | write := func(name, body string) string { |
| 190 | p := filepath.Join(dir, name) |
| 191 | if err := os.WriteFile(p, []byte(body), 0o644); err != nil { |
| 192 | t.Fatal(err) |
| 193 | } |
| 194 | return p |
| 195 | } |
| 196 | bare := write("bare.go", `package x |
| 197 | import awss3 "github.com/aws/aws-sdk-go-v2/service/s3" |
| 198 | func f(cfg any) { _ = awss3.NewFromConfig(cfg) } |
| 199 | `) |
| 200 | good := write("good.go", `package x |
| 201 | import ( |
| 202 | "github.com/aws/aws-sdk-go-v2/service/s3" |
| 203 | "github.com/carlosframework/platform/internal/s3compat" |
| 204 | ) |
| 205 | func f(cfg any) { _ = s3.NewFromConfig(cfg, s3compat.Options()) } |
| 206 | `) |
| 207 | if got := s3NewFromConfigCalls(t, bare); len(got) != 1 { |
| 208 | t.Fatalf("bare client: %v", got) |
| 209 | } |
| 210 | if got := s3NewFromConfigCalls(t, good); len(got) != 0 { |
| 211 | t.Fatalf("compat client flagged: %v", got) |
| 212 | } |
| 213 | } |
| 214 | |