Skip to content

Commit

Permalink
PRODENG-2719 refactor latest tag for msr (#500)
Browse files Browse the repository at this point in the history
- hub latest tag detector refactored to also work with msr registries
  such as registry.mirantis.com

ALSO

- added a unit test that tests dockerhub and registry.mirantis.com tag
  check

Signed-off-by: James Nesbitt <[email protected]>
  • Loading branch information
james-nesbitt authored Aug 26, 2024
1 parent eea2d66 commit ab83ea5
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 24 deletions.
87 changes: 73 additions & 14 deletions pkg/docker/hub/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,55 @@ import (
"github.com/hashicorp/go-version"
)

type tagListResponse struct {
Count int `json:"count"`
Results []struct {
Name string `json:"name"`
} `json:"results"`
}
const (
RegistryDockerHub = "hub.docker.com"
RegistryMirantis = "registry.mirantis.com"
RegistryMsrCi = "msr.ci.mirantis.com"

var errQueryFailed = fmt.Errorf("latest version query failed, you can try running with --disable-upgrade-check")
DockerHubImageURLPattern = "https://hub.docker.com/v2/repositories/%s/%s/tags"
MirantisRegistryImageURLPattern = "https://registry.mirantis.com/api/v0/repositories/%s/%s/tags"
MsrCIImageURLPattern = "https://msr.ci.mirantis.com/api/v0/repositories/%s/%s/tags"
)

var (
errQueryFailed = fmt.Errorf("latest version query failed")
errUnkownRegistry = fmt.Errorf("unknown registry")
)

// LatestTag returns the latest tag name from a public docker hub repository.
// If pre is true, also prereleases are considered.
func LatestTag(org, image string, pre bool) (string, error) {
url := fmt.Sprintf("https://hub.docker.com/v2/repositories/%s/%s/tags", org, image)
func LatestTag(registry, org, image string, pre bool) (string, error) {
var pattern string
var parser func([]byte, bool) (string, error)

switch registry {
case RegistryDockerHub:
pattern = DockerHubImageURLPattern
parser = TagParserDockerHub
case RegistryMirantis:
pattern = MirantisRegistryImageURLPattern
parser = TagParserMSR
case RegistryMsrCi:
pattern = MsrCIImageURLPattern
parser = TagParserMSR
default:
return "", fmt.Errorf("%w %s", errUnkownRegistry, registry)
}

url := fmt.Sprintf(pattern, org, image)
client := http.Client{
Timeout: time.Second * 5,
}

req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("%w: %w", errQueryFailed, err)
return "", fmt.Errorf("%w: request build failed %w", errQueryFailed, err)
}

req.Header.Set("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("%w: %w", errQueryFailed, err)
return "", fmt.Errorf("%w: client failure '%s' %w", errQueryFailed, url, err)
}

if res == nil {
Expand All @@ -48,16 +71,26 @@ func LatestTag(org, image string, pre bool) (string, error) {
defer res.Body.Close()
}
if res.StatusCode > 299 || res.StatusCode < http.StatusOK {
return "", fmt.Errorf("%w: response status %d", errQueryFailed, res.StatusCode)
return "", fmt.Errorf("%w: failed response status %d: %s", errQueryFailed, res.StatusCode, url)
}

body, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("%w: read response body: %w", errQueryFailed, err)
}
var taglist tagListResponse

if err := json.Unmarshal(body, &taglist); err != nil {
return parser(body, pre)
}

func TagParserDockerHub(r []byte, pre bool) (string, error) {
var taglist struct {
Count int `json:"count"`
Results []struct {
Name string `json:"name"`
} `json:"results"`
}

if err := json.Unmarshal(r, &taglist); err != nil {
return "", fmt.Errorf("%w: unmarshal response: %w", errQueryFailed, err)
}

Expand All @@ -77,3 +110,29 @@ func LatestTag(org, image string, pre bool) (string, error) {
sort.Sort(version.Collection(tags))
return tags[len(tags)-1].String(), nil
}

func TagParserMSR(r []byte, pre bool) (string, error) {
var taglist []struct {
Name string `json:"name"`
}

if err := json.Unmarshal(r, &taglist); err != nil {
return "", fmt.Errorf("%w: unmarshal response: %w", errQueryFailed, err)
}

var tags []*version.Version
for _, t := range taglist {
if !pre && strings.Contains(t.Name, "-") {
continue
}

if v, err := version.NewVersion(t.Name); err == nil {
tags = append(tags, v)
}
}
if len(tags) == 0 {
return "", fmt.Errorf("%w: no tags received", errQueryFailed)
}
sort.Sort(version.Collection(tags))
return tags[len(tags)-1].String(), nil
}
4 changes: 2 additions & 2 deletions pkg/product/mke/api/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func validateClusterSpec(vsl validator.StructLevel) {

// Init returns an example of configuration file contents.
func Init(kind string) *ClusterConfig {
mkeV, err := hub.LatestTag("mirantis", "ucp", false)
mkeV, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "ucp", false)
if err != nil {
mkeV = "required"
}
Expand Down Expand Up @@ -118,7 +118,7 @@ func Init(kind string) *ClusterConfig {
},
}
if kind == "mke+msr" {
msrV, err := hub.LatestTag("mirantis", "dtr", false)
msrV, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "dtr", false)
if err != nil {
msrV = "required"
}
Expand Down
38 changes: 30 additions & 8 deletions pkg/product/mke/phase/upgrade_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func (p *UpgradeCheck) ShouldRun() bool {
// Run the installer container.
func (p *UpgradeCheck) Run() (err error) {
if p.Config.Spec.ContainsMKE() {
mkeTag, err := hub.LatestTag("mirantis", "ucp", strings.Contains(p.Config.Spec.MKE.Version, "-"))
mkeTag, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "ucp", strings.Contains(p.Config.Spec.MKE.Version, "-"))
if err != nil {
log.Errorf("failed to check for MKE upgrade: %v", err)
return nil
Expand All @@ -56,30 +56,52 @@ func (p *UpgradeCheck) Run() (err error) {
}

if p.Config.Spec.ContainsMSR2() {
msrv, err := hub.LatestTag("mirantis", "dtr", strings.Contains(p.Config.Spec.MSR2.Version, "-"))
msrv, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "dtr", strings.Contains(p.Config.Spec.MSR2.Version, "-"))
if err != nil {
log.Errorf("failed to check for MSR upgrade: %s", err.Error())
log.Errorf("failed to check for MSR2 upgrade: %s", err.Error())
return nil
}

msrV, err := version.NewVersion(msrv)
if err != nil {
log.Errorf("invalid MSR version response: %s", err.Error())
log.Errorf("invalid MSR2 version response: %s", err.Error())
return nil
}

msrTargetV, err := version.NewVersion(p.Config.Spec.MSR2.Version)
if err != nil {
log.Errorf("invalid MSR version in configuration: %s", err.Error())
return fmt.Errorf("invalid MSR version in configuration: %w", err)
log.Errorf("invalid MSR2 version in configuration: %s", err.Error())
return fmt.Errorf("invalid MSR2 version in configuration: %w", err)
}

if msrV.GreaterThan(msrTargetV) {
log.Warnf("a newer version of MSR is available: %s (installing %s)", msrv, msrTargetV.String())
log.Warnf("a newer version of MSR2 is available: %s (installing %s)", msrv, msrTargetV.String())
}
}

// @TODO MSR3 version upgrade check
if p.Config.Spec.ContainsMSR3() {
msrv, err := hub.LatestTag(hub.RegistryMirantis, "msr", "msr-api", strings.Contains(p.Config.Spec.MSR3.Version, "-"))
if err != nil {
log.Errorf("failed to check for MSR3 upgrade: %s", err.Error())
return nil
}

msrV, err := version.NewVersion(msrv)
if err != nil {
log.Errorf("invalid MSR3 version response: %s", err.Error())
return nil
}

msrTargetV, err := version.NewVersion(p.Config.Spec.MSR2.Version)
if err != nil {
log.Errorf("invalid MSR3 version in configuration: %s", err.Error())
return fmt.Errorf("invalid MSR3 version in configuration: %w", err)
}

if msrV.GreaterThan(msrTargetV) {
log.Warnf("a newer version of MSR3 is available: %s (installing %s)", msrv, msrTargetV.String())
}
}

return nil
}
74 changes: 74 additions & 0 deletions pkg/product/mke/phase/upgrade_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package phase

import (
"testing"

"github.com/Mirantis/mcc/pkg/docker/hub"
"github.com/hashicorp/go-version"
)

func TestMKEVersionRetrieve(t *testing.T) {
msrv, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "ucp", false)
if err != nil {
t.Fatalf("Failed to retrieve MKE3 version: %s", err.Error())
}

if msrv == "" {
t.Fatal("Empty MKE3 version retrieved from registry")
}

msrV, err := version.NewVersion(msrv)
if err != nil {
t.Errorf("invalid MKE3 version response: %s", err.Error())
}

msrTargetV, _ := version.NewVersion("3.0.0")

if !msrV.GreaterThan(msrTargetV) {
t.Errorf("Failed to detect newer version MKE3: %s (%s)", msrv, msrTargetV.String())
}
}

func TestMSR2VersionRetrieve(t *testing.T) {
msrv, err := hub.LatestTag(hub.RegistryDockerHub, "mirantis", "dtr", false)
if err != nil {
t.Fatalf("Failed to retrieve MSR2 version: %s", err.Error())
}

if msrv == "" {
t.Fatal("Empty MSR2 version retrieved from registry")
}

msrV, err := version.NewVersion(msrv)
if err != nil {
t.Errorf("invalid MSR2 version response: %s", err.Error())
}

msrTargetV, _ := version.NewVersion("2.9.0")

if !msrV.GreaterThan(msrTargetV) {
t.Errorf("Failed to detect newer version MSR2: %s (%s)", msrv, msrTargetV.String())
}
}

func TestMSR3VersionRetrieve(t *testing.T) {
msrv, err := hub.LatestTag(hub.RegistryMirantis, "msr", "msr-api", false)
if err != nil {
t.Fatalf("Failed to retrieve MSR3 version: %s", err.Error())
}

if msrv == "" {
t.Fatal("Empty MSR3 version retrieved from registry")
}

msrV, err := version.NewVersion(msrv)
if err != nil {
t.Errorf("invalid MSR3 version response: %s", err.Error())
}

msrTargetV, _ := version.NewVersion("3.0.0")

if !msrV.GreaterThan(msrTargetV) {
t.Errorf("Failed to detect newer version MSR3: %s (%s)", msrv, msrTargetV.String())
}
}

0 comments on commit ab83ea5

Please sign in to comment.