|
|
|
@@ -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 != ""
|
|
|
|
@@ -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
|
|
|
|
|
}
|
|
|
|
|