Skip to content
Draft
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
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ GIT_COMMIT:=$(shell git rev-parse --short HEAD)
IMG ?= perconalab/version-service:$(GIT_BRANCH)-$(GIT_COMMIT)

init:
go build -modfile=tools/go.mod -o bin/yq github.com/mikefarah/yq/v3
go build -modfile=tools/go.mod -o bin/protoc-gen-go google.golang.org/protobuf/cmd/protoc-gen-go
go build -modfile=tools/go.mod -o bin/protoc-gen-go-grpc google.golang.org/grpc/cmd/protoc-gen-go-grpc
go build -modfile=tools/go.mod -o bin/protoc-gen-grpc-gateway github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
go build -modfile=tools/go.mod -o bin/protoc-gen-openapiv2 github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
cd tools && go generate -x -tags=tools

curl -L "https://github.com/bufbuild/buf/releases/download/v1.34.0/buf-$(shell uname -s)-$(shell uname -m)" -o "./bin/buf"
chmod +x ./bin/buf
Expand Down
75 changes: 75 additions & 0 deletions tools/operator-tool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# operator-tool

`operator-tool` is designed to generate a source file for a version service. It retrieves a list of product versions from the [Percona Downloads](https://www.percona.com/downloads) API (`https://www.percona.com/products-api.php`) and searches for the corresponding images in the [Docker Hub repository](https://hub.docker.com/u/percona). If an image is not specified in the API, the latest tag of that image will be used.

Build it using `make init`.

## Usage

### Help

```sh
$ ./bin/operator-tool --help
Usage of ./bin/operator-tool:
-cap int
Sets a limit on the number of versions allowed for each major version of a product
-file string
Specify an older source file. The operator-tool will exclude any versions that are older than those listed in this file
-include-arch-images
Include images with "-multi", "-arm64", "-aarch64" suffixes in the output file
-only-latest
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm using ./bin/operator-tool -operator psmdb --version 1.19.0 --only-latest --include-arch-images --file sources/operator.1.18.0.psmdb-operator.json

This makes backup, operator and pmm have 2 images, but ideally we want just the latest.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

When --file is used with --only-latest, operator-tool appends latest images to the provided file.

This commit makes operator-tool to output only the latest images if the --file is not specified.: fdc56d5

Add only latest major version images to the specified "-file". If "-file" is not specified, returns a file with latest major versions.
-operator string
Operator name. Available values: [psmdb pxc ps pg]
-patch string
Provide a path to a patch file to add additional images. Must be used together with the --file option
-verbose
Show logs
-version string
Operator version
```

### Generating source file from zero

```sh
$ ./bin/operator-tool --operator "psmdb" --version "1.17.0" # outputs source file for psmdb-operator
...
$ ./bin/operator-tool --operator "pg" --version "2.5.0" # outputs source file for pg-operator
...
$ ./bin/operator-tool --operator "ps" --version "0.8.0" # outputs source file for ps-operator
...
$ ./bin/operator-tool --operator "pxc" --version "1.15.1" # outputs source file for pxc-operator
...
```

### Generating source file based on older file

```sh
$ ./bin/operator-tool --file ./sources/operator.2.5.0.pg-operator.json --version "1.17.0" # outputs source file for pg-operator, excluding older versions specified in the file
...
```

### Patching existing source file with a patch file

```sh
$ ./bin/operator-tool --file ./sources/operator.2.5.0.pg-operator.json --patch ./tools/operator-tool/patch-file.json.example
...
```

*Patch file example:*
The example patch file can be found [here](./patch-file.json.example).

```json
{
"operator": {
"2.4.28": {
"image_path": "some-path:tag"
}
},
"pmm": {
"2.50.1": {
"image_path": "some-path:tag"
}
}
}
```
163 changes: 163 additions & 0 deletions tools/operator-tool/cmd/edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package main

import (
"fmt"
"reflect"
"slices"

vsAPI "github.com/Percona-Lab/percona-version-service/versionpb/api"
gover "github.com/hashicorp/go-version"

"operator-tool/internal/matrix"
"operator-tool/internal/util"
)

// deleteOldVersions removes versions from the matrix that are older than those specified in the file.
func deleteOldVersions(file string, m *vsAPI.VersionMatrix) error {
oldestVersions, err := getOldestVersions(file)
if err != nil {
return fmt.Errorf("failed to get oldest versions from base file: %w", err)
}

return matrix.Iterate(m, func(fieldName string, fieldValue reflect.Value) error {
oldestVersion, ok := oldestVersions[fieldName]
if !ok {
return nil
}

m := fieldValue.Interface().(map[string]*vsAPI.Version)
if len(m) == 0 {
return nil
}

for k := range m {
if util.Goversion(k).Compare(oldestVersion) < 0 {
fieldValue.SetMapIndex(reflect.ValueOf(k), reflect.Value{}) // delete old version from map
}
}
return nil
})
}

func keepOnlyLatestVersions(m *vsAPI.VersionMatrix) error {
return matrix.Iterate(m, func(fieldName string, fieldValue reflect.Value) error {
versionMap := fieldValue.Interface().(map[string]*vsAPI.Version)
if len(versionMap) == 0 {
return nil
}

latestByMajorVer := make(map[int]string)
for v := range versionMap {
majorVer := util.Goversion(v).Segments()[0]

curLatest, ok := latestByMajorVer[majorVer]
if !ok || util.Goversion(v).Compare(util.Goversion(curLatest)) > 0 {
latestByMajorVer[majorVer] = v
continue
}
}

for v := range versionMap {
majorVer := util.Goversion(v).Segments()[0]
if latestByMajorVer[majorVer] != v {
fieldValue.SetMapIndex(reflect.ValueOf(v), reflect.Value{}) // delete non-latest version from map
}
}
return nil
})
}

// getOldestVersions returns a map where each key is a struct field name from the VersionMatrix
// of the specified file, and each value is the corresponding oldest version for that field.
func getOldestVersions(filePath string) (map[string]*gover.Version, error) {
prod, err := util.ReadBaseFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read base file: %w", err)
}

versions := make(map[string]*gover.Version)
if err := matrix.Iterate(prod.Versions[0].Matrix, func(fieldName string, fieldValue reflect.Value) error {
versionMap := fieldValue.Interface().(map[string]*vsAPI.Version)
if len(versionMap) == 0 {
return nil
}
oldestVersion := ""
for k := range versionMap {
if oldestVersion == "" {
oldestVersion = k
continue
}
if util.Goversion(oldestVersion).Compare(util.Goversion(k)) > 0 {
oldestVersion = k
}
}
versions[fieldName] = util.Goversion(oldestVersion)
return nil
}); err != nil {
return nil, err
}

return versions, nil
}

func limitMajorVersions(m *vsAPI.VersionMatrix, capacity int) error {
if capacity <= 0 {
return nil
}
return matrix.Iterate(m, func(fieldName string, fieldValue reflect.Value) error {
versionMap := fieldValue.Interface().(map[string]*vsAPI.Version)
versionsByMajorVer := make(map[int][]string)
for v := range versionMap {
majorVer := util.Goversion(v).Segments()[0]
versionsByMajorVer[majorVer] = append(versionsByMajorVer[majorVer], v)
}
for _, versions := range versionsByMajorVer {
if len(versions) <= capacity {
return nil
}
slices.SortFunc(versions, func(a, b string) int {
return util.Goversion(b).Compare(util.Goversion(a))
})

versionsToDelete := versions[capacity:]
for _, v := range versionsToDelete {
fieldValue.SetMapIndex(reflect.ValueOf(v), reflect.Value{})
}
}

return nil
})
}

// updateMatrixStatuses updates the statuses of version maps.
// For each major version, it sets the highest version as "recommended"
// and all other versions as "available".
func updateMatrixStatuses(m *vsAPI.VersionMatrix) error {
setStatus := func(vm map[string]*vsAPI.Version) {
highestVersions := make(map[int]string)
for version := range vm {
vm[version].Status = vsAPI.Status_available

majorVersion := util.Goversion(version).Segments()[0]

currentHighestVersion, ok := highestVersions[majorVersion]

if !ok || util.Goversion(version).Compare(util.Goversion(currentHighestVersion)) > 0 {
highestVersions[majorVersion] = version
}
}

for _, version := range highestVersions {
vm[version].Status = vsAPI.Status_recommended
}
}

return matrix.Iterate(m, func(fieldName string, fieldValue reflect.Value) error {
versionMap := fieldValue.Interface().(map[string]*vsAPI.Version)
if len(versionMap) == 0 {
return nil
}
setStatus(versionMap)
return nil
})
}
Loading