Skip to content

Commit

Permalink
Added support for searching images that are available.
Browse files Browse the repository at this point in the history
  • Loading branch information
brett19 committed Dec 2, 2023
1 parent d9438aa commit fd51d93
Show file tree
Hide file tree
Showing 15 changed files with 455 additions and 21 deletions.
54 changes: 54 additions & 0 deletions cmd/images-list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"go.uber.org/zap"
)

type ImagesListOutput []ImagesListOutput_Item

type ImagesListOutput_Item struct {
Source string `json:"source"`
Name string `json:"name"`
}

var imagesListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "Lists all the images available locally",
Run: func(cmd *cobra.Command, args []string) {
helper := CmdHelper{}
logger := helper.GetLogger()
ctx := helper.GetContext()

outputJson, _ := cmd.Flags().GetBool("json")

deployer := helper.GetDeployer(ctx)
images, err := deployer.ListImages(ctx)
if err != nil {
logger.Fatal("failed to list images", zap.Error(err))
}

if !outputJson {
fmt.Printf("Images:\n")
for _, image := range images {
fmt.Printf(" %s [Source: %s]\n", image.Name, image.Source)
}
} else {
var out ImagesListOutput
for _, image := range images {
out = append(out, ImagesListOutput_Item{
Source: image.Source,
Name: image.Name,
})
}
helper.OutputJson(out)
}
},
}

func init() {
imagesCmd.AddCommand(imagesListCmd)
}
87 changes: 87 additions & 0 deletions cmd/images-search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package cmd

import (
"fmt"
"slices"
"strings"

"github.com/couchbaselabs/cbdinocluster/deployment"
"github.com/spf13/cobra"
"go.uber.org/zap"
)

type ImagesSearchOutput []ImagesSearchOutput_Item

type ImagesSearchOutput_Item struct {
Source string `json:"source"`
Name string `json:"name"`
}

var imagesSearchCmd = &cobra.Command{
Use: "search",
Aliases: []string{"find"},
Short: "Searches all the images available to use",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
helper := CmdHelper{}
logger := helper.GetLogger()
ctx := helper.GetContext()

outputJson, _ := cmd.Flags().GetBool("json")
limit, _ := cmd.Flags().GetInt("limit")

deployer := helper.GetDeployer(ctx)
images, err := deployer.SearchImages(ctx, args[0])
if err != nil {
logger.Fatal("failed to search images", zap.Error(err))
}

if !outputJson {
// we want dockerhub images before ghcr images
getSourceKey := func(source string) string {
if source == "ghcr" {
return "4"
} else if source == "dockerhub" {
return "6"
}
return "5"
}

// We sort backwards to put new versions at the top
slices.SortFunc(images, func(a deployment.Image, b deployment.Image) int {
return -strings.Compare(
getSourceKey(a.Source)+a.Name,
getSourceKey(b.Source)+b.Name)
})

fmt.Printf("Images:\n")
for imageNum, image := range images {
if limit > 0 && imageNum > limit {
fmt.Printf(" ...\n")
break
}

fmt.Printf(" %s [Source: %s]\n", image.Name, image.Source)
}
} else {
var out ImagesSearchOutput
for imageNum, image := range images {
if limit > 0 && imageNum > limit {
break
}

out = append(out, ImagesSearchOutput_Item{
Source: image.Source,
Name: image.Name,
})
}
helper.OutputJson(out)
}
},
}

func init() {
imagesCmd.AddCommand(imagesSearchCmd)

imagesSearchCmd.Flags().Int("limit", 10, "The maximum number of results to return")
}
15 changes: 15 additions & 0 deletions cmd/images.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package cmd

import (
"github.com/spf13/cobra"
)

var imagesCmd = &cobra.Command{
Use: "images",
Short: "Provides the ability to list/search server version images.",
Run: nil,
}

func init() {
rootCmd.AddCommand(imagesCmd)
}
8 changes: 8 additions & 0 deletions deployment/clouddeploy/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1123,3 +1123,11 @@ func (d *Deployer) AllowNodeTraffic(ctx context.Context, clusterID string, nodeI
func (d *Deployer) CollectLogs(ctx context.Context, clusterID string, destPath string) ([]string, error) {
return nil, errors.New("clouddeploy does not support log collection")
}

func (d *Deployer) ListImages(ctx context.Context) ([]deployment.Image, error) {
return nil, errors.New("clouddeploy does not support image listing")
}

func (d *Deployer) SearchImages(ctx context.Context, version string) ([]deployment.Image, error) {
return nil, errors.New("clouddeploy does not support image search")
}
7 changes: 7 additions & 0 deletions deployment/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ type CollectionInfo struct {
Name string
}

type Image struct {
Source string
Name string
}

type Deployer interface {
ListClusters(ctx context.Context) ([]ClusterInfo, error)
NewCluster(ctx context.Context, def *clusterdef.Cluster) (ClusterInfo, error)
Expand All @@ -83,4 +88,6 @@ type Deployer interface {
BlockNodeTraffic(ctx context.Context, clusterID string, nodeID string) error
AllowNodeTraffic(ctx context.Context, clusterID string, nodeID string) error
CollectLogs(ctx context.Context, clusterID string, destPath string) ([]string, error)
ListImages(ctx context.Context) ([]Image, error)
SearchImages(ctx context.Context, version string) ([]Image, error)
}
8 changes: 8 additions & 0 deletions deployment/dockerdeploy/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1194,3 +1194,11 @@ func (d *Deployer) CollectLogs(ctx context.Context, clusterID string, destPath s

return destPaths, nil
}

func (d *Deployer) ListImages(ctx context.Context) ([]deployment.Image, error) {
return d.imageProvider.ListImages(ctx)
}

func (d *Deployer) SearchImages(ctx context.Context, version string) ([]deployment.Image, error) {
return d.imageProvider.SearchImages(ctx, version)
}
80 changes: 80 additions & 0 deletions deployment/dockerdeploy/dockerhubimageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package dockerdeploy
import (
"context"
"fmt"
"strings"

"github.com/couchbaselabs/cbdinocluster/deployment"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"go.uber.org/zap"
Expand Down Expand Up @@ -41,3 +45,79 @@ func (p *DockerHubImageProvider) GetImage(ctx context.Context, def *ImageDef) (*
ImagePath: dhImagePath,
}.Pull(ctx)
}

func (p *DockerHubImageProvider) ListImages(ctx context.Context) ([]deployment.Image, error) {
dkrImages, err := p.DockerCli.ImageList(ctx, types.ImageListOptions{
Filters: filters.NewArgs(filters.Arg("reference", "couchbase")),
})
if err != nil {
return nil, errors.Wrap(err, "failed to list images")
}

var images []deployment.Image
for _, image := range dkrImages {
for _, repoTag := range image.RepoTags {
tagParts := strings.Split(repoTag, ":")
if len(tagParts) != 2 {
return nil, fmt.Errorf("encountered unexpected image name: %s", repoTag)
}

versionName := tagParts[1]

images = append(images, deployment.Image{
Source: "dockerhub",
Name: versionName,
})
}
}

return images, nil
}

func (p *DockerHubImageProvider) SearchImages(ctx context.Context, version string) ([]deployment.Image, error) {
nextPath := "https://hub.docker.com/v2/namespaces/library/repositories/couchbase/tags?page_size=100"

var images []deployment.Image
for nextPath != "" {
p.Logger.Debug("Fetching one registry listings page", zap.String("path", nextPath))

var respData struct {
Next string `json:"next"`
Results []struct {
Name string `json:"name"`
} `json:"results"`
}
err := doRegistryGet(ctx, nextPath, "", &respData)
if err != nil {
return nil, errors.Wrap(err, "failed to search images")
}

for _, tag := range respData.Results {
parsedParts := strings.Split(tag.Name, "-")
if len(parsedParts) != 2 {
// we ignore tags without the enterprise/community prefix
continue
}

if parsedParts[0] != "enterprise" {
// we only consider enterprise builds
continue
}

versionName := parsedParts[1]
if !strings.Contains(versionName, version) {
// ignore versions that don't match the search
continue
}

images = append(images, deployment.Image{
Source: "dockerhub",
Name: versionName,
})
}

nextPath = respData.Next
}

return images, nil
}
87 changes: 87 additions & 0 deletions deployment/dockerdeploy/ghcrimageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"strings"

"github.com/couchbaselabs/cbdinocluster/deployment"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"go.uber.org/zap"
Expand Down Expand Up @@ -60,3 +63,87 @@ func (p *GhcrImageProvider) GetImage(ctx context.Context, def *ImageDef) (*Image
ImagePath: ghcrImagePath,
}.Pull(ctx)
}

func (p *GhcrImageProvider) ListImages(ctx context.Context) ([]deployment.Image, error) {
dkrImages, err := p.DockerCli.ImageList(ctx, types.ImageListOptions{
Filters: filters.NewArgs(filters.Arg("reference", "ghcr.io/cb-vanilla/server")),
})
if err != nil {
return nil, errors.Wrap(err, "failed to list images")
}

var images []deployment.Image
for _, image := range dkrImages {
for _, repoTag := range image.RepoTags {
tagParts := strings.Split(repoTag, ":")
if len(tagParts) != 2 {
return nil, fmt.Errorf("encountered unexpected image name: %s", repoTag)
}

var versionName string

versionParts := strings.Split(tagParts[1], "-")
if len(versionParts) == 2 {
// 7.2.2-1852
versionName = "enterprise-" + tagParts[1]
} else if len(versionParts) == 3 {
// community-7.2.2-1852
if versionParts[0] != "community" {
return nil, fmt.Errorf("encountered unexpected image name: %s", repoTag)
}

versionName = tagParts[1]
} else {
return nil, fmt.Errorf("encountered unexpected image name: %s", repoTag)
}

images = append(images, deployment.Image{
Source: "ghcr",
Name: versionName,
})
}
}

return images, nil
}

func (p *GhcrImageProvider) SearchImages(ctx context.Context, version string) ([]deployment.Image, error) {
var respData struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
err := doRegistryGet(ctx,
"https://ghcr.io/v2/cb-vanilla/server/tags/list?n=10000",
"Bearer "+base64.StdEncoding.EncodeToString([]byte(p.GhcrPassword)),
&respData)
if err != nil {
return nil, errors.Wrap(err, "failed to search images")
}

var images []deployment.Image
for _, tagName := range respData.Tags {
parsedParts := strings.Split(tagName, "-")
if len(parsedParts) == 1 {
// we ignore generic tags with no build number
continue
}

if strings.Contains(tagName, "community") {
// ignore community versions
continue
}

versionName := tagName
if !strings.Contains(versionName, version) {
// ignore versions that don't match the search
continue
}

images = append(images, deployment.Image{
Source: "ghcr",
Name: versionName,
})
}

return images, nil
}
Loading

0 comments on commit fd51d93

Please sign in to comment.