feat: [AH-1223]: Implement delete version for non oci package types (#3684)

* feat: [AH-1223]: fix failing go lint
* feat: [AH-1223]: fix failing go lint
* feat: [AH-1223]: file name change
* feat: [AH-1223]: file name change
* feat: [AH-1223]: fix go lint errors
* feat: [AH-1223]: add RPM package type in switch case
* feat: [AH-1223]: cascade delete download_stats and bandwidth_stats
* feat: [AH-1223]: resolve PR comments
* feat: [AH-1223]: resolve PR comments
* feat: [AH-1223]: fix failing go lint checks
* feat: [AH-1223]: add different switch case for delete based on package type
* feat: [AH-1223]: Implement delete version for non oci package types
This commit is contained in:
Shivanand Sonnad 2025-04-18 09:01:07 +00:00 committed by Harness
parent e3e0af68ce
commit 8b277e62d7
14 changed files with 414 additions and 29 deletions

View File

@ -0,0 +1,6 @@
ALTER TABLE download_stats DROP CONSTRAINT IF EXISTS fk_artifacts_artifact_id;
ALTER TABLE download_stats
ADD CONSTRAINT fk_artifacts_artifact_id
FOREIGN KEY (download_stat_artifact_id)
REFERENCES artifacts(artifact_id);

View File

@ -0,0 +1,7 @@
ALTER TABLE download_stats DROP CONSTRAINT IF EXISTS fk_artifacts_artifact_id;
ALTER TABLE download_stats
ADD CONSTRAINT fk_artifacts_artifact_id
FOREIGN KEY (download_stat_artifact_id)
REFERENCES artifacts(artifact_id)
ON DELETE CASCADE;

View File

@ -0,0 +1,6 @@
ALTER TABLE bandwidth_stats DROP CONSTRAINT IF EXISTS fk_images_image_id;
ALTER TABLE bandwidth_stats
ADD CONSTRAINT fk_images_image_id
FOREIGN KEY (bandwidth_stat_image_id)
REFERENCES images(image_id);

View File

@ -0,0 +1,7 @@
ALTER TABLE bandwidth_stats DROP CONSTRAINT IF EXISTS fk_images_image_id;
ALTER TABLE bandwidth_stats
ADD CONSTRAINT fk_images_image_id
FOREIGN KEY (bandwidth_stat_image_id)
REFERENCES images(image_id)
ON DELETE CASCADE;

View File

@ -0,0 +1,39 @@
CREATE TABLE download_stats_new
(
download_stat_id INTEGER PRIMARY KEY AUTOINCREMENT,
download_stat_artifact_id INTEGER NOT NULL
CONSTRAINT fk_artifacts_artifact_id
REFERENCES artifacts (artifact_id),
download_stat_timestamp INTEGER NOT NULL,
download_stat_created_at INTEGER NOT NULL,
download_stat_updated_at INTEGER NOT NULL,
download_stat_created_by INTEGER NOT NULL,
download_stat_updated_by INTEGER NOT NULL
);
DROP INDEX IF EXISTS download_stat_artifact_id;
CREATE INDEX download_stat_artifact_id ON download_stats_new(download_stat_artifact_id);
INSERT INTO download_stats_new (
download_stat_id,
download_stat_artifact_id,
download_stat_timestamp,
download_stat_created_at,
download_stat_updated_at,
download_stat_created_by,
download_stat_updated_by
)
SELECT
download_stat_id,
download_stat_artifact_id,
download_stat_timestamp,
download_stat_created_at,
download_stat_updated_at,
download_stat_created_by,
download_stat_updated_by
FROM download_stats;
DROP TABLE download_stats;
ALTER TABLE download_stats_new RENAME TO download_stats;

View File

@ -0,0 +1,39 @@
CREATE TABLE download_stats_new
(
download_stat_id INTEGER PRIMARY KEY AUTOINCREMENT,
download_stat_artifact_id INTEGER NOT NULL
CONSTRAINT fk_artifacts_artifact_id
REFERENCES artifacts (artifact_id) ON DELETE CASCADE,
download_stat_timestamp INTEGER NOT NULL,
download_stat_created_at INTEGER NOT NULL,
download_stat_updated_at INTEGER NOT NULL,
download_stat_created_by INTEGER NOT NULL,
download_stat_updated_by INTEGER NOT NULL
);
DROP INDEX IF EXISTS download_stat_artifact_id;
CREATE INDEX download_stat_artifact_id ON download_stats_new(download_stat_artifact_id);
INSERT INTO download_stats_new (
download_stat_id,
download_stat_artifact_id,
download_stat_timestamp,
download_stat_created_at,
download_stat_updated_at,
download_stat_created_by,
download_stat_updated_by
)
SELECT
download_stat_id,
download_stat_artifact_id,
download_stat_timestamp,
download_stat_created_at,
download_stat_updated_at,
download_stat_created_by,
download_stat_updated_by
FROM download_stats;
DROP TABLE download_stats;
ALTER TABLE download_stats_new RENAME TO download_stats;

View File

@ -0,0 +1,41 @@
CREATE TABLE bandwidth_stats_new
(
bandwidth_stat_id INTEGER PRIMARY KEY AUTOINCREMENT,
bandwidth_stat_image_id INTEGER NOT NULL
CONSTRAINT fk_images_image_id
REFERENCES images(image_id),
bandwidth_stat_timestamp INTEGER NOT NULL,
bandwidth_stat_bytes INTEGER,
bandwidth_stat_type TEXT NOT NULL,
bandwidth_stat_created_at INTEGER NOT NULL,
bandwidth_stat_updated_at INTEGER NOT NULL,
bandwidth_stat_created_by INTEGER NOT NULL,
bandwidth_stat_updated_by INTEGER NOT NULL
);
INSERT INTO bandwidth_stats_new (
bandwidth_stat_id,
bandwidth_stat_image_id,
bandwidth_stat_timestamp,
bandwidth_stat_bytes,
bandwidth_stat_type,
bandwidth_stat_created_at,
bandwidth_stat_updated_at,
bandwidth_stat_created_by,
bandwidth_stat_updated_by
)
SELECT
bandwidth_stat_id,
bandwidth_stat_image_id,
bandwidth_stat_timestamp,
bandwidth_stat_bytes,
bandwidth_stat_type,
bandwidth_stat_created_at,
bandwidth_stat_updated_at,
bandwidth_stat_created_by,
bandwidth_stat_updated_by
FROM bandwidth_stats;
DROP TABLE bandwidth_stats;
ALTER TABLE bandwidth_stats_new RENAME TO bandwidth_stats;

View File

@ -0,0 +1,41 @@
CREATE TABLE bandwidth_stats_new
(
bandwidth_stat_id INTEGER PRIMARY KEY AUTOINCREMENT,
bandwidth_stat_image_id INTEGER NOT NULL
CONSTRAINT fk_images_image_id
REFERENCES images(image_id) ON DELETE CASCADE,
bandwidth_stat_timestamp INTEGER NOT NULL,
bandwidth_stat_bytes INTEGER,
bandwidth_stat_type TEXT NOT NULL,
bandwidth_stat_created_at INTEGER NOT NULL,
bandwidth_stat_updated_at INTEGER NOT NULL,
bandwidth_stat_created_by INTEGER NOT NULL,
bandwidth_stat_updated_by INTEGER NOT NULL
);
INSERT INTO bandwidth_stats_new (
bandwidth_stat_id,
bandwidth_stat_image_id,
bandwidth_stat_timestamp,
bandwidth_stat_bytes,
bandwidth_stat_type,
bandwidth_stat_created_at,
bandwidth_stat_updated_at,
bandwidth_stat_created_by,
bandwidth_stat_updated_by
)
SELECT
bandwidth_stat_id,
bandwidth_stat_image_id,
bandwidth_stat_timestamp,
bandwidth_stat_bytes,
bandwidth_stat_type,
bandwidth_stat_created_at,
bandwidth_stat_updated_at,
bandwidth_stat_created_by,
bandwidth_stat_updated_by
FROM bandwidth_stats;
DROP TABLE bandwidth_stats;
ALTER TABLE bandwidth_stats_new RENAME TO bandwidth_stats;

View File

@ -16,12 +16,14 @@ package metadata
import (
"context"
"fmt"
"net/http"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/request"
"github.com/harness/gitness/audit"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
"github.com/harness/gitness/registry/app/api/utils"
"github.com/harness/gitness/registry/services/webhook"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
@ -66,29 +68,86 @@ func (c *APIController) DeleteArtifactVersion(ctx context.Context, r artifact.De
}
repoEntity, err := c.RegistryRepository.GetByParentIDAndName(ctx, regInfo.parentID, regInfo.RegistryIdentifier)
if len(repoEntity.Name) == 0 {
if err != nil {
//nolint:nilerr
return artifact.DeleteArtifactVersion404JSONResponse{
NotFoundJSONResponse: artifact.NotFoundJSONResponse(
*GetErrorResponse(http.StatusNotFound, "registry doesn't exist with this key"),
),
}, nil
}
artifactName := string(r.Artifact)
versionName := string(r.Version)
registryName := repoEntity.Name
image, err := c.ImageStore.GetByRepoAndName(ctx, regInfo.parentID, regInfo.RegistryIdentifier, artifactName)
if err != nil {
//nolint:nilerr
return artifact.DeleteArtifactVersion404JSONResponse{
NotFoundJSONResponse: artifact.NotFoundJSONResponse(
*GetErrorResponse(http.StatusNotFound, "image doesn't exist with this key"),
),
}, nil
}
_, err = c.ArtifactStore.GetByName(ctx, image.ID, versionName)
if err != nil {
//nolint:nilerr
return artifact.DeleteArtifactVersion404JSONResponse{
NotFoundJSONResponse: artifact.NotFoundJSONResponse(
*GetErrorResponse(http.StatusNotFound, "version doesn't exist with this key"),
),
}, nil
}
switch regInfo.PackageType {
case artifact.PackageTypeDOCKER:
err = c.deleteTag(ctx, regInfo, registryName, session.Principal, artifactName,
versionName)
case artifact.PackageTypeHELM:
err = c.deleteTag(ctx, regInfo, registryName, session.Principal, artifactName,
versionName)
case artifact.PackageTypeNPM:
err = c.deleteVersion(ctx, regInfo, artifactName, versionName)
case artifact.PackageTypeMAVEN:
err = c.deleteVersion(ctx, regInfo, artifactName, versionName)
case artifact.PackageTypePYTHON:
err = c.deleteVersion(ctx, regInfo, artifactName, versionName)
case artifact.PackageTypeGENERIC:
err = c.deleteVersion(ctx, regInfo, artifactName, versionName)
case artifact.PackageTypeNUGET:
err = fmt.Errorf("delete version not supported for nuget")
case artifact.PackageTypeRPM:
err = fmt.Errorf("delete version not supported for rpm")
default:
err = fmt.Errorf("unsupported package type: %s", regInfo.PackageType)
}
if err != nil {
return throwDeleteArtifactVersion500Error(err), err
}
err = c.deleteTagWithAudit(ctx, regInfo, repoEntity.Name, session.Principal, string(r.Artifact),
string(r.Version))
if err != nil {
return throwDeleteArtifactVersion500Error(err), err
auditErr := c.AuditService.Log(
ctx,
session.Principal,
audit.NewResource(audit.ResourceTypeRegistry, artifactName),
audit.ActionDeleted,
regInfo.ParentRef,
audit.WithData("registry name", registryName),
audit.WithData("artifact name", artifactName),
audit.WithData("version name", versionName),
)
if auditErr != nil {
log.Ctx(ctx).Warn().Msgf("failed to insert audit log for delete artifact operation: %s", auditErr)
}
return artifact.DeleteArtifactVersion200JSONResponse{
SuccessJSONResponse: artifact.SuccessJSONResponse(*GetSuccessResponse()),
}, nil
}
func (c *APIController) deleteTagWithAudit(
func (c *APIController) deleteTag(
ctx context.Context, regInfo *RegistryRequestBaseInfo,
registryName string, principal types.Principal, artifactName string, versionName string,
) error {
@ -105,22 +164,54 @@ func (c *APIController) deleteTagWithAudit(
regInfo.PackageType, artifactName, c.URLProvider)
c.ArtifactEventReporter.ArtifactDeleted(ctx, &payload)
}
auditErr := c.AuditService.Log(
ctx,
principal,
audit.NewResource(audit.ResourceTypeRegistry, artifactName),
audit.ActionDeleted,
regInfo.ParentRef,
audit.WithData("registry name", registryName),
audit.WithData("artifact name", artifactName),
audit.WithData("version name", versionName),
)
if auditErr != nil {
log.Ctx(ctx).Warn().Msgf("failed to insert audit log for delete tag operation: %s", auditErr)
}
return err
}
func (c *APIController) deleteVersion(
ctx context.Context,
regInfo *RegistryRequestBaseInfo,
artifactName string,
versionName string,
) error {
// get the file path based on package type
filePath, err := utils.GetFilePath(regInfo.PackageType, artifactName, versionName)
if err != nil {
return fmt.Errorf("failed to get file path: %w", err)
}
err = c.tx.WithTx(
ctx,
func(ctx context.Context) error {
// delete nodes from nodes store
err = c.fileManager.DeleteNode(ctx, regInfo.RegistryID, filePath)
if err != nil {
return err
}
// delete artifacts from artifacts store
err = c.ArtifactStore.DeleteByVersionAndImageName(ctx, artifactName, versionName, regInfo.RegistryID)
if err != nil {
return fmt.Errorf("failed to delete version: %w", err)
}
// delete image if no other artifacts linked
err = c.ImageStore.DeleteByImageNameIfNoLinkedArtifacts(ctx, regInfo.RegistryID, artifactName)
if err != nil {
return fmt.Errorf("failed to delete image: %w", err)
}
return nil
},
)
if err != nil {
return err
}
return nil
}
func throwDeleteArtifactVersion500Error(err error) artifact.DeleteArtifactVersion500JSONResponse {
return artifact.DeleteArtifactVersion500JSONResponse{
InternalServerErrorJSONResponse: artifact.InternalServerErrorJSONResponse(

View File

@ -0,0 +1,65 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package utils
import (
"fmt"
"strings"
"github.com/harness/gitness/registry/app/api/openapi/contracts/artifact"
)
func GetMavenFilePath(imageName string, version string) string {
artifactName := strings.ReplaceAll(imageName, ".", "/")
artifactName = strings.ReplaceAll(artifactName, ":", "/")
filePathPrefix := "/" + artifactName
if version != "" {
filePathPrefix += "/" + version
}
return filePathPrefix
}
func GetGenericFilePath(imageName string, version string) string {
filePathPrefix := "/" + imageName
if version != "" {
filePathPrefix += "/" + version
}
return filePathPrefix
}
func GetFilePath(
packageType artifact.PackageType,
imageName string, version string) (string, error) {
switch packageType {
case artifact.PackageTypeDOCKER:
return "", fmt.Errorf("docker package type not supported")
case artifact.PackageTypeHELM:
return "", fmt.Errorf("helm package type not supported")
case artifact.PackageTypeNPM:
return GetGenericFilePath(imageName, version), nil
case artifact.PackageTypeMAVEN:
return GetMavenFilePath(imageName, version), nil
case artifact.PackageTypePYTHON:
return GetGenericFilePath(imageName, version), nil
case artifact.PackageTypeGENERIC:
return GetGenericFilePath(imageName, version), nil
case artifact.PackageTypeNUGET:
return "", fmt.Errorf("nuget package type not supported")
case artifact.PackageTypeRPM:
return "", fmt.Errorf("rpm package type not supported")
default:
return "", fmt.Errorf("unsupported package type: %s", packageType)
}
}

View File

@ -255,12 +255,15 @@ func (f *FileManager) DownloadFile(
return reader, blob.Size, "", nil
}
func (f *FileManager) DeleteFile(
func (f *FileManager) DeleteNode(
ctx context.Context,
regID int64,
filePath string,
regID int,
) error {
log.Ctx(ctx).Info().Msgf("%s%d", filePath, regID)
err := f.nodesDao.DeleteByNodePathAndRegistryID(ctx, filePath, regID)
if err != nil {
return fmt.Errorf("failed to delete file for path: %s, with error: %w", filePath, err)
}
return nil
}

View File

@ -441,6 +441,7 @@ type ImageRepository interface {
DeleteDownloadStatByRegistryID(ctx context.Context, registryID int64) (err error)
DeleteByImageNameAndRegID(ctx context.Context, regID int64, image string) (err error)
DeleteByImageNameIfNoLinkedArtifacts(ctx context.Context, regID int64, image string) (err error)
}
type ArtifactRepository interface {

View File

@ -185,9 +185,17 @@ func (a ArtifactDao) Count(ctx context.Context) (int64, error) {
}
func (a ArtifactDao) DeleteByImageNameAndRegistryID(ctx context.Context, regID int64, image string) (err error) {
delStmt := databaseg.Builder.Delete("artifacts").
Where("artifact_id IN (SELECT a.artifact_id FROM artifacts a JOIN images i ON i.image_id = a.artifact_image_id"+
" WHERE i.image_name = ? AND i.image_registry_id = ?)", image, regID)
var delStmt sq.DeleteBuilder
switch a.db.DriverName() {
case SQLITE3:
delStmt = databaseg.Builder.Delete("artifacts").
Where("artifact_id IN (SELECT a.artifact_id FROM artifacts a JOIN images i ON i.image_id = a.artifact_image_id"+
" WHERE i.image_name = ? AND i.image_registry_id = ?)", image, regID)
default:
delStmt = databaseg.Builder.Delete("artifacts a USING images i").
Where("a.artifact_image_id = i.image_id").
Where("i.image_name = ? AND i.image_registry_id = ?", image, regID)
}
db := dbtx.GetAccessor(ctx, a.db)
@ -208,9 +216,18 @@ func (a ArtifactDao) DeleteByVersionAndImageName(
ctx context.Context, image string,
version string, regID int64,
) (err error) {
delStmt := databaseg.Builder.Delete("artifacts").
Where("artifact_id IN (SELECT a.artifact_id FROM artifacts a JOIN images i ON i.image_id = a.artifact_image_id"+
" WHERE a.artifact_name = ? AND i.image_name = ? AND i.image_registry_id = ?)", version, image, regID)
var delStmt sq.DeleteBuilder
switch a.db.DriverName() {
case SQLITE3:
delStmt = databaseg.Builder.Delete("artifacts").
Where("artifact_id IN (SELECT a.artifact_id FROM artifacts a JOIN images i ON i.image_id = a.artifact_image_id"+
" WHERE a.artifact_version = ? AND i.image_name = ? AND i.image_registry_id = ?)", version, image, regID)
default:
delStmt = databaseg.Builder.Delete("artifacts a USING images i").
Where("a.artifact_image_id = i.image_id").
Where("a.artifact_version = ? AND i.image_name = ? AND i.image_registry_id = ?", version, image, regID)
}
sql, args, err := delStmt.ToSql()
if err != nil {

View File

@ -217,6 +217,28 @@ func (i ImageDao) DeleteDownloadStatByRegistryID(ctx context.Context, registryID
return nil
}
func (i ImageDao) DeleteByImageNameIfNoLinkedArtifacts(
ctx context.Context, regID int64, image string,
) error {
stmt := databaseg.Builder.Delete("images").
Where("image_name = ? AND image_registry_id = ?", image, regID).
Where("NOT EXISTS ( SELECT 1 FROM artifacts WHERE artifacts.artifact_image_id = images.image_id )")
sql, args, err := stmt.ToSql()
if err != nil {
return errors.Wrap(err, "Failed to convert query to sql")
}
db := dbtx.GetAccessor(ctx, i.db)
_, err = db.ExecContext(ctx, sql, args...)
if err != nil {
return databaseg.ProcessSQLErrorf(ctx, err, "the delete query failed")
}
return nil
}
func (i ImageDao) GetByName(ctx context.Context, registryID int64, name string) (*types.Image, error) {
q := databaseg.Builder.Select(util.ArrToStringByDelimiter(util.GetDBTagsFromStruct(imageDB{}), ",")).
From("images").