From 9a845664e10224ed1245fd52bb3077fe904ee6fe Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 3 Aug 2021 16:22:57 -0400 Subject: [PATCH] feat: implement image cleanup --- plugins/common/docker.go | 58 +++++++++++++++++------------------ plugins/registry/functions.go | 44 +++++++++++++++++++------- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/plugins/common/docker.go b/plugins/common/docker.go index fe9dfa4bd..9d914e365 100644 --- a/plugins/common/docker.go +++ b/plugins/common/docker.go @@ -210,7 +210,7 @@ func DockerCleanup(appName string, forceCleanup bool) error { } // delete dangling images - imageIDs, _ := listDanglingImages(appName) + imageIDs, _ := ListDanglingImages(appName) if len(imageIDs) > 0 { RemoveImages(imageIDs) } @@ -301,6 +301,34 @@ func IsImageHerokuishBased(image string, appName string) bool { return output != "" } +func ListDanglingImages(appName string) ([]string, error) { + command := []string{ + DockerBin(), + "image", + "list", + "--quiet", + "--filter", + "dangling=true", + } + + if appName != "" { + command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) + } + + var stderr bytes.Buffer + listCmd := NewShellCmd(strings.Join(command, " ")) + listCmd.ShowOutput = false + listCmd.Command.Stderr = &stderr + b, err := listCmd.Output() + + if err != nil { + return []string{}, errors.New(strings.TrimSpace(stderr.String())) + } + + output := strings.Split(strings.TrimSpace(string(b[:])), "\n") + return output, nil +} + // RemoveImages removes images by ID func RemoveImages(imageIDs []string) { command := []string{ @@ -356,34 +384,6 @@ func listContainers(status string, appName string) ([]string, error) { return output, nil } -func listDanglingImages(appName string) ([]string, error) { - command := []string{ - DockerBin(), - "image", - "list", - "--quiet", - "--filter", - "dangling=true", - } - - if appName != "" { - command = append(command, []string{"--filter", fmt.Sprintf("label=com.dokku.app-name=%v", appName)}...) - } - - var stderr bytes.Buffer - listCmd := NewShellCmd(strings.Join(command, " ")) - listCmd.ShowOutput = false - listCmd.Command.Stderr = &stderr - b, err := listCmd.Output() - - if err != nil { - return []string{}, errors.New(strings.TrimSpace(stderr.String())) - } - - output := strings.Split(strings.TrimSpace(string(b[:])), "\n") - return output, nil -} - func pruneUnusedImages(appName string) { command := []string{ DockerBin(), diff --git a/plugins/registry/functions.go b/plugins/registry/functions.go index 83bd0cac8..14eb40dc1 100644 --- a/plugins/registry/functions.go +++ b/plugins/registry/functions.go @@ -29,7 +29,7 @@ func isPushEnabled(appName string) bool { return reportComputedPushOnRelease(appName) == "true" } -func incrementTagVersion(appName string) (string, error) { +func incrementTagVersion(appName string) (int, error) { tag := common.PropertyGet("registry", appName, "tag-version") if tag == "" { tag = "0" @@ -37,19 +37,19 @@ func incrementTagVersion(appName string) (string, error) { version, err := strconv.Atoi(tag) if err != nil { - return "", fmt.Errorf("Unable to convert existing tag version (%s) to integer: %v", tag, err) + return 0, fmt.Errorf("Unable to convert existing tag version (%s) to integer: %v", tag, err) } version++ common.LogVerboseQuiet(fmt.Sprintf("Bumping tag to %d", version)) if err = common.PropertyWrite("registry", appName, "tag-version", strconv.Itoa(version)); err != nil { - return "", err + return 0, err } - return strconv.Itoa(version), nil + return version, nil } -func pushToRegistry(appName string, tag string) error { +func pushToRegistry(appName string, tag int) error { common.LogVerboseQuiet("Retrieving image info for app") registryServer := getRegistryServerForApp(appName) @@ -58,27 +58,29 @@ func pushToRegistry(appName string, tag string) error { image := common.GetAppImageName(appName, imageTag, "") imageID, _ := common.DockerInspect(image, "{{ .Id }}") - common.LogVerboseQuiet(fmt.Sprintf("Tagging $IMAGE_REPO:%s in registry format", tag)) - if !dockerTag(imageID, fmt.Sprintf("%s%s:%s", registryServer, imageRepo, tag)) { + common.LogVerboseQuiet(fmt.Sprintf("Tagging $IMAGE_REPO:%d in registry format", tag)) + if !dockerTag(imageID, fmt.Sprintf("%s%s:%d", registryServer, imageRepo, tag)) { // TODO: better error return errors.New("Unable to tag image") } - if !dockerTag(imageID, fmt.Sprintf("%s:%s", imageRepo, tag)) { + if !dockerTag(imageID, fmt.Sprintf("%s:%d", imageRepo, tag)) { // TODO: better error return errors.New("Unable to tag image") } - // fn-registry-create-repository "$APP" "$DOKKU_REGISTRY_SERVER" "$IMAGE_REPO" + // For the future, we should also add the ability to create the remote repository + // This is only really important for registries that do not support creation on push + // Examples include AWS and Quay.io common.LogVerboseQuiet("Pushing $IMAGE_REPO:$TAG") - if !dockerPush(fmt.Sprintf("%s%s:%s", registryServer, imageRepo, tag)) { + if !dockerPush(fmt.Sprintf("%s%s:%d", registryServer, imageRepo, tag)) { // TODO: better error return errors.New("Unable to push image") } common.LogVerboseQuiet("Cleaning up") - // fn-registry-image-cleanup "$APP" "$DOKKU_REGISTRY_SERVER" "$IMAGE_REPO" "$IMAGE_TAG" "$TAG" + imageCleanup(appName, registryServer, imageRepo, imageTag, tag) common.LogVerboseQuiet("Image $IMAGE_REPO:$TAG pushed") return nil @@ -105,3 +107,23 @@ func dockerPush(imageTag string) bool { return true } + +func imageCleanup(appName string, registryServer string, imageRepo string, imageTag string, tag int) { + // # keep last two images in place + oldTag := tag - 1 + tenImagesAgoTag := tag - 12 + + imagesToRemove := []string{} + for oldTag > 0 { + imagesToRemove = append(imagesToRemove, fmt.Sprintf("%s%s:%d", registryServer, imageRepo, oldTag)) + imagesToRemove = append(imagesToRemove, fmt.Sprintf("%s:%d", imageRepo, oldTag)) + oldTag = oldTag - 1 + if tenImagesAgoTag == oldTag { + break + } + } + + imageIDs, _ := common.ListDanglingImages(appName) + imagesToRemove = append(imagesToRemove, imageIDs...) + common.RemoveImages(imagesToRemove) +}