mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 12:40:36 -04:00
feat: update to osixia/baseimage 2.0.0 and add integration tests
- Update the base image to osixia/baseimage:alpine-2.0.0 and build the CI/CD tool with osixia/dagger-build (Go 1.26.9). - Add Dagger integration tests: default bootstrap, alternative schemas, all compatible modules and schemas with every bootstrap feature on a 3-node replicated cluster with TLS required, backup/restore, simulated upgrade and upgrade from previous releases. - Fix bootstrap failing with "Argument list too long" with large schemas: load them with LDIF include: lines, fail on missing schemas. - Default OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY to 1 so pwdMinLength is enforced. - Align the CI workflow with osixia/container-baseimage and add Dependabot. - Document available schemas and modules and their incompatibilities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa0eae90d5
commit
52230d6cd7
@@ -0,0 +1,21 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: develop
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: gomod
|
||||
directory: /build
|
||||
target-branch: develop
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- "*"
|
||||
+71
-23
@@ -1,6 +1,9 @@
|
||||
name: osixia
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -10,65 +13,110 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/setup-go@v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
go-version: '1.26.1'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: '1.26.9'
|
||||
cache: false
|
||||
- name: golangci-lint build
|
||||
uses: golangci/golangci-lint-action@v9.3.0
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
||||
with:
|
||||
version: v2.14.0
|
||||
working-directory: build
|
||||
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: '1.26.9'
|
||||
- name: Run build tool tests
|
||||
working-directory: build
|
||||
run: go test -race ./...
|
||||
|
||||
shellcheck:
|
||||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@2.0.0
|
||||
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
|
||||
|
||||
govulncheck:
|
||||
name: govulncheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: govulncheck build
|
||||
uses: golang/govulncheck-action@v1.1.0
|
||||
uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0
|
||||
with:
|
||||
go-version-input: '1.26.9'
|
||||
work-dir: build
|
||||
|
||||
build:
|
||||
name: build
|
||||
if: "!startsWith(github.ref, 'refs/tags/')"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, shellcheck, govulncheck]
|
||||
needs: [lint, test, shellcheck, govulncheck]
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: '1.26.9'
|
||||
- name: Vendor build go dependencies
|
||||
working-directory: build
|
||||
run: go mod vendor
|
||||
- name: Run tests
|
||||
working-directory: build
|
||||
run: go run main.go provider github ../Dockerfile --github-ref "${GITHUB_REF}" ${{ vars.OSX_GITHUB_FLAGS }}
|
||||
|
||||
publish:
|
||||
name: publish
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, test, shellcheck, govulncheck]
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
env:
|
||||
DIGESTS_FILE: ${{ github.workspace }}/digests.txt
|
||||
steps:
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/setup-go@v7.0.0
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
go-version: '1.26.1'
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: '1.26.9'
|
||||
cache: false
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v4.1.2
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
- name: Install syft
|
||||
uses: anchore/sbom-action/download-syft@v0.24.3
|
||||
uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
|
||||
- name: Vendor build go dependencies
|
||||
run: |
|
||||
cd build
|
||||
go mod vendor
|
||||
working-directory: build
|
||||
run: go mod vendor
|
||||
- name: Run pipeline
|
||||
run: |
|
||||
cd build
|
||||
go run main.go provider github ../Dockerfile --github-ref "${GITHUB_REF}" --github-token "${{ secrets.GITHUB_TOKEN }}" --changelog ../CHANGELOG.md --username "${{ secrets.REGISTRY_USERNAME }}" --password "${{ secrets.REGISTRY_PASSWORD }}" --digests-file "${DIGESTS_FILE}" ${{ vars.OSX_GITHUB_FLAGS }}
|
||||
- name: Sign images and generate SBOM
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
working-directory: build
|
||||
env:
|
||||
COSIGN_EXPERIMENTAL: '1'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: go run main.go provider github ../Dockerfile --github-ref "${GITHUB_REF}" --github-token "${GITHUB_TOKEN}" --changelog ../CHANGELOG.md --username "${REGISTRY_USERNAME}" --password "${REGISTRY_PASSWORD}" --digests-file "${DIGESTS_FILE}" ${{ vars.OSX_GITHUB_FLAGS }}
|
||||
- name: Sign images and generate SBOM
|
||||
env:
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
|
||||
printf '%s' "${REGISTRY_PASSWORD}" | docker login -u "${REGISTRY_USERNAME}" --password-stdin
|
||||
while IFS= read -r ref; do
|
||||
cosign sign --yes --registry-referrers-mode=oci-1-1 "${ref}"
|
||||
syft "${ref}" -o spdx-json > sbom.spdx.json
|
||||
|
||||
@@ -14,7 +14,7 @@ Guidance for coding agents working on `osixia/openldap` (v2), an OpenLDAP contai
|
||||
| `services/openldap-upgrade/` | Version check + restore-based migration on start. |
|
||||
| `services/openldap-pre-upgrade/` | Pre-upgrade backup, meant to run as an **init container on the N-1 image** (see below). |
|
||||
| `services/openldap-cron/` | `crond` running scheduled backups (as root, separate container). |
|
||||
| `build/` | Go CI/CD tool wrapping `osixia/container-baseimage/build` (dagger). Not part of the image. |
|
||||
| `build/` | Go CI/CD tool wrapping [osixia/dagger-build](https://github.com/osixia/dagger-build). Not part of the image. |
|
||||
| `docs/examples/` | Compose examples (quickstart, replication). |
|
||||
|
||||
`.dockerignore` excludes `*.md`, `.gitkeep`, `build/`, `docs/`: none of them reach the image.
|
||||
@@ -96,10 +96,11 @@ Adding a feature: create `services/openldap-bootstrap/assets/scripts/NN-feature.
|
||||
## Build, test, CI
|
||||
|
||||
- CI (`.github/workflows/go.yml`): golangci-lint + govulncheck on `build/`, ShellCheck on the repo, then the build pipeline (multi-arch build, tests, push, cosign signing + SBOM on tags).
|
||||
- Integration tests live in `build/tests/` (Go scenarios + bash checks in `testdata/`), see `build/README.md`. Add a scenario or a check there when adding a feature. Tested combinations: modules except `dyngroup.so` (conflicts with `dynlist.so`) and `mqtt.so` (conflicts with `lastbind.so`), schemas except `msuser.ldif` (not loadable) and `nis.ldif`/`virtualMail.ldif` (conflict with `rfc2307bis.ldif`/`postfix-book.ldif`, tested separately).
|
||||
- Local:
|
||||
```bash
|
||||
shellcheck $(git ls-files '*.sh')
|
||||
cd build && go mod vendor && go run main.go job test ../Dockerfile
|
||||
cd build && go mod vendor && go run main.go job test ../Dockerfile --arches amd64
|
||||
docker build -t osixia/openldap:dev .
|
||||
docker run --rm -it osixia/openldap:dev --log-level debug # --bash, --skip-process also useful
|
||||
```
|
||||
|
||||
+8
-1
@@ -8,8 +8,12 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
- Add the Postfix Book schema in both OpenLDAP `.schema` and `cn=config` `.ldif` formats.
|
||||
- Improve debug logging for file moves and schema conversion, including a check that the generated LDIF file exists.
|
||||
- Update the base image to `osixia/baseimage:alpine-2.0.0-rc3`.
|
||||
- Update the base image to `osixia/baseimage:alpine-2.0.0`.
|
||||
- Build the CI/CD tool with `osixia/dagger-build` (replaces `osixia/container-baseimage/build`) and Go 1.26.9.
|
||||
- Add integration tests: default bootstrap, all compatible modules and schemas, every bootstrap feature on a 3-node replicated cluster with TLS required, backup/restore and upgrades (simulated and from previous releases).
|
||||
- Harden the CI workflow: read-only default permissions, actions pinned by commit SHA, images only published from tags, Dependabot updates.
|
||||
- Allow authenticated users to read `entry`, `objectClass`, `ou`, `o` and `dc` in addition to `cn`, `sn`, `givenName`, `mail` and `uid` in the default database ACL (new bootstraps only).
|
||||
- Set `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY` to `1` (was `0`) so `pwdMinLength` is enforced on clear text password changes; pre-hashed passwords are still accepted (new bootstraps only).
|
||||
- Fail bootstrap with an explicit error when `OPENLDAP_BOOTSTRAP_SUFFIX` does not start with `dc=`, instead of creating a root entry with an empty `dc`.
|
||||
- Fail bootstrap with an explicit error when `OPENLDAP_BOOTSTRAP_REPLICATION_HOSTS` contains more than 9 hosts.
|
||||
- Check that backup files exist and are valid gzip archives before `openldap-ctl restore` stops OpenLDAP or deletes existing data.
|
||||
@@ -21,12 +25,15 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
- Fix `openldap-ctl password hash` for passwords containing spaces or shell special characters.
|
||||
- Fix the default database ACL so authenticated users can actually read the allowed attributes.
|
||||
- Remove bootstrap temporary files, including on failure.
|
||||
- Fix bootstrap failing with `Argument list too long` when the schemas listed in `OPENLDAP_BOOTSTRAP_SCHEMAS` exceed 128 KiB: schemas are now loaded with LDIF `include:` lines instead of being copied into an environment variable.
|
||||
- Fail bootstrap with an explicit error when a schema listed in `OPENLDAP_BOOTSTRAP_SCHEMAS` does not exist.
|
||||
- Strip leading and trailing empty lines from every bootstrap LDIF fragment, including the first file of each prefix group.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document the Postfix Book schema and its incompatibility with the `virtualMail` schema.
|
||||
- Improve the schema conversion example for local files and non-root execution.
|
||||
- List the schemas and modules available in the image, with their incompatibilities.
|
||||
|
||||
|
||||
## 2.6.15-alpha
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE="osixia/baseimage:alpine-2.0.0-rc3"
|
||||
ARG BASE_IMAGE="osixia/baseimage:alpine-2.0.0"
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG IMAGE="osixia/openldap:develop"
|
||||
|
||||
@@ -385,7 +385,7 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_GROUP_DN` | DN of the policy container created in the main database | `ou=policies,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_POLICY_DN` | DN of the default password policy | `cn=default,${OPENLDAP_BOOTSTRAP_PPOLICY_GROUP_DN}` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_LENGTH` | Value for `pwdMinLength` | `12` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY` | Value for `pwdCheckQuality` | `0` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY` | Value for `pwdCheckQuality` | `1` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_IN_HISTORY` | Value for `pwdInHistory` | `12` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_ALLOW_USER_CHANGE` | Value for `pwdAllowUserChange` | `true` |
|
||||
| `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_SAFE_MODIFY` | Value for `pwdSafeModify` | `true` |
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# CI/CD tool
|
||||
|
||||
Built with [osixia/dagger-build](https://github.com/osixia/dagger-build).
|
||||
|
||||
## Get Help
|
||||
```
|
||||
go run main.go --help
|
||||
@@ -10,3 +12,20 @@ go run main.go --help
|
||||
go mod vendor
|
||||
go run main.go job test ../Dockerfile
|
||||
```
|
||||
|
||||
Limit a local run with `--arches amd64`.
|
||||
|
||||
After the shared version check, `tests/` runs integration scenarios on the image built for
|
||||
the engine platform:
|
||||
|
||||
- default bootstrap;
|
||||
- alternative schemas (`nis`, `virtualMail`);
|
||||
- all features with 3 replicated nodes: all compatible modules and schemas, TLS required,
|
||||
readonly users, monitor, ppolicy, unique, refint, memberof and multi-provider replication;
|
||||
- `openldap-ctl` backup and restore;
|
||||
- upgrade: pre-upgrade backup on a simulated N-1 image, restore on N, downgrade refused;
|
||||
- upgrade from previous published releases (`previousReleases` in `tests/upgrade.go`).
|
||||
|
||||
Scenarios are written in Go and run their LDAP checks with the scripts in
|
||||
`tests/testdata/`. Replication nodes run with root capabilities in derived test containers,
|
||||
to get their hostname (see `tests/testdata/node.sh`).
|
||||
|
||||
+6
-3
@@ -1,15 +1,18 @@
|
||||
module github.com/osixia/container-openldap/build
|
||||
|
||||
go 1.25.0
|
||||
go 1.26.9
|
||||
|
||||
require github.com/osixia/container-baseimage/build v0.0.0-20261007183720-dc9d7714269f
|
||||
require (
|
||||
dagger.io/dagger v0.21.10
|
||||
github.com/osixia/dagger-build v0.0.0-20261015194426-55e8bdd25dbb
|
||||
)
|
||||
|
||||
require (
|
||||
dagger.io/dagger v0.20.6 // indirect
|
||||
github.com/99designs/gqlgen v0.17.90 // indirect
|
||||
github.com/Khan/genqlient v0.8.1 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/google/go-github/v69 v69.2.0 // indirect
|
||||
|
||||
+6
-4
@@ -1,5 +1,5 @@
|
||||
dagger.io/dagger v0.20.6 h1:Uac8tdqieoq+psVe5f+rhNk1sk2W8YK5t4OzpGyHuTI=
|
||||
dagger.io/dagger v0.20.6/go.mod h1:ZXg8+pQZaZUC8rAw4V/gPP8aKvKARIJZ+pfcV+RC1es=
|
||||
dagger.io/dagger v0.21.10 h1:TNg1nvHZWOIt5JkTZjfF4SCGW/Y31y1qeUq6vKQ/NXU=
|
||||
dagger.io/dagger v0.21.10/go.mod h1:8hztpM9rKNjSmOa2nzd8ot6aw1RZFDWCawMfc5V9tXA=
|
||||
github.com/99designs/gqlgen v0.17.90 h1:wSv6blm/PoplU6QoNw83EcQpNtC0HX3/+44vITJOzpk=
|
||||
github.com/99designs/gqlgen v0.17.90/go.mod h1:GqYrEwYsqCG8VaOsq2kJUCUKwAE1T+u2i+Nj7NtXiVI=
|
||||
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
|
||||
@@ -13,6 +13,8 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
|
||||
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/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59 h1:g6vfdGRyz6fAjfHz5FyYPZgHy8qcQ31fHrBl1iCOzxw=
|
||||
github.com/dagger/querybuilder v0.0.0-20260402040506-574a5e81cb59/go.mod h1:jsdUJeYzcbyK1j/EqMGPrQgNYxl/Zfg06vvM9C/xXxs=
|
||||
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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -35,8 +37,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/osixia/container-baseimage/build v0.0.0-20261007183720-dc9d7714269f h1:nxKAccHKyXoa5Sj15DVK+yEiuhN006YulVNAqP35L20=
|
||||
github.com/osixia/container-baseimage/build v0.0.0-20261007183720-dc9d7714269f/go.mod h1:gLTe03yDFd9kIL22dHdPad6G0f/u/NI+VB4+3cOHQbY=
|
||||
github.com/osixia/dagger-build v0.0.0-20261015194426-55e8bdd25dbb h1:V8FLn00GbtrHvOTII63DaCSZDoi2zQvmIztr20FmOw4=
|
||||
github.com/osixia/dagger-build v0.0.0-20261015194426-55e8bdd25dbb/go.mod h1:gxxc7tVjlUVYPeBpDBdWs1MFHuP48AR6zes7F1NIMKI=
|
||||
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=
|
||||
|
||||
+20
-3
@@ -4,8 +4,12 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/osixia/container-baseimage/build/cmd"
|
||||
"github.com/osixia/container-baseimage/build/config"
|
||||
"dagger.io/dagger"
|
||||
|
||||
"github.com/osixia/container-openldap/build/tests"
|
||||
"github.com/osixia/dagger-build/cmd"
|
||||
"github.com/osixia/dagger-build/config"
|
||||
"github.com/osixia/dagger-build/job"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -13,7 +17,7 @@ func main() {
|
||||
// image
|
||||
|
||||
var OpenLDAPImage = &config.Image{
|
||||
BaseImage: "osixia/baseimage:alpine-2.0.0-rc3",
|
||||
BaseImage: "osixia/baseimage:alpine-2.0.0",
|
||||
Distribution: config.Alpine,
|
||||
|
||||
Name: "osixia/openldap",
|
||||
@@ -42,6 +46,19 @@ func main() {
|
||||
Project: "container-openldap",
|
||||
}
|
||||
|
||||
// tests
|
||||
|
||||
job.Test = func(ctx context.Context, client *dagger.Client, options any) ([]*job.BuildResult, error) {
|
||||
builds, err := job.DefaultTest(ctx, client, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tests.Run(ctx, client, builds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
// execute cmd
|
||||
|
||||
mainCtx := context.Background()
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
const defaultSchemas = "core.ldif cosine.ldif inetorgperson.ldif rfc2307bis.ldif"
|
||||
|
||||
// allSchemas can be loaded together. Excluded:
|
||||
// - nis.ldif and virtualMail.ldif conflict with rfc2307bis.ldif and postfix-book.ldif,
|
||||
// they are tested by AlternativeSchemas;
|
||||
// - msuser.ldif conflicts with inetorgperson.ldif and contains duplicated attribute types
|
||||
// (OpenLDAP ships it "for informational purposes only").
|
||||
var allSchemas = []string{
|
||||
"core.ldif", "cosine.ldif", "inetorgperson.ldif", "rfc2307bis.ldif",
|
||||
// OpenLDAP
|
||||
"collective.ldif", "corba.ldif", "dsee.ldif", "duaconf.ldif", "dyngroup.ldif", "java.ldif",
|
||||
"misc.ldif", "namedobject.ldif", "openldap.ldif", "pmi.ldif",
|
||||
// osixia/openldap
|
||||
"eduperson.ldif", "kerberos.ldif", "openssh-lpk.ldif", "postfix-book.ldif", "samba.ldif",
|
||||
"sudo.ldif", "totp.ldif",
|
||||
}
|
||||
|
||||
// allModules can be loaded together. Excluded: dyngroup.so conflicts with dynlist.so and
|
||||
// mqtt.so conflicts with lastbind.so.
|
||||
var allModules = []string{
|
||||
"back_mdb.so", "argon2.so", "ppolicy.so", "unique.so", "refint.so", "memberof.so", "syncprov.so",
|
||||
"accesslog.so", "auditlog.so", "autoca.so", "back_asyncmeta.so", "back_dnssrv.so", "back_ldap.so",
|
||||
"back_meta.so", "back_null.so", "back_passwd.so", "back_relay.so", "back_sock.so", "back_sql.so",
|
||||
"collect.so", "constraint.so", "dds.so", "deref.so", "dynlist.so", "homedir.so", "lastbind.so",
|
||||
"lloadd.so", "nestgroup.so", "otp.so", "pcache.so", "pw-pbkdf2.so", "pw-sha2.so", "remoteauth.so",
|
||||
"retcode.so", "rwm.so", "seqmod.so", "sssvlv.so", "translucent.so", "valsort.so",
|
||||
}
|
||||
|
||||
// rootPasswords sets the config and database root passwords to the test password.
|
||||
func rootPasswords(ctx context.Context, image *dagger.Container) (env, error) {
|
||||
|
||||
hash, err := hashPassword(ctx, image)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env{
|
||||
"OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED": hash,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED": hash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Default starts the image with its default configuration.
|
||||
func Default(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const node = "ldap"
|
||||
|
||||
service, err := startNode(ctx, image, node, passwords, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopNodes(ctx, []*dagger.Service{service})
|
||||
|
||||
return check(ctx, image, nil, env{"TEST_NODES": node, "TEST_SCHEMAS": defaultSchemas}, defaultCheck)
|
||||
}
|
||||
|
||||
// AlternativeSchemas bootstraps with the schemas that conflict with the default ones.
|
||||
func AlternativeSchemas(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
schemas := "core.ldif cosine.ldif inetorgperson.ldif nis.ldif virtualMail.ldif"
|
||||
|
||||
r, err := expect(ctx, withEnv(image, env{"OPENLDAP_BOOTSTRAP_SCHEMAS": schemas}), true,
|
||||
[]string{"config file testing succeeded"}, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = expect(ctx, r.container, true, []string{"cn: {3}nis", "cn: {4}virtualMail"},
|
||||
"container", "run", "--", "slapcat", "-n", "0", "-a", "(objectClass=olcSchemaConfig)")
|
||||
return err
|
||||
}
|
||||
|
||||
// AllFeatures enables every bootstrap feature, all compatible modules and schemas, on a
|
||||
// 3 nodes multi-provider cluster with TLS required.
|
||||
func AllFeatures(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
hash, err := hashPassword(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nodes := []string{"ldap1", "ldap2", "ldap3"}
|
||||
certs := certificates(client, nodes)
|
||||
|
||||
hosts := make([]string, 0, len(nodes))
|
||||
for _, n := range nodes {
|
||||
hosts = append(hosts, fmt.Sprintf("ldap://%s:3890", n))
|
||||
}
|
||||
|
||||
features := env{
|
||||
"OPENLDAP_BOOTSTRAP_MODULES": strings.Join(allModules, " "),
|
||||
"OPENLDAP_BOOTSTRAP_SCHEMAS": strings.Join(allSchemas, " "),
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED": hash,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED": hash,
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY_PASSWORD_HASHED": hash,
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_MONITOR_ENABLED": "true",
|
||||
"OPENLDAP_BOOTSTRAP_MONITOR_READONLY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_MONITOR_READONLY_PASSWORD_HASHED": hash,
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_TLS": "true",
|
||||
"OPENLDAP_BOOTSTRAP_TLS_REQUIRED": "true",
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_REFINT": "true",
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_UNIQUE": "true",
|
||||
"OPENLDAP_BOOTSTRAP_MEMBEROF": "true",
|
||||
|
||||
"OPENLDAP_BOOTSTRAP_REPLICATION": "true",
|
||||
"OPENLDAP_BOOTSTRAP_REPLICATION_HOSTS": strings.Join(hosts, " "),
|
||||
"OPENLDAP_BOOTSTRAP_REPLICATION_DATA_READONLY_PASSWORD": password,
|
||||
}
|
||||
|
||||
services := make([]*dagger.Service, 0, len(nodes))
|
||||
defer func() { stopNodes(ctx, services) }()
|
||||
|
||||
for _, n := range nodes {
|
||||
urls := env{"OPENLDAP_URLS": fmt.Sprintf("ldap://%s:3890 ldaps://%s:6360 ldapi:///", n, n)}
|
||||
|
||||
service, err := startNode(ctx, image, n, merge(features, urls), certs, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
services = append(services, service)
|
||||
}
|
||||
|
||||
return check(ctx, image, certs, env{
|
||||
"TEST_NODES": strings.Join(nodes, " "),
|
||||
"TEST_MODULES": strings.Join(allModules, " "),
|
||||
"TEST_SCHEMAS": strings.Join(allSchemas, " "),
|
||||
}, fullCheck)
|
||||
}
|
||||
|
||||
//go:embed testdata/default.sh
|
||||
var defaultCheck string
|
||||
|
||||
//go:embed testdata/full.sh
|
||||
var fullCheck string
|
||||
@@ -0,0 +1,258 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
const (
|
||||
suffix = "dc=example,dc=org"
|
||||
password = "Test-passw0rd-2026"
|
||||
|
||||
confDir = "/etc/openldap/slapd.d"
|
||||
dataDir = "/var/lib/openldap/openldap-data"
|
||||
backupDir = "/var/lib/openldap/openldap-backups"
|
||||
certsDir = "/container/services/openldap/assets/certs"
|
||||
|
||||
ldapUser = "ldap"
|
||||
)
|
||||
|
||||
// env is a set of environment variables, applied in a stable order to keep Dagger caching
|
||||
// deterministic.
|
||||
type env map[string]string
|
||||
|
||||
func withEnv(c *dagger.Container, e env) *dagger.Container {
|
||||
keys := make([]string, 0, len(e))
|
||||
for k := range e {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
c = c.WithEnvVariable(k, e[k])
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// merge returns a new env with the values of all given envs, later ones win.
|
||||
func merge(envs ...env) env {
|
||||
r := env{}
|
||||
for _, e := range envs {
|
||||
for k, v := range e {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// volumes holds the OpenLDAP config, data and backup directories, shared between containers
|
||||
// like Docker or Kubernetes volumes.
|
||||
type volumes struct {
|
||||
conf *dagger.Directory
|
||||
data *dagger.Directory
|
||||
backup *dagger.Directory
|
||||
}
|
||||
|
||||
func emptyVolumes(client *dagger.Client) volumes {
|
||||
return volumes{conf: client.Directory(), data: client.Directory(), backup: client.Directory()}
|
||||
}
|
||||
|
||||
func withVolumes(c *dagger.Container, v volumes) *dagger.Container {
|
||||
opts := dagger.ContainerWithDirectoryOpts{Owner: ldapUser}
|
||||
return c.WithUser("0").
|
||||
WithDirectory(confDir, v.conf, opts).
|
||||
WithDirectory(dataDir, v.data, opts).
|
||||
WithDirectory(backupDir, v.backup, opts).
|
||||
WithUser(ldapUser)
|
||||
}
|
||||
|
||||
func readVolumes(c *dagger.Container) volumes {
|
||||
return volumes{conf: c.Directory(confDir), data: c.Directory(dataDir), backup: c.Directory(backupDir)}
|
||||
}
|
||||
|
||||
// result is the outcome of a command that is allowed to fail.
|
||||
type result struct {
|
||||
container *dagger.Container
|
||||
code int
|
||||
output string
|
||||
}
|
||||
|
||||
// run executes args in c, without failing on a non-zero exit code. A random value busts the
|
||||
// Dagger cache so the command really runs on each test run.
|
||||
func run(ctx context.Context, c *dagger.Container, args ...string) (*result, error) {
|
||||
|
||||
c = c.WithEnvVariable("OPENLDAP_TEST_RUN", rand.Text()).
|
||||
WithExec(args, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny})
|
||||
|
||||
code, err := c.ExitCode(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v: %w", args, err)
|
||||
}
|
||||
stdout, err := c.Stdout(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v stdout: %w", args, err)
|
||||
}
|
||||
stderr, err := c.Stderr(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%v stderr: %w", args, err)
|
||||
}
|
||||
|
||||
return &result{container: c, code: code, output: stdout + stderr}, nil
|
||||
}
|
||||
|
||||
// expect runs args in c and checks its exit status and that its output contains every
|
||||
// wanted string.
|
||||
func expect(ctx context.Context, c *dagger.Container, success bool, want []string, args ...string) (*result, error) {
|
||||
|
||||
r, err := run(ctx, c, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if success && r.code != 0 {
|
||||
return nil, fmt.Errorf("%v: exit code %d, expected success\n%s", args, r.code, r.output)
|
||||
}
|
||||
if !success && r.code == 0 {
|
||||
return nil, fmt.Errorf("%v: succeeded, expected failure\n%s", args, r.output)
|
||||
}
|
||||
|
||||
for _, w := range want {
|
||||
if !strings.Contains(r.output, w) {
|
||||
return nil, fmt.Errorf("%v: output does not contain %q\n%s", args, w, r.output)
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// hashPassword hashes password with the image's openldap-ctl and its default {ARGON2} scheme.
|
||||
func hashPassword(ctx context.Context, image *dagger.Container) (string, error) {
|
||||
return hashPasswordWith(ctx, image, "{ARGON2}", nil)
|
||||
}
|
||||
|
||||
// hashPasswordWith hashes password with the image's openldap-ctl, e is applied to change
|
||||
// the hash command.
|
||||
func hashPasswordWith(ctx context.Context, image *dagger.Container, scheme string, e env) (string, error) {
|
||||
|
||||
out, err := withEnv(image, e).WithExec([]string{"container", "run", "--", "openldap-ctl", "password", "hash", password}).Stdout(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
|
||||
hash := strings.TrimSpace(out)
|
||||
if !strings.HasPrefix(hash, scheme) {
|
||||
return "", fmt.Errorf("unexpected password hash: %q", hash)
|
||||
}
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// certificates generates a test CA and a server certificate valid for hosts.
|
||||
func certificates(client *dagger.Client, hosts []string) *dagger.Directory {
|
||||
|
||||
sans := make([]string, 0, len(hosts))
|
||||
for _, h := range hosts {
|
||||
sans = append(sans, "DNS:"+h)
|
||||
}
|
||||
|
||||
// a random value busts the Dagger cache, so certificates never expire between runs
|
||||
work := client.Container().From("alpine:3.24").
|
||||
WithExec([]string{"apk", "add", "--no-cache", "openssl"}).
|
||||
WithEnvVariable("OPENLDAP_TEST_RUN", rand.Text()).
|
||||
WithWorkdir("/work").
|
||||
WithNewFile("/work/san.ext", "subjectAltName="+strings.Join(sans, ",")+"\n").
|
||||
WithExec([]string{"openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2", "-subj", "/CN=osixia-openldap-test-ca", "-keyout", "ca.key", "-out", "ca.crt"}).
|
||||
WithExec([]string{"openssl", "req", "-newkey", "rsa:2048", "-nodes", "-subj", "/CN=" + hosts[0], "-keyout", "tls.key", "-out", "tls.csr"}).
|
||||
WithExec([]string{"openssl", "x509", "-req", "-days", "2", "-in", "tls.csr", "-CA", "ca.crt", "-CAkey", "ca.key", "-CAcreateserial", "-extfile", "san.ext", "-out", "tls.crt"})
|
||||
|
||||
return client.Directory().
|
||||
WithFile("ca.crt", work.File("/work/ca.crt")).
|
||||
WithFile("tls.crt", work.File("/work/tls.crt")).
|
||||
WithFile("tls.key", work.File("/work/tls.key"))
|
||||
}
|
||||
|
||||
// startNode starts the image as a service reachable at fqdn.
|
||||
//
|
||||
// fqdn must not contain dots: Dagger registers it under the session search domain, which
|
||||
// musl only uses for names without dots.
|
||||
//
|
||||
// The node.sh fixture gives the container this hostname and makes it resolve locally, as
|
||||
// Docker or Kubernetes do: the replication bootstrap looks for `hostname -f` in
|
||||
// OPENLDAP_BOOTSTRAP_REPLICATION_HOSTS and slapd matches olcServerID URLs against its
|
||||
// listeners. This needs root capabilities in this derived test container only, then
|
||||
// the image runs as the ldap user.
|
||||
func startNode(ctx context.Context, image *dagger.Container, fqdn string, e env, certs *dagger.Directory, v *volumes) (*dagger.Service, error) {
|
||||
|
||||
c := image
|
||||
if v != nil {
|
||||
c = withVolumes(c, *v)
|
||||
}
|
||||
if certs != nil {
|
||||
c = c.WithUser("0").WithDirectory(certsDir, certs, dagger.ContainerWithDirectoryOpts{Owner: ldapUser})
|
||||
}
|
||||
|
||||
service := withEnv(c, e).
|
||||
WithUser("0").
|
||||
WithNewFile("/tmp/openldap-test-node.sh", nodeScript, dagger.ContainerWithNewFileOpts{Permissions: 0755}).
|
||||
WithExposedPort(3890).
|
||||
WithExposedPort(6360).
|
||||
AsService(dagger.ContainerAsServiceOpts{
|
||||
Args: []string{"/tmp/openldap-test-node.sh", fqdn},
|
||||
InsecureRootCapabilities: true,
|
||||
}).
|
||||
WithHostname(fqdn)
|
||||
|
||||
if _, err := service.Start(ctx); err != nil {
|
||||
return nil, fmt.Errorf("start %s: %w", fqdn, err)
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func stopNodes(ctx context.Context, services []*dagger.Service) {
|
||||
for _, s := range services {
|
||||
_, _ = s.Stop(ctx, dagger.ServiceStopOpts{Kill: true})
|
||||
}
|
||||
}
|
||||
|
||||
// check runs a testdata script in a client container built from the image, with the test
|
||||
// CA trusted.
|
||||
func check(ctx context.Context, image *dagger.Container, certs *dagger.Directory, e env, script string) error {
|
||||
|
||||
c := image
|
||||
if certs != nil {
|
||||
c = c.WithMountedDirectory("/tmp/openldap-test-certs", certs).
|
||||
WithEnvVariable("LDAPTLS_CACERT", "/tmp/openldap-test-certs/ca.crt")
|
||||
}
|
||||
|
||||
c = withEnv(c, merge(env{"TEST_PASSWORD": password, "TEST_SUFFIX": suffix}, e)).
|
||||
WithNewFile("/tmp/openldap-test-lib.sh", libScript).
|
||||
WithNewFile("/tmp/openldap-test.sh", script)
|
||||
|
||||
r, err := expect(ctx, c, true, nil, "bash", "/tmp/openldap-test.sh")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for line := range strings.Lines(r.output) {
|
||||
if strings.HasPrefix(line, "ok: ") {
|
||||
log.Print(strings.TrimSpace(line))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/node.sh
|
||||
var nodeScript string
|
||||
|
||||
//go:embed testdata/lib.sh
|
||||
var libScript string
|
||||
@@ -0,0 +1,69 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"dagger.io/dagger"
|
||||
|
||||
"github.com/osixia/dagger-build/job"
|
||||
)
|
||||
|
||||
// scenario is an integration test run against a built image.
|
||||
type scenario struct {
|
||||
name string
|
||||
run func(ctx context.Context, client *dagger.Client, image *dagger.Container) error
|
||||
}
|
||||
|
||||
var scenarios = []scenario{
|
||||
{"default bootstrap", Default},
|
||||
{"alternative schemas", AlternativeSchemas},
|
||||
{"all features with 3 replicated nodes", AllFeatures},
|
||||
{"backup and restore", BackupRestore},
|
||||
{"upgrade", Upgrade},
|
||||
{"upgrade from previous releases", UpgradeFromPreviousReleases},
|
||||
}
|
||||
|
||||
// Run checks OpenLDAP behavior after the reusable job's version check, only on the engine
|
||||
// platform: emulated images are slow and fail as Dagger services.
|
||||
func Run(ctx context.Context, client *dagger.Client, builds []*job.BuildResult) error {
|
||||
|
||||
enginePlatform, err := client.DefaultPlatform(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("engine platform: %w", err)
|
||||
}
|
||||
|
||||
tested := 0
|
||||
for _, build := range builds {
|
||||
for _, image := range build.Containers {
|
||||
|
||||
platform, err := image.Platform(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("image platform: %w", err)
|
||||
}
|
||||
if platform != enginePlatform {
|
||||
continue
|
||||
}
|
||||
tested++
|
||||
|
||||
for _, s := range scenarios {
|
||||
log.Printf("scenario: %s ...", s.name)
|
||||
sctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
err := s.run(sctx, client, image)
|
||||
cancel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", s.name, err)
|
||||
}
|
||||
log.Printf("scenario: %s passed", s.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tested == 0 && len(builds) > 0 {
|
||||
return fmt.Errorf("no image built for the engine platform %v", enginePlatform)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /tmp/openldap-test-lib.sh
|
||||
|
||||
URI="ldap://${TEST_NODES}:3890"
|
||||
|
||||
eventually has_entry "${URI}" "${TEST_SUFFIX}"
|
||||
ok "admin can read the database"
|
||||
|
||||
expect_rc 32 ldapsearch -x -H "${URI}" -b "${TEST_SUFFIX}" -s base dn
|
||||
ok "anonymous users cannot read the database"
|
||||
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ou=people,${TEST_SUFFIX}
|
||||
objectClass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
dn: uid=alice,ou=people,${TEST_SUFFIX}
|
||||
objectClass: inetOrgPerson
|
||||
uid: alice
|
||||
cn: Alice
|
||||
sn: Liddell
|
||||
mail: alice@example.org
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
LDIF
|
||||
ok "admin can write the database"
|
||||
|
||||
expect_rc 0 bind_search "${URI}" "uid=alice,ou=people,${TEST_SUFFIX}" -b "ou=people,${TEST_SUFFIX}" -s one cn mail
|
||||
expect_output "^mail: alice@example.org$"
|
||||
ok "authenticated users can read allowed attributes"
|
||||
|
||||
expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" -b "cn=config" -s base dn
|
||||
ok "config admin can read cn=config"
|
||||
|
||||
expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" -b "cn=schema,cn=config" -s one cn
|
||||
for schema in ${TEST_SCHEMAS}; do
|
||||
expect_output "^cn: \{[0-9]+\}${schema%.ldif}$"
|
||||
done
|
||||
ok "schemas loaded: ${TEST_SCHEMAS}"
|
||||
Vendored
+224
@@ -0,0 +1,224 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /tmp/openldap-test-lib.sh
|
||||
|
||||
read -ra NODES <<< "${TEST_NODES}"
|
||||
|
||||
PEOPLE_DN="ou=people,${TEST_SUFFIX}"
|
||||
GROUPS_DN="ou=groups,${TEST_SUFFIX}"
|
||||
ALICE_DN="uid=alice,${PEOPLE_DN}"
|
||||
BOB_DN="uid=bob,${PEOPLE_DN}"
|
||||
CAROL_DN="uid=carol,${PEOPLE_DN}"
|
||||
TEAM_DN="cn=team,${GROUPS_DN}"
|
||||
|
||||
ldaps_uri() {
|
||||
echo "ldaps://$1:6360"
|
||||
}
|
||||
|
||||
ldap_uri() {
|
||||
echo "ldap://$1:3890"
|
||||
}
|
||||
|
||||
FIRST="$(ldaps_uri "${NODES[0]}")"
|
||||
LAST="$(ldaps_uri "${NODES[-1]}")"
|
||||
|
||||
# nodes
|
||||
for node in "${NODES[@]}"; do
|
||||
eventually has_entry "$(ldaps_uri "${node}")" "${TEST_SUFFIX}"
|
||||
done
|
||||
ok "all nodes are up: ${NODES[*]}"
|
||||
|
||||
# tls
|
||||
for node in "${NODES[@]}"; do
|
||||
expect_rc 0 bind_search "$(ldap_uri "${node}")" "${ADMIN_DN}" -ZZ -b "${TEST_SUFFIX}" -s base dn
|
||||
expect_rc 13 bind_search "$(ldap_uri "${node}")" "${ADMIN_DN}" -b "${TEST_SUFFIX}" -s base dn
|
||||
done
|
||||
ok "ldaps and StartTLS work, TLS is required"
|
||||
|
||||
expect_rc 32 ldapsearch -x -H "${FIRST}" -b "${TEST_SUFFIX}" -s base dn
|
||||
ok "anonymous users cannot read the database"
|
||||
|
||||
# config
|
||||
for node in "${NODES[@]}"; do
|
||||
uri="$(ldaps_uri "${node}")"
|
||||
|
||||
expect_rc 0 bind_search "${uri}" "${CONFIG_ADMIN_DN}" -b "cn=module{0},cn=config" -s base olcModuleLoad
|
||||
for module in ${TEST_MODULES}; do
|
||||
expect_output "^olcModuleLoad: \{[0-9]+\}${module}$"
|
||||
done
|
||||
|
||||
expect_rc 0 bind_search "${uri}" "${CONFIG_ADMIN_DN}" -b "cn=schema,cn=config" -s one cn
|
||||
for schema in ${TEST_SCHEMAS}; do
|
||||
expect_output "^cn: \{[0-9]+\}${schema%.ldif}$"
|
||||
done
|
||||
|
||||
expect_rc 0 bind_search "${uri}" "${CONFIG_ADMIN_DN}" -b "olcDatabase={1}mdb,cn=config" -s one olcOverlay
|
||||
for overlay in ppolicy unique refint memberof syncprov; do
|
||||
expect_output "^olcOverlay: \{[0-9]+\}${overlay}$"
|
||||
done
|
||||
expect_output "^olcOverlay: \{4\}syncprov$"
|
||||
|
||||
expect_rc 0 bind_search "${uri}" "${CONFIG_ADMIN_DN}" -b "cn=config" -s base olcServerID
|
||||
if [ "$(grep -c "^olcServerID: " "${OUTPUT_FILE}")" != "${#NODES[@]}" ]; then
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "expected ${#NODES[@]} server IDs on ${node}"
|
||||
fi
|
||||
done
|
||||
ok "config: modules, schemas, overlays and server IDs loaded on all nodes"
|
||||
|
||||
# data
|
||||
expect_rc 0 bind_modify "${FIRST}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ${PEOPLE_DN}
|
||||
objectClass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
dn: ${GROUPS_DN}
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: ${ALICE_DN}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
uid: alice
|
||||
cn: Alice
|
||||
sn: Liddell
|
||||
mail: alice@example.org
|
||||
uidNumber: 10001
|
||||
gidNumber: 10000
|
||||
homeDirectory: /home/alice
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
|
||||
dn: ${BOB_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: bob
|
||||
cn: Bob
|
||||
sn: Builder
|
||||
mail: bob@example.org
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
|
||||
dn: ${CAROL_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: carol
|
||||
cn: Carol
|
||||
sn: Danvers
|
||||
mail: carol@example.org
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
|
||||
dn: ${TEAM_DN}
|
||||
objectClass: groupOfNames
|
||||
cn: team
|
||||
member: ${ALICE_DN}
|
||||
member: ${BOB_DN}
|
||||
LDIF
|
||||
ok "entries added on ${NODES[0]}"
|
||||
|
||||
# replication
|
||||
for node in "${NODES[@]:1}"; do
|
||||
eventually has_entry "$(ldaps_uri "${node}")" "${TEAM_DN}"
|
||||
done
|
||||
ok "entries replicated from ${NODES[0]} to ${NODES[*]:1}"
|
||||
|
||||
expect_rc 0 bind_modify "${LAST}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ou=from-last-node,${TEST_SUFFIX}
|
||||
objectClass: organizationalUnit
|
||||
ou: from-last-node
|
||||
LDIF
|
||||
for node in "${NODES[@]::${#NODES[@]}-1}"; do
|
||||
eventually has_entry "$(ldaps_uri "${node}")" "ou=from-last-node,${TEST_SUFFIX}"
|
||||
done
|
||||
ok "entries replicated from ${NODES[-1]} to the other nodes (multi-provider)"
|
||||
|
||||
# password hashing
|
||||
expect_rc 0 bind_search "${FIRST}" "${ADMIN_DN}" -b "${ALICE_DN}" -s base userPassword
|
||||
expect_output "^userPassword:: $(printf '{ARGON2}' | base64 | cut -c1-10)"
|
||||
expect_rc 0 bind_search "${FIRST}" "${ALICE_DN}" -b "${ALICE_DN}" -s base dn
|
||||
ok "clear text passwords are hashed with {ARGON2} and users can bind"
|
||||
|
||||
# memberof
|
||||
is_team_member() {
|
||||
bind_search "$1" "${ADMIN_DN}" -b "$2" -s base memberOf | grep -qi "^memberOf: ${TEAM_DN}$"
|
||||
}
|
||||
|
||||
for node in "${NODES[@]}"; do
|
||||
eventually is_team_member "$(ldaps_uri "${node}")" "${ALICE_DN}"
|
||||
done
|
||||
ok "memberOf is maintained on all nodes"
|
||||
|
||||
# unique
|
||||
expect_rc 19 bind_modify "${FIRST}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: cn=alice-duplicate,${PEOPLE_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: alice
|
||||
cn: alice-duplicate
|
||||
sn: Duplicate
|
||||
LDIF
|
||||
expect_rc 19 bind_modify "${FIRST}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: uid=mail-duplicate,${PEOPLE_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: mail-duplicate
|
||||
cn: mail-duplicate
|
||||
sn: Duplicate
|
||||
mail: alice@example.org
|
||||
LDIF
|
||||
ok "uid and mail are unique"
|
||||
|
||||
# refint
|
||||
expect_rc 0 ldapdelete -x -H "${FIRST}" -D "${ADMIN_DN}" -w "${TEST_PASSWORD}" "${BOB_DN}"
|
||||
bob_removed_from_team() {
|
||||
bind_search "$1" "${ADMIN_DN}" -b "${TEAM_DN}" -s base member > "${OUTPUT_FILE}.member"
|
||||
grep -qi "^member: ${ALICE_DN}$" "${OUTPUT_FILE}.member" && ! grep -qi "^member: ${BOB_DN}$" "${OUTPUT_FILE}.member"
|
||||
}
|
||||
|
||||
for node in "${NODES[@]}"; do
|
||||
eventually bob_removed_from_team "$(ldaps_uri "${node}")"
|
||||
done
|
||||
ok "deleted users are removed from groups on all nodes"
|
||||
|
||||
# ppolicy
|
||||
expect_fail ldappasswd -x -H "${FIRST}" -D "${ALICE_DN}" -w "${TEST_PASSWORD}" -a "${TEST_PASSWORD}" -s "short"
|
||||
expect_output "quality|too short|Constraint violation"
|
||||
ok "password policy rejects short clear text passwords"
|
||||
|
||||
NEW_PASSWORD="Another-passw0rd-2026"
|
||||
expect_rc 0 bind_modify "${FIRST}" "${ALICE_DN}" <<LDIF
|
||||
dn: ${ALICE_DN}
|
||||
changetype: modify
|
||||
delete: userPassword
|
||||
userPassword: ${TEST_PASSWORD}
|
||||
-
|
||||
add: userPassword
|
||||
userPassword: $(slappasswd -h '{SSHA}' -s "${NEW_PASSWORD}")
|
||||
LDIF
|
||||
expect_rc 0 ldapsearch -x -H "${FIRST}" -D "${ALICE_DN}" -w "${NEW_PASSWORD}" -b "${ALICE_DN}" -s base dn
|
||||
ok "password policy accepts pre-hashed passwords (pwdCheckQuality: 1)"
|
||||
|
||||
for i in 1 2 3 4 5; do
|
||||
expect_rc 49 ldapsearch -x -H "${FIRST}" -D "${CAROL_DN}" -w "wrong-password-${i}" -b "${CAROL_DN}" -s base dn
|
||||
done
|
||||
expect_rc 49 bind_search "${FIRST}" "${CAROL_DN}" -b "${CAROL_DN}" -s base dn
|
||||
ok "password policy locks accounts after too many failures"
|
||||
|
||||
# readonly
|
||||
READONLY_DN="cn=readonly,${TEST_SUFFIX}"
|
||||
expect_rc 0 bind_search "${FIRST}" "${READONLY_DN}" -b "${ALICE_DN}" -s base cn userPassword
|
||||
expect_output "^cn: Alice$"
|
||||
expect_no_output "^userPassword"
|
||||
expect_rc 50 bind_modify "${FIRST}" "${READONLY_DN}" <<LDIF
|
||||
dn: ${ALICE_DN}
|
||||
changetype: modify
|
||||
replace: sn
|
||||
sn: Changed
|
||||
LDIF
|
||||
ok "readonly user can read but not write nor read passwords"
|
||||
|
||||
# monitor
|
||||
expect_rc 0 bind_search "${FIRST}" "cn=readonly-monitor,${TEST_SUFFIX}" -b "cn=monitor" -s base dn
|
||||
expect_output "^dn: cn=Monitor$"
|
||||
ok "readonly monitor user can read cn=monitor"
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2034
|
||||
|
||||
# Helpers sourced by the test scripts.
|
||||
|
||||
ADMIN_DN="cn=admin,${TEST_SUFFIX}"
|
||||
CONFIG_ADMIN_DN="cn=admin,cn=config"
|
||||
|
||||
OUTPUT_FILE="$(mktemp)"
|
||||
|
||||
ok() {
|
||||
echo "ok: $*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# expect_rc <rc> <command...>: run command and check its exit code, output is kept in
|
||||
# OUTPUT_FILE
|
||||
expect_rc() {
|
||||
local want="$1"
|
||||
shift
|
||||
|
||||
local rc=0
|
||||
"$@" > "${OUTPUT_FILE}" 2>&1 || rc=$?
|
||||
|
||||
if [ "${rc}" != "${want}" ]; then
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "expected exit code ${want}, got ${rc}: $*"
|
||||
fi
|
||||
}
|
||||
|
||||
# expect_output <pattern>: check the last command output matches an extended regex
|
||||
expect_output() {
|
||||
if ! grep -qE "$1" "${OUTPUT_FILE}"; then
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "output does not match '$1'"
|
||||
fi
|
||||
}
|
||||
|
||||
# expect_no_output <pattern>: check the last command output does not match an extended regex
|
||||
expect_no_output() {
|
||||
if grep -qE "$1" "${OUTPUT_FILE}"; then
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "output matches '$1'"
|
||||
fi
|
||||
}
|
||||
|
||||
# eventually <command...>: retry command for up to 60 seconds
|
||||
eventually() {
|
||||
local i
|
||||
for i in $(seq 1 60); do
|
||||
if "$@" > "${OUTPUT_FILE}" 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "timeout after ${i} attempts: $*"
|
||||
}
|
||||
|
||||
# bind_search <uri> <bind dn> <ldapsearch args...>: simple bind with the test password
|
||||
bind_search() {
|
||||
local uri="$1"
|
||||
local dn="$2"
|
||||
shift 2
|
||||
ldapsearch -LLL -o ldif-wrap=no -x -H "${uri}" -D "${dn}" -w "${TEST_PASSWORD}" "$@"
|
||||
}
|
||||
|
||||
# bind_modify <uri> <bind dn> <ldapmodify args...>: simple bind with the test password, LDIF on stdin
|
||||
bind_modify() {
|
||||
local uri="$1"
|
||||
local dn="$2"
|
||||
shift 2
|
||||
ldapmodify -x -H "${uri}" -D "${dn}" -w "${TEST_PASSWORD}" "$@"
|
||||
}
|
||||
|
||||
# has_entry <uri> <dn>: check an entry exists, as admin
|
||||
has_entry() {
|
||||
bind_search "$1" "${ADMIN_DN}" -b "$2" -s base dn | grep -qi "^dn: $2$"
|
||||
}
|
||||
|
||||
# expect_fail <command...>: run command and check it fails, output is kept in OUTPUT_FILE
|
||||
expect_fail() {
|
||||
if "$@" > "${OUTPUT_FILE}" 2>&1; then
|
||||
cat "${OUTPUT_FILE}" >&2
|
||||
fail "expected failure: $*"
|
||||
fi
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Test fixture, run as root in a derived container with root capabilities:
|
||||
# give the node its FQDN and make it resolve locally (0.0.0.0, so slapd listens on all
|
||||
# interfaces), then run the image entrypoint as the ldap user.
|
||||
|
||||
fqdn="$1"
|
||||
|
||||
hostname "${fqdn}"
|
||||
|
||||
cp /etc/hosts /tmp/openldap-test-hosts
|
||||
echo "0.0.0.0 ${fqdn}" >> /tmp/openldap-test-hosts
|
||||
mount --bind /tmp/openldap-test-hosts /etc/hosts
|
||||
|
||||
exec su -p -s /bin/sh ldap -c 'exec /usr/sbin/container'
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /tmp/openldap-test-lib.sh
|
||||
|
||||
eventually has_entry "ldap://${TEST_NODES}:3890" "uid=alice,ou=people,${TEST_SUFFIX}"
|
||||
ok "upgraded data is served"
|
||||
@@ -0,0 +1,284 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// olderVersion is lower than the image OpenLDAP version at the patch level and equal at the
|
||||
// minor level, to simulate the N-1 image with the N image.
|
||||
const olderVersion = "2.6.0-r0"
|
||||
|
||||
// previousRelease is a published image to upgrade from.
|
||||
type previousRelease struct {
|
||||
image string
|
||||
|
||||
// manualBackup takes the pre-upgrade backup with openldap-ctl: the openldap-pre-upgrade
|
||||
// service of 2.6.10-alpha is not executable.
|
||||
manualBackup bool
|
||||
}
|
||||
|
||||
var previousReleases = []previousRelease{
|
||||
{image: "osixia/openldap:2.6.10-alpha", manualBackup: true},
|
||||
{image: "osixia/openldap:2.6.15-alpha"},
|
||||
}
|
||||
|
||||
const peopleLDIF = `dn: ou=people,` + suffix + `
|
||||
objectClass: organizationalUnit
|
||||
ou: people
|
||||
|
||||
dn: uid=alice,ou=people,` + suffix + `
|
||||
objectClass: inetOrgPerson
|
||||
uid: alice
|
||||
cn: Alice
|
||||
sn: Liddell
|
||||
`
|
||||
|
||||
// bootstrap runs the startup scripts of image on v, with the test root passwords, then adds
|
||||
// peopleLDIF offline.
|
||||
func bootstrap(ctx context.Context, image *dagger.Container, v volumes, passwords env) (volumes, error) {
|
||||
|
||||
r, err := expect(ctx, withEnv(withVolumes(image, v), passwords), true, []string{"Writing OpenLDAP version"}, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
|
||||
r, err = expect(ctx, r.container.WithNewFile("/tmp/openldap-test-people.ldif", peopleLDIF), true, nil,
|
||||
"container", "run", "--", "slapadd", "-n", "1", "-l", "/tmp/openldap-test-people.ldif")
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("add test entries: %w", err)
|
||||
}
|
||||
|
||||
return readVolumes(r.container), nil
|
||||
}
|
||||
|
||||
// expectPeople checks peopleLDIF entries are in the database of v, offline.
|
||||
func expectPeople(ctx context.Context, image *dagger.Container, v volumes) error {
|
||||
_, err := expect(ctx, withVolumes(image, v), true, []string{"dn: uid=alice,ou=people," + suffix},
|
||||
"container", "run", "--", "slapcat", "-n", "1")
|
||||
return err
|
||||
}
|
||||
|
||||
// expectVersion checks the version file of v.
|
||||
func expectVersion(ctx context.Context, image *dagger.Container, v volumes, version string) error {
|
||||
_, err := expect(ctx, withVolumes(image, v), true, []string{version}, "cat", confDir+"/.version")
|
||||
return err
|
||||
}
|
||||
|
||||
// expectServed starts image on v and checks peopleLDIF entries are served.
|
||||
func expectServed(ctx context.Context, image *dagger.Container, v volumes) error {
|
||||
|
||||
const node = "ldap-upgraded"
|
||||
|
||||
service, err := startNode(ctx, image, node, nil, nil, &v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopNodes(ctx, []*dagger.Service{service})
|
||||
|
||||
return check(ctx, image, nil, env{"TEST_NODES": node}, servedCheck)
|
||||
}
|
||||
|
||||
// BackupRestore checks openldap-ctl backup and restore, offline.
|
||||
func BackupRestore(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := bootstrap(ctx, image, emptyVolumes(client), passwords)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r, err := expect(ctx, withVolumes(image, v), true, nil, "container", "run", "--", "openldap-ctl", "backup", "test-backup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v = readVolumes(r.container)
|
||||
|
||||
if _, err := expect(ctx, withVolumes(image, v), true, []string{"test-backup-config.gz", "test-backup-data.gz"}, "ls", backupDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// restore into empty config and data directories
|
||||
restored := emptyVolumes(client)
|
||||
restored.backup = v.backup
|
||||
|
||||
r, err = expect(ctx, withVolumes(image, restored), true, nil, "container", "run", "--", "openldap-ctl", "restore", "test-backup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectPeople(ctx, image, readVolumes(r.container)); err != nil {
|
||||
return fmt.Errorf("restore into empty directories: %w", err)
|
||||
}
|
||||
|
||||
// restore over existing data requires --force
|
||||
if _, err := expect(ctx, withVolumes(image, v), false, nil, "container", "run", "--", "openldap-ctl", "restore", "test-backup"); err != nil {
|
||||
return fmt.Errorf("restore over existing data without --force: %w", err)
|
||||
}
|
||||
|
||||
r, err = expect(ctx, withVolumes(image, v), true, nil, "container", "run", "--", "openldap-ctl", "restore", "test-backup", "--force")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectPeople(ctx, image, readVolumes(r.container)); err != nil {
|
||||
return fmt.Errorf("forced restore: %w", err)
|
||||
}
|
||||
|
||||
// an invalid backup is rejected before existing data is deleted
|
||||
corrupted := v
|
||||
corrupted.backup = v.backup.
|
||||
WithNewFile("corrupted-config.gz", "not a gzip file").
|
||||
WithNewFile("corrupted-data.gz", "not a gzip file")
|
||||
|
||||
r, err = expect(ctx, withVolumes(image, corrupted), false, nil, "container", "run", "--", "openldap-ctl", "restore", "corrupted", "--force")
|
||||
if err != nil {
|
||||
return fmt.Errorf("restore of a corrupted backup: %w", err)
|
||||
}
|
||||
if err := expectPeople(ctx, image, readVolumes(r.container)); err != nil {
|
||||
return fmt.Errorf("data after the restore of a corrupted backup: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Upgrade simulates the N-1 image with olderVersion and checks the pre-upgrade backup on
|
||||
// N-1 and the restore-based migration on N.
|
||||
func Upgrade(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
version, err := image.EnvVariable(ctx, "OPENLDAP_VERSION")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patch := env{"OPENLDAP_UPGRADE_MIGRATION_LEVEL": "patch"}
|
||||
newer := withEnv(image, patch)
|
||||
older := withEnv(image, merge(patch, env{"OPENLDAP_VERSION": olderVersion}))
|
||||
|
||||
v, err := bootstrap(ctx, older, emptyVolumes(client), passwords)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// N without pre-upgrade backup
|
||||
if _, err := expect(ctx, withVolumes(newer, v), false, []string{"Missing"}, "container", "--skip-process"); err != nil {
|
||||
return fmt.Errorf("upgrade without backup: %w", err)
|
||||
}
|
||||
|
||||
// the default minor migration level ignores patch versions
|
||||
if _, err := expect(ctx, withVolumes(image, v), true, []string{"No migration needed"}, "container", "--skip-process"); err != nil {
|
||||
return fmt.Errorf("upgrade at the minor level: %w", err)
|
||||
}
|
||||
|
||||
// N-1 pre-upgrade, as an init container
|
||||
backupPrefix := "openldap-upgrade-" + strings.TrimSuffix(olderVersion, "-r0")
|
||||
r, err := expect(ctx, withVolumes(older, v), true, nil, "container", "-x", "openldap-pre-upgrade", "--skip-process")
|
||||
if err != nil {
|
||||
return fmt.Errorf("pre-upgrade: %w", err)
|
||||
}
|
||||
v = readVolumes(r.container)
|
||||
|
||||
if _, err := expect(ctx, withVolumes(image, v), true, []string{backupPrefix + "-config.gz", backupPrefix + "-data.gz"}, "ls", backupDir); err != nil {
|
||||
return fmt.Errorf("pre-upgrade backup: %w", err)
|
||||
}
|
||||
|
||||
// N
|
||||
r, err = expect(ctx, withVolumes(newer, v), true, []string{"Migration required"}, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return fmt.Errorf("upgrade: %w", err)
|
||||
}
|
||||
v = readVolumes(r.container)
|
||||
|
||||
if err := expectVersion(ctx, image, v, version); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := expectPeople(ctx, image, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// N-1 again
|
||||
if _, err := expect(ctx, withVolumes(older, v), false, []string{"Downgrade forbidden"}, "container", "--skip-process"); err != nil {
|
||||
return fmt.Errorf("downgrade: %w", err)
|
||||
}
|
||||
|
||||
return expectServed(ctx, image, v)
|
||||
}
|
||||
|
||||
// UpgradeFromPreviousReleases upgrades data created by published images.
|
||||
func UpgradeFromPreviousReleases(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
|
||||
version, err := image.EnvVariable(ctx, "OPENLDAP_VERSION")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patch := env{"OPENLDAP_UPGRADE_MIGRATION_LEVEL": "patch"}
|
||||
|
||||
// 2.6.10-alpha expands "$" in hashed root passwords: use a {SSHA} hash, without "$"
|
||||
hash, err := hashPasswordWith(ctx, image, "{SSHA}", env{"OPENLDAP_CTL_PASSWORD_HASH_CMD": "slappasswd -h {SSHA}"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwords := env{
|
||||
"OPENLDAP_BOOTSTRAP_CONFIG_ROOT_PASSWORD_HASHED": hash,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED": hash,
|
||||
}
|
||||
|
||||
for _, p := range previousReleases {
|
||||
|
||||
previous := withEnv(client.Container().From(p.image), patch)
|
||||
|
||||
previousVersion, err := previous.EnvVariable(ctx, "OPENLDAP_VERSION")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", p.image, err)
|
||||
}
|
||||
|
||||
v, err := bootstrap(ctx, previous, emptyVolumes(client), passwords)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", p.image, err)
|
||||
}
|
||||
|
||||
preUpgrade := []string{"container", "-x", "openldap-pre-upgrade", "--skip-process"}
|
||||
if p.manualBackup {
|
||||
preUpgrade = []string{"container", "run", "--", "openldap-ctl", "backup", "openldap-upgrade-" + strings.Split(previousVersion, "-")[0]}
|
||||
}
|
||||
|
||||
r, err := expect(ctx, withVolumes(previous, v), true, nil, preUpgrade...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s pre-upgrade: %w", p.image, err)
|
||||
}
|
||||
v = readVolumes(r.container)
|
||||
|
||||
r, err = expect(ctx, withVolumes(withEnv(image, patch), v), true, nil, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s upgrade: %w", p.image, err)
|
||||
}
|
||||
v = readVolumes(r.container)
|
||||
|
||||
if err := expectVersion(ctx, image, v, version); err != nil {
|
||||
return fmt.Errorf("%s: %w", p.image, err)
|
||||
}
|
||||
if err := expectPeople(ctx, image, v); err != nil {
|
||||
return fmt.Errorf("%s: %w", p.image, err)
|
||||
}
|
||||
if err := expectServed(ctx, image, v); err != nil {
|
||||
return fmt.Errorf("%s: %w", p.image, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/served.sh
|
||||
var servedCheck string
|
||||
@@ -47,7 +47,7 @@ OPENLDAP_BOOTSTRAP_PPOLICY_CHECK_MODULE=""
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_GROUP_DN="ou=policies,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_POLICY_DN="cn=default,${OPENLDAP_BOOTSTRAP_PPOLICY_GROUP_DN}"
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_LENGTH=12
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY=0
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_CHECK_QUALITY=1
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_IN_HISTORY=12
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_ALLOW_USER_CHANGE=true
|
||||
OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_SAFE_MODIFY=true
|
||||
|
||||
@@ -29,8 +29,14 @@ if [ "${TYPE}" = "env" ]; then
|
||||
OPENLDAP_BOOTSTRAP_MODULES_LDIF+="olcModuleload: ${module}"$'\n'
|
||||
done
|
||||
|
||||
# schemas are included by path instead of by content: large schemas (e.g. msuser.ldif)
|
||||
# exceed the kernel size limit of a single environment variable and make every
|
||||
# following command fail with "Argument list too long"
|
||||
for schema in ${OPENLDAP_BOOTSTRAP_SCHEMAS}; do
|
||||
OPENLDAP_BOOTSTRAP_SCHEMAS_LDIF+="$(cat "${OPENLDAP_SCHEMAS_DIR}/${schema}")"$'\n\n'
|
||||
if [ ! -f "${OPENLDAP_SCHEMAS_DIR}/${schema}" ]; then
|
||||
container log fatal "Schema ${schema} not found in ${OPENLDAP_SCHEMAS_DIR}"
|
||||
fi
|
||||
OPENLDAP_BOOTSTRAP_SCHEMAS_LDIF+="include: file://${OPENLDAP_SCHEMAS_DIR}/${schema}"$'\n'
|
||||
done
|
||||
|
||||
OPENLDAP_BOOTSTRAP_MONITOR_ENABLED=${OPENLDAP_BOOTSTRAP_MONITOR_ENABLED^^}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# OpenLDAP Modules
|
||||
|
||||
This directory contains optional modules that can be loaded during bootstrap with `OPENLDAP_BOOTSTRAP_MODULES`.
|
||||
|
||||
## Available modules
|
||||
|
||||
All OpenLDAP backends and overlays from the Alpine packages are in `/usr/lib/openldap`:
|
||||
|
||||
`accesslog.so`, `argon2.so`, `auditlog.so`, `autoca.so`, `back_asyncmeta.so`, `back_dnssrv.so`, `back_ldap.so`, `back_mdb.so`, `back_meta.so`, `back_null.so`, `back_passwd.so`, `back_relay.so`, `back_sock.so`, `back_sql.so`, `collect.so`, `constraint.so`, `dds.so`, `deref.so`, `dyngroup.so`, `dynlist.so`, `homedir.so`, `lastbind.so`, `lloadd.so`, `memberof.so`, `mqtt.so`, `nestgroup.so`, `otp.so`, `pcache.so`, `ppolicy.so`, `pw-pbkdf2.so`, `pw-sha2.so`, `refint.so`, `remoteauth.so`, `retcode.so`, `rwm.so`, `seqmod.so`, `sssvlv.so`, `syncprov.so`, `translucent.so`, `unique.so`, `valsort.so`
|
||||
|
||||
Incompatibilities, all other modules can be loaded together:
|
||||
|
||||
- `dyngroup.so` and `dynlist.so`;
|
||||
- `lastbind.so` and `mqtt.so`.
|
||||
@@ -2,6 +2,19 @@
|
||||
|
||||
This directory contains optional schemas that can be loaded during bootstrap with `OPENLDAP_BOOTSTRAP_SCHEMAS`.
|
||||
|
||||
## Available schemas
|
||||
|
||||
All schemas are copied to `/etc/openldap/schema`:
|
||||
|
||||
- OpenLDAP (Alpine package): `collective.ldif`, `corba.ldif`, `core.ldif`, `cosine.ldif`, `dsee.ldif`, `duaconf.ldif`, `dyngroup.ldif`, `inetorgperson.ldif`, `java.ldif`, `misc.ldif`, `msuser.ldif`, `namedobject.ldif`, `nis.ldif`, `openldap.ldif`, `pmi.ldif`
|
||||
- this directory: `eduperson.ldif`, `kerberos.ldif`, `openssh-lpk.ldif`, `postfix-book.ldif`, `rfc2307bis.ldif`, `samba.ldif`, `sudo.ldif`, `totp.ldif`, `virtualMail.ldif`
|
||||
|
||||
Incompatibilities, all other schemas can be loaded together:
|
||||
|
||||
- `nis.ldif` and `rfc2307bis.ldif` (default);
|
||||
- `postfix-book.ldif` and `virtualMail.ldif`;
|
||||
- `msuser.ldif` is provided by OpenLDAP for reference only and cannot be loaded.
|
||||
|
||||
## eduperson
|
||||
|
||||
Academic identity schema used in higher-education federations (students, staff, affiliations, scoped identifiers).
|
||||
|
||||
Reference in New Issue
Block a user