Compare commits
4 Commits
benvin/initial
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe44518287 | |||
| 2241ea3d72 | |||
| 1e48c06bfe | |||
| 75ed5e5cdf |
@@ -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:
|
limits:
|
||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
cpu: 2
|
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
|
||||||
|
|||||||
+39
-28
@@ -18,13 +18,21 @@ steps:
|
|||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
cpu: 2
|
cpu: 2
|
||||||
|
|
||||||
# Build the static linux/amd64 binary and its checksum, both attached to the
|
# goreleaser is the single driver for the release: it builds the static
|
||||||
# Gitea release. The binary is what the puppet compilers pull at pod start.
|
# binary, builds the RPM (nfpm is built in) and creates the Gitea release
|
||||||
- name: build
|
# with the binary, RPM and per-artifact sha256 files attached. The binary
|
||||||
image: golang:1.25
|
# 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:
|
commands:
|
||||||
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${CI_COMMIT_TAG}" -o encapic_linux_amd64 .
|
# goreleaser reads the tag history for the changelog; Woodpecker's clone
|
||||||
- sha256sum encapic_linux_amd64 > encapic_linux_amd64.sha256
|
# can be shallow.
|
||||||
|
- git fetch --tags --force
|
||||||
|
- go install github.com/goreleaser/goreleaser/v2@v2.17.1
|
||||||
|
- goreleaser release --clean
|
||||||
depends_on: [test]
|
depends_on: [test]
|
||||||
backend_options:
|
backend_options:
|
||||||
kubernetes:
|
kubernetes:
|
||||||
@@ -37,35 +45,38 @@ steps:
|
|||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
cpu: 2
|
cpu: 2
|
||||||
|
|
||||||
# Cut a Gitea release with the binary + checksum attached.
|
# goreleaser cannot PUT to artifactapi, so the RPM it built into dist/ is
|
||||||
- name: release
|
# 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
|
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||||
environment:
|
|
||||||
RELEASER_TOKEN:
|
|
||||||
from_secret: RELEASER_TOKEN
|
|
||||||
commands:
|
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
|
HOST="https://artifactapi.k8s.syd1.au.unkin.net"
|
||||||
tea logins add --name gitea --url https://git.unkin.net --token "$${RELEASER_TOKEN}" --no-version-check
|
REPO="rpm-internal"
|
||||||
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
for rpm in dist/*.rpm; do
|
||||||
if [ -n "$PREV_TAG" ]; then
|
FILE=$$(basename "$$rpm")
|
||||||
NOTES=$(git log "${PREV_TAG}..${CI_COMMIT_TAG}" --pretty=format:"- %s")
|
# artifactapi has no HEAD route (returns 405); probe with GET against
|
||||||
else
|
# the served path (RPMs are stored under Packages/) to avoid re-upload.
|
||||||
NOTES=$(git log --pretty=format:"- %s")
|
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
|
fi
|
||||||
tea releases create --tag "${CI_COMMIT_TAG}" --title "${CI_COMMIT_TAG}" --note "${NOTES}" --login gitea --repo "${CI_REPO}"
|
echo "Uploading $$FILE to $$REPO (existence probe returned $$code)"
|
||||||
tea releases assets create "${CI_COMMIT_TAG}" \
|
curl -f -X PUT \
|
||||||
encapic_linux_amd64 \
|
"$$HOST/api/v2/remotes/$$REPO/files/$$FILE" \
|
||||||
encapic_linux_amd64.sha256 \
|
-H "Content-Type: application/x-rpm" \
|
||||||
--login gitea --repo "${CI_REPO}"
|
--data-binary @"$$rpm"
|
||||||
depends_on: [build]
|
done
|
||||||
|
depends_on: [goreleaser]
|
||||||
backend_options:
|
backend_options:
|
||||||
kubernetes:
|
kubernetes:
|
||||||
serviceAccountName: default
|
serviceAccountName: default
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: 128Mi
|
|
||||||
cpu: 100m
|
|
||||||
limits:
|
|
||||||
memory: 512Mi
|
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)
|
OS ?= $(shell go env GOOS)
|
||||||
ARCH ?= $(shell go env GOARCH)
|
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
|
all: build
|
||||||
|
|
||||||
@@ -22,12 +22,24 @@ lint:
|
|||||||
fmt:
|
fmt:
|
||||||
gofmt -w .
|
gofmt -w .
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
uvx pre-commit run --all-files
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf $(DIST) $(BINARY)
|
rm -rf $(DIST) $(BINARY)
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go install $(GOFLAGS) .
|
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.
|
# Bump helpers — read the latest semver tag and create the next one.
|
||||||
# If no tag exists yet, start from v0.0.0.
|
# 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)
|
_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 test # go test -v -race ./...
|
||||||
make lint # golangci-lint
|
make lint # golangci-lint
|
||||||
make fmt # gofmt -w .
|
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`
|
Release: `make minor` (etc.) tags `vX.Y.Z` and pushes it; the `release`
|
||||||
Woodpecker pipeline builds `encapic_linux_amd64` (+ `.sha256`) and attaches
|
Woodpecker pipeline runs goreleaser, which attaches `encapic_linux_amd64`
|
||||||
them to a Gitea release.
|
(+ `.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
|
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
|
j := i + 1
|
||||||
for ; j < len(lines); j++ {
|
for ; j < len(lines); j++ {
|
||||||
line := lines[j]
|
line := lines[j]
|
||||||
@@ -88,18 +92,23 @@ func parseClasses(lines []string, i int, inline string) ([]string, int, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
trimmed := strings.TrimSpace(line)
|
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 == "-" {
|
if strings.HasPrefix(trimmed, "- ") || trimmed == "-" {
|
||||||
// Block list form, at either the parent indent ("- roles::base",
|
// Block list form, at either the parent indent ("- roles::base",
|
||||||
// as yaml.dump emits) or nested (" - roles::base").
|
// as yaml.dump emits) or nested (" - roles::base").
|
||||||
classes = append(classes, unquote(strings.TrimSpace(trimmed[1:])))
|
classes = append(classes, unquote(strings.TrimSpace(trimmed[1:])))
|
||||||
continue
|
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)
|
name, _, ok := splitKV(trimmed)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, 0, fmt.Errorf("unexpected classes entry: %q", line)
|
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