Skip to content

Commit c17c6d0

Browse files
committed
Update GitHub builds
* Adjust chmod for licenses, queries * Adjust license aggregation Issues: [PGO-2695]
1 parent 5a08d75 commit c17c6d0

File tree

3 files changed

+233
-2
lines changed

3 files changed

+233
-2
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
/.git
44
/bin
55
/hack
6+
!/hack/extract-licenses.go
67
!/hack/tools/queries

Dockerfile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ COPY hack/tools/queries /opt/crunchy/conf
1010
WORKDIR /usr/src/app
1111
COPY . .
1212
ENV GOCACHE=/var/cache/go
13+
14+
# Build the operator and assemble the licenses
1315
RUN --mount=type=cache,target=/var/cache/go go build ./cmd/postgres-operator
16+
RUN go run ./hack/extract-licenses.go licenses postgres-operator
1417

1518
FROM docker.io/library/debian:bookworm
1619

17-
COPY --from=build /licenses /licenses
18-
COPY --from=build /opt/crunchy/conf /opt/crunchy/conf
20+
COPY --from=build --chmod=0444 /usr/src/app/licenses /licenses
21+
COPY --from=build --chmod=0444 /opt/crunchy/conf /opt/crunchy/conf
1922
COPY --from=build /usr/src/app/postgres-operator /usr/local/bin
2023

2124
USER 2

hack/extract-licenses.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//go:build go1.21
2+
3+
package main
4+
5+
import (
6+
"bytes"
7+
"context"
8+
"encoding/csv"
9+
"encoding/json"
10+
"errors"
11+
"flag"
12+
"fmt"
13+
"io"
14+
"io/fs"
15+
"os"
16+
"os/exec"
17+
"os/signal"
18+
"path/filepath"
19+
"slices"
20+
"strings"
21+
"syscall"
22+
)
23+
24+
func main() {
25+
flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
26+
flags.Usage = func() {
27+
fmt.Fprintln(flags.Output(), strings.TrimSpace(`
28+
Usage: `+flags.Name()+` {directory} {executables...}
29+
30+
This program downloads and extracts the licenses of Go modules used to build
31+
Go executables.
32+
33+
The first argument is a directory that will receive license files. It will be
34+
created if it does not exist. This program will overwrite existing files but
35+
not delete them. Remaining arguments must be Go executables.
36+
37+
Go modules are downloaded to the Go module cache which can be changed via
38+
the environment: https://go.dev/ref/mod#module-cache`,
39+
))
40+
}
41+
if _ = flags.Parse(os.Args[1:]); flags.NArg() < 2 || slices.ContainsFunc(
42+
os.Args, func(arg string) bool { return arg == "-help" || arg == "--help" },
43+
) {
44+
flags.Usage()
45+
os.Exit(2)
46+
}
47+
48+
ctx, cancel := context.WithCancel(context.Background())
49+
signals := make(chan os.Signal, 1)
50+
signal.Notify(signals, os.Interrupt, syscall.SIGTERM)
51+
go func() { <-signals; cancel() }()
52+
53+
// Create the target directory.
54+
if err := os.MkdirAll(flags.Arg(0), 0o755); err != nil {
55+
fmt.Fprintln(os.Stderr, err)
56+
os.Exit(1)
57+
}
58+
59+
// Extract module information from remaining arguments.
60+
modules := identifyModules(ctx, flags.Args()[1:]...)
61+
62+
// Ignore packages from Crunchy Data. Most are not available in any [proxy],
63+
// and we handle their licenses elsewhere.
64+
//
65+
// This is also a quick fix to avoid the [replace] directive in our projects.
66+
// The logic below cannot handle them. Showing xxhash versus a replace:
67+
//
68+
// dep github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
69+
// dep github.com/crunchydata/postgres-operator v0.0.0-00010101000000-000000000000
70+
// => ./postgres-operator (devel)
71+
//
72+
// [proxy]: https://go.dev/ref/mod#module-proxy
73+
// [replace]: https://go.dev/ref/mod#go-mod-file-replace
74+
modules = slices.DeleteFunc(modules, func(s string) bool {
75+
return strings.HasPrefix(s, "git.crunchydata.com/") ||
76+
strings.HasPrefix(s, "github.com/crunchydata/")
77+
})
78+
79+
// Download modules to the Go module cache.
80+
directories := downloadModules(ctx, modules...)
81+
82+
// Gather license files from every module into the target directory.
83+
for module, directory := range directories {
84+
for _, license := range findLicenses(ctx, directory) {
85+
relative := module + strings.TrimPrefix(license, directory)
86+
destination := filepath.Join(flags.Arg(0), relative)
87+
88+
var data []byte
89+
err := ctx.Err()
90+
91+
if err == nil {
92+
err = os.MkdirAll(filepath.Dir(destination), 0o755)
93+
}
94+
if err == nil {
95+
data, err = os.ReadFile(license)
96+
}
97+
if err == nil {
98+
err = os.WriteFile(destination, data, 0o644)
99+
}
100+
if err == nil {
101+
fmt.Println(license, "=>", destination)
102+
}
103+
if err != nil {
104+
fmt.Fprintln(os.Stderr, err)
105+
os.Exit(1)
106+
}
107+
}
108+
}
109+
}
110+
111+
func downloadModules(ctx context.Context, modules ...string) map[string]string {
112+
var stdout bytes.Buffer
113+
114+
// Download modules and read their details into a series of JSON objects.
115+
// - https://go.dev/ref/mod#go-mod-download
116+
cmd := exec.CommandContext(ctx, os.Getenv("GO"), append([]string{"mod", "download", "-json"}, modules...)...)
117+
if cmd.Path == "" {
118+
cmd.Path, cmd.Err = exec.LookPath("go")
119+
}
120+
cmd.Stderr = os.Stderr
121+
cmd.Stdout = &stdout
122+
if err := cmd.Run(); err != nil {
123+
fmt.Fprintln(os.Stderr, err)
124+
os.Exit(cmd.ProcessState.ExitCode())
125+
}
126+
127+
decoder := json.NewDecoder(&stdout)
128+
results := make(map[string]string, len(modules))
129+
130+
// NOTE: The directory in the cache is a normalized spelling of the module path;
131+
// ask Go for the directory; do not try to spell it yourself.
132+
// - https://go.dev/ref/mod#module-cache
133+
// - https://go.dev/ref/mod#module-path
134+
for {
135+
var module struct{ Path, Version, Dir string }
136+
err := decoder.Decode(&module)
137+
138+
if err == nil {
139+
results[module.Path+"@"+module.Version] = module.Dir
140+
continue
141+
}
142+
if errors.Is(err, io.EOF) {
143+
break
144+
}
145+
146+
fmt.Fprintln(os.Stderr, err)
147+
os.Exit(1)
148+
}
149+
150+
return results
151+
}
152+
153+
func findLicenses(ctx context.Context, directory string) []string {
154+
var results []string
155+
156+
// Syft maintains a list of license filenames that began as a list maintained by
157+
// Go. We gather a similar list by matching on "copying" and "license" filenames.
158+
// - https://pkg.go.dev/github.com/anchore/syft@v1.3.0/internal/licenses#FileNames
159+
//
160+
// Ignore Go files and anything in the special "testdata" directory.
161+
// - https://go.dev/cmd/go
162+
err := filepath.WalkDir(directory, func(path string, d fs.DirEntry, err error) error {
163+
if d.IsDir() && d.Name() == "testdata" {
164+
return fs.SkipDir
165+
}
166+
if d.IsDir() || strings.HasSuffix(path, ".go") {
167+
return err
168+
}
169+
170+
lower := strings.ToLower(d.Name())
171+
if strings.Contains(lower, "copying") || strings.Contains(lower, "license") {
172+
results = append(results, path)
173+
}
174+
175+
return err
176+
})
177+
178+
if err != nil {
179+
fmt.Fprintln(os.Stderr, err)
180+
os.Exit(1)
181+
}
182+
183+
return results
184+
}
185+
186+
func identifyModules(ctx context.Context, executables ...string) []string {
187+
var stdout bytes.Buffer
188+
189+
// Use `go version -m` to read the embedded module information as a text table.
190+
// - https://go.dev/ref/mod#go-version-m
191+
cmd := exec.CommandContext(ctx, os.Getenv("GO"), append([]string{"version", "-m"}, executables...)...)
192+
if cmd.Path == "" {
193+
cmd.Path, cmd.Err = exec.LookPath("go")
194+
}
195+
cmd.Stderr = os.Stderr
196+
cmd.Stdout = &stdout
197+
if err := cmd.Run(); err != nil {
198+
fmt.Fprintln(os.Stderr, err)
199+
os.Exit(cmd.ProcessState.ExitCode())
200+
}
201+
202+
// Parse the tab-separated table without checking row lengths.
203+
reader := csv.NewReader(&stdout)
204+
reader.Comma = '\t'
205+
reader.FieldsPerRecord = -1
206+
207+
lines, _ := reader.ReadAll()
208+
result := make([]string, 0, len(lines))
209+
210+
for _, fields := range lines {
211+
if len(fields) > 3 && fields[1] == "dep" {
212+
result = append(result, fields[2]+"@"+fields[3])
213+
}
214+
if len(fields) > 4 && fields[1] == "mod" && fields[4] != "" {
215+
result = append(result, fields[2]+"@"+fields[3])
216+
}
217+
}
218+
219+
// The `go version -m` command returns no information for empty files, and it
220+
// is possible for a Go executable to have no main module and no dependencies.
221+
if len(result) == 0 {
222+
fmt.Fprintf(os.Stderr, "no Go modules in %v\n", executables)
223+
os.Exit(0)
224+
}
225+
226+
return result
227+
}

0 commit comments

Comments
 (0)