Skip to content

Commit

Permalink
Merge pull request #752 from traPtitech/impr/image-pruner
Browse files Browse the repository at this point in the history
fix: image cleaner
  • Loading branch information
motoki317 authored Oct 17, 2023
2 parents 7fc9b14 + 41a6e03 commit 6575955
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 92 deletions.
3 changes: 2 additions & 1 deletion cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 60 additions & 26 deletions pkg/usecase/apiserver/app_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package apiserver
import (
"context"
"fmt"
"github.com/regclient/regclient/types/ref"
"github.com/traPtitech/neoshowcase/pkg/util/regutil"
"strconv"

"github.com/friendsofgo/errors"
Expand Down Expand Up @@ -145,32 +147,6 @@ func (s *Service) createApplicationDatabase(ctx context.Context, app *domain.App
return nil
}

func (s *Service) deleteApplicationDatabase(ctx context.Context, app *domain.Application, envs []*domain.Environment) error {
if app.Config.BuildConfig.MariaDB() {
dbKey, ok := lo.Find(envs, func(e *domain.Environment) bool { return e.Key == domain.EnvMariaDBDatabaseKey })
if !ok {
return errors.New("failed to find mariadb name from env key")
}
err := s.mariaDBManager.Delete(ctx, domain.DeleteArgs{Database: dbKey.Value})
if err != nil {
return err
}
}

if app.Config.BuildConfig.MongoDB() {
dbKey, ok := lo.Find(envs, func(e *domain.Environment) bool { return e.Key == domain.EnvMongoDBDatabaseKey })
if !ok {
return errors.New("failed to find mongodb name from env key")
}
err := s.mongoDBManager.Delete(ctx, domain.DeleteArgs{Database: dbKey.Value})
if err != nil {
return err
}
}

return nil
}

type TopAppInfo struct {
App *domain.Application
LatestBuild *domain.Build
Expand Down Expand Up @@ -274,7 +250,57 @@ func (s *Service) UpdateApplication(ctx context.Context, id string, args *domain
return nil
}

func (s *Service) deleteApplicationDatabase(ctx context.Context, app *domain.Application, envs []*domain.Environment) error {
if app.Config.BuildConfig.MariaDB() {
dbKey, ok := lo.Find(envs, func(e *domain.Environment) bool { return e.Key == domain.EnvMariaDBDatabaseKey })
if !ok {
return errors.New("failed to find mariadb name from env key")
}
err := s.mariaDBManager.Delete(ctx, domain.DeleteArgs{Database: dbKey.Value})
if err != nil {
return err
}
}

if app.Config.BuildConfig.MongoDB() {
dbKey, ok := lo.Find(envs, func(e *domain.Environment) bool { return e.Key == domain.EnvMongoDBDatabaseKey })
if !ok {
return errors.New("failed to find mongodb name from env key")
}
err := s.mongoDBManager.Delete(ctx, domain.DeleteArgs{Database: dbKey.Value})
if err != nil {
return err
}
}

return nil
}

func (s *Service) deleteApplicationImages(ctx context.Context, app *domain.Application) error {
if app.DeployType != domain.DeployTypeRuntime {
return nil
}

imageName := s.image.ImageName(app.ID)
tags, err := regutil.TagList(ctx, s.registry, imageName)
if err != nil {
return err
}
for _, tag := range tags {
tagRef, err := ref.New(imageName + ":" + tag)
if err != nil {
return err
}
err = s.registry.TagDelete(ctx, tagRef)
if err != nil {
return err
}
}
return nil
}

func (s *Service) DeleteApplication(ctx context.Context, id string) error {
// Validate
err := s.isApplicationOwner(ctx, id)
if err != nil {
return err
Expand All @@ -288,6 +314,7 @@ func (s *Service) DeleteApplication(ctx context.Context, id string) error {
return newError(ErrorTypeBadRequest, "stop the application first before deleting", nil)
}

// Delete app database
env, err := s.envRepo.GetEnv(ctx, domain.GetEnvCondition{ApplicationID: optional.From(id)})
if err != nil {
return err
Expand All @@ -296,6 +323,13 @@ func (s *Service) DeleteApplication(ctx context.Context, id string) error {
if err != nil {
return err
}
// Delete runtime app image in background
go func() {
err := s.deleteApplicationImages(context.WithoutCancel(ctx), app)
if err != nil {
log.Errorf("Deleting application %v (id: %v) image: %+v", app.Name, app.ID, err)
}
}()

// delete artifacts
artifacts, err := s.artifactRepo.GetArtifacts(ctx, domain.GetArtifactCondition{ApplicationID: optional.From(app.ID)})
Expand Down
7 changes: 7 additions & 0 deletions pkg/usecase/apiserver/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package apiserver

import (
"context"
"github.com/regclient/regclient"
"github.com/traPtitech/neoshowcase/pkg/domain/builder"

"github.com/friendsofgo/errors"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
Expand Down Expand Up @@ -35,6 +37,8 @@ type Service struct {
containerLogger domain.ContainerLogger
controller domain.ControllerServiceClient
fallbackKey *ssh.PublicKeys
image builder.ImageConfig
registry *regclient.RegClient

systemInfo func(ctx context.Context) (*domain.SystemInfo, error)
tmpKeys *tmpKeyPairService
Expand All @@ -53,6 +57,7 @@ func NewService(
metricsService domain.MetricsService,
containerLogger domain.ContainerLogger,
controller domain.ControllerServiceClient,
image builder.ImageConfig,
fallbackKey *ssh.PublicKeys,
) (*Service, error) {
return &Service{
Expand All @@ -69,6 +74,8 @@ func NewService(
containerLogger: containerLogger,
controller: controller,
fallbackKey: fallbackKey,
image: image,
registry: image.NewRegistry(),

systemInfo: scutil.Once(controller.GetSystemInfo),
tmpKeys: newTmpKeyPairService(),
Expand Down
110 changes: 47 additions & 63 deletions pkg/usecase/cleaner/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cleaner
import (
"context"
"fmt"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -60,7 +59,7 @@ func NewService(
c.start = func() {
go loop.Loop(ctx, func(ctx context.Context) {
start := time.Now()
err := c.pruneImages(ctx, r, image.Registry.Addr)
err := c.pruneImages(ctx, r)
if err != nil {
log.Errorf("failed to prune images: %+v", err)
return
Expand Down Expand Up @@ -92,73 +91,43 @@ func (c *cleanerService) Shutdown(_ context.Context) error {
return nil
}

func (c *cleanerService) getOlderBuilds(ctx context.Context, appID string, targetBuildID string) ([]*domain.Build, error) {
if targetBuildID == "" {
return nil, nil
}
builds, err := c.buildRepo.GetBuilds(ctx, domain.GetBuildCondition{ApplicationID: optional.From(appID)})
if err != nil {
return nil, err
}
current, ok := lo.Find(builds, func(b *domain.Build) bool { return b.ID == targetBuildID })
if !ok {
return nil, errors.Errorf("failed to find build %v in retrieved builds", targetBuildID)
}
return lo.Filter(builds, func(b *domain.Build, _ int) bool { return b.QueuedAt.Before(current.QueuedAt) }), nil
}

func (c *cleanerService) pruneImages(ctx context.Context, r *regclient.RegClient, regHost string) error {
func (c *cleanerService) pruneImages(ctx context.Context, r *regclient.RegClient) error {
applications, err := c.appRepo.GetApplications(ctx, domain.GetApplicationCondition{DeployType: optional.From(domain.DeployTypeRuntime)})
if err != nil {
return err
}
appsMap := lo.SliceToMap(applications, func(app *domain.Application) (string, *domain.Application) { return app.ID, app })

repos, err := regutil.RepoList(ctx, r, regHost)
if err != nil {
return errors.Wrap(err, "failed to get image repositories")
}

for _, imageName := range repos {
if !strings.HasPrefix(imageName, c.image.NamePrefix) {
continue
}
err = c.pruneImage(ctx, r, regHost, imageName, appsMap)
for _, app := range applications {
err = c.pruneImage(ctx, r, app)
if err != nil {
log.Errorf("pruning image %v: %+v", imageName, err)
log.Errorf("pruning image %v: %+v", c.image.NamePrefix+app.ID, err)
// fail-safe for each image
}
}

return nil
}

func (c *cleanerService) pruneImage(ctx context.Context, r *regclient.RegClient, regHost string, imageName string, appsMap map[string]*domain.Application) error {
appID := strings.TrimPrefix(imageName, c.image.NamePrefix)

tags, err := regutil.TagList(ctx, r, regHost, imageName)
func (c *cleanerService) pruneImage(ctx context.Context, r *regclient.RegClient, app *domain.Application) error {
imageName := c.image.ImageName(app.ID)
tags, err := regutil.TagList(ctx, r, imageName)
if err != nil {
return errors.Wrap(err, "getting tags")
}
app, ok := appsMap[appID]
var danglingTags []string
if ok {
// app still exists; compare by queued_at time, then delete any older builds
olderBuilds, err := c.getOlderBuilds(ctx, app.ID, app.CurrentBuild)
if err != nil {
return err
}
olderBuildIDs := ds.Map(olderBuilds, func(b *domain.Build) string { return b.ID })
danglingTags = lo.Filter(tags, func(tag string, _ int) bool { return lo.Contains(olderBuildIDs, tag) })
} else {
// app was deleted
danglingTags = tags

// compare by queued_at time, then delete any older builds
olderBuilds, err := c.getOlderBuilds(ctx, app.ID, app.CurrentBuild)
if err != nil {
return err
}
olderBuildIDs := ds.Map(olderBuilds, func(b *domain.Build) string { return b.ID })
danglingTags := lo.Filter(tags, func(tag string, _ int) bool { return lo.Contains(olderBuildIDs, tag) })

for _, tag := range danglingTags {
// NOTE: needs manual execution of "registry garbage-collect <config> --delete-untagged" in docker registry side
// to actually delete the layers
// https://docs.docker.com/registry/garbage-collection/
tagRef, err := ref.New(regHost + "/" + imageName + ":" + tag)
tagRef, err := ref.New(imageName + ":" + tag)
if err != nil {
return err
}
Expand All @@ -171,25 +140,19 @@ func (c *cleanerService) pruneImage(ctx context.Context, r *regclient.RegClient,
return nil
}

func (c *cleanerService) getArtifactsNoLongerInUse(ctx context.Context) ([]*domain.Artifact, error) {
applications, err := c.appRepo.GetApplications(ctx, domain.GetApplicationCondition{
DeployType: optional.From(domain.DeployTypeStatic),
})
func (c *cleanerService) getOlderBuilds(ctx context.Context, appID string, targetBuildID string) ([]*domain.Build, error) {
if targetBuildID == "" {
return nil, nil
}
builds, err := c.buildRepo.GetBuilds(ctx, domain.GetBuildCondition{ApplicationID: optional.From(appID)})
if err != nil {
return nil, err
}

artifacts := make([]*domain.Artifact, 0, len(applications))
for _, app := range applications {
olderBuilds, err := c.getOlderBuilds(ctx, app.ID, app.CurrentBuild)
if err != nil {
return nil, err
}
for _, b := range olderBuilds {
artifacts = append(artifacts, b.Artifacts...)
}
current, ok := lo.Find(builds, func(b *domain.Build) bool { return b.ID == targetBuildID })
if !ok {
return nil, errors.Errorf("failed to find build %v in retrieved builds", targetBuildID)
}
return artifacts, nil
return lo.Filter(builds, func(b *domain.Build, _ int) bool { return b.QueuedAt.Before(current.QueuedAt) }), nil
}

func (c *cleanerService) pruneArtifacts(ctx context.Context) error {
Expand All @@ -210,3 +173,24 @@ func (c *cleanerService) pruneArtifacts(ctx context.Context) error {
}
return nil
}

func (c *cleanerService) getArtifactsNoLongerInUse(ctx context.Context) ([]*domain.Artifact, error) {
applications, err := c.appRepo.GetApplications(ctx, domain.GetApplicationCondition{
DeployType: optional.From(domain.DeployTypeStatic),
})
if err != nil {
return nil, err
}

artifacts := make([]*domain.Artifact, 0, len(applications))
for _, app := range applications {
olderBuilds, err := c.getOlderBuilds(ctx, app.ID, app.CurrentBuild)
if err != nil {
return nil, err
}
for _, b := range olderBuilds {
artifacts = append(artifacts, b.Artifacts...)
}
}
return artifacts, nil
}
4 changes: 2 additions & 2 deletions pkg/util/regutil/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ func RepoList(ctx context.Context, r *regclient.RegClient, regHost string) ([]st
}
}

func TagList(ctx context.Context, r *regclient.RegClient, regHost string, imageName string) ([]string, error) {
func TagList(ctx context.Context, r *regclient.RegClient, imageName string) ([]string, error) {
const limit = 100
var tags []string
for {
opts := []scheme.TagOpts{scheme.WithTagLimit(limit)}
if len(tags) > 0 {
opts = append(opts, scheme.WithTagLast(tags[len(tags)-1]))
}
repoRef, err := ref.New(regHost + "/" + imageName)
repoRef, err := ref.New(imageName)
if err != nil {
return nil, err
}
Expand Down

0 comments on commit 6575955

Please sign in to comment.