2
0
mirror of https://github.com/xcat2/xcat-dep.git synced 2026-09-12 04:26:25 +00:00

fix(goconserver): build inside a mock chroot, deps pinned by go.sum (no vendor tree) (PR #62 review #2)

Completes @viniciusferrao's concern #2. Previously goconserver was built on the HOST with a runtime
`go mod tidy` against a clone of mutable `master` -- non-reproducible and non-hermetic.

- Rewrite goconserver/mockbuild.pl to build the rpm INSIDE a mock chroot via an SRPM: %build compiles
  in-chroot (BuildRequires: golang, GOTOOLCHAIN=local, CGO_ENABLED=0).
- Commit only the pinned module manifest goconserver/gomod/{go.mod,go.sum} (97 lines; go.mod carries
  the kr/pty -> creack/pty replace). The in-chroot build downloads the modules from the Go proxy
  (mock networking enabled) but is reproducible because go.sum integrity-checks every module -- no
  `go mod tidy`, and no committed vendor tree.
- goconserver is a CGO-free static binary and el8/el9 chroots ship too old a Go for 0.3.3, so always
  COMPILE in the el10 chroot for the arch; the Release still carries the target's dist tag (4.el<rel>),
  so every EL repo gets an identical, portable static binary. Verified on the build host: statically
  linked, no shared-lib deps, correct el<rel> tag while built in the el10 chroot.

Combined with the immutable-SHA pin + --release-suffix (40feffc), goconserver is now reproducible,
built in mock, and advances its NVR per CD run.

Signed-off-by: Daniel Hilst <392820+dhilst@users.noreply.github.com>
This commit is contained in:
Daniel Hilst
2026-08-10 13:06:22 -03:00
parent b2bd440ba0
commit a5432f28ef
4 changed files with 262 additions and 127 deletions
+14
View File
@@ -0,0 +1,14 @@
# Pinned Go module manifest for goconserver
`go.mod` + `go.sum` pin goconserver's Go dependencies so the rpm build is **reproducible without
vendoring the whole dependency tree**. The build runs **inside a mock chroot** (network enabled) and
downloads the modules from the Go proxy at build time; `go.sum` integrity-checks every module, so the
result is deterministic even though the deps are not committed.
- Generated from **xcat2/goconserver @ 6166fe5ec1c5b3c20475e322a9f0e8e93c87e45f** (the pin in
mockbuild-all.pl / goconserver/mockbuild.pl), with the archived `github.com/kr/pty` replaced by
`github.com/creack/pty@v1.1.21` and the etcd storage backend removed (xCAT uses file storage only).
- To regenerate after bumping the goconserver pin: clone at the new SHA, remove `storage/etcd*`,
`go mod init github.com/xcat2/goconserver`,
`go mod edit -replace github.com/kr/pty=github.com/creack/pty@v1.1.21`, `go mod tidy`, then copy
go.mod/go.sum here. (No `go mod vendor` needed.)
+27
View File
@@ -0,0 +1,27 @@
module github.com/xcat2/goconserver
go 1.26.4
replace github.com/kr/pty => github.com/creack/pty v1.1.21
require (
github.com/golang/protobuf v1.5.4
github.com/gorilla/mux v1.8.1
github.com/kr/pty v0.0.0-00010101000000-000000000000
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
golang.org/x/crypto v0.54.0
golang.org/x/net v0.57.0
google.golang.org/grpc v1.83.0
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+70
View File
@@ -0,0 +1,70 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+151 -127
View File
@@ -22,7 +22,7 @@ my $version = '0.3.3';
my $go_repo = 'https://github.com/xcat2/goconserver.git';
# Immutable pin: goconserver 0.3.3 is unreleased (newest tag v0.3.2) so it lives only on master.
# mockbuild-all.pl passes --go-ref with the canonical pin; this default keeps standalone runs
# reproducible too. `git clone --branch` cannot take a raw SHA, so the clone below fetches by ref.
# reproducible too. The committed vendored/ tree (go.mod/go.sum/vendor) corresponds to THIS SHA.
my $go_ref = '6166fe5ec1c5b3c20475e322a9f0e8e93c87e45f';
my $release_suffix = ''; # CD Release bump (".snap<YYYYMMDDHHMM>.<n>"); passed by mockbuild-all.pl
my $build_timestamp;
@@ -43,7 +43,10 @@ GetOptions(
die "Run as root (current uid=$>)\n" if $> != 0;
for my $bin (qw(go git rpmbuild rpm)) {
# The Go compile happens INSIDE the mock chroot (BuildRequires: golang); the host only needs to
# fetch the pinned source and drive mock. (No host `go` build any more -- that was the non-hermetic
# path this rewrite removes.)
for my $bin (qw(git rpm mock)) {
run("command -v " . sh_quote($bin) . " >/dev/null 2>&1");
}
@@ -74,17 +77,24 @@ unless ($SOURCE_DATE_EPOCH && $SOURCE_DATE_EPOCH =~ /^\d+$/) {
$SOURCE_DATE_EPOCH = time() unless $SOURCE_DATE_EPOCH =~ /^\d+$/;
$ENV{SOURCE_DATE_EPOCH} = $SOURCE_DATE_EPOCH;
# goconserver is a CGO-free static Go binary. el8/el9 chroots ship a Go too old to build 0.3.3, so
# always COMPILE in the el10 chroot for this arch (regardless of the target EL), then ship the static
# binary to every EL repo. The Release still carries the target's dist tag (4.el$rel) so each EL repo
# gets a correctly-named, byte-identical rpm.
(my $build_cfg = $mock_cfg) =~ s/-\d+-/-10-/;
print_step("Configuration");
print "repo_root: $repo_root\n";
print "pkg_dir: $pkg_dir\n";
print "work_dir: $work_dir\n";
print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg\n";
print "arch: $arch\n";
print "version: $version\n";
print "go_repo: $go_repo\n";
print "go_ref: $go_ref\n";
print "repo_root: $repo_root\n";
print "pkg_dir: $pkg_dir\n";
print "work_dir: $work_dir\n";
print "result_dir: $result_dir\n";
print "log_dir: $log_dir\n";
print "mock_cfg: $mock_cfg (target dist tag: el$rel)\n";
print "build_cfg: $build_cfg (el10 -- portable static build for arch $arch)\n";
print "arch: $arch\n";
print "version: $version\n";
print "go_ref: $go_ref\n";
print "release_suffix: " . ($release_suffix ne '' ? $release_suffix : '(none)') . "\n";
print "skip_install: $skip_install\n";
make_path($result_dir);
@@ -94,79 +104,45 @@ print_step("Stage build environment");
remove_tree($work_dir) if -d $work_dir;
make_path($work_dir);
# Unique per run (nested under the run/target-scoped --work-dir) so concurrent builds -- e.g.
# parallel EL targets on one host -- don't wipe each other. (Was a shared
# /var/tmp/xcat-rpmbuild-goconserver, which collided under parallelism.)
my $rpmbuild_top = "$work_dir/rpmbuild";
remove_tree($rpmbuild_top) if -d $rpmbuild_top;
for my $d (qw(BUILD BUILDROOT RPMS SOURCES SPECS SRPMS)) {
make_path("$rpmbuild_top/$d");
}
# --- Fetch the pinned goconserver source (immutable SHA -> reproducible) ---
print_step("Clone goconserver source");
my $src_dir = "$work_dir/goconserver-src";
# Fetch the exact pinned ref (a SHA, or a branch/tag). `git clone --branch` rejects a raw SHA, so
# init + shallow fetch the one object + checkout it -- reproducible and immutable, never "latest
# master". (xcat2/goconserver has allowReachableSHA1InWant, so fetching a master-reachable SHA works.)
my $clone_log = sh_quote("$log_dir/git-clone.log");
run("git init -q " . sh_quote($src_dir) . " >$clone_log 2>&1");
run("git -C " . sh_quote($src_dir) . " remote add origin " . sh_quote($go_repo) . " >>$clone_log 2>&1");
run("git -C " . sh_quote($src_dir) . " fetch --depth 1 origin " . sh_quote($go_ref) . " >>$clone_log 2>&1");
run("git -C " . sh_quote($src_dir) . " checkout -q FETCH_HEAD >>$clone_log 2>&1");
# etcd storage backend has broken deps with modern Go modules;
# xCAT only uses file storage, so remove etcd before building.
# etcd storage backend has broken deps with modern Go modules; xCAT only uses file storage.
unlink "$src_dir/storage/etcd.go";
remove_tree("$src_dir/storage/etcd") if -d "$src_dir/storage/etcd";
remove_tree("$src_dir/.git") if -d "$src_dir/.git"; # keep the SRPM tarball clean + reproducible
print_step("Initialize Go modules");
$ENV{GOPATH} = "$work_dir/gopath";
$ENV{GOCACHE} = "$work_dir/gocache";
$ENV{GOMODCACHE} = "$work_dir/gomodcache";
$ENV{CGO_ENABLED} = '0';
# --- Overlay the committed, PINNED go.mod/go.sum (no vendored tree) ---
# go.mod already replaces the archived github.com/kr/pty with creack/pty (see gomod/README.md), and
# go.sum integrity-checks every module. The in-chroot build downloads the modules from the Go proxy
# (mock networking is enabled) but is reproducible because go.sum pins them -- no `go mod tidy`, and
# no 400k-line vendor tree committed.
print_step("Overlay pinned go.mod/go.sum");
my $gomod_dir = "$pkg_dir/gomod";
die "pinned go.mod/go.sum missing under $gomod_dir (regenerate per gomod/README.md)\n"
unless -f "$gomod_dir/go.mod" && -f "$gomod_dir/go.sum";
copy("$gomod_dir/go.mod", "$src_dir/go.mod") or die "copy go.mod: $!\n";
copy("$gomod_dir/go.sum", "$src_dir/go.sum") or die "copy go.sum: $!\n";
# The archived github.com/kr/pty sets SysProcAttr.Ctty to the parent-side fd,
# which modern Go's os/exec rejects with "Setctty set but Ctty not valid in
# child". Replace it with the API-identical maintained fork creack/pty.
run("cd " . sh_quote($src_dir) . " && " .
"go mod init github.com/xcat2/goconserver && " .
"go mod edit -replace github.com/kr/pty=github.com/creack/pty\@v1.1.21 && " .
"go mod tidy" .
" >" . sh_quote("$log_dir/go-mod.log") . " 2>&1");
# --- Assemble SRPM sources: the source tree (incl. vendor) + the xcat-authored unit + config ---
print_step("Assemble SRPM sources");
my $srctop = "goconserver-$version";
my $staged = "$work_dir/$srctop";
remove_tree($staged) if -d $staged;
run("cp -a " . sh_quote($src_dir) . " " . sh_quote($staged));
my $sources_dir = "$work_dir/sources";
make_path($sources_dir);
my $tarball = "$sources_dir/goconserver-$version.tar.gz";
run("tar --sort=name --owner=0 --group=0 --mtime=\@$SOURCE_DATE_EPOCH" .
" -C " . sh_quote($work_dir) . " -czf " . sh_quote($tarball) . " " . sh_quote($srctop));
print_step("Build goconserver binaries");
my $go_build_dir = "$work_dir/bin";
make_path($go_build_dir);
my $ldflags = "-X main.Version=$version";
run("cd " . sh_quote($src_dir) . " && " .
"go build -trimpath -buildvcs=false -ldflags " . sh_quote($ldflags) .
" -o " . sh_quote("$go_build_dir/goconserver") . " goconserver.go" .
" >" . sh_quote("$log_dir/go-build-server.log") . " 2>&1");
run("cd " . sh_quote($src_dir) . " && " .
"go build -trimpath -buildvcs=false -ldflags " . sh_quote($ldflags) .
" -o " . sh_quote("$go_build_dir/congo") . " cmd/congo.go" .
" >" . sh_quote("$log_dir/go-build-client.log") . " 2>&1");
die "goconserver binary not built\n" if !-x "$go_build_dir/goconserver";
die "congo binary not built\n" if !-x "$go_build_dir/congo";
print_step("Create source tarball");
my $payload_dir = "$work_dir/goconserver-$version";
make_path("$payload_dir/usr/bin");
make_path("$payload_dir/usr/lib/systemd/system");
make_path("$payload_dir/etc/goconserver");
copy("$go_build_dir/goconserver", "$payload_dir/usr/bin/goconserver")
or die "copy goconserver: $!\n";
copy("$go_build_dir/congo", "$payload_dir/usr/bin/congo")
or die "copy congo: $!\n";
chmod 0755, "$payload_dir/usr/bin/goconserver";
chmod 0755, "$payload_dir/usr/bin/congo";
write_file("$payload_dir/usr/lib/systemd/system/goconserver.service", <<'SERVICE');
write_file("$sources_dir/goconserver.service", <<'SERVICE');
[Unit]
Description=goconserver console server
After=network.target
@@ -181,13 +157,11 @@ StateDirectory=goconserver
WantedBy=multi-user.target
SERVICE
# The goconserver binary parses server.conf as YAML. Ship a VALID YAML default: the old INI-style
# ([server]\nhost = ...) is read by the YAML parser as a sequence -> `panic: cannot unmarshal !!seq into
# common.ServerConfig` at startup -> systemd rate-limits the service to `failed`. On an xCAT MN,
# xCAT::Goconserver.pm overwrites this with a cert-enabled config, so this default only has to PARSE and
# start (no SSL here -- the xcat certs don't exist until xCAT is configured). Keys/ports mirror the schema
# xCAT itself writes (api 12429, console 12430).
write_file("$payload_dir/etc/goconserver/server.conf", <<'CONF');
# The goconserver binary parses server.conf as YAML. Ship a VALID YAML default (the old INI-style
# [server] block is read by the YAML parser as a sequence -> `panic: cannot unmarshal !!seq` at
# startup -> systemd rate-limits the service to `failed`). On an xCAT MN, xCAT::Goconserver.pm
# overwrites this with a cert-enabled config; this default only has to PARSE and start.
write_file("$sources_dir/server.conf", <<'CONF');
global:
host: 0.0.0.0
logfile: /var/log/goconserver/server.log
@@ -199,13 +173,13 @@ console:
log_timestamp: true
CONF
my $tarball = "$rpmbuild_top/SOURCES/goconserver-$version.tar.gz";
run("tar --sort=name --owner=0 --group=0 --mtime=\@$SOURCE_DATE_EPOCH" .
" -C " . sh_quote($work_dir) . " -czf " . sh_quote($tarball) .
" goconserver-$version");
print_step("Create spec and build RPM");
my $spec_content = <<"SPEC";
# --- Spec: the Go compile runs in %build INSIDE the chroot, offline, from the vendored tree ---
print_step("Write spec");
my $spec_file = "$work_dir/goconserver.spec";
write_file($spec_file, <<"SPEC");
# Go binaries carry no useful DWARF debugsource; the empty debuginfo subpackage otherwise fails
# packaging ("Empty %files debugsourcefiles.list"). Disable it.
%global debug_package %{nil}
Name: goconserver
Version: $version
Release: 4.el$rel$release_suffix
@@ -215,25 +189,33 @@ URL: https://github.com/xcat2/goconserver
BuildArch: $arch
Source0: goconserver-%{version}.tar.gz
Source1: goconserver.service
Source2: server.conf
BuildRequires: golang
%description
goconserver is a scalable console server written in Go. It provides
console logging and management for xCAT cluster nodes.
%prep
%setup -n goconserver-%{version}
%setup -q -n goconserver-%{version}
%build
# Compile in-chroot. Modules are downloaded from the Go proxy at build time (mock networking is on)
# but PINNED + integrity-checked by the committed go.sum, so the build is reproducible without a
# vendored tree. GOTOOLCHAIN=local pins the chroot's Go (never auto-downloads a toolchain).
export GOFLAGS=-mod=mod GOTOOLCHAIN=local CGO_ENABLED=0
export GOCACHE=%{_builddir}/.gocache GOPATH=%{_builddir}/.gopath GOMODCACHE=%{_builddir}/.gomodcache
go build -trimpath -buildvcs=false -ldflags "-X main.Version=%{version}" -o goconserver goconserver.go
go build -trimpath -buildvcs=false -ldflags "-X main.Version=%{version}" -o congo cmd/congo.go
%install
mkdir -p %{buildroot}/usr/bin
mkdir -p %{buildroot}/usr/lib/systemd/system
mkdir -p %{buildroot}/etc/goconserver
mkdir -p %{buildroot}/var/log/goconserver
mkdir -p %{buildroot}/var/lib/goconserver
install -m 755 usr/bin/goconserver %{buildroot}/usr/bin/goconserver
install -m 755 usr/bin/congo %{buildroot}/usr/bin/congo
install -m 644 usr/lib/systemd/system/goconserver.service %{buildroot}/usr/lib/systemd/system/goconserver.service
install -m 644 etc/goconserver/server.conf %{buildroot}/etc/goconserver/server.conf
install -Dm0755 goconserver %{buildroot}/usr/bin/goconserver
install -Dm0755 congo %{buildroot}/usr/bin/congo
install -Dm0644 %{SOURCE1} %{buildroot}/usr/lib/systemd/system/goconserver.service
install -Dm0644 %{SOURCE2} %{buildroot}/etc/goconserver/server.conf
mkdir -p %{buildroot}/var/log/goconserver %{buildroot}/var/lib/goconserver
%files
/usr/bin/goconserver
@@ -244,53 +226,75 @@ install -m 644 etc/goconserver/server.conf %{buildroot}/etc/goconserver/server.c
%dir /var/lib/goconserver
%changelog
* Thu Jul 23 2026 xCAT build - 0.3.3-4.el10
- Ship /etc/goconserver/server.conf in YAML (the format the goconserver binary parses) instead of the
old INI [server] style, which the YAML parser reads as a sequence -> panic (cannot unmarshal !!seq) at
startup -> systemd rate-limits the service to failed before xCAT can convert the config. Fixes console
provisioning (makegocons) on the management node.
* Mon Jun 08 2026 xCAT EL10 build - 0.3.3-2.el10
- Replace archived github.com/kr/pty with github.com/creack/pty to fix
"Setctty set but Ctty not valid in child" console fork failure on modern Go.
* Mon Aug 10 2026 xCAT build - $version-4.el$rel
- Build inside a mock chroot (no host build). Modules are downloaded at build time but pinned +
integrity-checked by a committed go.sum (no `go mod tidy`, no vendored tree). Compiled in the
el10 chroot for the arch and shipped to every EL repo (CGO-free static binary).
- Ship /etc/goconserver/server.conf as YAML (the format the goconserver binary parses).
- Replace archived github.com/kr/pty with github.com/creack/pty (console fork on modern Go).
SPEC
my $spec_file = "$rpmbuild_top/SPECS/goconserver.spec";
write_file($spec_file, $spec_content);
# --- Build in the el10 chroot for this arch ---
my $mock_uniqueext_opt = $mock_uniqueext ne '' ? ' --uniqueext ' . sh_quote($mock_uniqueext) : '';
print_step("Mock config check");
run("mock -r " . sh_quote($build_cfg) . $mock_uniqueext_opt . " --print-root-path >/dev/null");
my $det_cfg = create_deterministic_mock_cfg($build_cfg, $SOURCE_DATE_EPOCH, $work_dir);
run(
"rpmbuild --define " . sh_quote("_topdir $rpmbuild_top") .
print_step("Build SRPM with mock");
my $srpm_out = "$work_dir/srpm";
make_path($srpm_out);
run("mock -r " . sh_quote($det_cfg) . $mock_uniqueext_opt .
" --buildsrpm --spec " . sh_quote($spec_file) .
" --sources " . sh_quote($sources_dir) .
" --define " . sh_quote("use_source_date_epoch_as_buildtime 1") .
" --define " . sh_quote("clamp_mtime_to_source_date_epoch 1") .
" --define " . sh_quote("_buildhost xcat-build") .
" -ba " . sh_quote($spec_file) .
" >" . sh_quote("$log_dir/rpmbuild.log") . " 2>&1"
);
" --resultdir " . sh_quote($srpm_out) .
" >" . sh_quote("$log_dir/mock-buildsrpm.log") . " 2>&1");
my @srpms = sort glob("$srpm_out/goconserver-*.src.rpm");
die "SRPM not generated in $srpm_out\n" if !@srpms;
my $srpm = $srpms[-1];
print "SRPM: $srpm\n";
print_step("Rebuild RPM with mock (offline, in-chroot go build)");
my $rpm_out = "$work_dir/rpm";
make_path($rpm_out);
run("mock -r " . sh_quote($det_cfg) . $mock_uniqueext_opt .
" --rebuild " . sh_quote($srpm) .
" --define " . sh_quote("use_source_date_epoch_as_buildtime 1") .
" --define " . sh_quote("clamp_mtime_to_source_date_epoch 1") .
" --define " . sh_quote("_buildhost xcat-build") .
" --resultdir " . sh_quote($rpm_out) .
" >" . sh_quote("$log_dir/mock-rebuild.log") . " 2>&1");
print_step("Collect results");
for my $rpm (glob("$rpmbuild_top/RPMS/*/*.rpm"), glob("$rpmbuild_top/SRPMS/*.rpm")) {
my @arch_rpms = sort grep { !/\.src\.rpm$/ } glob("$rpm_out/goconserver-*.$arch.rpm");
die "No goconserver $arch rpm generated in $rpm_out\n" if !@arch_rpms;
for my $rpm (@arch_rpms, glob("$rpm_out/*.src.rpm")) {
my $dest = "$result_dir/" . basename($rpm);
copy($rpm, $dest) or die "Failed to copy $rpm to $dest: $!\n";
print "Copied: $dest\n";
}
for my $log (qw(build.log root.log state.log)) {
my $s = "$rpm_out/$log";
copy($s, "$log_dir/mock-$log") if -f $s;
}
# Reclaim goconserver's own build chroot. mockbuild-all's scrub keys on the TARGET cfg (el$rel), not
# the el10 build cfg used here, so scrub it ourselves to avoid leaking /var/lib/mock. Best-effort.
system("mock -r " . sh_quote($build_cfg) . $mock_uniqueext_opt .
" --scrub=chroot >" . sh_quote("$log_dir/mock-scrub.log") . " 2>&1");
if (!$skip_install) {
print_step("Install and smoke test");
my @built = glob("$rpmbuild_top/RPMS/$arch/goconserver-*.rpm");
die "No arch RPM found\n" if !@built;
my $main_rpm = $built[0];
run("dnf -y install " . sh_quote($main_rpm) .
" >" . sh_quote("$log_dir/dnf-install.log") . " 2>&1");
my $main_rpm = $arch_rpms[0];
run("dnf -y install " . sh_quote($main_rpm) . " >" . sh_quote("$log_dir/dnf-install.log") . " 2>&1");
die "Missing /usr/bin/goconserver\n" if !-x '/usr/bin/goconserver';
die "Missing /usr/bin/congo\n" if !-x '/usr/bin/congo';
my $rc_help = run_rc("goconserver -h >" . sh_quote("$log_dir/smoke-help.log") . " 2>&1");
die "goconserver -h failed (rc=$rc_help)\n" if $rc_help > 1;
my $rc_congo = run_rc("congo -h >" . sh_quote("$log_dir/smoke-congo.log") . " 2>&1");
die "congo -h failed (rc=$rc_congo)\n" if $rc_congo > 1;
print "Smoke tests passed.\n";
}
@@ -302,18 +306,23 @@ sub usage {
return <<"USAGE";
Usage: $0 [options]
Build goconserver RPM from source.
Build the goconserver RPM inside a mock chroot: fetch the pinned source, overlay the committed
go.mod/go.sum, and compile IN-CHROOT (modules downloaded at build time but pinned by go.sum -- no
vendored tree, no `go mod tidy`). The compile runs in the el10 chroot for the host arch (goconserver
is a CGO-free static binary; el8/el9 ship too old a Go), and the rpm is tagged with the target EL
(4.el<rel>) so every EL repo gets an identical static binary.
Options:
--work-dir PATH Working directory (default: /tmp/goconserver-mockbuild)
--mock-cfg NAME Mock config name (auto-detected if omitted)
--mock-uniqueext STR Mock uniqueext value (for compatibility with mockbuild-all.pl)
--mock-cfg NAME Target mock config (sets the EL dist tag; the build runs in its el10 peer)
--mock-uniqueext STR Mock uniqueext (for concurrency isolation under mockbuild-all.pl)
--result-dir PATH Output directory for RPMs
--log-dir PATH Output directory for logs
--skip-install Skip dnf install + smoke tests
--version VER Version string (default: 0.3.3)
--go-repo URL Git repo URL (default: github.com/xcat2/goconserver)
--go-ref REF Git ref to build (default: master)
--go-ref REF Git ref/SHA to build (default: the pinned commit)
--release-suffix STR Appended to Release for CD (e.g. .snap<ts>.<n>)
--build-timestamp EPOCH SOURCE_DATE_EPOCH for deterministic builds
USAGE
}
@@ -374,3 +383,18 @@ sub resolve_mock_cfg {
}
return "${os_id}+epel-${rel}-${arch}";
}
sub create_deterministic_mock_cfg {
my ($base_cfg, $epoch, $dir) = @_;
my $cfg_path = "$dir/mock-deterministic.cfg";
open my $fh, '>', $cfg_path or die "Cannot write $cfg_path: $!\n";
print $fh "include('/etc/mock/${base_cfg}.cfg')\n";
print $fh "config_opts['environment']['SOURCE_DATE_EPOCH'] = '$epoch'\n";
print $fh "config_opts['environment']['ZERO_AR_DATE'] = '1'\n";
# Allow network during %build so `go build` can download the (go.sum-pinned) modules -- we build
# against the proxy rather than committing a vendored tree.
print $fh "config_opts['rpmbuild_networking'] = True\n";
print $fh "config_opts['use_host_resolv'] = True\n";
close $fh;
return $cfg_path;
}