mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 19:21:50 +00:00
sg: cloud handle deploying a version (#62447)
* wait for build * make not found error more descriptive * bazel * fixup * fix list instances * fix deployment key * fixup * handle deploying a version * fix docker image filtering from registry - make filtering more efficient by filtering while we're iterating - match docker image format * merge fixup * fix formatting
This commit is contained in:
parent
ce854610d5
commit
a659fbbb08
@ -121,7 +121,7 @@ func (c *Client) GetBuildByNumber(ctx context.Context, pipeline string, number s
|
||||
b, _, err := c.bk.Builds.Get(BuildkiteOrg, pipeline, number, nil)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "404 Not Found") {
|
||||
return nil, errors.New("no build found")
|
||||
return nil, errors.Newf("build %s not found on pipeline %q", number, pipeline)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
go_library(
|
||||
name = "cloud",
|
||||
srcs = [
|
||||
"artifact_registry.go",
|
||||
"client.go",
|
||||
"common.go",
|
||||
"delete_command.go",
|
||||
|
||||
159
dev/sg/internal/cloud/artifact_registry.go
Normal file
159
dev/sg/internal/cloud/artifact_registry.go
Normal file
@ -0,0 +1,159 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/regexp"
|
||||
|
||||
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
|
||||
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
const DefaultArtifactRegistryPageSize = 10000
|
||||
|
||||
// DockerImage is a type alias around the Google artifact registry Docker Image type
|
||||
type DockerImage artifactregistrypb.DockerImage
|
||||
|
||||
type DockerImageFilterOpt func(image *DockerImage) bool
|
||||
|
||||
// FilterTagByRegex filters the Docker Image tags by the given regular expression
|
||||
func FilterTagByRegex(regex *regexp.Regexp) DockerImageFilterOpt {
|
||||
return func(image *DockerImage) bool {
|
||||
for _, tag := range image.Tags {
|
||||
if regex.MatchString(tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FilterNameByRegex filters the Docker Image names by the given regular expression
|
||||
func FilterNameByRegex(regex *regexp.Regexp) DockerImageFilterOpt {
|
||||
return func(image *DockerImage) bool {
|
||||
return regex.MatchString(image.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// FilterByName filters the Docker Images by name
|
||||
func FilterByName(name string) DockerImageFilterOpt {
|
||||
// the name format is projects/*/locations/*/repositories/*/dockerImages/<name>@sha256:digest
|
||||
regexp := regexp.MustCompile(fmt.Sprintf("/%s@", name))
|
||||
return FilterNameByRegex(regexp)
|
||||
}
|
||||
|
||||
// FilterByTag filters the Docker Images by tag
|
||||
func FilterByTag(tag string) DockerImageFilterOpt {
|
||||
return func(image *DockerImage) bool {
|
||||
for _, imageTag := range image.Tags {
|
||||
if imageTag == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ArtifactRegistry is wrapper around the Google Artifact Registry client
|
||||
type ArtifactRegistry struct {
|
||||
Project string
|
||||
Location string
|
||||
RepositoryName string
|
||||
PageSize int32
|
||||
|
||||
client *artifactregistry.Client
|
||||
}
|
||||
|
||||
// NewDefaultCloudEphemeralRegistry creates an Artifact Registry with all the details set to point to the cloud
|
||||
// ephemeral registry
|
||||
func NewDefaultCloudEphemeralRegistry(ctx context.Context) (*ArtifactRegistry, error) {
|
||||
return NewArtifactRegistry(ctx, "sourcegraph-ci", "us-central1", "cloud-ephemeral")
|
||||
}
|
||||
|
||||
func NewArtifactRegistry(ctx context.Context, project, location, repositoryName string) (*ArtifactRegistry, error) {
|
||||
client, err := artifactregistry.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ArtifactRegistry{
|
||||
Project: project,
|
||||
Location: location,
|
||||
RepositoryName: repositoryName,
|
||||
PageSize: DefaultArtifactRegistryPageSize,
|
||||
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parent returns the parent string in the format of projects/<project>/locations/<location>/repositories/<repositoryName>
|
||||
func (ar *ArtifactRegistry) Parent() string {
|
||||
return fmt.Sprintf("projects/%s/locations/%s/repositories/%s", ar.Project, ar.Location, ar.RepositoryName)
|
||||
}
|
||||
|
||||
func (ar *ArtifactRegistry) String() string {
|
||||
return ar.Parent()
|
||||
}
|
||||
|
||||
// ListVersions lists all Docker Images present in the Artifact Registry and optionally filter the images as the images are iterated upon.
|
||||
func (ar *ArtifactRegistry) ListDockerImages(ctx context.Context, filterOpts ...DockerImageFilterOpt) ([]*DockerImage, error) {
|
||||
req := &artifactregistrypb.ListDockerImagesRequest{
|
||||
Parent: ar.Parent(),
|
||||
PageSize: ar.PageSize,
|
||||
OrderBy: "upload_time",
|
||||
}
|
||||
|
||||
images := []*DockerImage{}
|
||||
resp := ar.client.ListDockerImages(ctx, req)
|
||||
for {
|
||||
image, err := resp.Next()
|
||||
if err != nil {
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dockerImage := (*DockerImage)(image)
|
||||
var shouldAdd bool
|
||||
for _, filterOpt := range filterOpts {
|
||||
shouldAdd = filterOpt(dockerImage)
|
||||
if !shouldAdd {
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldAdd {
|
||||
images = append(images, dockerImage)
|
||||
}
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// FindDockerImageExact finds Docker Images that match the name and tag exactly in the Artifact Registry.
|
||||
func (ar *ArtifactRegistry) FindDockerImageExact(ctx context.Context, name string, tag string) ([]*DockerImage, error) {
|
||||
return ar.ListDockerImages(ctx, FilterByName(name), FilterByTag(tag))
|
||||
}
|
||||
|
||||
// FindDockerImageByTagPattern finds all Docker Images that have a tag that matches the given tag pattern.
|
||||
func (ar *ArtifactRegistry) FindDockerImageByTagPattern(ctx context.Context, tagPattern string) ([]*DockerImage, error) {
|
||||
tagRegex := regexp.MustCompile(tagPattern)
|
||||
return ar.ListDockerImages(ctx, FilterTagByRegex(tagRegex))
|
||||
}
|
||||
|
||||
// GetDockerImage gets a Docker Image by name and digest from the Artifact Registry.
|
||||
func (ar *ArtifactRegistry) GetDockerImage(ctx context.Context, name, digest string) (*DockerImage, error) {
|
||||
name = fmt.Sprintf("%s/dockerImages/%s@sha256:%s", ar.Parent(), name, digest)
|
||||
req := &artifactregistrypb.GetDockerImageRequest{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
image, err := ar.client.GetDockerImage(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*DockerImage)(image), nil
|
||||
}
|
||||
@ -17,6 +17,8 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/lib/pointers"
|
||||
)
|
||||
|
||||
var ErrInstanceNotFound error = errors.New("instance not found")
|
||||
|
||||
// HeaderUserToken is the header name for the user token when communicating with the Cloud API.
|
||||
const HeaderUserToken = "X-GCP-User-Token"
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/buildkite/go-buildkite/v3/buildkite"
|
||||
@ -18,6 +18,8 @@ import (
|
||||
"github.com/sourcegraph/sourcegraph/lib/pointers"
|
||||
)
|
||||
|
||||
var ErrDeploymentExists error = errors.New("deployment already exists")
|
||||
|
||||
var DeployEphemeralCommand = cli.Command{
|
||||
Name: "deploy",
|
||||
Usage: "sg could deploy --branch <branch> --tag <tag>",
|
||||
@ -32,14 +34,6 @@ var DeployEphemeralCommand = cli.Command{
|
||||
Name: "version",
|
||||
DefaultText: "deploys an ephemeral cloud Sourcegraph environment with the specified version. The version MUST exist and implies that no build will be created",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
DefaultText: "the git tag that should be deployed",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-wip-notice",
|
||||
DefaultText: "skips the EXPERIMENTAL notice prompt",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -65,6 +59,42 @@ func determineVersion(build *buildkite.Build, tag string) (string, error) {
|
||||
), nil
|
||||
}
|
||||
|
||||
func createDeploymentForVersion(ctx context.Context, email, name, version string) error {
|
||||
cloudClient, err := NewClient(ctx, email, APIEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cloudEmoji := "☁️"
|
||||
pending := std.Out.Pending(output.Linef(cloudEmoji, output.StylePending, "Starting deployment %q for version %q", name, version))
|
||||
spec := NewDeploymentSpec(
|
||||
sanitizeInstanceName(name),
|
||||
version,
|
||||
)
|
||||
|
||||
// Check if the deployment already exists
|
||||
_, err = cloudClient.GetInstance(ctx, spec.Name)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrInstanceNotFound) {
|
||||
return errors.Wrapf(err, "failed to determine if instance %q already exists", spec.Name)
|
||||
}
|
||||
} else {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "Cannot deploy %q", err))
|
||||
// Deployment exists
|
||||
return ErrDeploymentExists
|
||||
}
|
||||
|
||||
inst, err := cloudClient.CreateInstance(ctx, spec)
|
||||
if err != nil {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "Deployment failed: %v", err))
|
||||
return errors.Wrapf(err, "failed to deploy version %v", version)
|
||||
}
|
||||
|
||||
pending.Writef("Deploy instance details: \n%s", inst.String())
|
||||
pending.Complete(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Deployment %q created for version %q - access at: %s", name, version, inst.URL))
|
||||
return nil
|
||||
}
|
||||
|
||||
func triggerEphemeralBuild(ctx context.Context, currRepo *repo.GitRepo) (*buildkite.Build, error) {
|
||||
pending := std.Out.Pending(output.Linef("🔨", output.StylePending, "Checking if branch %q is up to date with remote", currRepo.Branch))
|
||||
if isOutOfSync, err := currRepo.IsOutOfSync(ctx); err != nil {
|
||||
@ -76,9 +106,9 @@ func triggerEphemeralBuild(ctx context.Context, currRepo *repo.GitRepo) (*buildk
|
||||
|
||||
client, err := bk.NewClient(ctx, std.Out)
|
||||
if err != nil {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "failed to create client to trigger build"))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pending.Updatef("Starting cloud ephemeral build for %q on commit %q", currRepo.Branch, currRepo.Ref)
|
||||
build, err := client.TriggerBuild(ctx, "sourcegraph", currRepo.Branch, currRepo.Ref, bk.WithEnvVar("CLOUD_EPHEMERAL", "true"))
|
||||
if err != nil {
|
||||
@ -90,34 +120,21 @@ func triggerEphemeralBuild(ctx context.Context, currRepo *repo.GitRepo) (*buildk
|
||||
return build, nil
|
||||
}
|
||||
|
||||
func createDeploymentForVersion(ctx context.Context, name, version string) error {
|
||||
email, err := GetGCloudAccount(ctx)
|
||||
func checkVersionExistsInRegistry(ctx context.Context, version string) error {
|
||||
ar, err := NewDefaultCloudEphemeralRegistry(ctx)
|
||||
if err != nil {
|
||||
std.Out.WriteFailuref("failed to create Cloud Ephemeral registry")
|
||||
return err
|
||||
}
|
||||
if err := validateEmail(email); err != nil {
|
||||
pending := std.Out.Pending(output.Linef(CloudEmoji, output.StylePending, "Checking if version %q exists in Cloud ephemeral registry", version))
|
||||
if images, err := ar.FindDockerImageExact(ctx, "gitserver", version); err != nil {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "failed to check if version %q exists in Cloud ephemeral registry", version))
|
||||
return err
|
||||
} else if len(images) == 0 {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "no version %q found in Cloud ephemeral registry!", version))
|
||||
return errors.Newf("no image with tag %q found", version)
|
||||
}
|
||||
cloudClient, err := NewClient(ctx, email, APIEndpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cloudEmoji := "☁️"
|
||||
pending := std.Out.Pending(output.Linef(cloudEmoji, output.StylePending, "Creating deployment %q for version %q", name, version))
|
||||
|
||||
spec := NewDeploymentSpec(
|
||||
sanitizeInstanceName(name),
|
||||
version,
|
||||
)
|
||||
inst, err := cloudClient.CreateInstance(ctx, spec)
|
||||
if err != nil {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "deployment failed: %v", err))
|
||||
return errors.Wrapf(err, "failed to deploy version %v", version)
|
||||
}
|
||||
|
||||
pending.Writef("Deploy instance details: \n%s", inst.String())
|
||||
pending.Complete(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Deployment %q created for version %q - access at: %s", name, version, inst.URL))
|
||||
pending.Complete(output.Linef(output.EmojiSuccess, output.StyleSuccess, "Version %q found in Cloud ephemeral registry", version))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -145,15 +162,35 @@ func deployCloudEphemeral(ctx *cli.Context) error {
|
||||
std.Out.WriteWarningf(`Your branch %q is out of sync with remote.
|
||||
|
||||
Please make sure you have either pushed or pulled the latest changes before trying again`, currRepo.Branch)
|
||||
} else {
|
||||
std.Out.WriteFailuref("Cannot start deployment as there was problem with the ephemeral build")
|
||||
}
|
||||
return err
|
||||
return errors.Wrapf(err, "cloud ephemeral deployment failure")
|
||||
}
|
||||
|
||||
version, err = determineVersion(build, ctx.String("tag"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err = checkVersionExistsInRegistry(ctx.Context, version); err != nil {
|
||||
return err
|
||||
}
|
||||
email, err := GetGCloudAccount(ctx.Context)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := cmp.Or(ctx.String("name"), currRepo.Branch)
|
||||
return createDeploymentForVersion(ctx.Context, name, version)
|
||||
var deploymentName string
|
||||
if ctx.String("name") != "" {
|
||||
deploymentName = ctx.String("name")
|
||||
} else if ctx.String("version") != "" {
|
||||
// if a version is given we generate a name based on the email user and the given version
|
||||
// to make sure the deployment is unique
|
||||
user := strings.ReplaceAll(email[0:strings.Index(email, "@")], ".", "_")
|
||||
deploymentName = user[:min(12, len(user))] + "_" + version
|
||||
} else {
|
||||
deploymentName = currRepo.Branch
|
||||
}
|
||||
|
||||
return createDeploymentForVersion(ctx.Context, email, deploymentName, version)
|
||||
}
|
||||
|
||||
@ -213,7 +213,7 @@ func (f *InstanceFeatures) IsEphemeralInstance() bool {
|
||||
}
|
||||
|
||||
func (f *InstanceFeatures) SetEphemeralInstance(v bool) {
|
||||
f.features["ephemeral_instance"] = strconv.FormatBool(v)
|
||||
f.features["ephemeral"] = strconv.FormatBool(v)
|
||||
}
|
||||
|
||||
func (f *InstanceFeatures) SetEphemeralLeaseTime(expiresAt time.Time) {
|
||||
|
||||
@ -1,34 +1,30 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
artifactregistry "cloud.google.com/go/artifactregistry/apiv1"
|
||||
artifactregistrypb "cloud.google.com/go/artifactregistry/apiv1/artifactregistrypb"
|
||||
"github.com/grafana/regexp"
|
||||
"github.com/urfave/cli/v2"
|
||||
"google.golang.org/api/iterator"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/dev/sg/internal/std"
|
||||
"github.com/sourcegraph/sourcegraph/lib/output"
|
||||
)
|
||||
|
||||
const DefaultArtifactRegistryPageSize = 10000
|
||||
|
||||
var ListVersionsEphemeralCommand = cli.Command{
|
||||
Name: "list-versions",
|
||||
Usage: "sg could list-versions",
|
||||
Description: "list ephemeral cloud instances attached to your GCP account",
|
||||
Action: listTagsCloudEphemeral,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "format",
|
||||
Usage: "format to print out the list of versions out - can be one json, raw or formatted",
|
||||
Value: "formatted",
|
||||
&cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "print the instance details in JSON",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "print all of the instance details",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "limit",
|
||||
@ -36,87 +32,13 @@ var ListVersionsEphemeralCommand = cli.Command{
|
||||
Value: 100,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "filter",
|
||||
Usage: "filter versions by regex",
|
||||
Name: "filter",
|
||||
Usage: "filter versions by regex",
|
||||
Aliases: []string{"f"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// DockerImage is a type alias around the Google artifact registry Docker Image type
|
||||
type DockerImage artifactregistrypb.DockerImage
|
||||
|
||||
// ArtifactRegistry is wrapper around the Google Artifact Registry client
|
||||
type ArtifactRegistry struct {
|
||||
Project string
|
||||
Location string
|
||||
RepositoryName string
|
||||
PageSize int32
|
||||
|
||||
client *artifactregistry.Client
|
||||
}
|
||||
|
||||
func NewArtifactRegistry(ctx context.Context, project, location, repositoryName string) (*ArtifactRegistry, error) {
|
||||
client, err := artifactregistry.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ArtifactRegistry{
|
||||
Project: project,
|
||||
Location: location,
|
||||
RepositoryName: repositoryName,
|
||||
PageSize: DefaultArtifactRegistryPageSize,
|
||||
|
||||
client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Parent returns the parent string in the format of projects/<project>/locations/<location>/repositories/<repositoryName>
|
||||
func (ar *ArtifactRegistry) Parent() string {
|
||||
return fmt.Sprintf("projects/%s/locations/%s/repositories/%s", ar.Project, ar.Location, ar.RepositoryName)
|
||||
}
|
||||
|
||||
func (ar *ArtifactRegistry) String() string {
|
||||
return ar.Parent()
|
||||
}
|
||||
|
||||
// ListVersions lists all Docker Images present in the Artifact Registry.
|
||||
func (ar *ArtifactRegistry) ListDockerImages(ctx context.Context) ([]*DockerImage, error) {
|
||||
req := &artifactregistrypb.ListDockerImagesRequest{
|
||||
Parent: ar.Parent(),
|
||||
PageSize: ar.PageSize,
|
||||
}
|
||||
|
||||
images := []*DockerImage{}
|
||||
resp := ar.client.ListDockerImages(ctx, req)
|
||||
for {
|
||||
image, err := resp.Next()
|
||||
if err != nil {
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
images = append(images, (*DockerImage)(image))
|
||||
}
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// GetDockerImage gets a Docker Image by name from the Artifact Registry.
|
||||
func (ar *ArtifactRegistry) GetDockerImage(ctx context.Context, name string) (*DockerImage, error) {
|
||||
req := &artifactregistrypb.GetDockerImageRequest{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
image, err := ar.client.GetDockerImage(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return (*DockerImage)(image), nil
|
||||
}
|
||||
|
||||
func listTagsCloudEphemeral(ctx *cli.Context) error {
|
||||
var filterRegex *regexp.Regexp
|
||||
if ctx.String("filter") != "" {
|
||||
@ -127,9 +49,9 @@ func listTagsCloudEphemeral(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
pending := std.Out.Pending(output.Linef(CloudEmoji, output.StylePending, "Retrieving docker images from registry %q", ar.RepositoryName))
|
||||
images, err := ar.ListDockerImages(ctx.Context)
|
||||
images, err := ar.ListDockerImages(ctx.Context, FilterTagByRegex(filterRegex))
|
||||
if err != nil {
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "failed to retreive images from registry %q", ar.RepositoryName))
|
||||
pending.Complete(output.Linef(output.EmojiFailure, output.StyleFailure, "Failed to retreive images from registry %q", ar.RepositoryName))
|
||||
return err
|
||||
}
|
||||
pending.Complete(output.Linef(CloudEmoji, output.StyleSuccess, "Retrieved %d docker images from registry %q", len(images), ar.RepositoryName))
|
||||
@ -143,20 +65,20 @@ func listTagsCloudEphemeral(ctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
switch ctx.String("format") {
|
||||
case "json":
|
||||
switch {
|
||||
case ctx.Bool("json"):
|
||||
{
|
||||
return json.NewEncoder(os.Stdout).Encode(imagesByTag)
|
||||
}
|
||||
case "raw":
|
||||
case ctx.Bool("raw"):
|
||||
{
|
||||
count := 0
|
||||
limit := ctx.Int("limit")
|
||||
for tag, images := range imagesByTag {
|
||||
image := images[0]
|
||||
std.Out.Writef("Tag: %s\n", tag)
|
||||
std.Out.Writef(" %-50s %-20s %s", "Name", "Upload time", "URI")
|
||||
std.Out.Writef("- %-50s %-20s %s", image.Name, image.UploadTime.AsTime().Format(time.DateTime), image.Uri)
|
||||
std.Out.Writef(`Tag : %s
|
||||
Upload Time : %s
|
||||
Image count : %d`, tag, image.UploadTime.AsTime().Format(time.DateTime), len(images))
|
||||
count++
|
||||
if limit >= 1 && count >= limit {
|
||||
break
|
||||
@ -171,13 +93,16 @@ func listTagsCloudEphemeral(ctx *cli.Context) error {
|
||||
for tag, images := range imagesByTag {
|
||||
// we use the first image to get the upload time
|
||||
image := images[0]
|
||||
tag := tag[:min(50, len(tag))]
|
||||
if len(tag) > 50 {
|
||||
tag = tag[:47] + "..."
|
||||
}
|
||||
std.Out.Writef("%-50s %-20s %-5d", tag, image.UploadTime.AsTime().Format(time.DateTime), len(images))
|
||||
count++
|
||||
if limit >= 1 && count >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
std.Out.WriteSuggestionf("Some tags might have been truncated. To see the full tag ouput use the --raw format or filter the tags by using --filter")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
Loading…
Reference in New Issue
Block a user