Compare commits

...

2 Commits

Author SHA1 Message Date
Ebtasam Faridy 5e7aec2e21 Update pipeline drone-docker-harness 2026-01-28 13:55:48 +05:30
OP (oppenheimer) aabeaaf7bb feat: [CI-20527]: add push-only mode to skip build and push pre-existing images (#500)
* Add push-only support

* Include support for PLUGIN_NO_PUSH as well
2026-01-26 22:55:18 +05:30
4 changed files with 220 additions and 9 deletions
+4 -4
View File
@@ -33,7 +33,7 @@ pipeline:
identifier: Run_1
spec:
connectorRef: Plugins_Docker_Hub_Connector
image: golang:1.23.0
image: golang:1.24.11
shell: Sh
command: go vet ./...
- step:
@@ -42,7 +42,7 @@ pipeline:
identifier: Run_2
spec:
connectorRef: Plugins_Docker_Hub_Connector
image: golang:1.23.0
image: golang:1.24.11
shell: Sh
command: go test -cover ./...
- parallel:
@@ -70,7 +70,7 @@ pipeline:
identifier: Build_Push
spec:
connectorRef: Plugins_Docker_Hub_Connector
image: golang:1.23.0
image: golang:1.24.11
shell: Sh
command: go build -a -tags netgo -o release/linux/amd64/drone-<+matrix.repo> ./cmd/drone-<+matrix.repo>
envVariables:
@@ -157,7 +157,7 @@ pipeline:
identifier: buildpush
spec:
connectorRef: Plugins_Docker_Hub_Connector
image: golang:1.23.0
image: golang:1.24.11
shell: Sh
command: go build -a -tags netgo -o release/linux/arm64/drone-<+matrix.repo> ./cmd/drone-<+matrix.repo>
envVariables:
+12 -2
View File
@@ -17,8 +17,14 @@ import (
"github.com/inhies/go-bytesize"
)
// writeCard maintains backward compatibility by using TempTag
func (p Plugin) writeCard() error {
cmd := exec.Command(dockerExe, "inspect", p.Build.TempTag)
return p.writeCardForImage(p.Build.TempTag)
}
// writeCardForImage generates card for any image reference
func (p Plugin) writeCardForImage(imageRef string) error {
cmd := exec.Command(dockerExe, "inspect", imageRef)
data, err := cmd.CombinedOutput()
if err != nil {
return err
@@ -38,7 +44,11 @@ func (p Plugin) writeCard() error {
for _, tag := range inspect.RepoTags {
sliceTagStruct = append(sliceTagStruct, TagStruct{Tag: tag})
}
inspect.ParsedRepoTags = sliceTagStruct[1:] // remove the first tag which is always "hash:latest"
if len(sliceTagStruct) > 1 {
inspect.ParsedRepoTags = sliceTagStruct[1:] // remove the first tag which is always "hash:latest"
} else {
inspect.ParsedRepoTags = sliceTagStruct
}
// create the url from repo and registry
inspect.URL = mapRegistryToURL(p.Daemon.Registry, p.Build.Repo)
cardData, _ := json.Marshal(inspect)
+13 -1
View File
@@ -33,7 +33,7 @@ func main() {
cli.BoolFlag{
Name: "dry-run",
Usage: "dry run disables docker push",
EnvVar: "PLUGIN_DRY_RUN",
EnvVar: "PLUGIN_DRY_RUN, PLUGIN_NO_PUSH",
},
cli.StringFlag{
Name: "remote.url",
@@ -339,6 +339,16 @@ func main() {
Usage: "additional cosign parameters (e.g., annotations, flags)",
EnvVar: "PLUGIN_COSIGN_PARAMS",
},
cli.BoolFlag{
Name: "push-only",
Usage: "skip build and only push images",
EnvVar: "PLUGIN_PUSH_ONLY",
},
cli.StringFlag{
Name: "source-image",
Usage: "source image to tag and push (format: repo:tag)",
EnvVar: "PLUGIN_SOURCE_IMAGE",
},
}
if err := app.Run(os.Args); err != nil {
@@ -419,6 +429,8 @@ func run(c *cli.Context) error {
Password: c.String("cosign.password"),
Params: c.String("cosign.params"),
},
PushOnly: c.Bool("push-only"),
SourceImage: c.String("source-image"),
}
if c.Bool("tags.auto") {
+191 -2
View File
@@ -96,6 +96,8 @@ type (
BaseImageRegistry string // Docker registry to pull base image
BaseImageUsername string // Docker registry username to pull base image
BaseImagePassword string // Docker registry password to pull base image
PushOnly bool // Push only mode, skips build process
SourceImage string // Source image to push (optional)
}
Card []struct {
@@ -201,7 +203,8 @@ func (p Plugin) Exec() error {
fmt.Println(out)
return fmt.Errorf("Error authenticating base connector: exit status 1")
}
} else {
} else if !p.PushOnly {
// Skip base image connector warning in push-only mode (not pulling anything)
fmt.Println("\033[33mTo ensure consistent and reliable pipeline execution, we recommend setting up a Base Image Connector.\033[0m\n" +
"\033[33mWhile optional at this time, configuring it helps prevent failures caused by Docker Hub's rate limits.\033[0m")
}
@@ -229,6 +232,16 @@ func (p Plugin) Exec() error {
}
}
// Enforce mutual exclusivity: push-only and dry-run cannot be used together
if p.PushOnly && p.Dryrun {
return fmt.Errorf("conflict: push-only and dry-run cannot be used together")
}
// Handle push-only mode if requested
if p.PushOnly {
return p.pushOnly()
}
if p.Build.Squash && !p.Daemon.Experimental {
fmt.Println("Squash build flag is only available when Docker deamon is started with experimental flag. Ignoring...")
p.Build.Squash = false
@@ -742,6 +755,22 @@ func getDigest(buildName string) (string, error) {
return "", errors.New("unable to fetch digest")
}
// imageExists checks if an image exists in local daemon
func imageExists(tag string) bool {
cmd := exec.Command(dockerExe, "image", "inspect", tag)
return cmd.Run() == nil
}
// getDigestAfterPush gets digest from a pushed image
func getDigestAfterPush(tag string) (string, error) {
cmd := exec.Command(dockerExe, "inspect", "--format", "{{ index (split (index .RepoDigests 0) \"@\") 1 }}", tag)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get digest for %s: %w", tag, err)
}
return strings.TrimSpace(string(output)), nil
}
// shouldSignWithCosign determines if cosign signing should be performed
func (p Plugin) shouldSignWithCosign() bool {
return p.Cosign.PrivateKey != ""
@@ -837,7 +866,7 @@ func executeCosignCommand(cmd *exec.Cmd) {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fmt.Printf("🚀 Executing: %s %s\n", cmd.Path, strings.Join(cmd.Args[1:], " "))
if err := cmd.Run(); err != nil {
fmt.Printf("⚠️ WARNING: Image signing failed: %s\n", err)
fmt.Printf(" Image was pushed successfully but could not be signed\n")
@@ -845,4 +874,164 @@ func executeCosignCommand(cmd *exec.Cmd) {
}
}
// pushOnly handles pushing images without building them
func (p Plugin) pushOnly() error {
// Check if source image is specified
sourceImageName := p.SourceImage
var sourceTags []string
if sourceImageName == "" {
// If no source image specified, use the repo and first tag
fmt.Println("source_image not provided, using repo and tag value")
sourceImageName = p.Build.Repo
sourceTags = p.Build.Tags
} else {
// If source image is specified, check if it has a tag
lastColonIndex := strings.LastIndex(sourceImageName, ":")
if lastColonIndex > 0 && lastColonIndex < len(sourceImageName) {
// Check if there's a slash after the last colon (indicating it's a port, not a tag)
// For example: registry:5000/image (has slash after colon - port not tag)
// vs image:tag (no slash after colon - it's a tag)
if strings.LastIndex(sourceImageName, "/") > lastColonIndex {
// The last colon is part of the registry:port, not a tag separator
sourceTags = []string{"latest"}
} else {
// The last colon separates the tag
tag := sourceImageName[lastColonIndex+1:]
sourceImageName = sourceImageName[:lastColonIndex]
if tag == "" {
fmt.Printf("No tag specified in source image (or empty tag). Using 'latest' as the default tag.\n")
tag = "latest"
}
sourceTags = []string{tag}
}
} else {
// Default to "latest" if no tag specified
sourceTags = []string{"latest"}
}
fmt.Printf("Using source image: %s with tag(s): %s\n", sourceImageName, strings.Join(sourceTags, ", "))
}
// For each source tag and target tag combination
var digest string
var firstPushedImage string
for _, sourceTag := range sourceTags {
sourceFullImageName := fmt.Sprintf("%s:%s", sourceImageName, sourceTag)
// Check if the source image exists in local daemon
if !imageExists(sourceFullImageName) {
fmt.Printf("Warning: Source image %s not found\n", sourceFullImageName)
// Continue to the next source tag if available, otherwise return error
if len(sourceTags) > 1 {
continue
}
return fmt.Errorf("source image %s not found, cannot push", sourceFullImageName)
}
// For each target tag, tag and push
for _, targetTag := range p.Build.Tags {
targetFullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, targetTag)
// Skip if source and target are identical
if sourceFullImageName == targetFullImageName {
fmt.Printf("Source and target image names are identical: %s\n", sourceFullImageName)
} else {
// Tag the source image with the target name
fmt.Printf("Tagging %s as %s\n", sourceFullImageName, targetFullImageName)
tagCmd := exec.Command(dockerExe, "tag", sourceFullImageName, targetFullImageName)
tagCmd.Stdout = os.Stdout
tagCmd.Stderr = os.Stderr
trace(tagCmd)
if err := tagCmd.Run(); err != nil {
return fmt.Errorf("failed to tag image %s as %s: %w", sourceFullImageName, targetFullImageName, err)
}
}
}
}
// Push all target images
for _, tag := range p.Build.Tags {
fullImageName := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
// Check if image exists in local daemon
if !imageExists(fullImageName) {
return fmt.Errorf("image %s not found, cannot push", fullImageName)
}
// Push image
fmt.Println("Pushing image:", fullImageName)
pushCmd := commandPush(p.Build, tag)
pushCmd.Stdout = os.Stdout
pushCmd.Stderr = os.Stderr
trace(pushCmd)
if err := pushCmd.Run(); err != nil {
return fmt.Errorf("failed to push image %s: %w", fullImageName, err)
}
// Track the first pushed image for card generation
if firstPushedImage == "" {
firstPushedImage = fullImageName
}
// Get the digest after push (we only need one)
if digest == "" {
d, err := getDigestAfterPush(fullImageName)
if err == nil {
digest = d
} else {
fmt.Printf("Warning: Could not get digest for %s: %v\n", fullImageName, err)
}
}
}
// Output the adaptive card
if firstPushedImage != "" {
if err := p.writeCardForImage(firstPushedImage); err != nil {
fmt.Printf("Could not create adaptive card. %s\n", err)
}
}
// Write to artifact file
if p.ArtifactFile != "" && digest != "" {
if err := drone.WritePluginArtifactFile(
p.Daemon.RegistryType,
p.ArtifactFile,
p.Daemon.Registry,
p.Build.Repo,
digest,
p.Build.Tags,
); err != nil {
fmt.Printf("Failed to write plugin artifact file at path: %s with error: %s\n",
p.ArtifactFile, err)
}
}
// Handle cosign signing after push
if p.shouldSignWithCosign() {
// Set up environment variables for cosign
os.Setenv("COSIGN_YES", "true")
if digest != "" {
fmt.Printf("🔐 Found image digest: %s\n", digest)
// Sign with digest reference
imageRef := fmt.Sprintf("%s@%s", p.Build.Repo, digest)
cosignCmd := createCosignCommand(imageRef, p.Cosign)
executeCosignCommand(cosignCmd)
} else {
fmt.Printf("⚠️ WARNING: Could not get image digest for cosign signing\n")
fmt.Printf(" Falling back to tag-based signing\n")
// Fall back to tag-based signing for each tag
for _, tag := range p.Build.Tags {
imageRef := fmt.Sprintf("%s:%s", p.Build.Repo, tag)
cosignCmd := createCosignCommand(imageRef, p.Cosign)
executeCosignCommand(cosignCmd)
}
}
}
return nil
}