From dbdf305d6130463c02870dfd3957d5a9ce6b5596 Mon Sep 17 00:00:00 2001 From: monopole Date: Fri, 9 Aug 2024 11:34:02 -0700 Subject: [PATCH] Build docker image. --- Makefile | 9 +- README.md | 27 +++-- releasing/Dockerfile | 12 --- releasing/{buildWorkspace.sh => build.sh} | 6 +- releasing/cloudbuild.yaml | 1 + releasing/internal/dockerrunner.go | 118 ++++++++++++++++++++++ releasing/internal/ghrunner.go | 2 +- releasing/internal/gobuilder.go | 8 +- releasing/internal/gobuilder_test.go | 31 ++++++ releasing/main.go | 64 ++++++++---- 10 files changed, 229 insertions(+), 49 deletions(-) delete mode 100644 releasing/Dockerfile rename releasing/{buildWorkspace.sh => build.sh} (73%) create mode 100644 releasing/internal/dockerrunner.go create mode 100644 releasing/internal/gobuilder_test.go diff --git a/Makefile b/Makefile index 943434d..c0bfc0f 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ MYGOBIN = $(shell go env GOPATH)/bin endif # Perform a local build. -# To build an actual release, use the release target. +# To build an actual release, use the 'release' target instead. $(MYGOBIN)/mdrip: - releasing/buildWorkspace.sh + releasing/build.sh # Run an end-to-end test. .PHONY: testE2E @@ -31,9 +31,10 @@ clean: go clean -testcache # Force serial execution of dependencies. -# This only really matters in the release target. +# This only matters for build targets that declare multiple dependencies, +# and it forces those dependencies to be built serially in the order that +# they appear in the dependencies list. .NOTPARALLEL: - # Create a draft release and push it to github. # Requires go, git, zip, tar, gh (github cli) and env var GH_TOKEN. # Complains if workspace is dirty, tests fail, tags don't make sense, etc. diff --git a/README.md b/README.md index 988fc42..c4bd929 100644 --- a/README.md +++ b/README.md @@ -46,18 +46,29 @@ the corresponding code block to `tmux` via its api. mdrip screenshot + alt="mdrip screenshot" width="100%" height="auto"> -## Installation +## Installation options -Install via the [Go](https://golang.org/dl) tool: - -``` -go install github.com/monopole/mdrip/v2@latest -``` -or download a build from the [release page]. +* Download a build from the [release page]. + +* Install via the [Go](https://golang.org/dl) tool: + + ``` + go install github.com/monopole/mdrip/v2@latest + ``` + +* Run via `docker`: + ``` + image=monopole/mdrip:latest + docker run $image version # try 'help' instead of 'version' to see options. + docker run \ + --publish 8080:8080 \ + --mount type=bind,source=`pwd`,target=/mnt \ + $image serve /mnt + ``` ## Basic Extraction and Testing diff --git a/releasing/Dockerfile b/releasing/Dockerfile deleted file mode 100644 index 4f57459..0000000 --- a/releasing/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -# THIS IS JUST PLACEHOLDER, NOT YET WORKING - -FROM ubuntu -RUN apt-get update && apt-get install -y git -# FROM gcr.io/cloud-builders/go:alpine -# RUN apk update && apk add --no-cache bash git -COPY go/bin/mdrip /mdrip -EXPOSE 8080 -CMD ["/mdrip",\ - "demo",\ - "--port=8080",\ - "."] diff --git a/releasing/buildWorkspace.sh b/releasing/build.sh similarity index 73% rename from releasing/buildWorkspace.sh rename to releasing/build.sh index 15e47c9..fd80e89 100755 --- a/releasing/buildWorkspace.sh +++ b/releasing/build.sh @@ -1,7 +1,8 @@ #!/bin/bash -# Build the current workspace, injecting loader flag values -# so that the version command works. +# Build the current workspace, i.e. whatever exists on local disk +# in whatever state, injecting loader flag values so that the 'version' +# command works and shows a dirty, modified state. ldPath=github.com/monopole/mdrip/v2/internal/provenance @@ -15,6 +16,7 @@ version=$(git describe --tags --always --dirty) gitCommit="$(git branch --show-current)-modified" out=$(go env GOPATH)/bin/mdrip + /bin/rm -f $out go build \ diff --git a/releasing/cloudbuild.yaml b/releasing/cloudbuild.yaml index c7901d8..51b9cf5 100644 --- a/releasing/cloudbuild.yaml +++ b/releasing/cloudbuild.yaml @@ -1,3 +1,4 @@ +# Warning - This isn't being maintained and likely doesn't work. steps: - name: 'gcr.io/cloud-builders/go' args: ['install', '-a', '-ldflags', "'-s'", '-installsuffix', 'cgo', '.'] diff --git a/releasing/internal/dockerrunner.go b/releasing/internal/dockerrunner.go new file mode 100644 index 0000000..8c42862 --- /dev/null +++ b/releasing/internal/dockerrunner.go @@ -0,0 +1,118 @@ +package internal + +import ( + "log/slog" + "os" + "path/filepath" + "strings" + "time" +) + +// DockerRunner runs some "docker" commands. +type DockerRunner struct { + rn *MyRunner + ldVars *LdVars + pgmName string + dirTmp string +} + +const ( + imageRegistry = "hub.docker.com" + imageOwner = "monopole" + + dockerTemplate = ` +# This file is generated; DO NOT EDIT. +FROM golang:1.22.5-bullseye +WORKDIR /go/src/github.com/monopole/{{PGMNAME}} +COPY go.mod . +COPY go.sum . +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOWORK=off \ + go build -v -o /go/bin/{{PGMNAME}} \ + -ldflags "{{LDFLAGS}}" \ + . +ENTRYPOINT ["/go/bin/{{PGMNAME}}"] +` +) + +func NewDockerRunner(dirSrc, dirTmp string, ldVars *LdVars) *DockerRunner { + return &DockerRunner{ + rn: NewMyRunner("docker", dirSrc, DoIt, 3*time.Minute), + ldVars: ldVars, + dirTmp: dirTmp, + pgmName: filepath.Base(dirSrc), + } +} + +func (dr *DockerRunner) Content() []byte { + content := strings.Replace( + dockerTemplate[1:], "{{LDFLAGS}}", dr.ldVars.MakeLdFlags(), -1) + content = strings.Replace(content, "{{PGMNAME}}", dr.pgmName, -1) + return []byte(content) +} + +func (dr *DockerRunner) ImageName() string { + return imageOwner + "/" + dr.pgmName +} + +func (dr *DockerRunner) Build() error { + dr.rn.comment("building docker image at tag " + dr.ldVars.Version()) + dockerFileName := filepath.Join(dr.dirTmp, "Dockerfile") + if err := os.WriteFile(dockerFileName, dr.Content(), 0644); err != nil { + return err + } + slog.Info("Wrote", "file", dockerFileName) + err := dr.rn.run( + NoHarmDone, + "build", + "--file", dockerFileName, + // "--platform", "linux/amd64,linux/arm64" (not using this yet) + "-t", dr.ImageName()+":"+dr.ldVars.Version(), + ".", + ) + if err != nil { + slog.Error(err.Error()) + slog.Error(dr.rn.Out()) + return err + } + return nil +} + +func (dr *DockerRunner) Push() error { + err := dr.rn.run( + UndoIsHard, + "push", + dr.ImageName()+":"+dr.ldVars.Version(), + ) + if err != nil { + slog.Error(err.Error()) + slog.Error(dr.rn.Out()) + return err + } + err = dr.rn.run( + UndoIsHard, + "push", + dr.ImageName()+":"+"latest", + ) + if err != nil { + slog.Error(err.Error()) + slog.Error(dr.rn.Out()) + } + return err +} + +func (dr *DockerRunner) Login() error { + dur := dr.rn.duration + dr.rn.duration = 3 * time.Second + err := dr.rn.run( + NoHarmDone, + "login", + ) + dr.rn.duration = dur + if err != nil { + slog.Error(err.Error()) + slog.Error(dr.rn.Out()) + } + return err +} diff --git a/releasing/internal/ghrunner.go b/releasing/internal/ghrunner.go index 61f794e..5bd150f 100644 --- a/releasing/internal/ghrunner.go +++ b/releasing/internal/ghrunner.go @@ -4,7 +4,7 @@ import ( "time" ) -// GithubRunner runs some the "gh" commands. +// GithubRunner runs some "gh" commands. type GithubRunner struct { rn *MyRunner } diff --git a/releasing/internal/gobuilder.go b/releasing/internal/gobuilder.go index 51d8e33..4eeaf3f 100644 --- a/releasing/internal/gobuilder.go +++ b/releasing/internal/gobuilder.go @@ -61,7 +61,7 @@ func (gb *GoBuilder) Build(myOs EnumOs, myArch EnumArch) (string, error) { if err := gb.goRun.run(NoHarmDone, "build", "-o", binary, - "-ldflags", gb.ldVars.makeLdFlags(), + "-ldflags", gb.ldVars.MakeLdFlags(), ".", // Using the "." is why we need HOME defined. ); err != nil { return name, err @@ -82,7 +82,7 @@ func (gb *GoBuilder) packageIt( myOs EnumOs, myArch EnumArch, fileName string) (string, error) { base := strings.Join([]string{ gb.pgmName, - gb.ldVars.version(), + gb.ldVars.Version(), myOs.String(), myArch.String(), }, "_") @@ -119,7 +119,7 @@ func (ldv *LdVars) makeDefinitions() []string { return result } -func (ldv *LdVars) makeLdFlags() string { +func (ldv *LdVars) MakeLdFlags() string { result := []string{ "-s", // disable symbol table (small binary) "-w", // disable DWARF generation (ditto) @@ -131,7 +131,7 @@ func (ldv *LdVars) makeLdFlags() string { return strings.Join(result, " ") } -func (ldv *LdVars) version() string { +func (ldv *LdVars) Version() string { v, ok := ldv.Kvs["version"] if !ok { panic("version not in ldFlags!") diff --git a/releasing/internal/gobuilder_test.go b/releasing/internal/gobuilder_test.go new file mode 100644 index 0000000..481beac --- /dev/null +++ b/releasing/internal/gobuilder_test.go @@ -0,0 +1,31 @@ +package internal + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLdVars_MakeLdFlags(t *testing.T) { + tests := map[string]struct { + ImportPath string + Kvs map[string]string + want string + }{ + "t1": { + ImportPath: "github.com/foo/bar/provenance", + Kvs: map[string]string{"fruit": "apple", "animal": "dog"}, + want: `-s -w ` + + `-X github.com/foo/bar/provenance.fruit=apple ` + + `-X github.com/foo/bar/provenance.animal=dog`, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ldv := &LdVars{ + ImportPath: tc.ImportPath, + Kvs: tc.Kvs, + } + assert.Equalf(t, tc.want, ldv.MakeLdFlags(), "MakeLdFlags()") + }) + } +} diff --git a/releasing/main.go b/releasing/main.go index 03cae05..fec91fe 100644 --- a/releasing/main.go +++ b/releasing/main.go @@ -11,11 +11,18 @@ import ( "github.com/monopole/mdrip/releasing/internal" ) -// This builds and releases a module to github. -// - While running, the process's working directory must be the -// same as the repo from which code is being released. -// - The repo should have one go.mod at the top; that's what's being released. -// - The desired release tag should have already been applied. +// This builds and releases a Go module to GitHub and dockerhub. +// +// In the old days I'd have hacked this up in unreadable bash code. +// Here I'm using Go to make it more readable and robust. +// +// While running, the process's working directory must be the +// same as the top of the repo from which code is being released. +// Further, the repo should have one go.mod at the top; that's what's +// being released. +// +// The desired release tag should have already been applied +// to the local repo. func main() { if os.Getenv("GH_TOKEN") == "" { log.Fatal("GH_TOKEN not defined, so the gh tool won't work.") @@ -30,6 +37,10 @@ func main() { if !filepath.IsAbs(dirSrc) { log.Fatal(dirSrc + " is not an absolute path.") } + doIt(dirSrc) +} + +func doIt(dirSrc string) { var ( tag, commit, dirOut string err error @@ -56,7 +67,21 @@ func main() { if err != nil { log.Fatal(err) } - assets, err = buildReleaseAssets(dirSrc, dirOut, tag, commit) + buildDate := time.Now().UTC() + + ldVars := &internal.LdVars{ + ImportPath: "github.com/monopole/mdrip/v2/internal/provenance", + Kvs: map[string]string{ + "version": tag, + "gitCommit": commit, + "buildDate": buildDate.Format(time.RFC3339), + }, + } + err = buildAndPushDockerImage(dirSrc, dirOut, ldVars) + if err != nil { + log.Fatal(err) + } + assets, err = buildReleaseAssetsForGitHub(dirSrc, dirOut, ldVars) if err != nil { log.Fatal(err) } @@ -95,18 +120,9 @@ func findTag(git *internal.GitRunner) (string, string, error) { return tag, commitHead, err } -func buildReleaseAssets( - dirSrc, dirOut, tag, commitHash string) ([]string, error) { - goBuilder := internal.NewGoBuilder( - dirSrc, dirOut, - &internal.LdVars{ - ImportPath: "github.com/monopole/mdrip/v2/internal/provenance", - Kvs: map[string]string{ - "version": tag, - "gitCommit": commitHash, - "buildDate": time.Now().UTC().Format(time.RFC3339), - }, - }) +func buildReleaseAssetsForGitHub( + dirSrc, dirOut string, ldVars *internal.LdVars) ([]string, error) { + goBuilder := internal.NewGoBuilder(dirSrc, dirOut, ldVars) var assetPaths []string for _, pair := range []struct { myOs internal.EnumOs @@ -127,3 +143,15 @@ func buildReleaseAssets( } return assetPaths, nil } + +func buildAndPushDockerImage( + dirSrc, dirOut string, ldVars *internal.LdVars) error { + docker := internal.NewDockerRunner(dirSrc, dirOut, ldVars) + if err := docker.Login(); err != nil { + return err + } + if err := docker.Build(); err != nil { + return err + } + return docker.Push() +}