Merge pull request 'Package encapic as an RPM for the VM puppet masters' (#2) from benvin/rpm-packaging into main
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
version: 2
|
||||
|
||||
project_name: encapic
|
||||
|
||||
builds:
|
||||
- id: encapic
|
||||
main: .
|
||||
binary: encapic
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- -s -w -X main.version={{ .Version }}
|
||||
|
||||
archives:
|
||||
# Raw binary, not an archive: the puppetserver-compiler initContainer fetches
|
||||
# this exact asset name by URL and sha256-verifies it before installing it.
|
||||
- id: binaries
|
||||
ids: [encapic]
|
||||
formats: [binary]
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
|
||||
|
||||
nfpms:
|
||||
- id: encapic
|
||||
ids: [encapic]
|
||||
package_name: encapic
|
||||
# encapic-<version>-1.x86_64.rpm
|
||||
file_name_template: "{{ .ConventionalFileName }}"
|
||||
vendor: unkin
|
||||
homepage: https://git.unkin.net/unkin/encapic
|
||||
maintainer: Ben Vincent <ben@unkin.net>
|
||||
description: Puppet exec ENC client for encapi
|
||||
license: MIT
|
||||
formats:
|
||||
- rpm
|
||||
release: "1"
|
||||
bindir: /usr/bin
|
||||
contents:
|
||||
# Puppet's exec node_terminus passes a certname and no environment, so
|
||||
# external_nodes points at this shim, which sources the URL first.
|
||||
- src: packaging/encapic-enc
|
||||
dst: /usr/bin/encapic-enc
|
||||
file_info:
|
||||
mode: 0755
|
||||
# noreplace: puppet manages this file's contents (hiera-driven URL), so an
|
||||
# upgrade must not overwrite it.
|
||||
- src: packaging/encapic.conf
|
||||
dst: /etc/encapic/encapic.conf
|
||||
type: "config|noreplace"
|
||||
file_info:
|
||||
mode: 0644
|
||||
|
||||
checksum:
|
||||
# One <artifact>.sha256 per file rather than a single checksums.txt: the
|
||||
# compiler initContainer fetches encapic_linux_amd64.sha256 next to the binary.
|
||||
split: true
|
||||
name_template: "{{ .ArtifactName }}.sha256"
|
||||
|
||||
release:
|
||||
gitea:
|
||||
owner: unkin
|
||||
name: encapic
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.unkin.net/api/v1
|
||||
download: https://git.unkin.net
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "^chore:"
|
||||
@@ -16,3 +16,25 @@ steps:
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
# The release itself only runs on a tag, so validate the packaging config and
|
||||
# build the artifacts (binary + RPM) here without publishing anything.
|
||||
- name: package
|
||||
image: golang:1.26
|
||||
commands:
|
||||
- git fetch --tags --force
|
||||
- go install github.com/goreleaser/goreleaser/v2@v2.17.1
|
||||
- goreleaser check
|
||||
- goreleaser release --snapshot --clean
|
||||
- ls -l dist/*.rpm dist/*.sha256
|
||||
depends_on: [build]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
+40
-29
@@ -18,13 +18,21 @@ steps:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
# Build the static linux/amd64 binary and its checksum, both attached to the
|
||||
# Gitea release. The binary is what the puppet compilers pull at pod start.
|
||||
- name: build
|
||||
image: golang:1.25
|
||||
# goreleaser is the single driver for the release: it builds the static
|
||||
# binary, builds the RPM (nfpm is built in) and creates the Gitea release
|
||||
# with the binary, RPM and per-artifact sha256 files attached. The binary
|
||||
# asset name is unchanged — the puppet compilers pull it at pod start.
|
||||
- name: goreleaser
|
||||
image: golang:1.26
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: RELEASER_TOKEN
|
||||
commands:
|
||||
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${CI_COMMIT_TAG}" -o encapic_linux_amd64 .
|
||||
- sha256sum encapic_linux_amd64 > encapic_linux_amd64.sha256
|
||||
# goreleaser reads the tag history for the changelog; Woodpecker's clone
|
||||
# can be shallow.
|
||||
- git fetch --tags --force
|
||||
- go install github.com/goreleaser/goreleaser/v2@v2.17.1
|
||||
- goreleaser release --clean
|
||||
depends_on: [test]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
@@ -37,35 +45,38 @@ steps:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
# Cut a Gitea release with the binary + checksum attached.
|
||||
- name: release
|
||||
# goreleaser cannot PUT to artifactapi, so the RPM it built into dist/ is
|
||||
# published to the local rpm repo here (a real yum repo; repodata
|
||||
# regenerates automatically).
|
||||
- name: upload-rpm
|
||||
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||
environment:
|
||||
RELEASER_TOKEN:
|
||||
from_secret: RELEASER_TOKEN
|
||||
commands:
|
||||
- |
|
||||
curl --output /usr/local/bin/tea https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote/gitea-dl/tea/0.12.0/tea-0.12.0-linux-amd64 && chmod +x /usr/local/bin/tea
|
||||
tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
if [ -n "$PREV_TAG" ]; then
|
||||
NOTES=$(git log "${PREV_TAG}..${CI_COMMIT_TAG}" --pretty=format:"- %s")
|
||||
else
|
||||
NOTES=$(git log --pretty=format:"- %s")
|
||||
fi
|
||||
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "${NOTES}" --login gitea --repo "${CI_REPO}"
|
||||
tea releases assets create "${CI_COMMIT_TAG}" \
|
||||
encapic_linux_amd64 \
|
||||
encapic_linux_amd64.sha256 \
|
||||
--login gitea --repo "${CI_REPO}"
|
||||
depends_on: [build]
|
||||
HOST="https://artifactapi.k8s.syd1.au.unkin.net"
|
||||
REPO="rpm-internal"
|
||||
for rpm in dist/*.rpm; do
|
||||
FILE=$$(basename "$$rpm")
|
||||
# artifactapi has no HEAD route (returns 405); probe with GET against
|
||||
# the served path (RPMs are stored under Packages/) to avoid re-upload.
|
||||
code=$$(curl -s -o /dev/null -w '%{http_code}' "$$HOST/api/v2/remotes/$$REPO/files/Packages/$$FILE" || true)
|
||||
if [ "$$code" = "200" ]; then
|
||||
echo "$$FILE already exists in $$REPO (HTTP $$code); skipping upload"
|
||||
continue
|
||||
fi
|
||||
echo "Uploading $$FILE to $$REPO (existence probe returned $$code)"
|
||||
curl -f -X PUT \
|
||||
"$$HOST/api/v2/remotes/$$REPO/files/$$FILE" \
|
||||
-H "Content-Type: application/x-rpm" \
|
||||
--data-binary @"$$rpm"
|
||||
done
|
||||
depends_on: [goreleaser]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
cpu: 100m
|
||||
limits:
|
||||
memory: 512Mi
|
||||
cpu: 500m
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
@@ -5,7 +5,7 @@ GOFLAGS := -ldflags="-s -w -X main.version=$(VERSION)"
|
||||
OS ?= $(shell go env GOOS)
|
||||
ARCH ?= $(shell go env GOARCH)
|
||||
|
||||
.PHONY: all build test lint fmt clean install patch minor major _tag
|
||||
.PHONY: all build test lint fmt pre-commit clean install rpm rpm-package patch minor major _tag
|
||||
|
||||
all: build
|
||||
|
||||
@@ -22,12 +22,24 @@ lint:
|
||||
fmt:
|
||||
gofmt -w .
|
||||
|
||||
pre-commit:
|
||||
uvx pre-commit run --all-files
|
||||
|
||||
clean:
|
||||
rm -rf $(DIST) $(BINARY)
|
||||
|
||||
install:
|
||||
go install $(GOFLAGS) .
|
||||
|
||||
# Build every release artifact locally without publishing: the raw binary the
|
||||
# puppet compilers fetch, and the RPM the VM masters install.
|
||||
rpm: rpm-package
|
||||
|
||||
# goreleaser drives the build and the nfpm packaging in one pass, so packaging
|
||||
# an already-built binary is not a separate step.
|
||||
rpm-package:
|
||||
goreleaser release --snapshot --clean
|
||||
|
||||
# Bump helpers — read the latest semver tag and create the next one.
|
||||
# If no tag exists yet, start from v0.0.0.
|
||||
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||
|
||||
@@ -69,8 +69,33 @@ make build # static binary into dist/
|
||||
make test # go test -v -race ./...
|
||||
make lint # golangci-lint
|
||||
make fmt # gofmt -w .
|
||||
make rpm # binary + RPM into dist/ via goreleaser, publishing nothing
|
||||
```
|
||||
|
||||
Release: `make minor` (etc.) tags `vX.Y.Z` and pushes it; the `release`
|
||||
Woodpecker pipeline builds `encapic_linux_amd64` (+ `.sha256`) and attaches
|
||||
them to a Gitea release.
|
||||
Woodpecker pipeline runs goreleaser, which attaches `encapic_linux_amd64`
|
||||
(+ `.sha256`) and the RPM to a Gitea release, and then publishes the RPM to the
|
||||
artifactapi `rpm-internal` yum repo.
|
||||
|
||||
## Installation
|
||||
|
||||
The kubernetes compilers fetch `encapic_linux_amd64` from the Gitea release in
|
||||
an initContainer and install it to `/opt/bin/encapic`.
|
||||
|
||||
The VM puppet masters install the RPM from `rpm-internal`:
|
||||
|
||||
```
|
||||
dnf install encapic
|
||||
```
|
||||
|
||||
It ships three paths:
|
||||
|
||||
- `/usr/bin/encapic` — the binary;
|
||||
- `/usr/bin/encapic-enc` — the ENC entrypoint. Puppet's exec `node_terminus`
|
||||
invokes `external_nodes` with a certname and no environment, so the shim
|
||||
sources the URL before exec'ing the binary. Point `external_nodes` at this,
|
||||
not at `/usr/bin/encapic`;
|
||||
- `/etc/encapic/encapic.conf` — `ENCAPI_URL=...`, defaulting to the external
|
||||
address `https://encapi.k8s.syd1.au.unkin.net` because a VM cannot resolve
|
||||
the in-cluster default. Shipped `%config(noreplace)` so puppet may own its
|
||||
contents without an upgrade reverting them.
|
||||
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# Puppet's exec node_terminus invokes external_nodes with a certname and no
|
||||
# environment, so the encapi URL cannot be inherited; source it here instead.
|
||||
set -eu
|
||||
|
||||
if [ -r /etc/encapic/encapic.conf ]; then
|
||||
. /etc/encapic/encapic.conf
|
||||
fi
|
||||
if [ -n "${ENCAPI_URL:-}" ]; then
|
||||
export ENCAPI_URL
|
||||
fi
|
||||
|
||||
exec /usr/bin/encapic "$@"
|
||||
@@ -0,0 +1,4 @@
|
||||
# Base URL of the encapi service, read by /usr/bin/encapic-enc.
|
||||
# The binary's compiled-in default is the in-cluster address, which VM puppet
|
||||
# masters cannot resolve.
|
||||
ENCAPI_URL=https://encapi.k8s.syd1.au.unkin.net
|
||||
@@ -81,6 +81,10 @@ func parseClasses(lines []string, i int, inline string) ([]string, int, error) {
|
||||
}
|
||||
|
||||
var classes []string
|
||||
// Entries of the block sit at one indentation level. A class's value may
|
||||
// itself be a nested block (class parameters, which encapi emits keyed by
|
||||
// role name), whose deeper lines are values, not class names.
|
||||
entryIndent := -1
|
||||
j := i + 1
|
||||
for ; j < len(lines); j++ {
|
||||
line := lines[j]
|
||||
@@ -88,18 +92,23 @@ func parseClasses(lines []string, i int, inline string) ([]string, int, error) {
|
||||
continue
|
||||
}
|
||||
trimmed := strings.TrimSpace(line)
|
||||
indent := len(line) - len(strings.TrimLeft(line, " \t"))
|
||||
// An unindented, non-list line is the next top-level key, so stop.
|
||||
if indent == 0 && !strings.HasPrefix(trimmed, "-") {
|
||||
break
|
||||
}
|
||||
if entryIndent < 0 {
|
||||
entryIndent = indent
|
||||
}
|
||||
if indent > entryIndent {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "- ") || trimmed == "-" {
|
||||
// Block list form, at either the parent indent ("- roles::base",
|
||||
// as yaml.dump emits) or nested (" - roles::base").
|
||||
classes = append(classes, unquote(strings.TrimSpace(trimmed[1:])))
|
||||
continue
|
||||
}
|
||||
// An indented, non-list line is a nested map entry belonging to
|
||||
// classes (" roles::base: {}"). An unindented, non-list line is the
|
||||
// next top-level key, so stop.
|
||||
if line[0] != ' ' && line[0] != '\t' {
|
||||
break
|
||||
}
|
||||
name, _, ok := splitKV(trimmed)
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("unexpected classes entry: %q", line)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// Nodes imported from cobbler carry class parameters, which encapi emits as a
|
||||
// nested block under the role name. ausyd1nxvm2020/2021/2022 carry the role
|
||||
// name as its own class parameter, so a line-based parser that treats every
|
||||
// indented line as a class name emits the role twice.
|
||||
func TestParseClassesNestedBlockIsOneClass(t *testing.T) {
|
||||
body := []byte("classes:\n" +
|
||||
" roles::infra::proxy::jumphost:\n" +
|
||||
" roles::infra::proxy::jumphost: '~'\n" +
|
||||
"environment: develop\n" +
|
||||
"parameters: {}\n")
|
||||
|
||||
doc, err := parseCobbler(body)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCobbler returned error: %v", err)
|
||||
}
|
||||
want := []string{"roles::infra::proxy::jumphost"}
|
||||
if len(doc.classes) != len(want) || doc.classes[0] != want[0] {
|
||||
t.Errorf("classes = %q, want %q", doc.classes, want)
|
||||
}
|
||||
if doc.environment != "develop" {
|
||||
t.Errorf("environment = %q, want develop", doc.environment)
|
||||
}
|
||||
}
|
||||
|
||||
// The same shape with ordinary class parameters: the parameter keys are values
|
||||
// of the class, never classes themselves.
|
||||
func TestParseClassesNestedParamsAreNotClasses(t *testing.T) {
|
||||
body := []byte("classes:\n" +
|
||||
" roles::base:\n" +
|
||||
" listen_port: 8140\n" +
|
||||
" tls: true\n" +
|
||||
" roles::infra::dns::master: {}\n" +
|
||||
"environment: production\n" +
|
||||
"parameters: {}\n")
|
||||
|
||||
doc, err := parseCobbler(body)
|
||||
if err != nil {
|
||||
t.Fatalf("parseCobbler returned error: %v", err)
|
||||
}
|
||||
want := []string{"roles::base", "roles::infra::dns::master"}
|
||||
if len(doc.classes) != len(want) {
|
||||
t.Fatalf("classes = %q, want %q", doc.classes, want)
|
||||
}
|
||||
for i := range want {
|
||||
if doc.classes[i] != want[i] {
|
||||
t.Errorf("classes[%d] = %q, want %q", i, doc.classes[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user