From 83f28d0d76781b9349b4504068a791d0e67823e4 Mon Sep 17 00:00:00 2001 From: Alex Lovell-Troy Date: Wed, 30 Oct 2024 07:22:38 -0600 Subject: [PATCH] Fix build automation and linter errors. Add LDFLAG- supported version information. --- .github/workflows/PRTest.yml | 41 +++++++++++++++ .github/workflows/ochami.yml | 12 +++-- .goreleaser.yaml | 85 +++++++++++++++++++++++++------ Dockerfile | 11 ++-- cmd/cloud-init-server/handlers.go | 23 ++++----- cmd/cloud-init-server/main.go | 5 +- cmd/cloud-init-server/version.go | 58 +++++++++++++++++++++ internal/smdclient/SMDclient.go | 19 +++++-- 8 files changed, 209 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/PRTest.yml create mode 100644 cmd/cloud-init-server/version.go diff --git a/.github/workflows/PRTest.yml b/.github/workflows/PRTest.yml new file mode 100644 index 0000000..508f659 --- /dev/null +++ b/.github/workflows/PRTest.yml @@ -0,0 +1,41 @@ +name: Test build +on: [push, pull_request] + +jobs: + test_build: + defaults: + run: + working-directory: ./cmd + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-tags: 1 + fetch-depth: 1 + + - name: Set up latest stable Go + uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + + # Set environment variables required by GoReleaser + - name: Set build environment variables + run: | + echo "GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" >> $GITHUB_ENV + echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV + echo "GO_VERSION=$(go version | awk '{print $3}')" >> $GITHUB_ENV + echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV + echo "CGO_ENABLED=1" >> $GITHUB_ENV + + - name: Build with goreleaser + uses: goreleaser/goreleaser-action@v6 + with: + version: '~> v2' + args: release --snapshot + id: goreleaser \ No newline at end of file diff --git a/.github/workflows/ochami.yml b/.github/workflows/ochami.yml index 1c248bd..188a86d 100644 --- a/.github/workflows/ochami.yml +++ b/.github/workflows/ochami.yml @@ -1,5 +1,4 @@ name: Release with goreleaser - on: workflow_dispatch: push: @@ -16,23 +15,27 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.21 + - name: Set up latest stable Go uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: stable + - name: Set up QEMU uses: docker/setup-qemu-action@v3 + - name: Docker Login uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout uses: actions/checkout@v4 with: fetch-tags: 1 fetch-depth: 0 + - name: Release with goreleaser uses: goreleaser/goreleaser-action@v5 env: @@ -41,6 +44,7 @@ jobs: version: latest args: release --clean id: goreleaser + - name: Process goreleaser output id: process_goreleaser_output run: | @@ -51,10 +55,12 @@ jobs: echo "fs.writeFileSync('digest.txt', firstNonNullDigest);" >> process.js node process.js echo "digest=$(cat digest.txt)" >> $GITHUB_OUTPUT + - name: Attest Binaries uses: actions/attest-build-provenance@v1 with: subject-path: dist/cloud-init* + - name: generate build provenance uses: actions/attest-build-provenance@v1 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4dd451f..c910969 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,41 +1,96 @@ -# This is an example .goreleaser.yml file with some sensible defaults. -# Make sure to check the documentation at https://goreleaser.com - +version: 2 project_name: cloud-init before: hooks: - # You may remove this if you don't use go modules. - go mod tidy builds: - id: cloud-init main: ./cmd/cloud-init-server - binary: cloud-init-server goos: - linux + - darwin goarch: - amd64 - no_unique_dist_dir: true - tags: - - dynamic + - arm64 + goamd64: + - v3 + + # export GIT_STATE=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi) + # export BUILD_HOST=$(hostname) + # export GO_VERSION=$(go version | awk '{print $3}') + # export BUILD_USER=$(whoami) + ldflags: + - "-s -w -X main.GitCommit={{.Commit}} \ + -X main.BuildTime={{.Timestamp}} \ + -X main.Version={{.Version}} \ + -X main.GitBranch={{.Branch}} \ + -X main.GitTag={{.Tag}} \ + -X main.GitState={{ .Env.GIT_STATE }} \ + -X main.BuildHost={{ .Env.BUILD_HOST }} \ + -X main.GoVersion={{ .Env.GO_VERSION }} \ + -X main.BuildUser={{ .Env.BUILD_USER }} " + binary: cloud-init-server + env: + - CGO_ENABLED=0 dockers: - - - image_templates: - - ghcr.io/openchami/{{.ProjectName}}:latest - - ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }} - - ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }} - - ghcr.io/openchami/{{.ProjectName}}:v{{ .Major }}.{{ .Minor }} + - image_templates: + - &amd64_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-amd64 + - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}-amd64 + - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-amd64 + use: buildx build_flag_templates: - "--pull" + - "--platform=linux/amd64" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" + goarch: amd64 + goamd64: v3 extra_files: - LICENSE + - README.md - CHANGELOG.md + + - image_templates: + - &arm64v8_linux_image ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}-arm64 + - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}-arm64 + - ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}-arm64 + use: buildx + build_flag_templates: + - "--pull" + - "--platform=linux/arm64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + extra_files: - README.md + - LICENSE + goarch: arm64 + +docker_manifests: + - name_template: "ghcr.io/openchami/{{.ProjectName}}:latest" + image_templates: + - *amd64_linux_image + - *arm64v8_linux_image + + - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Tag }}" + image_templates: + - *amd64_linux_image + - *arm64v8_linux_image + + - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}" + image_templates: + - *amd64_linux_image + - *arm64v8_linux_image + + - name_template: "ghcr.io/openchami/{{.ProjectName}}:{{ .Major }}.{{ .Minor }}" + image_templates: + - *amd64_linux_image + - *arm64v8_linux_image archives: - format: tar.gz @@ -56,7 +111,7 @@ archives: checksum: name_template: 'checksums.txt' snapshot: - name_template: "{{ incpatch .Version }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: diff --git a/Dockerfile b/Dockerfile index 16d424c..c84c700 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,20 +20,17 @@ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -FROM cgr.dev/chainguard/wolfi-base +FROM chainguard/wolfi-base:latest STOPSIGNAL SIGTERM -RUN apk add --no-cache tini - -# Setup environment variables. # Include curl in the final image. RUN set -ex \ && apk -U upgrade \ - && apk add --no-cache curl + && apk add --no-cache curl tini # Get the boot-script-service from the builder stage. -COPY cloud-init-server /usr/local/bin/ +COPY cloud-init-server /cloud-init-server ENV TOKEN_URL="http://opaal:3333/token" ENV SMD_URL="http://smd:27779" @@ -44,7 +41,7 @@ ENV JWKS_URL="" USER 65534:65534 # Set up the command to start the service. -CMD /usr/local/bin/cloud-init-server --listen ${LISTEN_ADDR} --smd-url ${SMD_URL} --token-url ${TOKEN_URL} --jwks-url ${JWKS_URL:-""} +CMD cloud-init-server --listen ${LISTEN_ADDR} --smd-url ${SMD_URL} --token-url ${TOKEN_URL} --jwks-url ${JWKS_URL:-""} ENTRYPOINT ["/sbin/tini", "--"] diff --git a/cmd/cloud-init-server/handlers.go b/cmd/cloud-init-server/handlers.go index 6111b61..8a4541f 100644 --- a/cmd/cloud-init-server/handlers.go +++ b/cmd/cloud-init-server/handlers.go @@ -156,7 +156,7 @@ func (h CiHandler) getData(id string, dataKind ciDataKind, w http.ResponseWriter var data *map[string]interface{} switch dataKind { case UserData: - w.Write([]byte("#cloud-config\n")) + w.Write([]byte("#cloud-config\n")) //nolint:errcheck data = &ci.CIData.UserData case MetaData: data = &ci.CIData.MetaData @@ -169,7 +169,7 @@ func (h CiHandler) getData(id string, dataKind ciDataKind, w http.ResponseWriter fmt.Print(err) } w.Header().Set("Content-Type", "text/yaml") - w.Write([]byte(ydata)) + w.Write([]byte(ydata)) //nolint:errcheck } func (h CiHandler) UpdateEntry(w http.ResponseWriter, r *http.Request) { @@ -215,7 +215,7 @@ func (h CiHandler) DeleteEntry(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, map[string]string{"status": "success"}) } -func parseData(w http.ResponseWriter, r *http.Request) (citypes.GroupData, error) { +func parseData(r *http.Request) (citypes.GroupData, error) { var ( body []byte err error @@ -242,7 +242,7 @@ func (h CiHandler) AddGroups(w http.ResponseWriter, r *http.Request) { err error ) - data, err = parseData(w, r) + data, err = parseData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -277,7 +277,7 @@ func (h CiHandler) GetGroups(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Write(bytes) + w.Write(bytes) //nolint:errcheck } @@ -292,7 +292,7 @@ func (h CiHandler) UpdateGroups(w http.ResponseWriter, r *http.Request) { err error ) - data, err = parseData(w, r) + data, err = parseData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -315,11 +315,6 @@ func (h CiHandler) RemoveGroups(w http.ResponseWriter, r *http.Request) { } } -func writeInternalError(w http.ResponseWriter, err string) { - http.Error(w, err, http.StatusInternalServerError) - // log.Error().Err(err) -} - func (h CiHandler) AddGroupData(w http.ResponseWriter, r *http.Request) { var ( id string = chi.URLParam(r, "id") @@ -327,7 +322,7 @@ func (h CiHandler) AddGroupData(w http.ResponseWriter, r *http.Request) { err error ) - data, err = parseData(w, r) + data, err = parseData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -360,7 +355,7 @@ func (h CiHandler) GetGroupData(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - w.Write(bytes) + w.Write(bytes) //nolint:errcheck } func (h CiHandler) UpdateGroupData(w http.ResponseWriter, r *http.Request) { var ( @@ -369,7 +364,7 @@ func (h CiHandler) UpdateGroupData(w http.ResponseWriter, r *http.Request) { err error ) - data, err = parseData(w, r) + data, err = parseData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/cmd/cloud-init-server/main.go b/cmd/cloud-init-server/main.go index 00a1f54..2fe41d3 100644 --- a/cmd/cloud-init-server/main.go +++ b/cmd/cloud-init-server/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "log" "net/http" "os" "time" @@ -33,6 +34,8 @@ func main() { flag.StringVar(&jwksUrl, "jwks-url", jwksUrl, "JWT keyserver URL, required to enable secure route") flag.Parse() + PrintVersionInfo() + // Set up JWT verification via the specified URL, if any var keyset *jwtauth.JWTAuth secureRouteEnable := false @@ -87,7 +90,7 @@ func main() { } // Serve all routes - http.ListenAndServe(ciEndpoint, router) + log.Fatal(http.ListenAndServe(ciEndpoint, router)) } func initCiRouter(router chi.Router, handler *CiHandler) { diff --git a/cmd/cloud-init-server/version.go b/cmd/cloud-init-server/version.go new file mode 100644 index 0000000..1b848c0 --- /dev/null +++ b/cmd/cloud-init-server/version.go @@ -0,0 +1,58 @@ +package main + +import "fmt" + +// GitCommit stores the latest Git commit hash. +// Set via -ldflags "-X main.GitCommit=$(git rev-parse HEAD)" +var GitCommit string + +// BuildTime stores the build timestamp in UTC. +// Set via -ldflags "-X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" +var BuildTime string + +// Version indicates the version of the binary, such as a release number or semantic version. +// Set via -ldflags "-X main.Version=v1.0.0" +var Version string + +// GitBranch holds the name of the Git branch from which the build was created. +// Set via -ldflags "-X main.GitBranch=$(git rev-parse --abbrev-ref HEAD)" +var GitBranch string + +// GitTag represents the most recent Git tag at build time, if any. +// Set via -ldflags "-X main.GitTag=$(git describe --tags --abbrev=0)" +var GitTag string + +// GitState indicates whether the working directory was "clean" or "dirty" (i.e., with uncommitted changes). +// Set via -ldflags "-X main.GitState=$(if git diff-index --quiet HEAD --; then echo 'clean'; else echo 'dirty'; fi)" +var GitState string + +// BuildHost stores the hostname of the machine where the binary was built. +// Set via -ldflags "-X main.BuildHost=$(hostname)" +var BuildHost string + +// GoVersion captures the Go version used to build the binary. +// Typically, this can be obtained automatically with runtime.Version(), but you can set it manually. +// Set via -ldflags "-X main.GoVersion=$(go version | awk '{print $3}')" +var GoVersion string + +// BuildUser is the username of the person or system that initiated the build process. +// Set via -ldflags "-X main.BuildUser=$(whoami)" +var BuildUser string + +// PrintVersionInfo outputs all versioning information for troubleshooting or version checks. +func PrintVersionInfo() { + fmt.Printf("Version: %s\n", Version) + fmt.Printf("Git Commit: %s\n", GitCommit) + fmt.Printf("Build Time: %s\n", BuildTime) + fmt.Printf("Git Branch: %s\n", GitBranch) + fmt.Printf("Git Tag: %s\n", GitTag) + fmt.Printf("Git State: %s\n", GitState) + fmt.Printf("Build Host: %s\n", BuildHost) + fmt.Printf("Go Version: %s\n", GoVersion) + fmt.Printf("Build User: %s\n", BuildUser) +} + +func VersionInfo() string { + return fmt.Sprintf("Version: %s, Git Commit: %s, Build Time: %s, Git Branch: %s, Git Tag: %s, Git State: %s, Build Host: %s, Go Version: %s, Build User: %s", + Version, GitCommit, BuildTime, GitBranch, GitTag, GitState, BuildHost, GoVersion, BuildUser) +} diff --git a/internal/smdclient/SMDclient.go b/internal/smdclient/SMDclient.go index 105ff9c..7a8967b 100644 --- a/internal/smdclient/SMDclient.go +++ b/internal/smdclient/SMDclient.go @@ -48,7 +48,7 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { var resp *http.Response // Manage fetching a new JWT if we initially fail freshToken := false - for true { + for { req, err := http.NewRequest("GET", url, nil) if err != nil { return err @@ -64,7 +64,10 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { log.Println("Cached JWT was rejected by SMD") if !freshToken { log.Println("Fetching new JWT and retrying...") - s.RefreshToken() + err := s.RefreshToken() + if err != nil { + return err + } freshToken = true } else { log.Fatalln("SMD authentication failed, even with a fresh" + @@ -89,7 +92,9 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { func (s *SMDClient) IDfromMAC(mac string) (string, error) { var ethIfaceArray []sm.CompEthInterfaceV2 ep := "/hsm/v2/Inventory/EthernetInterfaces/" - s.getSMD(ep, ðIfaceArray) + if err := s.getSMD(ep, ðIfaceArray); err != nil { + return "", err + } for _, ep := range ethIfaceArray { if strings.EqualFold(mac, ep.MACAddr) { @@ -103,7 +108,9 @@ func (s *SMDClient) IDfromMAC(mac string) (string, error) { func (s *SMDClient) IDfromIP(ipaddr string) (string, error) { var ethIfaceArray []sm.CompEthInterfaceV2 ep := "/hsm/v2/Inventory/EthernetInterfaces/" - s.getSMD(ep, ðIfaceArray) + if err := s.getSMD(ep, ðIfaceArray); err != nil { + return "", err + } for _, ep := range ethIfaceArray { for _, v := range ep.IPAddrs { @@ -119,6 +126,8 @@ func (s *SMDClient) IDfromIP(ipaddr string) (string, error) { func (s *SMDClient) GroupMembership(id string) ([]string, error) { ml := new(sm.Membership) ep := "/hsm/v2/memberships/" + id - s.getSMD(ep, ml) + if err := s.getSMD(ep, ml); err != nil { + return nil, err + } return ml.GroupLabels, nil }