Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement image builder database maintenance (HMS-4244) #1402

11 changes: 11 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,14 @@ jobs:
with:
directory: processed-templates
config: templates/.kube-linter-config.yml
container-test:
name: "🚢 container test"
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Install Prerequisites
run: |
sudo apt update
sudo apt install -y podman
- name: Build and test maintenance container
run: make ubi-maintenance-container-test
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/image-builder
rpmbuild
/image-builder-db-test
/image-builder-maintenance
/image-builder-maintenance-test
/image-builder-migrate-db
/image-builder-migrate-db-tern
dnf-json
Expand Down
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ gen-oscap:
image-builder-migrate-db-tern:
go build -o image-builder-migrate-db-tern ./cmd/image-builder-migrate-db-tern/

.PHONY: image-builder-maintenance
image-builder-maintenance:
go build -o image-builder-maintenance ./cmd/image-builder-maintenance/

.PHONY: build
build: image-builder gen-oscap image-builder-migrate-db-tern
build: image-builder gen-oscap image-builder-migrate-db-tern image-builder-maintenance

.PHONY: run
run:
Expand All @@ -46,7 +50,15 @@ check-api-spec:
.PHONY: ubi-container
ubi-container:
if [ -f .git ]; then echo "You seem to be in a git worktree - build will fail here"; exit 1; fi
podman build --pull=always -t osbuild/image-builder -f distribution/Dockerfile-ubi .
# backwards compatibility with old podman used in github
podman build --pull=always -t osbuild/image-builder -f distribution/Dockerfile-ubi . || \
podman build -t osbuild/image-builder -f distribution/Dockerfile-ubi .

.PHONY: ubi-maintenance-container-test
ubi-maintenance-container-test: ubi-container
# just check if the container would start
# functional tests are in the target "db-tests"
podman run --rm --tty --entrypoint /app/image-builder-maintenance osbuild/image-builder 2>&1 | grep "Dry run, no state will be changed"

.PHONY: generate-openscap-blueprints
generate-openscap-blueprints:
Expand All @@ -73,7 +85,7 @@ generate:
go generate ./...

.PHONY: push-check
push-check: generate build unit-tests
push-check: generate build unit-tests ubi-maintenance-container-test
./tools/prepare-source.sh
@if [ 0 -ne $$(git status --porcelain --untracked-files|wc -l) ]; then \
echo "There should be no changed or untracked files"; \
Expand Down
150 changes: 38 additions & 112 deletions cmd/image-builder-db-test/main_test.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
//go:build integration
//go:build dbtests

package main

import (
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"testing"
"time"

"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/stretchr/testify/require"

"github.com/osbuild/image-builder/internal/common"
"github.com/osbuild/image-builder/internal/config"
"github.com/osbuild/image-builder/internal/db"
v1 "github.com/osbuild/image-builder/internal/v1"

"github.com/osbuild/image-builder/internal/tutils"
)

const (
Expand All @@ -35,80 +32,16 @@ const (
fortnight = time.Hour * 24 * 14
)

func conf(t *testing.T) *config.ImageBuilderConfig {
c := config.ImageBuilderConfig{
ListenAddress: "unused",
LogLevel: "INFO",
TernMigrationsDir: "/usr/share/image-builder/migrations-tern",
PGHost: "localhost",
PGPort: "5432",
PGDatabase: "imagebuilder",
PGUser: "postgres",
PGPassword: "foobar",
PGSSLMode: "disable",
}

err := config.LoadConfigFromEnv(&c)
require.NoError(t, err)

return &c
}

func connStr(t *testing.T) string {
c := conf(t)
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s", c.PGUser, c.PGPassword, c.PGHost, c.PGPort, c.PGDatabase, c.PGSSLMode)
}

func migrateTern(t *testing.T) {
// run tern migration on top of existing db
c := conf(t)

a := []string{
"migrate",
"--migrations", c.TernMigrationsDir,
"--database", c.PGDatabase,
"--host", c.PGHost, "--port", c.PGPort,
"--user", c.PGUser, "--password", c.PGPassword,
"--sslmode", c.PGSSLMode,
}
cmd := exec.Command("tern", a...)

output, err := cmd.CombinedOutput()
require.NoErrorf(t, err, "tern command failed with non-zero code, cmd: tern %s, combined output: %s", strings.Join(a, " "), string(output))
}

func connect(t *testing.T) *pgx.Conn {
ctx := context.Background()
conn, err := pgx.Connect(ctx, connStr(t))
require.NoError(t, err)
return conn
}

func tearDown(t *testing.T) {
ctx := context.Background()
conn := connect(t)
defer conn.Close(ctx)
_, err := conn.Exec(ctx, "drop schema public cascade")
require.NoError(t, err)
_, err = conn.Exec(ctx, "create schema public")
require.NoError(t, err)
_, err = conn.Exec(ctx, "grant all on schema public to postgres")
require.NoError(t, err)
_, err = conn.Exec(ctx, "grant all on schema public to public")
require.NoError(t, err)
}

func testInsertCompose(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testInsertCompose(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)

imageName := "MyImageName"
clientId := "ui"
blueprintId := uuid.New()
versionId := uuid.New()

migrateTern(t)
tutils.MigrateTern(ctx, t)

err = d.InsertBlueprint(ctx, blueprintId, versionId, ORGID1, ANR1, "blueprint", "blueprint desc", []byte("{}"), []byte("{}"))
require.NoError(t, err)
Expand All @@ -122,9 +55,8 @@ func testInsertCompose(t *testing.T) {
require.NoError(t, err)
}

func testGetCompose(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testGetCompose(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)

imageName := "MyImageName"
Expand Down Expand Up @@ -164,14 +96,13 @@ func testGetCompose(t *testing.T) {

}

func testCountComposesSince(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testCountComposesSince(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)

imageName := "MyImageName"

conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)
insert := "INSERT INTO composes(job_id, request, created_at, account_number, org_id, image_name) VALUES ($1, $2, CURRENT_TIMESTAMP - interval '2 days', $3, $4, $5)"
_, err = conn.Exec(ctx, insert, uuid.New().String(), "{}", ANR3, ORGID3, &imageName)
Expand All @@ -198,12 +129,11 @@ func testCountComposesSince(t *testing.T) {
require.Equal(t, 3, count)
}

func testCountGetComposesSince(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testCountGetComposesSince(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)

conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

job1 := uuid.New()
Expand Down Expand Up @@ -232,11 +162,10 @@ func testCountGetComposesSince(t *testing.T) {
require.Equal(t, job1, composes[0].Id)
}

func testGetComposeImageType(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testGetComposeImageType(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)
conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

composeId := uuid.New()
Expand Down Expand Up @@ -269,11 +198,10 @@ func testGetComposeImageType(t *testing.T) {
require.Error(t, err)
}

func testDeleteCompose(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testDeleteCompose(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)
conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

composeId := uuid.New()
Expand All @@ -299,11 +227,10 @@ func testDeleteCompose(t *testing.T) {
require.Equal(t, 1, count)
}

func testClones(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testClones(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)
conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

composeId := uuid.New()
Expand Down Expand Up @@ -373,11 +300,10 @@ func testClones(t *testing.T) {
require.Equal(t, clones[1], *entry)
}

func testBlueprints(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testBlueprints(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)
conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

name1 := "name"
Expand Down Expand Up @@ -526,11 +452,10 @@ func testBlueprints(t *testing.T) {
require.Equal(t, 0, count)
}

func testGetBlueprintComposes(t *testing.T) {
ctx := context.Background()
d, err := db.InitDBConnectionPool(connStr(t))
func testGetBlueprintComposes(ctx context.Context, t *testing.T) {
d, err := db.InitDBConnectionPool(ctx, tutils.ConnStr(t))
require.NoError(t, err)
conn := connect(t)
conn := tutils.Connect(t)
defer conn.Close(ctx)

id := uuid.New()
Expand Down Expand Up @@ -596,14 +521,9 @@ func testGetBlueprintComposes(t *testing.T) {
require.Equal(t, 2, version)
}

func runTest(t *testing.T, f func(*testing.T)) {
migrateTern(t)
defer tearDown(t)
f(t)
}

func TestAll(t *testing.T) {
fns := []func(*testing.T){
ctx := context.Background()
fns := []func(context.Context, *testing.T){
testInsertCompose,
testGetCompose,
testCountComposesSince,
Expand All @@ -615,6 +535,12 @@ func TestAll(t *testing.T) {
}

for _, f := range fns {
runTest(t, f)
select {
case <-ctx.Done():
require.NoError(t, ctx.Err())
return
default:
tutils.RunTest(ctx, t, f)
}
}
}
61 changes: 61 additions & 0 deletions cmd/image-builder-maintenance/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"fmt"
"os"
"reflect"
"strconv"
)

// Do not write this config to logs or stdout, it contains secrets!
type Config struct {
DryRun bool `env:"DRY_RUN"`
EnableDBMaintenance bool `env:"ENABLE_DB_MAINTENANCE"`
ClonesRetentionMonths int `env:"DB_CLONES_RETENTION_MONTHS"`
PGHost string `env:"PGHOST"`
PGPort string `env:"PGPORT"`
PGDatabase string `env:"PGDATABASE"`
PGUser string `env:"PGUSER"`
PGPassword string `env:"PGPASSWORD"`
PGSSLMode string `env:"PGSSLMODE"`
}

// *string means the value is not required
// string means the value is required and should have a default value
func LoadConfigFromEnv(intf interface{}) error {
croissanne marked this conversation as resolved.
Show resolved Hide resolved
t := reflect.TypeOf(intf).Elem()
v := reflect.ValueOf(intf).Elem()

for i := 0; i < v.NumField(); i++ {
fieldT := t.Field(i)
fieldV := v.Field(i)
key, ok := fieldT.Tag.Lookup("env")
if !ok {
return fmt.Errorf("No env tag in config field")
}

confV, ok := os.LookupEnv(key)
kind := fieldV.Kind()
if ok {
switch kind {
case reflect.String:
fieldV.SetString(confV)
case reflect.Int:
value, err := strconv.ParseInt(confV, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(value)
case reflect.Bool:
value, err := strconv.ParseBool(confV)
if err != nil {
return err
}
fieldV.SetBool(value)
default:
return fmt.Errorf("Unsupported type")
}
}
}
return nil
}
schuellerf marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading