diff --git a/.mockery.yaml b/.mockery.yaml index 04ecf7655..34b0ebccf 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -60,6 +60,6 @@ packages: TemplateDao: config: filename: "templates_mock.go" - ModuleStreamsDao: + ModuleStreamDao: config: filename: "modules_streams_mock.go" diff --git a/db/migrations.latest b/db/migrations.latest index d33ce6d06..636f3f3bf 100644 --- a/db/migrations.latest +++ b/db/migrations.latest @@ -1 +1 @@ -20241203143614 +20250107150808 diff --git a/db/migrations/20250107150808_AddModuleStreams.down.sql b/db/migrations/20250107150808_AddModuleStreams.down.sql new file mode 100644 index 000000000..cc402ca04 --- /dev/null +++ b/db/migrations/20250107150808_AddModuleStreams.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS module_streams, repositories_module_streams; + +COMMIT; diff --git a/db/migrations/20250107150808_AddModuleStreams.up.sql b/db/migrations/20250107150808_AddModuleStreams.up.sql new file mode 100644 index 000000000..f8884ae5f --- /dev/null +++ b/db/migrations/20250107150808_AddModuleStreams.up.sql @@ -0,0 +1,48 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS module_streams ( + uuid UUID UNIQUE NOT NULL PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE, + updated_at TIMESTAMP WITH TIME ZONE, + name text NOT NULL, + stream text NOT NULL, + version text NOT NULL, + context text NOT NULL, + arch text NOT NULL, + summary text NOT NULL, + description text NOT NULL, + package_names text[] NOT NULL, + packages text[] NOT NULL, + hash_value text NOT NULL, + profiles jsonb NOT NULL DEFAULT '{}'::jsonb +); + +CREATE TABLE IF NOT EXISTS repositories_module_streams ( + repository_uuid UUID NOT NULL, + module_stream_uuid UUID NOT NULL +); + +CREATE INDEX IF NOT EXISTS module_streams_pkgs_idx ON module_streams USING GIN (package_names); +CREATE INDEX IF NOT EXISTS module_streams_name_idx ON module_streams (uuid, name); + +ALTER TABLE ONLY repositories_module_streams +DROP CONSTRAINT IF EXISTS repositories_module_streams_pkey, +ADD CONSTRAINT repositories_module_streams_pkey PRIMARY KEY (repository_uuid, module_stream_uuid); + +ALTER TABLE ONLY repositories_module_streams +DROP CONSTRAINT IF EXISTS fk_repositories_module_streams_mstream, +ADD CONSTRAINT fk_repositories_module_streams_mstream +FOREIGN KEY (module_stream_uuid) REFERENCES module_streams(uuid) +ON DELETE CASCADE; + +ALTER TABLE ONLY repositories_module_streams +DROP CONSTRAINT IF EXISTS fk_repositories_module_streams_repository, +ADD CONSTRAINT fk_repositories_module_streams_repository +FOREIGN KEY (repository_uuid) REFERENCES repositories(uuid) +ON DELETE CASCADE; + +ALTER TABLE ONLY module_streams +DROP CONSTRAINT IF EXISTS fk_module_streams_uniq, +ADD CONSTRAINT fk_module_streams_uniq UNIQUE (hash_value); + +COMMIT; diff --git a/go.mod b/go.mod index 09b28b83f..2c50b8a4a 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/content-services/content-sources-backend go 1.22.7 + +replace github.com/content-services/yummy => /home/jlsherri/git/yummy/ + require ( github.com/ProtonMail/go-crypto v1.1.3 github.com/content-services/lecho/v3 v3.5.2 diff --git a/pkg/api/module_streams.go b/pkg/api/module_streams.go index fcea1c116..00d44f417 100644 --- a/pkg/api/module_streams.go +++ b/pkg/api/module_streams.go @@ -7,6 +7,14 @@ type SearchSnapshotModuleStreamsRequest struct { Search string `json:"search"` // Search string to search module names } +type SearchModuleStreamsRequest struct { + UUIDs []string `json:"uuids" validate:"required"` // List of repository UUIDs to search + URLs []string `json:"urls" validate:"required"` // List of repository URLs to search + RpmNames []string `json:"rpm_names" validate:"required"` // List of rpm names to search + SortBy string `json:"sort_by"` // SortBy sets the sort order of the result + Search string `json:"search"` // Search string to search rpm names +} + type Stream struct { Name string `json:"name"` // Name of the module Stream string `json:"stream"` // Module stream version diff --git a/pkg/dao/interfaces.go b/pkg/dao/interfaces.go index 69ccc007d..e2ad66120 100644 --- a/pkg/dao/interfaces.go +++ b/pkg/dao/interfaces.go @@ -22,10 +22,10 @@ type DaoRegistry struct { AdminTask AdminTaskDao Domain DomainDao PackageGroup PackageGroupDao + ModuleStream ModuleStreamDao Environment EnvironmentDao Template TemplateDao Uploads UploadDao - ModuleStreams ModuleStreamsDao } func GetDaoRegistry(db *gorm.DB) *DaoRegistry { @@ -38,9 +38,9 @@ func GetDaoRegistry(db *gorm.DB) *DaoRegistry { Rpm: &rpmDaoImpl{ db: db, }, - ModuleStreams: &moduleStreamsImpl{db: db}, - Repository: repositoryDaoImpl{db: db}, - Metrics: metricsDaoImpl{db: db}, + ModuleStream: &moduleStreamsImpl{db: db}, + Repository: repositoryDaoImpl{db: db}, + Metrics: metricsDaoImpl{db: db}, Snapshot: &snapshotDaoImpl{ db: db, pulpClient: pulp_client.GetPulpClientWithDomain(""), @@ -82,8 +82,11 @@ type RepositoryConfigDao interface { BulkImport(ctx context.Context, reposToImport []api.RepositoryRequest) ([]api.RepositoryImportResponse, []error) } -type ModuleStreamsDao interface { +type ModuleStreamDao interface { + SearchRepositoryModuleStreams(ctx context.Context, orgID string, request api.SearchModuleStreamsRequest) ([]api.SearchModuleStreams, error) SearchSnapshotModuleStreams(ctx context.Context, orgID string, request api.SearchSnapshotModuleStreamsRequest) ([]api.SearchModuleStreams, error) + InsertForRepository(ctx context.Context, repoUuid string, pkgGroups []yum.ModuleMD) (int64, error) + OrphanCleanup(ctx context.Context) error } type RpmDao interface { diff --git a/pkg/dao/module_streams.go b/pkg/dao/module_streams.go index 898c13f60..9855247f6 100644 --- a/pkg/dao/module_streams.go +++ b/pkg/dao/module_streams.go @@ -3,23 +3,115 @@ package dao import ( "context" "fmt" + "strings" "github.com/content-services/content-sources-backend/pkg/api" "github.com/content-services/content-sources-backend/pkg/config" ce "github.com/content-services/content-sources-backend/pkg/errors" + "github.com/content-services/content-sources-backend/pkg/models" "github.com/content-services/tang/pkg/tangy" + "github.com/content-services/yummy/pkg/yum" + "github.com/lib/pq" + "golang.org/x/exp/slices" "gorm.io/gorm" + "gorm.io/gorm/clause" ) +func GetModuleStreamsDao(db *gorm.DB) ModuleStreamDao { + // Return DAO instance + return &moduleStreamsImpl{db: db} +} + type moduleStreamsImpl struct { db *gorm.DB } -func GetModuleStreamsDao(db *gorm.DB) ModuleStreamsDao { - // Return DAO instance - return &moduleStreamsImpl{ - db: db, +func (r *moduleStreamsImpl) SearchRepositoryModuleStreams(ctx context.Context, orgID string, request api.SearchModuleStreamsRequest) (resp []api.SearchModuleStreams, err error) { + if orgID == "" { + return resp, fmt.Errorf("orgID can not be an empty string") + } + dbWithCtx := r.db.WithContext(ctx) + if request.RpmNames == nil { + request.RpmNames = []string{} + } + if len(request.UUIDs) == 0 && len(request.URLs) == 0 { + return resp, &ce.DaoError{ + BadValidation: true, + Message: "must contain at least 1 Repository UUID or URL", + } + } + + uuids := []string{} + if request.UUIDs != nil { + uuids = request.UUIDs + } + + urls := []string{} + for _, url := range request.URLs { + url = models.CleanupURL(url) + urls = append(urls, url) + } + + uuidsValid, urlsValid, uuid, url := checkForValidRepoUuidsUrls(ctx, uuids, urls, r.db) + if !uuidsValid { + return resp, &ce.DaoError{ + NotFound: true, + Message: "Could not find repository with UUID: " + uuid, + } + } + if !urlsValid { + return resp, &ce.DaoError{ + NotFound: true, + Message: "Could not find repository with URL: " + url, + } + } + + streams := []models.ModuleStream{} + + newestStreams := dbWithCtx.Model(&models.ModuleStream{}). + Select("DISTINCT ON (name, stream) uuid"). + Joins("inner join repositories_module_streams on module_streams.uuid = repositories_module_streams.module_stream_uuid"). + Where("repositories_module_streams.repository_uuid in (?)", readableRepositoryQuery(dbWithCtx, orgID, urls, uuids)) + + if len(request.RpmNames) > 0 { + // we are checking if two arrays have things in common, so we have to conver to pq array type + newestStreams = newestStreams.Where("module_streams.package_names && ?", pq.Array(request.RpmNames)) + } + if request.Search != "" { + newestStreams = newestStreams.Where("module_streams.name ilike ?", fmt.Sprintf("%%%s%%", request.Search)) + } + newestStreams = newestStreams.Order("name, stream, version DESC") + + order := convertSortByToSQL(request.SortBy, map[string]string{"name": "name"}, "name asc") + result := dbWithCtx.Model(&models.ModuleStream{}).Where("uuid in (?)", newestStreams).Order(fmt.Sprintf("%v, stream", order)).Find(&streams) + + if result.Error != nil { + return resp, result.Error + } + return ModuleStreamsToCollectionResponse(streams), nil +} + +func ModuleStreamsToCollectionResponse(modules []models.ModuleStream) (response []api.SearchModuleStreams) { + mapping := make(map[string][]api.Stream) + for _, mod := range modules { + mapping[mod.Name] = append(mapping[mod.Name], api.Stream{ + Name: mod.Name, + Stream: mod.Stream, + Context: mod.Context, + Arch: mod.Arch, + Version: mod.Version, + Description: mod.Description, + Profiles: mod.Profiles, + }) + } + + for k, v := range mapping { + response = append(response, api.SearchModuleStreams{ + ModuleName: k, + Streams: v, + }) } + return response } func (r *moduleStreamsImpl) SearchSnapshotModuleStreams(ctx context.Context, orgID string, request api.SearchSnapshotModuleStreamsRequest) ([]api.SearchModuleStreams, error) { @@ -95,3 +187,138 @@ func (r *moduleStreamsImpl) SearchSnapshotModuleStreams(ctx context.Context, org return response, nil } + +func (r moduleStreamsImpl) fetchRepo(ctx context.Context, uuid string) (models.Repository, error) { + found := models.Repository{} + if err := r.db.WithContext(ctx). + Where("UUID = ?", uuid). + First(&found). + Error; err != nil { + return found, err + } + return found, nil +} + +// Converts an rpm NVREA into just the name +func extractRpmName(nvrea string) string { + // rubygem-bson-debugsource-0:4.3.0-2.module+el8.1.0+3656+f80bfa1d.x86_64 + split := strings.Split(nvrea, "-") + if len(split) < 3 { + return nvrea + } + split = split[0 : len(split)-2] + return strings.Join(split, "-") +} + +func ModuleMdToModuleStreams(moduleMds []yum.ModuleMD) (moduleStreams []models.ModuleStream) { + for _, m := range moduleMds { + mStream := models.ModuleStream{ + Name: m.Data.Name, + Stream: m.Data.Stream, + Version: m.Data.Version, + Context: m.Data.Context, + Arch: m.Data.Arch, + Summary: m.Data.Summary, + Description: m.Data.Description, + Profiles: map[string][]string{}, + PackageNames: []string{}, + Packages: m.Data.Artifacts.Rpms, + } + for _, p := range m.Data.Artifacts.Rpms { + mStream.PackageNames = append(mStream.PackageNames, extractRpmName(p)) + } + slices.Sort(mStream.PackageNames) // Sort the package names so the hash is consistent + mStream.HashValue = generateHash(mStream.ToHashString()) + for pName, p := range m.Data.Profiles { + mStream.Profiles[pName] = p.Rpms + } + + moduleStreams = append(moduleStreams, mStream) + } + return moduleStreams +} + +// InsertForRepository inserts a set of yum module streams for a given repository +// and removes any that are not in the list. This will involve inserting the package groups +// if not present, and adding or removing any associations to the Repository +// Returns a count of new package groups added to the system (not the repo), as well as any error +func (r moduleStreamsImpl) InsertForRepository(ctx context.Context, repoUuid string, modules []yum.ModuleMD) (int64, error) { + var ( + err error + repo models.Repository + ) + ctxDb := r.db.WithContext(ctx) + + // Retrieve Repository record + if repo, err = r.fetchRepo(ctx, repoUuid); err != nil { + return 0, fmt.Errorf("failed to fetchRepo: %w", err) + } + + moduleStreams := ModuleMdToModuleStreams(modules) + + err = ctxDb.Model(&models.ModuleStream{}).Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "hash_value"}}, + DoNothing: true}). + Create(moduleStreams).Error + if err != nil { + return 0, fmt.Errorf("failed to insert module streams: %w", err) + } + + hashes := make([]string, len(moduleStreams)) + for _, m := range moduleStreams { + hashes = append(hashes, m.HashValue) + } + uuids := make([]string, len(moduleStreams)) + + // insert any modules streams, ignoring any hash conflicts + if err = r.db.WithContext(ctx). + Where("hash_value in (?)", hashes). + Model(&models.ModuleStream{}). + Pluck("uuid", &uuids).Error; err != nil { + return 0, fmt.Errorf("failed retrieving existing ids in module_streams: %w", err) + } + + // Delete repository module stream entries not needed + err = r.deleteUnneeded(ctx, repo, uuids) + if err != nil { + return 0, fmt.Errorf("failed to delete unneeded module streams: %w", err) + } + + // Add any needed repo module stream entries + repoModStreams := make([]models.RepositoryModuleStream, len(moduleStreams)) + for i, uuid := range uuids { + repoModStreams[i] = models.RepositoryModuleStream{ + RepositoryUUID: repo.UUID, + ModuleStreamUUID: uuid, + } + } + err = ctxDb.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "repository_uuid"}, {Name: "module_stream_uuid"}}, + DoNothing: true}). + Create(repoModStreams).Error + if err != nil { + return 0, fmt.Errorf("failed to insert repo module streams: %w", err) + } + return int64(len(repoModStreams)), nil +} + +// deleteUnneeded removes any RepositoryPackageGroup entries that are not in the list of package_group_uuids +func (r moduleStreamsImpl) deleteUnneeded(ctx context.Context, repo models.Repository, moduleStreamUUIDs []string) error { + if err := r.db.WithContext(ctx).Model(&models.RepositoryModuleStream{}). + Where("repository_uuid = ?", repo.UUID). + Where("module_stream_uuid NOT IN (?)", moduleStreamUUIDs). + Error; err != nil { + return err + } + return nil +} + +func (r moduleStreamsImpl) OrphanCleanup(ctx context.Context) error { + if err := r.db.WithContext(ctx). + Model(&models.ModuleStream{}). + Where("NOT EXISTS (select from repositories_module_streams where module_streams.uuid = repositories_module_streams.module_stream_uuid )"). + Delete(&models.ModuleStream{}).Error; err != nil { + return err + } + return nil +} diff --git a/pkg/dao/module_streams_test.go b/pkg/dao/module_streams_test.go index 8e250a33a..0805c56df 100644 --- a/pkg/dao/module_streams_test.go +++ b/pkg/dao/module_streams_test.go @@ -9,6 +9,8 @@ import ( "github.com/content-services/content-sources-backend/pkg/models" "github.com/content-services/content-sources-backend/pkg/seeds" "github.com/content-services/tang/pkg/tangy" + "github.com/content-services/yummy/pkg/yum" + "github.com/labstack/gommon/random" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -78,7 +80,7 @@ func (s *RpmSuite) TestSearchModulesForSnapshots() { require.NoError(s.T(), res.Error) // pulpHrefs, request.Search, *request.Limit) mTangy.On("RpmRepositoryVersionModuleStreamsList", ctx, hrefs, tangy.ModuleStreamListFilters{Search: "Foo", RpmNames: []string{}}, "").Return(expected, nil) - //ctx context.Context, hrefs []string, rpmNames []string, search string, pageOpts PageOption + // ctx context.Context, hrefs []string, rpmNames []string, search string, pageOpts PageOption dao := GetModuleStreamsDao(s.tx) resp, err := dao.SearchSnapshotModuleStreams(ctx, orgId, api.SearchSnapshotModuleStreamsRequest{ @@ -111,3 +113,190 @@ func (s *RpmSuite) TestSearchModulesForSnapshots() { assert.Error(s.T(), err) } + +func testYumModuleMD() yum.ModuleMD { + return yum.ModuleMD{ + Document: "", + Version: 0, + Data: yum.Stream{ + Name: "myModule", + Stream: "myStream", + Version: "Version", + Context: "lksdfoisdjf", + Arch: "x86_64", + Summary: "something short", + Description: "something long", + Profiles: map[string]yum.RpmProfiles{"common": {Rpms: []string{"foo"}}}, + Artifacts: yum.Artifacts{Rpms: []string{"ruby-0:2.5.5-106.module+el8.3.0+7153+c6f6daa5.i686", + "ruby-irb-0:2.5.5-106.module+el8.3.0+7153+c6f6daa5.noarch"}}, + }, + } +} + +func (s *ModuleStreamSuite) TestSearchRepositoryModuleStreams() { + t := s.Suite.T() + tx := s.tx + var err error + dao := GetModuleStreamsDao(tx) + + alpha1 := genModule("alpha", "1.0", "123") + alpha2 := genModule("alpha", "1.0", "124") + alphaNew := genModule("alpha", "1.1", "126") + unrel := genModule("unrelated", "1.1", "123") + beta1 := genModule("beta", "1.1", "123") + + err = tx.Create([]*models.ModuleStream{&alpha1, &alpha2, &beta1, &alphaNew, &unrel}).Error + require.NoError(t, err) + err = tx.Create([]models.RepositoryModuleStream{ + {RepositoryUUID: s.repo.UUID, ModuleStreamUUID: alpha1.UUID}, + {RepositoryUUID: s.repo.UUID, ModuleStreamUUID: alpha2.UUID}, + {RepositoryUUID: s.repo.UUID, ModuleStreamUUID: alphaNew.UUID}, + {RepositoryUUID: s.repo.UUID, ModuleStreamUUID: beta1.UUID}, + }).Error + require.NoError(t, err) + + resp, err := dao.SearchRepositoryModuleStreams(context.Background(), s.repoConfig.OrgID, api.SearchModuleStreamsRequest{ + UUIDs: []string{s.repoConfig.UUID}, + }) + assert.NoError(t, err) + + // 2 modules in total, 3rd isn't in the repo + assert.Equal(t, 2, len(resp)) + + // alpha module has 2 streams + assert.Equal(t, alpha2.Name, resp[0].ModuleName) + assert.Equal(t, 2, len(resp[0].Streams)) + assert.Equal(t, alpha2.Version, resp[0].Streams[0].Version) + assert.Equal(t, alphaNew.Version, resp[0].Streams[1].Version) + + // only 1 stream for beta module + assert.Equal(t, 1, len(resp[1].Streams)) + assert.Equal(t, beta1.Name, resp[1].ModuleName) + assert.Equal(t, beta1.Version, resp[1].Streams[0].Version) + + // reverse order + resp, err = dao.SearchRepositoryModuleStreams(context.Background(), s.repoConfig.OrgID, api.SearchModuleStreamsRequest{ + UUIDs: []string{s.repoConfig.UUID}, + SortBy: "name:desc", + }) + assert.NoError(t, err) + assert.Equal(t, beta1.Name, resp[0].ModuleName) // beta comes first + assert.Equal(t, alpha2.Name, resp[1].ModuleName) + + // URL + resp, err = dao.SearchRepositoryModuleStreams(context.Background(), s.repoConfig.OrgID, api.SearchModuleStreamsRequest{ + URLs: []string{s.repo.URL}, + }) + assert.NoError(t, err) + assert.Equal(t, 2, len(resp)) + + // test rpm name search + resp, err = dao.SearchRepositoryModuleStreams(context.Background(), s.repoConfig.OrgID, api.SearchModuleStreamsRequest{ + UUIDs: []string{s.repoConfig.UUID}, + RpmNames: []string{alpha1.PackageNames[0], alpha2.PackageNames[0]}, + }) + assert.NoError(t, err) + assert.Equal(t, 1, len(resp)) + assert.Equal(t, alpha2.Name, resp[0].ModuleName) +} + +func genModule(name string, stream string, version string) models.ModuleStream { + return models.ModuleStream{ + Name: name, + Stream: stream, + Version: version, + Context: "context " + random.String(5), + Arch: "x86_64", + Summary: "summary:" + random.String(5), + Description: "desc:" + random.String(5), + PackageNames: []string{random.String(5), random.String(5)}, + HashValue: random.String(10), + } +} + +func (s *ModuleStreamSuite) TestInsertForRepository() { + t := s.Suite.T() + tx := s.tx + + mods := []yum.ModuleMD{testYumModuleMD()} + + dao := GetModuleStreamsDao(tx) + cnt, err := dao.InsertForRepository(context.Background(), s.repo.UUID, mods) + assert.NoError(t, err) + assert.Equal(t, int64(1), cnt) + + created := models.ModuleStream{} + res := tx.Where("context = ?", mods[0].Data.Context).Find(&created) + assert.NoError(t, res.Error) + assert.NotEmpty(t, created.UUID) + assert.Equal(t, created.PackageNames[0], "ruby") + assert.Equal(t, created.PackageNames[1], "ruby-irb") + + pkgs, ok := created.Profiles["common"] + assert.True(t, ok) + assert.Len(t, pkgs, 1) + assert.Equal(t, created.PackageNames[0], "ruby") + + assert.Len(t, created.Packages, 2) + + // re-run and expect only 1 + cnt, err = dao.InsertForRepository(context.Background(), s.repo.UUID, mods) + assert.NoError(t, err) + assert.Equal(t, int64(1), cnt) + var count int64 + res = tx.Model(&models.ModuleStream{}).Where("context = ?", mods[0].Data.Context).Count(&count) + assert.NoError(t, res.Error) + assert.Equal(t, int64(1), count) +} + +func (s *ModuleStreamSuite) TestOrphanCleanup() { + mod1 := models.ModuleStream{ + Name: "mod1", + Stream: "mod1", + Version: "123", + Context: "mod1", + Arch: "mod1", + Summary: "mod1", + Description: "mod1", + PackageNames: []string{"foo1"}, + HashValue: random.String(10), + Repositories: nil, + } + mod2 := models.ModuleStream{ + Name: "mod2", + Stream: "mod2", + Version: "123", + Context: "mod2", + Arch: "mod2", + Summary: "mod2", + Description: "mod12", + PackageNames: []string{"foo2"}, + HashValue: random.String(10), + Repositories: nil, + } + + require.NoError(s.T(), s.tx.Create(&mod1).Error) + require.NoError(s.T(), s.tx.Create(&mod2).Error) + + repos, err := seeds.SeedRepositoryConfigurations(s.tx, 1, seeds.SeedOptions{}) + require.NoError(s.T(), err) + repo := repos[0] + + err = s.tx.Create(&models.RepositoryModuleStream{ + RepositoryUUID: repo.RepositoryUUID, + ModuleStreamUUID: mod1.UUID, + }).Error + require.NoError(s.T(), err) + + dao := GetModuleStreamsDao(s.tx) + err = dao.OrphanCleanup(context.Background()) + require.NoError(s.T(), err) + + // verify mod1 exists and mod2 doesn't + mods := []models.ModuleStream{} + err = s.tx.Where("uuid in (?)", []string{mod1.UUID, mod2.UUID}).Find(&mods).Error + require.NoError(s.T(), err) + + assert.Len(s.T(), mods, 1) + assert.Equal(s.T(), mod1.UUID, mods[0].UUID) +} diff --git a/pkg/dao/modules_streams_mock.go b/pkg/dao/modules_streams_mock.go index 4ac3d3f18..ce82c7ab3 100644 --- a/pkg/dao/modules_streams_mock.go +++ b/pkg/dao/modules_streams_mock.go @@ -8,15 +8,93 @@ import ( api "github.com/content-services/content-sources-backend/pkg/api" mock "github.com/stretchr/testify/mock" + + yum "github.com/content-services/yummy/pkg/yum" ) -// MockModuleStreamsDao is an autogenerated mock type for the ModuleStreamsDao type -type MockModuleStreamsDao struct { +// MockModuleStreamDao is an autogenerated mock type for the ModuleStreamDao type +type MockModuleStreamDao struct { mock.Mock } +// InsertForRepository provides a mock function with given fields: ctx, repoUuid, pkgGroups +func (_m *MockModuleStreamDao) InsertForRepository(ctx context.Context, repoUuid string, pkgGroups []yum.ModuleMD) (int64, error) { + ret := _m.Called(ctx, repoUuid, pkgGroups) + + if len(ret) == 0 { + panic("no return value specified for InsertForRepository") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []yum.ModuleMD) (int64, error)); ok { + return rf(ctx, repoUuid, pkgGroups) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []yum.ModuleMD) int64); ok { + r0 = rf(ctx, repoUuid, pkgGroups) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []yum.ModuleMD) error); ok { + r1 = rf(ctx, repoUuid, pkgGroups) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OrphanCleanup provides a mock function with given fields: ctx +func (_m *MockModuleStreamDao) OrphanCleanup(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for OrphanCleanup") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SearchRepositoryModuleStreams provides a mock function with given fields: ctx, orgID, request +func (_m *MockModuleStreamDao) SearchRepositoryModuleStreams(ctx context.Context, orgID string, request api.SearchModuleStreamsRequest) ([]api.SearchModuleStreams, error) { + ret := _m.Called(ctx, orgID, request) + + if len(ret) == 0 { + panic("no return value specified for SearchRepositoryModuleStreams") + } + + var r0 []api.SearchModuleStreams + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, api.SearchModuleStreamsRequest) ([]api.SearchModuleStreams, error)); ok { + return rf(ctx, orgID, request) + } + if rf, ok := ret.Get(0).(func(context.Context, string, api.SearchModuleStreamsRequest) []api.SearchModuleStreams); ok { + r0 = rf(ctx, orgID, request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]api.SearchModuleStreams) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, api.SearchModuleStreamsRequest) error); ok { + r1 = rf(ctx, orgID, request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SearchSnapshotModuleStreams provides a mock function with given fields: ctx, orgID, request -func (_m *MockModuleStreamsDao) SearchSnapshotModuleStreams(ctx context.Context, orgID string, request api.SearchSnapshotModuleStreamsRequest) ([]api.SearchModuleStreams, error) { +func (_m *MockModuleStreamDao) SearchSnapshotModuleStreams(ctx context.Context, orgID string, request api.SearchSnapshotModuleStreamsRequest) ([]api.SearchModuleStreams, error) { ret := _m.Called(ctx, orgID, request) if len(ret) == 0 { @@ -45,13 +123,13 @@ func (_m *MockModuleStreamsDao) SearchSnapshotModuleStreams(ctx context.Context, return r0, r1 } -// NewMockModuleStreamsDao creates a new instance of MockModuleStreamsDao. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockModuleStreamDao creates a new instance of MockModuleStreamDao. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockModuleStreamsDao(t interface { +func NewMockModuleStreamDao(t interface { mock.TestingT Cleanup(func()) -}) *MockModuleStreamsDao { - mock := &MockModuleStreamsDao{} +}) *MockModuleStreamDao { + mock := &MockModuleStreamDao{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/pkg/dao/registry_mock.go b/pkg/dao/registry_mock.go index 2d1559588..7b8766f25 100644 --- a/pkg/dao/registry_mock.go +++ b/pkg/dao/registry_mock.go @@ -16,7 +16,7 @@ type MockDaoRegistry struct { PackageGroup MockPackageGroupDao Environment MockEnvironmentDao Template MockTemplateDao - ModuleStreams MockModuleStreamsDao + ModuleStream MockModuleStreamDao } func (m *MockDaoRegistry) ToDaoRegistry() *DaoRegistry { @@ -32,7 +32,7 @@ func (m *MockDaoRegistry) ToDaoRegistry() *DaoRegistry { PackageGroup: &m.PackageGroup, Environment: &m.Environment, Template: &m.Template, - ModuleStreams: &m.ModuleStreams, + ModuleStream: &m.ModuleStream, } return &r } @@ -50,7 +50,7 @@ func GetMockDaoRegistry(t *testing.T) *MockDaoRegistry { PackageGroup: *NewMockPackageGroupDao(t), Environment: *NewMockEnvironmentDao(t), Template: *NewMockTemplateDao(t), - ModuleStreams: *NewMockModuleStreamsDao(t), + ModuleStream: *NewMockModuleStreamDao(t), } return ® } diff --git a/pkg/dao/repositories_test.go b/pkg/dao/repositories_test.go index fbd7edf7d..cd560624e 100644 --- a/pkg/dao/repositories_test.go +++ b/pkg/dao/repositories_test.go @@ -18,7 +18,7 @@ import ( "github.com/stretchr/testify/suite" ) -const testDeleteTablesQuery = `TRUNCATE repositories, snapshots, repositories_rpms, repositories_package_groups, repositories_environments, repository_configurations, templates_repository_configurations` +const testDeleteTablesQuery = `TRUNCATE repositories, snapshots, repositories_module_streams, repositories_rpms, repositories_package_groups, repositories_environments, repository_configurations, templates_repository_configurations` type RepositorySuite struct { *DaoSuite @@ -119,14 +119,15 @@ func (s *RepositorySuite) TestListPublic() { t := s.T() tx.SavePoint("testlistpublic") - tx.Exec(testDeleteTablesQuery) + err := tx.Exec(testDeleteTablesQuery).Error + require.NoError(t, err) dao := GetRepositoryDao(tx) pageData := api.PaginationData{ Limit: 100, Offset: 0, } - err := tx.Create(s.repo).Error + err = tx.Create(s.repo).Error require.NoError(t, err) err = tx.Create(s.repoPrivate).Error require.NoError(t, err) diff --git a/pkg/dao/repository_configs_mock.go b/pkg/dao/repository_configs_mock.go index 858f68515..660cda679 100644 --- a/pkg/dao/repository_configs_mock.go +++ b/pkg/dao/repository_configs_mock.go @@ -6,8 +6,10 @@ import ( context "context" api "github.com/content-services/content-sources-backend/pkg/api" - models "github.com/content-services/content-sources-backend/pkg/models" + mock "github.com/stretchr/testify/mock" + + models "github.com/content-services/content-sources-backend/pkg/models" ) // MockRepositoryConfigDao is an autogenerated mock type for the RepositoryConfigDao type @@ -309,36 +311,6 @@ func (_m *MockRepositoryConfigDao) InternalOnly_ListReposToSnapshot(ctx context. return r0, r1 } -// InternalOnly_ListReposWithOutdatedSnapshots provides a mock function with given fields: ctx, olderThanDays -func (_m *MockRepositoryConfigDao) ListReposWithOutdatedSnapshots(ctx context.Context, olderThanDays int) ([]models.RepositoryConfiguration, error) { - ret := _m.Called(ctx, olderThanDays) - - if len(ret) == 0 { - panic("no return value specified for ListReposWithOutdatedSnapshots") - } - - var r0 []models.RepositoryConfiguration - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, int) ([]models.RepositoryConfiguration, error)); ok { - return rf(ctx, olderThanDays) - } - if rf, ok := ret.Get(0).(func(context.Context, int) []models.RepositoryConfiguration); ok { - r0 = rf(ctx, olderThanDays) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]models.RepositoryConfiguration) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { - r1 = rf(ctx, olderThanDays) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // InternalOnly_RefreshRedHatRepo provides a mock function with given fields: ctx, request, label func (_m *MockRepositoryConfigDao) InternalOnly_RefreshRedHatRepo(ctx context.Context, request api.RepositoryRequest, label string) (*api.RepositoryResponse, error) { ret := _m.Called(ctx, request, label) @@ -404,6 +376,36 @@ func (_m *MockRepositoryConfigDao) List(ctx context.Context, orgID string, pagin return r0, r1, r2 } +// ListReposWithOutdatedSnapshots provides a mock function with given fields: ctx, olderThanDays +func (_m *MockRepositoryConfigDao) ListReposWithOutdatedSnapshots(ctx context.Context, olderThanDays int) ([]models.RepositoryConfiguration, error) { + ret := _m.Called(ctx, olderThanDays) + + if len(ret) == 0 { + panic("no return value specified for ListReposWithOutdatedSnapshots") + } + + var r0 []models.RepositoryConfiguration + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]models.RepositoryConfiguration, error)); ok { + return rf(ctx, olderThanDays) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []models.RepositoryConfiguration); ok { + r0 = rf(ctx, olderThanDays) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]models.RepositoryConfiguration) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, olderThanDays) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SavePublicRepos provides a mock function with given fields: ctx, urls func (_m *MockRepositoryConfigDao) SavePublicRepos(ctx context.Context, urls []string) error { ret := _m.Called(ctx, urls) diff --git a/pkg/dao/rpms.go b/pkg/dao/rpms.go index cb9319fd9..e815b7ebd 100644 --- a/pkg/dao/rpms.go +++ b/pkg/dao/rpms.go @@ -186,13 +186,7 @@ func (r rpmDaoImpl) Search(ctx context.Context, orgID string, request api.Conten } // Lookup repo uuids to search - repoUuids := []string{} - orGroupPublicPrivatePopular := r.db.Where("repository_configurations.org_id = ?", orgID).Or("repositories.public").Or("repositories.url in ?", popularRepoUrls()) - r.db.WithContext(ctx).Model(&models.Repository{}). - Joins("left join repository_configurations on repositories.uuid = repository_configurations.repository_uuid and repository_configurations.org_id = ?", orgID). - Where(orGroupPublicPrivatePopular). - Where(r.db.Where("repositories.url in ?", urls). - Or("repository_configurations.uuid in ?", UuidifyStrings(uuids))).Pluck("repositories.uuid", &repoUuids) + readableRepos := readableRepositoryQuery(r.db.WithContext(ctx), orgID, urls, uuids) // https://github.com/go-gorm/gorm/issues/5318 dataResponse := []api.SearchRpmResponse{} @@ -200,7 +194,7 @@ func (r rpmDaoImpl) Search(ctx context.Context, orgID string, request api.Conten Select("DISTINCT ON(rpms.name) rpms.name as package_name", "rpms.summary"). Table(models.TableNameRpm). Joins("inner join repositories_rpms on repositories_rpms.rpm_uuid = rpms.uuid"). - Where("repositories_rpms.repository_uuid in ?", repoUuids) + Where("repositories_rpms.repository_uuid in (?)", readableRepos) if len(request.ExactNames) != 0 { db = db.Where("rpms.name in (?)", request.ExactNames) @@ -219,6 +213,16 @@ func (r rpmDaoImpl) Search(ctx context.Context, orgID string, request api.Conten return dataResponse, nil } +func readableRepositoryQuery(dbWithContext *gorm.DB, orgID string, urls []string, uuids []string) *gorm.DB { + orGroupPublicPrivatePopular := dbWithContext.Where("repository_configurations.org_id = ?", orgID).Or("repositories.public").Or("repositories.url in ?", popularRepoUrls()) + readableRepos := dbWithContext.Model(&models.Repository{}). + Joins("left join repository_configurations on repositories.uuid = repository_configurations.repository_uuid and repository_configurations.org_id = ?", orgID). + Where(orGroupPublicPrivatePopular). + Where(dbWithContext.Where("repositories.url in ?", urls). + Or("repository_configurations.uuid in ?", UuidifyStrings(uuids))) + return readableRepos.Select("repositories.uuid") +} + func (r *rpmDaoImpl) fetchRepo(ctx context.Context, uuid string) (models.Repository, error) { found := models.Repository{} if err := r.db.WithContext(ctx). diff --git a/pkg/dao/templates_test.go b/pkg/dao/templates_test.go index 79c4b8482..3171bca0c 100644 --- a/pkg/dao/templates_test.go +++ b/pkg/dao/templates_test.go @@ -41,7 +41,7 @@ func (s *TemplateSuite) templateDao() templateDaoImpl { func (s *TemplateSuite) SetupTest() { s.DaoSuite.SetupTest() s.pulpClient = &pulp_client.MockPulpClient{} - s.pulpClient.On("GetContentPath", context.Background()).Return(testContentPath, nil) + s.pulpClient.On("GetContentPath", context.Background()).Return(testContentPath, nil).Maybe() } func (s *TemplateSuite) TestCreate() { templateDao := s.templateDao() diff --git a/pkg/external_repos/introspect.go b/pkg/external_repos/introspect.go index b6ec89fe0..05740a97c 100644 --- a/pkg/external_repos/introspect.go +++ b/pkg/external_repos/introspect.go @@ -78,6 +78,7 @@ func Introspect(ctx context.Context, repo *dao.Repository, dao *dao.DaoRegistry) total int64 repomd *yum.Repomd packages []yum.Package + modMds []yum.ModuleMD packageGroups []yum.PackageGroup environments []yum.Environment ) @@ -130,7 +131,6 @@ func Introspect(ctx context.Context, repo *dao.Repository, dao *dao.DaoRegistry) if packageGroups, _, err = yumRepo.PackageGroups(ctx); err != nil { return 0, err, false } - if _, err = dao.PackageGroup.InsertForRepository(ctx, repo.UUID, packageGroups); err != nil { return 0, err, false } @@ -138,11 +138,17 @@ func Introspect(ctx context.Context, repo *dao.Repository, dao *dao.DaoRegistry) if environments, _, err = yumRepo.Environments(ctx); err != nil { return 0, err, false } - if _, err = dao.Environment.InsertForRepository(ctx, repo.UUID, environments); err != nil { return 0, err, false } + if modMds, _, err = yumRepo.ModuleMDs(ctx); err != nil { + return 0, err, false + } + if _, err = dao.ModuleStream.InsertForRepository(ctx, repo.UUID, modMds); err != nil { + return 0, err, false + } + repo.RepomdChecksum = checksumStr repo.PackageCount = foundCount if err = dao.Repository.Update(ctx, RepoToRepoUpdate(*repo)); err != nil { diff --git a/pkg/external_repos/introspect_test.go b/pkg/external_repos/introspect_test.go index 1ccb73052..a51f4fbcb 100644 --- a/pkg/external_repos/introspect_test.go +++ b/pkg/external_repos/introspect_test.go @@ -122,6 +122,7 @@ func TestIntrospect(t *testing.T) { mockDao.Rpm.On("InsertForRepository", ctx, repoUpdate.UUID, mock.Anything).Return(int64(14), nil) mockDao.PackageGroup.On("InsertForRepository", ctx, repoUpdate.UUID, mock.Anything).Return(int64(1), nil) mockDao.Environment.On("InsertForRepository", ctx, repoUpdate.UUID, mock.Anything).Return(int64(1), nil) + mockDao.ModuleStream.On("InsertForRepository", ctx, repoUpdate.UUID, mock.Anything).Return(int64(1), nil) count, err, updated := Introspect( ctx, diff --git a/pkg/handler/module_streams.go b/pkg/handler/module_streams.go index 9a1c621da..06c1ccd0a 100644 --- a/pkg/handler/module_streams.go +++ b/pkg/handler/module_streams.go @@ -20,6 +20,7 @@ func RegisterModuleStreamsRoutes(engine *echo.Group, rDao *dao.DaoRegistry) { } addRepoRoute(engine, http.MethodPost, "/snapshots/module_streams/search", rh.searchSnapshotModuleStreams, rbac.RbacVerbRead) + addRepoRoute(engine, http.MethodPost, "/module_streams/search", rh.searchRepoModuleStreams, rbac.RbacVerbRead) } // searchSnapshotModuleStreams godoc @@ -45,7 +46,39 @@ func (rh *ModuleStreamsHandler) searchSnapshotModuleStreams(c echo.Context) erro return ce.NewErrorResponse(http.StatusBadRequest, "Error binding parameters", err.Error()) } - apiResponse, err := rh.Dao.ModuleStreams.SearchSnapshotModuleStreams(c.Request().Context(), orgId, dataInput) + apiResponse, err := rh.Dao.ModuleStream.SearchSnapshotModuleStreams(c.Request().Context(), orgId, dataInput) + + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error searching modules streams", err.Error()) + } + + return c.JSON(200, apiResponse) +} + +// searchRepoModuleStreams godoc +// @Summary List modules and their streams for repositories +// @ID searchRepositoryModuleStreams +// @Description List modules and their streams for repositories +// @Tags module_streams +// @Accept json +// @Produce json +// @Param body body api.SearchModuleStreamsRequest true "request body" +// @Success 200 {object} []api.SearchModuleStreams +// @Failure 400 {object} ce.ErrorResponse +// @Failure 401 {object} ce.ErrorResponse +// @Failure 404 {object} ce.ErrorResponse +// @Failure 500 {object} ce.ErrorResponse +// @Router /module_streams/search [post] +func (rh *ModuleStreamsHandler) searchRepoModuleStreams(c echo.Context) error { + _, orgId := getAccountIdOrgId(c) + + dataInput := api.SearchModuleStreamsRequest{} + + if err := c.Bind(&dataInput); err != nil { + return ce.NewErrorResponse(http.StatusBadRequest, "Error binding parameters", err.Error()) + } + + apiResponse, err := rh.Dao.ModuleStream.SearchRepositoryModuleStreams(c.Request().Context(), orgId, dataInput) if err != nil { return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error searching modules streams", err.Error()) diff --git a/pkg/handler/module_streams_test.go b/pkg/handler/module_streams_test.go index de8495e7c..9569e570c 100644 --- a/pkg/handler/module_streams_test.go +++ b/pkg/handler/module_streams_test.go @@ -135,7 +135,96 @@ func (suite *ModuleStreamsSuite) TestSearchSnapshotModuleStreams() { var bodyRequest api.SearchSnapshotModuleStreamsRequest err := json.Unmarshal([]byte(testCase.Given.Body), &bodyRequest) require.NoError(t, err) - suite.dao.ModuleStreams.On("SearchSnapshotModuleStreams", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId, bodyRequest). + suite.dao.ModuleStream.On("SearchSnapshotModuleStreams", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId, bodyRequest). + Return([]api.SearchModuleStreams{}, nil) + } + default: + { + } + } + + var bodyRequest io.Reader + if testCase.Given.Body == "" { + bodyRequest = nil + } else { + bodyRequest = strings.NewReader(testCase.Given.Body) + } + + // Prepare request + req := httptest.NewRequest(testCase.Given.Method, path, bodyRequest) + req.Header.Set(api.IdentityHeader, test_handler.EncodedIdentity(t)) + req.Header.Set("Content-Type", "application/json") + + // Execute the request + code, body, err := suite.serveModuleStreamsRouter(req) + + // Check results + assert.Equal(t, testCase.Expected.Code, code) + require.NoError(t, err) + assert.Equal(t, testCase.Expected.Body, string(body)) + } +} + +func (suite *ModuleStreamsSuite) TestSearchRepoModuleStreams() { + t := suite.T() + + config.Load() + config.Get().Features.Snapshots.Enabled = true + config.Get().Features.Snapshots.Accounts = &[]string{test_handler.MockAccountNumber} + defer resetFeatures() + + type TestCaseExpected struct { + Code int + Body string + } + + type TestCaseGiven struct { + Method string + Body string + } + + type TestCase struct { + Name string + Given TestCaseGiven + Expected TestCaseExpected + } + + var testCases = []TestCase{ + { + Name: "Success scenario", + Given: TestCaseGiven{ + Method: http.MethodPost, + Body: `{"urls":["URL"],"rpm_names":[],"search":"demo"}`, + }, + Expected: TestCaseExpected{ + Code: http.StatusOK, + Body: "[]\n", + }, + }, + { + Name: "Evoke a StatusBadRequest response", + Given: TestCaseGiven{ + Method: http.MethodPost, + Body: "{", + }, + Expected: TestCaseExpected{ + Code: http.StatusBadRequest, + Body: "{\"errors\":[{\"status\":400,\"title\":\"Error binding parameters\",\"detail\":\"code=400, message=unexpected EOF, internal=unexpected EOF\"}]}\n", + }, + }, + } + + for _, testCase := range testCases { + t.Log(testCase.Name) + + path := fmt.Sprintf("%s/module_streams/search", api.FullRootPath()) + switch { + case testCase.Expected.Code >= 200 && testCase.Expected.Code < 300: + { + var bodyRequest api.SearchModuleStreamsRequest + err := json.Unmarshal([]byte(testCase.Given.Body), &bodyRequest) + require.NoError(t, err) + suite.dao.ModuleStream.On("SearchRepositoryModuleStreams", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId, bodyRequest). Return([]api.SearchModuleStreams{}, nil) } default: diff --git a/pkg/models/module_stream.go b/pkg/models/module_stream.go new file mode 100644 index 000000000..b0e14777f --- /dev/null +++ b/pkg/models/module_stream.go @@ -0,0 +1,104 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + + "github.com/lib/pq" + "gorm.io/gorm" +) + +const TableNameModuleStream = "module_streams" + +type ModuleStream struct { + Base + Name string `json:"name" gorm:"not null"` + Stream string `json:"stream"` + Version string `json:"version" gorm:"type:text"` + Context string `json:"context"` + Arch string `json:"arch"` + Summary string `json:"summary"` + Description string `json:"description"` + PackageNames pq.StringArray `json:"package_names" gorm:"type:text"` + Packages pq.StringArray `json:"packages" gorm:"type:text"` + Profiles ModuleStreamProfiles `json:"profiles" gorm:"type:jsonb,not null,default:{}"` + HashValue string `json:"hash" gorm:"not null"` + Repositories []Repository `gorm:"many2many:repositories_module_streams"` +} + +func (r *ModuleStream) ToHashString() string { + return fmt.Sprintf("%v-%v-%v-%v-%v-%v-%v", r.Name, r.Stream, r.Version, r.Context, r.Arch, r.Description, r.PackageNames) +} + +// BeforeCreate hook performs validations and sets UUID of RepositoryPackageGroup +func (r *ModuleStream) BeforeCreate(tx *gorm.DB) (err error) { + if err := r.Base.BeforeCreate(tx); err != nil { + return err + } + // Ensure a default of empty + if r.Profiles == nil { + r.Profiles = ModuleStreamProfiles{} + } + if r.Packages == nil { + r.Packages = []string{} + } + if r.PackageNames == nil { + r.PackageNames = []string{} + } + return nil +} + +func (in *ModuleStream) DeepCopy() *ModuleStream { + out := &ModuleStream{} + in.DeepCopyInto(out) + return out +} + +func (in *ModuleStream) DeepCopyInto(out *ModuleStream) { + if in == nil || out == nil || in == out { + return + } + in.Base.DeepCopyInto(&out.Base) + out.Name = in.Name + out.Description = in.Description + out.Stream = in.Stream + out.Version = in.Version + out.Context = in.Context + out.Arch = in.Arch + out.Summary = in.Summary + out.Packages = in.Packages + out.Profiles = in.Profiles + out.PackageNames = in.PackageNames + + out.Repositories = make([]Repository, len(in.Repositories)) + for i, item := range in.Repositories { + item.DeepCopyInto(&out.Repositories[i]) + } +} + +type ModuleStreamProfiles map[string][]string + +func (p *ModuleStreamProfiles) Value() (driver.Value, error) { + if *p == nil { + return "{}", nil + } + j, err := json.Marshal(p) + return j, err +} + +func (p *ModuleStreamProfiles) Scan(src interface{}) error { + source, ok := src.([]byte) + if !ok { + return fmt.Errorf("type assertion .([]byte) failed") + } + + var profiles ModuleStreamProfiles + err := json.Unmarshal(source, &profiles) + if err != nil { + return err + } + + *p = profiles + return nil +} diff --git a/pkg/models/repository_module_stream.go b/pkg/models/repository_module_stream.go new file mode 100644 index 000000000..d1f6155d7 --- /dev/null +++ b/pkg/models/repository_module_stream.go @@ -0,0 +1,22 @@ +package models + +import "gorm.io/gorm" + +type RepositoryModuleStream struct { + RepositoryUUID string `json:"repository_uuid" gorm:"not null"` + ModuleStreamUUID string `json:"package_group_uuid" gorm:"not null"` +} + +func (r *RepositoryModuleStream) BeforeCreate(db *gorm.DB) (err error) { + if r.RepositoryUUID == "" { + return Error{Message: "RepositoryUUID cannot be empty", Validation: true} + } + if r.ModuleStreamUUID == "" { + return Error{Message: "ModuleStreamUUID cannot be empty", Validation: true} + } + return nil +} + +func (r *RepositoryModuleStream) TableName() string { + return "repositories_module_streams" +}