Skip to content

Commit b90234b

Browse files
committed
feat: generate extension catalogs
Signed-off-by: Niccolò Fei <niccolo.fei@enterprisedb.com>
1 parent 18ccd88 commit b90234b

5 files changed

Lines changed: 289 additions & 3 deletions

File tree

.github/workflows/bake.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,16 @@ jobs:
8989
extension_name: ${{ matrix.extension }}
9090
secrets:
9191
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
92+
93+
Catalogs:
94+
name: Update Catalogs
95+
needs: Bake
96+
runs-on: ubuntu-24.04
97+
permissions:
98+
contents: write
99+
if: github.ref == 'refs/heads/main'
100+
steps:
101+
- name: Repository Dispatch
102+
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4
103+
with:
104+
event-type: update-catalogs
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Update Catalogs
2+
3+
on:
4+
workflow_dispatch:
5+
repository_dispatch:
6+
types: [update-catalogs]
7+
8+
permissions: read-all
9+
10+
defaults:
11+
run:
12+
shell: "bash -Eeuo pipefail -x {0}"
13+
14+
jobs:
15+
update-catalogs:
16+
runs-on: ubuntu-24.04
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
20+
with:
21+
persist-credentials: false
22+
23+
- name: Checkout artifacts
24+
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
25+
with:
26+
path: artifacts
27+
repository: cloudnative-pg/artifacts
28+
token: ${{ secrets.REPO_GHA_PAT }}
29+
ref: main
30+
31+
- name: Update catalogs
32+
id: update-extension-catalogs
33+
uses: dagger/dagger-for-github@d913e70051faf3b907d4dd96ef1161083c88c644 # v8.2.0
34+
env:
35+
# renovate: datasource=github-tags depName=dagger/dagger versioning=semver
36+
DAGGER_VERSION: 0.19.10
37+
with:
38+
version: ${{ env.DAGGER_VERSION }}
39+
verb: call
40+
module: ./dagger/maintenance/
41+
args: generate-catalogs --catalogs-dir artifacts/image-catalogs/ export --path artifacts/image-catalogs/
42+
43+
- name: Diff
44+
working-directory: artifacts
45+
run: |
46+
git add -A .
47+
git status
48+
git diff --staged
49+
50+
- uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
51+
if: github.ref == 'refs/heads/main'
52+
with:
53+
cwd: 'artifacts'
54+
add: 'image-catalogs'
55+
author_name: CloudNativePG Automated Updates
56+
author_email: noreply@cnpg.com
57+
message: 'chore: update extensions imageCatalogs'

dagger/maintenance/catalogs.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"path/filepath"
8+
"slices"
9+
10+
"go.yaml.in/yaml/v3"
11+
12+
"dagger/maintenance/internal/dagger"
13+
)
14+
15+
const (
16+
LabelImageOS = "images.cnpg.io/os"
17+
LabelImageType = "images.cnpg.io/type"
18+
)
19+
20+
type ImageVolumeSource struct {
21+
Reference string `yaml:"reference"`
22+
PullPolicy string `yaml:"pullPolicy,omitempty"`
23+
}
24+
25+
type ExtensionConfiguration struct {
26+
Name string `yaml:"name"`
27+
ImageVolumeSource ImageVolumeSource `yaml:"image"`
28+
ExtensionControlPath []string `yaml:"extensionControlPath,omitempty"`
29+
DynamicLibraryPath []string `yaml:"dynamicLibraryPath,omitempty"`
30+
LdLibraryPath []string `yaml:"ldLibraryPath,omitempty"`
31+
}
32+
33+
type ImageCatalog struct {
34+
APIVersion string `yaml:"apiVersion"`
35+
Kind string `yaml:"kind"`
36+
Metadata struct {
37+
Name string `yaml:"name"`
38+
Labels map[string]string `yaml:"labels"`
39+
} `yaml:"metadata"`
40+
Spec struct {
41+
Images []struct {
42+
Major int `yaml:"major"`
43+
Image string `yaml:"image"`
44+
Extensions []ExtensionConfiguration `yaml:"extensions,omitempty"`
45+
} `yaml:"images"`
46+
} `yaml:"spec"`
47+
}
48+
49+
func getMinimalCatalogs(ctx context.Context, catalogsDir *dagger.Directory) ([]*ImageCatalog, error) {
50+
entries, err := catalogsDir.Entries(ctx)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
var catalogs []*ImageCatalog
56+
57+
for _, entry := range entries {
58+
if ext := filepath.Ext(entry); ext != ".yaml" && ext != ".yml" {
59+
continue
60+
}
61+
62+
content, err := catalogsDir.File(entry).Contents(ctx)
63+
if err != nil {
64+
return nil, fmt.Errorf("while retrieving %s: %w", entry, err)
65+
}
66+
67+
var catalog ImageCatalog
68+
if err := yaml.Unmarshal([]byte(content), &catalog); err != nil {
69+
return nil, fmt.Errorf("while decoding %s: %w", entry, err)
70+
}
71+
72+
// Only keep ClusterImageCatalogs
73+
if catalog.Kind != "ClusterImageCatalog" {
74+
continue
75+
}
76+
77+
// Only keep catalogs with minimal images
78+
if catalog.Metadata.Labels[LabelImageType] != "minimal" {
79+
continue
80+
}
81+
82+
// Only keep catalogs for Supported Distros
83+
catalogOS, ok := catalog.Metadata.Labels[LabelImageOS]
84+
if !ok {
85+
return nil, fmt.Errorf("while retrieving OS for %q catalog", entry)
86+
}
87+
if !slices.Contains(SupportedDistributions, catalogOS) {
88+
continue
89+
}
90+
91+
catalogs = append(catalogs, &catalog)
92+
}
93+
94+
return catalogs, nil
95+
}
96+
97+
func writeCatalogToDir(catalog *ImageCatalog, outDir *dagger.Directory) (*dagger.Directory, error) {
98+
var buf bytes.Buffer
99+
enc := yaml.NewEncoder(&buf)
100+
enc.SetIndent(2)
101+
102+
if err := enc.Encode(catalog); err != nil {
103+
return nil, fmt.Errorf("while encoding catalog %s: %w", catalog.Metadata.Name, err)
104+
}
105+
if err := enc.Close(); err != nil {
106+
return nil, err
107+
}
108+
109+
outName := fmt.Sprintf("catalog-extensions-%s.yaml", catalog.Metadata.Labels[LabelImageOS])
110+
111+
return outDir.WithNewFile(outName, buf.String()), nil
112+
}

dagger/maintenance/image.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ const (
1717
DefaultDistribution = "trixie"
1818
)
1919

20+
var SupportedDistributions = []string{
21+
"bookworm",
22+
"trixie",
23+
}
24+
2025
// getImageAnnotations returns the OCI annotations given an image ref.
2126
func getImageAnnotations(imageRef string) (map[string]string, error) {
2227
// Setting Insecure option to allow fetching images from local registries with no TLS
@@ -51,10 +56,16 @@ func getImageAnnotations(imageRef string) (map[string]string, error) {
5156
// getDefaultExtensionImage returns the default extension image for a given extension,
5257
// resolved from the metadata.
5358
func getDefaultExtensionImage(metadata *extensionMetadata) (string, error) {
54-
packageVersion := metadata.Versions[DefaultDistribution][strconv.Itoa(DefaultPgMajor)]
59+
return getExtensionImage(metadata, DefaultDistribution, DefaultPgMajor)
60+
}
61+
62+
// getExtensionImage returns the extension image for a given distribution and pgMajor,
63+
// resolved from the metadata.
64+
func getExtensionImage(metadata *extensionMetadata, distribution string, pgMajor int) (string, error) {
65+
packageVersion := metadata.Versions[distribution][strconv.Itoa(pgMajor)]
5566
if packageVersion == "" {
5667
return "", fmt.Errorf("no package version found for distribution %q and version %d",
57-
DefaultDistribution, DefaultPgMajor)
68+
distribution, pgMajor)
5869
}
5970

6071
re := regexp.MustCompile(`^(\d+(?:\.\d+)+)`)
@@ -64,7 +75,7 @@ func getDefaultExtensionImage(metadata *extensionMetadata) (string, error) {
6475
}
6576
version := matches[1]
6677
image := fmt.Sprintf("ghcr.io/cloudnative-pg/%s:%s-%d-%s",
67-
metadata.ImageName, version, DefaultPgMajor, DefaultDistribution)
78+
metadata.ImageName, version, pgMajor, distribution)
6879

6980
return image, nil
7081
}

dagger/maintenance/main.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"path"
1313
"regexp"
1414
"slices"
15+
"sort"
16+
"strconv"
1517
"strings"
1618
"text/template"
1719
"time"
@@ -410,3 +412,94 @@ func (m *Maintenance) Test(
410412

411413
return nil
412414
}
415+
416+
// Generate extension's ClusterImageCatalogs starting from a base set of catalogs
417+
func (m *Maintenance) GenerateCatalogs(
418+
ctx context.Context,
419+
// The source directory containing the extension folders. Defaults to the current directory
420+
// +ignore=["dagger", ".github"]
421+
// +defaultPath="/"
422+
source *dagger.Directory,
423+
// The directory containing the starting catalogs. Defaults to "/image-catalogs"
424+
// +defaultPath="/image-catalogs"
425+
catalogsDir *dagger.Directory,
426+
) (*dagger.Directory, error) {
427+
outDir := dag.Directory()
428+
429+
catalogs, err := getMinimalCatalogs(ctx, catalogsDir)
430+
if err != nil {
431+
return nil, fmt.Errorf("while retrieving base catalogs: %w", err)
432+
}
433+
434+
targetExtensions, err := getExtensions(ctx, source)
435+
if err != nil {
436+
return nil, fmt.Errorf("while retrieving extensions: %w", err)
437+
}
438+
if len(targetExtensions) == 0 {
439+
return nil, fmt.Errorf("no extensions found in source directory")
440+
}
441+
442+
catalogWritten := false
443+
for _, catalog := range catalogs {
444+
catalogOS, ok := catalog.Metadata.Labels[LabelImageOS]
445+
if !ok {
446+
return nil, fmt.Errorf("while retrieving OS for %q catalog", catalog.Metadata.Name)
447+
}
448+
449+
for dir, extension := range targetExtensions {
450+
matrix, err := parseBuildMatrix(ctx, source, dir)
451+
if err != nil {
452+
return nil, fmt.Errorf("while parsing build Matrix for extension %s: %w", extension, err)
453+
}
454+
if !slices.Contains(matrix.Distributions, catalogOS) {
455+
continue
456+
}
457+
458+
for i := range catalog.Spec.Images {
459+
img := &catalog.Spec.Images[i]
460+
if !slices.Contains(matrix.MajorVersions, strconv.Itoa(img.Major)) {
461+
continue
462+
}
463+
464+
metadata, err := parseExtensionMetadata(ctx, source.Directory(dir))
465+
if err != nil {
466+
return nil, fmt.Errorf("while parsing extension %s metadata: %w", extension, err)
467+
}
468+
469+
targetExtensionImage, err := getExtensionImage(metadata, catalogOS, img.Major)
470+
if err != nil {
471+
return nil, fmt.Errorf("while retrieving extension %s image: %w", extension, err)
472+
}
473+
474+
extensionsConfig := ExtensionConfiguration{
475+
Name: metadata.Name,
476+
ImageVolumeSource: ImageVolumeSource{
477+
Reference: targetExtensionImage,
478+
},
479+
ExtensionControlPath: metadata.ExtensionControlPath,
480+
DynamicLibraryPath: metadata.DynamicLibraryPath,
481+
LdLibraryPath: metadata.LdLibraryPath,
482+
}
483+
484+
img.Extensions = append(img.Extensions, extensionsConfig)
485+
486+
// Sort extensions by name
487+
sort.Slice(img.Extensions, func(i, j int) bool {
488+
return img.Extensions[i].Name < img.Extensions[j].Name
489+
})
490+
}
491+
}
492+
493+
outDir, err = writeCatalogToDir(catalog, outDir)
494+
if err != nil {
495+
return nil, fmt.Errorf("while writing catalog %s: %w", catalog.Metadata.Name, err)
496+
}
497+
catalogWritten = true
498+
}
499+
500+
if !catalogWritten {
501+
return nil, fmt.Errorf("no catalogs matched the selection criteria")
502+
}
503+
504+
return outDir, nil
505+
}

0 commit comments

Comments
 (0)