Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,57 @@ which primarily include:
5. **Docker Buildx:** The `docker buildx` plugin must be available.
6. **Docker Context:** A valid Docker context must be configured.

To verify that all prerequisites are correctly installed and configured:

```bash
task prereqs
```

---

## Usage and Targets
## Scaffolding a New Extension

### 1. Check prerequisites only
To create a new extension project structure, use the `create-extension` task:

To verify that all prerequisites are correctly installed and configured:
```bash
task create-extension NAME=<extension-name>
```

This command generates a new directory named after your extension with the
following scaffolded files:

- `Dockerfile`: The base file to build the extension container image.
- `metadata.hcl`: Provides specific metadata information used to build and test
the extension.
- `README.md`: A template to help you document the extension's usage.

> [!NOTE]
> These files are generated from generic templates and should be customized to
> meet your extension's specific requirements.

### Advanced Scaffolding

For more complex setups, you can use the `dagger` command directly to customize
distributions or package names:

```bash
task prereqs
dagger call -sm ./dagger/maintenance/ create --name="<name>" [ARGUMENTS]
```

### 2. Build configuration check (dry run)
**Common Arguments:**

| Argument | Description | Default |
| --- | --- | --- |
| `--distros` | Debian distributions the extension supports. | `[trixie, bookworm]` |
| `--package-name` | The Debian package name (uses `%version%` placeholder). | `postgresql-%version%-<name>` |
| `--versions` | Supported Postgres major versions. | `[18]` |
| `--templates-dir` | Source directory containing custom template files. | Internal template dir |

---

## Usage and Targets

### 1. Build configuration check (dry run)

To verify the configuration (running `docker buildx bake --check`) for all
projects without building or pulling layers:
Expand All @@ -39,7 +77,7 @@ projects without building or pulling layers:
task checks:all
```

### 3. Build all projects
### 2. Build all projects

To build all discovered projects:

Expand All @@ -49,31 +87,31 @@ task
task bake:all
```

### 4. Build a specific project
### 3. Build a specific project

To build a single project (e.g., the directory named `pgvector`):

```bash
task bake TARGET=pgvector
```

### 5. Push all images
### 4. Push all images

To build all images and immediately push them to the configured registry:

```bash
task bake:all PUSH=true
```

### 6. Push images for a specific project
### 5. Push images for a specific project

To push images for a single project (e.g., the directory named `pgvector`):

```bash
task bake TARGET=pgvector PUSH=true
```

### 7. Dry run mode
### 6. Dry run mode

To see the commands that would be executed without running the actual
`docker buildx bake` command, set the `DRY_RUN` flag:
Expand Down
14 changes: 14 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,20 @@ tasks:
TARGET: "{{.ITEM}}"
task: update-os-libs

create-extension:
desc: Scaffold a new extension directory. Usage - task create-extension NAME=myextension
cmds:
- dagger call -sm ./dagger/maintenance/ create --name {{.NAME}} export --path ./{{.NAME}}
preconditions:
- sh: test -n "{{.NAME}}"
msg: 'NAME variable is required. Usage - task create-extension NAME=myextension'
- sh: test ! -d {{.NAME}}
msg: 'Extension directory already exists at ./{{.NAME}}. Please choose a different name or remove the existing directory.'
requires:
vars:
- name: NAME
desc: The name of the extension to create (lowercase alphanumeric, hyphens, and underscores only)

generate-values:
desc: Generate Chainsaw testing values for the specified target
deps:
Expand Down
122 changes: 122 additions & 0 deletions dagger/maintenance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"maps"
"path"
"regexp"
"slices"
"strings"
"text/template"
"time"

"go.yaml.in/yaml/v3"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"dagger/maintenance/internal/dagger"
)
Expand Down Expand Up @@ -191,6 +197,122 @@ func (m *Maintenance) GenerateTestingValues(
return result.File("values.yaml"), nil
}

// Scaffolds a new Postgres extension directory structure
func (m *Maintenance) Create(
ctx context.Context,
// The source directory containing the extension template files
// +defaultPath="/templates"
templatesDir *dagger.Directory,
// The name of the extension
name string,
// The Postgres major versions the extension is supported for
// +default=["18"]
versions []string,
// The Debian distributions the extension is supported for
// +default=["trixie","bookworm"]
distros []string,
// The Debian package name for the extension. If the package name contains
// the postgres version, it can be templated using the "%version%" placeholder.
// (default "postgresql-%version%-<name>")
// +optional
packageName string,
) (*dagger.Directory, error) {
// Validate name parameter
if name == "" {
return nil, fmt.Errorf("name cannot be empty")
}
// Validate name contains only lowercase alphanumeric characters, hyphens, and underscores
validNamePattern := regexp.MustCompile(`^[a-z0-9_-]+$`)
if !validNamePattern.MatchString(name) {
return nil, fmt.Errorf(
"invalid extension name: %s (must contain only lowercase alphanumeric characters, hyphens, and underscores)",
name,
)
}

// Validate versions array is not empty
if len(versions) == 0 {
return nil, fmt.Errorf("versions array cannot be empty")
}

// Validate distros array is not empty
if len(distros) == 0 {
return nil, fmt.Errorf("distros array cannot be empty")
}

// Validate template files exist
var templateFiles = []string{
"metadata.hcl",
"Dockerfile",
"README.md",
}
for _, fileName := range templateFiles {
tmplFile := templatesDir.File(fileName + ".tmpl")
if _, err := tmplFile.Contents(ctx); err != nil {
return nil, fmt.Errorf("required template file %s.tmpl not found: %w", fileName, err)
}
}

extDir := dag.Directory()

type Extension struct {
Name string
Versions []string
Distros []string
Package string
DefaultVersion int
DefaultDistro string
}

if packageName == "" {
packageName = "postgresql-%version%-" + name
}

extension := Extension{
Name: name,
Versions: versions,
Distros: distros,
Package: packageName,
DefaultVersion: DefaultPgMajor,
DefaultDistro: DefaultDistribution,
}

toTitle := func(s string) string {
return cases.Title(language.English).String(s)
}

funcMap := template.FuncMap{
"replaceAll": strings.ReplaceAll,
"toTitle": toTitle,
}

executeTemplate := func(fileName string) error {
tmplFile := templatesDir.File(fileName + ".tmpl")
tmplContent, err := tmplFile.Contents(ctx)
if err != nil {
return fmt.Errorf("failed to read template file %s.tmpl: %w", fileName, err)
}
tmpl, err := template.New(fileName).Funcs(funcMap).Parse(tmplContent)
if err != nil {
return fmt.Errorf("failed to parse template %s.tmpl: %w", fileName, err)
}
buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, extension); err != nil {
return fmt.Errorf("failed to execute template %s.tmpl: %w", fileName, err)
}
extDir = extDir.WithNewFile(fileName, buf.String())
return nil
}

for _, fileName := range templateFiles {
if err := executeTemplate(fileName); err != nil {
return nil, err
}
}

return extDir, nil
}

// Tests the specified target using Chainsaw
func (m *Maintenance) Test(
ctx context.Context,
Expand Down
50 changes: 50 additions & 0 deletions templates/Dockerfile.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: Copyright © contributors to CloudNativePG, established as CloudNativePG a Series of LF Projects, LLC.
# SPDX-License-Identifier: Apache-2.0

{{ $packageName := replaceAll .Package "%version%" "${PG_MAJOR}" }}
ARG BASE=ghcr.io/cloudnative-pg/postgresql:{{ .DefaultVersion }}-minimal-{{ .DefaultDistro }}
FROM $BASE AS builder

ARG PG_MAJOR
ARG EXT_VERSION

USER 0

# TODO: Remove this comment block after customizing the Dockerfile
# Instructions:
# 1. Remove all TODO comment blocks after completing the customization.
# 2. Uncomment and customize the COPY/RUN commands below.
# 3. If your extension requires additional system libraries, ensure they are
# copied to /lib.

# Install extension via `apt-get`
# RUN apt-get update && apt-get install -y --no-install-recommends \
# "{{ $packageName }}=${EXT_VERSION}"

FROM scratch
ARG PG_MAJOR

# Licenses
# TODO: Uncomment and customize the COPY command below
# Including license files is essential for legal compliance and transparency.
# It ensures proper attribution to original authors, fulfills open source license
# requirements, and enables automated compliance scanning tools to verify licensing.
# COPY --from=builder /usr/share/doc/{{ $packageName }}/copyright /licenses/{{ $packageName }}/

# Libraries
# TODO: Uncomment and customize the COPY command below
# Include the extension's shared objects and related dependencies under the /lib directory.
# CNPG will configure PostgreSQL to locate these libraries at runtime by configuring
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

# the "dynamic_library_path" GUC to include this path.
# For more details, see: https://cloudnative-pg.io/docs/current/imagevolume_extensions#how-it-works
# COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/{{ .Name }}* /lib/

# Share
# TODO: Uncomment and customize the COPY command below
# Include the extension's SQL scripts and control files under the /share/extension directory.
# CNPG will configure PostgreSQL to include this path via the "extension_control_path" GUC to
# seamlessly find and manage the current extension.
# For more details, see: https://cloudnative-pg.io/docs/current/imagevolume_extensions#how-it-works
# COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/extension/{{ .Name }}* /share/extension/

USER 65532:65532
Loading