diff --git a/.gitignore b/.gitignore index a1f07ebf..dcc6aa3e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ github-release vendor # sometimes an example launchpad.yaml is used -**/launchpad.yaml \ No newline at end of file +**/launchpad.yaml +.gemini/ +gha-creds-*.json diff --git a/pkg/docker/image.go b/pkg/docker/image.go index 33a73380..d9275c0e 100644 --- a/pkg/docker/image.go +++ b/pkg/docker/image.go @@ -1,6 +1,7 @@ package docker import ( + "errors" "fmt" "regexp" "strings" @@ -146,3 +147,34 @@ func AllToRepository(images []*Image, repo string) (list []*Image) { } return list } + +var errInvalidVersion = errors.New("invalid image version") + +// ImageRepoAndTag returns the Repo and tag from a container image. +// +// e.g. `dtr.efzp.com:9026/mirantis/ucp-agent:3.8.10` => `dtr.efzp.com:9026/mirantis/ucp-agent`, `3.8.10` +func ImageRepoAndTag(image string) (string, string, error) { + lastColon := strings.LastIndexByte(image, ':') + lastSlash := strings.LastIndexByte(image, '/') + + // If there is no colon, tag is implicitly "latest" + if lastColon == -1 { + return image, "latest", nil + } + + // If the last colon is part of the registry host (before the first slash), + // and there are no more colons, then the tag is also implicitly "latest". + // e.g. "localhost:5000/my-image" + if lastColon < lastSlash { + return image, "latest", nil + } + + repo := image[:lastColon] + tag := image[lastColon+1:] + + if tag == "" { + return "", "", fmt.Errorf("%w: empty tag in version output: %s", errInvalidVersion, image) + } + + return repo, tag, nil +} diff --git a/pkg/docker/image_test.go b/pkg/docker/image_test.go index 0cba1944..7ac34a49 100644 --- a/pkg/docker/image_test.go +++ b/pkg/docker/image_test.go @@ -69,3 +69,35 @@ registry.ci.mirantis.com/mirantiseng/ucp-auth-store:3.8.7` require.Equal(t, "registry.ci.mirantis.com/mirantiseng/ucp-alertmanager:3.8.7", images[1].String()) require.Equal(t, "registry.ci.mirantis.com/mirantiseng/ucp-auth-store:3.8.7", images[2].String()) } + +func TestImageVersions(t *testing.T) { + cases := []struct { + input string + expected string + tag string + }{ + {"mirantis/ucp-proxy:3.8.8", "mirantis/ucp-proxy", "3.8.8"}, + {"dtr.efzp.com:9026/mirantis/ucp-agent:3.8.10", "dtr.efzp.com:9026/mirantis/ucp-agent", "3.8.10"}, + {"ucp-proxy:3.8.8", "ucp-proxy", "3.8.8"}, + {"docker.io/library/alpine:latest", "docker.io/library/alpine", "latest"}, + {"localhost:5000/my-image:1.0.0", "localhost:5000/my-image", "1.0.0"}, + {"registry.mirantis.com/mirantiseng/ucp:3.9.0-rc2", "registry.mirantis.com/mirantiseng/ucp", "3.9.0-rc2"}, + {"registry.ci.mirantis.com/mirantiseng/ecp", "registry.ci.mirantis.com/mirantiseng/ecp", "latest"}, + {"localhost:5000/my-image", "localhost:5000/my-image", "latest"}, + {"my-complex-repo:5000/org/image:v1", "my-complex-repo:5000/org/image", "v1"}, + } + + for _, tc := range cases { + repo, tag, err := docker.ImageRepoAndTag(tc.input) + require.Nil(t, err, "ImageRepoAndTag gave unexpected error from valid version string: %s", tc.input) + require.Equal(t, tc.expected, repo, "ImageRepoAndTag gave wrong repo value for: %s", tc.input) + require.Equal(t, tc.tag, tag, "ImageRepoAndTag gave wrong tag value for: %s", tc.input) + } +} + +func TestImageVersionsWithColon(t *testing.T) { + repo, tag, err := docker.ImageRepoAndTag("dtr.efzp.com:9026/mirantis/ucp-agent:3.8.10") + require.Nil(t, err, "SwarmMKEVersion gave unexpected error from valid version string") + require.Equal(t, "dtr.efzp.com:9026/mirantis/ucp-agent", repo, "SwarmMKEVersion gave wrong repo value") + require.Equal(t, "3.8.10", tag, "SwarmMKEVersion gave wrong repo value") +} diff --git a/pkg/mke/mke.go b/pkg/mke/mke.go index ea4a3980..beef6d47 100644 --- a/pkg/mke/mke.go +++ b/pkg/mke/mke.go @@ -20,6 +20,7 @@ import ( "time" "github.com/Mirantis/launchpad/pkg/constant" + "github.com/Mirantis/launchpad/pkg/docker" commonconfig "github.com/Mirantis/launchpad/pkg/product/common/config" mkeconfig "github.com/Mirantis/launchpad/pkg/product/mke/config" "github.com/hashicorp/go-version" @@ -40,8 +41,6 @@ type Credentials struct { Password string `json:"password,omitempty"` // #nosec G117 -- MKE API credentials } -var errInvalidVersion = errors.New("invalid version") - // CollectFacts gathers the current status of installed mke setup. func CollectFacts(swarmLeader *mkeconfig.Host, mkeMeta *mkeconfig.MKEMetadata) error { output, err := swarmLeader.ExecOutput(swarmLeader.Configurer.DockerCommandf(`inspect --format '{{.Config.Image}}' ucp-proxy`)) @@ -51,15 +50,13 @@ func CollectFacts(swarmLeader *mkeconfig.Host, mkeMeta *mkeconfig.MKEMetadata) e return nil } - vparts := strings.Split(output, ":") - if len(vparts) != 2 { - return fmt.Errorf("%w: malformed version output: %s", errInvalidVersion, output) + repo, tag, verr := docker.ImageRepoAndTag(output) + if verr != nil { + return fmt.Errorf("%w: malformed version output: %s", verr, output) } - repo := vparts[0][:strings.LastIndexByte(vparts[0], '/')] - mkeMeta.Installed = true - mkeMeta.InstalledVersion = vparts[1] - mkeMeta.InstalledBootstrapImage = fmt.Sprintf("%s:/ucp:%s", repo, vparts[1]) + mkeMeta.InstalledVersion = tag + mkeMeta.InstalledBootstrapImage = fmt.Sprintf("%s:/ucp:%s", repo, tag) // Find out calico data plane by inspecting the calico container's env variables cmd := swarmLeader.Configurer.DockerCommandf(`ps --filter label=name="Calico node" --format {{.ID}}`)