diff --git a/BUILD.md b/BUILD.md index d268231..6b7e226 100644 --- a/BUILD.md +++ b/BUILD.md @@ -192,3 +192,47 @@ To clean up all the resources created by the `e2e:setup-env` task, run: ```bash task e2e:cleanup ``` + +--- + +## Create a new extension + +To create a new extension project, you can use the `create-extension` task: + +```bash +task create-extension NAME= +``` + +The command will create a new directory named `` using default configurations. +For a more advanced and customized setup, you can use the dagger command directly: + +```bash +dagger call -sm ./dagger/maintenance/ create --help + +Scaffolds a new Postgres extension directory structure + +USAGE + dagger call create [arguments] + +FUNCTIONS: + +[...] + +ARGUMENTS + --name string The name of the extension [required] + --distros strings The Debian distributions the extension is supported for (default [trixie,bookworm]) + --package-name 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%-") + --templates-dir Directory The source directory containing the extension template files + --versions strings The Postgres major versions the extension is supported for (default [18]) +``` + +This command creates a new folder with the following files: + +- `Dockerfile`: the Dockerfile to build the extension container image. +- `metadata.hcl`: the file providing specific metadata information used to build and test the extension. +- `README.md`: a template README file to get started with documenting the extension usage. + +Those files are scaffolded with a generic template and are meant to be customized according to the +specific extension requirements. diff --git a/Taskfile.yml b/Taskfile.yml index ff4b01c..efb8bec 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -128,6 +128,17 @@ tasks: TARGET: "{{.ITEM}}" task: update-os-libs + create-extension: + desc: Scaffold a new extension directory + cmds: + - dagger call -sm ./dagger/maintenance/ create --name {{.NAME}} export --path ./{{.NAME}} + preconditions: + - sh: test ! -d {{ .NAME }} + msg: 'The provided extension already exists. Name: {{.NAME}}' + requires: + vars: + - name: NAME + generate-values: desc: Generate Chainsaw testing values for the specified target deps: diff --git a/dagger/maintenance/main.go b/dagger/maintenance/main.go index 6d3521d..ddb9133 100644 --- a/dagger/maintenance/main.go +++ b/dagger/maintenance/main.go @@ -4,15 +4,20 @@ package main import ( + "bytes" "context" "encoding/json" "fmt" "maps" "path" "slices" + "strings" + "text/template" "time" "go.yaml.in/yaml/v3" + "golang.org/x/text/cases" + "golang.org/x/text/language" "dagger/maintenance/internal/dagger" ) @@ -191,6 +196,91 @@ 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%-") + // +optional + packageName string, +) (*dagger.Directory, error) { + 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 err + } + tmpl, err := template.New(fileName).Funcs(funcMap).Parse(tmplContent) + if err != nil { + return err + } + buf := &bytes.Buffer{} + if err := tmpl.Execute(buf, extension); err != nil { + return err + } + extDir = extDir.WithNewFile(fileName, buf.String()) + return nil + } + + var templateFiles = []string{ + "metadata.hcl", + "Dockerfile", + "README.md", + } + 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, diff --git a/templates/Dockerfile.tmpl b/templates/Dockerfile.tmpl new file mode 100644 index 0000000..fb36854 --- /dev/null +++ b/templates/Dockerfile.tmpl @@ -0,0 +1,42 @@ +{{ $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 + +# Install extension + +# RUN apt-get update && \ +# apt-get install -y --no-install-recommends "{{ $packageName }}=${EXT_VERSION}" + +FROM scratch +ARG PG_MAJOR + +# Licenses +# 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 +# Include the extension's shared objects and related dependencies under the /lib directory. +# CNPG will load these libraries at runtime by configuring the dynamic linker path (LD_LIBRARY_PATH) and +# the PostgreSQL GUC "dynamic_library_path" based on defaults and Cluster configuration. +# For more details, see: https://cloudnative-pg.io/docs/1.28/imagevolume_extensions#how-it-works + +# COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/{{ .Name }}* /lib/ +# COPY --from=builder /usr/lib/postgresql/${PG_MAJOR}/lib/bitcode/ /lib/bitcode/ + +# Share +# 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/1.28/imagevolume_extensions#how-it-works + +# COPY --from=builder /usr/share/postgresql/${PG_MAJOR}/extension/{{ .Name }}* /share/extension/ + +USER 65532:65532 diff --git a/templates/README.md.tmpl b/templates/README.md.tmpl new file mode 100644 index 0000000..610fde1 --- /dev/null +++ b/templates/README.md.tmpl @@ -0,0 +1,72 @@ +# {{ toTitle .Name }} + + + +The {{ .Name }} PostgreSQL extension provides support for +vector data types and operations, enabling efficient storage and retrieval of +high-dimensional vectors. It is particularly useful for applications involving +machine learning, recommendation systems, and similarity search. + +## Usage + + + +### 1. Add the {{ .Name }} extension image to your Cluster + +Define the `{{ .Name }}` extension under the `postgresql.extensions` section of +your `Cluster` resource. For example: + +```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: cluster-{{ .Name }} +spec: + imageName: ghcr.io/cloudnative-pg/postgresql:{{ .DefaultVersion }}-minimal-{{ .DefaultDistro }} + instances: 1 + + storage: + size: 1Gi + + postgresql: + extensions: + - name: {{ .Name }} + image: + reference: ghcr.io/cloudnative-pg/{{ .Name }}:1.0-{{ .DefaultVersion }}-{{ .DefaultDistro }} +``` + +### 2. Enable the extension in a database + +You can install `{{ .Name }}` in a specific database by creating or updating a +`Database` resource. For example, to enable it in the `app` database: + +```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Database +metadata: + name: cluster-{{ .Name }}-app +spec: + name: app + owner: app + cluster: + name: cluster-{{ .Name }} + extensions: + - name: {{ .Name }} + version: '1.0' +``` + +### 3. Verify installation + +Once the database is ready, connect to it with `psql` and run: + +```sql +\dx +``` + +You should see `{{ .Name }}` listed among the installed extensions. diff --git a/templates/metadata.hcl.tmpl b/templates/metadata.hcl.tmpl new file mode 100644 index 0000000..b14f981 --- /dev/null +++ b/templates/metadata.hcl.tmpl @@ -0,0 +1,21 @@ +metadata = { + name = "{{ .Name }}" + sql_name = "{{ .Name }}" + image_name = "{{ .Name }}" + shared_preload_libraries = [] + extension_control_path = [] + dynamic_library_path = [] + ld_library_path = [] + auto_update_os_libs = false + + versions = { + {{- range $distro := .Distros}} + {{ $distro }} = { + {{- range $version := $.Versions}} + // renovate: suite={{ $distro }}-pgdg depName={{ replaceAll $.Package "%version%" $version }} + "{{ $version }}" = "" + {{- end}} + } + {{- end}} + } +}