Skip to content

Commit

Permalink
feat: types/functions for images and Postgres versions (#29)
Browse files Browse the repository at this point in the history
Signed-off-by: Leonardo Cecchi <leonardo.cecchi@enterprisedb.com>
Signed-off-by: Francesco Canovai <francesco.canovai@enterprisedb.com>
Signed-off-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com>
Co-authored-by: Gabriele Bartolini <gabriele.bartolini@enterprisedb.com>
  • Loading branch information
leonardoce and gbartolini authored Oct 1, 2024
1 parent 8b1bd3e commit 34c8797
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 0 deletions.
19 changes: 19 additions & 0 deletions pkg/image/reference/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package reference contains an image reference
// parser
package reference
78 changes: 78 additions & 0 deletions pkg/image/reference/imagename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package reference

import (
"fmt"
"regexp"
"strings"
)

var (
digestRegex = regexp.MustCompile(`@sha256:(?P<sha256>[a-fA-F0-9]+)$`)
tagRegex = regexp.MustCompile(`:(?P<tag>[^/]+)$`)
hostRegex = regexp.MustCompile(`^[^./:]+((\.[^./:]+)+(:[0-9]+)?|:[0-9]+)/`)
)

// Data is the main data type
type Data struct {
Name string
Tag string
Digest string
}

// GetNormalizedName returns the normalized name of a reference
func (r *Data) GetNormalizedName() (name string) {
name = r.Name
if r.Tag != "" {
name = fmt.Sprintf("%s:%s", name, r.Tag)
}
if r.Digest != "" {
name = fmt.Sprintf("%s@sha256:%s", name, r.Digest)
}
return name
}

// New parses the image name and returns an error if the name is invalid.
func New(name string) *Data {
reference := &Data{}

if !strings.Contains(name, "/") {
name = "docker.io/library/" + name
} else if !hostRegex.MatchString(name) {
name = "docker.io/" + name
}

if digestRegex.MatchString(name) {
res := digestRegex.FindStringSubmatch(name)
reference.Digest = res[1] // digest capture group index
name = strings.TrimSuffix(name, res[0])
}

if tagRegex.MatchString(name) {
res := tagRegex.FindStringSubmatch(name)
reference.Tag = res[1] // tag capture group index
name = strings.TrimSuffix(name, res[0])
} else if reference.Digest == "" {
reference.Tag = "latest"
}

// everything else is the name
reference.Name = name

return reference
}
49 changes: 49 additions & 0 deletions pkg/image/reference/imagename_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package reference

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("image name management", func() {
It("should normalize image names", func() {
Expect(New("postgres").GetNormalizedName()).To(
Equal("docker.io/library/postgres:latest"))
Expect(New("myimage/postgres").GetNormalizedName()).To(
Equal("docker.io/myimage/postgres:latest"))
Expect(New("localhost:5000/postgres").GetNormalizedName()).To(
Equal("localhost:5000/postgres:latest"))
Expect(New("registry.localhost:5000/postgres:14.4").GetNormalizedName()).To(
Equal("registry.localhost:5000/postgres:14.4"))
Expect(New("ghcr.io/cloudnative-pg/postgresql:34").GetNormalizedName()).To(
Equal("ghcr.io/cloudnative-pg/postgresql:34"))
})

It("should extract tag names", func() {
Expect(New("postgres").Tag).To(Equal("latest"))
Expect(New("postgres:34.3").Tag).To(Equal("34.3"))
Expect(New("postgres:13@sha256:cff94de382ca538861622bbe84cfe03f44f307a9846a5c5eda672cf4dc692866").Tag).
To(Equal("13"))
})

It("should not extract a tag name", func() {
Expect(New("postgres@sha256:cff94dd382ca538861622bbe84cfe03f44f307a9846a5c5eda672cf4dc692866").Tag).
To(BeEmpty())
})
})
33 changes: 33 additions & 0 deletions pkg/image/reference/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package reference

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

func TestPostgresVersion(t *testing.T) {
RegisterFailHandler(Fail)

RunSpecs(t, "PostgreSQL Version")
}
19 changes: 19 additions & 0 deletions pkg/postgres/version/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package version contains a PostgreSQL image version parser with
// the corresponding data type and operations on it
package version
33 changes: 33 additions & 0 deletions pkg/postgres/version/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package version

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

func TestPostgresVersion(t *testing.T) {
RegisterFailHandler(Fail)

RunSpecs(t, "PostgreSQL Version")
}
122 changes: 122 additions & 0 deletions pkg/postgres/version/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright The CloudNativePG Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package version

import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)

var semanticVersionRegex = regexp.MustCompile(`^(\d\.?)+`)

// Data is a structure used to manage a PostgreSQL version,
// consisting of two parts: major and minor. For instance,
// version 10.0 corresponds to a major of 10 and a minor of 0.
// See: https://www.postgresql.org/support/versioning/
type Data struct {
major uint64
minor uint64
}

// Major gets the major version (i.e. 10)
func (d Data) Major() uint64 {
return d.major
}

// Minor gets the minor version (i.e. 0)
func (d Data) Minor() uint64 {
return d.minor
}

// Less is the implementation of the "less than" operator
func (d Data) Less(other Data) bool {
if d.major < other.major {
return true
} else if d.major > other.major {
return false
}

return d.minor < other.minor
}

// New constructs a version from its components
func New(major, minor uint64) Data {
return Data{
major: major,
minor: minor,
}
}

// FromTag parse a PostgreSQL version string returning
// a major version ID. Example:
//
// FromTag("11.2") == (11,2)
// FromTag("12.1") == (12,1)
// FromTag("13.3.2.1-1") == (13,3)
// FromTag("13.4") == (13,4)
// FromTag("14") == (14,0)
// FromTag("15.5-10") == (15,5)
// FromTag("16.0") == (16,0)
// FromTag("17beta1") == (17,0)
// FromTag("17rc1") == (17,0)
func FromTag(version string) (Data, error) {
if !semanticVersionRegex.MatchString(version) {
return Data{},
fmt.Errorf("version not starting with a semantic version regex (%v): %s", semanticVersionRegex, version)
}

if versionOnly := semanticVersionRegex.FindString(version); versionOnly != "" {
version = versionOnly
}

splitVersion := strings.Split(version, ".")

majorVersion, err := strconv.Atoi(splitVersion[0])
if err != nil {
return Data{}, fmt.Errorf("wrong PostgreSQL major in version %v", version)
}

minorVersion := 0
if len(splitVersion) > 1 {
minorVersion, err = strconv.Atoi(splitVersion[1])
if err != nil {
return Data{}, fmt.Errorf("wrong PostgreSQL minor in version %v", version)
}
}

if majorVersion < 0 || majorVersion > math.MaxInt {
return Data{}, fmt.Errorf("wrong PostgreSQL major in version %v", version)
}

if minorVersion < 0 || minorVersion > math.MaxInt {
return Data{}, fmt.Errorf("wrong PostgreSQL minor in version %v", version)
}

return Data{
major: uint64(majorVersion),
minor: uint64(minorVersion),
}, nil
}

// IsUpgradePossible detect if it's possible to upgrade from fromVersion to
// toVersion
func IsUpgradePossible(fromVersion, toVersion Data) bool {
return fromVersion.major == toVersion.major
}
Loading

0 comments on commit 34c8797

Please sign in to comment.