Skip to content

Commit 380eb6c

Browse files
benjaminjbcbandy
andauthored
Github operator build update (#4305)
Update GitHub builds * Adjust chmod for licenses, queries * Adjust license aggregation Issues: [PGO-2695] Co-authored-by: Chris Bandy <bandy.chris@gmail.com>
1 parent b1b6652 commit 380eb6c

File tree

4 files changed

+242
-3
lines changed

4 files changed

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

licenses/.gitignore

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)