-
Notifications
You must be signed in to change notification settings - Fork 450
feat(contrib): docker-push plugin #6813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
31673ce
wip
fsamin 2541ee1
feat(contrib): docker-push plugin
fsamin 795f5e5
Merge branch 'feat_workflowv2_dockerpush' of https://github.com/ovh/c…
fsamin 81b6d33
feat(contrib): docker-push plugin
fsamin b53439d
feat(contrib): docker-push plugin
fsamin 84d7404
feat(contrib): docker-push plugin
fsamin 1276bb3
feat(contrib): docker-push plugin
fsamin d52fbb8
Update contrib/grpcplugins/action/docker-push/docker-push.yml
fsamin e8ff46d
fix: cr
fsamin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
PLUGIN_NAME = docker-push | ||
TARGET_NAME = docker-push | ||
|
||
##### ^^^^^^ EDIT ABOVE ^^^^^^ ##### | ||
|
||
include ../../../../.build/core.mk | ||
include ../../../../.build/go.mk | ||
include ../../../../.build/plugin.mk | ||
|
||
build: mk_go_build_plugin ## build action plugin and prepare configuration for publish | ||
|
||
clean: mk_go_clean ## clean binary and tests results | ||
|
||
test: mk_go_test ## run unit tests | ||
|
||
publish: mk_v2_plugin_publish ## publish the plugin on CDS. This use your cdsctl default context and commands cdsctl admin plugins import / binary-add. | ||
|
||
package: mk_plugin_package ## prepare the tar.gz file, with all binaries / conf files |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
name: docker-push | ||
type: action | ||
author: "François SAMIN <francois.samin@corp.ovh.com>" | ||
description: | | ||
This pushes Docker image | ||
inputs: | ||
image: | ||
type: string | ||
description: Image name | ||
required: true | ||
tags: | ||
type: string | ||
description: |- | ||
The tags to associate with the image on the registry. | ||
|
||
This parameter can be empty if you want to keep the same tag. | ||
required: false | ||
registry: | ||
type: string | ||
description: |- | ||
Docker registry to push on. | ||
|
||
This parameter can be empty when an Artifactory integration is set up. | ||
required: false | ||
registryAuth: | ||
type: string | ||
description: |- | ||
Docker base64url-encoded auth configuration. | ||
|
||
See docker authentication section for more details: https://docs.docker.com/engine/api/v1.41/#section/Authentication. | ||
|
||
This parameter can be empty when an Artifactory integration is set up. | ||
required: false | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,315 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/base64" | ||
"encoding/json" | ||
"net/url" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"github.com/docker/cli/cli/streams" | ||
"github.com/docker/docker/api/types" | ||
"github.com/docker/docker/pkg/jsonmessage" | ||
"github.com/docker/go-units" | ||
"github.com/golang/protobuf/ptypes/empty" | ||
"github.com/moby/moby/client" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/ovh/cds/contrib/grpcplugins" | ||
"github.com/ovh/cds/engine/worker/pkg/workerruntime" | ||
"github.com/ovh/cds/sdk" | ||
"github.com/ovh/cds/sdk/grpcplugin/actionplugin" | ||
) | ||
|
||
type dockerPushPlugin struct { | ||
actionplugin.Common | ||
} | ||
|
||
func main() { | ||
actPlugin := dockerPushPlugin{} | ||
if err := actionplugin.Start(context.Background(), &actPlugin); err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
func (actPlugin *dockerPushPlugin) Manifest(_ context.Context, _ *empty.Empty) (*actionplugin.ActionPluginManifest, error) { | ||
return &actionplugin.ActionPluginManifest{ | ||
Name: "docker-push", | ||
Author: "François SAMIN <francois.samin@corp.ovh.com>", | ||
Description: "Push an image docker on a docker registry", | ||
Version: sdk.VERSION, | ||
}, nil | ||
} | ||
|
||
// Run implements actionplugin.ActionPluginServer. | ||
func (actPlugin *dockerPushPlugin) Run(ctx context.Context, q *actionplugin.ActionQuery) (*actionplugin.ActionResult, error) { | ||
res := &actionplugin.ActionResult{ | ||
Status: sdk.StatusSuccess, | ||
} | ||
|
||
image := q.GetOptions()["image"] | ||
tags := q.GetOptions()["tags"] | ||
registry := q.GetOptions()["registry"] | ||
auth := q.GetOptions()["registryAuth"] | ||
|
||
tags = strings.Replace(tags, " ", ",", -1) // If tags are separated by <space> | ||
tags = strings.Replace(tags, ";", ",", -1) // If tags are separated by <semicolon> | ||
tagSlice := strings.Split(tags, ",") | ||
|
||
if err := actPlugin.perform(ctx, image, tagSlice, registry, auth); err != nil { | ||
res.Status = sdk.StatusFail | ||
res.Status = err.Error() | ||
return res, err | ||
} | ||
|
||
return res, nil | ||
} | ||
|
||
type img struct { | ||
repository string | ||
tag string | ||
imageID string | ||
created string | ||
size string | ||
} | ||
|
||
func (actPlugin *dockerPushPlugin) perform(ctx context.Context, image string, tags []string, registry, registryAuth string) error { | ||
if image == "" { | ||
return sdk.Errorf("wrong usage: <image> parameter should not be empty") | ||
} | ||
|
||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) | ||
if err != nil { | ||
return sdk.Errorf("unable to get instanciate docker client: %v", err) | ||
} | ||
|
||
imageSummaries, err := cli.ImageList(ctx, types.ImageListOptions{All: true}) | ||
if err != nil { | ||
return sdk.Errorf("unable to get docker image %q: %v", image, err) | ||
} | ||
|
||
images := []img{} | ||
for _, image := range imageSummaries { | ||
repository := "<none>" | ||
tag := "<none>" | ||
if len(image.RepoTags) > 0 { | ||
splitted := strings.Split(image.RepoTags[0], ":") | ||
repository = splitted[0] | ||
tag = splitted[1] | ||
} else if len(image.RepoDigests) > 0 { | ||
repository = strings.Split(image.RepoDigests[0], "@")[0] | ||
} | ||
duration := HumanDuration(image.Created) | ||
size := HumanSize(image.Size) | ||
images = append(images, img{repository: repository, tag: tag, imageID: image.ID[7:19], created: duration, size: size}) | ||
} | ||
|
||
var imgFound *img | ||
for i := range images { | ||
grpcplugins.Logf("image %s:%s", images[i].repository, images[i].tag) | ||
if images[i].repository+":"+images[i].tag == image { | ||
imgFound = &images[i] | ||
break | ||
} | ||
} | ||
|
||
if imgFound == nil { | ||
return sdk.Errorf("image %q not found", image) | ||
} | ||
|
||
if len(tags) == 0 { // If no tag is provided, keep the actual tag | ||
tags = []string{imgFound.tag} | ||
} | ||
|
||
for _, tag := range tags { | ||
result, d, err := actPlugin.performImage(ctx, cli, image, imgFound, registry, registryAuth, strings.TrimSpace(tag)) | ||
if err != nil { | ||
grpcplugins.Error(err.Error()) | ||
return err | ||
} | ||
grpcplugins.Logf("Image %s pushed in %.3fs", result.Name(), d.Seconds()) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (actPlugin *dockerPushPlugin) performImage(ctx context.Context, cli *client.Client, source string, img *img, registry string, registryAuth string, tag string) (*sdk.V2WorkflowRunResult, time.Duration, error) { | ||
var t0 = time.Now() | ||
|
||
// Create run result at status "pending" | ||
var runResultRequest = workerruntime.V2RunResultRequest{ | ||
RunResult: &sdk.V2WorkflowRunResult{ | ||
IssuedAt: time.Now(), | ||
Type: sdk.V2WorkflowRunResultTypeDocker, | ||
Status: sdk.V2WorkflowRunResultStatusPending, | ||
Detail: sdk.V2WorkflowRunResultDetail{ | ||
Data: sdk.V2WorkflowRunResultDockerDetail{ | ||
Name: source, | ||
ID: img.imageID, | ||
HumanSize: img.size, | ||
HumanCreated: img.created, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
response, err := grpcplugins.CreateRunResult(ctx, &actPlugin.Common, &runResultRequest) | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
|
||
result := response.RunResult | ||
|
||
var destination string | ||
// Upload the file to an artifactory or the docker registry | ||
switch { | ||
case result.ArtifactManagerIntegrationName != nil: | ||
integration, err := grpcplugins.GetIntegrationByName(ctx, &actPlugin.Common, *response.RunResult.ArtifactManagerIntegrationName) | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
|
||
repository := integration.Config[sdk.ArtifactoryConfigRepositoryPrefix].Value + "-docker" | ||
rtURLRaw := integration.Config[sdk.ArtifactoryConfigURL].Value | ||
if !strings.HasSuffix(rtURLRaw, "/") { | ||
rtURLRaw = rtURLRaw + "/" | ||
} | ||
rtURL, err := url.Parse(rtURLRaw) | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
|
||
destination = repository + "." + rtURL.Host + "/" + img.repository + ":" + tag | ||
|
||
result.Detail.Data = sdk.V2WorkflowRunResultDockerDetail{ | ||
Name: destination, | ||
ID: img.imageID, | ||
HumanSize: img.size, | ||
HumanCreated: img.created, | ||
} | ||
|
||
if tag != img.tag { // if the image already has the right tag, nothing to do | ||
if err := cli.ImageTag(ctx, img.imageID, destination); err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to tag %q to %q: %v", source, destination, err) | ||
} | ||
} | ||
|
||
auth := types.AuthConfig{ | ||
Username: integration.Config[sdk.ArtifactoryConfigTokenName].Value, | ||
Password: integration.Config[sdk.ArtifactoryConfigToken].Value, | ||
ServerAddress: repository + "." + rtURL.Host, | ||
} | ||
buf, _ := json.Marshal(auth) | ||
registryAuth = base64.URLEncoding.EncodeToString(buf) | ||
|
||
output, err := cli.ImagePush(ctx, destination, types.ImagePushOptions{RegistryAuth: registryAuth}) | ||
if err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to push %q: %v", destination, err) | ||
} | ||
|
||
if err := jsonmessage.DisplayJSONMessagesToStream(output, streams.NewOut(os.Stdout), nil); err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to push %q: %v", destination, err) | ||
} | ||
|
||
var rtConfig = grpcplugins.ArtifactoryConfig{ | ||
URL: rtURL.String(), | ||
Token: integration.Config[sdk.ArtifactoryConfigToken].Value, | ||
} | ||
|
||
rtFolderPath := img.repository + "/" + tag | ||
rtFolderPathInfo, err := grpcplugins.GetArtifactoryFolderInfo(ctx, &actPlugin.Common, rtConfig, repository, rtFolderPath) | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
|
||
var manifestFound bool | ||
for _, child := range rtFolderPathInfo.Children { | ||
if strings.HasSuffix(child.URI, "manifest.json") { // Can be manifest.json of list.manifest.json for multi-arch docker image | ||
rtPathInfo, err := grpcplugins.GetArtifactoryFileInfo(ctx, &actPlugin.Common, rtConfig, repository, rtFolderPath+child.URI) | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
manifestFound = true | ||
result.ArtifactManagerMetadata = &sdk.V2WorkflowRunResultArtifactManagerMetadata{} | ||
result.ArtifactManagerMetadata.Set("repository", repository) // This is the virtual repository | ||
result.ArtifactManagerMetadata.Set("type", "docker") | ||
result.ArtifactManagerMetadata.Set("maturity", integration.Config[sdk.ArtifactoryConfigPromotionLowMaturity].Value) | ||
result.ArtifactManagerMetadata.Set("name", destination) | ||
result.ArtifactManagerMetadata.Set("path", rtPathInfo.Path) | ||
result.ArtifactManagerMetadata.Set("md5", rtPathInfo.Checksums.Md5) | ||
result.ArtifactManagerMetadata.Set("sha1", rtPathInfo.Checksums.Sha1) | ||
result.ArtifactManagerMetadata.Set("sha256", rtPathInfo.Checksums.Sha256) | ||
result.ArtifactManagerMetadata.Set("uri", rtPathInfo.URI) | ||
result.ArtifactManagerMetadata.Set("mimeType", rtPathInfo.MimeType) | ||
result.ArtifactManagerMetadata.Set("downloadURI", rtPathInfo.DownloadURI) | ||
result.ArtifactManagerMetadata.Set("createdBy", rtPathInfo.CreatedBy) | ||
result.ArtifactManagerMetadata.Set("localRepository", rtPathInfo.Repo) | ||
result.ArtifactManagerMetadata.Set("id", img.imageID) | ||
break | ||
} | ||
} | ||
if !manifestFound { | ||
return nil, time.Since(t0), errors.New("unable to get uploaded image manifest") | ||
} | ||
|
||
default: | ||
// Push on the registry set as parameter | ||
if registry == "" && registryAuth == "" { | ||
return nil, time.Since(t0), errors.New("wrong usage: <registry> and <registryAuth> parameters should not be both empty") | ||
} | ||
|
||
destination = img.repository + ":" + tag | ||
if registry != "" { | ||
destination = registry + "/" + destination | ||
} | ||
|
||
if tag != img.tag { // if the image already has the right tag, nothing to do | ||
if err := cli.ImageTag(ctx, img.imageID, destination); err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to tag %q to %q: %v", source, destination, err) | ||
} | ||
} | ||
|
||
output, err := cli.ImagePush(ctx, destination, types.ImagePushOptions{RegistryAuth: registryAuth}) | ||
if err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to push %q: %v", destination, err) | ||
} | ||
|
||
if err := jsonmessage.DisplayJSONMessagesToStream(output, streams.NewOut(os.Stdout), nil); err != nil { | ||
return nil, time.Since(t0), errors.Errorf("unable to push %q: %v", destination, err) | ||
} | ||
|
||
result.ArtifactManagerMetadata = &sdk.V2WorkflowRunResultArtifactManagerMetadata{} | ||
result.ArtifactManagerMetadata.Set("registry", registry) | ||
result.ArtifactManagerMetadata.Set("name", destination) | ||
result.ArtifactManagerMetadata.Set("id", img.imageID) | ||
} | ||
|
||
details, err := result.GetDetailAsV2WorkflowRunResultDockerDetail() | ||
if err != nil { | ||
return nil, time.Since(t0), err | ||
} | ||
details.Name = destination | ||
result.Detail.Data = details | ||
result.Status = sdk.V2WorkflowRunResultStatusCompleted | ||
|
||
updatedRunresult, err := grpcplugins.UpdateRunResult(ctx, &actPlugin.Common, &workerruntime.V2RunResultRequest{RunResult: result}) | ||
return updatedRunresult.RunResult, time.Since(t0), err | ||
|
||
} | ||
|
||
func HumanDuration(seconds int64) string { | ||
createdAt := time.Unix(seconds, 0) | ||
|
||
if createdAt.IsZero() { | ||
return "" | ||
} | ||
// https://github.com/docker/cli/blob/0e70f1b7b831565336006298b9443b015c3c87a5/cli/command/formatter/buildcache.go#L156 | ||
return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" | ||
} | ||
|
||
func HumanSize(size int64) string { | ||
// https://github.com/docker/cli/blob/0e70f1b7b831565336006298b9443b015c3c87a5/cli/command/formatter/buildcache.go#L148 | ||
return units.HumanSizeWithPrecision(float64(size), 3) | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.