diff --git a/api/docs.go b/api/docs.go index 4d48c6553..45b48f550 100644 --- a/api/docs.go +++ b/api/docs.go @@ -942,6 +942,69 @@ const docTemplate = `{ } } }, + "/repositories/{uuid}/snapshots/{snapshot_uuid}/config.repo": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "repositories" + ], + "summary": "Get configuration file of a repository", + "operationId": "getRepoConfigurationFile", + "parameters": [ + { + "type": "string", + "description": "Identifier of the repository", + "name": "uuid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Identifier of the snapshot", + "name": "snapshot_uuid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errors.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errors.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errors.ErrorResponse" + } + } + } + } + }, "/repository_parameters/": { "get": { "description": "List repository parameters.", @@ -1897,6 +1960,13 @@ const docTemplate = `{ "repository_path": { "description": "Path to repository snapshot contents", "type": "string" + }, + "url": { + "description": "URL to the snapshot's content", + "type": "string" + }, + "uuid": { + "type": "string" } } }, diff --git a/api/openapi.json b/api/openapi.json index a32de0083..d940e2dd8 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -572,6 +572,13 @@ "repository_path": { "description": "Path to repository snapshot contents", "type": "string" + }, + "url": { + "description": "URL to the snapshot's content", + "type": "string" + }, + "uuid": { + "type": "string" } }, "type": "object" @@ -1942,6 +1949,87 @@ ] } }, + "/repositories/{uuid}/snapshots/{snapshot_uuid}/config.repo": { + "get": { + "operationId": "getRepoConfigurationFile", + "parameters": [ + { + "description": "Identifier of the repository", + "in": "path", + "name": "uuid", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Identifier of the snapshot", + "in": "path", + "name": "snapshot_uuid", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "OK" + }, + "400": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Not Found" + }, + "500": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/errors.ErrorResponse" + } + } + }, + "description": "Internal Server Error" + } + }, + "summary": "Get configuration file of a repository", + "tags": [ + "repositories" + ] + } + }, "/repository_parameters/": { "get": { "description": "List repository parameters.", diff --git a/cmd/test/main.go.bak b/cmd/test/main.go.bak deleted file mode 100644 index 565b04051..000000000 --- a/cmd/test/main.go.bak +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "sync" - - "github.com/content-services/content-sources-backend/pkg/config" - "github.com/content-services/content-sources-backend/pkg/db" - "github.com/content-services/content-sources-backend/pkg/pulp_client" - uuid2 "github.com/google/uuid" - "github.com/rs/zerolog/log" -) - -func main() { - config.Load() - config.ConfigureLogging() - err := db.Connect() - if err != nil { - panic(err) - } - defer db.Close() - - // _, err = pulp_client.GetGlobalPulpClient().LookupOrCreateDomain(uuid2.NewString()) - // for i := 0; i < 1000; i++ { - // seeds.SeedTasks(db.DB, 100, seeds.TaskSeedOptions{ - // AccountID: uuid2.NewString(), - // OrgID: uuid2.NewString(), - // }) - // } - count := 1 - wg := sync.WaitGroup{} - wg.Add(count) - c := pulp_client.GetGlobalPulpClient() - name := uuid2.NewString() - for i := 0; i < count; i++ { - go func() { - name, err := c.LookupOrCreateDomain(name) - if err != nil { - log.Logger.Error().Err(err).Msg("ERORR: ") - } else if name == nil { - log.Logger.Error().Msg("nil name") - } else { - log.Logger.Info().Msg("Created") - } - wg.Done() - }() - } - wg.Wait() -} diff --git a/cmd/test/main.go.listupstreampulps b/cmd/test/main.go.listupstreampulps deleted file mode 100644 index f4f82fbad..000000000 --- a/cmd/test/main.go.listupstreampulps +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "context" - "net/http" - "time" - - zest "github.com/content-services/zest/release/v2023" - "github.com/labstack/gommon/log" -) - -func connectPulp() { - configuration := zest.NewConfiguration() - apiClient := zest.NewAPIClient(configuration) - resp, r, err := apiClient.StatusAPI.StatusRead1(context.Background()).Execute() - if err != nil { - log.Fatalf("Error when calling `StatusAPI.StatusRead``: %v\n", err) - log.Fatalf("Full HTTP response: %v\n", r) - } - // response from `StatusRead`: StatusResponse - log.Infof("Response from `StatusAPI.StatusRead`: %v\n", resp) -} - -func listUpstreamPulps() { - ctx2 := context.WithValue(context.Background(), zest.ContextServerIndex, 0) - timeout := 60 * time.Second - transport := &http.Transport{ResponseHeaderTimeout: timeout} - httpClient := http.Client{Transport: transport, Timeout: timeout} - - pulpConfig := zest.NewConfiguration() - pulpConfig.HTTPClient = &httpClient - pulpConfig.Servers = zest.ServerConfigurations{zest.ServerConfiguration{ - URL: "http://localhost:8080", - }} - client := zest.NewAPIClient(pulpConfig) - - authCtx := context.WithValue(ctx2, zest.ContextBasicAuth, zest.BasicAuth{ - UserName: "admin", - Password: "password", - }) - - resp, r, err := client.UpstreamPulpsAPI.UpstreamPulpsList(authCtx, "default").Execute() - if err != nil { - log.Fatalf("Error when calling `StatusAPI.StatusRead``: %v\n", err) - log.Fatalf("Full HTTP response: %v\n", r) - } - // response from `UpstreamPulpsList`: PaginatedUpstreamPulpResponseList - log.Infof("Response from `StatusAPI.StatusRead`: %v\n", *resp.Count) -} - -func main() { - listUpstreamPulps() -} diff --git a/cmd/test/main.go.multirequest b/cmd/test/main.go.multirequest deleted file mode 100644 index 04bdc73b3..000000000 --- a/cmd/test/main.go.multirequest +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "sync" - - "github.com/content-services/content-sources-backend/pkg/config" - "github.com/content-services/content-sources-backend/pkg/db" - uuid2 "github.com/google/uuid" -) - -func main() { - config.Load() - config.ConfigureLogging() - err := db.Connect() - if err != nil { - panic(err) - } - defer db.Close() - - // _, err = pulp_client.GetGlobalPulpClient().LookupOrCreateDomain(uuid2.NewString()) - // for i := 0; i < 1000; i++ { - // seeds.SeedTasks(db.DB, 100, seeds.TaskSeedOptions{ - // AccountID: uuid2.NewString(), - // OrgID: uuid2.NewString(), - // }) - // } - count := 1000 - wg := sync.WaitGroup{} - wg.Add(count) - for i := 0; i < count; i++ { - go func() { - name := uuid2.NewString() - url := "http://example.com/" + uuid2.NewString() + "/" - postBody, _ := json.Marshal(map[string]string{ - "name": name, - "url": url, - }) - client := &http.Client{} - requestBody := bytes.NewBuffer(postBody) - req, err := http.NewRequest("POST", "http://localhost:8000/api/content-sources/v1.0/repositories/", requestBody) - if err != nil { - fmt.Println("ERROR creating request") - panic(-1) - } - req.Header.Add("x-rh-identity", "eyJpZGVudGl0eSI6eyJ0eXBlIjoiVXNlciIsInVzZXIiOnsidXNlcm5hbWUiOiJqZG9lIn0sImludGVybmFsIjp7Im9yZ19pZCI6IjEyMyJ9fX0K") - req.Header.Add("Content-Type", "application/json") - fmt.Printf("Starting request") - resp, err := client.Do(req) - defer resp.Body.Close() - if err != nil { - fmt.Printf("Request error, %v\n", "FOO") - } else { - fmt.Printf("DONE: %v\n", resp.Status) - } - wg.Done() - }() - } - wg.Wait() -} diff --git a/configs/config.yaml.example b/configs/config.yaml.example index 6c746ffe1..13f3187b0 100644 --- a/configs/config.yaml.example +++ b/configs/config.yaml.example @@ -95,7 +95,9 @@ clients: host: localhost port: 6379 db: 1 - expiration: 30s + expiration: + rbac: 1m + pulp_content_path: 1h # Configuration for the mocks mocks: diff --git a/pkg/api/snapshots.go b/pkg/api/snapshots.go index d05d9ef80..6d412595f 100644 --- a/pkg/api/snapshots.go +++ b/pkg/api/snapshots.go @@ -1,13 +1,17 @@ package api -import "time" +import ( + "time" +) type SnapshotResponse struct { + UUID string `json:"uuid"` CreatedAt time.Time `json:"created_at"` // Datetime the snapshot was created RepositoryPath string `json:"repository_path"` // Path to repository snapshot contents ContentCounts map[string]int64 `json:"content_counts"` // Count of each content type AddedCounts map[string]int64 `json:"added_counts"` // Count of each content type RemovedCounts map[string]int64 `json:"removed_counts"` // Count of each content type + URL string `json:"url"` // URL to the snapshot's content } type SnapshotCollectionResponse struct { @@ -20,3 +24,5 @@ func (r *SnapshotCollectionResponse) SetMetadata(meta ResponseMetadata, links Li r.Meta = meta r.Links = links } + +type RepositoryConfigurationFile string diff --git a/pkg/cache/rbac_cache.go b/pkg/cache/cache.go similarity index 70% rename from pkg/cache/rbac_cache.go rename to pkg/cache/cache.go index 8c7fc6359..7c5d0e40b 100644 --- a/pkg/cache/rbac_cache.go +++ b/pkg/cache/cache.go @@ -12,12 +12,16 @@ import ( var NotFound = errors.New("not found in cache") -type RbacCache interface { +//go:generate mockery --name Cache --filename cache_mock.go --inpackage +type Cache interface { GetAccessList(ctx context.Context) (rbac.AccessList, error) SetAccessList(ctx context.Context, accessList rbac.AccessList) error + + GetPulpContentPath(ctx context.Context) (string, error) + SetPulpContentPath(ctx context.Context, pulpContentPath string) error } -func Initialize() RbacCache { +func Initialize() Cache { if config.Get().Clients.Redis.Host != "" { return NewRedisCache() } else { diff --git a/pkg/cache/cache_mock.go b/pkg/cache/cache_mock.go new file mode 100644 index 000000000..6d2437342 --- /dev/null +++ b/pkg/cache/cache_mock.go @@ -0,0 +1,107 @@ +// Code generated by mockery v2.32.0. DO NOT EDIT. + +package cache + +import ( + context "context" + + rbac "github.com/RedHatInsights/rbac-client-go" + mock "github.com/stretchr/testify/mock" +) + +// MockCache is an autogenerated mock type for the Cache type +type MockCache struct { + mock.Mock +} + +// GetAccessList provides a mock function with given fields: ctx +func (_m *MockCache) GetAccessList(ctx context.Context) (rbac.AccessList, error) { + ret := _m.Called(ctx) + + var r0 rbac.AccessList + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (rbac.AccessList, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) rbac.AccessList); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rbac.AccessList) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetPulpContentPath provides a mock function with given fields: ctx +func (_m *MockCache) GetPulpContentPath(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetAccessList provides a mock function with given fields: ctx, accessList +func (_m *MockCache) SetAccessList(ctx context.Context, accessList rbac.AccessList) error { + ret := _m.Called(ctx, accessList) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, rbac.AccessList) error); ok { + r0 = rf(ctx, accessList) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetPulpContentPath provides a mock function with given fields: ctx, pulpContentPath +func (_m *MockCache) SetPulpContentPath(ctx context.Context, pulpContentPath string) error { + ret := _m.Called(ctx, pulpContentPath) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, pulpContentPath) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewMockCache creates a new instance of MockCache. 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 NewMockCache(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCache { + mock := &MockCache{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/cache/noop.go b/pkg/cache/noop.go index 816e90b6a..e8f1a8772 100644 --- a/pkg/cache/noop.go +++ b/pkg/cache/noop.go @@ -24,3 +24,13 @@ func (c *noOpCache) GetAccessList(ctx context.Context) (rbac.AccessList, error) func (c *noOpCache) SetAccessList(ctx context.Context, accessList rbac.AccessList) error { return nil } + +// GetPulpContentPath a NoOp version to fetch a cached AccessList +func (c *noOpCache) GetPulpContentPath(ctx context.Context) (string, error) { + return "", NotFound +} + +// SetPulpContentPath a NoOp version to store an AccessList +func (c *noOpCache) SetPulpContentPath(ctx context.Context, repoConfigFile string) error { + return nil +} diff --git a/pkg/cache/rbac_cache_mock.go b/pkg/cache/rbac_cache_mock.go deleted file mode 100644 index 0a9039f2e..000000000 --- a/pkg/cache/rbac_cache_mock.go +++ /dev/null @@ -1,70 +0,0 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. - -package cache - -import ( - context "context" - - rbac "github.com/RedHatInsights/rbac-client-go" - mock "github.com/stretchr/testify/mock" -) - -// MockRbacCache is an autogenerated mock type for the RbacCache type -type MockRbacCache struct { - mock.Mock -} - -// GetAccessList provides a mock function with given fields: ctx -func (_m *MockRbacCache) GetAccessList(ctx context.Context) (rbac.AccessList, error) { - ret := _m.Called(ctx) - - var r0 rbac.AccessList - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (rbac.AccessList, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) rbac.AccessList); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(rbac.AccessList) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SetAccessList provides a mock function with given fields: ctx, accessList -func (_m *MockRbacCache) SetAccessList(ctx context.Context, accessList rbac.AccessList) error { - ret := _m.Called(ctx, accessList) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, rbac.AccessList) error); ok { - r0 = rf(ctx, accessList) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewMockRbacCache interface { - mock.TestingT - Cleanup(func()) -} - -// NewMockRbacCache creates a new instance of MockRbacCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockRbacCache(t mockConstructorTestingTNewMockRbacCache) *MockRbacCache { - mock := &MockRbacCache{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go index 52ee7bdc0..8642027e3 100644 --- a/pkg/cache/redis.go +++ b/pkg/cache/redis.go @@ -12,6 +12,8 @@ import ( "github.com/redis/go-redis/v9" ) +const PulpContentPathKey = "pulp-content-path" + type redisCache struct { client *redis.Client } @@ -35,19 +37,17 @@ func authKey(ctx context.Context) string { return fmt.Sprintf("auth:%v,%v", identity.Identity.User.Username, identity.Identity.OrgID) } +// pulpContentPathKey returns the key for PulpContentPath caching +func pulpContentPathKey() string { + return PulpContentPathKey +} + // GetAccessList uses the request context to read user information, and then tries to retrieve the role AccessList from the cache func (c *redisCache) GetAccessList(ctx context.Context) (rbac.AccessList, error) { accessList := rbac.AccessList{} - cmd := c.client.Get(ctx, authKey(ctx)) - if errors.Is(cmd.Err(), redis.Nil) { - return nil, NotFound - } else if cmd.Err() != nil { - return nil, fmt.Errorf("redis error: %w", cmd.Err()) - } - - buf, err := cmd.Bytes() + buf, err := c.get(ctx, authKey(ctx)) if err != nil { - return nil, fmt.Errorf("redis bytes conversion error: %w", err) + return accessList, fmt.Errorf("redis get error: %w", err) } err = json.Unmarshal(buf, &accessList) @@ -64,6 +64,46 @@ func (c *redisCache) SetAccessList(ctx context.Context, accessList rbac.AccessLi return fmt.Errorf("unable to marshal for Redis cache: %w", err) } - c.client.Set(ctx, authKey(ctx), string(buf), config.Get().Clients.Redis.Expiration) + c.client.Set(ctx, authKey(ctx), string(buf), config.Get().Clients.Redis.Expiration.Rbac) return nil } + +func (c *redisCache) GetPulpContentPath(ctx context.Context) (string, error) { + var repoConfigFile string + key := pulpContentPathKey() + buf, err := c.get(ctx, key) + if err != nil { + return "", fmt.Errorf("redis get error: %w", err) + } + + err = json.Unmarshal(buf, &repoConfigFile) + if err != nil { + return "", fmt.Errorf("redis unmarshal error: %w", err) + } + return repoConfigFile, nil +} + +func (c *redisCache) SetPulpContentPath(ctx context.Context, contentPath string) error { + buf, err := json.Marshal(contentPath) + if err != nil { + return fmt.Errorf("unable to marshal for Redis cache: %w", err) + } + + c.client.Set(ctx, pulpContentPathKey(), string(buf), config.Get().Clients.Redis.Expiration.PulpContentPath) + return nil +} + +func (c *redisCache) get(ctx context.Context, key string) ([]byte, error) { + cmd := c.client.Get(ctx, key) + if errors.Is(cmd.Err(), redis.Nil) { + return nil, NotFound + } else if cmd.Err() != nil { + return nil, fmt.Errorf("redis error: %w", cmd.Err()) + } + + buf, err := cmd.Bytes() + if err != nil { + return nil, fmt.Errorf("redis bytes conversion error: %w", err) + } + return buf, err +} diff --git a/pkg/config/config.go b/pkg/config/config.go index ba46edd04..b7607c0ad 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -138,7 +138,12 @@ type Redis struct { Username string Password string DB int - Expiration time.Duration + Expiration Expiration `mapstructure:"pulp_content_path"` +} + +type Expiration struct { + Rbac time.Duration + PulpContentPath time.Duration } type Sentry struct { @@ -239,7 +244,8 @@ func setDefaults(v *viper.Viper) { v.SetDefault("clients.redis.username", "") v.SetDefault("clients.redis.password", "") v.SetDefault("clients.redis.db", 0) - v.SetDefault("clients.redis.expiration", 1*time.Minute) + v.SetDefault("clients.redis.expiration.rbac", 1*time.Minute) + v.SetDefault("clients.redis.expiration.pulp_content_path", 1*time.Hour) v.SetDefault("tasking.heartbeat", 1*time.Minute) v.SetDefault("tasking.worker_count", 3) diff --git a/pkg/dao/domain_dao_mock.go b/pkg/dao/domain_dao_mock.go index 5ced88194..07d556779 100644 --- a/pkg/dao/domain_dao_mock.go +++ b/pkg/dao/domain_dao_mock.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dao @@ -9,6 +9,30 @@ type MockDomainDao struct { mock.Mock } +// Fetch provides a mock function with given fields: orgId +func (_m *MockDomainDao) Fetch(orgId string) (string, error) { + ret := _m.Called(orgId) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string) (string, error)); ok { + return rf(orgId) + } + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(orgId) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(orgId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // FetchOrCreateDomain provides a mock function with given fields: orgId func (_m *MockDomainDao) FetchOrCreateDomain(orgId string) (string, error) { ret := _m.Called(orgId) diff --git a/pkg/dao/interfaces.go b/pkg/dao/interfaces.go index 9d9fef83b..188f3581d 100644 --- a/pkg/dao/interfaces.go +++ b/pkg/dao/interfaces.go @@ -23,14 +23,14 @@ type DaoRegistry struct { func GetDaoRegistry(db *gorm.DB) *DaoRegistry { reg := DaoRegistry{ - RepositoryConfig: repositoryConfigDaoImpl{ + RepositoryConfig: &repositoryConfigDaoImpl{ db: db, yumRepo: &yum.Repository{}, }, Rpm: rpmDaoImpl{db: db}, Repository: repositoryDaoImpl{db: db}, Metrics: metricsDaoImpl{db: db}, - Snapshot: snapshotDaoImpl{db: db}, + Snapshot: &snapshotDaoImpl{db: db}, TaskInfo: taskInfoDaoImpl{db: db}, AdminTask: adminTaskInfoDaoImpl{db: db, pulpClient: pulp_client.GetGlobalPulpClient(context.Background())}, Domain: domainDaoImpl{db: db}, @@ -55,6 +55,7 @@ type RepositoryConfigDao interface { InternalOnly_FetchRepoConfigsForRepoUUID(uuid string) []api.RepositoryResponse UpdateLastSnapshotTask(taskUUID string, orgID string, repoUUID string) error InternalOnly_RefreshRedHatRepo(request api.RepositoryRequest) (*api.RepositoryResponse, error) + InitializePulpClient(ctx context.Context, orgID string) error } //go:generate mockery --name RpmDao --filename rpms_mock.go --inpackage @@ -78,10 +79,12 @@ type RepositoryDao interface { //go:generate mockery --name SnapshotDao --filename snapshots_mock.go --inpackage type SnapshotDao interface { Create(snap *models.Snapshot) error - List(repoConfigUuid string, paginationData api.PaginationData, filterData api.FilterData) (api.SnapshotCollectionResponse, int64, error) + List(repoConfigUuid string, paginationData api.PaginationData, _ api.FilterData) (api.SnapshotCollectionResponse, int64, error) FetchForRepoConfigUUID(repoConfigUUID string) ([]models.Snapshot, error) Delete(snapUUID string) error FetchLatestSnapshot(repoConfigUUID string) (api.SnapshotResponse, error) + GetRepositoryConfigurationFile(orgID, snapshotUUID, repoConfigUUID string) (string, error) + InitializePulpClient(ctx context.Context, orgID string) error } //go:generate mockery --name MetricsDao --filename metrics_mock.go --inpackage @@ -109,4 +112,5 @@ type AdminTaskDao interface { //go:generate mockery --name DomainDao --filename domain_dao_mock.go --inpackage type DomainDao interface { FetchOrCreateDomain(orgId string) (string, error) + Fetch(orgId string) (string, error) } diff --git a/pkg/dao/repository_configs.go b/pkg/dao/repository_configs.go index a49f5dea1..0a479529c 100644 --- a/pkg/dao/repository_configs.go +++ b/pkg/dao/repository_configs.go @@ -1,6 +1,7 @@ package dao import ( + "context" "errors" "fmt" "net/http" @@ -14,6 +15,7 @@ import ( ce "github.com/content-services/content-sources-backend/pkg/errors" "github.com/content-services/content-sources-backend/pkg/models" "github.com/content-services/content-sources-backend/pkg/notifications" + "github.com/content-services/content-sources-backend/pkg/pulp_client" "github.com/content-services/yummy/pkg/yum" "github.com/jackc/pgx/v5/pgconn" "github.com/openlyinc/pointy" @@ -23,17 +25,26 @@ import ( ) type repositoryConfigDaoImpl struct { - db *gorm.DB - yumRepo yum.YumRepository + db *gorm.DB + yumRepo yum.YumRepository + pulpClient pulp_client.PulpClient } func GetRepositoryConfigDao(db *gorm.DB) RepositoryConfigDao { - return repositoryConfigDaoImpl{ + return &repositoryConfigDaoImpl{ db: db, yumRepo: &yum.Repository{}, } } +func GetRepositoryConfigDaoWithPulpClient(db *gorm.DB, pulpClient pulp_client.PulpClient) RepositoryConfigDao { + return &repositoryConfigDaoImpl{ + db: db, + yumRepo: &yum.Repository{}, + pulpClient: pulpClient, + } +} + func DBErrorToApi(e error) *ce.DaoError { var dupKeyName string if e == nil { @@ -64,6 +75,18 @@ func DBErrorToApi(e error) *ce.DaoError { return &ce.DaoError{Message: e.Error()} } +func (r *repositoryConfigDaoImpl) InitializePulpClient(ctx context.Context, orgID string) error { + dDao := GetDomainDao(r.db) + domainName, err := dDao.Fetch(orgID) + if err != nil { + return err + } + + pulpClient := pulp_client.GetPulpClientWithDomain(ctx, domainName) + r.pulpClient = pulpClient + return nil +} + func (r repositoryConfigDaoImpl) Create(newRepoReq api.RepositoryRequest) (api.RepositoryResponse, error) { var newRepo models.Repository var newRepoConfig models.RepositoryConfiguration @@ -208,6 +231,8 @@ func (r repositoryConfigDaoImpl) List( ) (api.RepositoryCollectionResponse, int64, error) { var totalRepos int64 repoConfigs := make([]models.RepositoryConfiguration, 0) + var err error + var contentPath string filteredDB := r.filteredDbForList(OrgID, r.db, filterData) @@ -244,7 +269,16 @@ func (r repositoryConfigDaoImpl) List( if filteredDB.Error != nil { return api.RepositoryCollectionResponse{}, totalRepos, filteredDB.Error } - repos := convertToResponses(repoConfigs) + + if r.pulpClient != nil { + contentPath, err = r.pulpClient.GetContentPath() + if err != nil { + return api.RepositoryCollectionResponse{}, totalRepos, err + } + } + + repos := convertToResponses(repoConfigs, contentPath) + return api.RepositoryCollectionResponse{Data: repos}, totalRepos, nil } @@ -317,24 +351,36 @@ func (r repositoryConfigDaoImpl) InternalOnly_FetchRepoConfigsForRepoUUID(uuid s Joins("inner join repositories on repository_configurations.repository_uuid = repositories.uuid") filteredDB.Preload("Repository").Preload("LastSnapshot").Find(&repoConfigs) - if filteredDB.Error != nil { log.Error().Msgf("Unable to ListRepos: %v", uuid) return []api.RepositoryResponse{} } - return convertToResponses(repoConfigs) + return convertToResponses(repoConfigs, "") } func (r repositoryConfigDaoImpl) Fetch(orgID string, uuid string) (api.RepositoryResponse, error) { - repo := api.RepositoryResponse{} - repoConfig, err := r.fetchRepoConfig(orgID, uuid) + var repo api.RepositoryResponse + repoConfig, err := r.fetchRepoConfig(orgID, uuid) if err != nil { - return repo, err + return api.RepositoryResponse{}, err } + ModelToApiFields(repoConfig, &repo) - return repo, err + + if repoConfig.LastSnapshot != nil { + if r.pulpClient == nil { + return api.RepositoryResponse{}, fmt.Errorf("pulpClient cannot be nil") + } + contentPath, err := r.pulpClient.GetContentPath() + if err != nil { + return api.RepositoryResponse{}, err + } + contentURL := pulpContentURL(contentPath, repoConfig.LastSnapshot.RepositoryPath) + repo.LastSnapshot.URL = contentURL + } + return repo, nil } func (r repositoryConfigDaoImpl) fetchRepoConfig(orgID string, uuid string) (models.RepositoryConfiguration, error) { @@ -371,6 +417,7 @@ func (r repositoryConfigDaoImpl) FetchByRepoUuid(orgID string, repoUuid string) return repo, DBErrorToApi(result.Error) } } + ModelToApiFields(repoConfig, &repo) return repo, nil } @@ -608,10 +655,12 @@ func ModelToApiFields(repoConfig models.RepositoryConfiguration, apiRepo *api.Re if repoConfig.LastSnapshot != nil { apiRepo.LastSnapshot = &api.SnapshotResponse{ - CreatedAt: repoConfig.LastSnapshot.CreatedAt, - ContentCounts: repoConfig.LastSnapshot.ContentCounts, - AddedCounts: repoConfig.LastSnapshot.AddedCounts, - RemovedCounts: repoConfig.LastSnapshot.RemovedCounts, + UUID: repoConfig.LastSnapshot.UUID, + CreatedAt: repoConfig.LastSnapshot.CreatedAt, + ContentCounts: repoConfig.LastSnapshot.ContentCounts, + AddedCounts: repoConfig.LastSnapshot.AddedCounts, + RemovedCounts: repoConfig.LastSnapshot.RemovedCounts, + RepositoryPath: repoConfig.LastSnapshot.RepositoryPath, } } apiRepo.LastSnapshotTaskUUID = repoConfig.LastSnapshotTaskUUID @@ -631,10 +680,13 @@ func ModelToApiFields(repoConfig models.RepositoryConfiguration, apiRepo *api.Re } // Converts the database models to our response objects -func convertToResponses(repoConfigs []models.RepositoryConfiguration) []api.RepositoryResponse { +func convertToResponses(repoConfigs []models.RepositoryConfiguration, pulpContentPath string) []api.RepositoryResponse { repos := make([]api.RepositoryResponse, len(repoConfigs)) for i := 0; i < len(repoConfigs); i++ { ModelToApiFields(repoConfigs[i], &repos[i]) + if repoConfigs[i].LastSnapshot != nil { + repos[i].LastSnapshot.URL = pulpContentURL(pulpContentPath, repos[i].LastSnapshot.RepositoryPath) + } } return repos } diff --git a/pkg/dao/repository_configs_mock.go b/pkg/dao/repository_configs_mock.go index f2f8814c2..62393be53 100644 --- a/pkg/dao/repository_configs_mock.go +++ b/pkg/dao/repository_configs_mock.go @@ -1,9 +1,12 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dao import ( + context "context" + api "github.com/content-services/content-sources-backend/pkg/api" + mock "github.com/stretchr/testify/mock" models "github.com/content-services/content-sources-backend/pkg/models" @@ -144,6 +147,20 @@ func (_m *MockRepositoryConfigDao) FetchByRepoUuid(orgID string, repoUuid string return r0, r1 } +// InitializePulpClient provides a mock function with given fields: ctx, orgID +func (_m *MockRepositoryConfigDao) InitializePulpClient(ctx context.Context, orgID string) error { + ret := _m.Called(ctx, orgID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, orgID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // InternalOnly_FetchRepoConfigsForRepoUUID provides a mock function with given fields: uuid func (_m *MockRepositoryConfigDao) InternalOnly_FetchRepoConfigsForRepoUUID(uuid string) []api.RepositoryResponse { ret := _m.Called(uuid) @@ -333,13 +350,12 @@ func (_m *MockRepositoryConfigDao) ValidateParameters(orgId string, params api.R return r0, r1 } -type mockConstructorTestingTNewMockRepositoryConfigDao interface { +// NewMockRepositoryConfigDao creates a new instance of MockRepositoryConfigDao. 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 NewMockRepositoryConfigDao(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockRepositoryConfigDao creates a new instance of MockRepositoryConfigDao. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockRepositoryConfigDao(t mockConstructorTestingTNewMockRepositoryConfigDao) *MockRepositoryConfigDao { +}) *MockRepositoryConfigDao { mock := &MockRepositoryConfigDao{} mock.Mock.Test(t) diff --git a/pkg/dao/repository_configs_test.go b/pkg/dao/repository_configs_test.go index 0dc1dde7f..392edef58 100644 --- a/pkg/dao/repository_configs_test.go +++ b/pkg/dao/repository_configs_test.go @@ -12,6 +12,7 @@ import ( "github.com/content-services/content-sources-backend/pkg/db" ce "github.com/content-services/content-sources-backend/pkg/errors" "github.com/content-services/content-sources-backend/pkg/models" + "github.com/content-services/content-sources-backend/pkg/pulp_client" "github.com/content-services/content-sources-backend/pkg/seeds" "github.com/content-services/content-sources-backend/pkg/test" mockExt "github.com/content-services/content-sources-backend/pkg/test/mocks/mock_external" @@ -594,11 +595,38 @@ func (suite *RepositoryConfigSuite) TestFetch() { Error assert.NoError(t, err) - fetched, err := GetRepositoryConfigDao(suite.tx).Fetch(found.OrgID, found.UUID) + mockPulpClient := pulp_client.NewMockPulpClient(t) + rDao := repositoryConfigDaoImpl{db: tx, pulpClient: mockPulpClient} + + snap := models.Snapshot{ + Base: models.Base{UUID: uuid.NewString()}, + VersionHref: "/pulp/version", + PublicationHref: "/pulp/publication", + DistributionPath: fmt.Sprintf("/path/to/%v", uuid.NewString()), + RepositoryConfigurationUUID: found.UUID, + ContentCounts: models.ContentCountsType{"rpm.package": int64(3), "rpm.advisory": int64(1)}, + AddedCounts: models.ContentCountsType{"rpm.package": int64(1), "rpm.advisory": int64(3)}, + RemovedCounts: models.ContentCountsType{"rpm.package": int64(2), "rpm.advisory": int64(2)}, + } + sDao := snapshotDaoImpl{db: tx} + err = sDao.Create(&snap) + assert.NoError(t, err) + + err = tx. + Preload("Repository").Preload("LastSnapshot"). + First(&found, "org_id = ?", orgID). + Error + assert.NoError(t, err) + + mockPulpClient.On("GetContentPath").Return(testContentPath, nil) + + fetched, err := rDao.Fetch(found.OrgID, found.UUID) assert.Nil(t, err) assert.Equal(t, found.UUID, fetched.UUID) assert.Equal(t, found.Name, fetched.Name) assert.Equal(t, found.Repository.URL, fetched.URL) + assert.Equal(t, found.LastSnapshot.UUID, fetched.LastSnapshot.UUID) + assert.Equal(t, testContentPath+"/", fetched.LastSnapshot.URL) } func (suite *RepositoryConfigSuite) TestFetchByRepo() { @@ -705,13 +733,40 @@ func (suite *RepositoryConfigSuite) TestList() { assert.Nil(t, result.Error) assert.Equal(t, int64(1), total) - response, total, err := GetRepositoryConfigDao(suite.tx).List(orgID, pageData, filterData) + snap := models.Snapshot{ + Base: models.Base{UUID: uuid.NewString()}, + VersionHref: "/pulp/version", + PublicationHref: "/pulp/publication", + DistributionPath: fmt.Sprintf("/path/to/%v", uuid.NewString()), + RepositoryConfigurationUUID: repoConfig.UUID, + ContentCounts: models.ContentCountsType{"rpm.package": int64(3), "rpm.advisory": int64(1)}, + AddedCounts: models.ContentCountsType{"rpm.package": int64(1), "rpm.advisory": int64(3)}, + RemovedCounts: models.ContentCountsType{"rpm.package": int64(2), "rpm.advisory": int64(2)}, + } + sDao := snapshotDaoImpl{db: suite.tx} + err = sDao.Create(&snap) + assert.NoError(t, err) + + err = suite.tx. + Preload("Repository").Preload("LastSnapshot"). + First(&repoConfig, "org_id = ?", orgID). + Error + assert.NoError(t, err) + + mockPulpClient := pulp_client.NewMockPulpClient(t) + rDao := repositoryConfigDaoImpl{db: suite.tx, pulpClient: mockPulpClient} + mockPulpClient.On("GetContentPath").Return(testContentPath, nil) + + response, total, err := rDao.List(orgID, pageData, filterData) assert.Nil(t, err) assert.Equal(t, int64(1), total) assert.Equal(t, 1, len(response.Data)) if len(response.Data) > 0 { assert.Equal(t, repoConfig.Name, response.Data[0].Name) assert.Equal(t, repoConfig.Repository.URL, response.Data[0].URL) + assert.Equal(t, repoConfig.LastSnapshot.UUID, response.Data[0].LastSnapshot.UUID) + assert.Equal(t, testContentPath+"/", response.Data[0].LastSnapshot.URL) + assert.Equal(t, repoConfig.LastSnapshot.RepositoryPath, response.Data[0].LastSnapshot.RepositoryPath) } } diff --git a/pkg/dao/snapshots.go b/pkg/dao/snapshots.go index 9affa7354..42daf504f 100644 --- a/pkg/dao/snapshots.go +++ b/pkg/dao/snapshots.go @@ -1,26 +1,30 @@ package dao import ( + "context" "fmt" + "strings" "github.com/content-services/content-sources-backend/pkg/api" ce "github.com/content-services/content-sources-backend/pkg/errors" "github.com/content-services/content-sources-backend/pkg/models" + "github.com/content-services/content-sources-backend/pkg/pulp_client" "gorm.io/gorm" ) type snapshotDaoImpl struct { - db *gorm.DB + db *gorm.DB + pulpClient pulp_client.PulpClient } func GetSnapshotDao(db *gorm.DB) SnapshotDao { - return snapshotDaoImpl{ + return &snapshotDaoImpl{ db: db, } } // Create records a snapshot of a repository -func (sDao snapshotDaoImpl) Create(s *models.Snapshot) error { +func (sDao *snapshotDaoImpl) Create(s *models.Snapshot) error { trans := sDao.db.Create(s) if trans.Error != nil { return trans.Error @@ -44,7 +48,7 @@ func (sDao snapshotDaoImpl) Create(s *models.Snapshot) error { } // List the snapshots for a given repository config -func (sDao snapshotDaoImpl) List(repoConfigUuid string, paginationData api.PaginationData, _ api.FilterData) (api.SnapshotCollectionResponse, int64, error) { +func (sDao *snapshotDaoImpl) List(repoConfigUuid string, paginationData api.PaginationData, _ api.FilterData) (api.SnapshotCollectionResponse, int64, error) { var snaps []models.Snapshot var totalSnaps int64 var repoConfig models.RepositoryConfiguration @@ -88,29 +92,81 @@ func (sDao snapshotDaoImpl) List(repoConfigUuid string, paginationData api.Pagin return api.SnapshotCollectionResponse{}, 0, filteredDB.Error } - resp := snapshotConvertToResponses(snaps) + if len(snaps) == 0 { + return api.SnapshotCollectionResponse{Data: []api.SnapshotResponse{}}, totalSnaps, nil + } + + pulpContentPath, err := sDao.pulpClient.GetContentPath() + if err != nil { + return api.SnapshotCollectionResponse{}, 0, err + } + + resp := snapshotConvertToResponses(snaps, pulpContentPath) return api.SnapshotCollectionResponse{Data: resp}, totalSnaps, nil } -// Converts the database models to our response objects -func snapshotConvertToResponses(snapshots []models.Snapshot) []api.SnapshotResponse { - repos := make([]api.SnapshotResponse, len(snapshots)) - for i := 0; i < len(snapshots); i++ { - snapshotModelToApi(snapshots[i], &repos[i]) +func (sDao *snapshotDaoImpl) Fetch(uuid string) (models.Snapshot, error) { + var snapshot models.Snapshot + result := sDao.db.Where("uuid = ?", UuidifyString(uuid)).First(&snapshot) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return models.Snapshot{}, &ce.DaoError{ + Message: "Could not find snapshot with UUID " + uuid, + NotFound: true, + } + } + return models.Snapshot{}, result.Error } - return repos + return snapshot, nil } -func snapshotModelToApi(model models.Snapshot, resp *api.SnapshotResponse) { - resp.CreatedAt = model.CreatedAt - resp.RepositoryPath = model.RepositoryPath - resp.ContentCounts = model.ContentCounts - resp.AddedCounts = model.AddedCounts - resp.RemovedCounts = model.RemovedCounts +func (sDao *snapshotDaoImpl) GetRepositoryConfigurationFile(orgID, snapshotUUID, repoConfigUUID string) (string, error) { + rcDao := repositoryConfigDaoImpl{db: sDao.db} + repoConfig, err := rcDao.fetchRepoConfig(orgID, repoConfigUUID) + if err != nil { + return "", err + } + + snapshot, err := sDao.Fetch(snapshotUUID) + if err != nil { + return "", err + } + + contentPath, err := sDao.pulpClient.GetContentPath() + if err != nil { + return "", err + } + + contentURL := pulpContentURL(contentPath, snapshot.RepositoryPath) + + repoID := strings.Replace(repoConfig.Name, " ", "_", len(repoConfig.Name)) + + fileConfig := fmt.Sprintf(""+ + "[%v]\n"+ + "name=%v\n"+ + "baseurl=%v\n"+ + "gpgcheck=0\n"+ + "repo_gpgcheck=0\n"+ + "enabled=1\n", + repoID, repoConfig.Name, contentURL) + + return fileConfig, nil +} + +func (sDao *snapshotDaoImpl) InitializePulpClient(ctx context.Context, orgID string) error { + dDao := GetDomainDao(sDao.db) + domainName, err := dDao.Fetch(orgID) + if err != nil { + return err + } + + pulpClient := pulp_client.GetPulpClientWithDomain(ctx, domainName) + sDao.pulpClient = pulpClient + return nil } -func (sDao snapshotDaoImpl) FetchForRepoConfigUUID(repoConfigUUID string) ([]models.Snapshot, error) { +func (sDao *snapshotDaoImpl) FetchForRepoConfigUUID(repoConfigUUID string) ([]models.Snapshot, error) { var snaps []models.Snapshot result := sDao.db.Model(&models.Snapshot{}). Where("repository_configuration_uuid = ?", repoConfigUUID). @@ -121,7 +177,7 @@ func (sDao snapshotDaoImpl) FetchForRepoConfigUUID(repoConfigUUID string) ([]mod return snaps, nil } -func (sDao snapshotDaoImpl) Delete(snapUUID string) error { +func (sDao *snapshotDaoImpl) Delete(snapUUID string) error { var snap models.Snapshot result := sDao.db.Where("uuid = ?", snapUUID).First(&snap) if result.Error != nil { @@ -134,7 +190,7 @@ func (sDao snapshotDaoImpl) Delete(snapUUID string) error { return nil } -func (sDao snapshotDaoImpl) FetchLatestSnapshot(repoConfigUUID string) (api.SnapshotResponse, error) { +func (sDao *snapshotDaoImpl) FetchLatestSnapshot(repoConfigUUID string) (api.SnapshotResponse, error) { var snap models.Snapshot result := sDao.db. Where("snapshots.repository_configuration_uuid = ?", repoConfigUUID). @@ -147,3 +203,27 @@ func (sDao snapshotDaoImpl) FetchLatestSnapshot(repoConfigUUID string) (api.Snap snapshotModelToApi(snap, &apiSnap) return apiSnap, nil } + +// Converts the database models to our response objects +func snapshotConvertToResponses(snapshots []models.Snapshot, pulpContentPath string) []api.SnapshotResponse { + snapsAPI := make([]api.SnapshotResponse, len(snapshots)) + for i := 0; i < len(snapshots); i++ { + snapshotModelToApi(snapshots[i], &snapsAPI[i]) + snapsAPI[i].URL = pulpContentURL(pulpContentPath, snapshots[i].RepositoryPath) + } + return snapsAPI +} + +func snapshotModelToApi(model models.Snapshot, resp *api.SnapshotResponse) { + resp.UUID = model.UUID + resp.CreatedAt = model.CreatedAt + resp.RepositoryPath = model.RepositoryPath + resp.ContentCounts = model.ContentCounts + resp.AddedCounts = model.AddedCounts + resp.RemovedCounts = model.RemovedCounts +} + +// pulpContentURL combines content path and repository path to get content URL +func pulpContentURL(pulpContentPath string, repositoryPath string) string { + return pulpContentPath + repositoryPath + "/" +} diff --git a/pkg/dao/snapshots_mock.go b/pkg/dao/snapshots_mock.go index fa4526c49..476066758 100644 --- a/pkg/dao/snapshots_mock.go +++ b/pkg/dao/snapshots_mock.go @@ -1,9 +1,12 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.32.0. DO NOT EDIT. package dao import ( + context "context" + api "github.com/content-services/content-sources-backend/pkg/api" + mock "github.com/stretchr/testify/mock" models "github.com/content-services/content-sources-backend/pkg/models" @@ -92,30 +95,68 @@ func (_m *MockSnapshotDao) FetchLatestSnapshot(repoConfigUUID string) (api.Snaps return r0, r1 } -// List provides a mock function with given fields: repoConfigUuid, paginationData, filterData -func (_m *MockSnapshotDao) List(repoConfigUuid string, paginationData api.PaginationData, filterData api.FilterData) (api.SnapshotCollectionResponse, int64, error) { - ret := _m.Called(repoConfigUuid, paginationData, filterData) +// GetRepositoryConfigurationFile provides a mock function with given fields: orgID, snapshotUUID, repoConfigUUID +func (_m *MockSnapshotDao) GetRepositoryConfigurationFile(orgID string, snapshotUUID string, repoConfigUUID string) (string, error) { + ret := _m.Called(orgID, snapshotUUID, repoConfigUUID) + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, string, string) (string, error)); ok { + return rf(orgID, snapshotUUID, repoConfigUUID) + } + if rf, ok := ret.Get(0).(func(string, string, string) string); ok { + r0 = rf(orgID, snapshotUUID, repoConfigUUID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, string, string) error); ok { + r1 = rf(orgID, snapshotUUID, repoConfigUUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// InitializePulpClient provides a mock function with given fields: ctx, orgID +func (_m *MockSnapshotDao) InitializePulpClient(ctx context.Context, orgID string) error { + ret := _m.Called(ctx, orgID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, orgID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// List provides a mock function with given fields: repoConfigUuid, paginationData, _a2 +func (_m *MockSnapshotDao) List(repoConfigUuid string, paginationData api.PaginationData, _a2 api.FilterData) (api.SnapshotCollectionResponse, int64, error) { + ret := _m.Called(repoConfigUuid, paginationData, _a2) var r0 api.SnapshotCollectionResponse var r1 int64 var r2 error if rf, ok := ret.Get(0).(func(string, api.PaginationData, api.FilterData) (api.SnapshotCollectionResponse, int64, error)); ok { - return rf(repoConfigUuid, paginationData, filterData) + return rf(repoConfigUuid, paginationData, _a2) } if rf, ok := ret.Get(0).(func(string, api.PaginationData, api.FilterData) api.SnapshotCollectionResponse); ok { - r0 = rf(repoConfigUuid, paginationData, filterData) + r0 = rf(repoConfigUuid, paginationData, _a2) } else { r0 = ret.Get(0).(api.SnapshotCollectionResponse) } if rf, ok := ret.Get(1).(func(string, api.PaginationData, api.FilterData) int64); ok { - r1 = rf(repoConfigUuid, paginationData, filterData) + r1 = rf(repoConfigUuid, paginationData, _a2) } else { r1 = ret.Get(1).(int64) } if rf, ok := ret.Get(2).(func(string, api.PaginationData, api.FilterData) error); ok { - r2 = rf(repoConfigUuid, paginationData, filterData) + r2 = rf(repoConfigUuid, paginationData, _a2) } else { r2 = ret.Error(2) } @@ -123,13 +164,12 @@ func (_m *MockSnapshotDao) List(repoConfigUuid string, paginationData api.Pagina return r0, r1, r2 } -type mockConstructorTestingTNewMockSnapshotDao interface { +// NewMockSnapshotDao creates a new instance of MockSnapshotDao. 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 NewMockSnapshotDao(t interface { mock.TestingT Cleanup(func()) -} - -// NewMockSnapshotDao creates a new instance of MockSnapshotDao. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewMockSnapshotDao(t mockConstructorTestingTNewMockSnapshotDao) *MockSnapshotDao { +}) *MockSnapshotDao { mock := &MockSnapshotDao{} mock.Mock.Test(t) diff --git a/pkg/dao/snapshots_test.go b/pkg/dao/snapshots_test.go index ff3822587..33303075c 100644 --- a/pkg/dao/snapshots_test.go +++ b/pkg/dao/snapshots_test.go @@ -8,7 +8,9 @@ import ( "github.com/content-services/content-sources-backend/pkg/api" ce "github.com/content-services/content-sources-backend/pkg/errors" "github.com/content-services/content-sources-backend/pkg/models" + "github.com/content-services/content-sources-backend/pkg/pulp_client" mockExt "github.com/content-services/content-sources-backend/pkg/test/mocks/mock_external" + zest "github.com/content-services/zest/release/v2023" uuid2 "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" @@ -25,10 +27,67 @@ func TestSnapshotsSuite(t *testing.T) { suite.Run(t, &r) } -func (s *SnapshotsSuite) TestCreateAndList() { +var testPulpStatusResponse = zest.StatusResponse{ + ContentSettings: zest.ContentSettingsResponse{ + ContentOrigin: "http://pulp-content", + ContentPathPrefix: "/pulp/content", + }, +} + +var testContentPath = testPulpStatusResponse.ContentSettings.ContentOrigin + testPulpStatusResponse.ContentSettings.ContentPathPrefix + +func (s *SnapshotsSuite) createRepository() models.RepositoryConfiguration { + t := s.T() + tx := s.tx + + testRepository := models.Repository{ + URL: "https://example.com", + LastIntrospectionTime: nil, + LastIntrospectionError: nil, + } + err := tx.Create(&testRepository).Error + assert.NoError(t, err) + + rConfig := models.RepositoryConfiguration{ + Name: "toSnapshot", + OrgID: "someOrg", + RepositoryUUID: testRepository.UUID, + } + + err = tx.Create(&rConfig).Error + assert.NoError(t, err) + return rConfig +} + +func (s *SnapshotsSuite) createSnapshot(rConfig models.RepositoryConfiguration) models.Snapshot { t := s.T() tx := s.tx + + snap := models.Snapshot{ + Base: models.Base{}, + VersionHref: "/pulp/version", + PublicationHref: "/pulp/publication", + DistributionPath: fmt.Sprintf("/path/to/%v", uuid2.NewString()), + RepositoryConfigurationUUID: rConfig.UUID, + ContentCounts: models.ContentCountsType{"rpm.package": int64(3), "rpm.advisory": int64(1)}, + AddedCounts: models.ContentCountsType{"rpm.package": int64(1), "rpm.advisory": int64(3)}, + RemovedCounts: models.ContentCountsType{"rpm.package": int64(2), "rpm.advisory": int64(2)}, + } + sDao := snapshotDaoImpl{db: tx} + err := sDao.Create(&snap) + assert.NoError(t, err) + return snap +} + +func (s *SnapshotsSuite) TestCreateAndList() { + t := s.T() + tx := s.tx + + mockPulpClient := pulp_client.NewMockPulpClient(t) + sDao := snapshotDaoImpl{db: tx, pulpClient: mockPulpClient} + mockPulpClient.On("GetContentPath").Return(testContentPath, nil) + repoDao := repositoryConfigDaoImpl{db: tx, yumRepo: &mockExt.YumRepositoryMock{}} rConfig := s.createRepository() pageData := api.PaginationData{ @@ -45,7 +104,7 @@ func (s *SnapshotsSuite) TestCreateAndList() { collection, total, err := sDao.List(rConfig.UUID, pageData, filterData) - repository, _ := repoDao.Fetch(rConfig.OrgID, rConfig.UUID) + repository, _ := repoDao.fetchRepoConfig(rConfig.OrgID, rConfig.UUID) repositoryList, repoCount, _ := repoDao.List(rConfig.OrgID, api.PaginationData{Limit: -1}, api.FilterData{}) assert.NoError(t, err) @@ -72,7 +131,9 @@ func (s *SnapshotsSuite) TestCreateAndList() { func (s *SnapshotsSuite) TestListNoSnapshots() { t := s.T() tx := s.tx + sDao := snapshotDaoImpl{db: tx} + pageData := api.PaginationData{ Limit: 100, Offset: 0, @@ -109,7 +170,11 @@ func (s *SnapshotsSuite) TestListNoSnapshots() { func (s *SnapshotsSuite) TestListPageLimit() { t := s.T() tx := s.tx - sDao := snapshotDaoImpl{db: tx} + + mockPulpClient := pulp_client.NewMockPulpClient(t) + sDao := snapshotDaoImpl{db: tx, pulpClient: mockPulpClient} + mockPulpClient.On("GetContentPath").Return(testContentPath, nil).Once() + rConfig := s.createRepository() pageData := api.PaginationData{ Limit: 10, @@ -134,7 +199,9 @@ func (s *SnapshotsSuite) TestListPageLimit() { func (s *SnapshotsSuite) TestListNotFound() { t := s.T() tx := s.tx + sDao := snapshotDaoImpl{db: tx} + rConfig := s.createRepository() pageData := api.PaginationData{ Limit: 100, @@ -157,50 +224,6 @@ func (s *SnapshotsSuite) TestListNotFound() { assert.Equal(t, 0, len(collection.Data)) } -func (s *SnapshotsSuite) createRepository() models.RepositoryConfiguration { - t := s.T() - tx := s.tx - - testRepository := models.Repository{ - URL: "https://example.com", - LastIntrospectionTime: nil, - LastIntrospectionError: nil, - } - err := tx.Create(&testRepository).Error - assert.NoError(t, err) - - rConfig := models.RepositoryConfiguration{ - Name: "toSnapshot", - OrgID: "someOrg", - RepositoryUUID: testRepository.UUID, - } - - err = tx.Create(&rConfig).Error - assert.NoError(t, err) - return rConfig -} - -func (s *SnapshotsSuite) createSnapshot(rConfig models.RepositoryConfiguration) models.Snapshot { - t := s.T() - tx := s.tx - - snap := models.Snapshot{ - Base: models.Base{}, - VersionHref: "/pulp/version", - PublicationHref: "/pulp/publication", - DistributionPath: fmt.Sprintf("/path/to/%v", uuid2.NewString()), - RepositoryConfigurationUUID: rConfig.UUID, - ContentCounts: models.ContentCountsType{"rpm.package": int64(3), "rpm.advisory": int64(1)}, - AddedCounts: models.ContentCountsType{"rpm.package": int64(1), "rpm.advisory": int64(3)}, - RemovedCounts: models.ContentCountsType{"rpm.package": int64(2), "rpm.advisory": int64(2)}, - } - - sDao := snapshotDaoImpl{db: tx} - err := sDao.Create(&snap) - assert.NoError(t, err) - return snap -} - func (s *SnapshotsSuite) TestFetchForRepoUUID() { t := s.T() tx := s.tx @@ -241,3 +264,73 @@ func (s *SnapshotsSuite) TestFetchLatestSnapshotNotFound() { _, err := sDao.FetchLatestSnapshot(repoConfig.UUID) assert.Equal(t, err, gorm.ErrRecordNotFound) } + +func (s *SnapshotsSuite) TestGetRepositoryConfigurationFile() { + t := s.T() + tx := s.tx + + mockPulpClient := pulp_client.NewMockPulpClient(t) + sDao := snapshotDaoImpl{db: tx, pulpClient: mockPulpClient} + + repoConfig := s.createRepository() + snapshot := s.createSnapshot(repoConfig) + + // Test happy scenario + mockPulpClient.On("GetContentPath").Return(testContentPath, nil).Once() + repoConfigFile, err := sDao.GetRepositoryConfigurationFile(repoConfig.OrgID, snapshot.UUID, repoConfig.UUID) + assert.NoError(t, err) + assert.Contains(t, repoConfigFile, repoConfig.Name) + assert.Contains(t, repoConfigFile, testContentPath) + + // Test error from pulp call + mockPulpClient.On("GetContentPath").Return("", fmt.Errorf("some error")).Once() + repoConfigFile, err = sDao.GetRepositoryConfigurationFile(repoConfig.OrgID, snapshot.UUID, repoConfig.UUID) + assert.Error(t, err) + assert.Empty(t, repoConfigFile) +} + +func (s *SnapshotsSuite) TestGetRepositoryConfigurationFileNotFound() { + t := s.T() + tx := s.tx + + mockPulpClient := pulp_client.MockPulpClient{} + sDao := snapshotDaoImpl{db: tx, pulpClient: &mockPulpClient} + + repoConfig := s.createRepository() + snapshot := s.createSnapshot(repoConfig) + + // Test bad repo UUID + mockPulpClient.On("GetContentPath").Return(testContentPath, nil).Once() + repoConfigFile, err := sDao.GetRepositoryConfigurationFile(repoConfig.OrgID, snapshot.UUID, uuid2.NewString()) + assert.Error(t, err) + if err != nil { + daoError, ok := err.(*ce.DaoError) + assert.True(t, ok) + assert.True(t, daoError.NotFound) + assert.Contains(t, daoError.Message, "Could not find repository") + } + assert.Empty(t, repoConfigFile) + + // Test bad snapshot UUID + mockPulpClient.On("GetContentPath").Return(testContentPath, nil).Once() + repoConfigFile, err = sDao.GetRepositoryConfigurationFile(repoConfig.OrgID, uuid2.NewString(), repoConfig.UUID) + assert.Error(t, err) + if err != nil { + daoError, ok := err.(*ce.DaoError) + assert.True(t, ok) + assert.True(t, daoError.NotFound) + assert.Contains(t, daoError.Message, "Could not find snapshot") + } + assert.Empty(t, repoConfigFile) + + // Test bad org ID + mockPulpClient.On("GetContentPath").Return(testContentPath, nil).Once() + repoConfigFile, err = sDao.GetRepositoryConfigurationFile("bad orgID", snapshot.UUID, repoConfig.UUID) + assert.Error(t, err) + if err != nil { + daoError, ok := err.(*ce.DaoError) + assert.True(t, ok) + assert.True(t, daoError.NotFound) + } + assert.Empty(t, repoConfigFile) +} diff --git a/pkg/handler/popular_repositories.go b/pkg/handler/popular_repositories.go index 868534801..e27c4932e 100644 --- a/pkg/handler/popular_repositories.go +++ b/pkg/handler/popular_repositories.go @@ -91,6 +91,12 @@ func filterPopularRepositories(configData []api.PopularRepositoryResponse, filte func (rh *PopularRepositoriesHandler) updateIfExists(c echo.Context, repo *api.PopularRepositoryResponse) error { _, orgID := getAccountIdOrgId(c) + + err := rh.Dao.RepositoryConfig.InitializePulpClient(c.Request().Context(), orgID) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error initializing pulp client", err.Error()) + } + // Go get the records for this URL repos, _, err := rh.Dao.RepositoryConfig.List(orgID, api.PaginationData{Limit: 1}, api.FilterData{Search: repo.URL}) if err != nil { diff --git a/pkg/handler/popular_repositories_test.go b/pkg/handler/popular_repositories_test.go index eb3be7421..2138c3485 100644 --- a/pkg/handler/popular_repositories_test.go +++ b/pkg/handler/popular_repositories_test.go @@ -17,6 +17,7 @@ import ( "github.com/labstack/echo/v4" "github.com/redhatinsights/platform-go-middlewares/identity" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -82,6 +83,7 @@ func (s *PopularReposSuite) TestPopularRepos() { s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: "https://dl.fedoraproject.org/pub/epel/9/Everything/x86_64/"}).Return(collection, int64(0), nil) s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: "https://dl.fedoraproject.org/pub/epel/8/Everything/x86_64/"}).Return(collection, int64(0), nil) s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: "https://dl.fedoraproject.org/pub/epel/7/x86_64/"}).Return(collection, int64(0), nil) + s.dao.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Times(3) path := fmt.Sprintf("%s/popular_repositories/?limit=%d", fullRootPath(), 10) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -109,6 +111,7 @@ func (s *PopularReposSuite) TestPopularReposSearchWithExisting() { collection := api.RepositoryCollectionResponse{Data: []api.RepositoryResponse{{UUID: magicalUUID, Name: existingName, URL: popularRepository.URL, DistributionVersions: popularRepository.DistributionVersions, DistributionArch: popularRepository.DistributionArch}}} paginationData := api.PaginationData{Limit: 1} s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: popularRepository.URL}).Return(collection, int64(0), nil) + s.dao.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/popular_repositories/?limit=%d&search=%s", fullRootPath(), 10, popularRepository.URL) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -136,7 +139,7 @@ func (s *PopularReposSuite) TestPopularReposSearchByURL() { collection := createRepoCollection(0, 10, 0) paginationData := api.PaginationData{Limit: 1} s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: popularRepository.URL}).Return(collection, int64(0), nil) - + s.dao.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/popular_repositories/?limit=%d&search=%s", fullRootPath(), 10, popularRepository.URL) req := httptest.NewRequest(http.MethodGet, path, nil) req.Header.Set(api.IdentityHeader, test_handler.EncodedIdentity(s.T())) @@ -161,6 +164,7 @@ func (s *PopularReposSuite) TestPopularReposSearchByName() { collection := createRepoCollection(0, 10, 0) paginationData := api.PaginationData{Limit: 1} s.dao.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{Search: popularRepository.URL}).Return(collection, int64(0), nil) + s.dao.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/popular_repositories/?limit=%d&search=%s", fullRootPath(), 10, url.QueryEscape(popularRepository.SuggestedName)) req := httptest.NewRequest(http.MethodGet, path, nil) diff --git a/pkg/handler/repositories.go b/pkg/handler/repositories.go index f0808f0fd..6e1e8deb9 100644 --- a/pkg/handler/repositories.go +++ b/pkg/handler/repositories.go @@ -108,6 +108,12 @@ func (rh *RepositoryHandler) listRepositories(c echo.Context) error { c.Logger().Infof("org_id: %s", orgID) pageData := ParsePagination(c) filterData := ParseFilters(c) + + err := rh.DaoRegistry.RepositoryConfig.InitializePulpClient(c.Request().Context(), orgID) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error initializing pulp client", err.Error()) + } + repos, totalRepos, err := rh.DaoRegistry.RepositoryConfig.List(orgID, pageData, filterData) if err != nil { return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error listing repositories", err.Error()) @@ -237,6 +243,11 @@ func (rh *RepositoryHandler) fetch(c echo.Context) error { _, orgID := getAccountIdOrgId(c) uuid := c.Param("uuid") + err := rh.DaoRegistry.RepositoryConfig.InitializePulpClient(c.Request().Context(), orgID) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error initializing pulp client", err.Error()) + } + response, err := rh.DaoRegistry.RepositoryConfig.Fetch(orgID, uuid) if err != nil { return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error fetching repository", err.Error()) diff --git a/pkg/handler/repositories_test.go b/pkg/handler/repositories_test.go index 6cad6fca1..c932e5494 100644 --- a/pkg/handler/repositories_test.go +++ b/pkg/handler/repositories_test.go @@ -22,10 +22,12 @@ import ( "github.com/content-services/content-sources-backend/pkg/tasks/payloads" "github.com/content-services/content-sources-backend/pkg/tasks/queue" test_handler "github.com/content-services/content-sources-backend/pkg/test/handler" + "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/openlyinc/pointy" "github.com/redhatinsights/platform-go-middlewares/identity" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -60,6 +62,11 @@ func createRepoCollection(size, limit, offset int) api.RepositoryCollectionRespo Status: "Valid", GpgKey: "foo", MetadataVerification: true, + LastSnapshot: &api.SnapshotResponse{ + RepositoryPath: "distribution/path/", + UUID: uuid.NewString(), + URL: "http://pulp-content/pulp/content", + }, } repos[i] = repo } @@ -143,6 +150,7 @@ func (suite *ReposSuite) TestSimple() { collection := createRepoCollection(1, 10, 0) paginationData := api.PaginationData{Limit: 10, Offset: DefaultOffset} suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{}).Return(collection, int64(1), nil) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/repositories/?limit=%d", fullRootPath(), 10) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -170,6 +178,8 @@ func (suite *ReposSuite) TestSimple() { assert.Equal(t, collection.Data[0].LastIntrospectionError, response.Data[0].LastIntrospectionError) assert.Equal(t, collection.Data[0].GpgKey, response.Data[0].GpgKey) assert.Equal(t, collection.Data[0].MetadataVerification, response.Data[0].MetadataVerification) + assert.Equal(t, collection.Data[0].LastSnapshot.URL, response.Data[0].LastSnapshot.URL) + assert.Equal(t, collection.Data[0].LastSnapshot.UUID, response.Data[0].LastSnapshot.UUID) } func (suite *ReposSuite) TestListNoRepositories() { @@ -178,6 +188,7 @@ func (suite *ReposSuite) TestListNoRepositories() { collection := api.RepositoryCollectionResponse{} paginationData := api.PaginationData{Limit: DefaultLimit, Offset: DefaultOffset} suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{}).Return(collection, int64(0), nil) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() req := httptest.NewRequest(http.MethodGet, fullRootPath()+"/repositories/", nil) req.Header.Set(api.IdentityHeader, test_handler.EncodedIdentity(t)) @@ -206,6 +217,7 @@ func (suite *ReposSuite) TestListPagedExtraRemaining() { suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData1, api.FilterData{}).Return(collection, int64(102), nil).Once() suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData2, api.FilterData{}).Return(collection, int64(102), nil).Once() + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Twice() path := fmt.Sprintf("%s/repositories/?limit=%d", fullRootPath(), 10) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -240,6 +252,7 @@ func (suite *ReposSuite) TestListWithFilters() { collection := api.RepositoryCollectionResponse{} suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, api.PaginationData{Limit: 100}, api.FilterData{ContentType: "rpm", Origin: "external"}).Return(collection, int64(100), nil) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/repositories/?origin=%v&content_type=%v", fullRootPath(), "external", "rpm") req := httptest.NewRequest(http.MethodGet, path, nil) @@ -258,6 +271,7 @@ func (suite *ReposSuite) TestListPagedNoRemaining() { collection := api.RepositoryCollectionResponse{} suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData1, api.FilterData{}).Return(collection, int64(100), nil) suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData2, api.FilterData{}).Return(collection, int64(100), nil) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Twice() path := fmt.Sprintf("%s/repositories/?limit=%d", fullRootPath(), 10) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -297,6 +311,7 @@ func (suite *ReposSuite) TestListDaoError() { suite.reg.RepositoryConfig.On("List", test_handler.MockOrgId, paginationData, api.FilterData{}). Return(api.RepositoryCollectionResponse{}, int64(0), &daoError) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() path := fmt.Sprintf("%s/repositories/", fullRootPath()) req := httptest.NewRequest(http.MethodGet, path, nil) @@ -318,6 +333,7 @@ func (suite *ReposSuite) TestFetch() { } suite.reg.RepositoryConfig.On("Fetch", test_handler.MockOrgId, uuid).Return(repo, nil) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() body, err := json.Marshal(repo) if err != nil { @@ -354,6 +370,7 @@ func (suite *ReposSuite) TestFetchNotFound() { Message: "Not found", } suite.reg.RepositoryConfig.On("Fetch", test_handler.MockOrgId, uuid).Return(api.RepositoryResponse{}, &daoError) + suite.reg.RepositoryConfig.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), test_handler.MockOrgId).Return(nil).Once() body, err := json.Marshal(repo) if err != nil { diff --git a/pkg/handler/snapshots.go b/pkg/handler/snapshots.go index 2e432e3ae..6193d2102 100644 --- a/pkg/handler/snapshots.go +++ b/pkg/handler/snapshots.go @@ -23,6 +23,7 @@ func RegisterSnapshotRoutes(group *echo.Group, daoReg *dao.DaoRegistry) { sh := SnapshotHandler{DaoRegistry: *daoReg} addRoute(group, http.MethodGet, "/repositories/:uuid/snapshots/", sh.listSnapshots, rbac.RbacVerbRead) + addRoute(group, http.MethodGet, "/repositories/:uuid/snapshots/:snapshot_uuid/config.repo", sh.getRepoConfigurationFile, rbac.RbacVerbRead) } // Get Snapshots godoc @@ -40,12 +41,52 @@ func RegisterSnapshotRoutes(group *echo.Group, daoReg *dao.DaoRegistry) { // @Failure 500 {object} ce.ErrorResponse // @Router /repositories/{uuid}/snapshots/ [get] func (sh *SnapshotHandler) listSnapshots(c echo.Context) error { + _, orgID := getAccountIdOrgId(c) uuid := c.Param("uuid") pageData := ParsePagination(c) filterData := ParseFilters(c) + + err := sh.DaoRegistry.Snapshot.InitializePulpClient(c.Request().Context(), orgID) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error initializing pulp client", err.Error()) + } + snapshots, totalSnaps, err := sh.DaoRegistry.Snapshot.List(uuid, pageData, filterData) if err != nil { return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error listing repository snapshots", err.Error()) } return c.JSON(200, setCollectionResponseMetadata(&snapshots, c, totalSnaps)) } + +// Get Snapshots godoc +// @Summary Get configuration file of a repository +// @ID getRepoConfigurationFile +// @Tags repositories +// @Accept json +// @Produce text/plain +// @Param uuid path string true "Identifier of the repository" +// @Param snapshot_uuid path string true "Identifier of the snapshot" +// @Success 200 {string} string +// @Failure 400 {object} ce.ErrorResponse +// @Failure 401 {object} ce.ErrorResponse +// @Failure 404 {object} ce.ErrorResponse +// @Failure 500 {object} ce.ErrorResponse +// @Router /repositories/{uuid}/snapshots/{snapshot_uuid}/config.repo [get] +func (sh *SnapshotHandler) getRepoConfigurationFile(c echo.Context) error { + _, orgID := getAccountIdOrgId(c) + uuid := c.Param("uuid") + snapshotUUID := c.Param("snapshot_uuid") + var repoConfigFile string + + err := sh.DaoRegistry.Snapshot.InitializePulpClient(c.Request().Context(), orgID) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error initializing pulp client", err.Error()) + } + + repoConfigFile, err = sh.DaoRegistry.Snapshot.GetRepositoryConfigurationFile(orgID, snapshotUUID, uuid) + if err != nil { + return ce.NewErrorResponse(ce.HttpCodeForDaoError(err), "Error getting repository configuration file", err.Error()) + } + + return c.String(http.StatusOK, repoConfigFile) +} diff --git a/pkg/handler/snapshots_test.go b/pkg/handler/snapshots_test.go index f58e91d97..77f3bc5e1 100644 --- a/pkg/handler/snapshots_test.go +++ b/pkg/handler/snapshots_test.go @@ -12,17 +12,21 @@ import ( "github.com/content-services/content-sources-backend/pkg/config" "github.com/content-services/content-sources-backend/pkg/dao" "github.com/content-services/content-sources-backend/pkg/middleware" + "github.com/content-services/content-sources-backend/pkg/pulp_client" test_handler "github.com/content-services/content-sources-backend/pkg/test/handler" + "github.com/google/uuid" "github.com/labstack/echo/v4" echo_middleware "github.com/labstack/echo/v4/middleware" "github.com/redhatinsights/platform-go-middlewares/identity" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) type SnapshotSuite struct { suite.Suite - reg *dao.MockDaoRegistry + reg *dao.MockDaoRegistry + pulpClient *pulp_client.MockPulpClient } func TestSnapshotSuite(t *testing.T) { @@ -30,6 +34,7 @@ func TestSnapshotSuite(t *testing.T) { } func (suite *SnapshotSuite) SetupTest() { suite.reg = dao.GetMockDaoRegistry(suite.T()) + suite.pulpClient = pulp_client.NewMockPulpClient(suite.T()) } func (suite *SnapshotSuite) serveSnapshotsRouter(req *http.Request) (int, []byte, error) { @@ -59,6 +64,8 @@ func (suite *SnapshotSuite) TestSnapshotList() { paginationData := api.PaginationData{Limit: 10, Offset: DefaultOffset} collection := createSnapshotCollection(1, 10, 0) uuid := "abcadaba" + orgID := test_handler.MockOrgId + suite.reg.Snapshot.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), orgID).Return(nil).Once() suite.reg.Snapshot.On("List", uuid, paginationData, api.FilterData{}).Return(collection, int64(1), nil) path := fmt.Sprintf("%s/repositories/%s/snapshots/?limit=%d", fullRootPath(), uuid, 10) @@ -77,6 +84,32 @@ func (suite *SnapshotSuite) TestSnapshotList() { assert.Equal(t, 10, response.Meta.Limit) assert.Equal(t, 1, len(response.Data)) assert.Equal(t, collection.Data[0].RepositoryPath, response.Data[0].RepositoryPath) + assert.Equal(t, collection.Data[0].UUID, response.Data[0].UUID) + assert.Equal(t, collection.Data[0].URL, response.Data[0].URL) +} + +func (suite *SnapshotSuite) TestGetRepositoryConfigurationFile() { + t := suite.T() + + orgID := test_handler.MockOrgId + repoUUID := uuid.NewString() + snapUUID := uuid.NewString() + repoConfigFile := "file" + + suite.reg.Snapshot.On("GetRepositoryConfigurationFile", orgID, snapUUID, repoUUID).Return(repoConfigFile, nil).Once() + suite.reg.Snapshot.On("InitializePulpClient", mock.AnythingOfType("*context.valueCtx"), orgID).Return(nil).Once() + + path := fmt.Sprintf("%s/repositories/%s/snapshots/%s/config.repo", fullRootPath(), repoUUID, snapUUID) + req := httptest.NewRequest(http.MethodGet, path, nil) + req.Header.Set(api.IdentityHeader, test_handler.EncodedIdentity(t)) + + code, body, err := suite.serveSnapshotsRouter(req) + assert.Nil(t, err) + + response := string(body) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, code) + assert.Equal(t, response, repoConfigFile) } func createSnapshotCollection(size, limit, offset int) api.SnapshotCollectionResponse { @@ -84,6 +117,8 @@ func createSnapshotCollection(size, limit, offset int) api.SnapshotCollectionRes for i := 0; i < size; i++ { snap := api.SnapshotResponse{ RepositoryPath: "distribution/path/", + UUID: uuid.NewString(), + URL: "http://pulp-content/pulp/content", } snaps[i] = snap } diff --git a/pkg/models/snapshot_model.go b/pkg/models/snapshot.go similarity index 100% rename from pkg/models/snapshot_model.go rename to pkg/models/snapshot.go diff --git a/pkg/models/snapshot_model_test.go b/pkg/models/snapshot_test.go similarity index 100% rename from pkg/models/snapshot_model_test.go rename to pkg/models/snapshot_test.go diff --git a/pkg/pulp_client/client.go b/pkg/pulp_client/client.go index 7cb28d53d..b372abb7d 100644 --- a/pkg/pulp_client/client.go +++ b/pkg/pulp_client/client.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "github.com/content-services/content-sources-backend/pkg/cache" "github.com/content-services/content-sources-backend/pkg/config" zest "github.com/content-services/zest/release/v2023" ) @@ -13,6 +14,7 @@ type pulpDaoImpl struct { client *zest.APIClient ctx context.Context domainName string + cache cache.Cache } func GetGlobalPulpClient(ctx context.Context) PulpGlobalClient { @@ -47,6 +49,7 @@ func getPulpImpl(ctx context.Context) pulpDaoImpl { impl := pulpDaoImpl{ client: client, ctx: auth, + cache: cache.Initialize(), } return impl } diff --git a/pkg/pulp_client/interfaces.go b/pkg/pulp_client/interfaces.go index f03fe53d7..796e8a161 100644 --- a/pkg/pulp_client/interfaces.go +++ b/pkg/pulp_client/interfaces.go @@ -1,6 +1,8 @@ package pulp_client -import zest "github.com/content-services/zest/release/v2023" +import ( + zest "github.com/content-services/zest/release/v2023" +) //go:generate mockery --name PulpGlobalClient --filename pulp_global_client_mock.go --inpackage type PulpGlobalClient interface { @@ -13,6 +15,7 @@ type PulpGlobalClient interface { GetTask(taskHref string) (zest.TaskResponse, error) PollTask(taskHref string) (*zest.TaskResponse, error) CancelTask(taskHref string) (zest.TaskResponse, error) + GetContentPath() (string, error) } //go:generate mockery --name PulpClient --filename pulp_client_mock.go --inpackage @@ -28,6 +31,7 @@ type PulpClient interface { GetTask(taskHref string) (zest.TaskResponse, error) PollTask(taskHref string) (*zest.TaskResponse, error) CancelTask(taskHref string) (zest.TaskResponse, error) + GetContentPath() (string, error) // Rpm Repository CreateRpmRepository(uuid string, rpmRemotePulpRef *string) (*zest.RpmRpmRepositoryResponse, error) diff --git a/pkg/pulp_client/pulp_client_mock.go b/pkg/pulp_client/pulp_client_mock.go index d90dcd0c0..c81f27b1a 100644 --- a/pkg/pulp_client/pulp_client_mock.go +++ b/pkg/pulp_client/pulp_client_mock.go @@ -164,20 +164,6 @@ func (_m *MockPulpClient) DeleteRpmDistribution(rpmDistributionHref string) (str return r0, r1 } -// DeleteRpmPublication provides a mock function with given fields: versionHref -func (_m *MockPulpClient) DeleteRpmPublication(versionHref string) error { - ret := _m.Called(versionHref) - - var r0 error - if rf, ok := ret.Get(0).(func(string) error); ok { - r0 = rf(versionHref) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // DeleteRpmRemote provides a mock function with given fields: pulpHref func (_m *MockPulpClient) DeleteRpmRemote(pulpHref string) (string, error) { ret := _m.Called(pulpHref) @@ -302,6 +288,30 @@ func (_m *MockPulpClient) FindRpmPublicationByVersion(versionHref string) (*zest return r0, r1 } +// GetContentPath provides a mock function with given fields: +func (_m *MockPulpClient) GetContentPath() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetRpmRemoteByName provides a mock function with given fields: name func (_m *MockPulpClient) GetRpmRemoteByName(name string) (*zest.RpmRpmRemoteResponse, error) { ret := _m.Called(name) diff --git a/pkg/pulp_client/pulp_global_client_mock.go b/pkg/pulp_client/pulp_global_client_mock.go index 1ae1f79f5..7076d3667 100644 --- a/pkg/pulp_client/pulp_global_client_mock.go +++ b/pkg/pulp_client/pulp_global_client_mock.go @@ -36,6 +36,30 @@ func (_m *MockPulpGlobalClient) CancelTask(taskHref string) (zest.TaskResponse, return r0, r1 } +// GetContentPath provides a mock function with given fields: +func (_m *MockPulpGlobalClient) GetContentPath() (string, error) { + ret := _m.Called() + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetTask provides a mock function with given fields: taskHref func (_m *MockPulpGlobalClient) GetTask(taskHref string) (zest.TaskResponse, error) { ret := _m.Called(taskHref) diff --git a/pkg/pulp_client/status.go b/pkg/pulp_client/status.go index bb819f8f6..107990d3d 100644 --- a/pkg/pulp_client/status.go +++ b/pkg/pulp_client/status.go @@ -1,6 +1,12 @@ package pulp_client -import zest "github.com/content-services/zest/release/v2023" +import ( + "errors" + + "github.com/content-services/content-sources-backend/pkg/cache" + zest "github.com/content-services/zest/release/v2023" + "github.com/rs/zerolog" +) func (r *pulpDaoImpl) Status() (*zest.StatusResponse, error) { // Change this back to StatusRead(r.ctx) on next zest update @@ -12,3 +18,33 @@ func (r *pulpDaoImpl) Status() (*zest.StatusResponse, error) { return status, nil } + +func (r *pulpDaoImpl) GetContentPath() (string, error) { + logger := zerolog.Ctx(r.ctx) + + pulpContentPath, err := r.cache.GetPulpContentPath(r.ctx) + if err != nil && !errors.Is(err, cache.NotFound) { + logger.Error().Err(err).Msg("GetContentPath: error reading from cache") + } + + cacheHit := err == nil + if cacheHit { + return pulpContentPath, nil + } + + resp, err := r.Status() + if err != nil { + return "", err + } + + contentOrigin := resp.ContentSettings.ContentOrigin + contentPathPrefix := resp.ContentSettings.ContentPathPrefix + pulpContentPath = contentOrigin + contentPathPrefix + + err = r.cache.SetPulpContentPath(r.ctx, pulpContentPath) + if err != nil { + logger.Error().Err(err).Msg("GetContentPath: error writing to cache") + } + + return contentOrigin + contentPathPrefix, nil +} diff --git a/pkg/rbac/client_wrapper.go b/pkg/rbac/client_wrapper.go index 8e97c4e5f..3e1bea994 100644 --- a/pkg/rbac/client_wrapper.go +++ b/pkg/rbac/client_wrapper.go @@ -41,7 +41,7 @@ type ClientWrapper interface { type ClientWrapperImpl struct { client rbac.Client timeout time.Duration - cache cache.RbacCache + cache cache.Cache } func NewClientWrapperImpl(baseUrl string, timeout time.Duration) ClientWrapper { diff --git a/pkg/rbac/client_wrapper_test.go b/pkg/rbac/client_wrapper_test.go index b320cf3a3..83d8848b8 100644 --- a/pkg/rbac/client_wrapper_test.go +++ b/pkg/rbac/client_wrapper_test.go @@ -18,7 +18,7 @@ type RbacTestSuite struct { suite.Suite echo *echo.Echo rbac ClientWrapperImpl - mockCache *cache.MockRbacCache + mockCache *cache.MockCache } func (s *RbacTestSuite) SetupTest() { @@ -34,7 +34,7 @@ func (s *RbacTestSuite) SetupTest() { err := s.echo.Start(":9932") assert.True(s.T(), err == http.ErrServerClosed, "Unexpected error %v", err) }() - s.mockCache = cache.NewMockRbacCache(s.T()) + s.mockCache = cache.NewMockCache(s.T()) // Configure the client to use the mock rbac service // manually create an ClientWrapperImpl so we can pass in our mock cache s.rbac = ClientWrapperImpl{ diff --git a/test/integration/snapshot_test.go b/test/integration/snapshot_test.go index d0d73d362..10f8132cb 100644 --- a/test/integration/snapshot_test.go +++ b/test/integration/snapshot_test.go @@ -76,6 +76,12 @@ func (s *SnapshotSuite) TestSnapshot() { repoUuid, err := uuid2.Parse(repo.RepositoryUUID) assert.NoError(s.T(), err) + err = s.dao.Snapshot.InitializePulpClient(context.Background(), accountId) + assert.NoError(s.T(), err) + + err = s.dao.RepositoryConfig.InitializePulpClient(context.Background(), accountId) + assert.NoError(s.T(), err) + // Start the task taskClient := client.NewTaskClient(&s.queue) s.snapshotAndWait(taskClient, repo, repoUuid, accountId)